mirror of
https://github.com/retspen/webvirtcloud
synced 2025-07-31 12:41:08 +00:00
Compare commits
1177 commits
Author | SHA1 | Date | |
---|---|---|---|
|
af774ebcc8 | ||
|
326579a6ea | ||
|
c0f31909bd | ||
|
9324b9c36d | ||
|
b909a81f51 | ||
|
79450eac6a | ||
|
a56ce2a58f | ||
|
4646597b39 | ||
|
659a6edb73 | ||
|
099769d305 | ||
|
120f406b48 | ||
|
fd54c68e6b | ||
|
34f08fc555 | ||
|
8bca7db917 | ||
|
f26fa3d050 | ||
|
8c4f1d9fe8 | ||
|
fb108f9ac9 | ||
|
1a800b4535 | ||
|
b892af989b | ||
|
b55552f05a | ||
|
3d53f45f27 | ||
|
24f751774b | ||
|
f725d8ec18 | ||
|
9e4dc3256e | ||
|
218ec5dd78 | ||
|
cb7b690a10 | ||
|
f4651d816d | ||
|
1edce931ed | ||
|
b76b6e382e | ||
|
072a922c15 | ||
|
ee2c55dac7 | ||
|
6d97557cd9 | ||
|
950e668631 | ||
|
46029d533c | ||
|
45be006694 | ||
|
fc60819ff1 | ||
|
3f23acbb9d | ||
|
765b759953 | ||
|
d8fe9c2f64 | ||
|
a048adef8e | ||
|
d54d50dc99 | ||
|
241f7462f7 | ||
|
7da5e4cbbe | ||
|
9ee1571f5b | ||
|
f86eb78cbf | ||
|
6144760456 | ||
|
81b96b6512 | ||
|
1f90a6c8c9 | ||
|
bfe7da1ccc | ||
|
9a05c182cc | ||
|
f0cd0bc04f | ||
|
bbe9e54395 | ||
|
462e5d8403 | ||
|
c58d6f484d | ||
|
e0d83a397b | ||
|
d261f4d2c9 | ||
|
296081d1e3 | ||
|
e324a19401 | ||
|
6c0cc3c274 | ||
|
bcf91ad7b7 | ||
|
2f24f77368 | ||
|
7e8c05fff9 | ||
|
2ff0abf8c5 | ||
|
7171a70f75 | ||
|
8995f47c13 | ||
|
f912dfccdb | ||
|
911bd54916 | ||
|
6974eae62e | ||
|
cc86730441 | ||
|
5c6bcbe610 | ||
|
2072890fc5 | ||
|
4aafad668b | ||
|
9a675918c6 | ||
|
91aaa93c2b | ||
|
31cace9994 | ||
|
149044a90c | ||
|
5a211c0c56 | ||
|
58e215f755 | ||
|
9755f307a6 | ||
|
f5e896813c | ||
|
e47d909e41 | ||
|
ce8432ce9e | ||
|
64fdb9315d | ||
|
84e22e4a8c | ||
|
3ef0fe19f8 | ||
|
cea2309c64 | ||
|
35afff26f4 | ||
|
53d1407483 | ||
|
43fc419490 | ||
|
1b2b3a3bce | ||
|
ca54ae0c65 | ||
|
561fedfccd | ||
|
74ee2c073a | ||
|
01a4225a84 | ||
|
4a4a998d4c | ||
|
635101a1b6 | ||
|
5e368d1ee0 | ||
|
cbd1f6f8b0 | ||
|
cedb7beedd | ||
|
3facb9e445 | ||
|
b58c4ef781 | ||
|
77c4f8981b | ||
|
5d3a11e7c9 | ||
|
cf6f74f6c3 | ||
|
56c92d90b5 | ||
|
2935c9d382 | ||
|
a02df250bf | ||
|
fc2063eccb | ||
|
f2222a6ea8 | ||
|
7d1cfb69b0 | ||
|
b47ec2e8f9 | ||
|
7e88db9d2b | ||
|
0926a3c3aa | ||
|
1e8d94731b | ||
|
1cbdf76df6 | ||
|
82de7f4262 | ||
|
07d7a6d752 | ||
|
91ec098aa0 | ||
|
4d5b346e44 | ||
|
54954137c2 | ||
|
8bf28a4f33 | ||
|
c0513f7c6b | ||
|
6e76eb2cd9 | ||
|
c099cd06ef | ||
|
beb13cdea1 | ||
|
86b03abd59 | ||
|
9671f7b3e0 | ||
|
2941840f0d | ||
|
5880b91c7a | ||
|
3cd4212cdd | ||
|
96ea999926 | ||
|
3403d21fea | ||
|
907d47ab78 | ||
|
edd265d5a5 | ||
|
0754d07540 | ||
|
da9fbeaff4 | ||
|
407605761f | ||
|
58af536dbf | ||
|
012bc97a59 | ||
|
71f05089f6 | ||
|
f4aa925e2a | ||
|
06994b0423 | ||
|
33d49a68a7 | ||
|
68b0494350 | ||
|
fd6b2ec4bf | ||
|
ad9f1db643 | ||
|
5a78713d1a | ||
|
bea6b1454b | ||
|
8160819b39 | ||
|
75ee073ed4 | ||
|
9f8aaee15a | ||
|
4d1e9cedc2 | ||
|
33002d5a49 | ||
|
c26e7f5e6f | ||
|
6201a2fe9f | ||
|
711870b5a1 | ||
|
2fdcc86b4e | ||
|
561b5ead43 | ||
|
8e1c3fa03b | ||
|
6d9025ea20 | ||
|
63216d6283 | ||
|
12d3530641 | ||
|
e3fb820b50 | ||
|
1348679997 | ||
|
31b029847f | ||
|
bb9fe83836 | ||
|
ce9498d06d | ||
|
2aa0d17db5 | ||
|
fe550317d2 | ||
|
1be57f987e | ||
|
ad7781b545 | ||
|
fea1571e1a | ||
|
7db75784c6 | ||
|
012b3041e8 | ||
|
fe7452fcc6 | ||
|
e2a15d926c | ||
|
a1eab70e2d | ||
|
b43d7b0a8c | ||
|
fcd4b79431 | ||
|
af38e90f41 | ||
|
217e106c8b | ||
|
ea409ca863 | ||
|
51b12a8dec | ||
|
773709ce57 | ||
|
7b33f511cf | ||
|
c9f1a7d7c5 | ||
|
2dc4048053 | ||
|
2ff188da45 | ||
|
48a2ab4997 | ||
|
481c110f01 | ||
|
5f5bd379af | ||
|
4569dd03f9 | ||
|
ea6524f79e | ||
|
c804b2cde8 | ||
|
4abf62702a | ||
|
97acbd9843 | ||
|
31c575e852 | ||
|
5425519408 | ||
|
6eb06280c5 | ||
|
3b43081b9d | ||
|
66a9edfae1 | ||
|
afffaa8589 | ||
|
aeee56fe45 | ||
|
46514d0897 | ||
|
234638a85c | ||
|
f500d44492 | ||
|
2910a3229a | ||
|
6d3550bb24 | ||
|
2db7a4fa27 | ||
|
33a7e30fc5 | ||
|
aebcbb63fd | ||
|
a67a51eaed | ||
|
1c84f241ad | ||
|
cfce71ec2b | ||
|
76e3a8b823 | ||
|
4d78917c35 | ||
|
aaea1d9451 | ||
|
92254401dc | ||
|
d23896ddf6 | ||
|
0680f02240 | ||
|
d24d6b037d | ||
|
6f7d62a860 | ||
|
92a20c0aaa | ||
|
9bf3b7e88f | ||
|
65dce10ce1 | ||
|
f6915ac51f | ||
|
8699575e6f | ||
|
89ca8010f4 | ||
|
22ac6f1c60 | ||
|
a272740be6 | ||
|
e2a1bc5f7e | ||
|
421f17d5ca | ||
|
af5acc8c24 | ||
|
d0e3c11ccf | ||
|
bb31c09d25 | ||
|
7bb5d6f313 | ||
|
f620b88f73 | ||
|
f3d1c17a23 | ||
|
a4baeae934 | ||
|
f60bb055a8 | ||
|
2c07df4c8f | ||
|
052610dd21 | ||
|
e26a114c44 | ||
|
02b02d3321 | ||
|
32eb91f53f | ||
|
de2dce7573 | ||
|
c817d3e61a | ||
|
12f0c70069 | ||
|
f1a9444f24 | ||
|
995b333038 | ||
|
9ddba262a4 | ||
|
448c66110b | ||
|
2877f7b120 | ||
|
5016458ff7 | ||
|
73201ebc8f | ||
|
2154f55e8b | ||
|
df13d762a9 | ||
|
5b685a100e | ||
|
3ddfa28189 | ||
|
7858825258 | ||
|
9554ffad49 | ||
|
acca92ecb4 | ||
|
ba06af9775 | ||
|
ac8db03102 | ||
|
9977c650db | ||
|
3d4c0aa5ef | ||
|
79aeb3e23f | ||
|
e0767f4145 | ||
|
9e832c2612 | ||
|
13f332c9a5 | ||
|
e6f7588593 | ||
|
6c3d716b5a | ||
|
53240548a2 | ||
|
9ddcd0e0f3 | ||
|
62d4cf88ba | ||
|
973cf9ab97 | ||
|
2547e74400 | ||
|
a33283e172 | ||
|
c77e049e82 | ||
|
e0e6e2b8aa | ||
|
0b16f85658 | ||
|
0234cf0e1a | ||
|
8b858f8964 | ||
|
86723a8271 | ||
|
5c9a5daedc | ||
|
f690d1fa60 | ||
|
b8971a1fca | ||
|
c354393685 | ||
|
83709eddbf | ||
|
61d822efaa | ||
|
877995d6d6 | ||
|
4d21a3184b | ||
|
c1488eee52 | ||
|
185444009b | ||
|
607e396352 | ||
|
890f592148 | ||
|
536caacc80 | ||
|
8a81a83f5e | ||
|
2f2bd9622d | ||
|
2e3480677c | ||
|
5c9b9ce15f | ||
|
950347c5d3 | ||
|
b08df75c53 | ||
|
8db77f6136 | ||
|
45b727ae84 | ||
|
073b7b6717 | ||
|
375479cf2f | ||
|
1663a49cee | ||
|
ede5710d7c | ||
|
f161d6a1f3 | ||
|
f3ac40277a | ||
|
18971f01a6 | ||
|
990b69ac51 | ||
|
5aa22ac042 | ||
|
d7c0f9445d | ||
|
a47d006771 | ||
|
d9c23291de | ||
|
646eae59a2 | ||
|
7c1eb5c854 | ||
|
768b49ea88 | ||
|
cb3ed61872 | ||
|
5328c4c2ba | ||
|
2c41a0ce76 | ||
|
d113e34796 | ||
|
e9b57bfcf7 | ||
|
24cd3f70dd | ||
|
610efd5520 | ||
|
a20fa8e8d7 | ||
|
70bc5a4408 | ||
|
6980c92970 | ||
|
c940e6fa5a | ||
|
8fd4df554d | ||
|
83159f3365 | ||
|
1bb25be234 | ||
|
b01d86b038 | ||
|
e3398bd6ff | ||
|
9065922eb8 | ||
|
bc6631a330 | ||
|
483e5c9a68 | ||
|
a5c1d8c308 | ||
|
05e4c72df0 | ||
|
90c3cc08fe | ||
|
5d14252957 | ||
|
8bf2a50047 | ||
|
1e6ea063ab | ||
|
71fc29daf3 | ||
|
1d8a63a329 | ||
|
a5b97ed994 | ||
|
49dd51d40c | ||
|
b2068592f5 | ||
|
3caf1d538c | ||
|
fa4a3149c9 | ||
|
2f1b11b3ca | ||
|
6571ab372f | ||
|
59af676f68 | ||
|
47331ba08f | ||
|
2c915e04e0 | ||
|
77d8ad95d8 | ||
|
596d55d481 | ||
|
e68a9bd47a | ||
|
0a8beacf29 | ||
|
2b33847e8a | ||
|
40af106ecc | ||
|
8b840153e5 | ||
|
fe15742519 | ||
|
66b1d7b8a6 | ||
|
d30e6470e2 | ||
|
0e858a7eb6 | ||
|
993a247228 | ||
|
bd44b03bb0 | ||
|
82ded115b9 | ||
|
3b74571580 | ||
|
88c261e278 | ||
|
dbf3fc0db3 | ||
|
cf01625945 | ||
|
60b5de924f | ||
|
1b3e7594c0 | ||
|
e2e62a3ad4 | ||
|
12fa9e7cf1 | ||
|
4b7554ea14 | ||
|
9aed7c7716 | ||
|
c83e26c797 | ||
|
917d14c74d | ||
|
508e3609be | ||
|
159e06221a | ||
|
c20c353a40 | ||
|
488b71a68f | ||
|
85d4c5f132 | ||
|
5a3e0ad6f2 | ||
|
f759dc6564 | ||
|
83c8eccde3 | ||
|
1f26bd0c31 | ||
|
5a19f0c949 | ||
|
1f642a4381 | ||
|
7d22fec124 | ||
|
38c6ef12df | ||
|
b4430eddcc | ||
|
25d5df47da | ||
|
9402ae2abb | ||
|
737e0faef7 | ||
|
01043ba8a8 | ||
|
4f959eaee8 | ||
|
d04267eaa4 | ||
|
0f66187e80 | ||
|
9e56378682 | ||
|
1b3fd9d05e | ||
|
5172a9f619 | ||
|
8afef36656 | ||
|
106985bc7e | ||
|
116b39018a | ||
|
a20a2fe955 | ||
|
28a58011b8 | ||
|
0052323190 | ||
|
a942f55854 | ||
|
d19ec05638 | ||
|
cbac82ba07 | ||
|
f39e174db4 | ||
|
922a8e62d1 | ||
|
119d874e85 | ||
|
ba8fa20737 | ||
|
32343c1ddc | ||
|
aee7fa34b8 | ||
|
3c72bedd7a | ||
|
2fe1f1c73a | ||
|
45e3b00180 | ||
|
82f5fbf159 | ||
|
a1fec1ebb5 | ||
|
372ba5a0f2 | ||
|
7dd9127572 | ||
|
c2bd8d8547 | ||
|
a13247080e | ||
|
83ba6da572 | ||
|
6d52587e60 | ||
|
5850d14722 | ||
|
43f1461e29 | ||
|
8e4073f4b7 | ||
|
bb935b3713 | ||
|
6a8d713ae6 | ||
|
015719b952 | ||
|
46884304b0 | ||
|
de63d9746d | ||
|
fb3ef6be98 | ||
|
6ab32e1697 | ||
|
6bbebdd37c | ||
|
02654f6eda | ||
|
2ef6563d09 | ||
|
400dd6c86e | ||
|
2d6aaaf707 | ||
|
84a545dc61 | ||
|
74028f35b8 | ||
|
845c78c564 | ||
|
618d5ec7c1 | ||
|
abfbc54594 | ||
|
30948ca445 | ||
|
0e0521c61f | ||
|
b5b43b71bd | ||
|
523f58b55e | ||
|
519234b521 | ||
|
44aa746f4b | ||
|
74a4a1a3ef | ||
|
522b95fe39 | ||
|
3d3069d94d | ||
|
0b86e34203 | ||
|
b6cb81c3bc | ||
|
47009d47ca | ||
|
f23e6b000f | ||
|
61060c0b06 | ||
|
c0f707854a | ||
|
6c79c2bace | ||
|
ed7cffd073 | ||
|
1320753d6c | ||
|
1430c73c13 | ||
|
ce123d1f22 | ||
|
4f8a1fd50d | ||
|
9a0b456e34 | ||
|
5a73f14889 | ||
|
48f9ba6d73 | ||
|
720b5cab84 | ||
|
ce7ebeb55c | ||
|
ecaf11e02a | ||
|
eda233ada7 | ||
|
bb8f95be14 | ||
|
c791f582af | ||
|
7eee811e65 | ||
|
636b5bb1bc | ||
|
1a3cc36ada | ||
|
d9785e397b | ||
|
b85e246aed | ||
|
2951d0b035 | ||
|
20d4aaac30 | ||
|
d3c59acb3f | ||
|
a4d28f2953 | ||
|
68f7376d15 | ||
|
354163fa1d | ||
|
e4e86ed576 | ||
|
9a75ceb0e5 | ||
|
cfd739778c | ||
|
ca3b88097e | ||
|
ad4b417695 | ||
|
e97d592e51 | ||
|
fd3212de90 | ||
|
b7fc8a5fea | ||
|
7bb6bd310b | ||
|
31c86a6a7b | ||
|
21fcb8cd64 | ||
|
f263d97377 | ||
|
a0665e9682 | ||
|
898de1ffbc | ||
|
c43c10dece | ||
|
3d95f6abae | ||
|
dff60e4be4 | ||
|
d834cbbb7b | ||
|
07be61a557 | ||
|
509c57d8ab | ||
|
6a0d04d300 | ||
|
e46bb99b3b | ||
|
e21f423bcf | ||
|
5e00f896d4 | ||
|
c6f0a05f48 | ||
|
028ce0c208 | ||
|
fc2662bd91 | ||
|
fe13e4dd66 | ||
|
91a6dcc9f6 | ||
|
24a578f61b | ||
|
7538ddab4f | ||
|
62f8ece0ef | ||
|
bf15de507a | ||
|
8bafaa8720 | ||
|
c9d9fdb9e8 | ||
|
2d5c701789 | ||
|
7409c197ed | ||
|
7c93ee1e2f | ||
|
ccd947a04c | ||
|
a93f8b3321 | ||
|
0461df7685 | ||
|
9ab776197c | ||
|
871083967f | ||
|
bc25e22ae4 | ||
|
34006ad223 | ||
|
0e5840498e | ||
|
e2b7b77da0 | ||
|
11fcb6c0f3 | ||
|
566a9d446a | ||
|
0c72ccd066 | ||
|
b48b112a40 | ||
|
d384c914a4 | ||
|
966da065b4 | ||
|
e72073fa79 | ||
|
e911db8124 | ||
|
22cb8f702a | ||
|
d283e0e1b3 | ||
|
37022df459 | ||
|
10b6f88dbb | ||
|
7103c52380 | ||
|
5ab22ba947 | ||
|
7b0016c0b2 | ||
|
83b1dde673 | ||
|
85929b5327 | ||
|
9718b4c215 | ||
|
e4e79d3d4b | ||
|
3d0493537f | ||
|
a62daad87b | ||
|
2a0d240038 | ||
|
27f62dff6c | ||
|
38befa4362 | ||
|
6d82c2820b | ||
|
618d88f1c4 | ||
|
d9fa43463b | ||
|
2a490cc820 | ||
|
52c856c504 | ||
|
f37d6601e1 | ||
|
6aa6ea0817 | ||
|
d3de4bbc30 | ||
|
f9b837841c | ||
|
9926edf208 | ||
|
d5156303ee | ||
|
4d40de1b55 | ||
|
fd9465c769 | ||
|
fc8612c604 | ||
|
450f633e79 | ||
|
4ff77e31c1 | ||
|
cf9530dabd | ||
|
ff311a609c | ||
|
1c9c717f67 | ||
|
a6f7618854 | ||
|
39b53dbfc7 | ||
|
b768dbf59d | ||
|
562fe5c3dc | ||
|
34394c2b5e | ||
|
d95b5a4017 | ||
|
57665c2ab9 | ||
|
9198615076 | ||
|
c79a635923 | ||
|
70032fc2e1 | ||
|
0c8a0523a8 | ||
|
d5fe941b2f | ||
|
59dcdacffc | ||
|
6cebd2f70e | ||
|
2a86384826 | ||
|
c37ae1dca7 | ||
|
2058395081 | ||
|
6f23ce4644 | ||
|
c62e6ba3bc | ||
|
8b9fe4e887 | ||
|
f2ba2b58b0 | ||
|
c05729158a | ||
|
b3538ee87b | ||
|
30ed71e8ae | ||
|
2b96089542 | ||
|
0ef876c7a7 | ||
|
9aee37ab60 | ||
|
3da734e804 | ||
|
38054d9882 | ||
|
e87d7463fe | ||
|
9d58e56d16 | ||
|
ecf31b0b5b | ||
|
01f2290dd9 | ||
|
592112590c | ||
|
adfdad013c | ||
|
fa852566aa | ||
|
2fa5a98844 | ||
|
c6cdb4929c | ||
|
d401d2f3ff | ||
|
6634207ef5 | ||
|
1b0324e3e3 | ||
|
ab024acc78 | ||
|
ff96ce6648 | ||
|
28ea64cd16 | ||
|
a5e77ef01e | ||
|
8df71efdca | ||
|
dd16a5b2d5 | ||
|
28b001e7cb | ||
|
3a925af3c2 | ||
|
b91f1cc36b | ||
|
b27d27d532 | ||
|
3752ad5160 | ||
|
8ed1630686 | ||
|
49e179590e | ||
|
e4223dde5d | ||
|
44df080b72 | ||
|
941a97201e | ||
|
c2d69d0f9b | ||
|
ba91c70d83 | ||
|
b816bb28e3 | ||
|
34df35c5d7 | ||
|
b297e10115 | ||
|
3bae40c8aa | ||
|
68107aee58 | ||
|
b83528658e | ||
|
f42a910d9b | ||
|
74eb8004d7 | ||
|
60684cc7b1 | ||
|
228a0949d1 | ||
|
d7f283f089 | ||
|
5282d3e556 | ||
|
ddd3dd5f65 | ||
|
718388ffef | ||
|
33407719b6 | ||
|
579ba922e2 | ||
|
3a4da5eb11 | ||
|
69bc58d94f | ||
|
f93fed9437 | ||
|
d7b350a591 | ||
|
79abcd460b | ||
|
488aa23d14 | ||
|
d05a948078 | ||
|
08e694db4d | ||
|
0a55804251 | ||
|
930cef24be | ||
|
0974193e68 | ||
|
8bc316355d | ||
|
78ec7ac746 | ||
|
23e496cf0c | ||
|
c5a96b7662 | ||
|
0738ec7ec4 | ||
|
f3f4f0afe8 | ||
|
568ff92449 | ||
|
38ae62d093 | ||
|
a66ebba12b | ||
|
c212a60bba | ||
|
537cefe90a | ||
|
f1dbc6a199 | ||
|
360f212583 | ||
|
54a6408107 | ||
|
a59dcde688 | ||
|
f72d969c25 | ||
|
d46306bc8a | ||
|
97e39fa6b8 | ||
|
fb26a002c2 | ||
|
a0324ac60e | ||
|
1a8eb7c2c3 | ||
|
7ffbedb44a | ||
|
509ef79edf | ||
|
1e2fbc8453 | ||
|
0fdc2dae33 | ||
|
caf00bd4f9 | ||
|
d22aed5ef7 | ||
|
828a271789 | ||
|
fdbb6739c1 | ||
|
1e22547b1a | ||
|
e47f97facf | ||
|
b18290794e | ||
|
f3b8b251b5 | ||
|
094f054063 | ||
|
15c42ee0f6 | ||
|
7404a1212a | ||
|
1ded650463 | ||
|
baf1e92774 | ||
|
6a47c48f68 | ||
|
a3e7a5472f | ||
|
edd4887a2d | ||
|
ac3ec24332 | ||
|
0ac126ee0d | ||
|
e601c23b4a | ||
|
61703b3faf | ||
|
1db6bb4cce | ||
|
0549323aca | ||
|
0056541c07 | ||
|
e05fa2354b | ||
|
181bd9d392 | ||
|
9ab198bd8c | ||
|
455f239093 | ||
|
a0147818b9 | ||
|
98edc599f2 | ||
|
3879a1bded | ||
|
7986784375 | ||
|
44fcb4b748 | ||
|
cb81505b05 | ||
|
30e1f0eb2d | ||
|
3a15b49796 | ||
|
6bc4e895b6 | ||
|
52fbe95e30 | ||
|
311f1b793e | ||
|
b3928d5026 | ||
|
3cffa6e505 | ||
|
3ad520209f | ||
|
a457c262e1 | ||
|
ad373cff7b | ||
|
1bd2c558f2 | ||
|
6b571ffb22 | ||
|
c5776fc13b | ||
|
fffbc92151 | ||
|
5d1df70d41 | ||
|
e63aaa926e | ||
|
dbe38f31a6 | ||
|
a9a2e1167b | ||
|
378066767e | ||
|
4edbf24703 | ||
|
ca6b3bbca1 | ||
|
7ece722fb1 | ||
|
03265d52dc | ||
|
3f32ae1adc | ||
|
f7d732302a | ||
|
1ade3a4042 | ||
|
2dc83f39a2 | ||
|
c483925ea4 | ||
|
ebf251ff5e | ||
|
03370ac705 | ||
|
f76ce4d485 | ||
|
4b6ab68e7f | ||
|
e28cc71710 | ||
|
a7e0e49007 | ||
|
aea7545030 | ||
|
3dc44662a0 | ||
|
8d90bc1372 | ||
|
d3cc19b4f8 | ||
|
5c3a9e2a81 | ||
|
1f005e90e1 | ||
|
cba5147cd1 | ||
|
ead0414a4d | ||
|
573df2acaa | ||
|
8d2b3e3024 | ||
|
b757c62d4f | ||
|
a3be93874c | ||
|
6d178a67d8 | ||
|
bd63e3e4e6 | ||
|
b812a05cdc | ||
|
be1acf8d77 | ||
|
d568680747 | ||
|
8da5527a2b | ||
|
276097a44e | ||
|
198c100524 | ||
|
96efde814a | ||
|
6b06ed25ff | ||
|
fc6761aabc | ||
|
1aea7baada | ||
|
4b9ae89324 | ||
|
867a10d0c5 | ||
|
4efc311dec | ||
|
fc3f04c244 | ||
|
a519980610 | ||
|
a63e9036fe | ||
|
b7639d2ba6 | ||
|
c98f0b1232 | ||
|
04d3568d1a | ||
|
62908f477c | ||
|
a67acc5d24 | ||
|
01019ddf3b | ||
|
64987b4ef4 | ||
|
c04596f078 | ||
|
91598357dc | ||
|
7672cc06f1 | ||
|
be75381bea | ||
|
eb8aae957e | ||
|
469cc0560c | ||
|
08451a06a4 | ||
|
84bf04a137 | ||
|
44d73cc780 | ||
|
45720d3bcf | ||
|
91f01d884b | ||
|
5b60dd7f84 | ||
|
65733197fa | ||
|
4eda0fb866 | ||
|
45c3f9b176 | ||
|
40ada89c4a | ||
|
605d24d699 | ||
|
5872700455 | ||
|
d3b43b2e6a | ||
|
8ecd453efe | ||
|
fab3b2891f | ||
|
69be623eeb | ||
|
f68d53f3e4 | ||
|
cd940c99e5 | ||
|
2ca2add444 | ||
|
07984a2a9a | ||
|
1196fb38c9 | ||
|
82c87f82bc | ||
|
1971029e3c | ||
|
1b913fd4d6 | ||
|
e80bbfd85b | ||
|
b3b9596a12 | ||
|
b5f38afbca | ||
|
9d07c2d7b6 | ||
|
2b04a89100 | ||
|
03ffa3a295 | ||
|
ea5e9cfead | ||
|
8f5cc5755a | ||
|
d63c0a8163 | ||
|
ca545878c4 | ||
|
f2d88b9c65 | ||
|
016e93ebca | ||
|
df4cf01f05 | ||
|
e1910c75ff | ||
|
d75df231a8 | ||
|
7d21c138b7 | ||
|
ea4f2cba8b | ||
|
93a8625aca | ||
|
358f9ae28a | ||
|
b81b01e468 | ||
|
bc321f1475 | ||
|
37b5093e13 | ||
|
25e6381fc9 | ||
|
e44e01cad4 | ||
|
b916c9dcf9 | ||
|
ba212971fa | ||
|
5d4a600908 | ||
|
f7d2d24d0b | ||
|
cdb5c4f412 | ||
|
a4bea0e765 | ||
|
812bbb2ccc | ||
|
4d14fdceea | ||
|
f45666d88b | ||
|
658309f022 | ||
|
5c2232f4e8 | ||
|
f474905719 | ||
|
a3a8814cc9 | ||
|
7f2104c19d | ||
|
35bc8c67e4 | ||
|
0d90e4bb46 | ||
|
1802ad0413 | ||
|
3f1acf09ef | ||
|
0a7c9f3826 | ||
|
be6c1e91f8 | ||
|
33f334289c | ||
|
1bf99b796f | ||
|
1f9041b42a | ||
|
53d7d1365d | ||
|
13dc4ff510 | ||
|
b58277c621 | ||
|
f477dd6a11 | ||
|
8e3dde808d | ||
|
717241a421 | ||
|
aba4ee8623 | ||
|
52ade2ee9d | ||
|
fb03f3c554 | ||
|
01c7991d80 | ||
|
76f857d218 | ||
|
7da5095220 | ||
|
7153a6196c | ||
|
105b8c180a | ||
|
6486c20168 | ||
|
3b4a1089c3 | ||
|
1e3c06b98b | ||
|
6a992b03c0 | ||
|
0f7a110535 | ||
|
cb5b0c1ecb | ||
|
8055616b1f | ||
|
33513ca648 | ||
|
1120b89c13 | ||
|
7efec407c2 | ||
|
6f8462d822 | ||
|
d0a964c516 | ||
|
bcd13c29fe | ||
|
14e145e0d8 | ||
|
e168d90bb0 | ||
|
d5d56a2b50 | ||
|
b4da655644 | ||
|
9c37dcc2dc | ||
|
e9e2185fb4 | ||
|
76e6388ec5 | ||
|
3f98aa2370 | ||
|
21ef6871cf | ||
|
01ace81c32 | ||
|
f73271e677 | ||
|
2a5a761f05 | ||
|
3748a46d8f | ||
|
0dd2eafc97 | ||
|
e7d649636c | ||
|
720937edcf | ||
|
edb59947af | ||
|
019d1523cd | ||
|
aa32d826d9 | ||
|
b05a252d7c | ||
|
12c80c5021 | ||
|
5bad045835 | ||
|
c80e142522 | ||
|
fcb2924e95 | ||
|
c231feb575 | ||
|
3b99c983d5 | ||
|
f300c1156f | ||
|
aba92e30b5 | ||
|
fc7ddacdca | ||
|
f9f216224f | ||
|
88f187c94f | ||
|
07ae335d79 | ||
|
eb621ef2c6 | ||
|
17c619606d | ||
|
aa236df40f | ||
|
6a57903fd6 | ||
|
dc9a5eb327 | ||
|
858d1f87d5 | ||
|
c355a4014f | ||
|
340d93463e | ||
|
951498e841 | ||
|
b909b9d0f1 | ||
|
13ffaae522 | ||
|
9f7ec62226 | ||
|
1700ddf8f1 | ||
|
c7b8d1ece0 | ||
|
171f98b232 | ||
|
4126ad2591 | ||
|
2585e64cfd | ||
|
948d657376 | ||
|
9176cb6204 | ||
|
b178bad93e | ||
|
8b2451284f | ||
|
8b347c1024 | ||
|
41b80bc719 | ||
|
332de3709b | ||
|
5adeead68d | ||
|
e387c3a21d | ||
|
e7ecf29359 | ||
|
b7150a1fae | ||
|
4769c5cf1b | ||
|
8e43523331 | ||
|
056a300b38 | ||
|
73ce98b63e | ||
|
7200a34699 | ||
|
7862fa8fdf | ||
|
65769eb894 | ||
|
5e2144b113 | ||
|
db67157907 | ||
|
654dcddf03 | ||
|
19b3d867f3 | ||
|
eb9160cadb | ||
|
cd072e488b | ||
|
d44c65a3f4 | ||
|
bee2e4222b | ||
|
bdfdd9238f | ||
|
30e2a66be7 | ||
|
c38b679c88 | ||
|
0778116a40 | ||
|
f4de792eff | ||
|
32b03b7184 | ||
|
904df30386 | ||
|
3628e5f355 | ||
|
1204c6d1b4 | ||
|
cfa9fd6066 | ||
|
85aac6fa0a | ||
|
360dab0892 | ||
|
6a7f30d4a6 | ||
|
20e8b876e4 | ||
|
a933ffb00f | ||
|
ace30ca952 | ||
|
19489cb08e | ||
|
5e8adec424 | ||
|
464e7bee39 | ||
|
27a7a7a365 | ||
|
4404d5941b | ||
|
beea57189c | ||
|
920739c4c5 | ||
|
16ff1fb52e | ||
|
22d03da60f | ||
|
6b444075b6 | ||
|
956b321928 | ||
|
5f24d5d222 | ||
|
82eb5abe52 | ||
|
078905df8a | ||
|
dd2830eddb | ||
|
b533d59e2f | ||
|
6587308dda | ||
|
aba3e5d714 | ||
|
0622e577f4 | ||
|
2450e9caff | ||
|
564bb45c87 | ||
|
43a8fb6dc1 | ||
|
8d46bd40fd | ||
|
4013b95d14 | ||
|
0ccc366fba | ||
|
5bba739e6b | ||
|
6e5e10594c | ||
|
c58374ea2e | ||
|
40049540ee | ||
|
41095ffb6d | ||
|
1ad2f03b52 | ||
|
748a79d9db | ||
|
6cbad06f2c | ||
|
15d7216368 | ||
|
9dc9fea2a1 | ||
|
6574532c16 | ||
|
b0e489abe7 | ||
|
62467854db | ||
|
839b8d420d | ||
|
fdca3cef9c | ||
|
6d153a6acf | ||
|
96674221b4 | ||
|
ede9bb6b10 | ||
|
80c023f2f3 | ||
|
b7cd731fda | ||
|
2ef672ffb5 | ||
|
d4158a33ee | ||
|
ad3b4d817b | ||
|
53f5518706 | ||
|
b095a77da5 | ||
|
f2e1273f85 | ||
|
fdf383a647 | ||
|
fd87cfd86e | ||
|
437be0df99 | ||
|
3e709cd253 | ||
|
c5f86358be | ||
|
d715e996b0 | ||
|
f959dc7016 | ||
|
4d08f956fb | ||
|
b7457a4362 | ||
|
6858448b60 | ||
|
14b0751930 | ||
|
fe89af9d8f | ||
|
d94ca38e5c | ||
|
609651d707 | ||
|
531ea1e652 | ||
|
b5f9f638f1 | ||
|
20d0e5a09d | ||
|
48371ff92d | ||
|
aa2a996e3f | ||
|
89f49b120b | ||
|
a3a572a8a8 | ||
|
ed4cb864ad | ||
|
525d42a74d | ||
|
29b722ff41 | ||
|
01fc85e1fc | ||
|
9e294ade3c | ||
|
346b96b319 | ||
|
08cc19900c | ||
|
14e8418472 | ||
|
3666ff0738 | ||
|
f484598414 | ||
|
0b80b030fe | ||
|
7efbfec17f | ||
|
7b3fcd17ea | ||
|
4ce76f57c6 | ||
|
e75fc99449 | ||
|
572f7b12cd | ||
|
9327cf36c4 | ||
|
c4ae3d2982 | ||
|
99bd6930b9 | ||
|
1425a7ae1f | ||
|
e966e6c030 | ||
|
e45c712d67 | ||
|
380cc2d09b | ||
|
fc71884cd4 | ||
|
8de4c6b131 | ||
|
25529835b5 | ||
|
db1ab88f51 | ||
|
0e7c5c25b7 | ||
|
17cb7ace88 | ||
|
05a7944c0e | ||
|
958e769258 | ||
|
69b955261f | ||
|
9520282a4b | ||
|
e1d3be17f1 | ||
|
ee2b97d62f | ||
|
9268cd3a61 | ||
|
f8e681dbf4 | ||
|
2f63d35804 | ||
|
9b5d745fb9 | ||
|
90fc5db411 | ||
|
c5c8d29dff | ||
|
6546fa2570 | ||
|
e9e62e3c41 | ||
|
16ef164ed9 | ||
|
1e84dcbbc9 | ||
|
bc0552e12e | ||
|
8148620025 | ||
|
27030113fb | ||
|
738993d97a | ||
|
7cdac17b53 | ||
|
c51e986b8c | ||
|
027bbbc776 | ||
|
6afcd00e2e | ||
|
317c2a85ae | ||
|
de5cb19913 | ||
|
78dcb10a57 | ||
|
c2b3938f2a | ||
|
9832e662a8 | ||
|
b6350e134e | ||
|
fc56e66555 | ||
|
ff4c05f90b | ||
|
84fe28e9d7 | ||
|
2aa81ccecb | ||
|
6c4a3a93e3 | ||
|
679f84f117 | ||
|
e1b4fdf5de | ||
|
2ceb456a4f | ||
|
510e0e6ee5 | ||
|
16510dee59 | ||
|
2958a21ad1 | ||
|
a1d5edebe2 | ||
|
96982ce58b | ||
|
71c6161291 | ||
|
39f3c9e12b | ||
|
33916c6a82 | ||
|
323e0a10d5 | ||
|
a34c55d3bc | ||
|
646bdbbe0e | ||
|
4ab8561360 | ||
|
d4fa71ff25 | ||
|
8deb844c35 | ||
|
d036d582fd | ||
|
ca328a7527 | ||
|
6151792d9b | ||
|
50ddda98f2 | ||
|
1499af1eef | ||
|
0eb60b1aa4 | ||
|
dac974ddab | ||
|
ae4fdcec92 | ||
|
f8c08cb719 | ||
|
decd5ab4a6 | ||
|
6dbd6e558c | ||
|
35369edf28 | ||
|
e6f0116908 | ||
|
cd787c3eaf | ||
|
04f3a76c05 | ||
|
8f2f95e128 | ||
|
164c9a9145 | ||
|
eac356b50a | ||
|
80a27a4a6a | ||
|
b12930c070 | ||
|
7d3ef56c08 | ||
|
f4f058fdcc | ||
|
bb5e5815c0 | ||
|
e2ead89fd1 | ||
|
2a3a33d746 | ||
|
1e6d177bba | ||
|
83ac06782a | ||
|
6f0c3dc5ac | ||
|
225db15d9e | ||
|
bccfbb0010 |
1078 changed files with 329648 additions and 46969 deletions
25
.dockerignore
Normal file
25
.dockerignore
Normal file
|
@ -0,0 +1,25 @@
|
|||
**/__pycache__
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
README.md
|
61
.github/workflows/codeql-analysis.yml
vendored
Normal file
61
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches-ignore: [master]
|
||||
schedule:
|
||||
- cron: '0 21 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['javascript', 'python']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
89
.github/workflows/linter.yml
vendored
Normal file
89
.github/workflows/linter.yml
vendored
Normal file
|
@ -0,0 +1,89 @@
|
|||
###########################
|
||||
###########################
|
||||
## Linter GitHub Actions ##
|
||||
###########################
|
||||
###########################
|
||||
|
||||
name: Lint Code Base
|
||||
|
||||
#
|
||||
# Documentation:
|
||||
# https://help.github.com/en/articles/workflow-syntax-for-github-actions
|
||||
#
|
||||
|
||||
#############################
|
||||
# Start the job on all push #
|
||||
#############################
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches-ignore: [master]
|
||||
|
||||
|
||||
###############
|
||||
# Set the Job #
|
||||
###############
|
||||
jobs:
|
||||
build:
|
||||
name: Lint Code Base
|
||||
# Set the agent to run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
##################
|
||||
# Load all steps #
|
||||
###################
|
||||
steps:
|
||||
##########################
|
||||
# Checkout the code base #
|
||||
##########################
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# Full git history is needed to get a proper list of changed files within `super-linter`
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install Required packages
|
||||
run: |
|
||||
sudo apt-get install -y python3-virtualenv libvirt-dev python3-lxml zlib1g-dev libxslt1-dev
|
||||
|
||||
- name: Create & Activate VENV
|
||||
run: |
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
pip3 install wheel
|
||||
if [ -f dev/requirements.txt ]; then pip3 install -r dev/requirements.txt; else pip3 install -r conf/requirements.txt; fi
|
||||
################################
|
||||
# Run Linter against code base #
|
||||
################################
|
||||
- name: Lint Code Base
|
||||
uses: docker://github/super-linter:latest
|
||||
env:
|
||||
FILTER_REGEX_EXCLUDE: .*(static|scss|venv|locale)/.*
|
||||
DEFAULT_BRANCH: master
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VALIDATE_ALL_CODEBASE: false
|
||||
VALIDATE_ANSIBLE: false
|
||||
VALIDATE_CLOJURE: false
|
||||
VALIDATE_COFFEE: false
|
||||
VALIDATE_DART: false
|
||||
VALIDATE_GO: false
|
||||
VALIDATE_JSX: false
|
||||
VALIDATE_KOTLIN: false
|
||||
VALIDATE_POWERSHELL: false
|
||||
VALIDATE_PERL: false
|
||||
VALIDATE_PHP: false
|
||||
VALIDATE_RAKU: false
|
||||
VALIDATE_RUBY: false
|
||||
VALIDATE_TSX: false
|
||||
VALIDATE_TERRAFORM: false
|
||||
|
15
.gitignore
vendored
15
.gitignore
vendored
|
@ -1,6 +1,19 @@
|
|||
.vagrant
|
||||
.venv
|
||||
venv
|
||||
venv2
|
||||
.vscode
|
||||
.idea
|
||||
.DS_*
|
||||
.webvirtcloud
|
||||
*.pyc
|
||||
db.sqlite3
|
||||
db.sqlite3*
|
||||
console/cert.pem*
|
||||
tags
|
||||
dhcpd.*
|
||||
webvirtcloud/settings.py
|
||||
*migrations/*
|
||||
.coverage
|
||||
htmlcov
|
||||
*.log
|
||||
templates/webvirtcloud.code-workspace
|
||||
|
|
5
.gitpod.yml
Normal file
5
.gitpod.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
image: gitpod/workspace-full
|
||||
|
||||
tasks:
|
||||
- init: 'echo "TODO: Replace with init/build command"'
|
||||
command: 'echo "TODO: Replace with command to start project"'
|
18
.travis.yml
18
.travis.yml
|
@ -1,15 +1,17 @@
|
|||
---
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.9"
|
||||
env:
|
||||
- DJANGO=1.8
|
||||
- DJANGO=4.2.4
|
||||
install:
|
||||
- pip install -r dev/requirements.txt --use-mirrors
|
||||
- pip install -r dev/requirements.txt
|
||||
script:
|
||||
- pep8 --exclude=IPy.py --ignore=E501 vrtManager accounts computes \
|
||||
console create instances interfaces \
|
||||
networks secrets storages
|
||||
- pyflakes vrtManager accounts computes console create instances interfaces \
|
||||
networks secrets storages
|
||||
- pep8 --exclude=IPy.py --ignore=E501 vrtManager accounts admin appsettings \
|
||||
computesconsole create datasource instances \
|
||||
interfaceslogs networks nwfilters storages \
|
||||
virtsecrets
|
||||
- pyflakes vrtManager accounts admin appsettings computes console create datasource \
|
||||
instances interfaces logs networks nwfilters storages virtsecrets
|
||||
- python manage.py migrate
|
||||
- python manage.py test --settings=webvirtcloud.settings-dev
|
||||
|
|
69
Dockerfile
Normal file
69
Dockerfile
Normal file
|
@ -0,0 +1,69 @@
|
|||
FROM phusion/baseimage:noble-1.0.2
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 6080
|
||||
|
||||
# Use baseimage-docker's init system.
|
||||
CMD ["/sbin/my_init"]
|
||||
|
||||
|
||||
RUN echo 'APT::Get::Clean=always;' >> /etc/apt/apt.conf.d/99AutomaticClean
|
||||
|
||||
RUN apt-get update -qqy \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get -qyy install \
|
||||
--no-install-recommends \
|
||||
git \
|
||||
python3-venv \
|
||||
python3-dev \
|
||||
python3-lxml \
|
||||
libvirt-dev \
|
||||
zlib1g-dev \
|
||||
nginx \
|
||||
pkg-config \
|
||||
gcc \
|
||||
libldap2-dev \
|
||||
libssl-dev \
|
||||
libsasl2-dev \
|
||||
libsasl2-modules \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY . /srv/webvirtcloud
|
||||
RUN chown -R www-data:www-data /srv/webvirtcloud
|
||||
|
||||
# Setup webvirtcloud
|
||||
WORKDIR /srv/webvirtcloud
|
||||
RUN python3 -m venv venv && \
|
||||
. venv/bin/activate && \
|
||||
pip3 install -U pip && \
|
||||
pip3 install wheel && \
|
||||
pip3 install -r conf/requirements.txt && \
|
||||
pip3 cache purge && \
|
||||
chown -R www-data:www-data /srv/webvirtcloud
|
||||
|
||||
RUN . venv/bin/activate && \
|
||||
python3 manage.py makemigrations && \
|
||||
python3 manage.py migrate && \
|
||||
python3 manage.py collectstatic --noinput && \
|
||||
chown -R www-data:www-data /srv/webvirtcloud
|
||||
|
||||
# Setup Nginx
|
||||
RUN printf "\n%s" "daemon off;" >> /etc/nginx/nginx.conf && \
|
||||
rm /etc/nginx/sites-enabled/default && \
|
||||
chown -R www-data:www-data /var/lib/nginx
|
||||
|
||||
COPY conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/
|
||||
|
||||
# Register services to runit
|
||||
RUN mkdir /etc/service/nginx && \
|
||||
mkdir /etc/service/nginx-log-forwarder && \
|
||||
mkdir /etc/service/webvirtcloud && \
|
||||
mkdir /etc/service/novnc
|
||||
COPY conf/runit/nginx /etc/service/nginx/run
|
||||
COPY conf/runit/nginx-log-forwarder /etc/service/nginx-log-forwarder/run
|
||||
COPY conf/runit/novncd.sh /etc/service/novnc/run
|
||||
COPY conf/runit/webvirtcloud.sh /etc/service/webvirtcloud/run
|
||||
|
||||
# Define mountable directories.
|
||||
#VOLUME []
|
||||
|
||||
WORKDIR /srv/webvirtcloud
|
344
README.md
344
README.md
|
@ -1,40 +1,78 @@
|
|||
## WebVirtCloud Beta
|
||||
[](https://gitpod.io/#https://github.com/retspen/webvirtcloud)
|
||||
|
||||
# WebVirtCloud
|
||||
###### Python >=3.11 & Django 4.2 LTS
|
||||
|
||||
## Features
|
||||
|
||||
* QEMU/KVM Hypervisor Management
|
||||
* QEMU/KVM Instance Management - Create, Delete, Update
|
||||
* Hypervisor & Instance web based stats
|
||||
* Manage Multiple QEMU/KVM Hypervisor
|
||||
* Manage Hypervisor Datastore pools
|
||||
* Manage Hypervisor Networks
|
||||
* Instance Console Access with Browsers
|
||||
* Libvirt API based web management UI
|
||||
* User Based Authorization and Authentication
|
||||
* User can add SSH public key to root in Instance (Tested only Ubuntu)
|
||||
* User can change root password in Instance (Tested only Ubuntu)
|
||||
* Supports cloud-init datasource interface
|
||||
|
||||
### Warning!!!
|
||||
|
||||
How to update <code>gstfsd</code> daemon on hypervisor:
|
||||
|
||||
```bash
|
||||
wget -O - https://clck.ru/9VMRH | sudo tee -a /usr/local/bin/gstfsd
|
||||
wget -O - https://bit.ly/2NAaWXG | sudo tee -a /usr/local/bin/gstfsd
|
||||
sudo service supervisor restart
|
||||
```
|
||||
|
||||
### Description
|
||||
## Description
|
||||
|
||||
WebVirtCloud is a virtualization web interface for admins and users. It can delegate Virtual Machine's to users. A noVNC viewer presents a full graphical console to the guest domain. KVM is currently the only hypervisor supported.
|
||||
|
||||
### Install WebVirtCloud panel (Ubuntu)
|
||||
## Quick Install with Installer (Beta)
|
||||
|
||||
Install an OS and run specified commands. Installer supported OSes: Ubuntu 20.04/22.04, Debian 10/11, Rocky/Alma/OEL/RHEL 10.
|
||||
It can be installed on a virtual machine, physical host or on a KVM host.
|
||||
|
||||
```bash
|
||||
sudo apt-get -y install git python-virtualenv python-dev libxml2-dev libvirt-dev zlib1g-dev nginx supervisor
|
||||
wget https://raw.githubusercontent.com/retspen/webvirtcloud/master/install.sh
|
||||
chmod 744 install.sh
|
||||
# run with sudo or root user
|
||||
./install.sh
|
||||
```
|
||||
|
||||
## Manual Installation
|
||||
|
||||
### Generate secret key
|
||||
|
||||
You should generate SECRET_KEY after cloning repository. Then put it into webvirtcloud/settings.py.
|
||||
|
||||
```python3
|
||||
import random, string
|
||||
haystack = string.ascii_letters + string.digits + string.punctuation
|
||||
print(''.join([random.SystemRandom().choice(haystack) for _ in range(50)]))
|
||||
```
|
||||
|
||||
### Install WebVirtCloud panel (Ubuntu 18.04+ LTS)
|
||||
|
||||
```bash
|
||||
sudo apt-get -y install git virtualenv python3-virtualenv python3-dev python3-lxml libvirt-dev zlib1g-dev libxslt1-dev nginx supervisor libsasl2-modules gcc pkg-config python3-guestfs libsasl2-dev libldap2-dev libssl-dev
|
||||
git clone https://github.com/retspen/webvirtcloud
|
||||
cd webvirtcloud
|
||||
cp webvirtcloud/settings.py.template webvirtcloud/settings.py
|
||||
# now put secret key to webvirtcloud/settings.py
|
||||
sudo cp conf/supervisor/webvirtcloud.conf /etc/supervisor/conf.d
|
||||
sudo cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d
|
||||
cd ..
|
||||
sudo mv webvirtcloud /srv
|
||||
sudo chown -R www-data:www-data /srv/webvirtcloud
|
||||
cd /srv/webvirtcloud
|
||||
virtualenv venv
|
||||
virtualenv -p python3 venv
|
||||
source venv/bin/activate
|
||||
pip install -r conf/requirements.txt
|
||||
python manage.py migrate
|
||||
python3 manage.py migrate
|
||||
python3 manage.py collectstatic --noinput
|
||||
sudo chown -R www-data:www-data /srv/webvirtcloud
|
||||
sudo rm /etc/nginx/sites-enabled/default
|
||||
```
|
||||
|
@ -49,33 +87,45 @@ sudo service supervisor restart
|
|||
Setup libvirt and KVM on server
|
||||
|
||||
```bash
|
||||
wget -O - https://clck.ru/9V9fH | sudo sh
|
||||
wget -O - https://bit.ly/36baWUu | sudo sh
|
||||
```
|
||||
|
||||
### Install WebVirtCloud panel (CentOS)
|
||||
Done!!
|
||||
|
||||
Go to http://serverip and you should see the login screen.
|
||||
|
||||
### Install WebVirtCloud panel (RHEL Based OS 8/9/10)
|
||||
|
||||
```bash
|
||||
sudo yum -y install python-virtualenv python-devel libvirt-devel glibc gcc nginx supervisor libxml2 libxml2-devel git
|
||||
sudo yum -y install epel-release
|
||||
sudo yum -y install python3-virtualenv python3-devel libvirt-devel glibc gcc nginx supervisor python3-lxml git python3-libguestfs iproute-tc cyrus-sasl-md5 python3-libguestfs libsasl2-dev libldap2-dev libssl-dev
|
||||
```
|
||||
|
||||
#### Creating directories and cloning repo
|
||||
#### Creating directories and cloning repository
|
||||
|
||||
```bash
|
||||
sudo mkdir /srv && cd /srv
|
||||
sudo git clone https://github.com/retspen/webvirtcloud && cd webvirtcloud
|
||||
cp webvirtcloud/settings.py.template webvirtcloud/settings.py
|
||||
# now put secret key to webvirtcloud/settings.py
|
||||
# create secret key manually or use that command
|
||||
sudo sed -r "s/SECRET_KEY = ''/SECRET_KEY = '"`python3 /srv/webvirtcloud/conf/runit/secret_generator.py`"'/" -i /srv/webvirtcloud/webvirtcloud/settings.py
|
||||
```
|
||||
|
||||
#### Start installation webvirtcloud
|
||||
```
|
||||
sudo virtualenv venv
|
||||
sudo source venv/bin/activate
|
||||
sudo pip install -r conf/requirements.txt
|
||||
sudo cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/
|
||||
sudo python manage.py migrate
|
||||
|
||||
```bash
|
||||
virtualenv-3 venv
|
||||
source venv/bin/activate
|
||||
pip3 install -r conf/requirements.txt
|
||||
cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/
|
||||
python3 manage.py migrate
|
||||
python3 manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
#### Configure the supervisor for CentOS
|
||||
Add the following after the [include] line (after **files = ... ** actually):
|
||||
#### Configure the supervisor for RHEL Based OS
|
||||
|
||||
Add the following after the [include] line (after **files = ...** actually):
|
||||
```bash
|
||||
sudo vim /etc/supervisord.conf
|
||||
|
||||
|
@ -88,7 +138,7 @@ autorestart=true
|
|||
redirect_stderr=true
|
||||
|
||||
[program:novncd]
|
||||
command=/srv/webvirtcloud/venv/bin/python /srv/webvirtcloud/console/novncd
|
||||
command=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/novncd
|
||||
directory=/srv/webvirtcloud
|
||||
user=nginx
|
||||
autostart=true
|
||||
|
@ -97,9 +147,10 @@ redirect_stderr=true
|
|||
```
|
||||
|
||||
#### Edit the nginx.conf file
|
||||
|
||||
You will need to edit the main nginx.conf file as the one that comes from the rpm's will not work. Comment the following lines:
|
||||
|
||||
```
|
||||
```bash
|
||||
# server {
|
||||
# listen 80 default_server;
|
||||
# listen [::]:80 default_server;
|
||||
|
@ -124,7 +175,12 @@ You will need to edit the main nginx.conf file as the one that comes from the rp
|
|||
```
|
||||
|
||||
Also make sure file in **/etc/nginx/conf.d/webvirtcloud.conf** has the proper paths:
|
||||
```
|
||||
|
||||
```bash
|
||||
upstream gunicorn_server {
|
||||
#server unix:/srv/webvirtcloud/venv/wvcloud.socket fail_timeout=0;
|
||||
server 127.0.0.1:8000 fail_timeout=0;
|
||||
}
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
|
@ -137,14 +193,14 @@ server {
|
|||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_pass http://gunicorn_server;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host:$server_port;
|
||||
proxy_set_header X-Forwarded-Proto $remote_addr;
|
||||
proxy_connect_timeout 600;
|
||||
proxy_read_timeout 600;
|
||||
proxy_send_timeout 600;
|
||||
proxy_connect_timeout 1800;
|
||||
proxy_read_timeout 1800;
|
||||
proxy_send_timeout 1800;
|
||||
client_max_body_size 1024M;
|
||||
}
|
||||
}
|
||||
|
@ -160,28 +216,48 @@ Change permission for selinux:
|
|||
|
||||
```bash
|
||||
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/webvirtcloud(/.*)"
|
||||
sudo setsebool -P httpd_can_network_connect on -P
|
||||
```
|
||||
|
||||
Add required user to the kvm group:
|
||||
Add required user to the kvm group(if you not install with root):
|
||||
|
||||
```bash
|
||||
sudo usermod -G kvm -a webvirtmgr
|
||||
sudo usermod -G kvm -a <username>
|
||||
```
|
||||
|
||||
Allow http ports on firewall:
|
||||
|
||||
```bash
|
||||
sudo firewall-cmd --add-service=http
|
||||
sudo firewall-cmd --add-service=http --permanent
|
||||
sudo firewall-cmd --add-port=6080/tcp
|
||||
sudo firewall-cmd --add-port=6080/tcp --permanent
|
||||
```
|
||||
|
||||
Let's restart nginx and the supervisord services:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart nginx && systemctl restart supervisord
|
||||
```
|
||||
|
||||
And finally, check everything is running:
|
||||
|
||||
```bash
|
||||
sudo supervisorctl status
|
||||
gstfsd RUNNING pid 24662, uptime 6:01:40
|
||||
novncd RUNNING pid 24661, uptime 6:01:40
|
||||
webvirtcloud RUNNING pid 24660, uptime 6:01:40
|
||||
```
|
||||
|
||||
novncd RUNNING pid 24186, uptime 2:59:14
|
||||
webvirtcloud RUNNING pid 24185, uptime 2:59:14
|
||||
#### Apache mod_wsgi configuration
|
||||
|
||||
```bash
|
||||
WSGIDaemonProcess webvirtcloud threads=2 maximum-requests=1000 display-name=webvirtcloud
|
||||
WSGIScriptAlias / /srv/webvirtcloud/webvirtcloud/wsgi_custom.py
|
||||
```
|
||||
|
||||
#### Install final required packages for libvirtd and others on Host Server
|
||||
|
||||
```bash
|
||||
wget -O - https://clck.ru/9V9fH | sudo sh
|
||||
```
|
||||
|
@ -190,19 +266,215 @@ Done!!
|
|||
|
||||
Go to http://serverip and you should see the login screen.
|
||||
|
||||
### Alternative running novncd via runit(Debian)
|
||||
|
||||
Alternative to running nonvcd via supervisor is runit.
|
||||
|
||||
On Debian systems install runit and configure novncd service:
|
||||
|
||||
```bash
|
||||
apt install runit runit-systemd
|
||||
mkdir /etc/service/novncd/
|
||||
ln -s /srv/webvirtcloud/conf/runit/novncd.sh /etc/service/novncd/run
|
||||
systemctl start runit.service
|
||||
```
|
||||
|
||||
### Default credentials
|
||||
<pre>
|
||||
|
||||
```html
|
||||
login: admin
|
||||
password: admin
|
||||
</pre>
|
||||
```
|
||||
|
||||
### Configuring Compute SSH connection
|
||||
|
||||
This is a short example of configuring cloud and compute side of the ssh connection.
|
||||
|
||||
On the webvirtcloud machine you need to generate ssh keys and optionally disable StrictHostKeyChecking.
|
||||
|
||||
### How To Update
|
||||
```bash
|
||||
chown www-data -R ~www-data
|
||||
sudo -u www-data ssh-keygen
|
||||
cat > ~www-data/.ssh/config << EOF
|
||||
Host *
|
||||
StrictHostKeyChecking no
|
||||
EOF
|
||||
chown www-data -R ~www-data/.ssh/config
|
||||
```
|
||||
|
||||
You need to put cloud public key into authorized keys on the compute node. Simpliest way of doing this is to use ssh tool from the webvirtcloud server.
|
||||
|
||||
```bash
|
||||
sudo -u www-data ssh-copy-id root@compute1
|
||||
```
|
||||
|
||||
### Host SMBIOS information is not available
|
||||
|
||||
If you see warning
|
||||
|
||||
```bash
|
||||
Unsupported configuration: Host SMBIOS information is not available
|
||||
```
|
||||
|
||||
Then you need to install `dmidecode` package on your host using your package manager and restart libvirt daemon.
|
||||
|
||||
Debian/Ubuntu like:
|
||||
|
||||
```bash
|
||||
sudo apt-get install dmidecode
|
||||
sudo service libvirt-bin restart
|
||||
```
|
||||
|
||||
Arch Linux
|
||||
|
||||
```bash
|
||||
sudo pacman -S dmidecode
|
||||
systemctl restart libvirtd
|
||||
```
|
||||
|
||||
### Cloud-init
|
||||
|
||||
Currently supports only root ssh authorized keys and hostname. Example configuration of the cloud-init client follows.
|
||||
|
||||
```bash
|
||||
datasource:
|
||||
OpenStack:
|
||||
metadata_urls: [ "http://webvirtcloud.domain.com/datasource" ]
|
||||
```
|
||||
|
||||
### Reverse-Proxy
|
||||
|
||||
Edit WS_PUBLIC_PORT at settings.py file to expose redirect to 80 or 443. Default: 6080
|
||||
|
||||
```bash
|
||||
WS_PUBLIC_PORT = 80
|
||||
```
|
||||
|
||||
## How To Update
|
||||
|
||||
```bash
|
||||
# Go to Installation Directory
|
||||
cd /srv/webvirtcloud
|
||||
source venv/bin/activate
|
||||
git pull
|
||||
python manage.py migrate
|
||||
pip3 install -U -r conf/requirements.txt
|
||||
python3 manage.py migrate
|
||||
python3 manage.py collectstatic --noinput
|
||||
sudo service supervisor restart
|
||||
```
|
||||
|
||||
### License
|
||||
### Running tests
|
||||
|
||||
Server on which tests will be performed must have libvirt up and running.
|
||||
It must not contain vms.
|
||||
It must have `default` storage which not contain any disk images.
|
||||
It must have `default` network which must be on.
|
||||
Setup venv
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r conf/requirements.txt
|
||||
```
|
||||
|
||||
Run tests
|
||||
|
||||
```bash
|
||||
python manage.py test
|
||||
```
|
||||
|
||||
## LDAP Configuration
|
||||
|
||||
The config options below can be changed in `webvirtcloud/settings.py` file. Variants for Active Directory and OpenLDAP are shown. This is a minimal config to get LDAP running, for further info read the [django-auth-ldap documentation](https://django-auth-ldap.readthedocs.io).
|
||||
|
||||
Enable LDAP
|
||||
|
||||
```bash
|
||||
sudo sed -i "s~#\"django_auth_ldap.backend.LDAPBackend\",~\"django_auth_ldap.backend.LDAPBackend\",~g" /srv/webvirtcloud/webvirtcloud/settings.py
|
||||
```
|
||||
|
||||
Set the LDAP server name and bind DN
|
||||
|
||||
```python
|
||||
# Active Directory
|
||||
AUTH_LDAP_SERVER_URI = "ldap://example.com"
|
||||
AUTH_LDAP_BIND_DN = "username@example.com"
|
||||
AUTH_LDAP_BIND_PASSWORD = "password"
|
||||
|
||||
# OpenLDAP
|
||||
AUTH_LDAP_SERVER_URI = "ldap://example.com"
|
||||
AUTH_LDAP_BIND_DN = "CN=username,CN=Users,OU=example,OU=com"
|
||||
AUTH_LDAP_BIND_PASSWORD = "password"
|
||||
```
|
||||
|
||||
Set the user filter and user and group search base and filter
|
||||
|
||||
```python
|
||||
# Active Directory
|
||||
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
||||
"CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"
|
||||
)
|
||||
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
|
||||
"CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(objectClass=group)"
|
||||
)
|
||||
AUTH_LDAP_GROUP_TYPE = NestedActiveDirectoryGroupType()
|
||||
|
||||
# OpenLDAP
|
||||
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
||||
"CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(cn=%(user)s)"
|
||||
)
|
||||
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
|
||||
"CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(objectClass=groupOfUniqueNames)"
|
||||
)
|
||||
AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType() # import needs to be changed at the top of settings.py
|
||||
```
|
||||
|
||||
Set group which is required to access WebVirtCloud. You may set this to `False` to disable this filter.
|
||||
|
||||
```python
|
||||
AUTH_LDAP_REQUIRE_GROUP = "CN=WebVirtCloud Access,CN=Users,DC=example,DC=com"
|
||||
```
|
||||
|
||||
Populate user fields with values from LDAP
|
||||
|
||||
```python
|
||||
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
|
||||
"is_staff": "CN=WebVirtCloud Staff,CN=Users,DC=example,DC=com",
|
||||
"is_superuser": "CN=WebVirtCloud Admins,CN=Users,DC=example,DC=com",
|
||||
}
|
||||
AUTH_LDAP_USER_ATTR_MAP = {
|
||||
"first_name": "givenName",
|
||||
"last_name": "sn",
|
||||
"email": "mail",
|
||||
}
|
||||
```
|
||||
|
||||
Now when you login with an LDAP user it will be assigned the rights defined. The user will be authenticated then with LDAP and authorized through the WebVirtCloud permissions.
|
||||
|
||||
If you'd like to move a user from ldap to WebVirtCloud, just change its password from the UI and (eventually) remove from the group in LDAP.
|
||||
|
||||
|
||||
## REST API / BETA
|
||||
Webvirtcloud provides a REST API for programmatic access.
|
||||
To access API methods open your browser and check them with Swagger interface
|
||||
```bash
|
||||
http://<webvirtloud-address:port>/swagger
|
||||
```
|
||||
```bash
|
||||
http://<webvirtloud-address:port>/redoc
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
Instance Detail:
|
||||
<img src="doc/images/instance.PNG" width="96%" align="center"/>
|
||||
Instance List:</br>
|
||||
<img src="doc/images/grouped.PNG" width="43%"/>
|
||||
<img src="doc/images/nongrouped.PNG" width="53%"/>
|
||||
Other: </br>
|
||||
<img src="doc/images/hosts.PNG" width="47%"/>
|
||||
<img src="doc/images/log.PNG" width="49%"/>
|
||||
|
||||
## License
|
||||
|
||||
WebVirtCloud is licensed under the [Apache Licence, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html).
|
||||
|
|
54
Vagrantfile
vendored
54
Vagrantfile
vendored
|
@ -2,17 +2,55 @@
|
|||
# vi: set ft=ruby :
|
||||
|
||||
Vagrant.configure(2) do |config|
|
||||
config.vm.box = "ubuntu/trusty64"
|
||||
config.vm.hostname = "webvirtcloud"
|
||||
config.vm.network "private_network", ip: "192.168.33.10"
|
||||
config.vm.provision "shell", inline: <<-SHELL
|
||||
# Default machine, if name not specified...
|
||||
config.vm.define "dev", primary: true do |dev|
|
||||
dev.vm.box = "ubuntu/bionic64"
|
||||
dev.vm.hostname = "webvirtcloud"
|
||||
dev.vm.network "private_network", ip: "192.168.33.10"
|
||||
dev.vm.provision "shell", inline: <<-SHELL
|
||||
sudo sh /vagrant/dev/libvirt-bootstrap.sh
|
||||
sudo sed -i 's/auth_tcp = \"sasl\"/auth_tcp = \"none\"/g' /etc/libvirt/libvirtd.conf
|
||||
sudo service libvirt-bin restart
|
||||
sudo adduser vagrant libvirtd
|
||||
sudo apt-get -y install python-virtualenv python-dev libxml2-dev libvirt-dev zlib1g-dev
|
||||
virtualenv /vagrant/venv
|
||||
sudo apt-get -y install python3-virtualenv virtualenv python3-pip python3-dev python3-lxml libvirt-dev zlib1g-dev python3-guestfs
|
||||
virtualenv -p python3 /vagrant/venv
|
||||
source /vagrant/venv/bin/activate
|
||||
pip install -r /vagrant/dev/requirements.txt
|
||||
SHELL
|
||||
pip3 install -r /vagrant/dev/requirements.txt
|
||||
SHELL
|
||||
end
|
||||
# To start this machine run "vagrant up prod"
|
||||
# To enter this machine run "vagrant ssh prod"
|
||||
config.vm.define "prod", autostart: false do |prod|
|
||||
prod.vm.box = "ubuntu/bionic64"
|
||||
prod.vm.hostname = "webvirtcloud"
|
||||
prod.vm.network "private_network", ip: "192.168.33.11"
|
||||
prod.vm.network "forwarded_port", guest: 80, host: 8081
|
||||
#prod.vm.synced_folder ".", "/srv/webvirtcloud"
|
||||
prod.vm.provision "shell", inline: <<-SHELL
|
||||
sudo mkdir /srv/webvirtcloud
|
||||
sudo cp -R /vagrant/* /srv/webvirtcloud
|
||||
sudo sh /srv/webvirtcloud/dev/libvirt-bootstrap.sh
|
||||
sudo sed -i 's/auth_tcp = \"sasl\"/auth_tcp = \"none\"/g' /etc/libvirt/libvirtd.conf
|
||||
sudo service libvirt-bin restart
|
||||
sudo adduser vagrant libvirtd
|
||||
sudo chown -R vagrant:vagrant /srv/webvirtcloud
|
||||
sudo apt-get -y install python3-virtualenv python3-dev python3-lxml python3-pip virtualenv libvirt-dev zlib1g-dev libxslt1-dev nginx supervisor libsasl2-modules gcc pkg-config python3-guestfs
|
||||
virtualenv -p python3 /srv/webvirtcloud/venv
|
||||
source /srv/webvirtcloud/venv/bin/activate
|
||||
pip3 install -r /srv/webvirtcloud/requirements.txt
|
||||
sudo cp /srv/webvirtcloud/conf/supervisor/webvirtcloud.conf /etc/supervisor/conf.d
|
||||
sudo cp /srv/webvirtcloud/conf/nginx/webvirtcloud.conf /etc/nginx/conf.d
|
||||
sudo cp /srv/webvirtcloud/webvirtcloud/settings.py.template /srv/webvirtcloud/webvirtcloud/settings.py
|
||||
sudo sed "s/SECRET_KEY = ''/SECRET_KEY = '"`python3 /srv/webvirtcloud/conf/runit/secret_generator.py`"'/" -i /srv/webvirtcloud/webvirtcloud/settings.py
|
||||
python3 /srv/webvirtcloud/manage.py makemigrations
|
||||
python3 /srv/webvirtcloud/manage.py migrate
|
||||
python3 /srv/webvirtcloud/manage.py collectstatic --noinput
|
||||
sudo rm /etc/nginx/sites-enabled/default
|
||||
sudo chown -R www-data:www-data /srv/webvirtcloud
|
||||
sudo service nginx restart
|
||||
sudo service supervisor restart
|
||||
SHELL
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
|
1
_config.yml
Normal file
1
_config.yml
Normal file
|
@ -0,0 +1 @@
|
|||
theme: jekyll-theme-cayman
|
|
@ -0,0 +1 @@
|
|||
default_app_config = 'accounts.apps.AccountsConfig'
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
63
accounts/apps.py
Normal file
63
accounts/apps.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
|
||||
def apply_change_password(sender, **kwargs):
|
||||
"""
|
||||
Apply new change_password permission for all users
|
||||
Depending on settings SHOW_PROFILE_EDIT_PASSWORD
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Permission, User
|
||||
|
||||
if hasattr(settings, "SHOW_PROFILE_EDIT_PASSWORD"):
|
||||
print("\033[1m! \033[92mSHOW_PROFILE_EDIT_PASSWORD is found inside settings.py\033[0m")
|
||||
print("\033[1m* \033[92mApplying permission can_change_password for all users\033[0m")
|
||||
users = User.objects.all()
|
||||
permission = Permission.objects.get(codename="change_password")
|
||||
if settings.SHOW_PROFILE_EDIT_PASSWORD:
|
||||
print("\033[1m! \033[91mWarning!!! Setting to True for all users\033[0m")
|
||||
for user in users:
|
||||
user.user_permissions.add(permission)
|
||||
else:
|
||||
print("\033[1m* \033[91mWarning!!! Setting to False for all users\033[0m")
|
||||
for user in users:
|
||||
user.user_permissions.remove(permission)
|
||||
print("\033[1m! Don`t forget to remove the option from settings.py\033[0m")
|
||||
|
||||
|
||||
def create_admin(sender, **kwargs):
|
||||
"""
|
||||
Create initial admin user
|
||||
"""
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from accounts.models import UserAttributes
|
||||
|
||||
plan = kwargs.get("plan", [])
|
||||
for migration, rolled_back in plan:
|
||||
if (
|
||||
migration.app_label == "accounts"
|
||||
and migration.name == "0001_initial"
|
||||
and not rolled_back
|
||||
):
|
||||
if User.objects.count() == 0:
|
||||
print("\033[1m* \033[92mCreating default admin user\033[0m")
|
||||
admin = User.objects.create_superuser("admin", None, "admin")
|
||||
UserAttributes(
|
||||
user=admin,
|
||||
max_instances=-1,
|
||||
max_cpus=-1,
|
||||
max_memory=-1,
|
||||
max_disk_size=-1,
|
||||
).save()
|
||||
break
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
name = "accounts"
|
||||
verbose_name = "Accounts"
|
||||
|
||||
def ready(self):
|
||||
post_migrate.connect(create_admin, sender=self)
|
||||
post_migrate.connect(apply_change_password, sender=self)
|
|
@ -1,24 +1,75 @@
|
|||
import re
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth.models import User
|
||||
from appsettings.settings import app_settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.forms import EmailField, Form, ModelForm, ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import UserInstance, UserSSHKey
|
||||
from .utils import validate_ssh_key
|
||||
|
||||
|
||||
class UserAddForm(forms.Form):
|
||||
name = forms.CharField(label="Name",
|
||||
error_messages={'required': _('No User name has been entered')},
|
||||
max_length=20)
|
||||
password = forms.CharField(required=True, error_messages={'required': _('No password has been entered')},)
|
||||
class UserInstanceForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UserInstanceForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean_name(self):
|
||||
name = self.cleaned_data['name']
|
||||
have_symbol = re.match('^[a-z0-9]+$', name)
|
||||
if not have_symbol:
|
||||
raise forms.ValidationError(_('The flavor name must not contain any special characters'))
|
||||
elif len(name) > 20:
|
||||
raise forms.ValidationError(_('The flavor name must not exceed 20 characters'))
|
||||
try:
|
||||
User.objects.get(username=name)
|
||||
except User.DoesNotExist:
|
||||
return name
|
||||
raise forms.ValidationError(_('Flavor name is already use'))
|
||||
# Make user and instance fields not editable after creation
|
||||
instance = getattr(self, "instance", None)
|
||||
if instance and instance.id is not None:
|
||||
self.fields["user"].disabled = True
|
||||
self.fields["instance"].disabled = True
|
||||
|
||||
def clean_instance(self):
|
||||
instance = self.cleaned_data["instance"]
|
||||
if app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER == "False":
|
||||
exists = UserInstance.objects.filter(instance=instance)
|
||||
if exists:
|
||||
raise ValidationError(_("Instance owned by another user"))
|
||||
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = UserInstance
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProfileForm(ModelForm):
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
fields = ("first_name", "last_name", "email")
|
||||
|
||||
|
||||
class UserSSHKeyForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop("user", None)
|
||||
self.publickeys = UserSSHKey.objects.filter(user=self.user)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_keyname(self):
|
||||
for key in self.publickeys:
|
||||
if self.cleaned_data["keyname"] == key.keyname:
|
||||
raise ValidationError(_("Key name already exist"))
|
||||
|
||||
return self.cleaned_data["keyname"]
|
||||
|
||||
def clean_keypublic(self):
|
||||
for key in self.publickeys:
|
||||
if self.cleaned_data["keypublic"] == key.keypublic:
|
||||
raise ValidationError(_("Public key already exist"))
|
||||
|
||||
if not validate_ssh_key(self.cleaned_data["keypublic"]):
|
||||
raise ValidationError(_("Invalid key"))
|
||||
return self.cleaned_data["keypublic"]
|
||||
|
||||
def save(self, commit=True):
|
||||
ssh_key = super().save(commit=False)
|
||||
ssh_key.user = self.user
|
||||
if commit:
|
||||
ssh_key.save()
|
||||
return ssh_key
|
||||
|
||||
class Meta:
|
||||
model = UserSSHKey
|
||||
fields = ("keyname", "keypublic")
|
||||
|
||||
|
||||
class EmailOTPForm(Form):
|
||||
email = EmailField(label=_("Email"))
|
||||
|
|
|
@ -1,29 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
# Generated by Django 2.2.10 on 2020-01-28 07:01
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('instances', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserSSHKey',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('keyname', models.CharField(max_length=25)),
|
||||
('keypublic', models.CharField(max_length=500)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserInstance',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_change', models.BooleanField(default=False)),
|
||||
('is_delete', models.BooleanField(default=False)),
|
||||
('instance', models.ForeignKey(to='instances.Instance')),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
('is_vnc', models.BooleanField(default=False)),
|
||||
('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='instances.Instance')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserAttributes',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('can_clone_instances', models.BooleanField(default=True)),
|
||||
('max_instances', models.IntegerField(default=1, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)])),
|
||||
('max_cpus', models.IntegerField(default=1, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)])),
|
||||
('max_memory', models.IntegerField(default=2048, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)])),
|
||||
('max_disk_size', models.IntegerField(default=20, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)])),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def add_useradmin(apps, schema_editor):
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
User.objects.create_superuser('admin', None, 'admin',
|
||||
last_login=timezone.now()
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_useradmin),
|
||||
]
|
24
accounts/migrations/0002_permissionset.py
Normal file
24
accounts/migrations/0002_permissionset.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.2.12 on 2020-05-27 12:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PermissionSet',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
],
|
||||
options={
|
||||
'permissions': (('change_password', 'Can change password'), ),
|
||||
'managed': False,
|
||||
'default_permissions': (),
|
||||
},
|
||||
),
|
||||
]
|
24
accounts/migrations/0003_auto_20200604_0930.py
Normal file
24
accounts/migrations/0003_auto_20200604_0930.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.2.12 on 2020-06-04 09:30
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0002_permissionset'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_cpus',
|
||||
field=models.IntegerField(default=2, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_instances',
|
||||
field=models.IntegerField(default=2, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
]
|
|
@ -1,25 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('accounts', '0002_auto_20150325_0846'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserSSHKey',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('keyname', models.CharField(max_length=25)),
|
||||
('keypublic', models.CharField(max_length=500)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
44
accounts/migrations/0004_auto_20200615_0637.py
Normal file
44
accounts/migrations/0004_auto_20200615_0637.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Generated by Django 2.2.13 on 2020-06-15 06:37
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0003_auto_20200604_0930'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_cpus',
|
||||
field=models.IntegerField(default=2, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max CPUs'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_disk_size',
|
||||
field=models.IntegerField(default=20, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max disk size'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_instances',
|
||||
field=models.IntegerField(default=2, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max instances'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_memory',
|
||||
field=models.IntegerField(default=2048, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max memory'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usersshkey',
|
||||
name='keyname',
|
||||
field=models.CharField(max_length=25, verbose_name='key name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usersshkey',
|
||||
name='keypublic',
|
||||
field=models.CharField(max_length=500, verbose_name='public key'),
|
||||
),
|
||||
]
|
20
accounts/migrations/0005_auto_20200616_1039.py
Normal file
20
accounts/migrations/0005_auto_20200616_1039.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.2.13 on 2020-06-16 10:39
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('instances', '0003_auto_20200615_0637'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('accounts', '0004_auto_20200615_0637'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='userinstance',
|
||||
unique_together={('user', 'instance')},
|
||||
),
|
||||
]
|
|
@ -1,22 +1,82 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from instances.models import Instance
|
||||
|
||||
|
||||
class UserInstanceManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_related("instance", "user")
|
||||
|
||||
|
||||
class UserInstance(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
instance = models.ForeignKey(Instance)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
|
||||
is_change = models.BooleanField(default=False)
|
||||
is_delete = models.BooleanField(default=False)
|
||||
is_vnc = models.BooleanField(default=False)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.instance.name
|
||||
objects = UserInstanceManager()
|
||||
|
||||
def __str__(self):
|
||||
return _('Instance "%(inst)s" of user %(user)s') % {
|
||||
"inst": self.instance,
|
||||
"user": self.user,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
unique_together = ["user", "instance"]
|
||||
|
||||
|
||||
class UserSSHKey(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
keyname = models.CharField(max_length=25)
|
||||
keypublic = models.CharField(max_length=500)
|
||||
user = models.ForeignKey(User, on_delete=models.DO_NOTHING)
|
||||
keyname = models.CharField(_("key name"), max_length=25)
|
||||
keypublic = models.CharField(_("public key"), max_length=500)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.keyname
|
||||
|
||||
|
||||
class UserAttributes(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
can_clone_instances = models.BooleanField(default=True)
|
||||
max_instances = models.IntegerField(
|
||||
_("max instances"),
|
||||
default=2,
|
||||
help_text=_("-1 for unlimited. Any integer value"),
|
||||
validators=[MinValueValidator(-1)],
|
||||
)
|
||||
max_cpus = models.IntegerField(
|
||||
_("max CPUs"),
|
||||
default=2,
|
||||
help_text=_("-1 for unlimited. Any integer value"),
|
||||
validators=[MinValueValidator(-1)],
|
||||
)
|
||||
max_memory = models.IntegerField(
|
||||
_("max memory"),
|
||||
default=2048,
|
||||
help_text=_("-1 for unlimited. Any integer value"),
|
||||
validators=[MinValueValidator(-1)],
|
||||
)
|
||||
max_disk_size = models.IntegerField(
|
||||
_("max disk size"),
|
||||
default=20,
|
||||
help_text=_("-1 for unlimited. Any integer value"),
|
||||
validators=[MinValueValidator(-1)],
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
|
||||
class PermissionSet(models.Model):
|
||||
"""
|
||||
Dummy model for holding set of permissions we need to be automatically added by Django
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = (("change_password", _("Can change password")),)
|
||||
|
||||
managed = False
|
||||
|
|
|
@ -1,104 +1,93 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "User" %} - {{ user }}{% endblock %}
|
||||
{% load bootstrap_icons %}
|
||||
{% load qr_code %}
|
||||
|
||||
{% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %}
|
||||
{% block page_heading %}{% trans "User Profile" %}: {{ user }}{% endblock page_heading %}
|
||||
|
||||
{% block page_heading_extra %}
|
||||
{% if otp_enabled %}
|
||||
<a href="{% url 'accounts:admin_email_otp' user.id %}" class="btn btn-secondary" title="{% trans "Email OTP QR code" %}">
|
||||
{% bs_icon 'qr-code' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'admin:user_update' user.id %}?next={% url 'accounts:account' user.id %}" class="btn btn-primary" title="{% trans "Edit user" %}">
|
||||
{% bs_icon 'pencil' %}
|
||||
</a>
|
||||
<a href="{% url 'accounts:user_instance_create' user.id %}" class="btn btn-success" title="{% trans "Create user instance" %}">
|
||||
{% bs_icon 'plus-circle-fill' %}
|
||||
</a>
|
||||
{% endblock page_heading_extra %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Heading -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% include 'create_user_inst_block.html' %}
|
||||
<h1 class="page-header">{{ user }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#instances" type="button" role="tab" aria-controls="instances" aria-selected="true">
|
||||
{% trans "Instances" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#public-keys" type="button" role="tab" aria-controls="public-keys" aria-selected="false">
|
||||
{% trans "Public Keys" %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% include 'errors_block.html' %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% if not user_insts %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning:" %}</strong> {% trans "User doesn't have any Instace" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{% trans "Instance" %}</th>
|
||||
<th>{% trans "Resize" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
<th colspan="2">{% trans "Action" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for inst in user_insts %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td><a href="{% url 'instance' inst.instance.compute.id inst.instance.name %}">{{ inst.instance.name }}</a></td>
|
||||
<td>{{ inst.is_change }}</td>
|
||||
<td>{{ inst.is_delete }}</td>
|
||||
<td style="width:5px;">
|
||||
<a href="#editPriv{{ forloop.counter }}" type="button" class="btn btn-xs btn-default" data-toggle="modal">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
<!-- Modal pool -->
|
||||
<div class="modal fade" id="editPriv{{ forloop.counter }}" tabindex="-1" role="dialog" aria-labelledby="editPrivLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{% trans "Edit privilegies for" %} {{ inst.instance.name }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
|
||||
<input type="hidden" name="user_inst" value="{{ inst.id }}">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Resize" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select type="text" class="form-control" name="inst_change">
|
||||
<option value="">False</option>
|
||||
<option value="1" {% if inst.is_change %}selected{% endif %}>True</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Delete" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select type="text" class="form-control" name="inst_delete">
|
||||
<option value="">False</option>
|
||||
<option value="1" {% if inst.is_delete %}selected{% endif %}>True</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</button>
|
||||
<button type="submit" class="btn btn-primary" name="permission">{% trans "Edit" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div> <!-- /.modal -->
|
||||
</td>
|
||||
<td style="width:5px;">
|
||||
<form action="" method="post" role="form">{% csrf_token %}
|
||||
<input type="hidden" name="user_inst" value="{{ inst.id }}">
|
||||
<button type="submit" class="btn btn-xs btn-default" name="delete" tittle="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure?" %}')">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="instances">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">{% trans "Instance" %}</th>
|
||||
<th scope="col">{% trans "VNC" %}</th>
|
||||
<th scope="col">{% trans "Resize" %}</th>
|
||||
<th scope="col">{% trans "Delete" %}</th>
|
||||
<th scope="colgroup" colspan="2">{% trans "Action" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for inst in user_insts %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td><a href="{% url 'instances:instance' inst.instance.id %}">{{ inst.instance.name }}</a></td>
|
||||
<td>{{ inst.is_vnc }}</td>
|
||||
<td>{{ inst.is_change }}</td>
|
||||
<td>{{ inst.is_delete }}</td>
|
||||
<td style="width:5px;">
|
||||
<a href="{% url 'accounts:user_instance_update' inst.id %}" class="btn btn-sm btn-secondary" title="{% trans "edit" %}">
|
||||
{% bs_icon 'pencil' %}
|
||||
</a>
|
||||
</td>
|
||||
<td style="width:5px;">
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'accounts:user_instance_delete' inst.id %}" title="{% trans "Delete" %}">
|
||||
{% bs_icon 'trash' %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="public-keys">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Key name" %}</th>
|
||||
<th scope="col">{% trans "Public key" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for publickey in publickeys %}
|
||||
<tr>
|
||||
<td>{{ publickey.keyname }}</td>
|
||||
<td title="{{ publickey.keypublic }}">{{ publickey.keypublic|truncatechars:64 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Users" %}{% endblock %}
|
||||
{% block content %}
|
||||
<!-- Page Heading -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% include 'create_user_block.html' %}
|
||||
<h1 class="page-header">{% trans "Users" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
{% include 'errors_block.html' %}
|
||||
|
||||
<div class="row">
|
||||
{% if not users %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning:" %}</strong> {% trans "You don't have any User" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% for user in users %}
|
||||
<div id="{{ user.username }}" class="col-xs-12 col-sm-4">
|
||||
<div class="panel {% if user.is_active %}panel-success{% else %}panel-danger{% endif %} panel-data">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a href="{% url 'account' user.id %}"><strong>{{ user.username }}</strong></a>
|
||||
<a data-toggle="modal" href="#editUser{{ user.id }}" class="pull-right" title="{% trans "Edit" %}">
|
||||
<span class="glyphicon glyphicon-cog"></span>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-xs-4 col-sm-4">
|
||||
<p><strong>{% trans "Status:" %}</strong></p>
|
||||
</div>
|
||||
<div class="col-xs-4 col-sm-6">
|
||||
{% if user.is_active %}
|
||||
<p>{% trans "Active" %}</p>
|
||||
{% else %}
|
||||
<p>{% trans "Blocked" %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Edit -->
|
||||
<div class="modal fade" id="editUser{{ user.id }}" tabindex="-1" role="dialog" aria-labelledby="editUserLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{% trans "Edit user info" %}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Name" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="hidden" name="user_id" value="{{ user.id }}">
|
||||
<input type="text" name="name" class="form-control" value="{{ user.username }}" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="password" name="user_pass" class="form-control" value="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="pull-left btn btn-danger" name="delete">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
{% if user.is_active %}
|
||||
<button type="submit" class="pull-left btn btn-warning" name="block">
|
||||
{% trans "Block" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" class="pull-left btn btn-success" name="unblock">
|
||||
{% trans "Unblock" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="edit">
|
||||
{% trans "Edit" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
34
accounts/templates/accounts/change_password_form.html
Normal file
34
accounts/templates/accounts/change_password_form.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load django_bootstrap5 %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap_icons %}
|
||||
|
||||
{% block title %}{%trans "Change Password" %}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="offset-2 col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">{%trans "Change Password" %}: {{ user }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" id="password-change">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout='horizontal' %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="float-end">
|
||||
<a class="btn btn-primary" href="javascript:history.back()">{% bs_icon 'x' %}
|
||||
{% trans "Cancel" %}</a>
|
||||
<button type="submit" form="password-change" class="btn btn-success">
|
||||
{% bs_icon 'check2' %} {% trans "Change" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
9
accounts/templates/accounts/email/otp.html
Normal file
9
accounts/templates/accounts/email/otp.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% load i18n %}
|
||||
{% load qr_code %}
|
||||
{% blocktrans %}
|
||||
Scan this QR code to get OTP for account '{{ user }}'
|
||||
{% endblocktrans %}
|
||||
<br>
|
||||
{% qr_from_text totp_url %}
|
||||
<p class="small">{% trans 'Some e-mail clients does not render SVG, also generating PNG.' %}</p>
|
||||
{% qr_from_text totp_url size="s" image_format="png" error_correction="M" %}
|
32
accounts/templates/accounts/email_otp_form.html
Normal file
32
accounts/templates/accounts/email_otp_form.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
{% extends "base.html" %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load bootstrap_icons %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block page_heading %}{{ title }}{% endblock page_heading %}
|
||||
|
||||
{% block content %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans %}
|
||||
Enter email address OTP QR code will be sent to.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form id="create-update" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout='horizontal' %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="mb-0 float-end">
|
||||
<a class="btn btn-primary" href="javascript:history.back()">{% bs_icon 'arrow-left' %} {% trans "Cancel" %}</a>
|
||||
<button type="submit" form="create-update" class="btn btn-success">
|
||||
{% bs_icon 'envelope-open' %} {% trans "Send" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
57
accounts/templates/accounts/otp_login.html
Normal file
57
accounts/templates/accounts/otp_login.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load django_bootstrap5 %}
|
||||
|
||||
{% block title %}{% trans "WebVirtCloud" %} - {% trans "Sign In with OTP" %}{% endblock title %}
|
||||
|
||||
{% block style %}
|
||||
<link href="{% static "css/signin.css" %}" rel="stylesheet">
|
||||
{% endblock style %}
|
||||
|
||||
{% block content %}
|
||||
<div class="login-box">
|
||||
<div class="page-header">
|
||||
<a href="/"><h1>WebVirtCloud</h1></a>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="col-12" role="main">
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% bootstrap_form_errors form %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form id="form-signin" class="form-signin" method="post" role="form" aria-label="Login form">{% csrf_token %}
|
||||
<h2 class="form-signin-heading">{% trans "Sign In" %}</h2>
|
||||
{% bootstrap_field form.username layout='inline' %}
|
||||
{% bootstrap_field form.password layout='inline' %}
|
||||
{% bootstrap_field form.otp_token layout='inline'%}
|
||||
<a href="{% url 'accounts:email_otp' %}" class="float-end">{% trans "I do not have/lost my OTP!" %}</a>
|
||||
<br>
|
||||
<div class="d-grid">
|
||||
<button id="btn-signin" class="btn btn-lg btn-success" type="submit">{% trans "Sign In" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$("#btn-signin").click(function() {
|
||||
// disable button
|
||||
$(this).prop("disabled", true);
|
||||
// add spinner to button
|
||||
$(this).html(
|
||||
`
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
{% trans "Loading" %}...
|
||||
`
|
||||
);
|
||||
$("#form-signin").submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock script%}
|
|
@ -1,41 +0,0 @@
|
|||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="WebVirtMgr panel for manage virtual machine">
|
||||
<meta name="author" content="anatoliy.guskov@gmail.com">
|
||||
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
|
||||
<!-- Bootstrap Core CSS -->
|
||||
<link href="{% static "css/bootstrap.min.css" %}" rel="stylesheet">
|
||||
|
||||
<!-- SB admin CSS -->
|
||||
<link href="{% static "css/signin.css" %}" rel="stylesheet">
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="{% static "js/jquery.js" %}"></script>
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script src="{% static "js/bootstrap.min.js" %}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,38 +0,0 @@
|
|||
{% load i18n %}
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="#AddUser" type="button" class="btn btn-success pull-right" data-toggle="modal">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
<!-- Modal pool -->
|
||||
<div class="modal fade" id="AddUser" tabindex="-1" role="dialog" aria-labelledby="AddUserLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{% trans "Add New User" %}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Name" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" name="name" placeholder="john" required pattern="[a-z0-9]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="password" class="form-control" name="password" placeholder="*******" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</button>
|
||||
<button type="submit" class="btn btn-primary" name="create">{% trans "Create" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div> <!-- /.modal -->
|
||||
{% endif %}
|
|
@ -1,36 +0,0 @@
|
|||
{% load i18n %}
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="#addUserInst" type="button" class="btn btn-success pull-right" data-toggle="modal">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
<!-- Modal pool -->
|
||||
<div class="modal fade" id="addUserInst" tabindex="-1" role="dialog" aria-labelledby="addUserInstLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{% trans "Add Instance for User" %}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Host / Instance" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select class="form-control" name="inst_id">
|
||||
{% for inst in instances %}
|
||||
<option value="{{ inst.id }}">{{ inst.compute.name }} / {{ inst.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</button>
|
||||
<button type="submit" class="btn btn-primary" name="add">{% trans "Add" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div> <!-- /.modal -->
|
||||
{% endif %}
|
|
@ -1,25 +1,55 @@
|
|||
{% extends "base_auth.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "WebVirtCloud - Sign In" %}{% endblock %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
{% block title %}{% trans "WebVirtCloud" %} - {% trans "Sign In" %}{% endblock title %}
|
||||
|
||||
{% block style %}
|
||||
<link href="{% static "css/signin.css" %}" rel="stylesheet">
|
||||
{% endblock style %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="page-header">
|
||||
<a href="/"><h1>WebVirtCloud</h1></a>
|
||||
</div>
|
||||
<div class="col-xs-12" role="main">
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
{% trans "Incorrect username or password." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form class="form-signin" method="post" role="form">{% csrf_token %}
|
||||
<h2 class="form-signin-heading">{% trans "Sign In" %}</h2>
|
||||
<input type="text" class="form-control" name="username" placeholder="Login" autocapitalize="none" autocorrect="off" autofocus>
|
||||
<input type="password" class="form-control" name="password" placeholder="Password">
|
||||
<input name="next" id="next" type="hidden" value="{% url 'instances' %}">
|
||||
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="login-box">
|
||||
<div class="page-header">
|
||||
<a href="/"><h1>WebVirtCloud</h1></a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<hr>
|
||||
<div class="col-12" role="main">
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
{% trans "Incorrect username or password." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form id="form-signin" class="form-signin" method="post" role="form" aria-label="Login form">{% csrf_token %}
|
||||
<h2 class="form-signin-heading">{% trans "Sign In" %}</h2>
|
||||
<input type="text" class="form-control" name="username" placeholder="{% trans 'User' %}" autocapitalize="none" autocorrect="off" autofocus required>
|
||||
<input type="password" class="form-control" name="password" placeholder="{% trans 'Password' %}" required>
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
<div class="d-grid">
|
||||
<button id="btn-signin" class="btn btn-lg btn-success" type="submit">{% trans "Sign In" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$("#btn-signin").click(function() {
|
||||
// disable button
|
||||
$(this).prop("disabled", true);
|
||||
// add spinner to button
|
||||
$(this).html(
|
||||
`
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
{% trans "Loading" %}...
|
||||
`
|
||||
);
|
||||
$("#form-signin").submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock script%}
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
{% extends "base_auth.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "WebVirtCloud - Sign Out" %}{% endblock %}
|
||||
{% block title %}
|
||||
{% trans "WebVirtCloud" %} - {% trans "Sign Out"%}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<a href="/"><h1>WebVirtCloud</h1></a>
|
||||
</div>
|
||||
<div class="col-xs-12" role="main">
|
||||
<div class="col-12" role="main">
|
||||
<div class="logout">
|
||||
<h1>{% trans "Successful log out" %}</h1>
|
||||
<h2>{% trans "Successful log out" %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,115 +1,84 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load bootstrap_icons %}
|
||||
{% load tags_fingerprint %}
|
||||
{% block title %}{% trans "Profile" %}{% endblock %}
|
||||
|
||||
{% block title %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock %}
|
||||
|
||||
{% block page_heading %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock page_heading %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Heading -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header">{% trans "Profile" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
{% include 'errors_block.html' %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h3 class="page-header">{% trans "Edit Profile" %}</h3>
|
||||
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Login" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" value="{{ request.user.username }}" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 control-label">{% trans "Username" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" name="username" value="{{ request.user.first_name }}" pattern="[0-9a-zA-Z]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 control-label">{% trans "Email" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="email" class="form-control" name="email" value="{{ request.user.email }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">{% trans "Change" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<h3 class="page-header">{% trans "Edit Password" %}</h3>
|
||||
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Old" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="password" class="form-control" name="oldpasswd" value="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 control-label">{% trans "New" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="password" class="form-control" name="passwd1" value="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 control-label">{% trans "Retry" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="password" class="form-control" name="passwd2" value="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">{% trans "Change" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<h3 class="page-header">{% trans "SSH Keys" %}</h3>
|
||||
{% if publickeys %}
|
||||
<div class="col-lg-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<tbody style="text-align: center;">
|
||||
{% for key in publickeys %}
|
||||
<tr>
|
||||
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
|
||||
<td>
|
||||
<form action="" method="post" role="form">{% csrf_token %}
|
||||
<input type="hidden" name="keyid" value="{{ key.id }}"/>
|
||||
<button type="submit" class="btn btn-sm btn-default" name="keydelete" title="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure?" %}')">
|
||||
<span class="glyphicon glyphicon-trash"></span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#editprofile" type="button" role="tab" aria-controls="editprofile" aria-selected="true">
|
||||
{% trans "Edit Profile" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#sshkeys" type="button" role="tab" aria-controls="sshkeys" aria-selected="false">
|
||||
{% trans "SSH Keys" %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane tab-pane-bordered active" id="editprofile">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" action="" role="form" aria-label="Edit user info form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form profile_form layout='horizontal' %}
|
||||
{% if perms.accounts.change_password %}
|
||||
<a href="{% url 'accounts:change_password' %}" class="btn btn-primary">
|
||||
{% bs_icon 'lock' %} {% trans "Change Password" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 control-label">{% trans "Retry" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" name="keyname" placeholder="{% trans "Enter Name" %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 control-label">{% trans "Retry" %}</label>
|
||||
<div class="col-sm-8">
|
||||
<textarea name="keypublic" class="form-control" rows="6" placeholder="{% trans "Enter Public Key" %}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">{% trans "Create" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mb-0 float-end">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% bs_icon 'pencil' %} {% trans "Update" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane tab-pane-bordered fade" id="sshkeys">
|
||||
{% if publickeys %}
|
||||
<div class="col-lg-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<tbody class="text-center">
|
||||
{% for key in publickeys %}
|
||||
<tr>
|
||||
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
|
||||
<td>
|
||||
<a href="{% url 'accounts:ssh_key_delete' key.id %}" title="{% trans "Delete" %}" class="btn btn-sm btn-secondary">
|
||||
{% bs_icon 'trash' %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{%trans "Add SSH Key" %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'accounts:ssh_key_create' %}" role="form" aria-label="Add key to user form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form ssh_key_form layout='horizontal' %}
|
||||
<div class="mb-0 float-end">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% bs_icon 'plus-circle-fill' %} {% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,12 +1,16 @@
|
|||
from django import template
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def ssh_to_fingerprint(line):
|
||||
key = base64.b64decode(line.strip().split()[1].encode('ascii'))
|
||||
fp_plain = hashlib.md5(key).hexdigest()
|
||||
return ':'.join(a+b for a, b in zip(fp_plain[::2], fp_plain[1::2]))
|
||||
try:
|
||||
key = base64.b64decode(line.strip().split()[1].encode('ascii'))
|
||||
fp_plain = hashlib.md5(key).hexdigest()
|
||||
return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2]))
|
||||
except Exception:
|
||||
return 'Invalid key'
|
||||
|
|
|
@ -1,3 +1,308 @@
|
|||
from django.test import TestCase
|
||||
from appsettings.settings import app_settings
|
||||
from computes.models import Compute
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
from instances.models import Instance
|
||||
from instances.utils import refr
|
||||
from libvirt import VIR_DOMAIN_UNDEFINE_NVRAM
|
||||
from vrtManager.create import wvmCreate
|
||||
|
||||
# Create your tests here.
|
||||
from accounts.forms import UserInstanceForm, UserSSHKeyForm
|
||||
from accounts.models import UserInstance, UserSSHKey
|
||||
from accounts.utils import validate_ssh_key
|
||||
|
||||
|
||||
class AccountsTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Add users for testing purposes
|
||||
User = get_user_model()
|
||||
cls.admin_user = User.objects.get(pk=1)
|
||||
cls.test_user = User.objects.create_user(username="test", password="test")
|
||||
|
||||
# Add localhost compute
|
||||
cls.compute = Compute(
|
||||
name="test-compute",
|
||||
hostname="localhost",
|
||||
login="",
|
||||
password="",
|
||||
details="local",
|
||||
type=4,
|
||||
)
|
||||
cls.compute.save()
|
||||
|
||||
cls.connection = wvmCreate(
|
||||
cls.compute.hostname,
|
||||
cls.compute.login,
|
||||
cls.compute.password,
|
||||
cls.compute.type,
|
||||
)
|
||||
|
||||
# Add disks for testing
|
||||
cls.connection.create_volume(
|
||||
"default",
|
||||
"test-volume",
|
||||
1,
|
||||
"qcow2",
|
||||
False,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
# XML for testing vm
|
||||
with open("conf/test-vm.xml", "r") as f:
|
||||
cls.xml = f.read()
|
||||
|
||||
# Create testing vm from XML
|
||||
cls.connection._defineXML(cls.xml)
|
||||
refr(cls.compute)
|
||||
cls.instance = Instance.objects.get(pk=1)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# Destroy testing vm
|
||||
cls.instance.proxy.delete_all_disks()
|
||||
cls.instance.proxy.delete(VIR_DOMAIN_UNDEFINE_NVRAM)
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
self.client.login(username="admin", password="admin")
|
||||
permission = Permission.objects.get(codename="change_password")
|
||||
self.test_user.user_permissions.add(permission)
|
||||
self.rsa_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6OOdbfv27QVnSC6sKxGaHb6YFc+3gxCkyVR3cTSXE/n5BEGf8aOgBpepULWa1RZfxYHY14PlKULDygdXSdrrR2kNSwoKz/Oo4d+3EE92L7ocl1+djZbptzgWgtw1OseLwbFik+iKlIdqPsH+IUQvX7yV545ZQtAP8Qj1R+uCqkw== test@test"
|
||||
self.ecdsa_key = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJc5xpT3R0iFJYNZbmWgAiDlHquX/BcV1kVTsnBfiMsZgU3lGaqz2eb7IBcir/dxGnsVENTTmPQ6sNcxLxT9kkQ= realgecko@archlinux"
|
||||
|
||||
def test_profile(self):
|
||||
response = self.client.get(reverse("accounts:profile"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("accounts:account", args=[self.test_user.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_account_with_otp(self):
|
||||
settings.OTP_ENABLED = True
|
||||
response = self.client.get(
|
||||
reverse("accounts:account", args=[self.test_user.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_login_logout(self):
|
||||
client = Client()
|
||||
|
||||
response = client.post(
|
||||
reverse("accounts:login"), {"username": "test", "password": "test"}
|
||||
)
|
||||
self.assertRedirects(response, reverse("accounts:profile"))
|
||||
|
||||
response = client.get(reverse("accounts:logout"))
|
||||
self.assertRedirects(response, reverse("accounts:login"))
|
||||
|
||||
def test_change_password(self):
|
||||
self.client.force_login(self.test_user)
|
||||
|
||||
response = self.client.get(reverse("accounts:change_password"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("accounts:change_password"),
|
||||
{
|
||||
"old_password": "wrongpass",
|
||||
"new_password1": "newpw",
|
||||
"new_password2": "newpw",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("accounts:change_password"),
|
||||
{
|
||||
"old_password": "test",
|
||||
"new_password1": "newpw",
|
||||
"new_password2": "newpw",
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, reverse("accounts:profile"))
|
||||
|
||||
self.client.logout()
|
||||
|
||||
logged_in = self.client.login(username="test", password="newpw")
|
||||
self.assertTrue(logged_in)
|
||||
|
||||
def test_user_instance_create_update_delete(self):
|
||||
# create
|
||||
response = self.client.get(
|
||||
reverse("accounts:user_instance_create", args=[self.test_user.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("accounts:user_instance_create", args=[self.test_user.id]),
|
||||
{
|
||||
"user": self.test_user.id,
|
||||
"instance": self.instance.id,
|
||||
"is_change": False,
|
||||
"is_delete": False,
|
||||
"is_vnc": False,
|
||||
},
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, reverse("accounts:account", args=[self.test_user.id])
|
||||
)
|
||||
|
||||
user_instance: UserInstance = UserInstance.objects.get(pk=1)
|
||||
self.assertEqual(user_instance.user, self.test_user)
|
||||
self.assertEqual(user_instance.instance, self.instance)
|
||||
self.assertEqual(user_instance.is_change, False)
|
||||
self.assertEqual(user_instance.is_delete, False)
|
||||
self.assertEqual(user_instance.is_vnc, False)
|
||||
|
||||
# update
|
||||
response = self.client.get(
|
||||
reverse("accounts:user_instance_update", args=[user_instance.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("accounts:user_instance_update", args=[user_instance.id]),
|
||||
{
|
||||
"user": self.test_user.id,
|
||||
"instance": self.instance.id,
|
||||
"is_change": True,
|
||||
"is_delete": True,
|
||||
"is_vnc": True,
|
||||
},
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, reverse("accounts:account", args=[self.test_user.id])
|
||||
)
|
||||
|
||||
user_instance: UserInstance = UserInstance.objects.get(pk=1)
|
||||
self.assertEqual(user_instance.user, self.test_user)
|
||||
self.assertEqual(user_instance.instance, self.instance)
|
||||
self.assertEqual(user_instance.is_change, True)
|
||||
self.assertEqual(user_instance.is_delete, True)
|
||||
self.assertEqual(user_instance.is_vnc, True)
|
||||
|
||||
# delete
|
||||
response = self.client.get(
|
||||
reverse("accounts:user_instance_delete", args=[user_instance.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("accounts:user_instance_delete", args=[user_instance.id])
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, reverse("accounts:account", args=[self.test_user.id])
|
||||
)
|
||||
|
||||
# test 'next' redirect during deletion
|
||||
user_instance = UserInstance.objects.create(
|
||||
user=self.test_user, instance=self.instance
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("accounts:user_instance_delete", args=[user_instance.id])
|
||||
+ "?next="
|
||||
+ reverse("index")
|
||||
)
|
||||
self.assertRedirects(response, reverse("index"))
|
||||
|
||||
def test_update_user_profile(self):
|
||||
self.client.force_login(self.test_user)
|
||||
|
||||
user = get_user_model().objects.get(username="test")
|
||||
self.assertEqual(user.first_name, "")
|
||||
self.assertEqual(user.last_name, "")
|
||||
self.assertEqual(user.email, "")
|
||||
|
||||
response = self.client.post(
|
||||
reverse("accounts:profile"),
|
||||
{
|
||||
"first_name": "first name",
|
||||
"last_name": "last name",
|
||||
"email": "email@mail.mail",
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, reverse("accounts:profile"))
|
||||
|
||||
user = get_user_model().objects.get(username="test")
|
||||
self.assertEqual(user.first_name, "first name")
|
||||
self.assertEqual(user.last_name, "last name")
|
||||
self.assertEqual(user.email, "email@mail.mail")
|
||||
|
||||
def test_create_delete_ssh_key(self):
|
||||
response = self.client.get(reverse("accounts:ssh_key_create"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("accounts:ssh_key_create"),
|
||||
{
|
||||
"keyname": "keyname",
|
||||
"keypublic": self.rsa_key,
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, reverse("accounts:profile"))
|
||||
|
||||
key = UserSSHKey.objects.get(pk=1)
|
||||
self.assertEqual(key.keyname, "keyname")
|
||||
self.assertEqual(key.keypublic, self.rsa_key)
|
||||
|
||||
response = self.client.get(reverse("accounts:ssh_key_delete", args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("accounts:ssh_key_delete", args=[1]))
|
||||
self.assertRedirects(response, reverse("accounts:profile"))
|
||||
|
||||
def test_validate_ssh_key(self):
|
||||
self.assertFalse(validate_ssh_key(""))
|
||||
self.assertFalse(validate_ssh_key("ssh-rsa ABBA test@test"))
|
||||
self.assertFalse(validate_ssh_key("ssh-rsa AAAABwdzZGY= test@test"))
|
||||
self.assertFalse(validate_ssh_key("ssh-rsa AAA test@test"))
|
||||
# validate ecdsa key
|
||||
self.assertTrue(validate_ssh_key(self.ecdsa_key))
|
||||
|
||||
def test_forms(self):
|
||||
# raise available validation errors for maximum coverage
|
||||
form = UserSSHKeyForm(
|
||||
{"keyname": "keyname", "keypublic": self.rsa_key}, user=self.test_user
|
||||
)
|
||||
form.save()
|
||||
|
||||
form = UserSSHKeyForm(
|
||||
{"keyname": "keyname", "keypublic": self.rsa_key}, user=self.test_user
|
||||
)
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
form = UserSSHKeyForm(
|
||||
{"keyname": "keyname", "keypublic": "invalid key"}, user=self.test_user
|
||||
)
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER = "False"
|
||||
form = UserInstanceForm(
|
||||
{
|
||||
"user": self.admin_user.id,
|
||||
"instance": self.instance.id,
|
||||
"is_change": False,
|
||||
"is_delete": False,
|
||||
"is_vnc": False,
|
||||
}
|
||||
)
|
||||
form.save()
|
||||
form = UserInstanceForm(
|
||||
{
|
||||
"user": self.test_user.id,
|
||||
"instance": self.instance.id,
|
||||
"is_change": False,
|
||||
"is_delete": False,
|
||||
"is_vnc": False,
|
||||
}
|
||||
)
|
||||
self.assertFalse(form.is_valid())
|
||||
|
|
|
@ -1,12 +1,55 @@
|
|||
from django.conf.urls import url
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.views import LoginView, LogoutView
|
||||
from django.urls import path
|
||||
from django_otp.forms import OTPAuthenticationForm
|
||||
|
||||
from . import views
|
||||
from .views import CustomLoginView
|
||||
|
||||
app_name = "accounts"
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^login/$', 'django.contrib.auth.views.login',
|
||||
{'template_name': 'login.html'}, name='login'),
|
||||
url(r'^logout/$', 'django.contrib.auth.views.logout',
|
||||
{'template_name': 'logout.html'}, name='logout'),
|
||||
url(r'^profile/$', views.profile, name='profile'),
|
||||
url(r'^$', views.accounts, name='accounts'),
|
||||
url(r'^profile/(?P<user_id>[0-9]+)/$', views.account, name='account'),
|
||||
path("logout/", LogoutView.as_view(template_name="logout.html"), name="logout"),
|
||||
path("profile/", views.profile, name="profile"),
|
||||
path("profile/<int:user_id>/", views.account, name="account"),
|
||||
path("change_password/", views.change_password, name="change_password"),
|
||||
path(
|
||||
"user_instance/create/<int:user_id>/",
|
||||
views.user_instance_create,
|
||||
name="user_instance_create",
|
||||
),
|
||||
path(
|
||||
"user_instance/<int:pk>/update/",
|
||||
views.user_instance_update,
|
||||
name="user_instance_update",
|
||||
),
|
||||
path(
|
||||
"user_instance/<int:pk>/delete/",
|
||||
views.user_instance_delete,
|
||||
name="user_instance_delete",
|
||||
),
|
||||
path("ssh_key/create/", views.ssh_key_create, name="ssh_key_create"),
|
||||
path("ssh_key/<int:pk>/delete/", views.ssh_key_delete, name="ssh_key_delete"),
|
||||
]
|
||||
|
||||
if settings.OTP_ENABLED:
|
||||
urlpatterns += [
|
||||
path(
|
||||
"login/",
|
||||
LoginView.as_view(
|
||||
template_name="accounts/otp_login.html",
|
||||
authentication_form=OTPAuthenticationForm,
|
||||
),
|
||||
name="login",
|
||||
),
|
||||
path("email_otp/", views.email_otp, name="email_otp"),
|
||||
path(
|
||||
"admin_email_otp/<int:user_id>/",
|
||||
views.admin_email_otp,
|
||||
name="admin_email_otp",
|
||||
),
|
||||
]
|
||||
else:
|
||||
urlpatterns += (
|
||||
path("login/", CustomLoginView.as_view(template_name="login.html"), name="login"),
|
||||
)
|
||||
|
|
61
accounts/utils.py
Normal file
61
accounts/utils.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
import base64
|
||||
import binascii
|
||||
import struct
|
||||
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext as _
|
||||
from django_otp import devices_for_user
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
|
||||
|
||||
def get_user_totp_device(user):
|
||||
devices = devices_for_user(user)
|
||||
for device in devices:
|
||||
if isinstance(device, TOTPDevice):
|
||||
return device
|
||||
|
||||
device = user.totpdevice_set.create()
|
||||
return device
|
||||
|
||||
|
||||
def validate_ssh_key(key):
|
||||
array = key.encode().split()
|
||||
# Each rsa-ssh key has 3 different strings in it, first one being
|
||||
# typeofkey second one being keystring third one being username .
|
||||
if len(array) != 3:
|
||||
return False
|
||||
typeofkey = array[0]
|
||||
string = array[1]
|
||||
|
||||
# must have only valid rsa-ssh key characters ie binascii characters
|
||||
try:
|
||||
data = base64.decodebytes(string)
|
||||
except binascii.Error:
|
||||
return False
|
||||
# unpack the contents of data, from data[:4] , property of ssh key .
|
||||
try:
|
||||
str_len = struct.unpack(">I", data[:4])[0]
|
||||
except struct.error:
|
||||
return False
|
||||
# data[4:str_len] must have string which matches with the typeofkey, another ssh key property.
|
||||
if data[4 : 4 + str_len] != typeofkey:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def send_email_with_otp(user, device):
|
||||
send_mail(
|
||||
_("OTP QR Code"),
|
||||
_("Please view HTML version of this message."),
|
||||
None,
|
||||
[user.email],
|
||||
html_message=render_to_string(
|
||||
"accounts/email/otp.html",
|
||||
{
|
||||
"totp_url": device.config_url,
|
||||
"user": user,
|
||||
},
|
||||
),
|
||||
fail_silently=False,
|
||||
)
|
|
@ -1,172 +1,215 @@
|
|||
from django.shortcuts import render
|
||||
from admin.decorators import superuser_only
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model, update_session_auth_hash, login as auth_login
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.forms import PasswordChangeForm
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth.models import User
|
||||
from accounts.models import UserInstance, UserSSHKey
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from instances.models import Instance
|
||||
from accounts.forms import UserAddForm
|
||||
|
||||
from accounts.forms import EmailOTPForm, ProfileForm, UserSSHKeyForm
|
||||
from accounts.models import *
|
||||
|
||||
from . import forms
|
||||
from .utils import get_user_totp_device, send_email_with_otp
|
||||
from django.contrib.auth.views import LoginView
|
||||
from logs.views import addlogmsg
|
||||
|
||||
class CustomLoginView(LoginView):
|
||||
def form_valid(self, form):
|
||||
username = form.cleaned_data['username']
|
||||
addlogmsg(username, "-", "-", "Logged In")
|
||||
auth_login(self.request, form.get_user())
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def form_invalid(self, form):
|
||||
username = form.cleaned_data['username']
|
||||
addlogmsg(username, "-", "-", "Failed Login Attempt")
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
|
||||
def profile(request):
|
||||
"""
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
if not request.user.is_authenticated():
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
error_messages = []
|
||||
user = User.objects.get(id=request.user.id)
|
||||
publickeys = UserSSHKey.objects.filter(user_id=request.user.id)
|
||||
profile_form = ProfileForm(request.POST or None, instance=request.user)
|
||||
ssh_key_form = UserSSHKeyForm()
|
||||
|
||||
if request.method == 'POST':
|
||||
if 'username' in request.POST:
|
||||
username = request.POST.get('username', '')
|
||||
email = request.POST.get('email', '')
|
||||
user.first_name = username
|
||||
user.email = email
|
||||
user.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if 'oldpasswd' in request.POST:
|
||||
oldpasswd = request.POST.get('oldpasswd', '')
|
||||
password1 = request.POST.get('passwd1', '')
|
||||
password2 = request.POST.get('passwd2', '')
|
||||
if not password1 or not password2:
|
||||
error_messages.append("Passwords didn't enter")
|
||||
if password1 and password2 and password1 != password2:
|
||||
error_messages.append("Passwords don't match")
|
||||
if not user.check_password(oldpasswd):
|
||||
error_messages.append("Old password is wrong!")
|
||||
if not error_messages:
|
||||
user.set_password(password1)
|
||||
user.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if 'keyname' in request.POST:
|
||||
keyname = request.POST.get('keyname', '')
|
||||
keypublic = request.POST.get('keypublic', '')
|
||||
for key in publickeys:
|
||||
if keyname == key.keyname:
|
||||
msg = _("Key name already exist")
|
||||
error_messages.append(msg)
|
||||
if keypublic == key.keypublic:
|
||||
msg = _("Public key already exist")
|
||||
error_messages.append(msg)
|
||||
if not error_messages:
|
||||
addkeypublic = UserSSHKey(user_id=request.user.id, keyname=keyname, keypublic=keypublic)
|
||||
addkeypublic.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if 'keydelete' in request.POST:
|
||||
keyid = request.POST.get('keyid', '')
|
||||
delkeypublic = UserSSHKey.objects.get(id=keyid)
|
||||
delkeypublic.delete()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
return render(request, 'profile.html', locals())
|
||||
if profile_form.is_valid():
|
||||
profile_form.save()
|
||||
messages.success(request, _("Profile updated"))
|
||||
return redirect("accounts:profile")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"profile.html",
|
||||
{
|
||||
"publickeys": publickeys,
|
||||
"profile_form": profile_form,
|
||||
"ssh_key_form": ssh_key_form,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def accounts(request):
|
||||
"""
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
def ssh_key_create(request):
|
||||
key_form = UserSSHKeyForm(request.POST or None, user=request.user)
|
||||
if key_form.is_valid():
|
||||
key_form.save()
|
||||
messages.success(request, _("SSH key added"))
|
||||
return redirect("accounts:profile")
|
||||
|
||||
if not request.user.is_authenticated():
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
if not request.user.is_superuser:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
error_messages = []
|
||||
users = User.objects.filter(is_staff=False, is_superuser=False)
|
||||
|
||||
if request.method == 'POST':
|
||||
if 'create' in request.POST:
|
||||
form = UserAddForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
else:
|
||||
for msg_err in form.errors.values():
|
||||
error_messages.append(msg_err.as_text())
|
||||
if not error_messages:
|
||||
new_user = User.objects.create_user(data['name'], None, data['password'])
|
||||
new_user.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if 'edit' in request.POST:
|
||||
user_id = request.POST.get('user_id', '')
|
||||
user_pass = request.POST.get('user_pass', '')
|
||||
user_edit = User.objects.get(id=user_id)
|
||||
user_edit.set_password(user_pass)
|
||||
user_edit.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if 'block' in request.POST:
|
||||
user_id = request.POST.get('user_id', '')
|
||||
user_block = User.objects.get(id=user_id)
|
||||
user_block.is_active = False
|
||||
user_block.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if 'unblock' in request.POST:
|
||||
user_id = request.POST.get('user_id', '')
|
||||
user_unblock = User.objects.get(id=user_id)
|
||||
user_unblock.is_active = True
|
||||
user_unblock.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if 'delete' in request.POST:
|
||||
user_id = request.POST.get('user_id', '')
|
||||
try:
|
||||
del_user_inst = UserInstance.objects.filter(user_id=user_id)
|
||||
del_user_inst.delete()
|
||||
finally:
|
||||
user_delete = User.objects.get(id=user_id)
|
||||
user_delete.delete()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
|
||||
return render(request, 'accounts.html', locals())
|
||||
return render(
|
||||
request,
|
||||
"common/form.html",
|
||||
{
|
||||
"form": key_form,
|
||||
"title": _("Add SSH key"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def ssh_key_delete(request, pk):
|
||||
ssh_key = get_object_or_404(UserSSHKey, pk=pk, user=request.user)
|
||||
if request.method == "POST":
|
||||
ssh_key.delete()
|
||||
messages.success(request, _("SSH key deleted"))
|
||||
return redirect("accounts:profile")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/confirm_delete.html",
|
||||
{
|
||||
"object": ssh_key,
|
||||
"title": _("Delete SSH key"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def account(request, user_id):
|
||||
"""
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
|
||||
if not request.user.is_authenticated():
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
if not request.user.is_superuser:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
error_messages = []
|
||||
user = User.objects.get(id=user_id)
|
||||
user_insts = UserInstance.objects.filter(user_id=user_id)
|
||||
instances = Instance.objects.all()
|
||||
instances = Instance.objects.all().order_by("name")
|
||||
publickeys = UserSSHKey.objects.filter(user_id=user_id)
|
||||
|
||||
if user.username == request.user.username:
|
||||
return HttpResponseRedirect(reverse('profile'))
|
||||
return render(
|
||||
request,
|
||||
"account.html",
|
||||
{
|
||||
"user": user,
|
||||
"user_insts": user_insts,
|
||||
"instances": instances,
|
||||
"publickeys": publickeys,
|
||||
"otp_enabled": settings.OTP_ENABLED,
|
||||
},
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
if 'delete' in request.POST:
|
||||
user_inst = request.POST.get('user_inst', '')
|
||||
del_user_inst = UserInstance.objects.get(id=user_inst)
|
||||
del_user_inst.delete()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if 'permission' in request.POST:
|
||||
user_inst = request.POST.get('user_inst', '')
|
||||
inst_change = request.POST.get('inst_change', '')
|
||||
inst_delete = request.POST.get('inst_delete', '')
|
||||
edit_user_inst = UserInstance.objects.get(id=user_inst)
|
||||
edit_user_inst.is_change = bool(inst_change)
|
||||
edit_user_inst.is_delete = bool(inst_delete)
|
||||
edit_user_inst.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if 'add' in request.POST:
|
||||
inst_id = request.POST.get('inst_id', '')
|
||||
try:
|
||||
check_inst = UserInstance.objects.get(instance_id=int(inst_id))
|
||||
msg = _("Instance already added")
|
||||
error_messages.append(msg)
|
||||
except UserInstance.DoesNotExist:
|
||||
add_user_inst = UserInstance(instance_id=int(inst_id), user_id=user_id)
|
||||
add_user_inst.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
|
||||
return render(request, 'account.html', locals())
|
||||
@permission_required("accounts.change_password", raise_exception=True)
|
||||
def change_password(request):
|
||||
form = PasswordChangeForm(request.user, request.POST or None)
|
||||
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
update_session_auth_hash(request, user) # Important!
|
||||
messages.success(request, _("Password Changed"))
|
||||
return redirect("accounts:profile")
|
||||
|
||||
return render(request, "accounts/change_password_form.html", {"form": form})
|
||||
|
||||
|
||||
@superuser_only
|
||||
def user_instance_create(request, user_id):
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
|
||||
form = forms.UserInstanceForm(request.POST or None, initial={"user": user})
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect(reverse("accounts:account", args=[user.id]))
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/form.html",
|
||||
{
|
||||
"form": form,
|
||||
"title": _("Create User Instance"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def user_instance_update(request, pk):
|
||||
user_instance = get_object_or_404(UserInstance, pk=pk)
|
||||
form = forms.UserInstanceForm(request.POST or None, instance=user_instance)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect(reverse("accounts:account", args=[user_instance.user.id]))
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/form.html",
|
||||
{
|
||||
"form": form,
|
||||
"title": _("Update User Instance"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def user_instance_delete(request, pk):
|
||||
user_instance = get_object_or_404(UserInstance, pk=pk)
|
||||
if request.method == "POST":
|
||||
user = user_instance.user
|
||||
user_instance.delete()
|
||||
next = request.GET.get("next", None)
|
||||
if next:
|
||||
return redirect(next)
|
||||
else:
|
||||
return redirect(reverse("accounts:account", args=[user.id]))
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/confirm_delete.html",
|
||||
{"object": user_instance},
|
||||
)
|
||||
|
||||
|
||||
def email_otp(request):
|
||||
form = EmailOTPForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
UserModel = get_user_model()
|
||||
try:
|
||||
user = UserModel.objects.get(email=form.cleaned_data["email"])
|
||||
except UserModel.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
device = get_user_totp_device(user)
|
||||
send_email_with_otp(user, device)
|
||||
|
||||
messages.success(
|
||||
request, _("OTP Sent to %(email)s") % {"email": form.cleaned_data["email"]}
|
||||
)
|
||||
return redirect("accounts:login")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"accounts/email_otp_form.html",
|
||||
{
|
||||
"form": form,
|
||||
"title": _("Email OTP"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def admin_email_otp(request, user_id):
|
||||
user = get_object_or_404(get_user_model(), pk=user_id)
|
||||
device = get_user_totp_device(user)
|
||||
if user.email != "":
|
||||
send_email_with_otp(user, device)
|
||||
messages.success(
|
||||
request, _("OTP QR code was emailed to user %(user)s") % {"user": user}
|
||||
)
|
||||
else:
|
||||
messages.error(request, _("User email not set, failed to send QR code"))
|
||||
return redirect("accounts:account", user.id)
|
||||
|
|
1
admin/__init__.py
Normal file
1
admin/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
default_app_config = 'admin.apps.AdminConfig'
|
5
admin/apps.py
Normal file
5
admin/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AdminConfig(AppConfig):
|
||||
name = "admin"
|
10
admin/decorators.py
Normal file
10
admin/decorators.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
|
||||
def superuser_only(function):
|
||||
def _inner(request, *args, **kwargs):
|
||||
if not request.user.is_superuser:
|
||||
raise PermissionDenied
|
||||
return function(request, *args, **kwargs)
|
||||
|
||||
return _inner
|
117
admin/forms.py
Normal file
117
admin/forms.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.models import UserAttributes
|
||||
|
||||
from .models import Permission
|
||||
|
||||
|
||||
class GroupForm(forms.ModelForm):
|
||||
permissions = forms.ModelMultipleChoiceField(
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
queryset=Permission.objects.filter(content_type__model="permissionset"),
|
||||
required=False,
|
||||
)
|
||||
|
||||
users = forms.ModelMultipleChoiceField(
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(GroupForm, self).__init__(*args, **kwargs)
|
||||
instance = getattr(self, "instance", None)
|
||||
if instance and instance.id:
|
||||
self.fields["users"].initial = self.instance.user_set.all()
|
||||
|
||||
def save_m2m(self):
|
||||
self.instance.user_set.set(self.cleaned_data["users"])
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super(GroupForm, self).save()
|
||||
self.save_m2m()
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class UserForm(forms.ModelForm):
|
||||
user_permissions = forms.ModelMultipleChoiceField(
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
queryset=Permission.objects.filter(content_type__model="permissionset"),
|
||||
label=_("Permissions"),
|
||||
required=False,
|
||||
)
|
||||
|
||||
groups = forms.ModelMultipleChoiceField(
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
queryset=Group.objects.all(),
|
||||
label=_("Groups"),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
"username",
|
||||
"groups",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"user_permissions",
|
||||
"is_staff",
|
||||
"is_active",
|
||||
"is_superuser",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UserForm, self).__init__(*args, **kwargs)
|
||||
if self.instance.id:
|
||||
password = ReadOnlyPasswordHashField(
|
||||
label=_("Password"),
|
||||
help_text=format_lazy(
|
||||
_(
|
||||
"""Raw passwords are not stored, so there is no way to see this user's password,
|
||||
but you can change the password using <a href='{}'>this form</a>."""
|
||||
),
|
||||
reverse_lazy(
|
||||
"admin:user_update_password",
|
||||
args=[
|
||||
self.instance.id,
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
self.fields["Password"] = password
|
||||
|
||||
|
||||
class UserCreateForm(UserForm):
|
||||
password = forms.CharField(widget=forms.PasswordInput)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
"username",
|
||||
"password",
|
||||
"groups",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"user_permissions",
|
||||
"is_staff",
|
||||
"is_active",
|
||||
"is_superuser",
|
||||
]
|
||||
|
||||
|
||||
class UserAttributesForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserAttributes
|
||||
exclude = ["user", "can_clone_instances"]
|
29
admin/migrations/0001_initial.py
Normal file
29
admin/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 2.2.12 on 2020-05-27 07:01
|
||||
|
||||
import django.contrib.auth.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Permission',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('auth.permission',),
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.PermissionManager()),
|
||||
],
|
||||
),
|
||||
]
|
14
admin/migrations/0002_auto_20200609_0830.py
Normal file
14
admin/migrations/0002_auto_20200609_0830.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.12 on 2020-06-09 08:30
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0001_initial'),
|
||||
('auth', '0011_update_proxy_permissions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
15
admin/migrations/0003_create_group_technicians.py
Normal file
15
admin/migrations/0003_create_group_technicians.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from django.db import models, migrations
|
||||
|
||||
def apply_migration(apps, schema_editor):
|
||||
Group = apps.get_model('auth', 'Group')
|
||||
Group.objects.create(name='Technicians')
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0002_auto_20200609_0830'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(apply_migration)
|
||||
]
|
13
admin/models.py
Normal file
13
admin/models.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.contrib.auth.models import Permission as P
|
||||
|
||||
|
||||
class Permission(P):
|
||||
"""
|
||||
Proxy model to Django Permissions model allows us to override __str__
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.content_type.app_label}: {self.name}"
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
59
admin/templates/admin/group_list.html
Normal file
59
admin/templates/admin/group_list.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap_icons %}
|
||||
{% block title %}{% trans "Users" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<a href="{% url 'admin:group_create' %}" class="btn btn-success btn-header float-end">
|
||||
{% bs_icon 'plus-circle-fill' %}
|
||||
</a>
|
||||
{% include 'search_block.html' %}
|
||||
<h1 class="page-header">{% trans "Groups" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if not groups %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning shadow-sm">
|
||||
{% bs_icon 'exclamation-triangle '%} <strong>{% trans "Warning" %}:</strong> {% trans "You don't have any groups" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th span="col">{% trans "Group Name" %}</th>
|
||||
<th span="col">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href=""><strong>{{ group.name }}</strong></a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="float-end btn-group">
|
||||
<a class="btn btn-primary" href="{% url 'admin:group_update' group.id %}" title="{%trans "Edit" %}">
|
||||
{% bs_icon 'pencil' %}
|
||||
</a>
|
||||
<a class="btn btn-danger" href="{% url 'admin:group_delete' group.id %}" title="{%trans "Delete" %}">
|
||||
{% bs_icon 'x' %}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block script %}
|
||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
||||
{% endblock script %}
|
59
admin/templates/admin/logs.html
Normal file
59
admin/templates/admin/logs.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load bootstrap_icons %}
|
||||
|
||||
{% block title %}{% trans "Logs" %}{% endblock %}
|
||||
|
||||
{% block page_heading %}{% trans "Logs" %}{% endblock page_heading %}
|
||||
|
||||
{% block page_heading_extra %}
|
||||
{% include 'search_block.html' %}
|
||||
{% endblock page_heading_extra %}
|
||||
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% if not logs %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning shadow-sm">
|
||||
{% bs_icon 'exclamation-triangle'%} <strong>{% trans "Warning" %}:</strong> {% trans "You don't have any Logs" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">{% trans "Date" %}</th>
|
||||
<th scope="col">{% trans "User" %}</th>
|
||||
<th scope="col">{% trans "Host" %}</th>
|
||||
<th scope="col">{% trans "Instance" %}</th>
|
||||
<th scope="col">{% trans "Message" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td>{{ log.id }}</td>
|
||||
<td style="width:130px;">{{ log.date|date:"M d H:i:s" }}</td>
|
||||
<td>{{ log.user }}</td>
|
||||
<td>{{ log.host }}</td>
|
||||
<td>{{ log.instance }}</td>
|
||||
<td>{{ log.message }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% bootstrap_pagination logs %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script src="/static/js/filter-table.js"></script>
|
||||
{% endblock script %}
|
27
admin/templates/admin/user_form.html
Normal file
27
admin/templates/admin/user_form.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
{% extends "base.html" %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load bootstrap_icons %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block page_heading %}{{ title }}{% endblock page_heading %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card col-sm-10 offset-1">
|
||||
<div class="card-body">
|
||||
<form id="create-update" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form user_form layout='horizontal' %}
|
||||
{% bootstrap_form attributes_form layout='horizontal' %}
|
||||
</form>
|
||||
<div class="float-end">
|
||||
<a class="btn btn-primary" href="javascript:history.back()">
|
||||
{% bs_icon 'x-square-fill' %} {% trans "Cancel" %}</a>
|
||||
<button type="submit" form="create-update" class="btn btn-success">
|
||||
{% bs_icon 'check2' %} {% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
81
admin/templates/admin/user_list.html
Normal file
81
admin/templates/admin/user_list.html
Normal file
|
@ -0,0 +1,81 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load common_tags %}
|
||||
{% load bootstrap_icons %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block page_heading %}{{ title }}{% endblock page_heading %}
|
||||
|
||||
{% block page_heading_extra %}
|
||||
<a href="{% url 'admin:user_create' %}" class="btn btn-success btn-header float-end">
|
||||
{% bs_icon 'plus-circle-fill' %}
|
||||
</a>
|
||||
{% include 'search_block.html' %}
|
||||
{% endblock page_heading_extra %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% if not users %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning shadow-sm">
|
||||
{% bs_icon 'exclamation-triangle '%} <strong>{% trans "Warning" %}:</strong> {% trans "You don't have any user" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th span="col">{% trans "Username" %}</th>
|
||||
<th span="col">{% trans "Status" %}</th>
|
||||
<th span="col">{% trans "Last Login" %}</th>
|
||||
<th span="col">{% trans "Staff" %}</th>
|
||||
<th span="col">{% trans "Superuser" %}</th>
|
||||
<th span="col">{% trans "Can Clone" %}</th>
|
||||
<th span="col">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for user in users %}
|
||||
{% has_perm user 'instances.clone_instances' as can_clone %}
|
||||
<tr class="{% if not user.is_active %}danger{% endif %}">
|
||||
<td>
|
||||
{{ user.username }}
|
||||
</td>
|
||||
<td>
|
||||
{% if user.is_active %}
|
||||
{% trans "Active" %}
|
||||
{% else %}
|
||||
{% trans "Blocked" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.last_login }}</td>
|
||||
<td>{% if user.is_staff %}{% bs_icon 'check' %}{% endif %}</td>
|
||||
<td>{% if user.is_superuser %}{% bs_icon 'check' %}</span>{% endif %}</td>
|
||||
<td>{% if can_clone %}{% bs_icon 'check' %}{% endif %}</td>
|
||||
<td>
|
||||
<div class="float-end btn-group">
|
||||
<a class="btn btn-success" title="{%trans "View Profile" %}" href="{% url 'accounts:account' user.id %}">{% bs_icon 'eye-fill' %}</a>
|
||||
<a class="btn btn-primary" title="{%trans "Edit" %}" href="{% url 'admin:user_update' user.id %}">{% bs_icon 'pencil-fill' %}</a>
|
||||
{% if user.is_active %}
|
||||
<a class="btn btn-warning" title="{%trans "Block" %}" href="{% url 'admin:user_block' user.id %}">{% bs_icon 'stop-fill' %}</a>
|
||||
{% else %}
|
||||
<a class="btn btn-success" title="{%trans "Unblock" %}" href="{% url 'admin:user_unblock' user.id %}">{% bs_icon 'play-fill' %}</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-danger" title="{%trans "Delete" %}" href="{% url 'admin:user_delete' user.id %}">{% bs_icon 'x-circle-fill' %}</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block script %}
|
||||
<script src="{% static "js/filter-table.js" %}"></script>
|
||||
{% endblock script %}
|
124
admin/tests.py
Normal file
124
admin/tests.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
from django.contrib.auth.models import Group, User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from accounts.models import UserAttributes
|
||||
|
||||
|
||||
class AdminTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.client.login(username="admin", password="admin")
|
||||
|
||||
def test_group_list(self):
|
||||
response = self.client.get(reverse("admin:group_list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_groups(self):
|
||||
response = self.client.get(reverse("admin:group_create"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("admin:group_create"), {"name": "Test Group"}
|
||||
)
|
||||
self.assertRedirects(response, reverse("admin:group_list"))
|
||||
|
||||
group = Group.objects.get(name="Test Group")
|
||||
self.assertEqual(group.id, 1)
|
||||
|
||||
response = self.client.get(reverse("admin:group_update", args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("admin:group_update", args=[1]), {"name": "Updated Group Test"}
|
||||
)
|
||||
self.assertRedirects(response, reverse("admin:group_list"))
|
||||
|
||||
group = Group.objects.get(id=1)
|
||||
self.assertEqual(group.name, "Updated Group Test")
|
||||
|
||||
response = self.client.get(reverse("admin:group_delete", args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("admin:group_delete", args=[1]))
|
||||
self.assertRedirects(response, reverse("admin:group_list"))
|
||||
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
Group.objects.get(id=1)
|
||||
|
||||
def test_user_list(self):
|
||||
response = self.client.get(reverse("admin:user_list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_users(self):
|
||||
response = self.client.get(reverse("admin:user_create"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("admin:user_create"),
|
||||
{
|
||||
"username": "test",
|
||||
"password": "test",
|
||||
"max_instances": 1,
|
||||
"max_cpus": 1,
|
||||
"max_memory": 1024,
|
||||
"max_disk_size": 4,
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, reverse("admin:user_list"))
|
||||
|
||||
user = User.objects.get(username="test")
|
||||
self.assertEqual(user.id, 2)
|
||||
|
||||
ua: UserAttributes = UserAttributes.objects.get(id=2)
|
||||
self.assertEqual(ua.user_id, 2)
|
||||
self.assertEqual(ua.max_instances, 1)
|
||||
self.assertEqual(ua.max_cpus, 1)
|
||||
self.assertEqual(ua.max_memory, 1024)
|
||||
self.assertEqual(ua.max_disk_size, 4)
|
||||
|
||||
response = self.client.get(reverse("admin:user_update", args=[2]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("admin:user_update", args=[2]),
|
||||
{
|
||||
"username": "utest",
|
||||
"max_instances": 2,
|
||||
"max_cpus": 2,
|
||||
"max_memory": 2048,
|
||||
"max_disk_size": 8,
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, reverse("admin:user_list"))
|
||||
|
||||
user = User.objects.get(id=2)
|
||||
self.assertEqual(user.username, "utest")
|
||||
|
||||
ua: UserAttributes = UserAttributes.objects.get(id=2)
|
||||
self.assertEqual(ua.user_id, 2)
|
||||
self.assertEqual(ua.max_instances, 2)
|
||||
self.assertEqual(ua.max_cpus, 2)
|
||||
self.assertEqual(ua.max_memory, 2048)
|
||||
self.assertEqual(ua.max_disk_size, 8)
|
||||
|
||||
response = self.client.get(reverse("admin:user_block", args=[2]))
|
||||
user = User.objects.get(id=2)
|
||||
self.assertFalse(user.is_active)
|
||||
|
||||
response = self.client.get(reverse("admin:user_unblock", args=[2]))
|
||||
user = User.objects.get(id=2)
|
||||
self.assertTrue(user.is_active)
|
||||
|
||||
response = self.client.get(reverse("admin:user_delete", args=[2]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("admin:user_delete", args=[2]))
|
||||
self.assertRedirects(response, reverse("admin:user_list"))
|
||||
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
User.objects.get(id=2)
|
||||
|
||||
def test_logs(self):
|
||||
response = self.client.get(reverse("admin:logs"))
|
||||
self.assertEqual(response.status_code, 200)
|
18
admin/urls.py
Normal file
18
admin/urls.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("groups/", views.group_list, name="group_list"),
|
||||
path("groups/create/", views.group_create, name="group_create"),
|
||||
path("groups/<int:pk>/update/", views.group_update, name="group_update"),
|
||||
path("groups/<int:pk>/delete/", views.group_delete, name="group_delete"),
|
||||
path("users/", views.user_list, name="user_list"),
|
||||
path("users/create/", views.user_create, name="user_create"),
|
||||
path("users/<int:pk>/update_password/", views.user_update_password, name="user_update_password"),
|
||||
path("users/<int:pk>/update/", views.user_update, name="user_update"),
|
||||
path("users/<int:pk>/delete/", views.user_delete, name="user_delete"),
|
||||
path("users/<int:pk>/block/", views.user_block, name="user_block"),
|
||||
path("users/<int:pk>/unblock/", views.user_unblock, name="user_unblock"),
|
||||
path("logs/", views.logs, name="logs"),
|
||||
]
|
217
admin/views.py
Normal file
217
admin/views.py
Normal file
|
@ -0,0 +1,217 @@
|
|||
from accounts.models import Instance, UserAttributes, UserInstance
|
||||
from appsettings.settings import app_settings
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.forms import AdminPasswordChangeForm
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.core.paginator import Paginator
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from logs.models import Logs
|
||||
|
||||
from . import forms
|
||||
from .decorators import superuser_only
|
||||
|
||||
|
||||
@superuser_only
|
||||
def group_list(request):
|
||||
groups = Group.objects.all()
|
||||
return render(
|
||||
request,
|
||||
"admin/group_list.html",
|
||||
{
|
||||
"groups": groups,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def group_create(request):
|
||||
form = forms.GroupForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("admin:group_list")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/form.html",
|
||||
{
|
||||
"form": form,
|
||||
"title": _("Create Group"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def group_update(request, pk):
|
||||
group = get_object_or_404(Group, pk=pk)
|
||||
form = forms.GroupForm(request.POST or None, instance=group)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("admin:group_list")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/form.html",
|
||||
{
|
||||
"form": form,
|
||||
"title": _("Update Group"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def group_delete(request, pk):
|
||||
group = get_object_or_404(Group, pk=pk)
|
||||
if request.method == "POST":
|
||||
group.delete()
|
||||
return redirect("admin:group_list")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/confirm_delete.html",
|
||||
{"object": group},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def user_list(request):
|
||||
users = User.objects.all()
|
||||
return render(
|
||||
request,
|
||||
"admin/user_list.html",
|
||||
{
|
||||
"users": users,
|
||||
"title": _("Users"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def user_create(request):
|
||||
user_form = forms.UserCreateForm(request.POST or None)
|
||||
attributes_form = forms.UserAttributesForm(request.POST or None)
|
||||
if user_form.is_valid() and attributes_form.is_valid():
|
||||
user = user_form.save()
|
||||
password = user_form.cleaned_data["password"]
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
attributes = attributes_form.save(commit=False)
|
||||
attributes.user = user
|
||||
attributes.save()
|
||||
add_default_instances(user)
|
||||
return redirect("admin:user_list")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"admin/user_form.html",
|
||||
{
|
||||
"user_form": user_form,
|
||||
"attributes_form": attributes_form,
|
||||
"title": _("Create User"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def user_update(request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
attributes, attributes_created = UserAttributes.objects.get_or_create(user=user)
|
||||
user_form = forms.UserForm(request.POST or None, instance=user)
|
||||
attributes_form = forms.UserAttributesForm(
|
||||
request.POST or None, instance=attributes
|
||||
)
|
||||
if user_form.is_valid() and attributes_form.is_valid():
|
||||
user_form.save()
|
||||
attributes_form.save()
|
||||
next = request.GET.get("next")
|
||||
return redirect(next or "admin:user_list")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"admin/user_form.html",
|
||||
{
|
||||
"user_form": user_form,
|
||||
"attributes_form": attributes_form,
|
||||
"title": _("Update User"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def user_update_password(request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = AdminPasswordChangeForm(user, request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
update_session_auth_hash(request, user) # Important!
|
||||
messages.success(
|
||||
request, _("Password changed for %(user)s") % {"user": user.username}
|
||||
)
|
||||
return redirect("admin:user_list")
|
||||
else:
|
||||
messages.error(request, _("Wrong Data Provided"))
|
||||
else:
|
||||
form = AdminPasswordChangeForm(user)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"accounts/change_password_form.html",
|
||||
{
|
||||
"form": form,
|
||||
"user": user.username,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def user_delete(request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
if request.method == "POST":
|
||||
user.delete()
|
||||
return redirect("admin:user_list")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/confirm_delete.html",
|
||||
{"object": user},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def user_block(request, pk):
|
||||
user: User = get_object_or_404(User, pk=pk)
|
||||
user.is_active = False
|
||||
user.save()
|
||||
return redirect("admin:user_list")
|
||||
|
||||
|
||||
@superuser_only
|
||||
def user_unblock(request, pk):
|
||||
user: User = get_object_or_404(User, pk=pk)
|
||||
user.is_active = True
|
||||
user.save()
|
||||
return redirect("admin:user_list")
|
||||
|
||||
|
||||
@superuser_only
|
||||
def logs(request):
|
||||
l = Logs.objects.order_by("-date")
|
||||
paginator = Paginator(l, int(app_settings.LOGS_PER_PAGE))
|
||||
page = request.GET.get("page", 1)
|
||||
logs = paginator.page(page)
|
||||
return render(request, "admin/logs.html", {"logs": logs})
|
||||
|
||||
|
||||
def add_default_instances(user):
|
||||
"""
|
||||
Adds instances listed in NEW_USER_DEFAULT_INSTANCES to user
|
||||
"""
|
||||
existing_instances = UserInstance.objects.filter(user=user)
|
||||
if not existing_instances:
|
||||
for instance_name in settings.NEW_USER_DEFAULT_INSTANCES:
|
||||
instance = Instance.objects.get(name=instance_name)
|
||||
user_instance = UserInstance(user=user, instance=instance)
|
||||
user_instance.save()
|
5
appsettings/apps.py
Normal file
5
appsettings/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AppsettingsConfig(AppConfig):
|
||||
name = "appsettings"
|
13
appsettings/context_processors.py
Normal file
13
appsettings/context_processors.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from .settings import app_settings as settings
|
||||
|
||||
|
||||
def app_settings(request):
|
||||
"""
|
||||
Simple context processor that puts the config into every\
|
||||
RequestContext. Just make sure you have a setting like this::
|
||||
TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
# ...
|
||||
'appsettings.context_processors.app_settings',
|
||||
)
|
||||
"""
|
||||
return {"app_settings": settings}
|
10
appsettings/middleware.py
Normal file
10
appsettings/middleware.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from .settings import app_settings, get_settings
|
||||
|
||||
|
||||
class AppSettingsMiddleware(object):
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
get_settings()
|
||||
return self.get_response(request)
|
25
appsettings/migrations/0001_initial.py
Normal file
25
appsettings/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 2.2.12 on 2020-05-27 16:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AppSettings',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=25)),
|
||||
('key', models.CharField(db_index=True, max_length=50, unique=True)),
|
||||
('value', models.CharField(max_length=25)),
|
||||
('choices', models.CharField(max_length=70)),
|
||||
('description', models.CharField(max_length=100, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
79
appsettings/migrations/0002_auto_20200527_1603.py
Normal file
79
appsettings/migrations/0002_auto_20200527_1603.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
# Generated by Django 2.2.12 on 2020-05-23 12:05
|
||||
|
||||
from django.db import migrations
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def add_default_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).bulk_create([
|
||||
setting(1, _("Theme"), "BOOTSTRAP_THEME", "flaty", "", _("Bootstrap CSS & Bootswatch Theme")),
|
||||
setting(2, _("Theme SASS Path"), "SASS_DIR", "dev/scss/", "", _("Bootstrap SASS & Bootswatch SASS Directory")),
|
||||
setting(3, _("All Instances View Style"), "VIEW_INSTANCES_LIST_STYLE", "grouped", "grouped,nongrouped", _("All instances list style")),
|
||||
setting(4, _("Logs per Page"), "LOGS_PER_PAGE", "100", "", _("Pagination for logs")),
|
||||
setting(5, _("Multiple Owner for VM"), "ALLOW_INSTANCE_MULTIPLE_OWNER", "True", "True,False", _("Allow to have multiple owner for instance")),
|
||||
setting(6, _("Quota Debug"), "QUOTA_DEBUG", "True", "True,False", _("Debug for user quotas")),
|
||||
setting(7, _("Disk Format"), "INSTANCE_VOLUME_DEFAULT_FORMAT", "qcow2", "raw,qcow,qcow2", _("Instance disk format")),
|
||||
setting(8, _("Disk Bus"), "INSTANCE_VOLUME_DEFAULT_BUS", "virtio", "virtio,scsi,ide,usb,sata", _("Instance disk bus type")),
|
||||
setting(9, _("Disk SCSI Controller"), "INSTANCE_VOLUME_DEFAULT_SCSI_CONTROLLER", "virtio-scsi", "virtio-scsi, lsilogic, virtio-blk", _("SCSI controller type")),
|
||||
setting(10, _("Disk Cache"), "INSTANCE_VOLUME_DEFAULT_CACHE", "directsync", "default,directsync,none,unsafe,writeback,writethrough", _("Disk volume cache type")),
|
||||
setting(11, _("Disk IO Type"), "INSTANCE_VOLUME_DEFAULT_IO", "default", "default,native,threads", _("Volume io modes")),
|
||||
setting(12, _("Disk Detect Zeroes"), "INSTANCE_VOLUME_DEFAULT_DETECT_ZEROES", "default", "default,on,off,unmap", _("Volume detect zeroes mode")),
|
||||
setting(13, _("Disk Discard"), "INSTANCE_VOLUME_DEFAULT_DISCARD", "default", "default,unmap,ignore", _("Volume discard mode")),
|
||||
setting(14, _("Disk Owner UID"), "INSTANCE_VOLUME_DEFAULT_OWNER_UID", "0", "", _("Owner UID: up to os, 0=root, 107=qemu or libvirt-bin(for ubuntu)")),
|
||||
setting(15, _("Disk Owner GID"), "INSTANCE_VOLUME_DEFAULT_OWNER_GID", "0", "", _("Owner GID: up to os, 0=root, 107=qemu or libvirt-bin(for ubuntu)")),
|
||||
setting(16, _("VM CPU Mode"), "INSTANCE_CPU_DEFAULT_MODE", "host-model", "no-model,host-model,host-passthrough,custom", _("Cpu modes")),
|
||||
setting(17, _("VM Machine Type"), "INSTANCE_MACHINE_DEFAULT_TYPE", "q35", "q35,x86_64", _("Chipset/Machine type")),
|
||||
setting(18, _("VM Firmware Type"), "INSTANCE_FIRMWARE_DEFAULT_TYPE", "BIOS", "BIOS,UEFI", _("Firmware type for x86_64")),
|
||||
setting(19, _("VM Architecture Type"), "INSTANCE_ARCH_DEFAULT_TYPE", "x86_64", "x86_64,i686", _("Architecture type: x86_64, i686, etc")),
|
||||
setting(20, _("VM Console Type"), "QEMU_CONSOLE_DEFAULT_TYPE", "vnc", "vnc,spice", _("Default console type")),
|
||||
setting(21, _("VM Clone Name Prefix"), "CLONE_INSTANCE_DEFAULT_PREFIX", "instance", "True,False", _("Prefix for cloned instance name")),
|
||||
setting(22, _("VM Clone Auto Name"), "CLONE_INSTANCE_AUTO_NAME", "False", "True,False", _("Generated name for cloned instance")),
|
||||
setting(23, _("VM Clone Auto Migrate"), "CLONE_INSTANCE_AUTO_MIGRATE", "False", "True,False", _("Auto migrate instance after clone")),
|
||||
setting(24, _("VM Bottom Bar"), "VIEW_INSTANCE_DETAIL_BOTTOM_BAR", "True", "True,False", _("Bottom navbar for instance details")),
|
||||
setting(25, _("Show Access Root Pass"), "SHOW_ACCESS_ROOT_PASSWORD", "False", "True,False", _("Show access root password")),
|
||||
setting(26, _("Show Access SSH Keys"), "SHOW_ACCESS_SSH_KEYS", "False", "True,False", _("Show access ssh keys")),
|
||||
])
|
||||
|
||||
|
||||
def del_default_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).filter(key="QEMU_CONSOLE_DEFAULT_TYPE").delete()
|
||||
setting.objects.using(db_alias).filter(key="ALLOW_INSTANCE_MULTIPLE_OWNER").delete()
|
||||
setting.objects.using(db_alias).filter(key="CLONE_INSTANCE_DEFAULT_PREFIX").delete()
|
||||
setting.objects.using(db_alias).filter(key="CLONE_INSTANCE_AUTO_NAME").delete()
|
||||
setting.objects.using(db_alias).filter(key="CLONE_INSTANCE_AUTO_MIGRATE").delete()
|
||||
setting.objects.using(db_alias).filter(key="LOGS_PER_PAGE").delete()
|
||||
setting.objects.using(db_alias).filter(key="QUOTA_DEBUG").delete()
|
||||
setting.objects.using(db_alias).filter(key="VIEW_INSTANCES_LIST_STYLE").delete()
|
||||
setting.objects.using(db_alias).filter(key="VIEW_INSTANCE_DETAIL_BOTTOM_BAR").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_FORMAT").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_BUS").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_SCSI_CONTROLLER").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_CACHE").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_IO").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_DETECT_ZEROES").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_DISCARD").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_OWNER_UID").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_OWNER_GID").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_CPU_DEFAULT_MODE").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_MACHINE_DEFAULT_TYPE").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_FIRMWARE_DEFAULT_TYPE").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_ARCH_DEFAULT_TYPE").delete()
|
||||
setting.objects.using(db_alias).filter(key="BOOTSTRAP_THEME").delete()
|
||||
setting.objects.using(db_alias).filter(key="SASS_DIR").delete()
|
||||
setting.objects.using(db_alias).filter(key="SHOW_ACCESS_ROOT_PASSWORD").delete()
|
||||
setting.objects.using(db_alias).filter(key="SHOW_ACCESS_SSH_KEYS").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('appsettings', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_default_settings, del_default_settings),
|
||||
]
|
38
appsettings/migrations/0003_auto_20200615_0637.py
Normal file
38
appsettings/migrations/0003_auto_20200615_0637.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 2.2.13 on 2020-06-15 06:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('appsettings', '0002_auto_20200527_1603'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='appsettings',
|
||||
name='choices',
|
||||
field=models.CharField(max_length=70, verbose_name='choices'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='appsettings',
|
||||
name='description',
|
||||
field=models.CharField(max_length=100, null=True, verbose_name='description'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='appsettings',
|
||||
name='key',
|
||||
field=models.CharField(db_index=True, max_length=50, unique=True, verbose_name='key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='appsettings',
|
||||
name='name',
|
||||
field=models.CharField(max_length=25, verbose_name='name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='appsettings',
|
||||
name='value',
|
||||
field=models.CharField(max_length=25, verbose_name='value'),
|
||||
),
|
||||
]
|
35
appsettings/migrations/0004_auto_20200716_0637.py
Normal file
35
appsettings/migrations/0004_auto_20200716_0637.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
# Generated by Django 2.2.13 on 2020-07-16 06:37
|
||||
|
||||
from django.db import migrations
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def add_default_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).bulk_create([
|
||||
setting(27, _("Console Scale"), "CONSOLE_SCALE", "False", "True,False", _("Allow console to scaling view")),
|
||||
setting(28, _("Console View-Only"), "CONSOLE_VIEW_ONLY", "False", "True,False", _("Allow only view not modify")),
|
||||
setting(29, _("Console Resize Session"), "CONSOLE_RESIZE_SESSION", "False", "True,False", _("Allow to resize session for console")),
|
||||
setting(30, _("Console Clip Viewport"), "CONSOLE_CLIP_VIEWPORT", "False", "True,False", _("Clip console viewport")),
|
||||
])
|
||||
|
||||
|
||||
def del_default_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).filter(key="CONSOLE_SCALE").delete()
|
||||
setting.objects.using(db_alias).filter(key="CONSOLE_VIEW_ONLY").delete()
|
||||
setting.objects.using(db_alias).filter(key="CONSOLE_RESIZE_SESSION").delete()
|
||||
setting.objects.using(db_alias).filter(key="CONSOLE_CLIP_VIEWPORT").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('appsettings', '0003_auto_20200615_0637'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_default_settings, del_default_settings),
|
||||
]
|
18
appsettings/migrations/0005_auto_20200911_1233.py
Normal file
18
appsettings/migrations/0005_auto_20200911_1233.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.14 on 2020-09-11 12:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('appsettings', '0004_auto_20200716_0637'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='appsettings',
|
||||
name='choices',
|
||||
field=models.CharField(max_length=70, verbose_name='choices'),
|
||||
),
|
||||
]
|
28
appsettings/migrations/0006_auto_20220630_0717.py
Normal file
28
appsettings/migrations/0006_auto_20220630_0717.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 3.2.13 on 2022-06-30 07:17
|
||||
|
||||
from django.db import migrations
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
def add_default_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).bulk_create([
|
||||
setting(31, _("VM DRBD Status"), "VM_DRBD_STATUS", "False", "True,False", _("Show VM DRBD Status")),
|
||||
])
|
||||
|
||||
|
||||
def del_default_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).filter(key="VM_DRBD_STATUS").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('appsettings', '0005_auto_20200911_1233'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_default_settings, del_default_settings),
|
||||
]
|
21
appsettings/migrations/0007_auto_20220905_0918.py
Normal file
21
appsettings/migrations/0007_auto_20220905_0918.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.2.13 on 2022-06-30 07:17
|
||||
|
||||
from django.db import migrations
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
def update_default_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_MACHINE_DEFAULT_TYPE").update(choices="q35,x86_64,virt"),
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_ARCH_DEFAULT_TYPE").update(choices="x86_64,i686,aarch64"),
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('appsettings', '0006_auto_20220630_0717'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_default_settings, None)
|
||||
]
|
32
appsettings/migrations/0008_auto_20220905_1459.py
Normal file
32
appsettings/migrations/0008_auto_20220905_1459.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 3.2.13 on 2022-06-30 07:17
|
||||
|
||||
from django.db import migrations
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
def add_default_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).bulk_create([
|
||||
setting(32, _("VM CD-ROM Device"), "INSTANCE_CDROM_ADD", "sata", "None,ide,sata,scsi,virtio", _("Add or not cdrom device while instance creating")),
|
||||
setting(33, _("VM Video Type"), "INSTANCE_VIDEO_DEFAULT_TYPE", "vga", "None,virtio,vga,cirrus,vmvga,bochs,ramfb", _("Change instance default video type")),
|
||||
setting(34, _("VM Input Device"), "INSTANCE_INPUT_DEFAULT_DEVICE", "default", "None,default,virtio,usb", _("Add or not input device with specify its type")),
|
||||
])
|
||||
|
||||
|
||||
def del_default_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_CDROM_ADD").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_VIDEO_DEFAULT_TYPE").delete()
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_INPUT_DEFAULT_DEVICE").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('appsettings', '0007_auto_20220905_0918'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_default_settings, del_default_settings),
|
||||
]
|
18
appsettings/migrations/0009_alter_appsettings_id.py
Normal file
18
appsettings/migrations/0009_alter_appsettings_id.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.2.5 on 2023-10-30 17:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('appsettings', '0008_auto_20220905_1459'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='appsettings',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
28
appsettings/migrations/0010_auto_20231030_1305.py
Normal file
28
appsettings/migrations/0010_auto_20231030_1305.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.2.5 on 2023-10-30 17:05
|
||||
|
||||
from django.db import migrations
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
def add_default_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).bulk_create([
|
||||
setting(35, _("VM NIC Type"), "INSTANCE_NIC_DEFAULT_TYPE", "default", "default,e1000,e1000e,rt18139,virtio", _("Change instance default NIC type"))
|
||||
])
|
||||
|
||||
|
||||
def del_default_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).filter(key="INSTANCE_NIC_DEFAULT_TYPE").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('appsettings', '0009_alter_appsettings_id')
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_default_settings,del_default_settings)
|
||||
]
|
20
appsettings/migrations/0011_alter_appsettings_id.py
Normal file
20
appsettings/migrations/0011_alter_appsettings_id.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 4.2.10 on 2024-02-14 11:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("appsettings", "0010_auto_20231030_1305"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="appsettings",
|
||||
name="id",
|
||||
field=models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
]
|
13
appsettings/models.py
Normal file
13
appsettings/models.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class AppSettings(models.Model):
|
||||
def choices_as_list(self):
|
||||
return self.choices.split(",")
|
||||
|
||||
name = models.CharField(_("name"), max_length=25, null=False)
|
||||
key = models.CharField(_("key"), db_index=True, max_length=50, unique=True)
|
||||
value = models.CharField(_("value"), max_length=25)
|
||||
choices = models.CharField(_("choices"), max_length=70)
|
||||
description = models.CharField(_("description"), max_length=100, null=True)
|
18
appsettings/settings.py
Normal file
18
appsettings/settings.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from .models import AppSettings
|
||||
|
||||
|
||||
class Settings:
|
||||
pass
|
||||
|
||||
|
||||
app_settings = Settings()
|
||||
|
||||
|
||||
def get_settings():
|
||||
try:
|
||||
entries = AppSettings.objects.all()
|
||||
except:
|
||||
pass
|
||||
|
||||
for entry in entries:
|
||||
setattr(app_settings, entry.key, entry.value)
|
74
appsettings/templates/appsettings.html
Normal file
74
appsettings/templates/appsettings.html
Normal file
|
@ -0,0 +1,74 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Edit Settings" %}{% endblock title %}
|
||||
|
||||
{% block page_heading %}{% trans "Edit Settings" %}{% endblock page_heading %}
|
||||
|
||||
{% block content %}
|
||||
<div class="">
|
||||
<div class="col-lg-12">
|
||||
<h3 class="page-header">{% trans "App Settings" %}</h3>
|
||||
<form action="{% url 'set_language' %}" method="post" style="display:inline" aria-label="Edit language.name_local settings form">{% csrf_token %}
|
||||
<div class="row mb-1">
|
||||
<input name="next" type="hidden" value="{{ redirect_to }}">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Language" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="language" class="form-control" onchange="this.form.submit()">
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
|
||||
{{ language.name_local }} ({{ language.code }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% if request.user.is_superuser %}
|
||||
<form method="post" action="" role="form" aria-label="Edit sass directory settings form">{% csrf_token %}
|
||||
<div class="row mb-1">
|
||||
<label class="col-sm-3 col-form-label">{% trans sass_dir.name %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" name="{{ sass_dir.key }}" value="{{ sass_dir.value }}" onchange="this.form.submit()" title="{% trans sass_dir.description %}"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form method="post" action="" role="form" aria-label="Edit theme settings form">{% csrf_token %}
|
||||
<div class="row mb-1">
|
||||
<label class="col-sm-3 col-form-label">{% trans bootstrap_theme.name %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select class="form-select" name="{{ bootstrap_theme.key }}" onchange="this.form.submit()" title="{% trans bootstrap_theme.description %}">
|
||||
{% for theme in themes_list %}
|
||||
<option {% if bootstrap_theme.value == theme %}selected{% endif %} value="{{ theme }}">{{ theme }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<span class="text-muted">{% trans "After change please full refresh page with 'Ctrl + F5' "%}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<h3 class="page-header">{% trans "Other Settings" %}</h3>
|
||||
{% for setting in appsettings %}
|
||||
<form method="post" action="" role="form" aria-label="{{setting.name}} form">{% csrf_token %}
|
||||
<div class="row mb-1">
|
||||
<label class="col-sm-3 col-form-label">{% trans setting.name %}</label>
|
||||
<div class="col-sm-6">
|
||||
{% if setting.choices %}
|
||||
<select class="form-select" name="{{ setting.key }}" onchange="this.form.submit()" title="{% trans setting.description %}">
|
||||
{% for choice in setting.choices_as_list %}
|
||||
<option {% if setting.value == choice %} selected {% endif %} value={{ choice }}>{% trans choice %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
<input class="form-control" name="{{ setting.key }}" value="{{ setting.value }}" title="{% trans setting.description %}" onchange="this.form.submit()"/>
|
||||
{% endif%}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
103
appsettings/views.py
Normal file
103
appsettings/views.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
import os
|
||||
|
||||
import sass
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext_noop as _
|
||||
from logs.views import addlogmsg
|
||||
|
||||
from appsettings.models import AppSettings
|
||||
|
||||
|
||||
@login_required
|
||||
def appsettings(request):
|
||||
"""
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
main_css = "wvc-main.min.css"
|
||||
sass_dir = AppSettings.objects.get(key="SASS_DIR")
|
||||
bootstrap_theme = AppSettings.objects.get(key="BOOTSTRAP_THEME")
|
||||
try:
|
||||
themes_list = os.listdir(sass_dir.value + "/wvc-themes")
|
||||
except FileNotFoundError as err:
|
||||
messages.error(request, err)
|
||||
addlogmsg(request.user.username, "-", "", err)
|
||||
|
||||
# Bootstrap settings related with filesystems, because of that they are excluded from other settings
|
||||
appsettings = AppSettings.objects.exclude(
|
||||
description__startswith="Bootstrap"
|
||||
).order_by("name")
|
||||
|
||||
if request.method == "POST":
|
||||
if "SASS_DIR" in request.POST:
|
||||
try:
|
||||
sass_dir.value = request.POST.get("SASS_DIR", "")
|
||||
sass_dir.save()
|
||||
|
||||
msg = _("SASS directory path is changed. Now: %(dir)s") % {
|
||||
"dir": sass_dir.value
|
||||
}
|
||||
messages.success(request, msg)
|
||||
except Exception as err:
|
||||
msg = err
|
||||
messages.error(request, msg)
|
||||
|
||||
addlogmsg(request.user.username, "-", "", msg)
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
|
||||
if "BOOTSTRAP_THEME" in request.POST:
|
||||
theme = request.POST.get("BOOTSTRAP_THEME", "")
|
||||
scss_var = f"@import '{sass_dir.value}/wvc-themes/{theme}/variables';"
|
||||
# scss_boot = f"@import '{sass_dir.value}/bootstrap/bootstrap.scss';"
|
||||
scss_boot = f"@import '{sass_dir.value}/bootstrap-overrides.scss';"
|
||||
scss_bootswatch = (
|
||||
f"@import '{sass_dir.value}/wvc-themes/{theme}/bootswatch';"
|
||||
)
|
||||
|
||||
try:
|
||||
with open(sass_dir.value + "/wvc-main.scss", "w") as main:
|
||||
main.write(
|
||||
scss_var + "\n" + scss_boot + "\n" + scss_bootswatch + "\n"
|
||||
)
|
||||
|
||||
css_compressed = sass.compile(
|
||||
string=scss_var + "\n" + scss_boot + "\n" + scss_bootswatch,
|
||||
output_style="compressed",
|
||||
)
|
||||
with open("static/css/" + main_css, "w") as css:
|
||||
css.write(css_compressed)
|
||||
|
||||
bootstrap_theme.value = theme
|
||||
bootstrap_theme.save()
|
||||
|
||||
msg = _("Theme is changed. Now: %(theme)s") % {"theme": theme}
|
||||
messages.success(request, msg)
|
||||
except Exception as err:
|
||||
msg = err
|
||||
messages.error(request, msg)
|
||||
|
||||
addlogmsg(request.user.username, "-", "", msg)
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
|
||||
for setting in appsettings:
|
||||
if setting.key in request.POST:
|
||||
try:
|
||||
setting.value = request.POST.get(setting.key, "")
|
||||
setting.save()
|
||||
|
||||
msg = _("%(setting)s is changed. Now: %(value)s") % {
|
||||
"setting": setting.name,
|
||||
"value": setting.value,
|
||||
}
|
||||
messages.success(request, msg)
|
||||
except Exception as err:
|
||||
msg = err
|
||||
messages.error(request, msg)
|
||||
|
||||
addlogmsg(request.user.username, "-", "", msg)
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
|
||||
return render(request, "appsettings.html", locals())
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
20
computes/api/serializers.py
Normal file
20
computes/api/serializers.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from computes.models import Compute
|
||||
from rest_framework import serializers
|
||||
from vrtManager.connection import CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS
|
||||
|
||||
|
||||
class ComputeSerializer(serializers.ModelSerializer):
|
||||
# Use <input type="password"> for the input.
|
||||
password = serializers.CharField(style={"input_type": "password"})
|
||||
# Use a radio input instead of a select input.
|
||||
conn_types = (
|
||||
(CONN_SSH, "SSH"),
|
||||
(CONN_TCP, "TCP"),
|
||||
(CONN_TLS, "TLS"),
|
||||
(CONN_SOCKET, "SOCK"),
|
||||
)
|
||||
type = serializers.ChoiceField(choices=conn_types)
|
||||
|
||||
class Meta:
|
||||
model = Compute
|
||||
fields = ["id", "name", "hostname", "login", "password", "type", "details"]
|
56
computes/api/viewsets.py
Normal file
56
computes/api/viewsets.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
from computes.models import Compute
|
||||
from rest_framework import permissions, viewsets
|
||||
from rest_framework.response import Response
|
||||
from vrtManager.create import wvmCreate
|
||||
|
||||
from .serializers import ComputeSerializer
|
||||
|
||||
|
||||
class ComputeViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows computes to be viewed or edited.
|
||||
"""
|
||||
|
||||
queryset = Compute.objects.all().order_by("name")
|
||||
serializer_class = ComputeSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class ComputeArchitecturesView(viewsets.ViewSet):
|
||||
def list(self, request, compute_pk=None):
|
||||
"""
|
||||
Return a list of supported host architectures.
|
||||
"""
|
||||
compute = Compute.objects.get(pk=compute_pk)
|
||||
conn = wvmCreate(
|
||||
compute.hostname,
|
||||
compute.login,
|
||||
compute.password,
|
||||
compute.type,
|
||||
)
|
||||
return Response(conn.get_hypervisors_machines())
|
||||
|
||||
def retrieve(self, request, compute_pk=None, pk=None):
|
||||
compute = Compute.objects.get(pk=compute_pk)
|
||||
conn = wvmCreate(
|
||||
compute.hostname,
|
||||
compute.login,
|
||||
compute.password,
|
||||
compute.type,
|
||||
)
|
||||
return Response(conn.get_machine_types(pk))
|
||||
|
||||
|
||||
class ComputeMachinesView(viewsets.ViewSet):
|
||||
def list(self, request, compute_pk=None, archs_pk=None):
|
||||
"""
|
||||
Return a list of supported host architectures.
|
||||
"""
|
||||
compute = Compute.objects.get(pk=compute_pk)
|
||||
conn = wvmCreate(
|
||||
compute.hostname,
|
||||
compute.login,
|
||||
compute.password,
|
||||
compute.type,
|
||||
)
|
||||
return Response(conn.get_machine_types(archs_pk))
|
|
@ -1,164 +1,45 @@
|
|||
import re
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from vrtManager.connection import CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS
|
||||
|
||||
from computes.models import Compute
|
||||
|
||||
|
||||
class ComputeAddTcpForm(forms.Form):
|
||||
name = forms.CharField(error_messages={'required': _('No hostname has been entered')},
|
||||
max_length=20)
|
||||
hostname = forms.CharField(error_messages={'required': _('No IP / Domain name has been entered')},
|
||||
max_length=100)
|
||||
login = forms.CharField(error_messages={'required': _('No login has been entered')},
|
||||
max_length=100)
|
||||
password = forms.CharField(error_messages={'required': _('No password has been entered')},
|
||||
max_length=100)
|
||||
|
||||
def clean_name(self):
|
||||
name = self.cleaned_data['name']
|
||||
have_symbol = re.match('[^a-zA-Z0-9._-]+', name)
|
||||
if have_symbol:
|
||||
raise forms.ValidationError(_('The host name must not contain any special characters'))
|
||||
elif len(name) > 20:
|
||||
raise forms.ValidationError(_('The host name must not exceed 20 characters'))
|
||||
try:
|
||||
Compute.objects.get(name=name)
|
||||
except Compute.DoesNotExist:
|
||||
return name
|
||||
raise forms.ValidationError(_('This host is already connected'))
|
||||
|
||||
def clean_hostname(self):
|
||||
hostname = self.cleaned_data['hostname']
|
||||
have_symbol = re.match('[^a-z0-9.-]+', hostname)
|
||||
wrong_ip = re.match('^0.|^255.', hostname)
|
||||
if have_symbol:
|
||||
raise forms.ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."'))
|
||||
elif wrong_ip:
|
||||
raise forms.ValidationError(_('Wrong IP address'))
|
||||
try:
|
||||
Compute.objects.get(hostname=hostname)
|
||||
except Compute.DoesNotExist:
|
||||
return hostname
|
||||
raise forms.ValidationError(_('This host is already connected'))
|
||||
from .validators import validate_hostname
|
||||
|
||||
|
||||
class ComputeAddSshForm(forms.Form):
|
||||
name = forms.CharField(error_messages={'required': _('No hostname has been entered')},
|
||||
max_length=20)
|
||||
hostname = forms.CharField(error_messages={'required': _('No IP / Domain name has been entered')},
|
||||
max_length=100)
|
||||
login = forms.CharField(error_messages={'required': _('No login has been entered')},
|
||||
max_length=20)
|
||||
class TcpComputeForm(forms.ModelForm):
|
||||
hostname = forms.CharField(validators=[validate_hostname])
|
||||
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TCP)
|
||||
|
||||
def clean_name(self):
|
||||
name = self.cleaned_data['name']
|
||||
have_symbol = re.match('[^a-zA-Z0-9._-]+', name)
|
||||
if have_symbol:
|
||||
raise forms.ValidationError(_('The name of the host must not contain any special characters'))
|
||||
elif len(name) > 20:
|
||||
raise forms.ValidationError(_('The name of the host must not exceed 20 characters'))
|
||||
try:
|
||||
Compute.objects.get(name=name)
|
||||
except Compute.DoesNotExist:
|
||||
return name
|
||||
raise forms.ValidationError(_('This host is already connected'))
|
||||
|
||||
def clean_hostname(self):
|
||||
hostname = self.cleaned_data['hostname']
|
||||
have_symbol = re.match('[^a-zA-Z0-9._-]+', hostname)
|
||||
wrong_ip = re.match('^0.|^255.', hostname)
|
||||
if have_symbol:
|
||||
raise forms.ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."'))
|
||||
elif wrong_ip:
|
||||
raise forms.ValidationError(_('Wrong IP address'))
|
||||
try:
|
||||
Compute.objects.get(hostname=hostname)
|
||||
except Compute.DoesNotExist:
|
||||
return hostname
|
||||
raise forms.ValidationError(_('This host is already connected'))
|
||||
class Meta:
|
||||
model = Compute
|
||||
widgets = {"password": forms.PasswordInput()}
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ComputeAddTlsForm(forms.Form):
|
||||
name = forms.CharField(error_messages={'required': _('No hostname has been entered')},
|
||||
max_length=20)
|
||||
hostname = forms.CharField(error_messages={'required': _('No IP / Domain name has been entered')},
|
||||
max_length=100)
|
||||
login = forms.CharField(error_messages={'required': _('No login has been entered')},
|
||||
max_length=100)
|
||||
password = forms.CharField(error_messages={'required': _('No password has been entered')},
|
||||
max_length=100)
|
||||
class SshComputeForm(forms.ModelForm):
|
||||
hostname = forms.CharField(validators=[validate_hostname], label=_("FQDN/IP"))
|
||||
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SSH)
|
||||
|
||||
def clean_name(self):
|
||||
name = self.cleaned_data['name']
|
||||
have_symbol = re.match('[^a-zA-Z0-9._-]+', name)
|
||||
if have_symbol:
|
||||
raise forms.ValidationError(_('The host name must not contain any special characters'))
|
||||
elif len(name) > 20:
|
||||
raise forms.ValidationError(_('The host name must not exceed 20 characters'))
|
||||
try:
|
||||
Compute.objects.get(name=name)
|
||||
except Compute.DoesNotExist:
|
||||
return name
|
||||
raise forms.ValidationError(_('This host is already connected'))
|
||||
|
||||
def clean_hostname(self):
|
||||
hostname = self.cleaned_data['hostname']
|
||||
have_symbol = re.match('[^a-z0-9.-]+', hostname)
|
||||
wrong_ip = re.match('^0.|^255.', hostname)
|
||||
if have_symbol:
|
||||
raise forms.ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."'))
|
||||
elif wrong_ip:
|
||||
raise forms.ValidationError(_('Wrong IP address'))
|
||||
try:
|
||||
Compute.objects.get(hostname=hostname)
|
||||
except Compute.DoesNotExist:
|
||||
return hostname
|
||||
raise forms.ValidationError(_('This host is already connected'))
|
||||
class Meta:
|
||||
model = Compute
|
||||
exclude = ["password"]
|
||||
|
||||
|
||||
class ComputeEditHostForm(forms.Form):
|
||||
host_id = forms.CharField()
|
||||
name = forms.CharField(error_messages={'required': _('No hostname has been entered')},
|
||||
max_length=20)
|
||||
hostname = forms.CharField(error_messages={'required': _('No IP / Domain name has been entered')},
|
||||
max_length=100)
|
||||
login = forms.CharField(error_messages={'required': _('No login has been entered')},
|
||||
max_length=100)
|
||||
password = forms.CharField(max_length=100)
|
||||
class TlsComputeForm(forms.ModelForm):
|
||||
hostname = forms.CharField(validators=[validate_hostname])
|
||||
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TLS)
|
||||
|
||||
def clean_name(self):
|
||||
name = self.cleaned_data['name']
|
||||
have_symbol = re.match('[^a-zA-Z0-9._-]+', name)
|
||||
if have_symbol:
|
||||
raise forms.ValidationError(_('The name of the host must not contain any special characters'))
|
||||
elif len(name) > 20:
|
||||
raise forms.ValidationError(_('The name of the host must not exceed 20 characters'))
|
||||
return name
|
||||
|
||||
def clean_hostname(self):
|
||||
hostname = self.cleaned_data['hostname']
|
||||
have_symbol = re.match('[^a-zA-Z0-9._-]+', hostname)
|
||||
wrong_ip = re.match('^0.|^255.', hostname)
|
||||
if have_symbol:
|
||||
raise forms.ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."'))
|
||||
elif wrong_ip:
|
||||
raise forms.ValidationError(_('Wrong IP address'))
|
||||
return hostname
|
||||
class Meta:
|
||||
model = Compute
|
||||
widgets = {"password": forms.PasswordInput()}
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ComputeAddSocketForm(forms.Form):
|
||||
name = forms.CharField(error_messages={'required': _('No hostname has been entered')},
|
||||
max_length=20)
|
||||
class SocketComputeForm(forms.ModelForm):
|
||||
hostname = forms.CharField(widget=forms.HiddenInput, initial="localhost")
|
||||
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SOCKET)
|
||||
|
||||
def clean_name(self):
|
||||
name = self.cleaned_data['name']
|
||||
have_symbol = re.match('[^a-zA-Z0-9._-]+', name)
|
||||
if have_symbol:
|
||||
raise forms.ValidationError(_('The host name must not contain any special characters'))
|
||||
elif len(name) > 20:
|
||||
raise forms.ValidationError(_('The host name must not exceed 20 characters'))
|
||||
try:
|
||||
Compute.objects.get(name=name)
|
||||
except Compute.DoesNotExist:
|
||||
return name
|
||||
raise forms.ValidationError(_('This host is already connected'))
|
||||
class Meta:
|
||||
model = Compute
|
||||
fields = ["name", "details", "hostname", "type"]
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
# Generated by Django 2.2.10 on 2020-01-28 07:01
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
|
@ -13,15 +14,13 @@ class Migration(migrations.Migration):
|
|||
migrations.CreateModel(
|
||||
name='Compute',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=20)),
|
||||
('hostname', models.CharField(max_length=20)),
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('hostname', models.CharField(max_length=64)),
|
||||
('login', models.CharField(max_length=20)),
|
||||
('password', models.CharField(max_length=14, null=True, blank=True)),
|
||||
('password', models.CharField(blank=True, max_length=14, null=True)),
|
||||
('details', models.CharField(blank=True, max_length=64, null=True)),
|
||||
('type', models.IntegerField()),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
|
|
18
computes/migrations/0002_auto_20200529_1320.py
Normal file
18
computes/migrations/0002_auto_20200529_1320.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.12 on 2020-05-29 13:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('computes', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='compute',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64, unique=True),
|
||||
),
|
||||
]
|
38
computes/migrations/0003_auto_20200615_0637.py
Normal file
38
computes/migrations/0003_auto_20200615_0637.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 2.2.13 on 2020-06-15 06:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('computes', '0002_auto_20200529_1320'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='compute',
|
||||
name='details',
|
||||
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='details'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='compute',
|
||||
name='hostname',
|
||||
field=models.CharField(max_length=64, verbose_name='hostname'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='compute',
|
||||
name='login',
|
||||
field=models.CharField(max_length=20, verbose_name='login'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='compute',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64, unique=True, verbose_name='name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='compute',
|
||||
name='password',
|
||||
field=models.CharField(blank=True, max_length=14, null=True, verbose_name='password'),
|
||||
),
|
||||
]
|
|
@ -1,12 +1,65 @@
|
|||
from django.db import models
|
||||
from django.db.models import CharField, IntegerField, Model
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from libvirt import virConnect
|
||||
|
||||
from vrtManager.connection import connection_manager
|
||||
from vrtManager.hostdetails import wvmHostDetails
|
||||
|
||||
|
||||
class Compute(models.Model):
|
||||
name = models.CharField(max_length=20)
|
||||
hostname = models.CharField(max_length=20)
|
||||
login = models.CharField(max_length=20)
|
||||
password = models.CharField(max_length=14, blank=True, null=True)
|
||||
type = models.IntegerField()
|
||||
class Compute(Model):
|
||||
name = CharField(_("name"), max_length=64, unique=True)
|
||||
hostname = CharField(_("hostname"), max_length=64)
|
||||
login = CharField(_("login"), max_length=20)
|
||||
password = CharField(_("password"), max_length=14, blank=True, null=True)
|
||||
details = CharField(_("details"), max_length=64, null=True, blank=True)
|
||||
type = IntegerField()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.hostname
|
||||
@cached_property
|
||||
def status(self):
|
||||
# return connection_manager.host_is_up(self.type, self.hostname)
|
||||
# TODO: looks like socket has problems connecting via VPN
|
||||
if isinstance(self.connection, virConnect):
|
||||
return True
|
||||
else:
|
||||
return self.connection
|
||||
|
||||
@cached_property
|
||||
def connection(self):
|
||||
try:
|
||||
return connection_manager.get_connection(
|
||||
self.hostname,
|
||||
self.login,
|
||||
self.password,
|
||||
self.type,
|
||||
)
|
||||
except Exception as e:
|
||||
return e
|
||||
|
||||
@cached_property
|
||||
def proxy(self):
|
||||
return wvmHostDetails(
|
||||
self.hostname,
|
||||
self.login,
|
||||
self.password,
|
||||
self.type,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def cpu_count(self):
|
||||
return self.proxy.get_node_info()[3]
|
||||
|
||||
@cached_property
|
||||
def cpu_usage(self):
|
||||
return round(self.proxy.get_cpu_usage(diff=False).get('usage'))
|
||||
|
||||
@cached_property
|
||||
def ram_size(self):
|
||||
return self.proxy.get_node_info()[2]
|
||||
|
||||
@cached_property
|
||||
def ram_usage(self):
|
||||
return self.proxy.get_memory_usage()["percent"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -1,221 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Computes" %}{% endblock %}
|
||||
{% block content %}
|
||||
<!-- Page Heading -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% include 'create_comp_block.html' %}
|
||||
<h1 class="page-header">{% trans "Computes" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
{% include 'errors_block.html' %}
|
||||
|
||||
<div class="row">
|
||||
{% if computes_info %}
|
||||
{% for compute in computes_info %}
|
||||
<div id="{{ compute.name }}" class="col-xs-12 col-sm-4">
|
||||
<div class="panel {% if compute.status %}panel-success{% else %}panel-danger{% endif %} panel-data">
|
||||
<div class="panel-heading">
|
||||
{% ifequal compute.status 1 %}
|
||||
<h3 class="panel-title">
|
||||
<a href="{% url 'overview' compute.id %}"><strong>{{ compute.name }}</strong></a>
|
||||
<a data-toggle="modal" href="#editHost{{ compute.id }}" class="pull-right" title="{% trans "Edit" %}">
|
||||
<i class="fa fa-cog"></i>
|
||||
</a>
|
||||
</h3>
|
||||
{% else %}
|
||||
<h3 class="panel-title"><strong>{{ compute.name }}</strong>
|
||||
<a data-toggle="modal" href="#editHost{{ compute.id }}" class="pull-right" title="{% trans "Edit" %}">
|
||||
<span class="glyphicon glyphicon-cog"></span>
|
||||
</a>
|
||||
</h3>
|
||||
{% endifequal %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 col-sm-4">
|
||||
<p><strong>{% trans "Status:" %}</strong></p>
|
||||
</div>
|
||||
<div class="col-xs-4 col-sm-6">
|
||||
{% if compute.status %}
|
||||
<p>{% trans "Connected" %}</p>
|
||||
{% else %}
|
||||
<p>{% trans "Not Connected" %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Edit -->
|
||||
<div class="modal fade" id="editHost{{ compute.id }}" tabindex="-1" role="dialog" aria-labelledby="editHostLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{% trans "Edit connection" %}</h4>
|
||||
</div>
|
||||
{% ifequal compute.type 1 %}
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="hidden" name="host_id" value="{{ compute.id }}">
|
||||
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-zA-Z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="hostname" class="form-control" value="{{ compute.hostname }}" required pattern="[a-z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="login" class="form-control" value="{{ compute.login }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="password" name="password" class="form-control" value="{{ compute.password }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="pull-left btn btn-danger" name="host_del">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="host_edit">
|
||||
{% trans "Change" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endifequal %}
|
||||
{% ifequal compute.type 2 %}
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
||||
<p class="modal-body">{% trans "Need create ssh <a href='https://github.com/retspen/webvirtmgr/wiki/Setup-SSH-Authorization'>authorization key</a>. If you have another SSH port on your server, you can add IP:PORT like '192.168.1.1:2222'." %}</p>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="hidden" name="host_id" value="{{ compute.id }}">
|
||||
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="hostname" class="form-control" value="{{ compute.hostname }}" required pattern="[a-z0-9\:\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="login" class="form-control" value="{{ compute.login }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="pull-left btn btn-danger" name="host_del">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="host_edit">
|
||||
{% trans "Change" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endifequal %}
|
||||
{% ifequal compute.type 3 %}
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="hidden" name="host_id" value="{{ compute.id }}">
|
||||
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="hostname" class="form-control" value="{{ compute.hostname }}" required pattern="[a-z0-9\:\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="login" class="form-control" placeholder="{% trans "Name" %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="password" name="password" class="form-control" value="{{ compute.password }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="pull-left btn btn-danger" name="host_del">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="host_edit">
|
||||
{% trans "Change" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endifequal %}
|
||||
{% ifequal compute.type 4 %}
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="hidden" name="host_id" value="{{ compute.id }}">
|
||||
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="pull-left btn btn-danger" name="host_del">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="host_edit">
|
||||
{% trans "Change" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endifequal %}
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning:" %}</strong> {% trans "Hypervisor doesn't have any Computes" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
29
computes/templates/computes/form.html
Normal file
29
computes/templates/computes/form.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{% extends "base.html" %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load bootstrap_icons %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Add Compute" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h2 class="page-header">{% trans "Create Compute" %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="thumbnail col-sm-10 offset-1">
|
||||
<form id="create-update" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout='horizontal' %}
|
||||
</form>
|
||||
<div class="float-end">
|
||||
<a class="btn btn-primary" href="javascript:history.back()">
|
||||
{% bs_icon 'x-square-fill' %} {% trans "Cancel" %}</a>
|
||||
<button type="submit" form="create-update" class="btn btn-success">
|
||||
{% bs_icon 'check-circle-fill' %} {% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
116
computes/templates/computes/instances.html
Normal file
116
computes/templates/computes/instances.html
Normal file
|
@ -0,0 +1,116 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap_icons %}
|
||||
{% block title %}{% trans "Instances" %} - {{ compute.name }}{% endblock %}
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{% static "css/sortable-theme-bootstrap.css" %}" />
|
||||
{% endblock %}
|
||||
{% block page_heading %}{{ compute.name }} - {% trans "Instances" %}{% endblock page_heading %}
|
||||
|
||||
{% block page_heading_extra %}
|
||||
<a href="{% url 'instances:create_instance_select_type' compute.id %}"
|
||||
class="btn btn-success btn-header float-end">
|
||||
{% bs_icon 'plus-circle-fill' %}
|
||||
</a>
|
||||
{% if instances %}
|
||||
{% include 'search_block.html' %}
|
||||
{% endif %}
|
||||
{% endblock page_heading_extra %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb shadow-sm">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'overview' compute.id %}">{% bs_icon 'laptop' %} {% trans "Overview" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<span class="fw-bold">{% bs_icon 'server' %} {% trans "Instances" %}</span>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'storages' compute.id %}">{% bs_icon 'device-hdd' %} {% trans "Storages" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'networks' compute.id %}">{% bs_icon 'hdd-network' %} {% trans "Networks" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'interfaces' compute.id %}">{% bs_icon 'wifi' %} {% trans "Interfaces" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'nwfilters' compute.id %}">{% bs_icon 'filter' %} {% trans "NWFilters" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'virtsecrets' compute.id %}">{% bs_icon 'key' %} {% trans "Secrets" %}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% if not instances %}
|
||||
<div class="alert alert-warning shadow-sm fade show">
|
||||
{% bs_icon 'exclamation-triangle' %} <strong>{% trans "Warning" %}:</strong>
|
||||
{% trans "Hypervisor doesn't have any Instances" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<table class="table table-hover sortable-theme-bootstrap" data-sortable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Name' %}<br>{% trans 'Description' %}</th>
|
||||
<th scope="col">{% trans 'User' %}</th>
|
||||
<th scope="col">{% trans 'Status' %}</th>
|
||||
<th scope="col">{% trans 'VCPU' %}</th>
|
||||
<th scope="col" class="text-end">{% trans 'Memory' %}</th>
|
||||
<th scope="col" data-sortable="false">{% trans 'Actions' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for instance in instances %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="link-primary" href="{% url 'instances:instance' instance.id %}">{{ instance.name }}</a>
|
||||
<br>
|
||||
<p class="m-0 small fst-italic">{{ instance.title }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<em>
|
||||
{% if instance.userinstance_set.all.count > 0 %}
|
||||
{{ instance.userinstance_set.all.0.user }}
|
||||
{% if instance.userinstance_set.all.count > 1 %}
|
||||
(+{{ instance.userinstance_set.all.count|add:"-1" }})
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
{% if instance.proxy.instance.info.0 == 1 %}
|
||||
<span class="text-success">{% trans "Active" %}</span>
|
||||
{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 5 %}
|
||||
<span class="text-danger">{% trans "Off" %}</span>
|
||||
{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 3 %}
|
||||
<span class="text-warning">{% trans "Suspended" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ instance.proxy.instance.info.3 }}</td>
|
||||
<td><p class="text-end">{% widthratio instance.proxy.instance.info.1 1024 1 %} MiB</p></td>
|
||||
<td class="text-nowrap">
|
||||
{% include 'instance_actions.html' %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script src="{% static "js/sortable.min.js" %}"></script>
|
||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
||||
{% endblock %}
|
68
computes/templates/computes/list.html
Normal file
68
computes/templates/computes/list.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load common_tags %}
|
||||
{% load bootstrap_icons %}
|
||||
{% block title %}{% trans "Computes" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% include 'create_comp_block.html' %}
|
||||
{% include 'search_block.html' %}
|
||||
<h3 class="page-header">{% trans "Computes" %}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if not computes %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning shadow-sm">
|
||||
{% bs_icon 'exclamation-triangle'%} <strong>{% trans "Warning" %}:</strong> {% trans "You don't have any computes" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-striped table-hover sortable-theme-bootstrap" data-sortable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th span="col" class="col-sm-3">{% trans "Name" %}</th>
|
||||
<th span="col" class="col-sm-2">{% trans "Status" %}</th>
|
||||
<th span="col" class="col-sm-5">{% trans "Details" %}</th>
|
||||
<th span="col" class="col-sm-2 text-center">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for compute in computes %}
|
||||
<tr>
|
||||
<td class="col-sm-3">
|
||||
{{ compute.name }}
|
||||
</td>
|
||||
<td class="col-sm-2">
|
||||
{% if compute.status is True %}{% trans "Connected" %}{% else %}{% trans "Not Connected" %}{% endif %}
|
||||
</td>
|
||||
<td class="col-sm-5">
|
||||
{{ compute.details|default:"" }}
|
||||
</td>
|
||||
<td class="col-sm-2">
|
||||
<div class="float-end btn-group">
|
||||
{% if compute.status is True %}
|
||||
<a class="btn btn-success" title="{%trans "Overview" %}" href="{% url 'overview' compute.id %}">{% bs_icon 'eye-fill' %}</a>
|
||||
{% else %}
|
||||
<a class="btn btn-light" title="{%trans "Overview" %}">{% bs_icon 'eye' %}</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-primary" title="{%trans "Edit" %}" href="{% url 'compute_update' compute.id %}">{% bs_icon 'pencil-fill' %}</a>
|
||||
<a class="btn btn-danger" title="{%trans "Delete" %}" href="{% url 'compute_delete' compute.id %}">{% bs_icon 'x-circle-fill' %}</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block script %}
|
||||
<script src="{% static 'js/sortable.min.js' %}"></script>
|
||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
||||
{% endblock script %}
|
|
@ -1,159 +1,11 @@
|
|||
{% load i18n %}
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="#addHost" type="button" class="btn btn-success pull-right" data-toggle="modal">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
</a>
|
||||
{% load django_bootstrap5 %}
|
||||
{% load bootstrap_icons %}
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="addHost" tabindex="-1" role="dialog" aria-labelledby="addHostLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{% trans "Add Connection" %}</h4>
|
||||
</div>
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="#1" data-toggle="tab">{% trans "TCP Connections" %}</a>
|
||||
</li>
|
||||
<li><a href="#2" data-toggle="tab">{% trans "SSH Connections" %}</a></li>
|
||||
<li><a href="#3" data-toggle="tab">{% trans "TLS Connection" %}</a></li>
|
||||
<li><a href="#4" data-toggle="tab">{% trans "Local Socket" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="1">
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="hostname" class="form-control" placeholder="{% trans "FQDN or IP Address" %}" required pattern="[a-z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="login" class="form-control" placeholder="{% trans "Username" %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="password" name="password" class="form-control" placeholder="{% trans "Password" %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="host_tcp_add">
|
||||
{% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="2">
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
||||
<p class="modal-body">{% trans "You must create ssh <a href='https://github.com/retspen/webvirtmgr/wiki/Setup-SSH-Authorization'>authorization key</a>. If you have another SSH port on your server, you can add IP:PORT like '192.168.1.1:2222'." %}</p>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="hostname" class="form-control" placeholder="{% trans "FQDN or IP Address" %}" required pattern="[a-z0-9\:\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="login" class="form-control" placeholder="{% trans "Username" %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="host_ssh_add">
|
||||
{% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="3">
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="hostname" class="form-control" placeholder="{% trans "FQDN or IP Address" %}" required pattern="[a-z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="login" class="form-control" placeholder="{% trans "Username" %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="password" name="password" class="form-control" placeholder="{% trans "Password" %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="host_tls_add">
|
||||
{% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="4">
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="host_socket_add">
|
||||
{% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div> <!-- /.tab-content -->
|
||||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
{% endif %}
|
||||
<div class="btn-group float-end mt-1" role="group" aria-label="Add host button group">
|
||||
<a href="{% url 'add_tcp_host' %}" class="btn btn-success">{% trans "TCP" %}</a>
|
||||
<a href="{% url 'add_ssh_host' %}" class="btn btn-success">{% trans "SSH" %}</a>
|
||||
<a href="{% url 'add_tls_host' %}" class="btn btn-success">{% trans "TLS" %}</a>
|
||||
<a href="{% url 'add_socket_host' %}" class="btn btn-success">{% trans "Local" %}</a>
|
||||
<a href="#" class="btn btn-success disabled" title="{% trans "Add new host" %}">{% bs_icon "plus-circle-fill" %}</a>
|
||||
</div>
|
||||
|
|
|
@ -1,150 +1,247 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
{% load bootstrap_icons %}
|
||||
|
||||
{% block title %}{% trans "Overview" %} - {{ compute.name }}{% endblock %}
|
||||
|
||||
{% block page_heading %}{{ compute.name }}{% endblock page_heading %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Heading -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header">{{ compute.name }}</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li class="active">
|
||||
<i class="fa fa-dashboard"></i> {% trans "Overview" %}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-hdd-o"></i> <a href="{% url 'storages' compute.id %}">{% trans "Storages" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-sitemap"></i> <a href="{% url 'networks' compute.id %}">{% trans "Networks" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-wifi"></i> <a href="{% url 'interfaces' compute.id %}">{% trans "Interfaces" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-key"></i> <a href="{% url 'secrets' compute.id %}">{% trans "Secrets" %}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
<div class="row">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb shadow-sm">
|
||||
<li class="breadcrumb-item" aria-current="page">
|
||||
<span class="fw-bold">{% bs_icon 'laptop' %} {% trans "Overview" %}</span>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'instances' compute.id %}">{% bs_icon 'server' %} {% trans "Instances" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'storages' compute.id %}">{% bs_icon 'device-hdd' %} {% trans "Storages" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'networks' compute.id %}">{% bs_icon 'hdd-network' %} {% trans "Networks" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'interfaces' compute.id %}">{% bs_icon 'wifi' %} {% trans "Interfaces" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'nwfilters' compute.id %}">{% bs_icon 'filter' %} {% trans "NWFilters" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'virtsecrets' compute.id %}">{% bs_icon 'key' %} {% trans "Secrets" %}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{% include 'errors_block.html' %}
|
||||
|
||||
<div class="row" id="max-width-page">
|
||||
<h3 class="page-header">{% trans "Basic details" %}</h3>
|
||||
<div class="col-xs-4 col-sm-3">
|
||||
<p>{% trans "Hostname" %}</p>
|
||||
<p>{% trans "Hypervisor" %}</p>
|
||||
<p>{% trans "Memory" %}</p>
|
||||
<p>{% trans "Architecture" %}</p>
|
||||
<p>{% trans "Logical CPUs" %}</p>
|
||||
<p>{% trans "Processor" %}</p>
|
||||
<p>{% trans "Connection" %}</p>
|
||||
</div>
|
||||
<div class="col-xs-8 col-sm-7">
|
||||
<p>{{ hostname }}</p>
|
||||
<p>{{ hypervisor }}</p>
|
||||
<p>{{ host_memory|filesizeformat }}</p>
|
||||
<p>{{ host_arch }}</p>
|
||||
<p>{{ logical_cpu }}</p>
|
||||
<p>{{ model_cpu }}</p>
|
||||
<p>{{ uri_conn }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h3 class="page-header">{% trans "Performance" %}</h3>
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-long-arrow-right"></i> {% trans "CPU utilization" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="flot-chart">
|
||||
<div class="flot-chart-content" id="flot-moving-line-chart" style="padding: 0px; position: relative;">
|
||||
<canvas id="cpuChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shadow-sm">
|
||||
<h3 class="page-header">{% trans "Basic details" %}</h3>
|
||||
<dl class="mx-3 row">
|
||||
<dt class="col-3">{% trans "Hostname" %}</dt>
|
||||
<dd class="col-9">{{ hostname }}</dd>
|
||||
<dt class="col-3">{% trans "Hypervisors" %}</dt>
|
||||
<dd class="col-9">
|
||||
<div class="dropdown">
|
||||
{% for arch, hpv in hypervisor.items|slice:":4" %}
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="hpvArchDrop{{ forloop.counter0 }}" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ arch }}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="hpvArchDrop{{ forloop.counter0 }}">
|
||||
{% for h in hpv %}
|
||||
<a class="dropdown-item" href="#">{{ h }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-long-arrow-right"></i> {% trans "RAM utilization" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="flot-chart">
|
||||
<div class="flot-chart-content" id="flot-moving-line-chart" style="padding: 0px; position: relative;">
|
||||
<canvas id="memChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
{% if hypervisor.items|length > 4 %}
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="hpvDrop{{ forloop.counter0 }}" data-bs-toggle="dropdown">
|
||||
{{ hypervisor.items|slice:"4:"|length }} {% trans 'more' %}...
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="hpvDrop{{ forloop.counter0 }}" role="menu">
|
||||
{% for arc in hypervisor.keys|slice:"4:" %}
|
||||
<a class="dropdown-item" tabindex="-1" href="#">{{ arc }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</dd>
|
||||
<dt class="col-3">{% trans "Emulator" %}</dt>
|
||||
<dd class="col-9">{{ emulator }}</dd>
|
||||
<dt class="col-3">{% trans "Version" %}</dt>
|
||||
<dd class="col-9">
|
||||
<span class="badge bg-secondary">{% trans 'Qemu' %} </span>
|
||||
<span class="badge bg-primary">{{ version }}</span>
|
||||
<span class="badge bg-secondary">{% trans 'Libvirt' %} </span>
|
||||
<span class="badge bg-primary">{{ lib_version }}</span>
|
||||
</dd>
|
||||
<dt class="col-3">{% trans "Memory" %}</dt>
|
||||
<dd class="col-9">{{ host_memory|filesizeformat }}</dd>
|
||||
<dt class="col-3">{% trans "Architecture" %}</dt>
|
||||
<dd class="col-9">{{ host_arch }}</dd>
|
||||
<dt class="col-3">{% trans "Logical CPUs" %}</dt>
|
||||
<dd class="col-9">{{ logical_cpu }}</dd>
|
||||
<dt class="col-3">{% trans "Processor" %}</dt>
|
||||
<dd class="col-9">{{ model_cpu }}</dd>
|
||||
<dt class="col-3">{% trans "Connection" %}</dt>
|
||||
<dd class="col-9">{{ uri_conn }}</dd>
|
||||
<dt class="col-3">{% trans "Details" %}</dt>
|
||||
<dd class="col-9">{{ compute.details }}</dd>
|
||||
</dl>
|
||||
|
||||
<h3 class="page-header">{% trans "Performance" %}</h3>
|
||||
<div class="mx-3 shadow-sm">
|
||||
<div class="my-3 card border-success">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
{% bs_icon 'arrow-right' %}
|
||||
{% trans "CPU Utilization" %}
|
||||
</h5>
|
||||
<canvas id="cpuChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3 card border-primary">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title ">
|
||||
{% bs_icon 'arrow-right'%} {% trans "RAM Utilization" %}
|
||||
</h5>
|
||||
<canvas id="memChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script src="{% static "js/Chart.min.js" %}"></script>
|
||||
<script src="{% static "js/Chart.bundle.min.js" %}"></script>
|
||||
<script>
|
||||
var cpuLineData = {
|
||||
labels : [0, 0, 0, 0, 0],
|
||||
datasets : [
|
||||
{
|
||||
fillColor: "rgba(241,72,70,0.5)",
|
||||
strokeColor: "rgba(241,72,70,1)",
|
||||
pointColor : "rgba(241,72,70,1)",
|
||||
pointStrokeColor : "#fff",
|
||||
pointHighlightFill : "#fff",
|
||||
pointHighlightStroke : "rgba(220,220,220,1)",
|
||||
data : [0, 0, 0, 0, 0]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
var cpu_ctx = document.getElementById("cpuChart").getContext("2d");
|
||||
var cpuChart = new Chart(cpu_ctx).Line(cpuLineData, {
|
||||
animation: false,
|
||||
pointDotRadius: 2,
|
||||
scaleLabel: "<%=value%> %",
|
||||
scaleOverride: true,
|
||||
scaleSteps: 5,
|
||||
scaleStepWidth: 20,
|
||||
scaleStartValue: 0,
|
||||
responsive: true
|
||||
var cpuChart = new Chart(cpu_ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets : [{
|
||||
label: 'Usage',
|
||||
backgroundColor: "rgba(241,72,70,0.5)",
|
||||
pointRadius: 2,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes:[{
|
||||
offset: false,
|
||||
ticks: {
|
||||
beginAtZero: false,
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 10,
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
stepSize: 10,
|
||||
},
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
max: 100,
|
||||
min: 0,
|
||||
stepSize: 20,
|
||||
callback: function(value, index, values) {
|
||||
return value + ' %';
|
||||
}
|
||||
},
|
||||
}],
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem, chart) {
|
||||
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
return label += tooltipItem.yLabel + ' %';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var memLineData = {
|
||||
labels : [0, 0, 0, 0, 0],
|
||||
datasets : [
|
||||
{
|
||||
fillColor : "rgba(249,134,33,0.5)",
|
||||
strokeColor : "rgba(249,134,33,1)",
|
||||
pointColor : "rgba(249,134,33,1)",
|
||||
pointStrokeColor : "#fff",
|
||||
pointHighlightFill : "#fff",
|
||||
pointHighlightStroke : "rgba(151,187,205,1)",
|
||||
data : [0, 0, 0, 0, 0]
|
||||
var mem_ctx = document.getElementById("memChart").getContext("2d");
|
||||
var memChart = new Chart(mem_ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
pointRadius: 2,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes:[{
|
||||
offset: false,
|
||||
ticks: {
|
||||
beginAtZero: false,
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 10,
|
||||
maxRotation: 0,
|
||||
minRotation: 0
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks:{
|
||||
suggestedMin: 0,
|
||||
suggestedMax: 100,
|
||||
callback: function(value, index, values) {
|
||||
return value + ' MB';
|
||||
}
|
||||
},
|
||||
}],
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem, chart) {
|
||||
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
return label += tooltipItem.yLabel + ' MB';
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
if (Boolean({{ status }}) === true) {
|
||||
window.setInterval(function graph_usage() {
|
||||
$.getJSON('{% url 'compute_graph' compute_id %}', function (data) {
|
||||
cpuChart.data.labels.push(data.timeline);
|
||||
memChart.data.labels.push(data.timeline);
|
||||
|
||||
cpuChart.data.datasets[0].data.push(data.cpudata);
|
||||
if (cpuChart.data.datasets[0].data.length > 10){
|
||||
cpuChart.data.labels.shift();
|
||||
cpuChart.data.datasets[0].data.shift();
|
||||
}
|
||||
memChart.options.scales.yAxes[0].ticks.max = parseInt(data.memdata.total / 1048576);
|
||||
memChart.options.scales.yAxes[0].ticks.stepSize = parseInt(data.memdata.total / (1048576 * 5));
|
||||
memChart.data.datasets[0].data.push(parseInt(data.memdata.usage / 1048576));
|
||||
|
||||
if (memChart.data.datasets[0].data.length > 10){
|
||||
memChart.data.labels.shift();
|
||||
memChart.data.datasets[0].data.shift();
|
||||
}
|
||||
|
||||
cpuChart.update();
|
||||
memChart.update();
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
var mem_ctx = $("#memChart").get(0).getContext("2d");
|
||||
var memChart = new Chart(mem_ctx).Line(memLineData, {
|
||||
animation: false,
|
||||
pointDotRadius: 2,
|
||||
scaleLabel: "<%=value%> Mb",
|
||||
responsive: true
|
||||
});
|
||||
|
||||
window.setInterval(function graph_usage() {
|
||||
$.getJSON('{% url 'compute_graph' compute_id %}', function (data) {
|
||||
cpuChart.scale.xLabels = data.timeline;
|
||||
memChart.scale.xLabels = data.timeline;
|
||||
for (var i = 0; i < 5; i++) {
|
||||
cpuChart.datasets[0].points[i].value = data.cpudata[i];
|
||||
memChart.datasets[0].points[i].value = data.memdata[i];
|
||||
}
|
||||
cpuChart.update();
|
||||
memChart.update();
|
||||
});
|
||||
}, 5000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,3 +1,159 @@
|
|||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from .models import Compute
|
||||
|
||||
|
||||
class ComputesTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.client.login(username="admin", password="admin")
|
||||
Compute(
|
||||
name="local",
|
||||
hostname="localhost",
|
||||
login="",
|
||||
password="",
|
||||
details="local",
|
||||
type=4,
|
||||
).save()
|
||||
|
||||
def test_index(self):
|
||||
response = self.client.get(reverse("computes"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_create_update_delete(self):
|
||||
response = self.client.get(reverse("add_socket_host"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("add_socket_host"),
|
||||
{
|
||||
"name": "l1",
|
||||
"details": "Created",
|
||||
"hostname": "localhost",
|
||||
"type": 4,
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, reverse("computes"))
|
||||
|
||||
compute = Compute.objects.get(pk=2)
|
||||
self.assertEqual(compute.name, "l1")
|
||||
self.assertEqual(compute.details, "Created")
|
||||
|
||||
response = self.client.get(reverse("compute_update", args=[2]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("compute_update", args=[2]),
|
||||
{
|
||||
"name": "l2",
|
||||
"details": "Updated",
|
||||
"hostname": "localhost",
|
||||
"type": 4,
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, reverse("computes"))
|
||||
|
||||
compute = Compute.objects.get(pk=2)
|
||||
self.assertEqual(compute.name, "l2")
|
||||
self.assertEqual(compute.details, "Updated")
|
||||
|
||||
response = self.client.get(reverse("compute_delete", args=[2]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("compute_delete", args=[2]))
|
||||
self.assertRedirects(response, reverse("computes"))
|
||||
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
Compute.objects.get(id=2)
|
||||
|
||||
def test_overview(self):
|
||||
response = self.client.get(reverse("overview", args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_graph(self):
|
||||
response = self.client.get(reverse("compute_graph", args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_instances(self):
|
||||
response = self.client.get(reverse("instances", args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_storages(self):
|
||||
response = self.client.get(reverse("storages", args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_storage(self):
|
||||
pass
|
||||
|
||||
def test_default_storage_volumes(self):
|
||||
response = self.client.get(
|
||||
reverse("volumes", kwargs={"compute_id": 1, "pool": "default"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_default_storage(self):
|
||||
response = self.client.get(
|
||||
reverse("storage", kwargs={"compute_id": 1, "pool": "default"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_networks(self):
|
||||
response = self.client.get(reverse("networks", args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_default_network(self):
|
||||
response = self.client.get(
|
||||
reverse("network", kwargs={"compute_id": 1, "pool": "default"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_interfaces(self):
|
||||
response = self.client.get(reverse("interfaces", args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# TODO: add test for single interface
|
||||
|
||||
def test_nwfilters(self):
|
||||
response = self.client.get(reverse("nwfilters", args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# TODO: add test for single nwfilter
|
||||
|
||||
def test_secrets(self):
|
||||
response = self.client.get(reverse("virtsecrets", args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# def test_create_instance_select_type(self):
|
||||
# response = self.client.get(reverse('create_instance_select_type', args=[1]))
|
||||
# self.assertEqual(response.status_code, 200)
|
||||
|
||||
# TODO: create_instance
|
||||
|
||||
def test_machines(self):
|
||||
response = self.client.get(
|
||||
reverse("machines", kwargs={"compute_id": 1, "arch": "x86_64"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_compute_disk_buses(self):
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"buses",
|
||||
kwargs={
|
||||
"compute_id": 1,
|
||||
"arch": "x86_64",
|
||||
"machine": "pc",
|
||||
"disk": "disk",
|
||||
},
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_dom_capabilities(self):
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"domcaps", kwargs={"compute_id": 1, "arch": "x86_64", "machine": "pc"}
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
|
@ -1,9 +1,79 @@
|
|||
from django.conf.urls import url
|
||||
from . import views
|
||||
from virtsecrets.views import secrets
|
||||
|
||||
from django.urls import include, path
|
||||
from interfaces.views import interface, interfaces
|
||||
from networks.views import network, networks
|
||||
from nwfilters.views import nwfilter, nwfilters
|
||||
from storages.views import create_volume, get_volumes, storage, storages
|
||||
|
||||
from . import forms, views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.computes, name='computes'),
|
||||
url(r'^overview/(?P<compute_id>[0-9]+)/$', views.overview, name='overview'),
|
||||
url(r'^statistics/(?P<compute_id>[0-9]+)/$',
|
||||
views.compute_graph, name='compute_graph'),
|
||||
path("", views.computes, name="computes"),
|
||||
path(
|
||||
"add_tcp_host/",
|
||||
views.compute_create,
|
||||
{"FormClass": forms.TcpComputeForm},
|
||||
name="add_tcp_host",
|
||||
),
|
||||
path(
|
||||
"add_ssh_host/",
|
||||
views.compute_create,
|
||||
{"FormClass": forms.SshComputeForm},
|
||||
name="add_ssh_host",
|
||||
),
|
||||
path(
|
||||
"add_tls_host/",
|
||||
views.compute_create,
|
||||
{"FormClass": forms.TlsComputeForm},
|
||||
name="add_tls_host",
|
||||
),
|
||||
path(
|
||||
"add_socket_host/",
|
||||
views.compute_create,
|
||||
{"FormClass": forms.SocketComputeForm},
|
||||
name="add_socket_host",
|
||||
),
|
||||
path(
|
||||
"<int:compute_id>/",
|
||||
include(
|
||||
[
|
||||
path("", views.overview, name="overview"),
|
||||
path("update/", views.compute_update, name="compute_update"),
|
||||
path("delete/", views.compute_delete, name="compute_delete"),
|
||||
path("statistics", views.compute_graph, name="compute_graph"),
|
||||
path("instances/", views.instances, name="instances"),
|
||||
path("storages/", storages, name="storages"),
|
||||
path("storage/<str:pool>/volumes/", get_volumes, name="volumes"),
|
||||
path("storage/<str:pool>/", storage, name="storage"),
|
||||
path(
|
||||
"storage/<str:pool>/create_volume/",
|
||||
create_volume,
|
||||
name="create_volume",
|
||||
),
|
||||
path("networks/", networks, name="networks"),
|
||||
path("network/<str:pool>/", network, name="network"),
|
||||
path("interfaces/", interfaces, name="interfaces"),
|
||||
path("interface/<str:iface>/", interface, name="interface"),
|
||||
path("nwfilters/", nwfilters, name="nwfilters"),
|
||||
path("nwfilter/<str:nwfltr>/", nwfilter, name="nwfilter"),
|
||||
path("virtsecrets/", secrets, name="virtsecrets"),
|
||||
path(
|
||||
"archs/<str:arch>/machines",
|
||||
views.get_compute_machine_types,
|
||||
name="machines",
|
||||
),
|
||||
path(
|
||||
"archs/<str:arch>/machines/<str:machine>/disks/<str:disk>/buses",
|
||||
views.get_compute_disk_buses,
|
||||
name="buses",
|
||||
),
|
||||
path(
|
||||
"archs/<str:arch>/machines/<str:machine>/capabilities",
|
||||
views.get_dom_capabilities,
|
||||
name="domcaps",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
17
computes/utils.py
Normal file
17
computes/utils.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from instances.models import Instance
|
||||
|
||||
|
||||
def refresh_instance_database(compute):
|
||||
domains = compute.proxy.wvm.listAllDomains()
|
||||
domain_names = [d.name() for d in domains]
|
||||
domain_uuids = [d.UUIDString() for d in domains]
|
||||
# Delete instances that're not on host from DB
|
||||
Instance.objects.filter(compute=compute).exclude(name__in=domain_names).delete()
|
||||
Instance.objects.filter(compute=compute).exclude(uuid__in=domain_uuids).delete()
|
||||
# Create instances that're on host but not in DB
|
||||
names = Instance.objects.filter(compute=compute).values_list("name", flat=True)
|
||||
for domain in domains:
|
||||
if domain.name() not in names:
|
||||
Instance(
|
||||
compute=compute, name=domain.name(), uuid=domain.UUIDString()
|
||||
).save()
|
26
computes/validators.py
Normal file
26
computes/validators.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
have_symbol = re.compile("[^a-zA-Z0-9._-]+")
|
||||
wrong_ip = re.compile("^0.|^255.")
|
||||
wrong_name = re.compile("[^a-zA-Z0-9._-]+")
|
||||
|
||||
|
||||
def validate_hostname(value):
|
||||
sym = have_symbol.match(value)
|
||||
wip = wrong_ip.match(value)
|
||||
|
||||
if sym:
|
||||
raise ValidationError(
|
||||
_('Hostname must contain only numbers, or the domain name separated by "."')
|
||||
)
|
||||
elif wip:
|
||||
raise ValidationError(_("Wrong IP address"))
|
||||
|
||||
|
||||
def validate_name(value):
|
||||
have_symbol = wrong_name.match("[^a-zA-Z0-9._-]+")
|
||||
if have_symbol:
|
||||
raise ValidationError(_("The hostname must not contain any special characters"))
|
|
@ -1,225 +1,278 @@
|
|||
import time
|
||||
import json
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from computes.models import Compute
|
||||
|
||||
from admin.decorators import superuser_only
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from instances.models import Instance
|
||||
from accounts.models import UserInstance
|
||||
from computes.forms import ComputeAddTcpForm, ComputeAddSshForm, ComputeEditHostForm, ComputeAddTlsForm, ComputeAddSocketForm
|
||||
from vrtManager.hostdetails import wvmHostDetails
|
||||
from vrtManager.connection import CONN_SSH, CONN_TCP, CONN_TLS, CONN_SOCKET, connection_manager
|
||||
from libvirt import libvirtError
|
||||
from vrtManager.connection import (
|
||||
CONN_SOCKET,
|
||||
CONN_SSH,
|
||||
CONN_TCP,
|
||||
CONN_TLS,
|
||||
connection_manager,
|
||||
wvmConnect,
|
||||
)
|
||||
from vrtManager.hostdetails import wvmHostDetails
|
||||
|
||||
from computes.forms import (
|
||||
SocketComputeForm,
|
||||
SshComputeForm,
|
||||
TcpComputeForm,
|
||||
TlsComputeForm,
|
||||
)
|
||||
from computes.models import Compute
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
@superuser_only
|
||||
def computes(request):
|
||||
"""
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
|
||||
if not request.user.is_authenticated():
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
computes = Compute.objects.filter().order_by("name")
|
||||
|
||||
if not request.user.is_superuser:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
def get_hosts_status(computes):
|
||||
"""
|
||||
Function return all hosts all vds on host
|
||||
"""
|
||||
compute_data = []
|
||||
for compute in computes:
|
||||
compute_data.append({'id': compute.id,
|
||||
'name': compute.name,
|
||||
'hostname': compute.hostname,
|
||||
'status': connection_manager.host_is_up(compute.type, compute.hostname),
|
||||
'type': compute.type,
|
||||
'login': compute.login,
|
||||
'password': compute.password
|
||||
})
|
||||
return compute_data
|
||||
|
||||
error_messages = []
|
||||
computes = Compute.objects.filter()
|
||||
computes_info = get_hosts_status(computes)
|
||||
|
||||
if request.method == 'POST':
|
||||
if 'host_del' in request.POST:
|
||||
compute_id = request.POST.get('host_id', '')
|
||||
try:
|
||||
del_user_inst_on_host = UserInstance.objects.filter(instance__compute_id=compute_id)
|
||||
del_user_inst_on_host.delete()
|
||||
finally:
|
||||
try:
|
||||
del_inst_on_host = Instance.objects.filter(compute_id=compute_id)
|
||||
del_inst_on_host.delete()
|
||||
finally:
|
||||
del_host = Compute.objects.get(id=compute_id)
|
||||
del_host.delete()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if 'host_tcp_add' in request.POST:
|
||||
form = ComputeAddTcpForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
new_tcp_host = Compute(name=data['name'],
|
||||
hostname=data['hostname'],
|
||||
type=CONN_TCP,
|
||||
login=data['login'],
|
||||
password=data['password'])
|
||||
new_tcp_host.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
else:
|
||||
for msg_err in form.errors.values():
|
||||
error_messages.append(msg_err.as_text())
|
||||
if 'host_ssh_add' in request.POST:
|
||||
form = ComputeAddSshForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
new_ssh_host = Compute(name=data['name'],
|
||||
hostname=data['hostname'],
|
||||
type=CONN_SSH,
|
||||
login=data['login'])
|
||||
new_ssh_host.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
else:
|
||||
for msg_err in form.errors.values():
|
||||
error_messages.append(msg_err.as_text())
|
||||
if 'host_tls_add' in request.POST:
|
||||
form = ComputeAddTlsForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
new_tls_host = Compute(name=data['name'],
|
||||
hostname=data['hostname'],
|
||||
type=CONN_TLS,
|
||||
login=data['login'],
|
||||
password=data['password'])
|
||||
new_tls_host.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
else:
|
||||
for msg_err in form.errors.values():
|
||||
error_messages.append(msg_err.as_text())
|
||||
if 'host_socket_add' in request.POST:
|
||||
form = ComputeAddSocketForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
new_socket_host = Compute(name=data['name'],
|
||||
hostname='localhost',
|
||||
type=CONN_SOCKET,
|
||||
login='',
|
||||
password='')
|
||||
new_socket_host.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
else:
|
||||
for msg_err in form.errors.values():
|
||||
error_messages.append(msg_err.as_text())
|
||||
if 'host_edit' in request.POST:
|
||||
form = ComputeEditHostForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
compute_edit = Compute.objects.get(id=data['host_id'])
|
||||
compute_edit.name = data['name']
|
||||
compute_edit.hostname = data['hostname']
|
||||
compute_edit.login = data['login']
|
||||
compute_edit.password = data['password']
|
||||
compute_edit.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
else:
|
||||
for msg_err in form.errors.values():
|
||||
error_messages.append(msg_err.as_text())
|
||||
return render(request, 'computes.html', locals())
|
||||
return render(request, "computes/list.html", {"computes": computes})
|
||||
|
||||
|
||||
@superuser_only
|
||||
def overview(request, compute_id):
|
||||
"""
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
status = (
|
||||
"true"
|
||||
if connection_manager.host_is_up(compute.type, compute.hostname) is True
|
||||
else "false"
|
||||
)
|
||||
|
||||
if not request.user.is_authenticated():
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
conn = wvmHostDetails(
|
||||
compute.hostname,
|
||||
compute.login,
|
||||
compute.password,
|
||||
compute.type,
|
||||
)
|
||||
(
|
||||
hostname,
|
||||
host_arch,
|
||||
host_memory,
|
||||
logical_cpu,
|
||||
model_cpu,
|
||||
uri_conn,
|
||||
) = conn.get_node_info()
|
||||
hypervisor = conn.get_hypervisors_domain_types()
|
||||
mem_usage = conn.get_memory_usage()
|
||||
emulator = conn.get_emulator(host_arch)
|
||||
version = conn.get_version()
|
||||
lib_version = conn.get_lib_version()
|
||||
conn.close()
|
||||
|
||||
if not request.user.is_superuser:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
return render(request, "overview.html", locals())
|
||||
|
||||
error_messages = []
|
||||
|
||||
@superuser_only
|
||||
def instances(request, compute_id):
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
|
||||
try:
|
||||
conn = wvmHostDetails(compute.hostname,
|
||||
compute.login,
|
||||
compute.password,
|
||||
compute.type)
|
||||
hostname, host_arch, host_memory, logical_cpu, model_cpu, uri_conn = conn.get_node_info()
|
||||
hypervisor = conn.hypervisor_type()
|
||||
mem_usage = conn.get_memory_usage()
|
||||
conn.close()
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
utils.refresh_instance_database(compute)
|
||||
instances = Instance.objects.filter(compute=compute).prefetch_related(
|
||||
"userinstance_set"
|
||||
)
|
||||
|
||||
return render(request, 'overview.html', locals())
|
||||
return render(
|
||||
request, "computes/instances.html", {"compute": compute, "instances": instances}
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def compute_create(request, FormClass):
|
||||
form = FormClass(request.POST or None)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect(reverse("computes"))
|
||||
|
||||
return render(request, "computes/form.html", {"form": form})
|
||||
|
||||
|
||||
@superuser_only
|
||||
def compute_update(request, compute_id):
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
|
||||
if compute.type == 1:
|
||||
FormClass = TcpComputeForm
|
||||
elif compute.type == 2:
|
||||
FormClass = SshComputeForm
|
||||
elif compute.type == 3:
|
||||
FormClass = TlsComputeForm
|
||||
elif compute.type == 4:
|
||||
FormClass = SocketComputeForm
|
||||
|
||||
form = FormClass(request.POST or None, instance=compute)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect(reverse("computes"))
|
||||
|
||||
return render(request, "computes/form.html", {"form": form})
|
||||
|
||||
|
||||
@superuser_only
|
||||
def compute_delete(request, compute_id):
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
if request.method == "POST":
|
||||
compute.delete()
|
||||
return redirect("computes")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/confirm_delete.html",
|
||||
{"object": compute},
|
||||
)
|
||||
|
||||
|
||||
def compute_graph(request, compute_id):
|
||||
"""
|
||||
:param request:
|
||||
:param compute_id:
|
||||
:return:
|
||||
"""
|
||||
comp_mgr = ComputeManager(compute_id)
|
||||
data = comp_mgr.compute_graph()
|
||||
|
||||
if not request.user.is_authenticated():
|
||||
return HttpResponseRedirect(reverse('login'))
|
||||
|
||||
points = 5
|
||||
datasets = {}
|
||||
cookies = {}
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
curent_time = time.strftime("%H:%M:%S")
|
||||
|
||||
try:
|
||||
conn = wvmHostDetails(compute.hostname,
|
||||
compute.login,
|
||||
compute.password,
|
||||
compute.type)
|
||||
cpu_usage = conn.get_cpu_usage()
|
||||
mem_usage = conn.get_memory_usage()
|
||||
conn.close()
|
||||
except libvirtError:
|
||||
cpu_usage = 0
|
||||
mem_usage = 0
|
||||
|
||||
try:
|
||||
cookies['cpu'] = request.COOKIES['cpu']
|
||||
cookies['mem'] = request.COOKIES['mem']
|
||||
cookies['timer'] = request.COOKIES['timer']
|
||||
except KeyError:
|
||||
cookies['cpu'] = None
|
||||
cookies['mem'] = None
|
||||
|
||||
if not cookies['cpu'] or not cookies['mem']:
|
||||
datasets['cpu'] = [0] * points
|
||||
datasets['mem'] = [0] * points
|
||||
datasets['timer'] = [0] * points
|
||||
else:
|
||||
datasets['cpu'] = eval(cookies['cpu'])
|
||||
datasets['mem'] = eval(cookies['mem'])
|
||||
datasets['timer'] = eval(cookies['timer'])
|
||||
|
||||
datasets['timer'].append(curent_time)
|
||||
datasets['cpu'].append(int(cpu_usage['usage']))
|
||||
datasets['mem'].append(int(mem_usage['usage']) / 1048576)
|
||||
|
||||
if len(datasets['timer']) > points:
|
||||
datasets['timer'].pop(0)
|
||||
if len(datasets['cpu']) > points:
|
||||
datasets['cpu'].pop(0)
|
||||
if len(datasets['mem']) > points:
|
||||
datasets['mem'].pop(0)
|
||||
|
||||
data = json.dumps({'cpudata': datasets['cpu'], 'memdata': datasets['mem'], 'timeline': datasets['timer']})
|
||||
response = HttpResponse()
|
||||
response['Content-Type'] = "text/javascript"
|
||||
response.cookies['cpu'] = datasets['cpu']
|
||||
response.cookies['timer'] = datasets['timer']
|
||||
response.cookies['mem'] = datasets['mem']
|
||||
response["Content-Type"] = "text/javascript"
|
||||
response.write(data)
|
||||
return response
|
||||
|
||||
|
||||
def get_compute_disk_buses(request, compute_id, arch, machine, disk):
|
||||
"""
|
||||
:param request:
|
||||
:param compute_id:
|
||||
:param arch:
|
||||
:param machine:
|
||||
:param disk:
|
||||
:return:
|
||||
"""
|
||||
comp_mgr = ComputeManager(compute_id)
|
||||
return HttpResponse(comp_mgr.get_disk_buses(arch, machine, disk))
|
||||
|
||||
|
||||
def get_compute_machine_types(request, compute_id, arch):
|
||||
"""
|
||||
:param request:
|
||||
:param compute_id:
|
||||
:param arch:
|
||||
:return:
|
||||
"""
|
||||
comp_mgr = ComputeManager(compute_id)
|
||||
return HttpResponse(comp_mgr.get_machine_types(arch))
|
||||
|
||||
|
||||
def get_compute_video_models(request, compute_id, arch, machine):
|
||||
"""
|
||||
:param request:
|
||||
:param compute_id:
|
||||
:param arch:
|
||||
:param machine:
|
||||
:return:
|
||||
"""
|
||||
comp_mgr = ComputeManager(compute_id)
|
||||
return HttpResponse(comp_mgr.get_video_models(arch, machine))
|
||||
|
||||
|
||||
def get_dom_capabilities(request, compute_id, arch, machine):
|
||||
"""
|
||||
:param request:
|
||||
:param compute_id:
|
||||
:param arch:
|
||||
:param machine:
|
||||
:return:
|
||||
"""
|
||||
comp_mgr = ComputeManager(compute_id)
|
||||
return HttpResponse(comp_mgr.get_dom_capabilities(arch, machine))
|
||||
|
||||
|
||||
class ComputeManager:
|
||||
def __init__(self, compute_id):
|
||||
self.compute = get_object_or_404(Compute, pk=compute_id)
|
||||
self.conn = wvmConnect(
|
||||
self.compute.hostname,
|
||||
self.compute.login,
|
||||
self.compute.password,
|
||||
self.compute.type,
|
||||
)
|
||||
|
||||
def get_video_models(self, arch, machine):
|
||||
data = dict()
|
||||
try:
|
||||
data["videos"] = self.conn.get_video_models(arch, machine)
|
||||
except libvirtError:
|
||||
pass
|
||||
|
||||
return json.dumps(data)
|
||||
|
||||
def get_dom_capabilities(self, arch, machine):
|
||||
data = dict()
|
||||
try:
|
||||
data["videos"] = self.conn.get_disk_device_types(arch, machine)
|
||||
data["bus"] = self.conn.get_disk_device_types(arch, machine)
|
||||
except libvirtError:
|
||||
pass
|
||||
|
||||
return json.dumps(data)
|
||||
|
||||
def get_machine_types(self, arch):
|
||||
data = dict()
|
||||
try:
|
||||
data["machines"] = self.conn.get_machine_types(arch)
|
||||
except libvirtError:
|
||||
pass
|
||||
|
||||
return json.dumps(data)
|
||||
|
||||
def get_disk_buses(self, arch, machine, disk):
|
||||
data = dict()
|
||||
try:
|
||||
disk_device_types = self.conn.get_disk_device_types(arch, machine)
|
||||
|
||||
if disk in disk_device_types:
|
||||
if disk == "disk":
|
||||
data["bus"] = sorted(disk_device_types)
|
||||
elif disk == "cdrom":
|
||||
data["bus"] = ["ide", "sata", "scsi"]
|
||||
elif disk == "floppy":
|
||||
data["bus"] = ["fdc"]
|
||||
elif disk == "lun":
|
||||
data["bus"] = ["scsi"]
|
||||
except libvirtError:
|
||||
pass
|
||||
|
||||
return json.dumps(data)
|
||||
|
||||
def compute_graph(self):
|
||||
try:
|
||||
conn = wvmHostDetails(
|
||||
self.compute.hostname,
|
||||
self.compute.login,
|
||||
self.compute.password,
|
||||
self.compute.type,
|
||||
)
|
||||
current_time = timezone.now().strftime("%H:%M:%S")
|
||||
cpu_usage = conn.get_cpu_usage()
|
||||
mem_usage = conn.get_memory_usage()
|
||||
conn.close()
|
||||
except libvirtError:
|
||||
cpu_usage = {"usage": 0}
|
||||
mem_usage = {"usage": 0}
|
||||
current_time = 0
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"cpudata": cpu_usage["usage"],
|
||||
"memdata": mem_usage,
|
||||
"timeline": current_time,
|
||||
}
|
||||
)
|
||||
|
|
100
conf/daemon/consolecallback
Executable file
100
conf/daemon/consolecallback
Executable file
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env python3
|
||||
# consolecallback - provide a persistent console that survives guest reboots
|
||||
|
||||
import os
|
||||
import logging
|
||||
import libvirt
|
||||
import tty
|
||||
import termios
|
||||
import atexit
|
||||
from argparse import ArgumentParser
|
||||
from typing import Optional # noqa F401
|
||||
|
||||
|
||||
def reset_term() -> None:
|
||||
termios.tcsetattr(0, termios.TCSADRAIN, attrs)
|
||||
|
||||
|
||||
def error_handler(unused, error) -> None:
|
||||
# The console stream errors on VM shutdown; we don't care
|
||||
if error[0] == libvirt.VIR_ERR_RPC and error[1] == libvirt.VIR_FROM_STREAMS:
|
||||
return
|
||||
logging.warning(error)
|
||||
|
||||
|
||||
class Console(object):
|
||||
def __init__(self, uri: str, uuid: str) -> None:
|
||||
self.uri = uri
|
||||
self.uuid = uuid
|
||||
self.connection = libvirt.open(uri)
|
||||
self.domain = self.connection.lookupByUUIDString(uuid)
|
||||
self.state = self.domain.state(0)
|
||||
self.connection.domainEventRegister(lifecycle_callback, self)
|
||||
self.stream = None # type: Optional[libvirt.virStream]
|
||||
self.run_console = True
|
||||
self.stdin_watch = -1
|
||||
logging.info("%s initial state %d, reason %d",
|
||||
self.uuid, self.state[0], self.state[1])
|
||||
|
||||
|
||||
def check_console(console: Console) -> bool:
|
||||
if (console.state[0] == libvirt.VIR_DOMAIN_RUNNING or console.state[0] == libvirt.VIR_DOMAIN_PAUSED):
|
||||
if console.stream is None:
|
||||
console.stream = console.connection.newStream(libvirt.VIR_STREAM_NONBLOCK)
|
||||
console.domain.openConsole(None, console.stream, 0)
|
||||
console.stream.eventAddCallback(libvirt.VIR_STREAM_EVENT_READABLE, stream_callback, console)
|
||||
else:
|
||||
if console.stream:
|
||||
console.stream.eventRemoveCallback()
|
||||
console.stream = None
|
||||
|
||||
return console.run_console
|
||||
|
||||
|
||||
def stdin_callback(watch: int, fd: int, events: int, console: Console) -> None:
|
||||
readbuf = os.read(fd, 1024)
|
||||
if readbuf.startswith(b""):
|
||||
console.run_console = False
|
||||
return
|
||||
if console.stream:
|
||||
console.stream.send(readbuf)
|
||||
|
||||
|
||||
def stream_callback(stream: libvirt.virStream, events: int, console: Console) -> None:
|
||||
try:
|
||||
assert console.stream
|
||||
received_data = console.stream.recv(1024)
|
||||
except Exception:
|
||||
return
|
||||
os.write(0, received_data)
|
||||
|
||||
|
||||
def lifecycle_callback(connection: libvirt.virConnect, domain: libvirt.virDomain, event: int, detail: int, console: Console) -> None:
|
||||
console.state = console.domain.state(0)
|
||||
logging.info("%s transitioned to state %d, reason %d",
|
||||
console.uuid, console.state[0], console.state[1])
|
||||
|
||||
|
||||
# main
|
||||
parser = ArgumentParser(epilog="Example: %(prog)s 'qemu:///system' '32ad945f-7e78-c33a-e96d-39f25e025d81'")
|
||||
parser.add_argument("uri")
|
||||
parser.add_argument("uuid")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("Escape character is ^]")
|
||||
logging.basicConfig(filename='msg.log', level=logging.DEBUG)
|
||||
logging.info("URI: %s", args.uri)
|
||||
logging.info("UUID: %s", args.uuid)
|
||||
|
||||
libvirt.virEventRegisterDefaultImpl()
|
||||
libvirt.registerErrorHandler(error_handler, None)
|
||||
|
||||
atexit.register(reset_term)
|
||||
attrs = termios.tcgetattr(0)
|
||||
tty.setraw(0)
|
||||
|
||||
console = Console(args.uri, args.uuid)
|
||||
console.stdin_watch = libvirt.virEventAddHandle(0, libvirt.VIR_EVENT_HANDLE_READABLE, stdin_callback, console)
|
||||
|
||||
while check_console(console):
|
||||
libvirt.virEventRunDefaultImpl()
|
|
@ -3,24 +3,24 @@
|
|||
# gstfsd - WebVirtCloud daemon for managing VM's filesystem
|
||||
#
|
||||
|
||||
import SocketServer
|
||||
import socketserver
|
||||
import json
|
||||
import guestfs
|
||||
import re
|
||||
|
||||
|
||||
PORT = 16510
|
||||
ADDRESS = "0.0.0.0"
|
||||
|
||||
|
||||
class MyTCPServer(SocketServer.ThreadingTCPServer):
|
||||
class MyTCPServer(socketserver.ThreadingTCPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
|
||||
class MyTCPServerHandler(SocketServer.BaseRequestHandler):
|
||||
class MyTCPServerHandler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
# recive data
|
||||
data = json.loads(self.request.recv(1024).strip())
|
||||
d = self.request.recv(1024).strip()
|
||||
data = json.loads(d)
|
||||
|
||||
# GuestFS
|
||||
gfs = guestfs.GuestFS(python_return_dict=True)
|
||||
|
@ -42,17 +42,18 @@ class MyTCPServerHandler(SocketServer.BaseRequestHandler):
|
|||
if data['action'] == 'publickey':
|
||||
if not gfs.is_dir('/root/.ssh'):
|
||||
gfs.mkdir('/root/.ssh')
|
||||
gfs.chmod(0700, "/root/.ssh")
|
||||
gfs.chmod(700, "/root/.ssh")
|
||||
gfs.write('/root/.ssh/authorized_keys', data['key'])
|
||||
gfs.chmod(0600, '/root/.ssh/authorized_keys')
|
||||
gfs.chmod(600, '/root/.ssh/authorized_keys')
|
||||
self.request.sendall(json.dumps({'return': 'success'}))
|
||||
gfs.umount(part)
|
||||
except RuntimeError:
|
||||
pass
|
||||
gfs.shutdown()
|
||||
gfs.close()
|
||||
except RuntimeError, err:
|
||||
self.request.sendall(json.dumps({'return': 'error', 'message': err.message}))
|
||||
except Exception as err:
|
||||
self.request.sendall(bytes(json.dumps({'return': 'error', 'message': str(err)}).encode()))
|
||||
|
||||
|
||||
server = MyTCPServer((ADDRESS, PORT), MyTCPServerHandler)
|
||||
server.serve_forever()
|
||||
|
|
37
conf/nginx/centos_nginx.conf
Normal file
37
conf/nginx/centos_nginx.conf
Normal file
|
@ -0,0 +1,37 @@
|
|||
# For more information on configuration, see:
|
||||
# * Official English Documentation: http://nginx.org/en/docs/
|
||||
# * Official Russian Documentation: http://nginx.org/ru/docs/
|
||||
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
|
||||
include /usr/share/nginx/modules/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Load modular configuration files from the /etc/nginx/conf.d directory.
|
||||
# See http://nginx.org/en/docs/ngx_core_module.html#include
|
||||
# for more information.
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
85
conf/nginx/debian_nginx.conf
Normal file
85
conf/nginx/debian_nginx.conf
Normal file
|
@ -0,0 +1,85 @@
|
|||
user www-data;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
include /etc/nginx/modules-enabled/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 768;
|
||||
# multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
|
||||
##
|
||||
# Basic Settings
|
||||
##
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
# server_tokens off;
|
||||
|
||||
# server_names_hash_bucket_size 64;
|
||||
# server_name_in_redirect off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
##
|
||||
# SSL Settings
|
||||
##
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
##
|
||||
# Logging Settings
|
||||
##
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
##
|
||||
# Gzip Settings
|
||||
##
|
||||
|
||||
gzip on;
|
||||
|
||||
# gzip_vary on;
|
||||
# gzip_proxied any;
|
||||
# gzip_comp_level 6;
|
||||
# gzip_buffers 16 8k;
|
||||
# gzip_http_version 1.1;
|
||||
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
##
|
||||
# Virtual Host Configs
|
||||
##
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
|
||||
|
||||
#mail {
|
||||
# # See sample authentication script at:
|
||||
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
|
||||
#
|
||||
# # auth_http localhost/auth.php;
|
||||
# # pop3_capabilities "TOP" "USER";
|
||||
# # imap_capabilities "IMAP5rev1" "UIDPLUS";
|
||||
#
|
||||
# server {
|
||||
# listen localhost:110;
|
||||
# protocol pop3;
|
||||
# proxy on;
|
||||
# }
|
||||
#
|
||||
# server {
|
||||
# listen localhost:143;
|
||||
# protocol imap;
|
||||
# proxy on;
|
||||
# }
|
||||
#}
|
38
conf/nginx/openEuler_nginx.conf
Normal file
38
conf/nginx/openEuler_nginx.conf
Normal file
|
@ -0,0 +1,38 @@
|
|||
# For more information on configuration, see:
|
||||
# * Official English Documentation: http://nginx.org/en/docs/
|
||||
# * Official Russian Documentation: http://nginx.org/ru/docs/
|
||||
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
|
||||
include /usr/share/nginx/modules/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Load modular configuration files from the /etc/nginx/conf.d directory.
|
||||
# See http://nginx.org/en/docs/ngx_core_module.html#include
|
||||
# for more information.
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue