mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-06-07 19:16:26 +03:00
Compare commits
1137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7f7a688d3 | ||
|
|
bb66555896 | ||
|
|
37726630ce | ||
|
|
e26762fce3 | ||
|
|
32e59fcce4 | ||
|
|
9b43330e95 | ||
|
|
b3b94243b4 | ||
|
|
fba1032388 | ||
|
|
ccc0803c5b | ||
|
|
f0340bcc98 | ||
|
|
8d19b8fcbf | ||
|
|
dbf66d5a9a | ||
|
|
0fa8d61b19 | ||
|
|
02d326419b | ||
|
|
608e80a80b | ||
|
|
d90f11eb86 | ||
|
|
dc39187091 | ||
|
|
e4cd418533 | ||
|
|
8559469c73 | ||
|
|
e6ba7bdd98 | ||
|
|
784055689f | ||
|
|
c937811f45 | ||
|
|
79c8021faa | ||
|
|
a428730f59 | ||
|
|
3cad8ea046 | ||
|
|
ab1775e44b | ||
|
|
9c3facb07a | ||
|
|
e5ae7b2d25 | ||
|
|
60462ff986 | ||
|
|
39443cd676 | ||
|
|
25ab8249ae | ||
|
|
6e86c606cc | ||
|
|
8878b96e74 | ||
|
|
c3ef2edbab | ||
|
|
f7e398edc3 | ||
|
|
8d4fc9585e | ||
|
|
5fe65e26ce | ||
|
|
daaebe7f96 | ||
|
|
f9f6a52e8b | ||
|
|
601c0217b9 | ||
|
|
b0971b4ba3 | ||
|
|
5a4549c36c | ||
|
|
fe2cc362f8 | ||
|
|
dd0220fd59 | ||
|
|
26fdd9ef6f | ||
|
|
733ee259e5 | ||
|
|
762fecbcff | ||
|
|
5e85dfe5fd | ||
|
|
e01632d60a | ||
|
|
60916e8b80 | ||
|
|
4fe55634ae | ||
|
|
b1b861c99d | ||
|
|
07c0474386 | ||
|
|
755fb5c8f3 | ||
|
|
fdb382874d | ||
|
|
471b3e1009 | ||
|
|
f64a226336 | ||
|
|
dec257bb6b | ||
|
|
e736fbbb87 | ||
|
|
aeb42b3ffe | ||
|
|
1694a57ed9 | ||
|
|
26001463a0 | ||
|
|
36c1197f1f | ||
|
|
1fa16936fe | ||
|
|
fca65784ee | ||
|
|
8983e6c5a9 | ||
|
|
c6e06f3941 | ||
|
|
b7e32a60ce | ||
|
|
da100e494b | ||
|
|
9d7b5bccb8 | ||
|
|
112b05c3dd | ||
|
|
987e85cce8 | ||
|
|
8142d0baa7 | ||
|
|
cc32b2661c | ||
|
|
781857f598 | ||
|
|
69179dcb63 | ||
|
|
8017838d60 | ||
|
|
81946493ec | ||
|
|
a7f40c3d50 | ||
|
|
c28723287f | ||
|
|
c7a8588647 | ||
|
|
4a64261c5d | ||
|
|
5f4365542c | ||
|
|
2e2c951ffc | ||
|
|
2ba7dde326 | ||
|
|
a1579ca86b | ||
|
|
81c4ddb85f | ||
|
|
285a8d413a | ||
|
|
672c86b38d | ||
|
|
905611d2a8 | ||
|
|
339fb22217 | ||
|
|
a8f5fa5dd5 | ||
|
|
bd365dd6eb | ||
|
|
8e4a6169e0 | ||
|
|
b1790844f3 | ||
|
|
4b0050d26c | ||
|
|
19bf40dc89 | ||
|
|
23cda61d17 | ||
|
|
c74ffde65a | ||
|
|
0a9c10e748 | ||
|
|
6973eaaa02 | ||
|
|
9ce483398b | ||
|
|
55744ab129 | ||
|
|
0e1cb47aa1 | ||
|
|
3bf12753df | ||
|
|
8907659220 | ||
|
|
a4ccb0b620 | ||
|
|
7d845c0ef8 | ||
|
|
8282ec1da6 | ||
|
|
edfe2c7f47 | ||
|
|
ca69673439 | ||
|
|
8677707a9a | ||
|
|
f3abbe5d58 | ||
|
|
c0f36590be | ||
|
|
bdb097a173 | ||
|
|
510491e26d | ||
|
|
6f0015a678 | ||
|
|
690e1d5ea0 | ||
|
|
19734e0bd3 | ||
|
|
5172c0b19e | ||
|
|
d74898c8a3 | ||
|
|
eb10f10988 | ||
|
|
eb2e504acc | ||
|
|
6c502e1213 | ||
|
|
462c00b358 | ||
|
|
b83eb61a42 | ||
|
|
96c35c49ff | ||
|
|
36b48f24dc | ||
|
|
efb21665f0 | ||
|
|
9ab77cfe20 | ||
|
|
6031a349be | ||
|
|
7163daf480 | ||
|
|
f662c39df0 | ||
|
|
d7caba76c4 | ||
|
|
36ff527cc0 | ||
|
|
d96ab77a67 | ||
|
|
b4ade1982e | ||
|
|
4c9ef92c2d | ||
|
|
078fc0f5ed | ||
|
|
40cc73c4f1 | ||
|
|
07c1443a8d | ||
|
|
dc592e0d4f | ||
|
|
c873b6c603 | ||
|
|
c7d0a7a504 | ||
|
|
e7eddf0a7e | ||
|
|
579e382971 | ||
|
|
95ecb05434 | ||
|
|
a2353f32e4 | ||
|
|
793181d208 | ||
|
|
932ff53190 | ||
|
|
e92e23a8be | ||
|
|
a16adb2b46 | ||
|
|
f579ebf5a0 | ||
|
|
67593c97ab | ||
|
|
fe185283a0 | ||
|
|
d2f8214a52 | ||
|
|
a2293b21ce | ||
|
|
57872c38b3 | ||
|
|
51f998c177 | ||
|
|
7abbc630c3 | ||
|
|
a0dee6cb42 | ||
|
|
d38143ae1c | ||
|
|
1db36dfb3e | ||
|
|
a347de96ed | ||
|
|
31c76bf56d | ||
|
|
8abf321d9a | ||
|
|
3f7b6e7be4 | ||
|
|
7b1337cd83 | ||
|
|
7f63a221af | ||
|
|
cfeb5c9495 | ||
|
|
17655cd855 | ||
|
|
ab071cd989 | ||
|
|
c73dd10783 | ||
|
|
6a5584ae41 | ||
|
|
16e82592a6 | ||
|
|
842e771eef | ||
|
|
d31f9c49f5 | ||
|
|
8b61c7ddc3 | ||
|
|
af247e2dd6 | ||
|
|
990cb49854 | ||
|
|
c530924d8a | ||
|
|
35360ae196 | ||
|
|
f37117ed09 | ||
|
|
9d2d80db25 | ||
|
|
364d8db5b2 | ||
|
|
0ac5215d4b | ||
|
|
493d81c519 | ||
|
|
c5ab0c8d02 | ||
|
|
03323e5df8 | ||
|
|
a8c429b77e | ||
|
|
52095c8ed9 | ||
|
|
b13855a062 | ||
|
|
0be67907ba | ||
|
|
cf7285a179 | ||
|
|
ba564869a0 | ||
|
|
a6ddddea99 | ||
|
|
a686a18d56 | ||
|
|
a98bd132e1 | ||
|
|
a100ca33b9 | ||
|
|
d2f75262d8 | ||
|
|
c65fd1683d | ||
|
|
76a8735133 | ||
|
|
da3c1334da | ||
|
|
0c370ec45e | ||
|
|
ec27e418fc | ||
|
|
92500e96d4 | ||
|
|
c2ba6f5cb3 | ||
|
|
07610e7556 | ||
|
|
741749dffb | ||
|
|
593c777ff0 | ||
|
|
8a616fa0c5 | ||
|
|
a191ec7326 | ||
|
|
f842812745 | ||
|
|
3cc43aa11f | ||
|
|
1d9839ed23 | ||
|
|
32a64f1259 | ||
|
|
3c4fc4c1bd | ||
|
|
1013cf527d | ||
|
|
1684ee3074 | ||
|
|
f50e08207c | ||
|
|
d3973d4a7b | ||
|
|
7170e58551 | ||
|
|
02968f1ece | ||
|
|
eb94ba8b93 | ||
|
|
67c6e03f3b | ||
|
|
faa3b519b6 | ||
|
|
3b4dca7545 | ||
|
|
66495cbf83 | ||
|
|
95fea87b29 | ||
|
|
4b0cea36d0 | ||
|
|
ec27683f0e | ||
|
|
a4d7aaf0a1 | ||
|
|
e2bcd1fdd1 | ||
|
|
d7fd04de03 | ||
|
|
f447cd9be6 | ||
|
|
a080b65615 | ||
|
|
bd034237c0 | ||
|
|
66aff0d9a2 | ||
|
|
04a4142128 | ||
|
|
ec71da4062 | ||
|
|
0ae97aa933 | ||
|
|
580e19de0c | ||
|
|
e41e95b0e6 | ||
|
|
1adeee9ba1 | ||
|
|
b598339e55 | ||
|
|
c65e5d8795 | ||
|
|
b8597bc196 | ||
|
|
be75a8e2b8 | ||
|
|
2352840c3b | ||
|
|
98f9f59af3 | ||
|
|
00ba2f1046 | ||
|
|
0389245cb7 | ||
|
|
2f746ff791 | ||
|
|
8b881dfd20 | ||
|
|
cfece32185 | ||
|
|
d09ce859aa | ||
|
|
227a8a0c9e | ||
|
|
1b0da301e3 | ||
|
|
0f3efaa3fe | ||
|
|
55b5f49c79 | ||
|
|
3601acf66e | ||
|
|
383306c0ab | ||
|
|
5f87be16d7 | ||
|
|
63ab01d364 | ||
|
|
4252f19819 | ||
|
|
1095ed1663 | ||
|
|
c5557e45c4 | ||
|
|
f5159a93f3 | ||
|
|
b00d39e531 | ||
|
|
0c077b5647 | ||
|
|
98aed2a8b6 | ||
|
|
0523873e5e | ||
|
|
ed5ce93df6 | ||
|
|
ab4f85b862 | ||
|
|
67912a918f | ||
|
|
0af939bd37 | ||
|
|
b712b54786 | ||
|
|
19bf52661e | ||
|
|
c0c0261c68 | ||
|
|
1eea941b55 | ||
|
|
547a8fb6c4 | ||
|
|
a0271926e6 | ||
|
|
7e796fc3f1 | ||
|
|
ba17b9d789 | ||
|
|
a4c17562ad | ||
|
|
ca173fb491 | ||
|
|
9330689641 | ||
|
|
ca67553e66 | ||
|
|
dabcb6d4f4 | ||
|
|
753eb79a15 | ||
|
|
034c9e8ad5 | ||
|
|
ba04783e8f | ||
|
|
a8ba51cdee | ||
|
|
24997c39f5 | ||
|
|
919311e2ab | ||
|
|
61c3b0bfff | ||
|
|
8024738400 | ||
|
|
ac58bc677f | ||
|
|
3b887f7f1b | ||
|
|
8ea24a156e | ||
|
|
264fe8f0c8 | ||
|
|
3f67cd4a99 | ||
|
|
5d9e9dfc4a | ||
|
|
95bec70e27 | ||
|
|
c1c6c812c8 | ||
|
|
0a8d947375 | ||
|
|
c9a699fc30 | ||
|
|
8bf350a7dd | ||
|
|
bbc468db0f | ||
|
|
2353f074ab | ||
|
|
79b7174a83 | ||
|
|
82e1c088c7 | ||
|
|
1d13a3de3c | ||
|
|
0fb0bafc14 | ||
|
|
6cc5547509 | ||
|
|
a72df53b69 | ||
|
|
f0802038db | ||
|
|
631b4b7a61 | ||
|
|
d9436520af | ||
|
|
d805cfbd6a | ||
|
|
078d5023c4 | ||
|
|
bc053da538 | ||
|
|
5d79bbce39 | ||
|
|
98c8d56dc5 | ||
|
|
1acaa18c3c | ||
|
|
8325fe0cee | ||
|
|
eedd12207c | ||
|
|
64ed8c3de0 | ||
|
|
ab4f2f3922 | ||
|
|
fc5bf2dc4b | ||
|
|
bd0fabd1f6 | ||
|
|
a4a2963bc3 | ||
|
|
310f47b52a | ||
|
|
b4a187dd02 | ||
|
|
f4afdca576 | ||
|
|
28bebff7b0 | ||
|
|
c3a4daef87 | ||
|
|
18a5a28283 | ||
|
|
36d08d2dca | ||
|
|
91bf6e667d | ||
|
|
a88a47e223 | ||
|
|
8ede0f2089 | ||
|
|
576f6c81a1 | ||
|
|
1666fc4aa0 | ||
|
|
1216882e89 | ||
|
|
3f825a163b | ||
|
|
2fcfa10573 | ||
|
|
dd95b24d32 | ||
|
|
bb2777ed5b | ||
|
|
e0f03ec582 | ||
|
|
51e844559e | ||
|
|
dc4a984c41 | ||
|
|
54139845ac | ||
|
|
8f9672d9e2 | ||
|
|
b007aea2f5 | ||
|
|
3900645d1f | ||
|
|
5d644467fc | ||
|
|
29fcd07b1c | ||
|
|
22c2ee31c4 | ||
|
|
69f99742a8 | ||
|
|
e7dc901d5e | ||
|
|
4dba7fa7fc | ||
|
|
55d4201aaf | ||
|
|
74a4e464c8 | ||
|
|
329c196047 | ||
|
|
85e74b482e | ||
|
|
dc12fdd1c9 | ||
|
|
0ee64ecbb8 | ||
|
|
2946e931aa | ||
|
|
30871f0cd1 | ||
|
|
70aa165444 | ||
|
|
beb35c6108 | ||
|
|
90d1cb9a7f | ||
|
|
5754d66560 | ||
|
|
77ba02f0ae | ||
|
|
effdc862a2 | ||
|
|
59c4736f41 | ||
|
|
dff831eafe | ||
|
|
4765640dc7 | ||
|
|
b8799a91b4 | ||
|
|
17aaa90fb1 | ||
|
|
40b8969d44 | ||
|
|
73454d97e1 | ||
|
|
7cf39aac5a | ||
|
|
2880168566 | ||
|
|
615d9240ad | ||
|
|
76f8aa4f60 | ||
|
|
0172dd0b53 | ||
|
|
ccb6a5cf0f | ||
|
|
7dc37c9dca | ||
|
|
69f13621ca | ||
|
|
92b9efea12 | ||
|
|
d1af92942d | ||
|
|
b47b5e2dc0 | ||
|
|
3265a94d26 | ||
|
|
e9d9dc2748 | ||
|
|
8b38d4967c | ||
|
|
3729070e33 | ||
|
|
d415c20446 | ||
|
|
efb5931be3 | ||
|
|
4867a5510a | ||
|
|
932dd79ac1 | ||
|
|
dc5957dd0a | ||
|
|
bb131b4ff5 | ||
|
|
76e3d3523e | ||
|
|
2bc9dd2802 | ||
|
|
0950c3d80d | ||
|
|
808ea00787 | ||
|
|
c4400f8a64 | ||
|
|
4a78cfe00a | ||
|
|
a10f8939e2 | ||
|
|
4b681a5e55 | ||
|
|
4e073b4681 | ||
|
|
e9548b2e03 | ||
|
|
05820e973b | ||
|
|
f779b6fe7d | ||
|
|
210c46f49d | ||
|
|
e746ddc525 | ||
|
|
d186d4ce1e | ||
|
|
cee80cc579 | ||
|
|
1c0232cf96 | ||
|
|
77ff7a8aba | ||
|
|
fed48de0d5 | ||
|
|
95c1df229f | ||
|
|
78b2cb8a32 | ||
|
|
e1ba7a0ad6 | ||
|
|
e64597eaf0 | ||
|
|
f1b7ea13ef | ||
|
|
25d6a0a160 | ||
|
|
5be13a645e | ||
|
|
dfa85e9886 | ||
|
|
8a612970e6 | ||
|
|
f18a9fe7f5 | ||
|
|
a5d0fe5b8b | ||
|
|
3ef70a1213 | ||
|
|
3bd2136450 | ||
|
|
c165146d7c | ||
|
|
adfdae3a8e | ||
|
|
f92aae4e6e | ||
|
|
bed867cf7b | ||
|
|
81aaeaf25f | ||
|
|
3cc62563df | ||
|
|
aaa21be642 | ||
|
|
aa87cb0064 | ||
|
|
1e6e2d32e3 | ||
|
|
a95805bc04 | ||
|
|
a5ccce30d6 | ||
|
|
f6fdd79d0f | ||
|
|
3f9c1af8e8 | ||
|
|
be9a88029b | ||
|
|
de53581826 | ||
|
|
d8db54d8eb | ||
|
|
64aa8e9190 | ||
|
|
7719fb5fed | ||
|
|
e5d8fac004 | ||
|
|
c8ce6e0dc1 | ||
|
|
7523ff50ff | ||
|
|
74cd7f07a0 | ||
|
|
1edfcc2c42 | ||
|
|
631f283cba | ||
|
|
a33609b144 | ||
|
|
572e028917 | ||
|
|
31262f9f7f | ||
|
|
e5af0e778f | ||
|
|
97a6d23d36 | ||
|
|
773d680e42 | ||
|
|
a43236dfc3 | ||
|
|
12e65994ae | ||
|
|
d7b43ef5cf | ||
|
|
97a7496854 | ||
|
|
e6e24ef953 | ||
|
|
1903fc2a6f | ||
|
|
94613f0458 | ||
|
|
2072fee01a | ||
|
|
b1658f3799 | ||
|
|
115654fa1d | ||
|
|
8c3179f0fe | ||
|
|
4313aed9f0 | ||
|
|
1cbc3a0063 | ||
|
|
102e5cba6b | ||
|
|
e932780f02 | ||
|
|
1b0415f81d | ||
|
|
aa82383d2c | ||
|
|
57d1c2a81a | ||
|
|
606ddb7b38 | ||
|
|
f3f7829bbe | ||
|
|
ae9a009f1f | ||
|
|
6c6bf7870d | ||
|
|
6cff9d6fb5 | ||
|
|
0a71be1205 | ||
|
|
3736c3380c | ||
|
|
1beb25a820 | ||
|
|
202eb95eb3 | ||
|
|
eaa5a326fe | ||
|
|
409d3de69c | ||
|
|
e05ee6bba0 | ||
|
|
111ed742ec | ||
|
|
8fd5743d75 | ||
|
|
63b79ccf3b | ||
|
|
45f4265c03 | ||
|
|
b7b13ea2cb | ||
|
|
1f660b180e | ||
|
|
3b2ccf75ec | ||
|
|
734fc65c29 | ||
|
|
5252ed16ca | ||
|
|
cec6fcf81a | ||
|
|
a812796bdc | ||
|
|
9abb4fe692 | ||
|
|
42b86c6b18 | ||
|
|
8aec2275fd | ||
|
|
61f03d734b | ||
|
|
907e20d0ec | ||
|
|
f84c759e8d | ||
|
|
f6fb4695c1 | ||
|
|
3a1ccb5ba0 | ||
|
|
7b8ab4ac2c | ||
|
|
53504f1c3d | ||
|
|
d5408165f9 | ||
|
|
9bf8c115c1 | ||
|
|
a06ac4cbb6 | ||
|
|
6406b7412d | ||
|
|
41aca47f92 | ||
|
|
a170e1cfb5 | ||
|
|
b2429a6a1b | ||
|
|
23e1baf92f | ||
|
|
278c94b7df | ||
|
|
256fc5a222 | ||
|
|
a0fa28b3fd | ||
|
|
09d8212225 | ||
|
|
e658786e88 | ||
|
|
4a1e6eba8c | ||
|
|
ce5e209681 | ||
|
|
40178b5277 | ||
|
|
631c487233 | ||
|
|
5e47afe3a4 | ||
|
|
942bf9094d | ||
|
|
985200b6d9 | ||
|
|
35b61bc891 | ||
|
|
b149abbb23 | ||
|
|
536387ad8c | ||
|
|
1628edbb8b | ||
|
|
39adbbdb27 | ||
|
|
be49fa9b54 | ||
|
|
aaed8435d1 | ||
|
|
c5bd406351 | ||
|
|
4004caadc7 | ||
|
|
1ac2a782c4 | ||
|
|
6887928bba | ||
|
|
1401537796 | ||
|
|
e53f5ca175 | ||
|
|
463f49586c | ||
|
|
5a87098f95 | ||
|
|
da44ff05aa | ||
|
|
3839171d42 | ||
|
|
8d9a009f89 | ||
|
|
2c0a4cf7a7 | ||
|
|
4b2269b668 | ||
|
|
3bfff093f8 | ||
|
|
6d835a2068 | ||
|
|
36398c54e0 | ||
|
|
2a2777b22d | ||
|
|
11dc69334d | ||
|
|
c4c17aa115 | ||
|
|
4b41c06dc4 | ||
|
|
a9e27cd63f | ||
|
|
0f6b4f2b32 | ||
|
|
08877155e2 | ||
|
|
7007f3ea44 | ||
|
|
9419cce747 | ||
|
|
033c884059 | ||
|
|
99b0b65e89 | ||
|
|
67042470f3 | ||
|
|
7c5388ee71 | ||
|
|
96634ece3f | ||
|
|
ee9ea92a11 | ||
|
|
246e9f7e3f | ||
|
|
fc43f89d9e | ||
|
|
be75dc95a3 | ||
|
|
6bb0f7b902 | ||
|
|
6178c56606 | ||
|
|
0a2ca923ee | ||
|
|
5adc4cb437 | ||
|
|
28d4371f4d | ||
|
|
d343bbe1ac | ||
|
|
c6a6163aec | ||
|
|
be60a37a29 | ||
|
|
b3d42866b7 | ||
|
|
9633fb659a | ||
|
|
fab637f5ae | ||
|
|
edcd991659 | ||
|
|
84f41b9c2f | ||
|
|
e7a61f07a2 | ||
|
|
57616ffd83 | ||
|
|
ea002e6634 | ||
|
|
24574360a0 | ||
|
|
e4dff4916b | ||
|
|
bd1ff4c954 | ||
|
|
563c762756 | ||
|
|
6c6bd65969 | ||
|
|
e2dbd5216d | ||
|
|
959a05643f | ||
|
|
3e0f33859a | ||
|
|
b70615c19c | ||
|
|
f6956baf89 | ||
|
|
47cd7cf2a2 | ||
|
|
a1d6d17685 | ||
|
|
fe768a650d | ||
|
|
d9ba282024 | ||
|
|
1b48cc99e5 | ||
|
|
8740e20c90 | ||
|
|
93a2cdf4bf | ||
|
|
0faa4e62f0 | ||
|
|
78e97e9731 | ||
|
|
d59be759bd | ||
|
|
2df78eb436 | ||
|
|
3f132a759f | ||
|
|
b87244bf9a | ||
|
|
6d826001cc | ||
|
|
2de4d36c9f | ||
|
|
ef01212ce8 | ||
|
|
9fa8c36b5f | ||
|
|
a4973616b4 | ||
|
|
a8dd2ed2da | ||
|
|
2766667d16 | ||
|
|
a7e7b9a3ca | ||
|
|
7df272fd4a | ||
|
|
cd694366ed | ||
|
|
4a9fb62663 | ||
|
|
8c7144205b | ||
|
|
f4057d4c2c | ||
|
|
ad5de8c84c | ||
|
|
15f414cae3 | ||
|
|
54d01d2ffd | ||
|
|
f8e87c5aa1 | ||
|
|
090a85bdd5 | ||
|
|
403611443f | ||
|
|
82b14a14e0 | ||
|
|
a0789b45e4 | ||
|
|
12975b1ecf | ||
|
|
59de1212cb | ||
|
|
cc8246b474 | ||
|
|
f13a91e83e | ||
|
|
598aae8ef1 | ||
|
|
fe5414bdf4 | ||
|
|
fd40289887 | ||
|
|
225b8aa63a | ||
|
|
e63bbe734c | ||
|
|
8c76fb6f7c | ||
|
|
617ed0a3cd | ||
|
|
e32e7dd828 | ||
|
|
d6ba027ae7 | ||
|
|
1ea4a7c113 | ||
|
|
8d7e161662 | ||
|
|
332b04d640 | ||
|
|
e7af8305c2 | ||
|
|
edffba3496 | ||
|
|
88834250c5 | ||
|
|
0da15c21e6 | ||
|
|
3076f98127 | ||
|
|
5a7f52b41f | ||
|
|
f403ff7776 | ||
|
|
57998195f6 | ||
|
|
e873150542 | ||
|
|
73440be270 | ||
|
|
e8caa8853e | ||
|
|
4d63643fbf | ||
|
|
c632303fd6 | ||
|
|
06596d8626 | ||
|
|
e0a47b050a | ||
|
|
55bd9bbc7e | ||
|
|
a4c47b920c | ||
|
|
25355596bb | ||
|
|
a40d128704 | ||
|
|
2f0ab09250 | ||
|
|
6be425de4d | ||
|
|
aa55b984a2 | ||
|
|
35725b2324 | ||
|
|
bb4a3487a2 | ||
|
|
2f51f985d8 | ||
|
|
bacdca038e | ||
|
|
f80e190e22 | ||
|
|
024aec1891 | ||
|
|
b80f6cc507 | ||
|
|
be11046cfc | ||
|
|
0a0522d92d | ||
|
|
f436a34474 | ||
|
|
7ab3884cb9 | ||
|
|
b616be8c0e | ||
|
|
65431c462f | ||
|
|
bc6ae0e773 | ||
|
|
5698b2eab9 | ||
|
|
4b1ff7deb5 | ||
|
|
327c0d7a2e | ||
|
|
90522914c0 | ||
|
|
cf44a36153 | ||
|
|
ba23cfdaca | ||
|
|
a85888bcbd | ||
|
|
3d8c25159d | ||
|
|
543f73bf7a | ||
|
|
4130082a8d | ||
|
|
692815713b | ||
|
|
14bef07d25 | ||
|
|
e9c69a118c | ||
|
|
275faea616 | ||
|
|
f4268bb447 | ||
|
|
35eeae7c58 | ||
|
|
ed04df26f8 | ||
|
|
79efaad817 | ||
|
|
1075745439 | ||
|
|
2f7255301d | ||
|
|
fc60d50560 | ||
|
|
488d32974f | ||
|
|
bdd12b262e | ||
|
|
ec8e645679 | ||
|
|
a239c923a3 | ||
|
|
e2fa8b3199 | ||
|
|
2128f46165 | ||
|
|
1378cab008 | ||
|
|
65aca8ab76 | ||
|
|
9db42c9783 | ||
|
|
d9e551031d | ||
|
|
554a163d7d | ||
|
|
858a59568e | ||
|
|
e0eabbebd1 | ||
|
|
1b5c675711 | ||
|
|
bf4daff685 | ||
|
|
d669b19906 | ||
|
|
95e665a917 | ||
|
|
4557094716 | ||
|
|
eb8e585c8c | ||
|
|
5052d1263b | ||
|
|
14bd2c6a3b | ||
|
|
109ee591c1 | ||
|
|
c67cf56dc5 | ||
|
|
727dcc149d | ||
|
|
b450178444 | ||
|
|
23e281689a | ||
|
|
f96eb630d1 | ||
|
|
7cd16c7063 | ||
|
|
4734d2645f | ||
|
|
87a04193f9 | ||
|
|
ea8326ca24 | ||
|
|
5c196d7e47 | ||
|
|
8db1b230d4 | ||
|
|
f92e98d587 | ||
|
|
9b3ed76fb0 | ||
|
|
91780f0b9c | ||
|
|
0f7f7946cd | ||
|
|
c4a0037956 | ||
|
|
586492fc92 | ||
|
|
cfe34628fa | ||
|
|
1e2b7c7e02 | ||
|
|
c12a91ee5f | ||
|
|
c93a2dcb1c | ||
|
|
9baa529200 | ||
|
|
29bf1e5dc4 | ||
|
|
0f1b78e1a5 | ||
|
|
d03820bf91 | ||
|
|
adbf7aeb42 | ||
|
|
c3cadf0db3 | ||
|
|
b57bf29247 | ||
|
|
c5f1289aee | ||
|
|
4a96468e42 | ||
|
|
272c7850d7 | ||
|
|
7466bda816 | ||
|
|
c202399eb6 | ||
|
|
3c046020ef | ||
|
|
d4ffc21a11 | ||
|
|
aefb061f22 | ||
|
|
af3ab140bd | ||
|
|
9d9d43a249 | ||
|
|
5045447bc6 | ||
|
|
a9772dd313 | ||
|
|
675d161df0 | ||
|
|
fe45fd263b | ||
|
|
2e7d8299a1 | ||
|
|
59999abb61 | ||
|
|
23a42b7c48 | ||
|
|
8bda1fe719 | ||
|
|
207e55e869 | ||
|
|
a09b8f1762 | ||
|
|
28f23ae595 | ||
|
|
da0dc31ed0 | ||
|
|
6a9873dbaa | ||
|
|
c68d311f92 | ||
|
|
c148fa9000 | ||
|
|
26fc48ce14 | ||
|
|
0eb7174183 | ||
|
|
248e8750e1 | ||
|
|
6c240fc5d3 | ||
|
|
def3d617b0 | ||
|
|
c49314e755 | ||
|
|
18a80d4fdd | ||
|
|
195e136798 | ||
|
|
606e702e6f | ||
|
|
99d3925b6c | ||
|
|
aed7a6fbf3 | ||
|
|
1d584235e0 | ||
|
|
bbb79bba0f | ||
|
|
731a838c16 | ||
|
|
1a55c472e0 | ||
|
|
222b476d84 | ||
|
|
0e42f31b88 | ||
|
|
d5b3f605f3 | ||
|
|
4e6c354ff9 | ||
|
|
7203165d88 | ||
|
|
17471db248 | ||
|
|
e2cdff3604 | ||
|
|
118b1a039b | ||
|
|
127ead0518 | ||
|
|
ea8119f3ad | ||
|
|
9b0f548336 | ||
|
|
e8a7c15fee | ||
|
|
6055127118 | ||
|
|
81e4a402f2 | ||
|
|
ca975e4f94 | ||
|
|
5bf0f08f25 | ||
|
|
2e06972161 | ||
|
|
d6f26f78a5 | ||
|
|
8eb4c1e7c1 | ||
|
|
3b4f0f67f7 | ||
|
|
bf1436fdff | ||
|
|
0bd47c8c72 | ||
|
|
7599ec6248 | ||
|
|
266df9bf71 | ||
|
|
69b937321c | ||
|
|
ae53634f48 | ||
|
|
09d8e1ce6b | ||
|
|
5751a5c7e5 | ||
|
|
535069587e | ||
|
|
f9550e93d3 | ||
|
|
5318fbaca1 | ||
|
|
5f251c296e | ||
|
|
be7278294a | ||
|
|
291f87e197 | ||
|
|
cca86141fd | ||
|
|
84c4fb825c | ||
|
|
cd9bb16f79 | ||
|
|
79272be631 | ||
|
|
4b9d03fb59 | ||
|
|
3c95e88f08 | ||
|
|
73ed4fa6f3 | ||
|
|
6451f580cb | ||
|
|
dcf8a4948b | ||
|
|
3458cec41e | ||
|
|
809008561f | ||
|
|
72d60e1227 | ||
|
|
8fc335718a | ||
|
|
e6129ad78b | ||
|
|
10802d6a2c | ||
|
|
7b05d26d7e | ||
|
|
fa8d67ebe1 | ||
|
|
0d320c26cd | ||
|
|
96c0276a0e | ||
|
|
3cc5a8ae5c | ||
|
|
0bb15cc6b8 | ||
|
|
5b0ca03640 | ||
|
|
df5abad690 | ||
|
|
549758d789 | ||
|
|
b49a850297 | ||
|
|
5cc1c11f1d | ||
|
|
1e5a596aae | ||
|
|
c63d3a5b61 | ||
|
|
0b8f195249 | ||
|
|
0af08cf578 | ||
|
|
dfe21c5b8c | ||
|
|
f8c5da52f3 | ||
|
|
20752bf48e | ||
|
|
7778790bae | ||
|
|
1b04a50836 | ||
|
|
fccbc90307 | ||
|
|
c5895a7d21 | ||
|
|
9262c8527b | ||
|
|
32484570bb | ||
|
|
f26f6c33d0 | ||
|
|
ec2db0594e | ||
|
|
a6f3f425c3 | ||
|
|
54c1e64739 | ||
|
|
e6e88d2a2b | ||
|
|
7cd229acf0 | ||
|
|
891c540d60 | ||
|
|
ebd8f300d0 | ||
|
|
4566fec58a | ||
|
|
a362206e55 | ||
|
|
a9543e50f2 | ||
|
|
06fc3e726a | ||
|
|
e19a2ac67a | ||
|
|
5e28fb5246 | ||
|
|
2ad9fcf701 | ||
|
|
427b38912f | ||
|
|
5d4619a70a | ||
|
|
5e56f27e45 | ||
|
|
92fca450f1 | ||
|
|
e52f8bbbde | ||
|
|
94c6ccb549 | ||
|
|
a8edd8ffb9 | ||
|
|
52dee04e72 | ||
|
|
e34f030073 | ||
|
|
2b86c24175 | ||
|
|
298768f0cb | ||
|
|
0ba01ddcdd | ||
|
|
abc6ae2dca | ||
|
|
e3d47e1e5d | ||
|
|
17dae79660 | ||
|
|
d6336710b8 | ||
|
|
ed25a4191d | ||
|
|
5723097cbe | ||
|
|
b566098a56 | ||
|
|
f8f34e46e3 | ||
|
|
9227831922 | ||
|
|
625d4c951e | ||
|
|
38b600520f | ||
|
|
0fae016d15 | ||
|
|
5cdc479029 | ||
|
|
d385585291 | ||
|
|
248107bfb8 | ||
|
|
539e336fa1 | ||
|
|
8ab07563e0 | ||
|
|
74b4013002 | ||
|
|
eede8bdc2f | ||
|
|
7db0de7ccc | ||
|
|
487332df40 | ||
|
|
68d156c0e8 | ||
|
|
a9a5622525 | ||
|
|
9e02950844 | ||
|
|
a1730c9524 | ||
|
|
ed3257399b | ||
|
|
763f65cbbe | ||
|
|
364cde1287 | ||
|
|
26675eac64 | ||
|
|
4789be70fc | ||
|
|
2b0168296f | ||
|
|
c532d74f8b | ||
|
|
2a8868e029 | ||
|
|
7410abf895 | ||
|
|
86d0c3bfcb | ||
|
|
fb14747a8b | ||
|
|
2303c54ac5 | ||
|
|
6762ce347d | ||
|
|
2e884339a8 | ||
|
|
3bdb18a2b4 | ||
|
|
aaf2b7e206 | ||
|
|
ef7a711f07 | ||
|
|
d11fbc1069 | ||
|
|
b48d8d4372 | ||
|
|
e257a64772 | ||
|
|
a6b1961d77 | ||
|
|
dc95bad4aa | ||
|
|
327d0277fc | ||
|
|
d363212191 | ||
|
|
266bf202bb | ||
|
|
f280445138 | ||
|
|
fcd93c8db8 | ||
|
|
72add39182 | ||
|
|
d02f0bf5b4 | ||
|
|
509a946c92 | ||
|
|
986734e29b | ||
|
|
147d9dbe83 | ||
|
|
c974853fe8 | ||
|
|
d124f9c8e0 | ||
|
|
cb9222271d | ||
|
|
72505b550d | ||
|
|
b10946f0e8 | ||
|
|
6bcb1ab113 | ||
|
|
9dc22142ef | ||
|
|
e23818b3b4 | ||
|
|
0eef72b6e3 | ||
|
|
00995e3a6a | ||
|
|
089dad31c0 | ||
|
|
b79d36e0eb | ||
|
|
07a78b55cb | ||
|
|
7aa6f8fa9d | ||
|
|
47eaa1ac96 | ||
|
|
0866632b2f | ||
|
|
7500b602dc | ||
|
|
31b341cb68 | ||
|
|
7c62945560 | ||
|
|
59fb3c1dbe | ||
|
|
ea86e2990b | ||
|
|
b2e0ec4130 | ||
|
|
de8a73efb7 | ||
|
|
df95c2c85d | ||
|
|
df322ceacf | ||
|
|
78df3fc12c | ||
|
|
b1a1824522 | ||
|
|
ffab68e809 | ||
|
|
89410cc55d | ||
|
|
9d3b17d86b | ||
|
|
a4a242d58b | ||
|
|
b488764b79 | ||
|
|
38848f43e4 | ||
|
|
9ffea5cadb | ||
|
|
9e05675823 | ||
|
|
3b4efc21a6 | ||
|
|
beaf31ae05 | ||
|
|
631e0b0741 | ||
|
|
553e3b02f5 | ||
|
|
c11db9f0af | ||
|
|
9ce91345a7 | ||
|
|
a6653a3419 | ||
|
|
756f1dd218 | ||
|
|
accf332668 | ||
|
|
8483b0d08e | ||
|
|
b46be2445a | ||
|
|
d45328576f | ||
|
|
a31cc33c67 | ||
|
|
b874147b9c | ||
|
|
4b0b5cb85c | ||
|
|
23ad776a74 | ||
|
|
02cc9ec935 | ||
|
|
1d0c21eff1 | ||
|
|
422c97f681 | ||
|
|
f0d00420c3 | ||
|
|
64c1eac2d0 | ||
|
|
09046ab89e | ||
|
|
ae7c98ce78 | ||
|
|
62bff009c1 | ||
|
|
0c3ea4b05d | ||
|
|
9dde2fbcf8 | ||
|
|
740041a844 | ||
|
|
31bcb8c82d | ||
|
|
425ef9f059 | ||
|
|
e059ecc6c4 | ||
|
|
e04fc7d950 | ||
|
|
97f75fb971 | ||
|
|
425a8c864b | ||
|
|
56c2b1d9b5 | ||
|
|
ab74d19584 | ||
|
|
3e197b72a8 | ||
|
|
863e1a2e20 | ||
|
|
a00c8e0a53 | ||
|
|
3b98206942 | ||
|
|
f6cee7297a | ||
|
|
6792e3d701 | ||
|
|
df1e5aef9b | ||
|
|
6c3d2926b7 | ||
|
|
302fcfc7d7 | ||
|
|
42dd9969e4 | ||
|
|
6a69ce26e3 | ||
|
|
8d6db96a4c | ||
|
|
e61a6bae7e | ||
|
|
37989854a7 | ||
|
|
c70f8441e2 | ||
|
|
f9073055e9 | ||
|
|
a22cdd9553 | ||
|
|
67b8166ebc | ||
|
|
762e498a5c | ||
|
|
48c9862e18 | ||
|
|
a8b7faaed3 | ||
|
|
c56abc020e | ||
|
|
20067b91eb | ||
|
|
43004af842 | ||
|
|
6eb51fdafd | ||
|
|
c3f831727b | ||
|
|
6d328eb376 | ||
|
|
b9424f41ab | ||
|
|
b8aa1af55a | ||
|
|
1ba6e361bd | ||
|
|
0e2c411198 | ||
|
|
ca7917b407 | ||
|
|
69013d2765 | ||
|
|
8a022a2365 | ||
|
|
0d07af4862 | ||
|
|
d5279fb50c | ||
|
|
7d887bdbef | ||
|
|
d688db95e9 | ||
|
|
d12770f97c | ||
|
|
a3d9efbda2 | ||
|
|
3f6479e578 | ||
|
|
9c14b42bda | ||
|
|
5f04224d57 | ||
|
|
5d8d8a5ffe | ||
|
|
9d511e0370 | ||
|
|
37f3bb42a0 | ||
|
|
6af2b098e2 | ||
|
|
a30a0f8d0b | ||
|
|
73d26b45fc | ||
|
|
03d1cc4f78 | ||
|
|
49c89810d8 | ||
|
|
4dc3647370 | ||
|
|
b78c37dbbe | ||
|
|
6ab00e46b2 | ||
|
|
ec22d72f3f | ||
|
|
e7fdb804ae | ||
|
|
6749aee5cd | ||
|
|
c3b846bac7 | ||
|
|
5c9bb477b4 | ||
|
|
97324ce4d4 | ||
|
|
a11d84e812 | ||
|
|
57247cd5cd | ||
|
|
1d7774bcd6 | ||
|
|
415ab6298e | ||
|
|
806a8efe94 | ||
|
|
50925c4c30 | ||
|
|
c9d12184e0 | ||
|
|
b0894b1e75 | ||
|
|
c2781b1f8b | ||
|
|
bd2ccc3612 | ||
|
|
92faccdd90 | ||
|
|
5b42b41dcb | ||
|
|
8a5c429e87 | ||
|
|
194923ca27 | ||
|
|
a2b8391174 | ||
|
|
c0fd535067 | ||
|
|
6629c38d2a | ||
|
|
58032012aa | ||
|
|
7025accd88 | ||
|
|
cc40afcc4d | ||
|
|
3ff2b5f546 | ||
|
|
f5e2309563 | ||
|
|
75aa6d50b3 | ||
|
|
021ad2a5c2 | ||
|
|
ffe486e284 | ||
|
|
6132aa7864 | ||
|
|
56e55d9cd6 | ||
|
|
2f0e91d96e | ||
|
|
32a45ad0d3 | ||
|
|
f0d0d6b73a | ||
|
|
03c470846b | ||
|
|
66f347c973 | ||
|
|
1cf8cae166 | ||
|
|
f2585438bd | ||
|
|
f1e2c5b0d1 | ||
|
|
466ba18ec8 | ||
|
|
844fb2e6ec | ||
|
|
41d8e2a0ae | ||
|
|
49ea016980 | ||
|
|
34a8972ce4 | ||
|
|
a980e1910c | ||
|
|
10758879db | ||
|
|
1e0986b1f6 | ||
|
|
57e16f46ce | ||
|
|
cc076cc803 | ||
|
|
254262c5c4 | ||
|
|
dc8279c3ea | ||
|
|
d34d9316f3 | ||
|
|
7496b1de21 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -37,6 +37,7 @@ nosetests.xml
|
||||
|
||||
# PyCharm
|
||||
.idea
|
||||
/.eggs
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
@@ -50,3 +51,11 @@ nosetests.xml
|
||||
|
||||
# Qt creator
|
||||
*.autosave
|
||||
|
||||
# Licence keys
|
||||
keys
|
||||
|
||||
# Custom config
|
||||
/gns3_server.ini
|
||||
updates
|
||||
.cache
|
||||
|
||||
31
.travis.yml
31
.travis.yml
@@ -1,24 +1,12 @@
|
||||
language: python
|
||||
sudo: required
|
||||
|
||||
#New container architecture
|
||||
#http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
#sudo: false
|
||||
|
||||
python:
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
|
||||
cache:
|
||||
apt: true
|
||||
directories:
|
||||
- build
|
||||
|
||||
before_install:
|
||||
- sh scripts/prepare_travis.sh
|
||||
services:
|
||||
- docker
|
||||
|
||||
notifications:
|
||||
email:
|
||||
- julien@gns3.net
|
||||
email: false
|
||||
#email:
|
||||
# - julien@gns3.net
|
||||
#irc:
|
||||
# channels:
|
||||
# - "chat.freenode.net#gns3"
|
||||
@@ -26,8 +14,9 @@ notifications:
|
||||
# on_failure: always
|
||||
|
||||
install:
|
||||
- "pip install -r dev-requirements.txt"
|
||||
- "pip install --upgrade -r dev-requirements.txt"
|
||||
|
||||
script:
|
||||
- "xvfb-run py.test -vv" # Run tests in a fake X server
|
||||
# - "pep8 --exclude=build,.git,ui"
|
||||
- docker build -t gns3-gui-test .
|
||||
- docker run gns3-gui-test
|
||||
|
||||
|
||||
646
CHANGELOG
646
CHANGELOG
@@ -1,5 +1,651 @@
|
||||
# Change Log
|
||||
|
||||
## 1.4.0 12/01/2016
|
||||
|
||||
* Fix rare crash when showing the progress dialog
|
||||
* Fix crash on Windows when a gui is already running
|
||||
* Add default idle-pc value for c7200-adventerprisek9-mz.155-2.XB. Fixes #389.
|
||||
|
||||
## 1.4.0rc3 05/01/2016
|
||||
|
||||
* Add information about antivirus and firewall in case of connection fail
|
||||
* Change link to doc for missing router image
|
||||
* On windows and OSX experimental Qemu GNS3A support
|
||||
* Wait server in thread
|
||||
* Fix local server non avaible in appliance wizard when local server started by hand.
|
||||
* Improve pid checks
|
||||
* Allow Qemu appliances to optionally specify desired disk interfaces in their configuration (defaults to "ide").
|
||||
* Fix project non closing when you have only remote servers
|
||||
* Fix Windows layout not saved in some scenario
|
||||
* Added the ability to name Qemu/VirtualBox/VMware interfaces with numbers starting from 1, along with named aliases for numbers starting from 0, and a tooltip explaining the possible name format variables.
|
||||
* Added missing Qemu adapters to the topology schema.
|
||||
* Fix Cannot save my topology getting an error message for temporary topology
|
||||
* Fix creation of ASA devices
|
||||
* Turn off Docker until 1.5
|
||||
* Fix display of server preferences on small screen
|
||||
* Fix If you turn off the local server and close the gui and reopen preferences you have an issue
|
||||
* Zoc 7 support
|
||||
* Fix warning when closing GUI
|
||||
* Fix crash when opening a new topologies after gns3 converter failure
|
||||
|
||||
## 1.3.13 11/12/2015
|
||||
|
||||
* Release server 1.3.13
|
||||
|
||||
## 1.3.12 11/12/2015
|
||||
|
||||
* Fix warning when closing GUI
|
||||
* Update links for new website.
|
||||
* Ask user to send bug reports to GNS3.com
|
||||
* Fix travis dependencies installation
|
||||
* Drop Webkit from 1.3.X
|
||||
* Fix crash when opening a new topologies after gns3 converter failure
|
||||
* Set Wireshark 2.0 as default OSX version
|
||||
* iTerm 2.9 support
|
||||
* Add informations about GNS3 VM
|
||||
* Drops securecrt.vbs
|
||||
* Fix analytics report on OSX
|
||||
* Analytics send windows
|
||||
* Fix the progress dialog freeze bug
|
||||
* Fix typo in analytics
|
||||
* Send stats to GNS3 team
|
||||
* Licenses compliance.
|
||||
* Fix error when importing dynamips config from non existent directory
|
||||
* Fix crash when url is invalid
|
||||
* Add a debug level 2 in the console
|
||||
|
||||
## 1.4.0rc2 10/12/2015
|
||||
|
||||
* Prevent user turning off the Local server when using the GNS3 VM
|
||||
* Force VM wizard to be modal
|
||||
* Fix unicode error when exporting debug informations
|
||||
* Fix Debug can't be deactivated for current session
|
||||
* Support relative path for configuration file.
|
||||
* Report bug to GNS3.com
|
||||
* Avoid crash when cancel connection to a server
|
||||
* Fix version number display twice when installing appliance
|
||||
* Show a warning when you try to save as a remote topology
|
||||
* Fix application restart after self upgrade
|
||||
* Cleanup server autostart
|
||||
* Have default console port start from 2000.
|
||||
* Fix crash when opening a new topologies after gns3 converter failure
|
||||
* Fix SSH support
|
||||
* Fix bus error when writting on console
|
||||
* Fix Ok & Cancel button in preferences are broken
|
||||
* Fix After a project load failure you can't open new project
|
||||
* More fix around closing the GUI
|
||||
* Fix crash in progress dialog on OSX
|
||||
* Kill already running zombie server
|
||||
* Store the pid of the server when started
|
||||
* Fix a crash in rare case after a PyQT garbage collect
|
||||
* Allow to use the VNC port range for console
|
||||
* Preferences dialog resize
|
||||
* Fix a race condition when opening telnet from apple script
|
||||
* Experimental support for tabbed terminal on OSX
|
||||
* Fix validation error when saving topology with an usage
|
||||
* Fix another case of not closing windows
|
||||
* Fix GUI doesn't close after connection error to remote server
|
||||
* Add usage text to device template and on hover
|
||||
* Fix a rare crash in progress dialog
|
||||
* Fix crash when displaying an error from the update
|
||||
* Url encode royal tx url
|
||||
* Use Royal TX URI scheme thanks to @lemonmojo
|
||||
* OSX support for Royal TSX
|
||||
* Merge branch 'master' into unstable
|
||||
* Set Wireshark 2.0 as default OSX version
|
||||
* iTerm 2.9 support
|
||||
* Update debug information dialog.
|
||||
* Change text for export debug information.
|
||||
* Add informations about GNS3 VM
|
||||
|
||||
## 1.4.0rc1 12/15/2015
|
||||
|
||||
* Rename an appliance if the default name is already taken
|
||||
* Existing image option should be hidden when none is available
|
||||
* Warn users about the need to uncompress the image
|
||||
* Fix crash when you have no qemu vms and use gns3a
|
||||
* Change sentry key
|
||||
* Fix format_exception() missing 2 required positional arguments: 'value' and 'tb' in topologyFile
|
||||
* Fix dialog box not returning their result
|
||||
* Log to console the Qt Message Boxes
|
||||
* Drops securecrt.vbs
|
||||
|
||||
## 1.4.0b5 02/11/2015
|
||||
|
||||
* Fix crash when loading invalid appliance file
|
||||
* Show a message is starting or is stopping in progress dialog
|
||||
* Drop duplicate code
|
||||
* Changes some references for "GNS3 VM" to "Local GNS3 VM". Ref #506.
|
||||
* Fixes progress dialog remains #741.
|
||||
* Support more boot order for Qemu
|
||||
* Add Royal TS terminal
|
||||
* Add a Qt compatible version of partial
|
||||
* Show Upload filename instead of waiting for
|
||||
* Fix error where ISO is detected as an OVA during gns3a import
|
||||
* Removes News Dock Widget.
|
||||
* Support cdrom image in missing images detections
|
||||
* Fix crash when appliance is missing
|
||||
* Support for capture description in Wireshark window title.
|
||||
* Set the name of the VM in OSX Terminal application
|
||||
* Fix crash of symbol selector if the first file is a non builtin symbol
|
||||
* Install appliance from wizard instead of web app
|
||||
* Fix crash when using an old version of 1.4 server
|
||||
* Ensure default settings are saved when starting the app
|
||||
|
||||
## 1.4.0b4 19/10/2015
|
||||
|
||||
* Mockup of appliances wizard
|
||||
* Fix tests
|
||||
* Fix Crash when opening an appliance #728
|
||||
* Mock up for appliance wizard.
|
||||
* Registry: add -nographic to Qemu options by default. Fixes #730.
|
||||
* Registry: support for initrd, cpu throttling and process priority.
|
||||
* Support for modifications to a base Qemu VM (not a linked clone).
|
||||
* Registry: adds support for switch category, first_port_name and port_segment_size.
|
||||
* Fix traceback when exiting the GUI
|
||||
* Show a download button
|
||||
* Corrects some typos.
|
||||
* Drops securecrt.vbs
|
||||
* Display an error message if QtWebkit is not installed.
|
||||
* Fix console port lost when applying settings
|
||||
* Remove unused code
|
||||
* Fix analytics report on OSX
|
||||
* Fix invalid path with frozen application
|
||||
* When raising an appliance not found error show full path
|
||||
* Remove unnecessary checks to know if the local server is running.
|
||||
* Analytics send windows
|
||||
* Fixes issue when loading a project using VMware vmnet interfaces. Fixes #319.
|
||||
* Fix the progress dialog freeze bug
|
||||
* Revert "Try to solve Scanning for Appliance images doesn't end"
|
||||
* Try to fix the progress dialog freeze bug
|
||||
* Drop dead code from getting started dialog
|
||||
* Fix Appliance installs image without adapting the filename
|
||||
* Raise error if reference in GNS3a is invalid
|
||||
* Fix typo in analytics
|
||||
* Add information on how to debug
|
||||
* Send stats to GNS3 team
|
||||
* Licenses compliance.
|
||||
* Handles warning notifications.
|
||||
* Try to solve Scanning for Appliance images doesn't end
|
||||
* Fix TypeError: 'NoneType' object is not iterable in isLocalServerRunning
|
||||
* Fix crash AttributeError: 'NoneType' object has no attribute getNewProjectSettings
|
||||
* Fix error when importing dynamips config from non existent directory
|
||||
* Fix crash when url is invalid
|
||||
* Add a debug level 2 in the console
|
||||
* Fix a crash when loading appliance
|
||||
* Merge branch 'master' into unstable
|
||||
* Support upload of multiple vmdk file
|
||||
* Support PNG in the custom symbol selection dialog
|
||||
* Add custom messages when computing Idle-PC values. Fixes #704.
|
||||
* Display the version of Qt in the console
|
||||
* Do not crash when parsing a Qt version with a snapshot notation
|
||||
* Force nc path to /usr/bin/nc on Apple
|
||||
* Revert "Drop netcat for unix socket it's not supported by OSX"
|
||||
* Catch errors when we have an infinite recursion when copying a folder
|
||||
* Fix crash in recent files when changing locale
|
||||
* Catch error when we can't extract egg
|
||||
* Fix securecrt command line (backported from master)
|
||||
* Updates SecureCRT command line.
|
||||
* When it's an ova explain to user he need to download the ova
|
||||
* OVA file support
|
||||
* Ignore .cache directory
|
||||
* Fix update manager crash on Windows
|
||||
* Support for image in local subdirectory
|
||||
* Fix duplicate code
|
||||
* Fixes issue when saving Idle-PC into template. Fixes #674.
|
||||
* Add link for downloading VMware
|
||||
* Cache md5sum in memory when loading a gns3a
|
||||
* Support symbol import from GNS3A
|
||||
* Allow user to select symbol from his library
|
||||
* Improve HTTP progress reliability
|
||||
* Support ubuntu default VNC client (Vinagre)
|
||||
* Adds the COPYING file.
|
||||
* Fix error when receiving an HTTP error during HTTP progress
|
||||
* Support port_name_format in GNS3 a files
|
||||
* options is not mandatory in a .gns3 file
|
||||
* Xshell 5 support
|
||||
* Add missing gns3-converter to requirements.txt
|
||||
* Fixes Qemu binaries not listed in the node configuration dialog. Fixes #683.
|
||||
* Fixes SecureCRT command line.
|
||||
* Speedup directory scan for images when loading a gns3a file
|
||||
* Show a progress bar during directory scan when searching for appliances
|
||||
* Search image by default also in the download directory
|
||||
* Fixes issue when Telnet doesn't let you to login to an appliance on Linux.
|
||||
|
||||
## 1.3.11 07/10/2015
|
||||
|
||||
* Display the version of Qt in the console
|
||||
* Catch errors when we have an infinite recursion when copying a folder
|
||||
* Fix crash in recent files when changing locale
|
||||
* Catch error when we can't extract egg
|
||||
* Updates SecureCRT command line.
|
||||
* Fixes issue when saving Idle-PC into template. Fixes #674.
|
||||
* Adds the COPYING file.
|
||||
* Xshell 5 support
|
||||
* Add missing gns3-converter to requirements.txt
|
||||
* Fixes issue when Telnet doesn't let you to login to an appliance on Linux.
|
||||
* Improve alignments. Fixes #215.
|
||||
* Spelling correction
|
||||
|
||||
## 1.4.0b3 22/09/15
|
||||
|
||||
* Add a warning if you don't select VMware or VBox in Setup Wizard
|
||||
* Fix Configuration not always saved in client
|
||||
* Fixes file path reference issue.
|
||||
* Fix Appliance doesn't work on local Server #669
|
||||
* Allows Qemu VM template editing if the server is not running. Fixes #671.
|
||||
* Fixes NIO_VMNET != NIO_VMnet.
|
||||
* Automatically add the -no-kvm option if -icount is detected to help with the migration of ASA VMs created before version 1.4
|
||||
* Fix the Runtime error
|
||||
* Improve alignments. Fixes #215.
|
||||
* Updates kernel command line of ASA.
|
||||
|
||||
## 1.4.0beta2 17/09/15
|
||||
|
||||
* Drop netcat for unix socket it's not supported by OSX
|
||||
* VMware fusion is supported
|
||||
* Fix race conditions in http_client
|
||||
* On OSX show a warning when using an old Qemu
|
||||
* Tab support for xfce4-terminal
|
||||
* Improve alignments. Fixes #215.
|
||||
* Fixes ethertype validation error.
|
||||
* Improve check for uBridge permissions.
|
||||
* Fix an uuid is display instead of the server url
|
||||
* Set root permission to ubridge on OSX
|
||||
* Show GNS3 version at startup
|
||||
* Allows to select a remote server to run a switch. Fixes #653.
|
||||
* Support for packet capture on VMware VM links.
|
||||
* Always use ubridge with VMware by default
|
||||
* Fix PermissionError: [Errno 1] Operation not permitted when kill process
|
||||
* Support drag & drop gns3a
|
||||
* Fix an error when loading IOU schema with private_config
|
||||
* Support open a file via double click on OSX
|
||||
* Initial version of an appliance file format
|
||||
* Adds docker symbols. Fixes #643.
|
||||
* Call the vmnet management script from the GUI (with admin rights). Implements #639.
|
||||
* Use self update only if experimental features are allowed
|
||||
* Add an option for enabling experimental features
|
||||
* Backport docker support from Google Summer Of Code (not enabled)
|
||||
* Merge branch 'qinq_ethertype' of https://github.com/Bevaz/gns3-gui into Bevaz-qinq_ethertype
|
||||
* Allows VMware VMs to use vmnet interfaces for connections without using uBridge.
|
||||
* Add a firewall symbol
|
||||
* Detect broken link in topologies
|
||||
* Fix project not closing
|
||||
* Fix autostart
|
||||
* Fix file not found exception in vpcs list dir
|
||||
* Add missing virtio-net-pci to the json schema
|
||||
* Fix the all devices views
|
||||
* Fix double click event on Note Item
|
||||
* Fix Accepting insecure https connections creates additional server entry
|
||||
* Allow developer to debug packet capture on Windows
|
||||
* Fix saveAs error unsupported operand type(s) for +=: 'NoneType' and 'str'
|
||||
* Catch error when antivirus corrupt our own JSON errors
|
||||
* Add a note about VIX API require for VMware player
|
||||
* Create image directory if not exists
|
||||
* Allow GUI to be start with python -m gns3
|
||||
* Avoid errors in qemu configuration if server has been deleted
|
||||
* Fix error when the IOS image directory is not writable
|
||||
* Do not crash if something intercept the call to the update server
|
||||
* Fix super(): no arguments in SSH client
|
||||
* Catch error when configuration file contain invalid UTF-8 chars
|
||||
* Fix JSON schema for dynamips power supply and sensors
|
||||
* Fix missing boot_priority in JSON schema
|
||||
* Complete the error message about corrupted topologies
|
||||
* Removes ASAv warning in Qemu Wizard.
|
||||
* EthernetSwitch: Allow to choose ethertype for QinQ outer tag.
|
||||
* Adds missing properties for rectangle and ellipse in schema validation.
|
||||
* Use Qemu 0.11.0 instead of version 0.13.0 on Windows.
|
||||
* Fixes bug when opening Node properties dialog via a double click.
|
||||
* SecureCRT (installed on personal profile) command line.
|
||||
|
||||
## 1.3.10 04/09/2015
|
||||
|
||||
* Updates kernel command line of ASA.
|
||||
* Fix file not found exception in vpcs list dir
|
||||
* Fix saveAs error unsupported operand type(s) for +=: 'NoneType' and 'str'
|
||||
* Catch error when antivirus corrupt our own JSON errors
|
||||
* Use Qemu 0.11.0 instead of version 0.13.0 on Windows.
|
||||
* Removes "resources_type" references. Fixes #493.
|
||||
* Fixes bug when opening Node properties dialog via a double click.
|
||||
* SecureCRT (installed on personal profile) command line.
|
||||
|
||||
## 1.4.0beta1 07/08/2015
|
||||
|
||||
* Show an error if you try to use a local server not started
|
||||
* CLear the list before asking for VM list for Vbox and Vmware
|
||||
* Fix password lost for remote servers
|
||||
* Fix schema for virtualbox 1.3.2 topologies
|
||||
* Refactor all VM wizards
|
||||
* Fix Apply not enabled when removing a remote server
|
||||
* Fix remote server list display when use_local_server for Qemu
|
||||
* Fixes #601 (spelling error).
|
||||
* Store the url of server in gns3_gui for third party apps
|
||||
* Fix error when editing a qemu device with a relative path
|
||||
* Catch exception when starting packet capture reader for a remote packet capture. Fixes #597.
|
||||
* Fixes KeyError: 'vmx_path'. Fixes #595.
|
||||
* Support for CPUs setting for Qemu VMs.
|
||||
* Fix chicken of VNC command and add a warning about bugs in OSX VNC
|
||||
* Fix issue whith auto update when we release a new build
|
||||
|
||||
## 1.4.0alpha4 04/08/2015
|
||||
|
||||
* Use half the available physical memory for the GNS3 VM in the Setup Wizard.
|
||||
* Drop useless notion of resource_type
|
||||
* Fix When you modify config from outside router list is not refresh
|
||||
* Sync auth settings between 1.3 and 1.4
|
||||
* Group HTTP bad request by path
|
||||
* Disallow connection to a different major version in all cases
|
||||
* Show the server choice in wizard if you have a VM or a remote server
|
||||
* Fix local server settings erased at each launch
|
||||
* Support for Qemu disk interfaces, cd/dvd-rom image and boot priority.
|
||||
* Prevents progress dialog to stay displayed (fixes graphical bug).
|
||||
* Check that DHCP is enabled on the VirtualBox host-only network associated with the VirtualBox GNS3 VM. Fixes #287.
|
||||
* Fixes Qt5 incompatibility.
|
||||
* Catch exception when trying to launch Wireshark.
|
||||
* Fixes migration of cloud interfaces. Fixes #582.
|
||||
* Wait for the server to be fully started in the GNS3 VM. Fixes #581.
|
||||
* Fix issue with file upload and Qt 5.5
|
||||
* Improves the symbol dialog. Implements #514.
|
||||
|
||||
## 1.3.9 03/08/2015
|
||||
|
||||
* Catch exception when trying to launch Wireshark.
|
||||
* Backport: fixes migration of cloud interfaces.
|
||||
|
||||
## 1.4.0alpha3 28/07/2015
|
||||
|
||||
* IOUVM converter
|
||||
* Create qemu disk image on remote server
|
||||
* Fix _addRemoteServer() got an unexpected keyword argument 'local'
|
||||
* Do not crash if configuration file is removed
|
||||
* Fixes rare issue when adding a link. Fixes #573.
|
||||
* Fixes crash with PyQt 5.5. Fixes #568.
|
||||
* Changes how to look for the vmrun.exe location.
|
||||
* Inform user before exiting preferences dialog with changes
|
||||
* Write GNS3 upgrade to appdata
|
||||
* Fix windows asking for upgrade to the wrong version
|
||||
|
||||
## 1.3.8 27/07/2015
|
||||
|
||||
* Fixes rare issue when adding a link. Fixes #573.
|
||||
* Backport: option to drop nvram & disk files for IOS routers in order to save disk space.
|
||||
* Avoid the creation of a NIO when one has been cancelled.
|
||||
* Fix Crash with chinese characters
|
||||
* Use the same location for the server config on GUI and server
|
||||
* Catch invalid reply from the remote server
|
||||
|
||||
## 1.4.0alpha2 22/07/2015
|
||||
|
||||
* Cloud support with the GNS3 VM.
|
||||
* Display an error message when Qemu binaries cannot be retrieved in the Qemu VM configuration page.
|
||||
* Remove default FLASH when no hda disk for Qemu VMs. Fixes #535.
|
||||
* Use the registry to find vmrun if the default VMware install path doesn't exist. Fixes #546.
|
||||
* Avoid the creation of a NIO when one has been cancelled.
|
||||
* Fix Crash with chinese characters
|
||||
* Display an error if terminal command is invalid
|
||||
* Prevents "Show in File Manager" to be used with generic switches.
|
||||
* Remove unused dependencies
|
||||
* Drop PyQt4 support and show an error for users
|
||||
* Fixes symbol for VM template gone after restart. Fixes #538.
|
||||
* Fix VirtualBox GNS3 VM
|
||||
* Fix issue with remote server not saved/migrated
|
||||
* Remove ram as a mandatory dynamips settings
|
||||
* Force UTF-8 when reading server configuration file
|
||||
|
||||
## 1.4.0alpha1 09/07/2015
|
||||
|
||||
* Remove unused cloud code from the 1.4
|
||||
* Setup Wizard (to be tweaked). Implements #402.
|
||||
* Adds -no-kvm to the ASA template and ignore -no-kvm on platforms other than Linux. Should resolve #472.
|
||||
* Explicitly set the acceleration method to tcg for ASA templates. Should resolve #472.
|
||||
* Show an error if the console port range overlaps the default VNC port range (5900 to 6000) in the server preferences.
|
||||
* Support self update of the application
|
||||
* Option to adjust the local server IP address to be in the same subnet as the GNS3 VM.
|
||||
* Warning about deprecated ASA on Qemu
|
||||
* Moves KVM setting to Qemu preferences.
|
||||
* VNC console support for Qemu VMs. Implements #447.
|
||||
* Change the location of the config file on OSX
|
||||
* Adds first port name option (for management interfaces). Completes #309.
|
||||
* Add a force quit button when closing the app
|
||||
* Basic auth support for remote servers
|
||||
* Adds symbol overview in tooltips for all symbol text fields.
|
||||
* Remove SVG icons used in hover events.
|
||||
* Support for custom symbols
|
||||
* RAM usage based load balancing. #419.
|
||||
* Creates a new "Servers" config section and moves "LocalServer", "RemoteServers" and "GNS3VM" under it.
|
||||
* Support spaces in the local server log path.
|
||||
* Round-Robin load balancing support. #419.
|
||||
* Auto upload image if missing on remote server
|
||||
* ACPI shutdown support for VMware VMs. Fixes #436.
|
||||
* Add timestamps to gns3_gui.log
|
||||
* Store MD5 of images in topology
|
||||
* SSL support
|
||||
* GNS3 VM support
|
||||
* Add a specific icon for VPCS
|
||||
* Ensure no colored log output on Windows
|
||||
* Enable KVM acceleration option.
|
||||
* Apply the result of the auto Idle-PC feature to other routers with the same IOS image.
|
||||
* Improve config change autodetect
|
||||
* Show in file manager (#260: to complete using the VM directory instead).
|
||||
* Open/save dialog is opened in project folder when importing/exporting configs. Fixes #299.
|
||||
* IPv6 support.
|
||||
* Import/Export support for IOU nvrams.
|
||||
* Option to drop nvram & disk files for IOS routers in order to save disk space.
|
||||
* Fix IOU server edit
|
||||
* JSON schema for checking topologies
|
||||
* Support for base MAC address for Qemu VMs.
|
||||
* Drop Python 3.3
|
||||
* ACPI shutdown support for Qemu VMs.
|
||||
* ACPI shutdown support for VirtualBox VMs.
|
||||
* Rename node configurator to node properties.
|
||||
* Merge pull request #381 from GNS3/doubleclick_label
|
||||
* If you doubleclick on a label we open the change hostname dialog
|
||||
* Fix GNS3 server location for OSX
|
||||
* Serial console implementation for VMware VMs.
|
||||
* Ubridge configuration support.
|
||||
* Adds a wizard for creating images with qemu-img and mofified qemu configuration page to use it.
|
||||
* Download remote project with md5 support
|
||||
* SSH support
|
||||
* Avoid moving .gns3_temporary files.
|
||||
* New inline help text for the idle-pc dialog.
|
||||
* Add support for IOS-XRv under qemu wizard.
|
||||
* Upload images from gui
|
||||
* Text can now has an alpha channel, allowing for transparent or semi-transparent text.
|
||||
* The device list in the configuration dialog is hidden by default when only one device is selected.
|
||||
* Adds multi select support in all device template pages.
|
||||
* Adjusted the double click action so that a click on a stopped node opens the configuration dialog with all selected nodes and a double click on a started node consoles to all selected devices.
|
||||
* VMware support for Windows and Linux
|
||||
* Listen for notifications from servers.
|
||||
* Migration to QT5
|
||||
* Wireshark remote packet capture
|
||||
|
||||
|
||||
## 1.3.7 22/06/2015
|
||||
|
||||
* Makes sure Hub Ethernet port names are string.
|
||||
* Support spaces in the local server log path.
|
||||
* Fixes issue when setting the local server settings.
|
||||
* Fix a crash with Python 3.3
|
||||
* Fixes WICs are not displayed correctly. Fixes #434.
|
||||
* Do not load settings that the GUI doesn't use.
|
||||
|
||||
## 1.3.6 16/06/2015
|
||||
|
||||
* Fix an issue with 1.4dev compatibility
|
||||
|
||||
## 1.3.5 16/06/2015
|
||||
|
||||
* Do not crash in a very rare case on Windows when stoping local server
|
||||
* Escape usage to glob
|
||||
* Fix QMessageBox.NoButton): argument 1 has unexpected type 'Servers'
|
||||
* Turn on/off local server auth
|
||||
* Fix 'ValueError' object has no attribute 'errno' in IOS decompress
|
||||
* Fix error if communication with the update server is intercepted by a third party.
|
||||
* Fix auth errors if you change the local server IP
|
||||
* Support auth for local server
|
||||
* Ensure no colored log output on Windows
|
||||
* Fixes issue with default router settings for templates.
|
||||
* Display a proper message if you use a remote server started with --local
|
||||
* Catch zlib error when uncompress IOS
|
||||
* Raise error if we pass non string to Port name
|
||||
* Add basic auth support for local server
|
||||
|
||||
## 1.3.4 02/06/2015
|
||||
|
||||
* Check if an IOS image is set in the IOS router template
|
||||
* Ensure the version number is written in configuration file
|
||||
* Prevent users to add links to running Qemu VMs and start a capture on running VirtualBox VMs.
|
||||
* Fix resize issue in server page
|
||||
* Fix segfault when starting OSX server with allow connection from anywhere
|
||||
* Fixes bug when editing c7200 templates.
|
||||
* Fixes IOS decompression. Fixes #370.
|
||||
* Topology auto start work for VPCS
|
||||
* Avoid moving .gns3_temporary files.
|
||||
* Handles MemoryError.
|
||||
* Fix crash when a process listen on GNS3 port return an empty JSON
|
||||
* Another fix for the topology None error
|
||||
* Fix a rare crash in completion
|
||||
* Fix crash when loading topology in rare conditions
|
||||
|
||||
## 1.3.3 14/05/2015
|
||||
|
||||
* New inline help text for the idle-pc dialog.
|
||||
* Reactivate auto idle-pc in device contextual menu + save a chosen idle-pc value in template.
|
||||
* Adds name to the thank you section.
|
||||
* Prevent users to use VirtualBox linked clone VMs in temporary projects (for now).
|
||||
* Capture error if the command is invalid
|
||||
* Cleanup egg cache when exit
|
||||
* Fix a crash in console when you used non UTF-8 terminal
|
||||
* Fix crash during save as
|
||||
* Change title when exporting an IOS startup-config.
|
||||
* Removes analytics client on closing.
|
||||
|
||||
## 1.3.3rc1 07/05/2015
|
||||
|
||||
* Catch broken pipe error catched for OSX
|
||||
* Prevent a topology made for next version to be open in previous version
|
||||
* Check if the local server is really a local server
|
||||
* NIO NAT support for QEMU VMs (user mode back-end is used).
|
||||
* Modified version requirements, so that they require the dependency versions as minimums. Added some more detailed instructions for compilation on Windows.
|
||||
* Prevent user to enter a None port
|
||||
* Fix broken pipe error on OSX when frozen
|
||||
* Prevent the same link created twice on OSX
|
||||
* Project loading: names and IDs must be assigned to ports on the client side after nodes have been created. Fixes #326. Fixes config updating for IOS router and IOU devices.
|
||||
* Fix a crash when dropping a .gns3
|
||||
* Cleanup VPCS code
|
||||
* Turn off config parser interpolation
|
||||
* Support unicode characters in regex
|
||||
* Fixes duplicate entries for "Recent files" on Windows. Fixes #316.
|
||||
* Fixes VPCS multi-host. Fixes #318.
|
||||
* Fixes issues when importing configs for IOS, IOU and VPCS. Fixes #314.
|
||||
* Fixes issue when console setting present in IOS router templates.
|
||||
* Do not send empty settings when creating VMs.
|
||||
* Do not set a default private-config when creating a new IOS router template. Fixes #317.
|
||||
* Refactors how startup-config and private-config are handled for IOS routers.
|
||||
* Fixes IOU and QEMU tests.
|
||||
* Makes sure all IOS router settings are saved in the project file & simplify loading from a project.
|
||||
* Makes sure all VirtualBox VM settings are saved in the project file & simplify loading from a project.
|
||||
* Makes sure all VPCS VM settings are saved in the project file & simplify loading from a project.
|
||||
* Makes sure all IOU VM settings are saved in the project file & simplify loading from a project.
|
||||
* Makes sure all QEMU VM settings are saved in the project file & simplify loading from a project.
|
||||
* Fix save as by correctly renaming VM uuid project directory
|
||||
* Fix save as duplicate the .gns3 file
|
||||
* Fix broken project in Another assert topology fixe
|
||||
* Prevent user to enter bad hostname
|
||||
* Fixes an issue when the IOU VM template has a console setting.
|
||||
* Releasing adding a link. Fixes #235.
|
||||
* Fix RuntimeError: wrapped C/C++ object of type QNetworkReply has been deleted.
|
||||
* Do not crash if terminal doesn't support UTF-8
|
||||
* Fix windows build
|
||||
* Fixes "show only devices with captures" in the topology summary.
|
||||
|
||||
## 1.3.2 28/04/2015
|
||||
|
||||
* Fixes bug when IOS configs are not in VM settings.
|
||||
* Fixes small issue with Qemu VM monitor.
|
||||
* Fixes issue when only one port is added after a QEMU VM is created. Fixes #296.
|
||||
* Avoid Cygwin warning with VPCS on Windows.
|
||||
* Fixes issues with QThread handling.
|
||||
* Fixes missing title + icon in layer position warning message box.
|
||||
* Allows the warning message box to be displayed once only when moving an object to a background layer.
|
||||
* Fixes small issue with old monitor setting.
|
||||
* Check the config path is set when creating a IOU or IOS router.
|
||||
* Removes residual link when a NIO cannot be created on the server. Fixes #294.
|
||||
* Fix VPCS tests
|
||||
* Do not crash if an antivirus intercept a message and send non UTF-8
|
||||
* Avoid C++ runtime error when progress dialog is finished.
|
||||
* Merge remote-tracking branch 'origin/master'
|
||||
* Move FileCopyThread to FileCopyWorker.
|
||||
* If project loading fail fallback to real temporary project
|
||||
* Do not crash if rotation is a string
|
||||
* I think it's prevent empty topologies
|
||||
* Explicit utf-8 decoding.
|
||||
* Fixes rare maximum recursion depth exceeded exception.
|
||||
* Check for invalid base VM configuration files.
|
||||
* Catch ValueError exception thrown by mmap(): cannot mmap an empty file.
|
||||
* Use QThreads the correct way (moveToThread).
|
||||
* Fixes broken serial console connection.
|
||||
* Fixes "RuntimeError: wrapped C/C++ object ... has been deleted" exceptions with item links.
|
||||
* Allows exported config files to be created even when there is no config set on VMs.
|
||||
* Do not try to export empty VPCS startup configs.
|
||||
* Prevent issues when a file with a simple number is considered valid JSON.
|
||||
* Explicit error when mmap throw an invalid argument exception.
|
||||
* Do not replace invalid utf-8 characters when reading the iourc file (we catch the exception to tell the user this is an invalid file).
|
||||
* Explicit utf-8 encoding where necessary to avoid Unicode errors on Windows (we require/set an utf-8 locale on other systems).
|
||||
* Save as dialog opens in the projects directory. Fixes #267.
|
||||
* Adds Terminal + nc for serial console connections on OSX. Fixes #228.
|
||||
* Improve warning when non unicode char in iourc
|
||||
* Crash report not for developers and new key
|
||||
* Do not crash if we can't change IOU permission
|
||||
* More checks when decompressing IOS images.
|
||||
* Warn users that they must provide their router images.
|
||||
* Display an error and link to the documentation if no router available
|
||||
* Display print( in std console and Qt Console
|
||||
* Fix tests and a potential issue where initial_content is not send
|
||||
* Fix a crash in qemu loading
|
||||
* Removes unnecessary progress dialog when listing VirtualBox VMs.
|
||||
* Fixes issues when pushing configs for Dynamips and IOU.
|
||||
* Allow for empty initial-config path for IOU VM templates. Send IOU VM settings while creating it (POST) and not using the update API call immediately after (PUT).
|
||||
* Allow for empty startup-config and private-config paths for IOS routers.
|
||||
* Send QEMU VM settings while creating it (POST) and not using the update API call immediately after (PUT).
|
||||
* Include resources and tests in pypi packages
|
||||
* Fix issue during project import on Windows with non local server
|
||||
|
||||
## 1.3.1 11/04/2015
|
||||
|
||||
* Release
|
||||
|
||||
## 1.3.1rc4 09/04/2015
|
||||
|
||||
* Fix crash when save as can't create a directory
|
||||
* Allow less strict dependencies
|
||||
|
||||
## 1.3.1rc3 07/04/2015
|
||||
|
||||
* Send HTTP errors 400 to the crash report system
|
||||
|
||||
## 1.3.1rc2 06/04/2015
|
||||
|
||||
* Fix race condition during old project import
|
||||
|
||||
## 1.3.1rc1 05/04/2015
|
||||
|
||||
* Fix rare occasion when user manage to put text in port field
|
||||
* Fix a crash when exporting vpcs startup script
|
||||
* Fix an issue with sending iourc when a topologies is reloaded
|
||||
* Solve issue when iourc contains non ascii characters
|
||||
* Handle corrupted zip file with IOS image
|
||||
* Don't crash if we try to contact a non GNS3 remote server returning JSON
|
||||
* Skip tests in package
|
||||
* Check port range
|
||||
* Add a warning about too much ram for IOS
|
||||
* Fix crash if project is already closed
|
||||
* Check if wait for connection thread still running before emitting a signal.
|
||||
* Check if process files thread still running before emitting a signal.
|
||||
* Raven is an optionnal dependencies for Debian
|
||||
* Fix crash if a dumped topology as no node during save as
|
||||
* Fix: remove old ID references for ATM and Frame-Relay switches.
|
||||
|
||||
## 1.3.0 30/03/2015
|
||||
|
||||
* Fix etherswitch router
|
||||
|
||||
50
CONTRIBUTING.md
Normal file
50
CONTRIBUTING.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Contributing to GNS3
|
||||
|
||||
We welcome contributions and bugs reports from everyone.
|
||||
We are friendly so don't be afraid to ask questions.
|
||||
|
||||
## Bug reports
|
||||
|
||||
Before reporting an issue:
|
||||
* check our website over at https://gns3.com
|
||||
* check if an issue already exists on https://github.com/GNS3/gns3-gui
|
||||
* check if an issue already exists on https://github.com/GNS3/gns3-server
|
||||
|
||||
Please post on our community website if you are unsure you found a bug,
|
||||
you will get faster support and be able to exchange with more users.
|
||||
|
||||
If you are unsure which project you should create an issue for, just do
|
||||
it on https://github.com/GNS3/gns3-gui we will take care of the triage.
|
||||
|
||||
For bugs specific to the GNS3 VM, please report on https://github.com/GNS3/gns3-vm
|
||||
|
||||
## Asking for new features
|
||||
|
||||
The best is to start a discussion on the community website in order to get feedback
|
||||
from the whole community.
|
||||
|
||||
|
||||
## Contributing code
|
||||
|
||||
We welcome code contribution from everyone including beginners.
|
||||
Don't be afraid to submit a half finished or mediocre contribution and we will help you.
|
||||
|
||||
Don't hesitate to share your plans before starting working on a contribution, we can help
|
||||
you to find the best approach.
|
||||
|
||||
### Contributors License Agreements
|
||||
|
||||
We at GNS3 are eager to work with you. For small changes — little bugfixes, correcting typos, and the like — please just submit pull requests to any of our projects. For larger changes, though, we have to ask you to jump through a little hoop.
|
||||
|
||||
In particular, in order for us to accept any major patches from you, you will have to electronically sign a statement that indicates two things:
|
||||
|
||||
- You are willingly licensing your contributions under the terms of the open source license of the project that you’re contributing to.
|
||||
- You are legally able to license your contributions as stated.
|
||||
|
||||
The reason we do this is to ensure, to the extent possible, that we don’t “taint” the projects we manage with contributions that turn out to be improper. This protects everyone who wants to use the projects, including you!
|
||||
|
||||
More information there: https://github.com/GNS3/cla
|
||||
|
||||
### Pull requests
|
||||
|
||||
Creating a pull request is the easiest way to contribute code. Do not hesitate to create one early when contributing for new feature in order to get our feedback.
|
||||
522
COPYING
Normal file
522
COPYING
Normal file
@@ -0,0 +1,522 @@
|
||||
GNU Public License (GPL)
|
||||
------------------------
|
||||
|
||||
GNS3 is released under the GPLv3 (see LICENSE) with the additional
|
||||
exemption that compiling, linking, and/or using OpenSSL is allowed.
|
||||
|
||||
GNS3 trademark
|
||||
--------------
|
||||
|
||||
"GNS3" is a trademark of GNS3 Technologies, Inc.
|
||||
|
||||
Windows Driver Kit
|
||||
------------------
|
||||
|
||||
The Windows binary distribution includes devcon.exe, a Microsoft(R)
|
||||
Windows Driver Kit (WDK) sample in object code form which is
|
||||
redistributed under the terms of the WDK License terms.
|
||||
|
||||
With respect to binaries built using the Microsoft(R) Windows
|
||||
Driver Kit (WDK), GPLv3 does not extend to any WDK Distributable Code.
|
||||
All WDK Distributable Code is considered by the licensors of GNS3
|
||||
to constitute, or be equivalent to, "System Libraries" as defined in
|
||||
section 1 of GPLv3.
|
||||
|
||||
OpenSSL License
|
||||
---------------
|
||||
|
||||
The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
|
||||
the OpenSSL License and the original SSLeay license apply to the toolkit.
|
||||
See below for the actual license texts. Actually both licenses are BSD-style
|
||||
Open Source licenses. In case of any license issues related to OpenSSL
|
||||
please contact openssl-core@openssl.org.
|
||||
|
||||
/* ====================================================================
|
||||
* Copyright (c) 1998-2003 The OpenSSL Project. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in
|
||||
* the documentation and/or other materials provided with the
|
||||
* distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this
|
||||
* software must display the following acknowledgment:
|
||||
* "This product includes software developed by the OpenSSL Project
|
||||
* for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
|
||||
*
|
||||
* 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
|
||||
* endorse or promote products derived from this software without
|
||||
* prior written permission. For written permission, please contact
|
||||
* openssl-core@openssl.org.
|
||||
*
|
||||
* 5. Products derived from this software may not be called "OpenSSL"
|
||||
* nor may "OpenSSL" appear in their names without prior written
|
||||
* permission of the OpenSSL Project.
|
||||
*
|
||||
* 6. Redistributions of any form whatsoever must retain the following
|
||||
* acknowledgment:
|
||||
* "This product includes software developed by the OpenSSL Project
|
||||
* for use in the OpenSSL Toolkit (http://www.openssl.org/)"
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
|
||||
* EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
|
||||
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
||||
* OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
* ====================================================================
|
||||
*
|
||||
* This product includes cryptographic software written by Eric Young
|
||||
* (eay@cryptsoft.com). This product includes software written by Tim
|
||||
* Hudson (tjh@cryptsoft.com).
|
||||
*
|
||||
*/
|
||||
|
||||
Original SSLeay License
|
||||
-----------------------
|
||||
|
||||
/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
|
||||
* All rights reserved.
|
||||
*
|
||||
* This package is an SSL implementation written
|
||||
* by Eric Young (eay@cryptsoft.com).
|
||||
* The implementation was written so as to conform with Netscapes SSL.
|
||||
*
|
||||
* This library is free for commercial and non-commercial use as long as
|
||||
* the following conditions are aheared to. The following conditions
|
||||
* apply to all code found in this distribution, be it the RC4, RSA,
|
||||
* lhash, DES, etc., code; not just the SSL code. The SSL documentation
|
||||
* included with this distribution is covered by the same copyright terms
|
||||
* except that the holder is Tim Hudson (tjh@cryptsoft.com).
|
||||
*
|
||||
* Copyright remains Eric Young's, and as such any Copyright notices in
|
||||
* the code are not to be removed.
|
||||
* If this package is used in a product, Eric Young should be given attribution
|
||||
* as the author of the parts of the library used.
|
||||
* This can be in the form of a textual message at program startup or
|
||||
* in documentation (online or textual) provided with the package.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. All advertising materials mentioning features or use of this software
|
||||
* must display the following acknowledgement:
|
||||
* "This product includes cryptographic software written by
|
||||
* Eric Young (eay@cryptsoft.com)"
|
||||
* The word 'cryptographic' can be left out if the rouines from the library
|
||||
* being used are not cryptographic related :-).
|
||||
* 4. If you include any Windows specific code (or a derivative thereof) from
|
||||
* the apps directory (application code) you must include an acknowledgement:
|
||||
* "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*
|
||||
* The licence and distribution terms for any publically available version or
|
||||
* derivative of this code cannot be changed. i.e. this code cannot simply be
|
||||
* copied and put under another distribution licence
|
||||
* [including the GNU Public Licence.]
|
||||
*/
|
||||
|
||||
=====================================================================================================
|
||||
Several fantastic pieces of free and open-source software have really get GNS3 to where it is today.
|
||||
A few require that we include their license agreements within our software.
|
||||
=====================================================================================================
|
||||
|
||||
License notice for Qt
|
||||
---------------------
|
||||
http://doc.qt.io/qt-4.8/gpl.html
|
||||
|
||||
License notice for PyQt
|
||||
-----------------------
|
||||
http://www.gnu.org/licenses/gpl.html
|
||||
|
||||
License notice for jsonschema
|
||||
-----------------------------
|
||||
https://github.com/Julian/jsonschema/blob/master/COPYING
|
||||
|
||||
Copyright (c) 2013 Julian Berman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
License notice for aiohttp
|
||||
--------------------------
|
||||
https://github.com/KeepSafe/aiohttp/blob/master/LICENSE.txt
|
||||
|
||||
Copyright (c) 2013, 2014, 2015 Nikolay Kim and Andrew Svetlov
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
License notice for Jinja
|
||||
------------------------
|
||||
https://github.com/KeepSafe/aiohttp/blob/master/LICENSE.txt
|
||||
|
||||
Copyright (c) 2009 by the Jinja Team, see AUTHORS for more details.
|
||||
|
||||
Some rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* The names of the contributors may not be used to endorse or
|
||||
promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License notice for raven
|
||||
------------------------
|
||||
https://github.com/getsentry/raven-python/blob/master/LICENSE
|
||||
|
||||
Copyright (c) 2015 Functional Software, Inc and individual contributors.
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* The names of the contributors may not be used to endorse or
|
||||
promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License notice for Apache Libcloud
|
||||
----------------------------------
|
||||
https://github.com/apache/libcloud/blob/trunk/LICENSE
|
||||
|
||||
Copyright (c) 2010-2015 The Apache Software Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
License notice for requests
|
||||
---------------------------
|
||||
https://github.com/kennethreitz/requests/blob/master/LICENSE
|
||||
|
||||
Copyright (c) 2015 Kenneth Reitz
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
License notice for gns3-converter
|
||||
---------------------------------
|
||||
https://github.com/dlintott/gns3-converter/blob/master/COPYING
|
||||
|
||||
License notice for paramiko
|
||||
---------------------------
|
||||
https://github.com/paramiko/paramiko/blob/master/LICENSE
|
||||
|
||||
License notice for pywin32
|
||||
--------------------------
|
||||
https://github.com/SublimeText/Pywin32/blob/master/License.txt
|
||||
|
||||
Unless stated in the specfic source file, this work is
|
||||
Copyright (c) 1996-2008, Greg Stein and Mark Hammond.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the distribution.
|
||||
|
||||
Neither names of Greg Stein, Mark Hammond nor the name of contributors may be used
|
||||
to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
|
||||
IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License notice for Winpcap
|
||||
--------------------------
|
||||
https://www.winpcap.org/misc/copyright.htm
|
||||
|
||||
Copyright (c) 1999 - 2005 NetGroup, Politecnico di Torino (Italy).
|
||||
Copyright (c) 2005 - 2010 CACE Technologies, Davis (California).
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* The names of the contributors may not be used to endorse or
|
||||
promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License notice for cpulimit
|
||||
---------------------------
|
||||
https://github.com/opsengine/cpulimit/blob/master/LICENSE
|
||||
|
||||
Copyright (C) 2005-2012, by: Angelo Marletta <angelo dot marletta at gmail dot com>
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
License notice for Cygwin
|
||||
-------------------------
|
||||
https://cygwin.com/licensing.html
|
||||
|
||||
License notice for SuperPutty
|
||||
-----------------------------
|
||||
https://github.com/jimradford/superputty/blob/master/License.txt
|
||||
|
||||
Copyright (c) 2009 Jim Radford http://www.jimradford.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
License notice for Putty
|
||||
------------------------
|
||||
http://www.chiark.greenend.org.uk/~sgtatham/putty/licence.html
|
||||
|
||||
PuTTY is copyright 1997-2015 Simon Tatham.
|
||||
|
||||
Portions copyright Robert de Bath, Joris van Rantwijk, Delian Delchev, Andreas Schultz, Jeroen Massar,
|
||||
Wez Furlong, Nicolas Barry, Justin Bradford, Ben Harris, Malcolm Smith, Ahmad Khalifa, Markus Kuhn,
|
||||
Colin Watson, Christopher Staite, and CORE SDI S.A.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
License notice for iouyap
|
||||
-------------------------
|
||||
https://github.com/GNS3/iouyap/blob/master/LICENSE
|
||||
|
||||
License notice for Dynamips
|
||||
---------------------------
|
||||
https://github.com/GNS3/dynamips/blob/master/COPYING
|
||||
|
||||
License notice for Qemu
|
||||
-----------------------
|
||||
http://wiki.qemu.org/License
|
||||
|
||||
License notice for VPCS
|
||||
-----------------------
|
||||
|
||||
Copyright (c) 2007-2013, Paul Meng (mirnshi@gmail.com)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License notice for Python
|
||||
-------------------------
|
||||
https://www.python.org/download/releases/3.4.2/license/
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# Run tests inside a container
|
||||
FROM ubuntu:vivid
|
||||
|
||||
MAINTAINER GNS3 Team
|
||||
|
||||
#ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --force-yes python3.4 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3.4-dev xvfb
|
||||
RUN apt-get clean
|
||||
|
||||
|
||||
ADD dev-requirements.txt /dev-requirements.txt
|
||||
ADD requirements.txt /requirements.txt
|
||||
RUN pip3 install -r /dev-requirements.txt
|
||||
|
||||
|
||||
ADD . /src
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
CMD xvfb-run python3.4 -m pytest -vv
|
||||
@@ -4,8 +4,8 @@ include INSTALL
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include tox.ini
|
||||
recursive-exclude tests *
|
||||
recursive-include docs *
|
||||
recursive-include tests *
|
||||
recursive-include gns3 *
|
||||
recursive-include resources *
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
|
||||
64
README.rst
64
README.rst
@@ -20,7 +20,7 @@ Dependencies:
|
||||
|
||||
- Python 3.3 or above
|
||||
- Setuptools
|
||||
- PyQt libraries
|
||||
- PyQt 5 libraries
|
||||
- Apache Libcloud library
|
||||
- Requests library
|
||||
- Paramiko library
|
||||
@@ -30,6 +30,13 @@ The following commands will install some of these dependencies:
|
||||
.. code:: bash
|
||||
|
||||
sudo apt-get install python3-setuptools
|
||||
sudo apt-get install python3-pyqt5
|
||||
sudo apt-get install python3-pyqt5.qtsvg
|
||||
sudo apt-get install python3-pyqt5.qtwebkit
|
||||
|
||||
If you want to test using PyQt4
|
||||
|
||||
.. code:: bash
|
||||
sudo apt-get install python3-pyqt4
|
||||
|
||||
Finally these commands will install the GUI as well as the rest of the dependencies:
|
||||
@@ -43,7 +50,23 @@ Finally these commands will install the GUI as well as the rest of the dependenc
|
||||
Windows
|
||||
-------
|
||||
|
||||
Please use our all-in-one installer.
|
||||
Please use our `all-in-one installer <https://gns3.com/software/download>`_ to install the stable build.
|
||||
|
||||
If you install via source you need to first install:
|
||||
|
||||
- Python (3.3 or above) - https://www.python.org/downloads/windows/
|
||||
- Pywin32 - https://sourceforge.net/projects/pywin32/
|
||||
- Qt5 - http://www.qt.io/download-open-source/
|
||||
- PyQt5 - http://www.riverbankcomputing.com/software/pyqt/download5
|
||||
- PyCrypto (which if you compile from source, requires Visual Studio 2010 with GMP or MPIR libraries)
|
||||
|
||||
And finally, call
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python setup.py install
|
||||
|
||||
to install the remaining dependencies.
|
||||
|
||||
Mac OS X
|
||||
--------
|
||||
@@ -59,6 +82,11 @@ Then install the GNS3 dependencies.
|
||||
brew install python3
|
||||
brew install qt
|
||||
brew install sip --without-python --with-python3
|
||||
brew install pyqt5 --without-python --with-python3
|
||||
|
||||
If you want to test using PyQt4
|
||||
|
||||
.. code:: bash
|
||||
brew install pyqt --without-python --with-python3
|
||||
|
||||
Finally, install both the GUI & server from the source.
|
||||
@@ -75,12 +103,40 @@ Finally, install both the GUI & server from the source.
|
||||
|
||||
Or follow this `HOWTO that uses MacPorts <http://binarynature.blogspot.ca/2014/05/install-gns3-early-release-on-mac-os-x.html>`_.
|
||||
|
||||
Developement
|
||||
Development
|
||||
-------------
|
||||
|
||||
If you want to update the interface modify the .ui files using QT tools. And:
|
||||
If you want to update the interface, modify the .ui files using QT tools. And:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd scripts
|
||||
python build_pyqt.py
|
||||
|
||||
Debug
|
||||
"""""
|
||||
|
||||
If you want to see the full logs in the internal shell you can type:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
debug 2
|
||||
|
||||
|
||||
Or start the app with --debug flag.
|
||||
|
||||
Due to the fact PyQT intercept you can use a web debugger for inspecting stuff:
|
||||
https://github.com/Kozea/wdb
|
||||
|
||||
|
||||
Test with PyQT4
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to simulate a user with PyQT4:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
export GNS3_QT4=1
|
||||
python gns3/main.py
|
||||
|
||||
|
||||
|
||||
38
fake_frozen_gns3.py
Executable file
38
fake_frozen_gns3.py
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
This script fake GNS3 run as a frozen app.
|
||||
|
||||
Use it for testing stuff like self update.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
|
||||
# Fake GNS3 run from a binary
|
||||
sys.executable = os.path.realpath(__file__)
|
||||
|
||||
# Add site-package directory before cx_freeze directory
|
||||
sys.path.insert(0, os.path.dirname(sys.executable))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(sys.executable), 'site-packages'))
|
||||
|
||||
sys.frozen = True
|
||||
|
||||
module = importlib.import_module("gns3.main")
|
||||
module.main()
|
||||
20
gns3/__main__.py
Normal file
20
gns3/__main__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .main import main
|
||||
|
||||
main()
|
||||
50
gns3/application.py
Normal file
50
gns3/application.py
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
from .qt import QtWidgets, QtGui, QtCore
|
||||
from .version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Application(QtWidgets.QApplication):
|
||||
file_open_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, argv):
|
||||
super().__init__(argv)
|
||||
# this info is necessary for QSettings
|
||||
self.setOrganizationName("GNS3")
|
||||
self.setOrganizationDomain("gns3.net")
|
||||
self.setApplicationName("GNS3")
|
||||
self.setApplicationVersion(__version__)
|
||||
|
||||
# File path if we have received the path to
|
||||
# a file on system via an OSX event
|
||||
self.open_file_at_startup = None
|
||||
|
||||
def event(self, event):
|
||||
# When you double click file you receive an event
|
||||
# and not the file as command line parameter
|
||||
if sys.platform.startswith("darwin"):
|
||||
if isinstance(event, QtGui.QFileOpenEvent):
|
||||
self.open_file_at_startup = str(event.file())
|
||||
self.file_open_signal.emit(str(event.file()))
|
||||
return super().event(event)
|
||||
@@ -1,337 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Base cloud controller class.
|
||||
|
||||
Base class for interacting with Cloud APIs to create and manage cloud
|
||||
instances.
|
||||
|
||||
"""
|
||||
from collections import namedtuple
|
||||
import hashlib
|
||||
import os
|
||||
import logging
|
||||
from io import StringIO, BytesIO
|
||||
|
||||
from libcloud.compute.base import NodeAuthSSHKey
|
||||
from libcloud.storage.types import ContainerAlreadyExistsError, ContainerDoesNotExistError, ObjectDoesNotExistError
|
||||
|
||||
from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed
|
||||
from .exceptions import OverLimit, BadRequest, ServiceUnavailable
|
||||
from .exceptions import Unauthorized, ApiError
|
||||
|
||||
|
||||
KeyPair = namedtuple("KeyPair", ['name'], verbose=False)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_exception(exception):
|
||||
"""
|
||||
Parse the exception to separate the HTTP status code from the text.
|
||||
|
||||
Libcloud raises many exceptions of the form:
|
||||
Exception("<http status code> <http error> <reponse body>")
|
||||
|
||||
in lieu of raising specific incident-based exceptions.
|
||||
|
||||
"""
|
||||
|
||||
e_str = str(exception)
|
||||
|
||||
try:
|
||||
status = int(e_str[0:3])
|
||||
error_text = e_str[3:]
|
||||
|
||||
except ValueError:
|
||||
status = None
|
||||
error_text = e_str
|
||||
|
||||
return status, error_text
|
||||
|
||||
|
||||
class BaseCloudCtrl(object):
|
||||
|
||||
""" Base class for interacting with a cloud provider API. """
|
||||
|
||||
http_status_to_exception = {
|
||||
400: BadRequest,
|
||||
401: Unauthorized,
|
||||
404: ItemNotFound,
|
||||
405: MethodNotAllowed,
|
||||
413: OverLimit,
|
||||
500: ApiError,
|
||||
503: ServiceUnavailable
|
||||
}
|
||||
|
||||
GNS3_CONTAINER_NAME = 'GNS3'
|
||||
|
||||
def __init__(self, username, api_key):
|
||||
self.username = username
|
||||
self.api_key = api_key
|
||||
|
||||
def _handle_exception(self, status, error_text, response_overrides=None):
|
||||
""" Raise an exception based on the HTTP status. """
|
||||
|
||||
if response_overrides:
|
||||
if status in response_overrides:
|
||||
raise response_overrides[status](error_text)
|
||||
|
||||
raise self.http_status_to_exception[status](error_text)
|
||||
|
||||
def authenticate(self):
|
||||
""" Validate cloud account credentials. Return boolean. """
|
||||
raise NotImplementedError
|
||||
|
||||
def list_sizes(self):
|
||||
""" Return a list of NodeSize objects. """
|
||||
|
||||
return self.driver.list_sizes()
|
||||
|
||||
def list_flavors(self):
|
||||
""" Return an iterable of flavors """
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def create_instance(self, name, size_id, image_id, keypair):
|
||||
"""
|
||||
Create a new instance with the supplied attributes.
|
||||
|
||||
Return a Node object.
|
||||
|
||||
"""
|
||||
try:
|
||||
image = self.get_image(image_id)
|
||||
if image is None:
|
||||
raise ItemNotFound("Image not found")
|
||||
|
||||
size = self.driver.ex_get_size(size_id)
|
||||
|
||||
args = {
|
||||
"name": name,
|
||||
"size": size,
|
||||
"image": image,
|
||||
}
|
||||
|
||||
if keypair is not None:
|
||||
auth_key = NodeAuthSSHKey(keypair.public_key)
|
||||
args["auth"] = auth_key
|
||||
args["ex_keyname"] = name
|
||||
|
||||
return self.driver.create_node(**args)
|
||||
|
||||
except Exception as e:
|
||||
status, error_text = parse_exception(e)
|
||||
|
||||
if status:
|
||||
self._handle_exception(status, error_text)
|
||||
else:
|
||||
log.error("create_instance method raised an exception: {}".format(e))
|
||||
log.error('image id {}'.format(image_id))
|
||||
|
||||
def delete_instance(self, instance):
|
||||
""" Delete the specified instance. Returns True or False. """
|
||||
|
||||
try:
|
||||
return self.driver.destroy_node(instance)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
status, error_text = parse_exception(e)
|
||||
|
||||
if status:
|
||||
self._handle_exception(status, error_text)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def get_instance(self, instance):
|
||||
""" Return a Node object representing the requested instance. """
|
||||
|
||||
for i in self.driver.list_nodes():
|
||||
if i.id == instance.id:
|
||||
return i
|
||||
|
||||
raise ItemNotFound("Instance not found")
|
||||
|
||||
def list_instances(self):
|
||||
""" Return a list of instances in the current region. """
|
||||
|
||||
return self.driver.list_nodes()
|
||||
|
||||
def create_key_pair(self, name):
|
||||
""" Create and return a new Key Pair. """
|
||||
|
||||
response_overrides = {
|
||||
409: KeyPairExists
|
||||
}
|
||||
try:
|
||||
return self.driver.create_key_pair(name)
|
||||
|
||||
except Exception as e:
|
||||
status, error_text = parse_exception(e)
|
||||
if status:
|
||||
self._handle_exception(status, error_text, response_overrides)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def delete_key_pair(self, keypair):
|
||||
""" Delete the keypair. Returns True or False. """
|
||||
|
||||
try:
|
||||
return self.driver.delete_key_pair(keypair)
|
||||
|
||||
except Exception as e:
|
||||
status, error_text = parse_exception(e)
|
||||
if status:
|
||||
self._handle_exception(status, error_text)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def delete_key_pair_by_name(self, keypair_name):
|
||||
""" Utility method to incapsulate boilerplate code """
|
||||
|
||||
kp = KeyPair(name=keypair_name)
|
||||
return self.delete_key_pair(kp)
|
||||
|
||||
def list_key_pairs(self):
|
||||
""" Return a list of Key Pairs. """
|
||||
|
||||
return self.driver.list_key_pairs()
|
||||
|
||||
def upload_file(self, file_path, cloud_object_name):
|
||||
"""
|
||||
Uploads file to cloud storage (if it is not identical to a file already in cloud storage).
|
||||
:param file_path: path to file to upload
|
||||
:param cloud_object_name: name of file saved in cloud storage
|
||||
:return: True if file was uploaded, False if it was skipped because it already existed and was identical
|
||||
"""
|
||||
try:
|
||||
gns3_container = self.storage_driver.create_container(self.GNS3_CONTAINER_NAME)
|
||||
except ContainerAlreadyExistsError:
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
|
||||
with open(file_path, 'rb') as file:
|
||||
local_file_hash = hashlib.md5(file.read()).hexdigest()
|
||||
|
||||
cloud_hash_name = cloud_object_name + '.md5'
|
||||
cloud_objects = [obj.name for obj in gns3_container.list_objects()]
|
||||
|
||||
# if the file and its hash are in object storage, and the local and storage file hashes match
|
||||
# do not upload the file, otherwise upload it
|
||||
if cloud_object_name in cloud_objects and cloud_hash_name in cloud_objects:
|
||||
hash_object = gns3_container.get_object(cloud_hash_name)
|
||||
cloud_object_hash = ''
|
||||
for chunk in hash_object.as_stream():
|
||||
cloud_object_hash += chunk.decode('utf8')
|
||||
|
||||
if cloud_object_hash == local_file_hash:
|
||||
return False
|
||||
|
||||
file.seek(0)
|
||||
self.storage_driver.upload_object_via_stream(file, gns3_container, cloud_object_name)
|
||||
self.storage_driver.upload_object_via_stream(StringIO(local_file_hash), gns3_container, cloud_hash_name)
|
||||
return True
|
||||
|
||||
def list_projects(self):
|
||||
"""
|
||||
Lists projects in cloud storage
|
||||
:return: Dictionary where project names are keys and values are names of objects in storage
|
||||
"""
|
||||
|
||||
try:
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
projects = {
|
||||
obj.name.replace('projects/', '').replace('.zip', ''): obj.name
|
||||
for obj in gns3_container.list_objects()
|
||||
if obj.name.startswith('projects/') and obj.name[-4:] == '.zip'
|
||||
}
|
||||
return projects
|
||||
except ContainerDoesNotExistError:
|
||||
return []
|
||||
|
||||
def download_file(self, file_name, destination=None):
|
||||
"""
|
||||
Downloads file from cloud storage. If a file exists at destination, and it is identical to the file in cloud
|
||||
storage, it is not downloaded.
|
||||
:param file_name: name of file in cloud storage to download
|
||||
:param destination: local path to save file to (if None, returns file contents as a file-like object)
|
||||
:return: A file-like object if file contents are returned, or None if file is saved to filesystem
|
||||
"""
|
||||
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
storage_object = gns3_container.get_object(file_name)
|
||||
|
||||
if destination is not None:
|
||||
if os.path.isfile(destination):
|
||||
# if a file exists at destination and its hash matches that of the
|
||||
# file in cloud storage, don't download it
|
||||
with open(destination, 'rb') as f:
|
||||
local_file_hash = hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
hash_object = gns3_container.get_object(file_name + '.md5')
|
||||
cloud_object_hash = ''
|
||||
for chunk in hash_object.as_stream():
|
||||
cloud_object_hash += chunk.decode('utf8')
|
||||
|
||||
if local_file_hash == cloud_object_hash:
|
||||
return
|
||||
|
||||
storage_object.download(destination)
|
||||
else:
|
||||
contents = b''
|
||||
|
||||
for chunk in storage_object.as_stream():
|
||||
contents += chunk
|
||||
|
||||
return BytesIO(contents)
|
||||
|
||||
def find_storage_image_names(self, images_to_find):
|
||||
"""
|
||||
Maps names of image files to their full name in cloud storage
|
||||
:param images_to_find: list of image names to find
|
||||
:return: A dictionary where keys are image names, and values are the corresponding names of
|
||||
the files in cloud storage
|
||||
"""
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
images_in_storage = [obj.name for obj in gns3_container.list_objects() if obj.name.startswith('images/')]
|
||||
|
||||
images = {}
|
||||
for image_name in images_to_find:
|
||||
images_with_same_name =\
|
||||
list(filter(lambda storage_image_name: storage_image_name.endswith(image_name), images_in_storage))
|
||||
|
||||
if len(images_with_same_name) == 1:
|
||||
images[image_name] = images_with_same_name[0]
|
||||
else:
|
||||
raise Exception('Image does not exist in cloud storage or is duplicated')
|
||||
|
||||
return images
|
||||
|
||||
def delete_file(self, file_name):
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
|
||||
try:
|
||||
object_to_delete = gns3_container.get_object(file_name)
|
||||
object_to_delete.delete()
|
||||
except ObjectDoesNotExistError:
|
||||
pass
|
||||
|
||||
try:
|
||||
hash_object = gns3_container.get_object(file_name + '.md5')
|
||||
hash_object.delete()
|
||||
except ObjectDoesNotExistError:
|
||||
pass
|
||||
@@ -1,67 +0,0 @@
|
||||
""" Exception classes for CloudCtrl classes. """
|
||||
|
||||
|
||||
class ApiError(Exception):
|
||||
|
||||
""" Raised when the server returns 500 Compute Error. """
|
||||
pass
|
||||
|
||||
|
||||
class BadRequest(Exception):
|
||||
|
||||
""" Raised when the server returns 400 Bad Request. """
|
||||
pass
|
||||
|
||||
|
||||
class ComputeFault(Exception):
|
||||
|
||||
""" Raised when the server returns 400|500 Compute Fault. """
|
||||
pass
|
||||
|
||||
|
||||
class Forbidden(Exception):
|
||||
|
||||
""" Raised when the server returns 403 Forbidden. """
|
||||
pass
|
||||
|
||||
|
||||
class ItemNotFound(Exception):
|
||||
|
||||
""" Raised when the server returns 404 Not Found. """
|
||||
pass
|
||||
|
||||
|
||||
class KeyPairExists(Exception):
|
||||
|
||||
""" Raised when the server returns 409 Conflict Key pair exists. """
|
||||
pass
|
||||
|
||||
|
||||
class MethodNotAllowed(Exception):
|
||||
|
||||
""" Raised when the server returns 405 Method Not Allowed. """
|
||||
pass
|
||||
|
||||
|
||||
class OverLimit(Exception):
|
||||
|
||||
""" Raised when the server returns 413 Over Limit. """
|
||||
pass
|
||||
|
||||
|
||||
class ServerCapacityUnavailable(Exception):
|
||||
|
||||
""" Raised when the server returns 503 Server Capacity Uavailable. """
|
||||
pass
|
||||
|
||||
|
||||
class ServiceUnavailable(Exception):
|
||||
|
||||
""" Raised when the server returns 503 Service Unavailable. """
|
||||
pass
|
||||
|
||||
|
||||
class Unauthorized(Exception):
|
||||
|
||||
""" Raised when the server returns 401 Unauthorized. """
|
||||
pass
|
||||
@@ -1,259 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
""" Interacts with Rackspace API to create and manage cloud instances. """
|
||||
|
||||
from .base_cloud_ctrl import BaseCloudCtrl
|
||||
import json
|
||||
import requests
|
||||
from libcloud.compute.drivers.rackspace import ENDPOINT_ARGS_MAP
|
||||
from libcloud.compute.providers import get_driver
|
||||
from libcloud.compute.types import Provider
|
||||
from libcloud.storage.providers import get_driver as get_storage_driver
|
||||
from libcloud.storage.types import Provider as StorageProvider
|
||||
|
||||
from .exceptions import ItemNotFound, ApiError
|
||||
from ..version import __version__
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
RACKSPACE_REGIONS = [{ENDPOINT_ARGS_MAP[k]['region']: k} for k in
|
||||
ENDPOINT_ARGS_MAP]
|
||||
|
||||
|
||||
class RackspaceCtrl(BaseCloudCtrl):
|
||||
|
||||
""" Controller class for interacting with Rackspace API. """
|
||||
|
||||
def __init__(self, username, api_key, *args, **kwargs):
|
||||
super(RackspaceCtrl, self).__init__(username, api_key)
|
||||
|
||||
# set this up so it can be swapped out with a mock for testing
|
||||
self.post_fn = requests.post
|
||||
self.driver_cls = get_driver(Provider.RACKSPACE)
|
||||
self.storage_driver_cls = get_storage_driver(StorageProvider.CLOUDFILES)
|
||||
|
||||
self.driver = None
|
||||
self.storage_driver = None
|
||||
self.region = None
|
||||
self.instances = {}
|
||||
|
||||
self.authenticated = False
|
||||
self.identity_ep = \
|
||||
"https://identity.api.rackspacecloud.com/v2.0/tokens"
|
||||
|
||||
self.regions = []
|
||||
self.token = None
|
||||
self.tenant_id = None
|
||||
self.flavor_ep = "https://dfw.servers.api.rackspacecloud.com/v2/{username}/flavors"
|
||||
self._flavors = OrderedDict([
|
||||
('2', '512MB, 1 VCPU'),
|
||||
('3', '1GB, 1 VCPU'),
|
||||
('4', '2GB, 2 VCPUs'),
|
||||
('5', '4GB, 2 VCPUs'),
|
||||
('6', '8GB, 4 VCPUs'),
|
||||
('7', '15GB, 6 VCPUs'),
|
||||
('8', '30GB, 8 VCPUs'),
|
||||
('performance1-1', '1GB Performance, 1 VCPU'),
|
||||
('performance1-2', '2GB Performance, 2 VCPUs'),
|
||||
('performance1-4', '4GB Performance, 4 VCPUs'),
|
||||
('performance1-8', '8GB Performance, 8 VCPUs'),
|
||||
('performance2-15', '15GB Performance, 4 VCPUs'),
|
||||
('performance2-30', '30GB Performance, 8 VCPUs'),
|
||||
('performance2-60', '60GB Performance, 16 VCPUs'),
|
||||
('performance2-90', '90GB Performance, 24 VCPUs'),
|
||||
('performance2-120', '120GB Performance, 32 VCPUs',)
|
||||
])
|
||||
|
||||
def authenticate(self):
|
||||
"""
|
||||
Submit username and api key to API service.
|
||||
|
||||
If authentication is successful, set self.regions and self.token.
|
||||
Return boolean.
|
||||
|
||||
"""
|
||||
|
||||
self.authenticated = False
|
||||
|
||||
if len(self.username) < 1:
|
||||
return False
|
||||
|
||||
if len(self.api_key) < 1:
|
||||
return False
|
||||
|
||||
data = json.dumps({
|
||||
"auth": {
|
||||
"RAX-KSKEY:apiKeyCredentials": {
|
||||
"username": self.username,
|
||||
"apiKey": self.api_key
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
headers = {
|
||||
'Content-type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
response = self.post_fn(self.identity_ep, data=data, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
|
||||
api_data = response.json()
|
||||
self.token = self._parse_token(api_data)
|
||||
|
||||
if self.token:
|
||||
self.authenticated = True
|
||||
user_regions = self._parse_endpoints(api_data)
|
||||
self.regions = self._make_region_list(user_regions)
|
||||
self.tenant_id = self._parse_tenant_id(api_data)
|
||||
|
||||
else:
|
||||
self.regions = []
|
||||
self.token = None
|
||||
|
||||
response.connection.close()
|
||||
|
||||
return self.authenticated
|
||||
|
||||
def list_regions(self):
|
||||
""" Return a list the regions available to the user. """
|
||||
|
||||
return self.regions
|
||||
|
||||
def list_flavors(self):
|
||||
""" Return the dictionary containing flavors id and names """
|
||||
|
||||
return self._flavors
|
||||
|
||||
def _parse_endpoints(self, api_data):
|
||||
"""
|
||||
Parse the JSON-encoded data returned by the Identity Service API.
|
||||
|
||||
Return a list of regions available for Compute v2.
|
||||
|
||||
"""
|
||||
|
||||
region_codes = []
|
||||
|
||||
for ep_type in api_data['access']['serviceCatalog']:
|
||||
if ep_type['name'] == "cloudServersOpenStack" \
|
||||
and ep_type['type'] == "compute":
|
||||
|
||||
for ep in ep_type['endpoints']:
|
||||
if ep['versionId'] == "2":
|
||||
region_codes.append(ep['region'])
|
||||
|
||||
return region_codes
|
||||
|
||||
def _parse_token(self, api_data):
|
||||
""" Parse the token from the JSON-encoded data returned by the API. """
|
||||
|
||||
try:
|
||||
token = api_data['access']['token']['id']
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
return token
|
||||
|
||||
def _parse_tenant_id(self, api_data):
|
||||
""" """
|
||||
try:
|
||||
roles = api_data['access']['user']['roles']
|
||||
for role in roles:
|
||||
if 'tenantId' in role and role['name'] == 'compute:default':
|
||||
return role['tenantId']
|
||||
return None
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def _make_region_list(self, region_codes):
|
||||
"""
|
||||
Make a list of regions for use in the GUI.
|
||||
|
||||
Returns a list of key-value pairs in the form:
|
||||
<API's Region Name>: <libcloud's Region Name>
|
||||
eg,
|
||||
[
|
||||
{'DFW': 'dfw'}
|
||||
{'ORD': 'ord'},
|
||||
...
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
region_list = []
|
||||
|
||||
for ep in ENDPOINT_ARGS_MAP:
|
||||
if ENDPOINT_ARGS_MAP[ep]['region'] in region_codes:
|
||||
region_list.append({ENDPOINT_ARGS_MAP[ep]['region']: ep})
|
||||
|
||||
return region_list
|
||||
|
||||
def set_region(self, region):
|
||||
""" Set self.region and self.driver. Returns True or False. """
|
||||
|
||||
try:
|
||||
self.driver = self.driver_cls(self.username, self.api_key,
|
||||
region=region)
|
||||
self.storage_driver = self.storage_driver_cls(self.username, self.api_key,
|
||||
region=region)
|
||||
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
self.region = region
|
||||
return True
|
||||
|
||||
def get_image(self, image_id):
|
||||
return self.driver.get_image(image_id)
|
||||
|
||||
|
||||
def get_provider(cloud_settings):
|
||||
"""
|
||||
Utility function to retrieve a cloud provider instance already authenticated and with the
|
||||
region set
|
||||
|
||||
:param cloud_settings: cloud settings dictionary
|
||||
:return: a provider instance or None on errors
|
||||
"""
|
||||
try:
|
||||
username = cloud_settings['cloud_user_name']
|
||||
apikey = cloud_settings['cloud_api_key']
|
||||
region = cloud_settings['cloud_region']
|
||||
except KeyError as e:
|
||||
log.error("Unable to create cloud provider: {}".format(e))
|
||||
return
|
||||
|
||||
provider = RackspaceCtrl(username, apikey)
|
||||
|
||||
if not provider.authenticate():
|
||||
log.error("Authentication failed for cloud provider")
|
||||
return
|
||||
|
||||
if not region:
|
||||
region = provider.list_regions().values()[0]
|
||||
|
||||
if not provider.set_region(region):
|
||||
log.error("Unable to set cloud provider region")
|
||||
return
|
||||
|
||||
return provider
|
||||
@@ -1,538 +0,0 @@
|
||||
from contextlib import contextmanager
|
||||
import io
|
||||
import json
|
||||
from socket import error as socket_error
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
from ..qt import QtCore
|
||||
|
||||
from .exceptions import KeyPairExists
|
||||
from .rackspace_ctrl import get_provider
|
||||
from ..topology import Topology
|
||||
from ..servers import Servers
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def ssh_client(host, key_string):
|
||||
"""
|
||||
Context manager wrapping a SSHClient instance: the client connects on
|
||||
enter and close the connection on exit
|
||||
"""
|
||||
|
||||
import paramiko
|
||||
|
||||
class AllowAndForgetPolicy(paramiko.MissingHostKeyPolicy):
|
||||
|
||||
"""
|
||||
Custom policy for server host keys: we simply accept the key
|
||||
the server sent to us without storing it.
|
||||
"""
|
||||
|
||||
def missing_host_key(self, *args, **kwargs):
|
||||
"""
|
||||
According to MissingHostKeyPolicy protocol, to accept
|
||||
the key, simply return.
|
||||
"""
|
||||
return
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
try:
|
||||
f_key = io.StringIO(key_string)
|
||||
key = paramiko.RSAKey.from_private_key(f_key)
|
||||
client.set_missing_host_key_policy(AllowAndForgetPolicy())
|
||||
client.connect(hostname=host, username="root", pkey=key)
|
||||
yield client
|
||||
except socket_error as e:
|
||||
log.debug("SSH connection socket error to {}: {}".format(host, e))
|
||||
yield None
|
||||
except Exception as e:
|
||||
log.debug("SSH connection error to {}: {}".format(host, e))
|
||||
yield None
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
class ListInstancesThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Helper class to retrieve data from the provider in a separate thread,
|
||||
avoid freezing the gui
|
||||
"""
|
||||
instancesReady = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent, provider):
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
instances = self._provider.list_instances()
|
||||
log.debug('Instance list: {}'.format([(i.name, i.state) for i in instances]))
|
||||
self.instancesReady.emit(instances)
|
||||
except Exception as e:
|
||||
log.info('list_instances error: {}'.format(e))
|
||||
|
||||
|
||||
class CreateInstanceThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Helper class to create instances in a separate thread
|
||||
"""
|
||||
instanceCreated = QtCore.pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, parent, provider, name, flavor_id, image_id):
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
self._name = name
|
||||
self._flavor_id = flavor_id
|
||||
self._image_id = image_id
|
||||
|
||||
def run(self):
|
||||
log.debug("Creating cloud keypair with name {}".format(self._name))
|
||||
try:
|
||||
k = self._provider.create_key_pair(self._name)
|
||||
except KeyPairExists:
|
||||
log.debug("Cloud keypair with name {} exists. Recreating.".format(self._name))
|
||||
# delete keypairs if they already exist
|
||||
self._provider.delete_key_pair_by_name(self._name)
|
||||
k = self._provider.create_key_pair(self._name)
|
||||
|
||||
log.debug("Creating cloud server with name {}".format(self._name))
|
||||
i = self._provider.create_instance(self._name, self._flavor_id, self._image_id, k)
|
||||
log.debug("Cloud server {} created".format(self._name))
|
||||
|
||||
self.instanceCreated.emit(i, k)
|
||||
|
||||
|
||||
class DeleteInstanceThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Helper class to remove an instance in a separate thread
|
||||
"""
|
||||
instanceDeleted = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent, provider, instance):
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
self._instance = instance
|
||||
|
||||
def run(self):
|
||||
if self._provider.delete_instance(self._instance):
|
||||
self.instanceDeleted.emit(self._instance)
|
||||
|
||||
|
||||
class StartGNS3ServerThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Perform an SSH connection to the instances in a separate thread,
|
||||
outside the GUI event loop, and start GNS3 server
|
||||
"""
|
||||
gns3server_started = QtCore.pyqtSignal(str, str, str)
|
||||
|
||||
# This is for testing without pushing to github
|
||||
# commands = '''
|
||||
# DEBIAN_FRONTEND=noninteractive dpkg --configure -a
|
||||
# DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -y update
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
|
||||
# ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
|
||||
# mkdir -p /opt/gns3
|
||||
# tar xzf /tmp/gns3-server.tgz -C /opt/gns3
|
||||
# cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
|
||||
# cd /opt/gns3/gns3-server; python3 ./setup.py install
|
||||
# ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
|
||||
# wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap.tar.gz'
|
||||
# python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
|
||||
# hostname gns3-iouvm
|
||||
# tar xzf iouyap.tar.gz -C /usr/local/bin
|
||||
# killall python3 gns3server gns3dms
|
||||
# '''
|
||||
|
||||
commands = '''
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --configure -a
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
|
||||
ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
|
||||
mkdir -p /opt/gns3
|
||||
cd /opt/gns3; git clone https://github.com/planctechnologies/gns3-server.git
|
||||
cd /opt/gns3/gns3-server; git checkout dev; git pull
|
||||
cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
|
||||
cd /opt/gns3/gns3-server; python3 ./setup.py install
|
||||
ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
|
||||
wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap-64-bit.tar.gz'
|
||||
tar xzf iouyap-64-bit.tar.gz -C /usr/local/bin
|
||||
python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
|
||||
hostname gns3-iouvm # set hostname for iou
|
||||
wget -O vpcs http://sourceforge.net/projects/vpcs/files/0.6/vpcs_0.6_Linux64/download
|
||||
cp vpcs /usr/local/bin/vpcs
|
||||
chmod a+x /usr/local/bin/vpcs
|
||||
killall python3 gns3server gns3dms
|
||||
'''
|
||||
|
||||
def __init__(self, parent, host, private_key_string, server_id, username, api_key, region, dead_time):
|
||||
super().__init__(parent)
|
||||
self._host = host
|
||||
self._private_key_string = private_key_string
|
||||
self._server_id = server_id
|
||||
self._username = username
|
||||
self._api_key = api_key
|
||||
self._region = region
|
||||
self._dead_time = dead_time
|
||||
|
||||
def exec_command(self, client, cmd, wait_time=-1):
|
||||
|
||||
cmd += '; exit $?'
|
||||
|
||||
stdout_data = b''
|
||||
stderr_data = b''
|
||||
|
||||
log.debug('cmd: {}'.format(cmd))
|
||||
# Send the command (non-blocking)
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
|
||||
# Wait for the command to terminate
|
||||
wait = int(wait_time)
|
||||
while not stdout.channel.exit_status_ready() and wait != 0:
|
||||
time.sleep(1)
|
||||
wait -= 1
|
||||
|
||||
stdout_data = stdout.read()
|
||||
stderr_data = stderr.read()
|
||||
log.debug('exit status: {}'.format(stdout.channel.exit_status))
|
||||
log.debug('stdout: {}'.format(stdout_data.decode('utf-8')))
|
||||
log.debug('stderr: {}'.format(stderr_data.decode('utf-8')))
|
||||
return stdout_data, stderr_data
|
||||
|
||||
def run(self):
|
||||
# We might be attempting a connection before the instance is fully booted, so retry
|
||||
# when the ssh connection fails.
|
||||
ssh_connected = False
|
||||
while not ssh_connected:
|
||||
with ssh_client(self._host, self._private_key_string) as client:
|
||||
if client is None:
|
||||
time.sleep(1)
|
||||
continue
|
||||
ssh_connected = True
|
||||
|
||||
# This is for testing without pushing to github
|
||||
# os.system('rm -rf /tmp/gns3-server')
|
||||
# os.system('cp -a /Users/jseutter/projects/gns3-server /tmp/gns3-server')
|
||||
# os.system('cd /tmp; tar czf /tmp/gns3-server.tgz gns3-server')
|
||||
# sftp = client.open_sftp()
|
||||
# sftp.put('/tmp/gns3-server.tgz', '/tmp/gns3-server.tgz')
|
||||
# sftp.close()
|
||||
|
||||
for cmd in [l for l in self.commands.splitlines() if l.strip()]:
|
||||
self.exec_command(client, cmd)
|
||||
|
||||
data = {
|
||||
'instance_id': self._server_id,
|
||||
'cloud_user_name': self._username,
|
||||
'cloud_api_key': self._api_key,
|
||||
'cloud_region': self._region,
|
||||
'dead_time': self._dead_time,
|
||||
}
|
||||
# TODO: Properly escape the data portion of the command line
|
||||
start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --ip={} --data="{}" 2>/tmp/gns3-stderr.log'.format(self._host, data)
|
||||
stdout, stderr = self.exec_command(client, start_cmd, wait_time=15)
|
||||
response = stdout.decode('utf-8')
|
||||
self.gns3server_started.emit(str(self._server_id), str(self._host), str(response))
|
||||
|
||||
|
||||
class WSConnectThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Establish a websocket connection with the remote gns3server
|
||||
instance. Run outside the GUI event loop.
|
||||
"""
|
||||
established = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent, provider, server_id, host, port, ca_file,
|
||||
auth_user, auth_password, ssh_pkey, instance_id):
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
self._server_id = server_id
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._ca_file = ca_file
|
||||
self._auth_user = auth_user
|
||||
self._auth_password = auth_password
|
||||
self._ssh_pkey = ssh_pkey
|
||||
self._instance_id = instance_id
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Establish a websocket connection to gns3server on the cloud instance.
|
||||
"""
|
||||
|
||||
log.debug('WSConnectThread.run() begin')
|
||||
servers = Servers.instance()
|
||||
server = servers.getCloudServer(self._host, self._port, self._ca_file,
|
||||
self._auth_user, self._auth_password, self._ssh_pkey,
|
||||
self._instance_id)
|
||||
log.debug('after getCloudServer call. {}'.format(server))
|
||||
self.established.emit(str(self._server_id))
|
||||
|
||||
log.debug('WSConnectThread.run() end')
|
||||
# emit signal on success
|
||||
self.established.emit(self._server_id)
|
||||
|
||||
|
||||
class UploadProjectThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Zip and Upload project to the cloud
|
||||
"""
|
||||
|
||||
# signals to update the progress dialog.
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent, cloud_settings, project_path, images_path):
|
||||
super().__init__(parent)
|
||||
self.cloud_settings = cloud_settings
|
||||
self.project_path = project_path
|
||||
self.images_path = images_path
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
log.info("Exporting project to cloud")
|
||||
self.update.emit(0)
|
||||
|
||||
zipped_project_file = self.zip_project_dir()
|
||||
|
||||
self.update.emit(10) # update progress to 10%
|
||||
|
||||
provider = get_provider(self.cloud_settings)
|
||||
provider.upload_file(zipped_project_file, 'projects/' + os.path.basename(zipped_project_file))
|
||||
|
||||
self.update.emit(20) # update progress to 20%
|
||||
|
||||
topology = Topology.instance()
|
||||
images = set([node.settings()["image"] for node in topology.nodes() if 'image' in node.settings()])
|
||||
|
||||
for i, image in enumerate(images):
|
||||
provider.upload_file(image, 'images/' + os.path.relpath(image, self.images_path))
|
||||
self.update.emit(20 + (float(i) / len(images) * 80))
|
||||
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error exporting project to cloud")
|
||||
self.error.emit("Error exporting project: {}".format(e), True)
|
||||
|
||||
def zip_project_dir(self):
|
||||
"""
|
||||
Zips project files
|
||||
:return: path to zipped project file
|
||||
"""
|
||||
project_name = os.path.basename(self.project_path)
|
||||
output_filename = os.path.join(tempfile.gettempdir(), project_name + ".zip")
|
||||
project_dir = os.path.dirname(self.project_path)
|
||||
relroot = os.path.abspath(os.path.join(project_dir, os.pardir))
|
||||
with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for root, dirs, files in os.walk(project_dir):
|
||||
# add directory (needed for empty dirs)
|
||||
zip_file.write(root, os.path.relpath(root, relroot))
|
||||
for file in files:
|
||||
filename = os.path.join(root, file)
|
||||
if os.path.isfile(filename) and not self._should_exclude(filename): # regular files only
|
||||
arcname = os.path.join(os.path.relpath(root, relroot), file)
|
||||
zip_file.write(filename, arcname)
|
||||
|
||||
return output_filename
|
||||
|
||||
def _should_exclude(self, filename):
|
||||
"""
|
||||
Returns True if file should be excluded from zip of project files
|
||||
:param filename:
|
||||
:return: True if file should be excluded from zip, False otherwise
|
||||
"""
|
||||
return filename.endswith('.ghost')
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class UploadFilesThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Uploads files to cloud files
|
||||
|
||||
:param cloud_settings:
|
||||
:param files_to_upload: list of tuples of (file path, file name to save in cloud)
|
||||
"""
|
||||
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent, cloud_settings, files_to_upload):
|
||||
super().__init__(parent)
|
||||
self._cloud_settings = cloud_settings
|
||||
self._files_to_upload = files_to_upload
|
||||
|
||||
def run(self):
|
||||
self.update.emit(0)
|
||||
|
||||
try:
|
||||
for i, file_to_upload in enumerate(self._files_to_upload):
|
||||
provider = get_provider(self._cloud_settings)
|
||||
|
||||
log.debug('Uploading image {} to cloud as {}'.format(file_to_upload[0], file_to_upload[1]))
|
||||
provider.upload_file(file_to_upload[0], file_to_upload[1])
|
||||
|
||||
self.update.emit((i + 1) * 100 / len(self._files_to_upload))
|
||||
log.debug('Uploading image completed')
|
||||
except Exception as e:
|
||||
log.exception("Error uploading images to cloud")
|
||||
self.error.emit("Error uploading images: {}".format(e), True)
|
||||
|
||||
self.completed.emit()
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class DownloadProjectThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Downloads project from cloud storage
|
||||
"""
|
||||
|
||||
# signals to update the progress dialog.
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent, cloud_project_file_name, project_dest_path, images_dest_path, cloud_settings):
|
||||
super().__init__(parent)
|
||||
self.project_name = cloud_project_file_name
|
||||
self.project_dest_path = project_dest_path
|
||||
self.images_dest_path = images_dest_path
|
||||
self.cloud_settings = cloud_settings
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.update.emit(0)
|
||||
provider = get_provider(self.cloud_settings)
|
||||
zip_file = provider.download_file(self.project_name)
|
||||
zip_file = zipfile.ZipFile(zip_file, mode='r')
|
||||
zip_file.extractall(self.project_dest_path)
|
||||
zip_file.close()
|
||||
project_name = zip_file.namelist()[0].strip('/')
|
||||
|
||||
self.update.emit(20)
|
||||
|
||||
with open(os.path.join(self.project_dest_path, project_name, project_name + '.gns3'), 'r') as f:
|
||||
project_settings = json.loads(f.read())
|
||||
|
||||
images = set()
|
||||
for node in project_settings["topology"].get("nodes", []):
|
||||
if "properties" in node and "image" in node["properties"]:
|
||||
images.add(node["properties"]["image"])
|
||||
|
||||
image_names_in_cloud = provider.find_storage_image_names(images)
|
||||
|
||||
for i, image in enumerate(images):
|
||||
dest_path = os.path.join(self.images_dest_path, *image_names_in_cloud[image].split('/')[1:])
|
||||
|
||||
if not os.path.exists(os.path.dirname(dest_path)):
|
||||
os.makedirs(os.path.dirname(dest_path))
|
||||
|
||||
provider.download_file(image_names_in_cloud[image], dest_path)
|
||||
self.update.emit(20 + (float(i) / len(images) * 80))
|
||||
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error importing project from cloud")
|
||||
self.error.emit("Error importing project: {}".format(e), True)
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class DownloadImagesThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Downloads multiple files from cloud files
|
||||
"""
|
||||
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, cloud_settings, images_dest_path, image_names):
|
||||
super().__init__()
|
||||
self._cloud_settings = cloud_settings
|
||||
self._images_dest_path = images_dest_path
|
||||
self._image_names = image_names
|
||||
|
||||
def run(self):
|
||||
self.update.emit(0)
|
||||
try:
|
||||
provider = get_provider(self._cloud_settings)
|
||||
image_names_in_cloud = provider.find_storage_image_names(self._image_names)
|
||||
|
||||
for i, image in enumerate(self._image_names):
|
||||
dest_path = os.path.join(self._images_dest_path, *image_names_in_cloud[image].split('/')[1:])
|
||||
|
||||
if not os.path.exists(os.path.dirname(dest_path)):
|
||||
os.makedirs(os.path.dirname(dest_path))
|
||||
|
||||
provider.download_file(image_names_in_cloud[image], dest_path)
|
||||
|
||||
self.update.emit(i * 100 / len(self._image_names))
|
||||
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error importing project from cloud")
|
||||
self.error.emit("Error importing project: {}".format(e), True)
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class DeleteProjectThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Deletes project from cloud storage
|
||||
"""
|
||||
|
||||
# signals to update the progress dialog.
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent, project_file_name, cloud_settings):
|
||||
super().__init__(parent)
|
||||
self.project_file_name = project_file_name
|
||||
self.cloud_settings = cloud_settings
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
provider = get_provider(self.cloud_settings)
|
||||
provider.delete_file(self.project_file_name)
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error deleting project")
|
||||
self.error.emit("Error deleting project: {}".format(e), True)
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_cloud_projects(cloud_settings):
|
||||
provider = get_provider(cloud_settings)
|
||||
return provider.list_projects()
|
||||
@@ -1,252 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
from PyQt4.QtCore import QThread
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from .cloud.utils import ssh_client
|
||||
from .cloud.exceptions import KeyPairExists
|
||||
|
||||
from .servers import Servers
|
||||
from .topology import Topology
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudBuilder(QThread):
|
||||
|
||||
"""
|
||||
"""
|
||||
# Notify with progress amount and instance_id
|
||||
progressUpdate = pyqtSignal(object, str)
|
||||
|
||||
# Notify with current state and instance_id
|
||||
stateChange = pyqtSignal(object, str)
|
||||
|
||||
# Notify when instance is ready with instance_id
|
||||
buildComplete = pyqtSignal(str)
|
||||
|
||||
# Notify when the instance has been created with instance and keypair
|
||||
instanceCreated = pyqtSignal(object, object)
|
||||
|
||||
# Notify when the public ip is available with ip and instance_id
|
||||
instanceHasIP = pyqtSignal(str, str)
|
||||
|
||||
# Notify when instance id exists with builder and instance_id
|
||||
instanceIdExists = pyqtSignal(object, str)
|
||||
|
||||
def __init__(self, parent, cloud_provider, ca_dir):
|
||||
super(QThread, self).__init__(parent)
|
||||
# Store our parent so it can be passed to threads we spawn.
|
||||
self._parent = parent
|
||||
self._provider = cloud_provider
|
||||
self._ca_dir = ca_dir
|
||||
self._start_at_create = False
|
||||
self._start_at_setup = False
|
||||
self._instance = None
|
||||
|
||||
def startAtCreate(self, instance_name, flavor_id, image_id):
|
||||
self._start_at_create = True
|
||||
self._instance_name = instance_name
|
||||
self._flavor_id = flavor_id
|
||||
self._image_id = image_id
|
||||
|
||||
def startAtSetup(self, instance, keypair):
|
||||
self._start_at_setup = True
|
||||
self._instance = instance
|
||||
self._key_pair = keypair
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
log.debug('CloudBuilder.run')
|
||||
if self._start_at_create:
|
||||
log.debug('CloudBuilder._start_at_create')
|
||||
self._createInstance(self._provider, self._instance_name, self._flavor_id,
|
||||
self._image_id)
|
||||
log.debug('got here 3')
|
||||
if self._start_at_setup:
|
||||
log.debug('CloudBuilder start at setup')
|
||||
self._instanceCreated(self._instance, self._key_pair)
|
||||
except Exception:
|
||||
log.exception("CloudBuilder trapped an exception:")
|
||||
log.error('CloudBuilder stopped in error state.')
|
||||
|
||||
def _createInstance(self, provider, name, flavor_id, image_id):
|
||||
log.debug("Creating cloud keypair with name {}".format(name))
|
||||
key_pair = None
|
||||
while key_pair is None:
|
||||
try:
|
||||
key_pair = provider.create_key_pair(name)
|
||||
except KeyPairExists:
|
||||
log.debug("Deleting old key pair with name {}.".format(name))
|
||||
self._provider.delete_key_pair_by_name(name)
|
||||
except Exception as e:
|
||||
log.debug("create_key_pair exception {}".format(e))
|
||||
|
||||
log.debug("Creating cloud server with name {}".format(name))
|
||||
instance = None
|
||||
while instance is None:
|
||||
try:
|
||||
instance = self._provider.create_instance(name, flavor_id, image_id, key_pair)
|
||||
except Exception as e:
|
||||
log.debug("create_instance exception {}".format(e))
|
||||
log.debug("Cloud server {} created".format(name))
|
||||
self._instanceCreated(instance, key_pair)
|
||||
|
||||
def _instanceCreated(self, instance, key_pair):
|
||||
log.debug('CloudBuilder._instanceCreated {}'.format(instance.id))
|
||||
self._instance = instance
|
||||
self._instance_id = instance.id
|
||||
self._key_pair = key_pair
|
||||
self.instanceIdExists.emit(self, instance.id)
|
||||
self.instanceCreated.emit(instance, key_pair)
|
||||
self._waitForPublicIP()
|
||||
|
||||
def _waitForPublicIP(self):
|
||||
public_ip = None
|
||||
while public_ip is None:
|
||||
time.sleep(10)
|
||||
try:
|
||||
instance = self._provider.get_instance(self._instance)
|
||||
# Look for public ip address
|
||||
for ip in instance.public_ips:
|
||||
# Don't use the ipv6 address
|
||||
if ':' not in ip:
|
||||
public_ip = ip
|
||||
break
|
||||
except Exception as e:
|
||||
log.debug('list_instances error: {}'.format(e))
|
||||
|
||||
# updated info, keep it.
|
||||
self._instance = instance
|
||||
self._public_ip = public_ip
|
||||
self.instanceHasIP.emit(self._public_ip, self._instance.id)
|
||||
time.sleep(60)
|
||||
self._startGNS3Server(1800)
|
||||
|
||||
def _startGNS3Server(self, dead_time):
|
||||
commands = '''
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --configure -a
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
|
||||
ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
|
||||
mkdir -p /opt/gns3
|
||||
cd /opt/gns3; git clone https://github.com/planctechnologies/gns3-server.git
|
||||
cd /opt/gns3/gns3-server; git checkout dev; git pull
|
||||
cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
|
||||
cd /opt/gns3/gns3-server; python3 ./setup.py install
|
||||
ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
|
||||
wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap.tar.gz'
|
||||
tar xzf iouyap.tar.gz -C /usr/local/bin
|
||||
python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
|
||||
hostname gns3-iouvm # set hostname for iou
|
||||
wget 'http://downloads.sourceforge.net/project/vpcs/0.6/vpcs_0.6_Linux64'
|
||||
cp vpcs_0.6_Linux64 /usr/local/bin/vpcs
|
||||
chmod a+x /usr/local/bin/vpcs
|
||||
killall python3 gns3server gns3dms
|
||||
'''
|
||||
|
||||
def exec_command(client, cmd, wait_time=-1):
|
||||
|
||||
cmd += '; exit $?'
|
||||
|
||||
stdout_data = b''
|
||||
stderr_data = b''
|
||||
|
||||
log.debug('cmd: {}'.format(cmd))
|
||||
# Send the command (non-blocking)
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
|
||||
# Wait for the command to terminate
|
||||
wait = int(wait_time)
|
||||
while not stdout.channel.exit_status_ready() and wait != 0:
|
||||
time.sleep(1)
|
||||
wait -= 1
|
||||
|
||||
stdout_data = stdout.read()
|
||||
stderr_data = stderr.read()
|
||||
log.debug('exit status: {}'.format(stdout.channel.exit_status))
|
||||
log.debug('stdout: {}'.format(stdout_data.decode('utf-8')))
|
||||
log.debug('stderr: {}'.format(stderr_data.decode('utf-8')))
|
||||
return stdout_data, stderr_data
|
||||
|
||||
# We might be attempting a connection before the instance is fully booted, so retry
|
||||
# when the ssh connection fails.
|
||||
ssh_connected = False
|
||||
response = None
|
||||
while not ssh_connected:
|
||||
with ssh_client(self._public_ip, self._key_pair.private_key) as client:
|
||||
if client is None:
|
||||
time.sleep(1)
|
||||
continue
|
||||
ssh_connected = True
|
||||
|
||||
for cmd in [l for l in commands.splitlines() if l.strip()]:
|
||||
exec_command(client, cmd)
|
||||
|
||||
data = {
|
||||
'instance_id': self._instance_id,
|
||||
'cloud_user_name': self._provider.username,
|
||||
'cloud_api_key': self._provider.api_key,
|
||||
'cloud_region': self._provider.region,
|
||||
'dead_time': dead_time,
|
||||
}
|
||||
# TODO: Properly escape the data portion of the command line
|
||||
start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --ip={} --data="{}" 2>/tmp/gns3-stderr.log'.format(self._public_ip, data)
|
||||
stdout, stderr = exec_command(client, start_cmd, wait_time=15)
|
||||
response = stdout.decode('utf-8')
|
||||
|
||||
log.debug(response)
|
||||
data = ast.literal_eval(response)
|
||||
# TODO: have the server return the port it is running on
|
||||
port = 8000
|
||||
|
||||
username = data['WEB_USERNAME']
|
||||
password = data['WEB_PASSWORD']
|
||||
|
||||
ssl_cert = ''.join(data['SSL_CRT'])
|
||||
ca_filename = 'cloud_server_{}.crt'.format(self._public_ip)
|
||||
ca_dir = self._ca_dir
|
||||
ca_file = os.path.join(ca_dir, ca_filename)
|
||||
try:
|
||||
os.makedirs(ca_dir)
|
||||
except FileExistsError:
|
||||
pass
|
||||
with open(ca_file, 'wb') as ca_fh:
|
||||
ca_fh.write(ssl_cert.encode('utf-8'))
|
||||
|
||||
topology = Topology.instance()
|
||||
top_instance = topology.getInstance(self._instance_id)
|
||||
top_instance.set_later_attributes(self._public_ip, port, ssl_cert, ca_file)
|
||||
|
||||
servers = Servers.instance()
|
||||
server = servers.getCloudServer(self._public_ip, port, ca_file, username, password,
|
||||
self._key_pair.private_key, self._instance_id)
|
||||
servers.save()
|
||||
log.debug('Cloud server gns3server started.')
|
||||
self.buildComplete.emit(self._instance_id)
|
||||
@@ -1,427 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
|
||||
from libcloud.compute.types import NodeState
|
||||
|
||||
from .qt import QtCore, QtGui
|
||||
from .cloud.utils import (ListInstancesThread, DeleteInstanceThread)
|
||||
from .topology import Topology
|
||||
from .servers import Servers
|
||||
|
||||
|
||||
# this widget was promoted on Creator, must use absolute imports
|
||||
from gns3.ui.cloud_inspector_view_ui import Ui_CloudInspectorView
|
||||
from gns3.cloud_builder import CloudBuilder
|
||||
from gns3.cloud_instances import CloudInstances
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
POLLING_TIMER = 10000 # in milliseconds
|
||||
|
||||
|
||||
class RunningInstanceState(NodeState):
|
||||
|
||||
"""
|
||||
GNS3 states for running instances
|
||||
"""
|
||||
GNS3SERVER_STARTING = -1
|
||||
GNS3SERVER_STARTED = -2
|
||||
WS_CONNECTED = -3
|
||||
|
||||
|
||||
class InstanceTableModel(QtCore.QAbstractTableModel):
|
||||
|
||||
"""
|
||||
A custom table model storing data of cloud instances
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InstanceTableModel, self).__init__(*args, **kwargs)
|
||||
self._header_data = ['Instance', '', 'Size', 'Devices'] # status has an empty header label
|
||||
self._width = len(self._header_data)
|
||||
self._instances = {}
|
||||
self._ids = []
|
||||
self.flavors = {}
|
||||
|
||||
@property
|
||||
def instanceIds(self):
|
||||
return self._ids
|
||||
|
||||
def clear(self):
|
||||
self._instances = {}
|
||||
self._ids = []
|
||||
self.reset()
|
||||
|
||||
def _get_status_icon_path(self, instance):
|
||||
"""
|
||||
Return a string pointing to the graphic resource
|
||||
"""
|
||||
if instance.state == RunningInstanceState.WS_CONNECTED:
|
||||
return ':/icons/led_green.svg'
|
||||
elif instance.state in (RunningInstanceState.STOPPED,
|
||||
RunningInstanceState.TERMINATED,
|
||||
RunningInstanceState.UNKNOWN):
|
||||
return ':/icons/led_red.svg'
|
||||
else:
|
||||
return ':/icons/led_yellow.svg'
|
||||
|
||||
def rowCount(self, QModelIndex_parent=None, *args, **kwargs):
|
||||
return len(self._instances)
|
||||
|
||||
def columnCount(self, QModelIndex_parent=None, *args, **kwargs):
|
||||
return self._width if len(self._instances) else 0
|
||||
|
||||
def data(self, index, role=None):
|
||||
instance = self._instances.get(self._ids[index.row()])
|
||||
col = index.column()
|
||||
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
if col == 1:
|
||||
# status
|
||||
return QtGui.QIcon(self._get_status_icon_path(instance))
|
||||
|
||||
elif role == QtCore.Qt.DisplayRole:
|
||||
if col == 0:
|
||||
# name
|
||||
return instance.name
|
||||
elif col == 2:
|
||||
# size
|
||||
try:
|
||||
# for Rackspace instances, update flavor id with a verbose description
|
||||
return self.flavors.get(instance.extra['flavorId'])
|
||||
except KeyError:
|
||||
# fallback to libcloud size property
|
||||
if instance.size:
|
||||
return instance.size.ram
|
||||
# giveup on showing size
|
||||
return 'Unknown'
|
||||
elif col == 3:
|
||||
# devices
|
||||
count = 0
|
||||
topology = Topology.instance()
|
||||
for node in topology.nodes():
|
||||
id = node._server.instance_id or 0
|
||||
if instance.id == id:
|
||||
count += 1
|
||||
return count
|
||||
return None
|
||||
|
||||
def headerData(self, section, orientation, role=None):
|
||||
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
|
||||
try:
|
||||
return self._header_data[section]
|
||||
except IndexError:
|
||||
return None
|
||||
return super(InstanceTableModel, self).headerData(section, orientation, role)
|
||||
|
||||
def addInstance(self, instance):
|
||||
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
|
||||
if not len(self._instances):
|
||||
self.beginInsertColumns(QtCore.QModelIndex(), 0, self._width - 1)
|
||||
self.endInsertColumns()
|
||||
self._ids.append(instance.id)
|
||||
self._instances[instance.id] = instance
|
||||
self.endInsertRows()
|
||||
|
||||
def getInstance(self, index):
|
||||
"""
|
||||
Retrieve the i-th instance if index is in range
|
||||
"""
|
||||
try:
|
||||
return self._instances.get(self._ids[index])
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def removeInstance(self, instance):
|
||||
self.removeInstanceById(instance.id)
|
||||
|
||||
def removeInstanceById(self, instance_id):
|
||||
try:
|
||||
index = self._ids.index(instance_id)
|
||||
self.beginRemoveRows(QtCore.QModelIndex(), index, index)
|
||||
del self._instances[instance_id]
|
||||
del self._ids[index]
|
||||
self.endRemoveRows()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def updateInstanceFields(self, instance, field_names):
|
||||
"""
|
||||
Update model data and notify connected views
|
||||
"""
|
||||
if instance.id in self._ids:
|
||||
index = self._ids.index(instance.id)
|
||||
current = self._instances[instance.id]
|
||||
for field in field_names:
|
||||
setattr(current, field, getattr(instance, field))
|
||||
first_index = self.createIndex(index, 0)
|
||||
last_index = self.createIndex(index, self.columnCount() - 1)
|
||||
self.dataChanged.emit(first_index, last_index)
|
||||
else:
|
||||
self.addInstance(instance)
|
||||
|
||||
def getInstanceById(self, instance_id):
|
||||
return self._instances.get(instance_id, None)
|
||||
|
||||
|
||||
class CloudInspectorView(QtGui.QWidget, Ui_CloudInspectorView):
|
||||
|
||||
"""
|
||||
Table view showing data coming from InstanceTableModel
|
||||
|
||||
Signals:
|
||||
instanceSelected(int) Emitted when users click and select an instance on the inspector.
|
||||
Param int is the ID of the instance
|
||||
"""
|
||||
instanceSelected = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent):
|
||||
super(QtGui.QWidget, self).__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._provider = None
|
||||
self._settings = None
|
||||
self._project_instances_id = []
|
||||
self._main_window = None
|
||||
|
||||
self._model = InstanceTableModel() # shortcut for self.uiInstancesTableView.model()
|
||||
self.uiInstancesTableView.setModel(self._model)
|
||||
self.uiInstancesTableView.verticalHeader().hide()
|
||||
self.uiInstancesTableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.uiInstancesTableView.horizontalHeader().setStretchLastSection(True)
|
||||
# connections
|
||||
self.uiInstancesTableView.customContextMenuRequested.connect(self._contextMenu)
|
||||
self.uiInstancesTableView.clicked.connect(self._rowChanged)
|
||||
self.uiCreateInstanceButton.clicked.connect(self._create_new_instance)
|
||||
|
||||
self._pollingTimer = QtCore.QTimer(self)
|
||||
self._pollingTimer.timeout.connect(self._polling_slot)
|
||||
|
||||
# map flavor ids to combobox indexes
|
||||
self.flavor_index_id = []
|
||||
|
||||
# A dictionary of {image_id, CloudBuilder}
|
||||
self._builders = {}
|
||||
|
||||
def _get_flavor_index(self, flavor_id):
|
||||
try:
|
||||
return self.flavor_index_id.index(flavor_id)
|
||||
except ValueError:
|
||||
return -1
|
||||
|
||||
def load(self, main_win, instance_ids):
|
||||
"""
|
||||
Fill the model data layer with instance info loaded from the topology file
|
||||
"""
|
||||
self._main_window = main_win
|
||||
self._provider = main_win.cloudProvider
|
||||
self._settings = main_win.cloudSettings()
|
||||
log.info('CloudInspectorView.load')
|
||||
|
||||
for instance_id in instance_ids:
|
||||
self._project_instances_id.append(instance_id)
|
||||
|
||||
update_thread = ListInstancesThread(self, self._provider)
|
||||
update_thread.instancesReady.connect(self._update_model)
|
||||
update_thread.start()
|
||||
self._pollingTimer.start(POLLING_TIMER)
|
||||
# fill sizes comboboxes
|
||||
for id, name in self._provider.list_flavors().items():
|
||||
self.uiCreateInstanceComboBox.addItem(name)
|
||||
self.flavor_index_id.append(id)
|
||||
# select default flavor
|
||||
new_instance_flavor = self._settings["new_instance_flavor"]
|
||||
self.uiCreateInstanceComboBox.setCurrentIndex(self._get_flavor_index(new_instance_flavor))
|
||||
|
||||
def addInstance(self, instance):
|
||||
"""
|
||||
Add a new instance to the inspector
|
||||
"""
|
||||
self._project_instances_id.append(instance.id)
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear contents and stop polling timer
|
||||
"""
|
||||
self._model.clear()
|
||||
self._pollingTimer.stop()
|
||||
self._project_instances_id = []
|
||||
|
||||
def _contextMenu(self, pos):
|
||||
# create actions
|
||||
delete_action = QtGui.QAction("Delete", self)
|
||||
delete_action.triggered.connect(self._deleteSelectedInstance)
|
||||
# create context menu and add actions
|
||||
menu = QtGui.QMenu(self.uiInstancesTableView)
|
||||
menu.addAction(delete_action)
|
||||
# show the menu
|
||||
menu.popup(self.uiInstancesTableView.viewport().mapToGlobal(pos))
|
||||
|
||||
def _deleteSelectedInstance(self):
|
||||
"""
|
||||
Delete the instance corresponding to the selected table row
|
||||
"""
|
||||
sel = self.uiInstancesTableView.selectedIndexes()
|
||||
if len(sel) and self._provider is not None:
|
||||
index = sel[0].row()
|
||||
instance = self._model.getInstance(index)
|
||||
|
||||
# warn user this is destructive
|
||||
msg = "Do you want to remove the instance and any devices running on it?"
|
||||
proceed = QtGui.QMessageBox.question(self, 'Warning', msg,
|
||||
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
|
||||
|
||||
if proceed == QtGui.QMessageBox.Yes:
|
||||
# disconnect and remove the server
|
||||
servers = Servers.instance()
|
||||
cs = servers.cloudServerById(instance.id)
|
||||
if cs is not None:
|
||||
servers.removeCloudServer(cs)
|
||||
|
||||
# remove instance from the the topology
|
||||
topology = Topology.instance()
|
||||
topology.removeInstance(instance.id)
|
||||
|
||||
delete_thread = DeleteInstanceThread(self, self._provider, instance)
|
||||
delete_thread.instanceDeleted.connect(self._main_window.remove_instance_from_project)
|
||||
delete_thread.start()
|
||||
|
||||
instance.name = 'Deleting...'
|
||||
self._model.updateInstanceFields(instance, ['name'])
|
||||
|
||||
def _rowChanged(self, index):
|
||||
"""
|
||||
This slot is invoked every time users change the current selected row on the
|
||||
inspector
|
||||
"""
|
||||
selection = self.uiInstancesTableView.selectionModel().selection()
|
||||
if selection.isEmpty():
|
||||
return
|
||||
|
||||
item = selection.indexes()[0]
|
||||
if item.isValid():
|
||||
instance = self._model.getInstance(item.row())
|
||||
self.instanceSelected.emit(instance.id)
|
||||
|
||||
def _polling_slot(self):
|
||||
"""
|
||||
Sync model data with instances status
|
||||
"""
|
||||
if self._provider is None:
|
||||
return
|
||||
|
||||
update_thread = ListInstancesThread(self, self._provider)
|
||||
update_thread.instancesReady.connect(self._update_model)
|
||||
update_thread.start()
|
||||
|
||||
def _instanceBuilt(self, id):
|
||||
"""
|
||||
This slot is called when instance has finished building.
|
||||
"""
|
||||
instance = self._model.getInstanceById(id)
|
||||
instance.state = RunningInstanceState.WS_CONNECTED
|
||||
self._model.updateInstanceFields(instance, ['state'])
|
||||
|
||||
if self._main_window.loading_cloud_project:
|
||||
project = self._main_window.project()
|
||||
path = project.topologyPath()
|
||||
with open(path, "r") as f:
|
||||
json_topology = json.load(f)
|
||||
topology = Topology.instance()
|
||||
topology.load(json_topology)
|
||||
self._main_window.loading_cloud_project = False
|
||||
|
||||
def _update_model(self, instances):
|
||||
if not instances:
|
||||
return
|
||||
|
||||
# Filter instances to only those in the current project
|
||||
project_instances = [i for i in instances if i.id in self._project_instances_id]
|
||||
|
||||
# populate underlying model if this is the first call
|
||||
if self._model.rowCount() == 0 and len(project_instances) > 0:
|
||||
self._populate_model(project_instances)
|
||||
self._rebuild_instances(project_instances)
|
||||
|
||||
instance_manager = CloudInstances.instance()
|
||||
instance_manager.update_instances(instances)
|
||||
|
||||
# Clean up removed instances
|
||||
real = set(i.id for i in project_instances)
|
||||
current = set(self._model.instanceIds)
|
||||
for i in current.difference(real):
|
||||
self._model.removeInstanceById(i)
|
||||
self.uiInstancesTableView.resizeColumnsToContents()
|
||||
|
||||
# Update instance status
|
||||
for i in project_instances:
|
||||
# get the customized instance state from self._model
|
||||
model_instance = self._model.getInstanceById(i.id)
|
||||
|
||||
# update model instance state if needed
|
||||
if i.state != RunningInstanceState.RUNNING:
|
||||
self._model.updateInstanceFields(i, ['state'])
|
||||
|
||||
def _populate_model(self, instances):
|
||||
log.info('CloudInspectorView._populate_model')
|
||||
self._model.flavors = self._provider.list_flavors()
|
||||
# filter instances for current project
|
||||
for inst in instances:
|
||||
self._model.addInstance(inst)
|
||||
self.uiInstancesTableView.resizeColumnsToContents()
|
||||
|
||||
def _create_new_instance(self):
|
||||
idx = self.uiCreateInstanceComboBox.currentIndex()
|
||||
flavor_id = self.flavor_index_id[idx]
|
||||
image_id = self._settings['default_image']
|
||||
|
||||
name, ok = QtGui.QInputDialog.getText(self,
|
||||
"New instance",
|
||||
"Choose a name for the instance and press Ok,\n"
|
||||
"then wait for the instance to appear in the inspector.")
|
||||
|
||||
if ok:
|
||||
self.createInstance(name, flavor_id, image_id)
|
||||
|
||||
def createInstance(self, instance_name, flavor_id, image_id):
|
||||
if not instance_name.endswith("-gns3"):
|
||||
instance_name += "-gns3"
|
||||
# TODO: Add a keys_dir to projectSettings
|
||||
ca_dir = os.path.join(self._main_window.projectSettings()["project_files_dir"], "keys")
|
||||
|
||||
builder = CloudBuilder(self, self._provider, ca_dir)
|
||||
builder.startAtCreate(instance_name, flavor_id, image_id)
|
||||
builder.instanceCreated.connect(self._main_window.add_instance_to_project)
|
||||
builder.instanceCreated.connect(CloudInstances.instance().add_instance)
|
||||
builder.instanceIdExists.connect(self._associateBuilderWithInstance)
|
||||
builder.instanceHasIP.connect(CloudInstances.instance().update_host_for_instance)
|
||||
builder.buildComplete.connect(self._instanceBuilt)
|
||||
builder.start()
|
||||
return builder
|
||||
|
||||
def _associateBuilderWithInstance(self, builder, instance_id):
|
||||
self._builders[instance_id] = builder
|
||||
|
||||
def _rebuild_instances(self, instances):
|
||||
# TODO: Add a keys_dir to projectSettings
|
||||
ca_dir = os.path.join(self._main_window.projectSettings()["project_files_dir"], "keys")
|
||||
|
||||
for instance in instances:
|
||||
log.debug('CloudInspectorView._rebuild_instances {}'.format(instance.name))
|
||||
builder = CloudBuilder(self, self._provider, ca_dir)
|
||||
cloud_instance = CloudInstances.instance().get_instance(instance.id)
|
||||
public_key = cloud_instance.public_key
|
||||
private_key = cloud_instance.private_key
|
||||
# Fake a KeyPair object because we don't store it.
|
||||
keypair = namedtuple('KeyPair', ['private_key', 'public_key'])(private_key, public_key)
|
||||
builder.startAtSetup(instance, keypair)
|
||||
builder.instanceCreated.connect(self._main_window.add_instance_to_project)
|
||||
builder.instanceCreated.connect(CloudInstances.instance().add_instance)
|
||||
builder.instanceIdExists.connect(self._associateBuilderWithInstance)
|
||||
builder.instanceHasIP.connect(CloudInstances.instance().update_host_for_instance)
|
||||
builder.buildComplete.connect(self._instanceBuilt)
|
||||
builder.start()
|
||||
return builder
|
||||
@@ -1,154 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Keeps track of all cloud instances the app has started.
|
||||
"""
|
||||
|
||||
from .qt import QtCore
|
||||
from gns3.topology import TopologyInstance
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudInstances(QtCore.QObject):
|
||||
|
||||
"""
|
||||
This class stores the instances that gns3 gui has started. This can be different than the list
|
||||
of instances in the topology that can be changed when switching projects. This list is not touched
|
||||
when switching projects and is stored in the .ini file.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CloudInstances, self).__init__(*args, **kwargs)
|
||||
self._instances = []
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only one instance of CloudInstances.
|
||||
|
||||
:returns: instance of CloudInstances
|
||||
"""
|
||||
|
||||
if not hasattr(CloudInstances, "_instance"):
|
||||
CloudInstances._instance = CloudInstances()
|
||||
return CloudInstances._instance
|
||||
|
||||
@property
|
||||
def instances(self):
|
||||
return self._instances
|
||||
|
||||
def clear(self):
|
||||
self._instances.clear()
|
||||
|
||||
def add(self, topology_instance):
|
||||
self._instances.append(topology_instance)
|
||||
|
||||
def add_instance(self, instance, keypair):
|
||||
if instance is None:
|
||||
return
|
||||
existing = self.get_instance(instance.id)
|
||||
if existing is None:
|
||||
ti = TopologyInstance(instance.name, instance.id, instance.extra['flavorId'],
|
||||
instance.extra['imageId'], keypair.private_key, keypair.public_key)
|
||||
self._instances.append(ti)
|
||||
self.save()
|
||||
|
||||
def update_instances(self, instances):
|
||||
"""
|
||||
Compare with the existing list of instances to purge instances that no
|
||||
longer exist.
|
||||
"""
|
||||
save_needed = False
|
||||
# Look for instances that have been deleted
|
||||
for stored in self._instances:
|
||||
found = False
|
||||
for dynamic in instances:
|
||||
if stored.id == dynamic.id:
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
self._instances.remove(stored)
|
||||
save_needed = True
|
||||
|
||||
if save_needed:
|
||||
self.save()
|
||||
|
||||
def update_host_for_instance(self, host, instance_id):
|
||||
"""
|
||||
Update the public IP for the instance.
|
||||
"""
|
||||
for instance in self.instances:
|
||||
if instance.id == instance_id:
|
||||
if instance.host != host:
|
||||
instance.host = host
|
||||
self.save()
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save the list of cloud instances to the config file
|
||||
"""
|
||||
log.debug('Saving cloud instances')
|
||||
settings = QtCore.QSettings()
|
||||
settings.beginGroup("CloudInstances")
|
||||
settings.remove("")
|
||||
|
||||
# Save the instances
|
||||
settings.beginWriteArray("cloud_instance", len(self._instances))
|
||||
index = 0
|
||||
for instance in self._instances:
|
||||
settings.setArrayIndex(index)
|
||||
for name in instance.fields():
|
||||
value = getattr(instance, name) if not None else ""
|
||||
log.debug('{}={}'.format(name, str(value)[0:60]))
|
||||
settings.setValue(name, value)
|
||||
index += 1
|
||||
settings.endArray()
|
||||
settings.endGroup()
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Load instance info from the config file to the topology
|
||||
"""
|
||||
log.debug('Loading cloud instances')
|
||||
settings = QtCore.QSettings()
|
||||
settings.beginGroup("CloudInstances")
|
||||
|
||||
# Load the instances
|
||||
size = settings.beginReadArray("cloud_instance")
|
||||
for index in range(0, size):
|
||||
settings.setArrayIndex(index)
|
||||
info = {}
|
||||
for name in TopologyInstance.fields():
|
||||
value = settings.value(name, "")
|
||||
log.debug('{}={}'.format(name, str(value)[0:60]))
|
||||
info[name] = value
|
||||
ti = TopologyInstance(**info)
|
||||
self._instances.append(ti)
|
||||
|
||||
def get_instance(self, instance_id):
|
||||
"""
|
||||
Retrieve a TopologyInstance objects if present
|
||||
"""
|
||||
for i in self._instances:
|
||||
if i.id == instance_id:
|
||||
return i
|
||||
return None
|
||||
@@ -36,10 +36,6 @@ except ImportError:
|
||||
|
||||
class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
cmd.Cmd.__init__(self)
|
||||
|
||||
def do_version(self, args):
|
||||
"""
|
||||
Show the version of GNS3 and its dependencies.
|
||||
@@ -194,7 +190,7 @@ class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
name = node.name()
|
||||
console_port = node.console()
|
||||
console_host = node.server().host
|
||||
console_host = node.server().host()
|
||||
try:
|
||||
from .telnet_console import telnetConsole
|
||||
telnetConsole(name, console_host, console_port)
|
||||
@@ -204,7 +200,7 @@ class ConsoleCmd(cmd.Cmd):
|
||||
def do_debug(self, args):
|
||||
"""
|
||||
Activate or deactivate debugging messages
|
||||
debug [level] (0 or 1).
|
||||
debug [level] (0, 1 or 2).
|
||||
"""
|
||||
|
||||
if '?' in args or args.strip() == "":
|
||||
@@ -212,16 +208,22 @@ class ConsoleCmd(cmd.Cmd):
|
||||
return
|
||||
|
||||
root = logging.getLogger()
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
|
||||
if len(args) == 1:
|
||||
level = int(args[0])
|
||||
if level == 0:
|
||||
print("Deactivating debugging")
|
||||
root.removeHandler(ch)
|
||||
for handler in root.handlers:
|
||||
if isinstance(handler, logging.StreamHandler):
|
||||
root.removeHandler(handler)
|
||||
root.setLevel(logging.INFO)
|
||||
else:
|
||||
print("Activating debugging")
|
||||
root.addHandler(ch)
|
||||
root.addHandler(logging.StreamHandler(sys.stdout))
|
||||
if level == 1:
|
||||
print("Activating debugging")
|
||||
else:
|
||||
print("Activating full debugging")
|
||||
root.setLevel(logging.DEBUG)
|
||||
from .main_window import MainWindow
|
||||
MainWindow.instance().setSettings({"debug_level": level})
|
||||
else:
|
||||
|
||||
@@ -20,11 +20,14 @@ import sys
|
||||
import struct
|
||||
import inspect
|
||||
import datetime
|
||||
|
||||
from .qt import QtCore
|
||||
from .topology import Topology
|
||||
from .version import __version__
|
||||
from .console_cmd import ConsoleCmd
|
||||
from .pycutext import PyCutExt
|
||||
from .modules import MODULES
|
||||
from .local_config import LocalConfig
|
||||
|
||||
|
||||
class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
@@ -38,12 +41,15 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
# Set introduction message
|
||||
bitness = struct.calcsize("P") * 8
|
||||
current_year = datetime.date.today().year
|
||||
self.intro = "GNS3 management console. Running GNS3 version {} on {} ({}-bit).\n" \
|
||||
"Copyright (c) 2006-{} GNS3 Technologies.".format(__version__, platform.system(), bitness, current_year)
|
||||
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {}.\n" \
|
||||
"Copyright (c) 2006-{} GNS3 Technologies.".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, current_year)
|
||||
|
||||
if LocalConfig.instance().experimental():
|
||||
self.intro += "\nWARNING: Experimental features enable. You can use some unfinished features and lost data."
|
||||
|
||||
# Parent class initialization
|
||||
try:
|
||||
PyCutExt.__init__(self, None, self.intro, parent=parent)
|
||||
super().__init__(None, self.intro, parent=parent)
|
||||
|
||||
# dynamically get all the available commands so we can color them
|
||||
methods = inspect.getmembers(self, predicate=inspect.ismethod)
|
||||
@@ -85,7 +91,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
|
||||
if len(self.line) > 0:
|
||||
cmd, args, _ = self.parseline(line)
|
||||
if cmd == '':
|
||||
if cmd is None or cmd == '':
|
||||
compfunc = self.completedefault
|
||||
else:
|
||||
try:
|
||||
@@ -186,8 +192,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
if node:
|
||||
if node.name():
|
||||
name = " {}:".format(node.name())
|
||||
server = "from {}:{}".format(node.server().host,
|
||||
node.server().port)
|
||||
server = "from {}".format(node.server().url())
|
||||
|
||||
text = "Server error {server}:{name} {message}".format(server=server,
|
||||
name=name,
|
||||
|
||||
@@ -17,41 +17,71 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import raven
|
||||
import struct
|
||||
import platform
|
||||
import struct
|
||||
|
||||
from .version import __version__
|
||||
from .servers import Servers
|
||||
try:
|
||||
import raven
|
||||
RAVEN_AVAILABLE = True
|
||||
except ImportError:
|
||||
# raven is not installed with deb package in order to simplify packaging
|
||||
RAVEN_AVAILABLE = False
|
||||
|
||||
from .utils.get_resource import get_resource
|
||||
from .version import __version__, __version_info__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Dev build
|
||||
if __version_info__[3] != 0:
|
||||
import faulthandler
|
||||
# Display a traceback in case of segfault crash. Usefull when frozen
|
||||
# Not enabled by default for security reason
|
||||
log.info("Enable catching segfault")
|
||||
faulthandler.enable()
|
||||
|
||||
|
||||
class CrashReport:
|
||||
|
||||
"""
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "sync+https://2e90f3a4dc094b39a94864ae1ced969c:fc65363ea35245f28ae2b7205de744c9@app.getsentry.com/38506"
|
||||
DSN = "sync+https://3d44e34021504514a5fb0539ae6f8f92:af41562761754b4c9beca492d1b9115d@app.getsentry.com/38506"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = os.path.join(os.getcwd(), "cacert.pem")
|
||||
if os.path.isfile(cacert):
|
||||
cacert = get_resource("cacert.pem")
|
||||
if cacert is not None and os.path.isfile(cacert):
|
||||
DSN += "?ca_certs={}".format(cacert)
|
||||
else:
|
||||
log.warning("The SSL certificate bundle file '{}' could not be found".format(cacert))
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
self._client = None
|
||||
# We don't want sentry making noise if an error is catched when you don't have internet
|
||||
sentry_errors = logging.getLogger('sentry.errors')
|
||||
sentry_errors.disabled = True
|
||||
|
||||
sentry_uncaught = logging.getLogger('sentry.errors.uncaught')
|
||||
sentry_uncaught.disabled = True
|
||||
|
||||
def captureException(self, exception, value, tb):
|
||||
from .servers import Servers
|
||||
|
||||
local_server = Servers.instance().localServerSettings()
|
||||
if local_server["report_errors"]:
|
||||
if self._client is None:
|
||||
self._client = raven.Client(CrashReport.DSN, release=__version__)
|
||||
self._client.tags_context({
|
||||
if not RAVEN_AVAILABLE:
|
||||
return
|
||||
if os.path.exists(".git"):
|
||||
log.warning("A .git directory exist crash report is turn off for developers")
|
||||
return
|
||||
|
||||
if hasattr(exception, "fingerprint"):
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, fingerprint=['{{ default }}', exception.fingerprint])
|
||||
else:
|
||||
client = raven.Client(CrashReport.DSN, release=__version__)
|
||||
context = {
|
||||
"os:name": platform.system(),
|
||||
"os:release": platform.release(),
|
||||
"os:win_32": " ".join(platform.win32_ver()),
|
||||
@@ -63,13 +93,26 @@ class CrashReport:
|
||||
"python:bit": struct.calcsize("P") * 8,
|
||||
"python:encoding": sys.getdefaultencoding(),
|
||||
"python:frozen": "{}".format(hasattr(sys, "frozen"))
|
||||
})
|
||||
}
|
||||
context = self._add_qt_information(context)
|
||||
client.tags_context(context)
|
||||
try:
|
||||
report = self._client.captureException((exception, value, tb))
|
||||
report = client.captureException((exception, value, tb))
|
||||
except Exception as e:
|
||||
log.error("Can't send crash report to Sentry: {}".format(e))
|
||||
return
|
||||
log.info("Crash report sent with event ID: {}".format(self._client.get_ident(report)))
|
||||
log.info("Crash report sent with event ID: {}".format(client.get_ident(report)))
|
||||
|
||||
def _add_qt_information(self, context):
|
||||
try:
|
||||
from .qt import QtCore
|
||||
import sip
|
||||
except ImportError:
|
||||
return context
|
||||
context["pyqt:version"] = QtCore.PYQT_VERSION_STR
|
||||
context["qt:version"] = QtCore.QT_VERSION_STR
|
||||
context["sip:version"] = sip.SIP_VERSION_STR
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def instance(cls):
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtGui
|
||||
from ..qt import QtWidgets
|
||||
from ..version import __version__
|
||||
from ..ui.about_dialog_ui import Ui_AboutDialog
|
||||
|
||||
|
||||
class AboutDialog(QtGui.QDialog, Ui_AboutDialog):
|
||||
class AboutDialog(QtWidgets.QDialog, Ui_AboutDialog):
|
||||
|
||||
"""
|
||||
About dialog.
|
||||
@@ -28,7 +28,7 @@ class AboutDialog(QtGui.QDialog, Ui_AboutDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
# dynamically add the current version number
|
||||
|
||||
440
gns3/dialogs/appliance_wizard.py
Normal file
440
gns3/dialogs/appliance_wizard.py
Normal file
@@ -0,0 +1,440 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ..qt import QtWidgets, QtCore, QtGui, qpartial
|
||||
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
|
||||
from ..image_manager import ImageManager
|
||||
from ..modules import Qemu
|
||||
from ..registry.appliance import Appliance
|
||||
from ..registry.registry import Registry
|
||||
from ..registry.config import Config, ConfigException
|
||||
from ..registry.image import Image
|
||||
from ..utils import human_filesize
|
||||
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..servers import Servers
|
||||
from ..gns3_vm import GNS3VM
|
||||
from ..local_config import LocalConfig
|
||||
|
||||
class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
def __init__(self, parent, path):
|
||||
super().__init__(parent)
|
||||
|
||||
self._path = path
|
||||
self.setupUi(self)
|
||||
images_directories = list()
|
||||
images_directories.append(os.path.dirname(self._path))
|
||||
download_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
|
||||
if download_directory != "" and download_directory != os.path.dirname(self._path):
|
||||
images_directories.append(download_directory)
|
||||
self._registry = Registry(images_directories)
|
||||
self._appliance = Appliance(self._registry, self._path)
|
||||
self._registry.appendImageDirectory(os.path.join(ImageManager.instance().getDirectory(), self._appliance.image_dir_name()))
|
||||
|
||||
self.uiApplianceVersionTreeWidget.currentItemChanged.connect(self._applianceVersionCurrentItemChangedSlot)
|
||||
self.uiRefreshPushButton.clicked.connect(self._refreshVersions)
|
||||
self.uiDownloadPushButton.clicked.connect(self._downloadPushButtonClickedSlot)
|
||||
self.uiImportPushButton.clicked.connect(self._importPushButtonClickedSlot)
|
||||
|
||||
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
if hasattr(self, "uiLoadBalanceCheckBox"):
|
||||
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
|
||||
|
||||
self.uiServerWizardPage.isComplete = self._uiServerWizardPage_isComplete
|
||||
|
||||
def initializePage(self, page_id):
|
||||
"""
|
||||
Initialize Wizard pages.
|
||||
|
||||
:param page_id: page identifier
|
||||
"""
|
||||
super().initializePage(page_id)
|
||||
|
||||
if self._appliance["category"] == "guest":
|
||||
symbol = ":/symbols/computer.svg"
|
||||
else:
|
||||
symbol = ":/symbols/{}.svg".format(self._appliance["category"])
|
||||
self.page(page_id).setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(symbol))
|
||||
|
||||
if "qemu" in self._appliance:
|
||||
type = "qemu"
|
||||
elif "iou" in self._appliance:
|
||||
type = "iou"
|
||||
elif "dynamips" in self._appliance:
|
||||
type = "dynamips"
|
||||
|
||||
if self.page(page_id) == self.uiInfoWizardPage:
|
||||
self.uiInfoWizardPage.setTitle(self._appliance["product_name"])
|
||||
self.uiDescriptionLabel.setText(self._appliance["description"])
|
||||
|
||||
info = (
|
||||
("Category", "category"),
|
||||
("Product", "product_name"),
|
||||
("Vendor", "vendor_name"),
|
||||
("Status", "status"),
|
||||
("Maintainer", "maintainer"),
|
||||
("KVM", "kvm")
|
||||
)
|
||||
|
||||
self.uiInfoTreeWidget.clear()
|
||||
for (name, key) in info:
|
||||
if key in self._appliance:
|
||||
item = QtWidgets.QTreeWidgetItem([name + ":", self._appliance[key]])
|
||||
font = item.font(0)
|
||||
font.setBold(True)
|
||||
item.setFont(0, font)
|
||||
self.uiInfoTreeWidget.addTopLevelItem(item)
|
||||
|
||||
elif self.page(page_id) == self.uiServerWizardPage:
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
for server in Servers.instance().remoteServers().values():
|
||||
self.uiRemoteServersComboBox.addItem(server.url(), server)
|
||||
|
||||
if not GNS3VM.instance().isRunning():
|
||||
self.uiVMRadioButton.setEnabled(False)
|
||||
|
||||
# Qemu has issues on OSX and Windows we disallow usage of the local server
|
||||
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")) and not LocalConfig.instance().experimental():
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
|
||||
if GNS3VM.instance().isRunning():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
elif Servers.instance().localServer().isLocalServerRunning():
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
elif len(Servers.instance().remoteServers().values()) > 0:
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setChecked(False)
|
||||
|
||||
elif self.page(page_id) == self.uiFilesWizardPage:
|
||||
self._refreshVersions()
|
||||
|
||||
elif self.page(page_id) == self.uiQemuWizardPage:
|
||||
Qemu.instance().getQemuBinariesFromServer(self._server, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
|
||||
|
||||
elif self.page(page_id) == self.uiSummaryWizardPage:
|
||||
self.uiSummaryTreeWidget.clear()
|
||||
|
||||
for key in self._appliance[type]:
|
||||
item = QtWidgets.QTreeWidgetItem([key.replace('_', ' ').capitalize() + ":", str(self._appliance[type][key])])
|
||||
font = item.font(0)
|
||||
font.setBold(True)
|
||||
item.setFont(0, font)
|
||||
self.uiSummaryTreeWidget.addTopLevelItem(item)
|
||||
self.uiSummaryTreeWidget.resizeColumnToContents(0)
|
||||
|
||||
elif self.page(page_id) == self.uiUsageWizardPage:
|
||||
self.uiUsageTextEdit.setText("The appliance is available in the {} category. \n\n{}".format(
|
||||
self._appliance["category"].replace("_", " "),
|
||||
self._appliance.get("usage", ""))
|
||||
)
|
||||
|
||||
def _uiServerWizardPage_isComplete(self):
|
||||
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
|
||||
|
||||
def _refreshVersions(self):
|
||||
"""
|
||||
Refresh the list of files for different version of the appliance
|
||||
"""
|
||||
self.uiFilesWizardPage.setSubTitle("The following versions are available for " + self._appliance["product_name"] + ". Check the status of files required to install.")
|
||||
self.uiApplianceVersionTreeWidget.clear()
|
||||
|
||||
worker = WaitForLambdaWorker(lambda: self._resfreshDialogWorker())
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Scanning directories for images...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if progress_dialog.exec_():
|
||||
for version in self._appliance["versions"]:
|
||||
top = QtWidgets.QTreeWidgetItem(["{} {}".format(self._appliance["product_name"], version["name"])])
|
||||
|
||||
size = 0
|
||||
status = "Ready to install"
|
||||
for image in version["images"].values():
|
||||
if image["status"] == "Missing":
|
||||
status = "Missing files"
|
||||
|
||||
size += image["filesize"]
|
||||
image_widget = QtWidgets.QTreeWidgetItem(
|
||||
[
|
||||
"",
|
||||
image["filename"],
|
||||
human_filesize(image["filesize"]),
|
||||
image["status"],
|
||||
image["version"]
|
||||
])
|
||||
|
||||
if image["status"] == "Missing":
|
||||
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
|
||||
else:
|
||||
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
|
||||
|
||||
# Associated data stored are col 0: version, col 1: image
|
||||
image_widget.setData(0, QtCore.Qt.UserRole, version)
|
||||
image_widget.setData(1, QtCore.Qt.UserRole, image)
|
||||
image_widget.setData(2, QtCore.Qt.UserRole, self._appliance)
|
||||
top.addChild(image_widget)
|
||||
|
||||
font = top.font(0)
|
||||
font.setBold(True)
|
||||
top.setFont(0, font)
|
||||
|
||||
expand = True
|
||||
if status == "Missing files":
|
||||
top.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
|
||||
else:
|
||||
expand = False
|
||||
top.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
|
||||
|
||||
top.setData(2, QtCore.Qt.DisplayRole, human_filesize(size))
|
||||
top.setData(3, QtCore.Qt.DisplayRole, status)
|
||||
top.setData(2, QtCore.Qt.UserRole, self._appliance)
|
||||
top.setData(0, QtCore.Qt.UserRole, version)
|
||||
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
|
||||
if expand:
|
||||
top.setExpanded(True)
|
||||
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(0)
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(1)
|
||||
self.uiApplianceVersionTreeWidget.setCurrentItem(self.uiApplianceVersionTreeWidget.topLevelItem(0))
|
||||
|
||||
def _resfreshDialogWorker(self):
|
||||
"""
|
||||
Scan local directory in order to found the images on disk
|
||||
"""
|
||||
for version in self._appliance["versions"]:
|
||||
for image in version["images"].values():
|
||||
if self._registry.search_image_file(image["filename"], image["md5sum"], image["filesize"]):
|
||||
image["status"] = "Found"
|
||||
else:
|
||||
image["status"] = "Missing"
|
||||
|
||||
def _applianceVersionCurrentItemChangedSlot(self, current, previous):
|
||||
"""
|
||||
Called when user select a different item in the list of appliance files
|
||||
"""
|
||||
self.uiDownloadPushButton.hide()
|
||||
self.uiImportPushButton.hide()
|
||||
|
||||
if current is None:
|
||||
return
|
||||
|
||||
image = current.data(1, QtCore.Qt.UserRole)
|
||||
if image is not None:
|
||||
if "direct_download_url" in image or "download_url" in image:
|
||||
self.uiDownloadPushButton.show()
|
||||
self.uiImportPushButton.show()
|
||||
|
||||
def _downloadPushButtonClickedSlot(self):
|
||||
"""
|
||||
Called when user want to download an appliance images.
|
||||
He should have selected the file before.
|
||||
"""
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
data = current.data(1, QtCore.Qt.UserRole)
|
||||
if data is not None:
|
||||
if "direct_download_url" in data:
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["direct_download_url"]))
|
||||
if "compression" in data:
|
||||
QtWidgets.QMessageBox.warning(self, "Add appliance", "The image is compressed with {} you need to uncompress it before using it.".format(data["compression"]))
|
||||
else:
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["download_url"]))
|
||||
|
||||
def _importPushButtonClickedSlot(self):
|
||||
"""
|
||||
Called when user want to import an appliance images.
|
||||
He should have selected the file before.
|
||||
"""
|
||||
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
disk = current.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName()
|
||||
if len(path) == 0:
|
||||
return
|
||||
|
||||
image = Image(path)
|
||||
if image.md5sum != disk["md5sum"]:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "This is not the correct image file. The MD5 sum is {} and should be {}".format(image.md5sum, disk["md5sum"]))
|
||||
return
|
||||
|
||||
config = Config()
|
||||
worker = WaitForLambdaWorker(lambda: image.copy(os.path.join(config.images_dir, self._appliance.image_dir_name()), disk["filename"]), allowed_exceptions=[OSError])
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Import the appliance...", None, busy=True, parent=self)
|
||||
if not progress_dialog.exec_():
|
||||
return
|
||||
self._refreshVersions()
|
||||
|
||||
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for getQemuBinariesFromServer.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu binaries", "{}".format(result["message"]))
|
||||
else:
|
||||
self.uiQemuListComboBox.clear()
|
||||
for qemu in result:
|
||||
if qemu["version"]:
|
||||
self.uiQemuListComboBox.addItem("{path} (v{version})".format(path=qemu["path"], version=qemu["version"]), qemu["path"])
|
||||
else:
|
||||
self.uiQemuListComboBox.addItem("{path}".format(path=qemu["path"]), qemu["path"])
|
||||
if self.uiQemuListComboBox.count() == 1:
|
||||
self.next()
|
||||
|
||||
def _install(self, version):
|
||||
"""
|
||||
Install the appliance to GNS3
|
||||
|
||||
:params version: Version name
|
||||
"""
|
||||
|
||||
try:
|
||||
config = Config()
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
||||
return False
|
||||
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
|
||||
if self._server.isLocal():
|
||||
server_string = "local"
|
||||
elif self._server.isGNS3VM():
|
||||
server_string = "vm"
|
||||
else:
|
||||
server_string = self._server.url()
|
||||
|
||||
while len(appliance_configuration["name"]) == 0 or not config.is_name_available(appliance_configuration["name"]):
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "The name \"{}\" is already used by another appliance".format(appliance_configuration["name"]))
|
||||
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add appliance", "New name:", QtWidgets.QLineEdit.Normal, appliance_configuration["name"])
|
||||
appliance_configuration["name"] = appliance_configuration["name"].strip()
|
||||
|
||||
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
|
||||
|
||||
worker = WaitForLambdaWorker(lambda: config.add_appliance(appliance_configuration, server_string), allowed_exceptions=[ConfigException, OSError])
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if not progress_dialog.exec_():
|
||||
return False
|
||||
|
||||
worker = WaitForLambdaWorker(lambda: config.save(), allowed_exceptions=[ConfigException, OSError])
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if progress_dialog.exec_():
|
||||
QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
|
||||
return True
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the settings.
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiFilesWizardPage:
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
appliance = current.data(2, QtCore.Qt.UserRole)
|
||||
name = "{} {}".format(appliance["name"], version["name"])
|
||||
if not self._appliance.is_version_installable(version["name"]):
|
||||
QtWidgets.QMessageBox.warning(self, "Appliance", "Sorry, you cannot install {} with missing files".format(name))
|
||||
return False
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Would you like to install {}?".format(name), QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
|
||||
elif self.currentPage() == self.uiUsageWizardPage:
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
return self._install(version["name"])
|
||||
|
||||
elif self.currentPage() == self.uiServerWizardPage:
|
||||
if self.uiRemoteRadioButton.isChecked():
|
||||
if not Servers.instance().remoteServers():
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote server registered in your preferences")
|
||||
return False
|
||||
self._server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
|
||||
gns3_vm_server = Servers.instance().vmServer()
|
||||
if gns3_vm_server is None:
|
||||
QtWidgets.QMessageBox.critical(self, "GNS3 VM", "The GNS3 VM is not running")
|
||||
return False
|
||||
self._server = gns3_vm_server
|
||||
else:
|
||||
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")):
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and MacOSX is not supported by the GNS3 team. Are you sur to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
|
||||
self._server = Servers.instance().localServer()
|
||||
|
||||
elif self.currentPage() == self.uiQemuWizardPage:
|
||||
if self.uiQemuListComboBox.currentIndex() == -1:
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu binary", "No compatible Qemu binary selected")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _vmToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the VM radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the remote server radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(True)
|
||||
self.uiRemoteServersGroupBox.show()
|
||||
|
||||
def _localToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the local server radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
def _loadBalanceToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the load balance checkbox is toggled.
|
||||
|
||||
:param checked: either the box is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersComboBox.setEnabled(False)
|
||||
else:
|
||||
self.uiRemoteServersComboBox.setEnabled(True)
|
||||
@@ -19,12 +19,12 @@
|
||||
Dialog to configure and update node settings using widget pages.
|
||||
"""
|
||||
|
||||
from ..qt import QtGui
|
||||
from ..qt import QtWidgets
|
||||
from ..ui.configuration_dialog_ui import Ui_configurationDialog
|
||||
from .node_configurator_dialog import ConfigurationError
|
||||
from .node_properties_dialog import ConfigurationError
|
||||
|
||||
|
||||
class ConfigurationDialog(QtGui.QDialog, Ui_configurationDialog):
|
||||
class ConfigurationDialog(QtWidgets.QDialog, Ui_configurationDialog):
|
||||
|
||||
"""
|
||||
Configuration dialog implementation.
|
||||
@@ -37,7 +37,7 @@ class ConfigurationDialog(QtGui.QDialog, Ui_configurationDialog):
|
||||
|
||||
def __init__(self, name, settings, configuration_page, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.uiTitleLabel.setText(name)
|
||||
@@ -55,11 +55,11 @@ class ConfigurationDialog(QtGui.QDialog, Ui_configurationDialog):
|
||||
:param button: button that was clicked (QAbstractButton)
|
||||
"""
|
||||
|
||||
if button == self.uiButtonBox.button(QtGui.QDialogButtonBox.Cancel):
|
||||
QtGui.QDialog.reject(self)
|
||||
if button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
|
||||
QtWidgets.QDialog.reject(self)
|
||||
else:
|
||||
try:
|
||||
self._configuration_page.saveSettings(self._settings)
|
||||
except ConfigurationError:
|
||||
return
|
||||
QtGui.QDialog.accept(self)
|
||||
QtWidgets.QDialog.accept(self)
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from ..ui.exec_command_dialog_ui import Ui_ExecCommandDialog
|
||||
|
||||
|
||||
class ExecCommandDialog(QtGui.QDialog, Ui_ExecCommandDialog):
|
||||
class ExecCommandDialog(QtWidgets.QDialog, Ui_ExecCommandDialog):
|
||||
|
||||
"""
|
||||
Execute a command and display its output.
|
||||
@@ -27,7 +27,7 @@ class ExecCommandDialog(QtGui.QDialog, Ui_ExecCommandDialog):
|
||||
|
||||
def __init__(self, parent, command, params):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.setWindowTitle("Executing {}".format(command))
|
||||
@@ -57,4 +57,4 @@ class ExecCommandDialog(QtGui.QDialog, Ui_ExecCommandDialog):
|
||||
|
||||
self._process.kill()
|
||||
self._process.waitForFinished()
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
120
gns3/dialogs/export_debug_dialog.py
Normal file
120
gns3/dialogs/export_debug_dialog.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from zipfile import ZipFile
|
||||
import platform
|
||||
import psutil
|
||||
import os
|
||||
|
||||
from gns3.version import __version__
|
||||
from gns3.qt import QtWidgets, QtCore
|
||||
from gns3.ui.export_debug_dialog_ui import Ui_ExportDebugDialog
|
||||
from gns3.local_config import LocalConfig
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
"""
|
||||
This dialog allow user to export useful information
|
||||
for remote debugging by a GNS3 developers.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, project):
|
||||
|
||||
super().__init__(parent)
|
||||
self._project = project
|
||||
self.setupUi(self)
|
||||
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export debug file", None, "Zip file (*.zip)", "Zip file (*.zip)")
|
||||
|
||||
if len(path) == 0:
|
||||
self.reject()
|
||||
return
|
||||
|
||||
log.info("Export debug information to %s", path)
|
||||
|
||||
try:
|
||||
with ZipFile(path, 'w') as zip:
|
||||
zip.writestr("debug.txt", self._getDebugData())
|
||||
dir = LocalConfig.configDirectory()
|
||||
for filename in os.listdir(dir):
|
||||
path = os.path.join(dir, filename)
|
||||
if os.path.isfile(path):
|
||||
zip.write(path, filename)
|
||||
|
||||
dir = self._project.filesDir()
|
||||
if dir:
|
||||
for filename in os.listdir(dir):
|
||||
path = os.path.join(dir, filename)
|
||||
if os.path.isfile(path):
|
||||
zip.write(path, filename)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Debug", "Can't export debug information: {}".format(str(e)))
|
||||
self.accept()
|
||||
|
||||
def _getDebugData(self):
|
||||
try:
|
||||
connections = psutil.net_connections()
|
||||
# You need to be root for OSX
|
||||
except psutil.AccessDenied:
|
||||
connections = None
|
||||
|
||||
try:
|
||||
addrs = ["* {}: {}".format(key, val) for key, val in psutil.net_if_addrs().items()]
|
||||
except UnicodeDecodeError:
|
||||
addrs = ["INVALID ADDR WITH UNICODE CHARACTERS"]
|
||||
|
||||
data = """Version: {version}
|
||||
OS: {os}
|
||||
Python: {python}
|
||||
Qt: {qt}
|
||||
CPU: {cpu}
|
||||
Memory: {memory}
|
||||
|
||||
Networks:
|
||||
{addrs}
|
||||
|
||||
Open connections:
|
||||
{connections}
|
||||
|
||||
Processus:
|
||||
""".format(
|
||||
version=__version__,
|
||||
qt=QtCore.BINDING_VERSION_STR,
|
||||
os=platform.platform(),
|
||||
python=platform.python_version(),
|
||||
memory=psutil.virtual_memory(),
|
||||
cpu=psutil.cpu_times(),
|
||||
connections=connections,
|
||||
addrs="\n".join(addrs)
|
||||
)
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
psinfo = proc.as_dict(attrs=["name", "exe"])
|
||||
data += "* {} {}\n".format(psinfo["name"], psinfo["exe"])
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
return data
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
print(ExportDebugDialog(None)._getDebugData())
|
||||
@@ -1,81 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWebKit
|
||||
from ..ui.getting_started_dialog_ui import Ui_GettingStartedDialog
|
||||
from ..utils.get_resource import get_resource
|
||||
from ..local_config import LocalConfig
|
||||
|
||||
|
||||
class GettingStartedDialog(QtGui.QDialog, Ui_GettingStartedDialog):
|
||||
|
||||
"""
|
||||
GettingStarted dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.uiWebView.page().mainFrame().setScrollBarPolicy(QtCore.Qt.Horizontal, QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.uiWebView.page().mainFrame().setScrollBarPolicy(QtCore.Qt.Vertical, QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.adjustSize()
|
||||
self.uiWebView.page().setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks)
|
||||
self.uiWebView.linkClicked.connect(self._urlClickedSlot)
|
||||
self._local_config = LocalConfig.instance()
|
||||
gui_settings = self._local_config.loadSectionSettings("GUI", {"hide_getting_started_dialog": False})
|
||||
self.uiCheckBox.setChecked(gui_settings["hide_getting_started_dialog"])
|
||||
getting_started = get_resource(os.path.join("static", "getting_started.html"))
|
||||
if getting_started and not (sys.platform.startswith("win") and not sys.maxsize > 2 ** 32):
|
||||
# do not show the page on Windows 32-bit (crash when no Internet connection)
|
||||
self.uiWebView.load(QtCore.QUrl.fromLocalFile(getting_started))
|
||||
else:
|
||||
self.uiCheckBox.setChecked(True)
|
||||
self.accept()
|
||||
|
||||
def showit(self):
|
||||
"""
|
||||
Either this dialog should be automatically showed at startup.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return not self.uiCheckBox.isChecked()
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
This dialog is closed.
|
||||
|
||||
:param result: ignored
|
||||
"""
|
||||
|
||||
self._local_config.saveSectionSettings("GUI", {"hide_getting_started_dialog": self.uiCheckBox.isChecked()})
|
||||
QtGui.QDialog.done(self, result)
|
||||
|
||||
def _urlClickedSlot(self, url):
|
||||
"""
|
||||
Opens a clicked URL using user's default browser.
|
||||
|
||||
:param url: URL to open
|
||||
"""
|
||||
|
||||
if QtGui.QDesktopServices.openUrl(url) is False:
|
||||
QtGui.QMessageBox.critical(self, "Getting started", "Failed to open the URL: {}".format(url))
|
||||
@@ -15,14 +15,15 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from ..qt import QtGui
|
||||
from ..qt import QtWidgets
|
||||
from ..topology import Topology
|
||||
from ..ui.idlepc_dialog_ui import Ui_IdlePCDialog
|
||||
|
||||
|
||||
class IdlePCDialog(QtGui.QDialog, Ui_IdlePCDialog):
|
||||
class IdlePCDialog(QtWidgets.QDialog, Ui_IdlePCDialog):
|
||||
|
||||
"""
|
||||
Idle-PC dialog.
|
||||
@@ -30,10 +31,10 @@ class IdlePCDialog(QtGui.QDialog, Ui_IdlePCDialog):
|
||||
|
||||
def __init__(self, router, idlepcs, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applySlot)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Help).clicked.connect(self._helpSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applySlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpSlot)
|
||||
|
||||
self._router = router
|
||||
self._idlepcs = idlepcs
|
||||
@@ -52,11 +53,13 @@ class IdlePCDialog(QtGui.QDialog, Ui_IdlePCDialog):
|
||||
Shows the help for Idle-PC.
|
||||
"""
|
||||
|
||||
help_text = "Finding the right idlepc value is a trial and error process, consisting of applying " \
|
||||
"different Idle-PC values and monitoring the CPU usage.\n\nBest Idle-PC values are usually " \
|
||||
"obtained when IOS is in idle state, the following message being displayed " \
|
||||
"on the console: {} con0 is now available ... Press RETURN to get started.".format(self._router.name())
|
||||
QtGui.QMessageBox.information(self, "Hints for Idle-PC", help_text)
|
||||
help_text = """Best Idle-PC values are obtained when IOS is in idle state, after the "Press RETURN to get started" message has appeared on the console, messages have finished displaying on the console and you have have actually pressed the RETURN key.
|
||||
|
||||
Finding the right idle-pc value is a trial and error process, consisting of applying different Idle-PC values and monitoring the CPU usage.
|
||||
|
||||
Select each value that appears in the list and click Apply, and note the CPU usage a few moments later. When you have found the value that minimises the CPU usage, apply that value.
|
||||
"""
|
||||
QtWidgets.QMessageBox.information(self, "Hints for Idle-PC", help_text)
|
||||
|
||||
def _applySlot(self):
|
||||
"""
|
||||
@@ -64,16 +67,19 @@ class IdlePCDialog(QtGui.QDialog, Ui_IdlePCDialog):
|
||||
"""
|
||||
|
||||
if not self.uiComboBox.count():
|
||||
QtGui.QMessageBox.critical(self, "Idle-PC", "Sorry could not find a valid Idle-PC value, please check again with Cisco IOS in a different state")
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC", "Sorry could not find a valid Idle-PC value, please check again with Cisco IOS in a different state")
|
||||
return
|
||||
|
||||
idlepc = self.uiComboBox.itemData(self.uiComboBox.currentIndex())
|
||||
# apply Idle-PC to all routers with the same IOS image
|
||||
ios_image = self._router.settings()["image"]
|
||||
ios_image = os.path.basename(self._router.settings()["image"])
|
||||
for node in Topology.instance().nodes():
|
||||
if hasattr(node, "idlepc") and node.settings()["image"] == ios_image:
|
||||
node.setIdlepc(idlepc)
|
||||
|
||||
# apply the idle-pc to templates with the same IOS image
|
||||
self._router.module().updateImageIdlepc(ios_image, idlepc)
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
Called when the dialog is closed.
|
||||
@@ -83,4 +89,4 @@ class IdlePCDialog(QtGui.QDialog, Ui_IdlePCDialog):
|
||||
|
||||
if result:
|
||||
self._applySlot()
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
"""
|
||||
Dialog for importing cloud projects
|
||||
"""
|
||||
|
||||
from ..ui.import_cloud_project_dialog_ui import Ui_ImportCloudProjectDialog
|
||||
from ..qt import QtGui
|
||||
from ..cloud.utils import get_cloud_projects, DownloadProjectThread, DeleteProjectThread
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
|
||||
|
||||
class ImportCloudProjectDialog(QtGui.QDialog, Ui_ImportCloudProjectDialog):
|
||||
|
||||
"""
|
||||
Import cloud project dialog implementation.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, project_dest_path, images_dest_path, cloud_settings):
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.project_dest_path = project_dest_path
|
||||
self.images_dest_path = images_dest_path
|
||||
self.cloud_settings = cloud_settings
|
||||
|
||||
self.uiImportProjectAction.clicked.connect(self._importProject)
|
||||
self.uiDeleteProjectAction.clicked.connect(self._deleteProject)
|
||||
self._listCloudProjects()
|
||||
|
||||
def _listCloudProjects(self):
|
||||
self.listWidget.clear()
|
||||
self.projects = get_cloud_projects(self.cloud_settings)
|
||||
self.listWidget.addItems(list(self.projects.keys()))
|
||||
|
||||
def _importProject(self):
|
||||
project_file_name = self.projects[self.listWidget.currentItem().text()]
|
||||
|
||||
download_thread = DownloadProjectThread(
|
||||
self,
|
||||
project_file_name,
|
||||
self.project_dest_path,
|
||||
self.images_dest_path,
|
||||
self.cloud_settings
|
||||
)
|
||||
progress_dialog = ProgressDialog(download_thread, "Importing project", "Downloading project files...", "Cancel",
|
||||
parent=self.parent())
|
||||
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
self.close()
|
||||
|
||||
def _deleteProject(self):
|
||||
project_file_name = self.projects[self.listWidget.currentItem().text()]
|
||||
|
||||
button_clicked = QtGui.QMessageBox.question(
|
||||
self,
|
||||
"Delete project",
|
||||
"Are you sure you want to delete project " + self.listWidget.currentItem().text(),
|
||||
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
|
||||
QtGui.QMessageBox.Yes
|
||||
)
|
||||
|
||||
if button_clicked == QtGui.QMessageBox.Yes:
|
||||
delete_project_thread = DeleteProjectThread(self, project_file_name, self.cloud_settings)
|
||||
progress_dialog = ProgressDialog(delete_project_thread, "Deleting project", "Deleting project files...",
|
||||
"Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
self._listCloudProjects()
|
||||
@@ -16,12 +16,11 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..ui.new_project_dialog_ui import Ui_NewProjectDialog
|
||||
from ..settings import ENABLE_CLOUD
|
||||
|
||||
|
||||
class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
|
||||
class NewProjectDialog(QtWidgets.QDialog, Ui_NewProjectDialog):
|
||||
|
||||
"""
|
||||
New project dialog.
|
||||
@@ -33,7 +32,7 @@ class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
|
||||
|
||||
def __init__(self, parent, showed_from_startup=False):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = parent
|
||||
@@ -46,8 +45,6 @@ class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
|
||||
self.uiLocationBrowserToolButton.clicked.connect(self._projectPathSlot)
|
||||
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
|
||||
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
|
||||
if not ENABLE_CLOUD:
|
||||
self.uiCloudRadioButton.hide()
|
||||
|
||||
if not showed_from_startup:
|
||||
self.uiOpenProjectPushButton.hide()
|
||||
@@ -72,8 +69,9 @@ class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
|
||||
Slot to select the a new project location.
|
||||
"""
|
||||
|
||||
path = QtGui.QFileDialog.getSaveFileName(self, "Project location", os.path.join(self._main_window.projectsDirPath(),
|
||||
self.uiNameLineEdit.text()))
|
||||
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Project location", os.path.join(self._main_window.projectsDirPath(),
|
||||
self.uiNameLineEdit.text()))
|
||||
|
||||
if path:
|
||||
self.uiLocationLineEdit.setText(path)
|
||||
|
||||
@@ -104,7 +102,7 @@ class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
|
||||
lot to show all the recent projects in a menu.
|
||||
"""
|
||||
|
||||
menu = QtGui.QMenu()
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.triggered.connect(self._menuTriggeredSlot)
|
||||
for action in self._main_window._recent_file_actions:
|
||||
menu.addAction(action)
|
||||
@@ -115,26 +113,23 @@ class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
|
||||
if result:
|
||||
project_name = self.uiNameLineEdit.text()
|
||||
project_location = self.uiLocationLineEdit.text()
|
||||
if self.uiCloudRadioButton.isChecked():
|
||||
project_type = "cloud"
|
||||
else:
|
||||
project_type = "local"
|
||||
project_type = "local"
|
||||
|
||||
if not project_name:
|
||||
QtGui.QMessageBox.critical(self, "New project", "Project name is empty")
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project name is empty")
|
||||
return
|
||||
|
||||
if not project_location:
|
||||
QtGui.QMessageBox.critical(self, "New project", "Project location is empty")
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project location is empty")
|
||||
return
|
||||
|
||||
if os.path.isdir(project_location):
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
"New project",
|
||||
"Location {} already exists, overwrite it?".format(project_location),
|
||||
QtGui.QMessageBox.Yes,
|
||||
QtGui.QMessageBox.No)
|
||||
if reply == QtGui.QMessageBox.No:
|
||||
reply = QtWidgets.QMessageBox.question(self,
|
||||
"New project",
|
||||
"Location {} already exists, overwrite it?".format(project_location),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
self._project_settings["project_name"] = project_name
|
||||
@@ -142,4 +137,4 @@ class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
|
||||
self._project_settings["project_files_dir"] = project_location
|
||||
self._project_settings["project_type"] = project_type
|
||||
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
@@ -19,17 +19,14 @@
|
||||
Dialog to configure and update node settings using widget pages.
|
||||
"""
|
||||
|
||||
from gns3.http_client import HTTPClient
|
||||
from gns3.progress import Progress
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..ui.node_configurator_dialog_ui import Ui_NodeConfiguratorDialog
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..ui.node_properties_dialog_ui import Ui_NodePropertiesDialog
|
||||
|
||||
|
||||
class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
|
||||
|
||||
"""
|
||||
Node configurator implementation.
|
||||
Node properties implementation.
|
||||
|
||||
:param node_items: list of NodeItem instances
|
||||
:param parent: parent widget
|
||||
@@ -37,41 +34,44 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
|
||||
def __init__(self, node_items, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._node_items = node_items
|
||||
self._parent_items = {}
|
||||
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Reset).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(False)
|
||||
|
||||
self.previousItem = None
|
||||
self.previousPage = None
|
||||
|
||||
# load the empty page widget by default
|
||||
self.uiEmptyPageWidget = self.uiConfigStackedWidget.findChildren(QtGui.QWidget, "uiEmptyPageWidget")[0]
|
||||
self.uiEmptyPageWidget = self.uiConfigStackedWidget.findChildren(QtWidgets.QWidget, "uiEmptyPageWidget")[0]
|
||||
self.uiConfigStackedWidget.setCurrentWidget(self.uiEmptyPageWidget)
|
||||
|
||||
self._loadNodeItems()
|
||||
self.splitter.setSizes([250, 600])
|
||||
self._loadNodeItems()
|
||||
|
||||
self.uiNodesTreeWidget.itemClicked.connect(self.showConfigurationPageSlot)
|
||||
HTTPClient.setProgressCallback(Progress(self, min_duration=0))
|
||||
|
||||
def _loadNodeItems(self):
|
||||
"""
|
||||
Loads the nodes into the Node configurator QTreeWidget
|
||||
Loads the nodes into the Node properties QTreeWidget
|
||||
"""
|
||||
|
||||
# create the parent (group) items
|
||||
for node_item in self._node_items:
|
||||
if not node_item.node().initialized():
|
||||
continue
|
||||
|
||||
# If something of one of the displayed nodes we reload everything
|
||||
node_item.node().updated_signal.connect(self.resetSettings)
|
||||
|
||||
group_name = " {} group".format(str(node_item.node()))
|
||||
parent = group_name
|
||||
if parent not in self._parent_items:
|
||||
item = QtGui.QTreeWidgetItem(self.uiNodesTreeWidget, [group_name])
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiNodesTreeWidget, [group_name])
|
||||
item.setIcon(0, QtGui.QIcon(node_item.node().defaultSymbol()))
|
||||
item.setExpanded(True)
|
||||
self._parent_items[parent] = item
|
||||
@@ -81,11 +81,19 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
if not node_item.node().initialized():
|
||||
continue
|
||||
parent = " {} group".format(str(node_item.node()))
|
||||
item = ConfigurationPageItem(self._parent_items[parent], node_item)
|
||||
ConfigurationPageItem(self._parent_items[parent], node_item)
|
||||
|
||||
# sort the tree
|
||||
self.uiNodesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
if len(self._node_items) == 1:
|
||||
parent = " {} group".format(str(node_item.node()))
|
||||
item = self._parent_items[parent].child(0)
|
||||
item.setSelected(True)
|
||||
self.uiNodesTreeWidget.setCurrentItem(item)
|
||||
self.showConfigurationPageSlot(item, 0)
|
||||
self.splitter.setSizes([0, 600])
|
||||
|
||||
def showConfigurationPageSlot(self, item, column):
|
||||
"""
|
||||
Shows a configuration page widget.
|
||||
@@ -122,11 +130,11 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
self.uiConfigStackedWidget.setCurrentWidget(page)
|
||||
|
||||
if page != self.uiEmptyPageWidget:
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).setEnabled(True)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Reset).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(True)
|
||||
else:
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Reset).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(False)
|
||||
|
||||
def on_uiButtonBox_clicked(self, button):
|
||||
"""
|
||||
@@ -136,18 +144,15 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
"""
|
||||
|
||||
try:
|
||||
from gns3.main_window import MainWindow
|
||||
if button == self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply):
|
||||
if button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply):
|
||||
self.applySettings()
|
||||
elif button == self.uiButtonBox.button(QtGui.QDialogButtonBox.Reset):
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset):
|
||||
self.resetSettings()
|
||||
elif button == self.uiButtonBox.button(QtGui.QDialogButtonBox.Cancel):
|
||||
HTTPClient.setProgressCallback(Progress(MainWindow.instance()))
|
||||
QtGui.QDialog.reject(self)
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
|
||||
QtWidgets.QDialog.reject(self)
|
||||
else:
|
||||
self.applySettings()
|
||||
HTTPClient.setProgressCallback(Progress(MainWindow.instance()))
|
||||
QtGui.QDialog.accept(self)
|
||||
QtWidgets.QDialog.accept(self)
|
||||
except ConfigurationError:
|
||||
pass
|
||||
|
||||
@@ -208,7 +213,7 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
child.setSettings(child.node().settings().copy())
|
||||
|
||||
|
||||
class ConfigurationPageItem(QtGui.QTreeWidgetItem):
|
||||
class ConfigurationPageItem(QtWidgets.QTreeWidgetItem):
|
||||
|
||||
"""
|
||||
Item for the QTreeWidget instance.
|
||||
@@ -221,7 +226,7 @@ class ConfigurationPageItem(QtGui.QTreeWidgetItem):
|
||||
def __init__(self, parent, node_item):
|
||||
|
||||
self._node = node_item.node()
|
||||
QtGui.QTreeWidgetItem.__init__(self, parent, [self._node.name()])
|
||||
super().__init__(parent, [self._node.name()])
|
||||
|
||||
# return the configuration page widget used to configure the node.
|
||||
self._page = self._node.configPage()
|
||||
@@ -242,7 +247,7 @@ class ConfigurationPageItem(QtGui.QTreeWidgetItem):
|
||||
|
||||
def page(self):
|
||||
"""
|
||||
Returns the page widget to be displayed by the node configurator.
|
||||
Returns the page widget to be displayed by the node properties dialog.
|
||||
|
||||
:returns: QWidget instance
|
||||
"""
|
||||
@@ -285,4 +290,4 @@ class ConfigurationError(Exception):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
Exception.__init__(self)
|
||||
super().__init__()
|
||||
@@ -19,19 +19,15 @@
|
||||
Dialog to load module and built-in preference pages.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from ..ui.preferences_dialog_ui import Ui_PreferencesDialog
|
||||
from ..pages.server_preferences_page import ServerPreferencesPage
|
||||
from ..pages.general_preferences_page import GeneralPreferencesPage
|
||||
from ..pages.cloud_preferences_page import CloudPreferencesPage
|
||||
from ..pages.packet_capture_preferences_page import PacketCapturePreferencesPage
|
||||
from ..modules import MODULES
|
||||
from ..settings import ENABLE_CLOUD
|
||||
from ..http_client import HTTPClient
|
||||
from ..progress import Progress
|
||||
|
||||
|
||||
class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
|
||||
"""
|
||||
Preferences dialog implementation.
|
||||
@@ -41,17 +37,32 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi(self)
|
||||
|
||||
# We adapt the max size to the screen resolution
|
||||
height = QtWidgets.QDesktopWidget().screenGeometry().height() - 100
|
||||
if height > 800:
|
||||
height = 800
|
||||
self.setMaximumSize(QtCore.QSize(900, height))
|
||||
self.resize(900, height)
|
||||
|
||||
self.uiTreeWidget.currentItemChanged.connect(self._showPreferencesPageSlot)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applyPreferences)
|
||||
self._applyButton = self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply)
|
||||
self._applyButton.clicked.connect(self._applyPreferences)
|
||||
self._applyButton.setEnabled(False)
|
||||
self._applyButton.setStyleSheet("QPushButton:disabled {color: gray}")
|
||||
self._items = []
|
||||
self._loadPreferencePages()
|
||||
|
||||
# select the first available page
|
||||
self.uiTreeWidget.setCurrentItem(self._items[0])
|
||||
HTTPClient.setProgressCallback(Progress(self, min_duration=0))
|
||||
|
||||
# Something has change?
|
||||
self._modified = False
|
||||
|
||||
|
||||
|
||||
def _loadPreferencePages(self):
|
||||
"""
|
||||
@@ -64,18 +75,17 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
ServerPreferencesPage,
|
||||
PacketCapturePreferencesPage,
|
||||
]
|
||||
if ENABLE_CLOUD:
|
||||
pages.append(CloudPreferencesPage)
|
||||
|
||||
for page in pages:
|
||||
preferences_page = page(self)
|
||||
preferences_page.loadPreferences()
|
||||
name = preferences_page.windowTitle()
|
||||
item = QtGui.QTreeWidgetItem(self.uiTreeWidget)
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiTreeWidget)
|
||||
item.setText(0, name)
|
||||
item.setData(0, QtCore.Qt.UserRole, preferences_page)
|
||||
self.uiStackedWidget.addWidget(preferences_page)
|
||||
self._items.append(item)
|
||||
self._watchForChanges(preferences_page)
|
||||
|
||||
# load module preference pages
|
||||
for module in MODULES:
|
||||
@@ -85,17 +95,43 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
preferences_page = cls()
|
||||
preferences_page.loadPreferences()
|
||||
name = preferences_page.windowTitle()
|
||||
item = QtGui.QTreeWidgetItem(parent)
|
||||
item = QtWidgets.QTreeWidgetItem(parent)
|
||||
item.setText(0, name)
|
||||
item.setData(0, QtCore.Qt.UserRole, preferences_page)
|
||||
self.uiStackedWidget.addWidget(preferences_page)
|
||||
self._items.append(item)
|
||||
if cls is preference_pages[0]:
|
||||
parent = item
|
||||
self._watchForChanges(preferences_page)
|
||||
|
||||
# expand all items by default
|
||||
self.uiTreeWidget.expandAll()
|
||||
|
||||
def _watchForChanges(self, preferences_page):
|
||||
"""
|
||||
Connect all the widget of a page to check if something has change
|
||||
"""
|
||||
|
||||
# Class name, changed signal
|
||||
widget_to_watch = {
|
||||
QtWidgets.QLineEdit: "textChanged",
|
||||
QtWidgets.QTreeWidget: "itemChanged",
|
||||
QtWidgets.QComboBox: "currentIndexChanged",
|
||||
QtWidgets.QSpinBox: "valueChanged",
|
||||
QtWidgets.QAbstractButton: "pressed"
|
||||
}
|
||||
|
||||
for widget, signal in widget_to_watch.items():
|
||||
for children in preferences_page.findChildren(widget):
|
||||
getattr(children, signal).connect(self._preferenceChangeSlot)
|
||||
|
||||
def _preferenceChangeSlot(self, *args):
|
||||
"""
|
||||
Called when somthing change in the preference dialog
|
||||
"""
|
||||
self._applyButton.setEnabled(True)
|
||||
self._modified = True
|
||||
|
||||
def _showPreferencesPageSlot(self, current, previous):
|
||||
"""
|
||||
Shows a preference page in the current dialog.
|
||||
@@ -132,6 +168,9 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
# if page.savePreferences() returns None, assume success
|
||||
if ok is not None and not ok:
|
||||
success = False
|
||||
if success:
|
||||
self._applyButton.setEnabled(False)
|
||||
self._modified = False
|
||||
return success
|
||||
|
||||
def reject(self):
|
||||
@@ -139,9 +178,15 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
Closes this dialog.
|
||||
"""
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
HTTPClient.setProgressCallback(Progress(MainWindow.instance()))
|
||||
QtGui.QDialog.reject(self)
|
||||
if self._modified:
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"Preferences",
|
||||
"You have unsaved preferences.\n\nContinue without saving?",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
QtWidgets.QDialog.reject(self)
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
@@ -149,11 +194,10 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
"""
|
||||
|
||||
# close the nodes dock to refresh the node list
|
||||
main_window = self.parentWidget()
|
||||
from ..main_window import MainWindow
|
||||
main_window = MainWindow.instance()
|
||||
main_window.uiNodesDockWidget.setVisible(False)
|
||||
main_window.uiNodesDockWidget.setWindowTitle("")
|
||||
|
||||
if self._applyPreferences():
|
||||
from gns3.main_window import MainWindow
|
||||
HTTPClient.setProgressCallback(Progress(MainWindow.instance()))
|
||||
QtGui.QDialog.accept(self)
|
||||
QtWidgets.QDialog.accept(self)
|
||||
|
||||
289
gns3/dialogs/setup_wizard.py
Normal file
289
gns3/dialogs/setup_wizard.py
Normal file
@@ -0,0 +1,289 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import psutil
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets, QtGui
|
||||
from gns3.servers import Servers
|
||||
from ..gns3_vm import GNS3VM
|
||||
from ..dialogs.preferences_dialog import PreferencesDialog
|
||||
from ..ui.setup_wizard_ui import Ui_SetupWizard
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..utils.wait_for_vm_worker import WaitForVMWorker
|
||||
from ..utils.wait_for_connection_worker import WaitForConnectionWorker
|
||||
|
||||
|
||||
class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
"""
|
||||
Base class for VM wizard.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
|
||||
|
||||
self._server = Servers.instance().localServer()
|
||||
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
|
||||
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
|
||||
self.uiVirtualBoxRadioButton.clicked.connect(self._listVirtualBoxVMsSlot)
|
||||
self.uiVMwareBannerButton.clicked.connect(self._VMwareBannerButtonClickedSlot)
|
||||
settings = parent.settings()
|
||||
self.uiShowCheckBox.setChecked(settings["hide_setup_wizard"])
|
||||
|
||||
# by default all radio buttons are unchecked
|
||||
self.uiVmwareRadioButton.setAutoExclusive(False)
|
||||
self.uiVirtualBoxRadioButton.setAutoExclusive(False)
|
||||
self.uiVmwareRadioButton.setChecked(False)
|
||||
self.uiVirtualBoxRadioButton.setChecked(False)
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.jpg"))
|
||||
else:
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.jpg"))
|
||||
|
||||
def _VMwareBannerButtonClickedSlot(self):
|
||||
if sys.platform.startswith("darwin"):
|
||||
url = "http://send.onenetworkdirect.net/z/616461/CD225091/"
|
||||
else:
|
||||
url = "http://send.onenetworkdirect.net/z/616460/CD225091/"
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
|
||||
|
||||
def _listVMwareVMsSlot(self):
|
||||
"""
|
||||
Slot to refresh the VMware VMs list.
|
||||
"""
|
||||
|
||||
self.uiVirtualBoxRadioButton.setChecked(False)
|
||||
from gns3.modules import VMware
|
||||
settings = VMware.instance().settings()
|
||||
if not os.path.exists(settings["vmrun_path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed")
|
||||
return
|
||||
self._refreshVMListSlot()
|
||||
|
||||
def _listVirtualBoxVMsSlot(self):
|
||||
"""
|
||||
Slot to refresh the VirtualBox VMs list.
|
||||
"""
|
||||
|
||||
self.uiVmwareRadioButton.setChecked(False)
|
||||
from gns3.modules import VirtualBox
|
||||
settings = VirtualBox.instance().settings()
|
||||
if not os.path.exists(settings["vboxmanage_path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "VirtualBox", "VBoxManage could not be found, VirtualBox is probably not installed")
|
||||
return
|
||||
self._refreshVMListSlot()
|
||||
|
||||
def showit(self):
|
||||
"""
|
||||
Either this dialog should be automatically showed at startup.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return not self.uiShowCheckBox.isChecked()
|
||||
|
||||
def _setPreferencesPane(self, dialog, name):
|
||||
"""
|
||||
Finds the first child of the QTreeWidgetItem name.
|
||||
|
||||
:param dialog: PreferencesDialog instance
|
||||
:param name: QTreeWidgetItem name
|
||||
|
||||
:returns: current QWidget
|
||||
"""
|
||||
|
||||
pane = dialog.uiTreeWidget.findItems(name, QtCore.Qt.MatchFixedString)[0]
|
||||
child_pane = pane.child(0)
|
||||
dialog.uiTreeWidget.setCurrentItem(child_pane)
|
||||
return dialog.uiStackedWidget.currentWidget()
|
||||
|
||||
def initializePage(self, page_id):
|
||||
"""
|
||||
Initialize Wizard pages.
|
||||
|
||||
:param page_id: page identifier
|
||||
"""
|
||||
|
||||
super().initializePage(page_id)
|
||||
if self.page(page_id) == self.uiVMWizardPage:
|
||||
cpu_count = psutil.cpu_count()
|
||||
self.uiCPUSpinBox.setValue(cpu_count)
|
||||
# we want to allocate half of the available physical memory
|
||||
ram = int(psutil.virtual_memory().total / (1024 * 1024) / 2)
|
||||
# value must be a multiple of 4 (VMware requirement)
|
||||
ram -= ram % 4
|
||||
self.uiRAMSpinBox.setValue(ram)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the settings.
|
||||
"""
|
||||
|
||||
gns3_vm = GNS3VM.instance()
|
||||
servers = Servers.instance()
|
||||
if self.currentPage() == self.uiVMWizardPage:
|
||||
vmname = self.uiVMListComboBox.currentText()
|
||||
if vmname:
|
||||
# save the GNS3 VM settings
|
||||
vm_settings = {"auto_start": True,
|
||||
"vmname": vmname,
|
||||
"vmx_path": self.uiVMListComboBox.currentData()}
|
||||
if self.uiVmwareRadioButton.isChecked():
|
||||
vm_settings["virtualization"] = "VMware"
|
||||
elif self.uiVirtualBoxRadioButton.isChecked():
|
||||
vm_settings["virtualization"] = "VirtualBox"
|
||||
gns3_vm.setSettings(vm_settings)
|
||||
servers.save()
|
||||
|
||||
# set the vCPU count and RAM
|
||||
vpcus = self.uiCPUSpinBox.value()
|
||||
ram = self.uiRAMSpinBox.value()
|
||||
if ram < 1024:
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM memory", "It is recommended to allocate a minimum of 1024 MB of RAM to the GNS3 VM")
|
||||
available_ram = int(psutil.virtual_memory().available / (1024 * 1024))
|
||||
if ram > available_ram:
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM memory", "You have probably allocated too much memory for the GNS3 VM! (available memory is {} MB)".format(available_ram))
|
||||
if gns3_vm.setvCPUandRAM(vpcus, ram) is False:
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "Could not configure vCPUs and RAM amounts for the GNS3 VM")
|
||||
|
||||
# start the GNS3 VM
|
||||
servers.initVMServer()
|
||||
worker = WaitForVMWorker()
|
||||
progress_dialog = ProgressDialog(worker, "GNS3 VM", "Starting the GNS3 VM...", "Cancel", busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if progress_dialog.exec_():
|
||||
previous_local_server_ip = servers.localServer().host()
|
||||
new_local_server_ip = gns3_vm.adjustLocalServerIP()
|
||||
self.uiShowCheckBox.setChecked(True)
|
||||
# restart the local server if necessary
|
||||
if new_local_server_ip != previous_local_server_ip:
|
||||
servers.stopLocalServer(wait=True)
|
||||
if servers.startLocalServer():
|
||||
worker = WaitForConnectionWorker(new_local_server_ip, servers.localServer().port())
|
||||
dialog = ProgressDialog(worker, "Local server", "Connecting...", "Cancel", busy=True, parent=self)
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
else:
|
||||
if not self.uiVmwareRadioButton.isChecked() and not self.uiVirtualBoxRadioButton.isChecked():
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "Please select VMware or VirtualBox")
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "Please select a VM. If no VM is listed, check if the GNS3 VM is correctly imported and press refresh.")
|
||||
return False
|
||||
elif self.currentPage() == self.uiAddVMsWizardPage:
|
||||
|
||||
use_local_server = self.uiLocalRadioButton.isChecked()
|
||||
if use_local_server:
|
||||
# deactivate the GNS3 VM if using the local server
|
||||
vm_settings = {"auto_start": False}
|
||||
gns3_vm.setSettings(vm_settings)
|
||||
servers.save()
|
||||
self.uiShowCheckBox.setChecked(True)
|
||||
|
||||
from gns3.modules import Dynamips
|
||||
Dynamips.instance().setSettings({"use_local_server": use_local_server})
|
||||
if sys.platform.startswith("linux"):
|
||||
# IOU only works on Linux
|
||||
from gns3.modules import IOU
|
||||
IOU.instance().setSettings({"use_local_server": use_local_server})
|
||||
from gns3.modules import Qemu
|
||||
Qemu.instance().setSettings({"use_local_server": use_local_server})
|
||||
from gns3.modules import VPCS
|
||||
VPCS.instance().setSettings({"use_local_server": use_local_server})
|
||||
|
||||
dialog = PreferencesDialog(self)
|
||||
if self.uiAddIOSRouterCheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "Dynamips").uiNewIOSRouterPushButton.clicked.emit(False)
|
||||
if self.uiAddIOUDeviceCheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "IOS on UNIX").uiNewIOUDevicePushButton.clicked.emit(False)
|
||||
if self.uiAddQemuVMcheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "QEMU").uiNewQemuVMPushButton.clicked.emit(False)
|
||||
if self.uiAddVirtualBoxVMcheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "VirtualBox").uiNewVirtualBoxVMPushButton.clicked.emit(False)
|
||||
if self.uiAddVMwareVMcheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "VMware").uiNewVMwareVMPushButton.clicked.emit(False)
|
||||
dialog.exec_()
|
||||
return True
|
||||
|
||||
def _refreshVMListSlot(self):
|
||||
"""
|
||||
Refresh the list of VM available in VMware or VirtualBox.
|
||||
"""
|
||||
|
||||
server = Servers.instance().localServer()
|
||||
if self.uiVmwareRadioButton.isChecked():
|
||||
server.get("/vmware/vms", self._getVMsFromServerCallback)
|
||||
elif self.uiVirtualBoxRadioButton.isChecked():
|
||||
server.get("/virtualbox/vms", self._getVMsFromServerCallback)
|
||||
|
||||
def _getVMsFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for getVMsFromServer.
|
||||
|
||||
:param progress_dialog: QProgressDialog instance
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "VM List", "{}".format(result["message"]))
|
||||
else:
|
||||
self.uiVMListComboBox.clear()
|
||||
for vm in result:
|
||||
self.uiVMListComboBox.addItem(vm["vmname"], vm.get("vmx_path", ""))
|
||||
gns3_vm = Servers.instance().vmSettings()
|
||||
index = self.uiVMListComboBox.findText(gns3_vm["vmname"])
|
||||
if index != -1:
|
||||
self.uiVMListComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
index = self.uiVMListComboBox.findText("GNS3 VM")
|
||||
if index != -1:
|
||||
self.uiVMListComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "GNS3 VM", "Could not find a VM named 'GNS3 VM', is it imported in VMware or VirtualBox?")
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
This dialog is closed.
|
||||
|
||||
:param result: ignored
|
||||
"""
|
||||
|
||||
settings = self.parentWidget().settings()
|
||||
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
|
||||
self.parentWidget().setSettings(settings)
|
||||
super().done(result)
|
||||
|
||||
def nextId(self):
|
||||
"""
|
||||
Wizard rules!
|
||||
"""
|
||||
|
||||
current_id = self.currentId()
|
||||
if self.page(current_id) == self.uiServerWizardPage and not self.uiVMRadioButton.isChecked():
|
||||
# skip the GNS3 VM page if using the local server.
|
||||
return self.uiServerWizardPage.nextId() + 1
|
||||
return QtWidgets.QWizard.nextId(self)
|
||||
@@ -24,15 +24,15 @@ import re
|
||||
import time
|
||||
import os
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..utils.process_files_thread import ProcessFilesThread
|
||||
from ..utils.process_files_worker import ProcessFilesWorker
|
||||
from ..ui.snapshots_dialog_ui import Ui_SnapshotsDialog
|
||||
from ..topology import Topology
|
||||
from ..node import Node
|
||||
|
||||
|
||||
class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
"""
|
||||
Snapshots dialog implementation.
|
||||
@@ -42,7 +42,7 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
def __init__(self, parent, project_path, project_files_dir):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._project_path = project_path
|
||||
@@ -70,7 +70,7 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
snapshot_name = match.group(1)
|
||||
snapshot_date = match.group(2)[:2] + '/' + match.group(2)[2:4] + '/' + match.group(2)[4:]
|
||||
snapshot_time = match.group(3)[:2] + ':' + match.group(3)[2:4] + ':' + match.group(3)[4:]
|
||||
item = QtGui.QListWidgetItem(self.uiSnapshotsList)
|
||||
item = QtWidgets.QListWidgetItem(self.uiSnapshotsList)
|
||||
item.setText("{} on {} at {}".format(snapshot_name, snapshot_date, snapshot_time))
|
||||
item.setData(QtCore.Qt.UserRole, os.path.join(snapshot_dir, snapshot))
|
||||
|
||||
@@ -89,17 +89,16 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
Slot to create a snapshot.
|
||||
"""
|
||||
|
||||
snapshot_name, ok = QtGui.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtGui.QLineEdit.Normal, "Unnamed")
|
||||
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.Normal, "Unnamed")
|
||||
if ok and snapshot_name:
|
||||
from ..main_window import MainWindow
|
||||
MainWindow.instance().saveProject(self._project_path)
|
||||
snapshot_name = "{name}_{date}".format(name=snapshot_name, date=time.strftime("%d%m%y_%H%M%S"))
|
||||
snapshot_dir = os.path.join(self._project_files_dir, "snapshots", snapshot_name)
|
||||
thread = ProcessFilesThread(os.path.dirname(self._project_path), snapshot_dir, skip_dirs=["snapshots"])
|
||||
progress_dialog = ProgressDialog(thread, "Creating snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
worker = ProcessFilesWorker(os.path.dirname(self._project_path), snapshot_dir, skip_dirs=["snapshots"])
|
||||
progress_dialog = ProgressDialog(worker, "Creating snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
thread.deleteLater()
|
||||
self._listSnaphosts()
|
||||
|
||||
def _deleteSnapshotSlot(self):
|
||||
@@ -135,9 +134,9 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
snapshot_name = match.group(1)
|
||||
else:
|
||||
snapshot_name = "Unknown"
|
||||
reply = QtGui.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot \"{}\" was taken?".format(snapshot_name),
|
||||
QtGui.QMessageBox.Ok, QtGui.QMessageBox.Cancel)
|
||||
if reply == QtGui.QMessageBox.Cancel:
|
||||
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot \"{}\" was taken?".format(snapshot_name),
|
||||
QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
|
||||
if reply == QtWidgets.QMessageBox.Cancel:
|
||||
return
|
||||
|
||||
# stop all the nodes
|
||||
@@ -159,21 +158,21 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
dirs = os.listdir(legacy_project_files_dir)
|
||||
for snapshot_subdir in dirs:
|
||||
snapshot_subdir_path = os.path.join(legacy_project_files_dir, snapshot_subdir)
|
||||
thread = ProcessFilesThread(snapshot_subdir_path, os.path.join(self._project_files_dir, snapshot_subdir))
|
||||
progress_dialog = ProgressDialog(thread, "Restoring snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
worker = ProcessFilesWorker(snapshot_subdir_path, os.path.join(self._project_files_dir, snapshot_subdir))
|
||||
progress_dialog = ProgressDialog(worker, "Restoring snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
thread.deleteLater()
|
||||
|
||||
os.remove(self._project_path)
|
||||
shutil.copy(os.path.join(snapshot_path, os.path.basename(self._project_path)), self._project_path)
|
||||
|
||||
try:
|
||||
os.remove(self._project_path)
|
||||
shutil.copy(os.path.join(snapshot_path, os.path.basename(self._project_path)), self._project_path)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Restore snapshot", "Cannot restore snapshot: {}".format(e))
|
||||
else:
|
||||
thread = ProcessFilesThread(snapshot_path, os.path.dirname(self._project_path), skip_dirs=["snapshots"])
|
||||
progress_dialog = ProgressDialog(thread, "Restoring snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
worker = ProcessFilesWorker(snapshot_path, os.path.dirname(self._project_path), skip_dirs=["snapshots"])
|
||||
progress_dialog = ProgressDialog(worker, "Restoring snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
thread.deleteLater()
|
||||
|
||||
from ..main_window import MainWindow
|
||||
MainWindow.instance().loadSnapshot(self._project_path)
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
Style editor to edit Shape items.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
|
||||
|
||||
|
||||
class StyleEditorDialog(QtGui.QDialog, Ui_StyleEditorDialog):
|
||||
class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
"""
|
||||
Style editor dialog.
|
||||
@@ -34,13 +34,13 @@ class StyleEditorDialog(QtGui.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
def __init__(self, parent, items):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._items = items
|
||||
self.uiColorPushButton.clicked.connect(self._setColorSlot)
|
||||
self.uiBorderColorPushButton.clicked.connect(self._setBorderColorSlot)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
|
||||
self.uiBorderStyleComboBox.addItem("Solid", QtCore.Qt.SolidLine)
|
||||
self.uiBorderStyleComboBox.addItem("Dash", QtCore.Qt.DashLine)
|
||||
@@ -74,7 +74,7 @@ class StyleEditorDialog(QtGui.QDialog, Ui_StyleEditorDialog):
|
||||
Slot to select the filling color.
|
||||
"""
|
||||
|
||||
color = QtGui.QColorDialog.getColor(self._color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel)
|
||||
color = QtWidgets.QColorDialog.getColor(self._color, self, "Select Color", QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self._color = color
|
||||
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
|
||||
@@ -87,7 +87,7 @@ class StyleEditorDialog(QtGui.QDialog, Ui_StyleEditorDialog):
|
||||
Slot to select the border color.
|
||||
"""
|
||||
|
||||
color = QtGui.QColorDialog.getColor(self._border_color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel)
|
||||
color = QtWidgets.QColorDialog.getColor(self._border_color, self, "Select Color", QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self._border_color = color
|
||||
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
|
||||
@@ -118,4 +118,4 @@ class StyleEditorDialog(QtGui.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
if result:
|
||||
self._applyPreferencesSlot()
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
@@ -16,15 +16,21 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Dialog to change the topology symbol of NodeItems
|
||||
Dialog to change node symbols.
|
||||
"""
|
||||
|
||||
from ..qt import QtSvg, QtCore, QtGui
|
||||
import os
|
||||
|
||||
from ..qt import QtSvg, QtCore, QtGui, QtWidgets
|
||||
from ..items.pixmap_node_item import PixmapNodeItem
|
||||
from ..ui.symbol_selection_dialog_ui import Ui_SymbolSelectionDialog
|
||||
from ..node import Node
|
||||
from ..servers import Servers
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
|
||||
class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
|
||||
"""
|
||||
Symbol selection dialog.
|
||||
@@ -33,90 +39,166 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
|
||||
:param items: list of items
|
||||
"""
|
||||
|
||||
def __init__(self, parent, items=None, symbol=None, category=None):
|
||||
def __init__(self, parent, items=None, symbol=None):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._items = items
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
self.uiCustomSymbolRadioButton.toggled.connect(self._customSymbolToggledSlot)
|
||||
self.uiBuiltInSymbolRadioButton.toggled.connect(self._builtInSymbolToggledSlot)
|
||||
self.uiSearchLineEdit.textChanged.connect(self._searchTextChangedSlot)
|
||||
self.uiBuiltinSymbolOnlyCheckBox.toggled.connect(self._builtinSymbolOnlyToggledSlot)
|
||||
self._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
|
||||
self._symbols_path = Servers.instance().localServerSettings()["symbols_path"]
|
||||
|
||||
selected_symbol = symbol
|
||||
selected_category = category
|
||||
if not self._items:
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).hide()
|
||||
|
||||
# current categories
|
||||
categories = {"Routers": Node.routers,
|
||||
"Switches": Node.switches,
|
||||
"End devices": Node.end_devices,
|
||||
"Security devices": Node.security_devices
|
||||
}
|
||||
|
||||
index = 0
|
||||
for name, category in categories.items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
if category == selected_category:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
index += 1
|
||||
else:
|
||||
self.uiCategoryLabel.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
custom_symbol = items[0].defaultRenderer().objectName()
|
||||
if not custom_symbol:
|
||||
symbol_name = items[0].node().defaultSymbol()
|
||||
else:
|
||||
symbol_name = custom_symbol
|
||||
selected_symbol = symbol_name
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).hide()
|
||||
|
||||
self.uiBuiltInSymbolRadioButton.setChecked(True)
|
||||
self.uiSymbolListWidget.setFocus()
|
||||
self.uiSymbolListWidget.setIconSize(QtCore.QSize(64, 64))
|
||||
symbol_resources = QtCore.QResource(":/symbols")
|
||||
for symbol in symbol_resources.children():
|
||||
if symbol.endswith(".normal.svg"):
|
||||
name = symbol[:-11]
|
||||
item = QtGui.QListWidgetItem(self.uiSymbolListWidget)
|
||||
self._symbol_items = []
|
||||
symbols = symbol_resources.children()
|
||||
|
||||
try:
|
||||
for file in os.listdir(self._symbols_path):
|
||||
symbols.append(file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
symbols.sort()
|
||||
for symbol in symbols:
|
||||
if symbol.endswith(".svg") or symbol.endswith(".png"):
|
||||
name = os.path.splitext(symbol)[0]
|
||||
item = QtWidgets.QListWidgetItem(self.uiSymbolListWidget)
|
||||
self._symbol_items.append(item)
|
||||
item.setText(name)
|
||||
resource_path = ":/symbols/" + symbol
|
||||
svg_renderer = QtSvg.QSvgRenderer(resource_path)
|
||||
if resource_path == selected_symbol:
|
||||
self.uiSymbolListWidget.setCurrentItem(item)
|
||||
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
svg_renderer.render(QtGui.QPainter(image))
|
||||
|
||||
if os.path.exists(os.path.join(self._symbols_path, symbol)):
|
||||
svg_renderer = QtSvg.QSvgRenderer(os.path.join(self._symbols_path, symbol))
|
||||
if svg_renderer.isValid():
|
||||
svg_renderer.render(QtGui.QPainter(image))
|
||||
else:
|
||||
image.load(os.path.join(self._symbols_path, symbol))
|
||||
else:
|
||||
resource_path = ":/symbols/" + symbol
|
||||
svg_renderer = QtSvg.QSvgRenderer(resource_path)
|
||||
svg_renderer.render(QtGui.QPainter(image))
|
||||
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
|
||||
item.setIcon(icon)
|
||||
|
||||
self.adjustSize()
|
||||
|
||||
def _builtinSymbolOnlyToggledSlot(self, checked):
|
||||
self._filter()
|
||||
|
||||
def _searchTextChangedSlot(self, text):
|
||||
self._filter()
|
||||
|
||||
def _filter(self):
|
||||
"""
|
||||
Hide element not matching the search
|
||||
"""
|
||||
text = self.uiSearchLineEdit.text()
|
||||
for item in self._symbol_items:
|
||||
if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not QtCore.QResource(":/symbols/{}.svg".format(item.text())).isValid():
|
||||
item.setHidden(True)
|
||||
else:
|
||||
if len(text.strip()) == 0 or text.strip().lower() in item.text().lower():
|
||||
item.setHidden(False)
|
||||
else:
|
||||
item.setHidden(True)
|
||||
|
||||
def _customSymbolToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the custom symbol radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiCustomSymbolGroupBox.setEnabled(True)
|
||||
self.uiCustomSymbolGroupBox.show()
|
||||
self.uiBuiltInGroupBox.setEnabled(False)
|
||||
self.uiBuiltInGroupBox.hide()
|
||||
self.adjustSize()
|
||||
|
||||
def _builtInSymbolToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the built-in symbol radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiCustomSymbolGroupBox.setEnabled(False)
|
||||
self.uiCustomSymbolGroupBox.hide()
|
||||
self.uiBuiltInGroupBox.setEnabled(True)
|
||||
self.uiBuiltInGroupBox.show()
|
||||
self.adjustSize()
|
||||
|
||||
def _applyPreferencesSlot(self):
|
||||
"""
|
||||
Applies the selected symbol to the items.
|
||||
"""
|
||||
|
||||
current = self.uiSymbolListWidget.currentItem()
|
||||
if current:
|
||||
name = current.text()
|
||||
path = ":/symbols/{}.normal.svg".format(name)
|
||||
default_renderer = QtSvg.QSvgRenderer(path)
|
||||
default_renderer.setObjectName(path)
|
||||
path = ":/symbols/{}.selected.svg".format(name)
|
||||
hover_renderer = QtSvg.QSvgRenderer(path)
|
||||
hover_renderer.setObjectName(path)
|
||||
symbol_path = self.getSymbol()
|
||||
|
||||
pixmap = QtGui.QPixmap(symbol_path)
|
||||
if not pixmap.isNull():
|
||||
for item in self._items:
|
||||
item.setDefaultRenderer(default_renderer)
|
||||
item.setHoverRenderer(hover_renderer)
|
||||
if isinstance(item, PixmapNodeItem):
|
||||
item.setPixmap(pixmap)
|
||||
item.setPixmapSymbolPath(symbol_path)
|
||||
else:
|
||||
renderer = QtSvg.QSvgRenderer(symbol_path)
|
||||
renderer.setObjectName(symbol_path)
|
||||
if renderer.isValid():
|
||||
item.setSharedRenderer(renderer)
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Custom pixmap symbol", "Custom pixmap symbol which is not SVG format cannot be applied on SVG node item")
|
||||
return False
|
||||
|
||||
def getSymbols(self):
|
||||
return True
|
||||
|
||||
current = self.uiSymbolListWidget.currentItem()
|
||||
if current:
|
||||
name = current.text()
|
||||
normal_symbol = ":/symbols/{}.normal.svg".format(name)
|
||||
selected_symbol = ":/symbols/{}.selected.svg".format(name)
|
||||
return normal_symbol, selected_symbol
|
||||
def getSymbol(self):
|
||||
|
||||
def getCategory(self):
|
||||
if self.uiSymbolListWidget.isEnabled():
|
||||
current = self.uiSymbolListWidget.currentItem()
|
||||
if current:
|
||||
name = current.text()
|
||||
if QtCore.QResource(":/symbols/{}.svg".format(name)).isValid():
|
||||
return ":/symbols/{}.svg".format(name)
|
||||
else:
|
||||
symbol_path = os.path.join(self._symbols_path, "{}.svg".format(name))
|
||||
if not os.path.exists(symbol_path):
|
||||
symbol_path = os.path.join(self._symbols_path, "{}.png".format(name))
|
||||
return symbol_path
|
||||
else:
|
||||
return self.uiSymbolLineEdit.text()
|
||||
return None
|
||||
|
||||
return self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
def _symbolBrowserSlot(self):
|
||||
|
||||
# supported image file formats
|
||||
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.pbm *.pgm *.png *.ppm *.xbm *.xpm);;All files (*.*)"
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Image", self._symbols_dir, file_formats)
|
||||
if not path:
|
||||
return
|
||||
|
||||
self._symbols_dir = os.path.dirname(path)
|
||||
self.uiSymbolLineEdit.clear()
|
||||
self.uiSymbolLineEdit.setText(path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(path))
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
@@ -125,6 +207,10 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
|
||||
if result and self._items:
|
||||
self._applyPreferencesSlot()
|
||||
QtGui.QDialog.done(self, result)
|
||||
if result:
|
||||
if not self.uiSymbolListWidget.isEnabled() and not os.path.exists(self.uiSymbolLineEdit.text()):
|
||||
QtWidgets.QMessageBox.critical(self, "Custom symbol", "Invalid path to custom symbol: {}".format(self.uiSymbolLineEdit.text()))
|
||||
result = 0
|
||||
elif result and self._items and not self._applyPreferencesSlot():
|
||||
result = 0
|
||||
super().done(result)
|
||||
|
||||
@@ -19,12 +19,11 @@
|
||||
Text editor to edit Note items.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from ..ui.text_editor_dialog_ui import Ui_TextEditorDialog
|
||||
|
||||
|
||||
class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
|
||||
class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
|
||||
"""
|
||||
Text editor dialog.
|
||||
|
||||
@@ -34,22 +33,20 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
|
||||
def __init__(self, parent, items):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._items = items
|
||||
self.uiFontPushButton.clicked.connect(self._setFontSlot)
|
||||
self.uiColorPushButton.clicked.connect(self._setColorSlot)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
|
||||
# use the first item in the list as the model
|
||||
first_item = items[0]
|
||||
self._color = first_item.defaultTextColor()
|
||||
self._setColor(first_item.defaultTextColor())
|
||||
self.uiRotationSpinBox.setValue(first_item.rotation())
|
||||
self.uiColorPushButton.setStyleSheet("background-color: {}".format(self._color.name()))
|
||||
self.uiPlainTextEdit.setPlainText(first_item.toPlainText())
|
||||
self.uiPlainTextEdit.setFont(first_item.font())
|
||||
self.uiPlainTextEdit.setStyleSheet("color : {}".format(self._color.name()))
|
||||
|
||||
if not first_item.editable():
|
||||
self.uiPlainTextEdit.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
|
||||
@@ -58,12 +55,23 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
self.uiApplyTextToAllItemsCheckBox.setChecked(True)
|
||||
self.uiApplyTextToAllItemsCheckBox.hide()
|
||||
|
||||
def _setColor(self, color):
|
||||
self._color = color
|
||||
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(color.red(),
|
||||
color.green(),
|
||||
color.blue(),
|
||||
color.alpha()))
|
||||
self.uiPlainTextEdit.setStyleSheet("color: rgba({}, {}, {}, {});".format(color.red(),
|
||||
color.green(),
|
||||
color.blue(),
|
||||
color.alpha()))
|
||||
|
||||
def _setFontSlot(self):
|
||||
"""
|
||||
Slot to select the font.
|
||||
"""
|
||||
|
||||
selected_font, ok = QtGui.QFontDialog.getFont(self.uiPlainTextEdit.font(), self)
|
||||
selected_font, ok = QtWidgets.QFontDialog.getFont(self.uiPlainTextEdit.font(), self)
|
||||
if ok:
|
||||
self.uiPlainTextEdit.setFont(selected_font)
|
||||
|
||||
@@ -72,11 +80,9 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
Slot to select the color.
|
||||
"""
|
||||
|
||||
color = QtGui.QColorDialog.getColor(self._color, self)
|
||||
color = QtWidgets.QColorDialog.getColor(self._color, self, None, QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self._color = color
|
||||
self.uiColorPushButton.setStyleSheet("background-color: {}".format(self._color.name()))
|
||||
self.uiPlainTextEdit.setStyleSheet("color : {}".format(self._color.name()))
|
||||
self._setColor(color)
|
||||
|
||||
def _applyPreferencesSlot(self):
|
||||
"""
|
||||
@@ -99,4 +105,4 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
|
||||
if result:
|
||||
self._applyPreferencesSlot()
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
178
gns3/dialogs/vm_with_images_wizard.py
Normal file
178
gns3/dialogs/vm_with_images_wizard.py
Normal file
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from .vm_wizard import VMWizard
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.servers import Servers
|
||||
|
||||
|
||||
class VMWithImagesWizard(VMWizard):
|
||||
"""
|
||||
Base class for VM wizard with image management (Qemu, IOU...)
|
||||
|
||||
:param devices: List of existing device for this type
|
||||
:param use_local_server: Value the use_local_server settings for this module
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, devices, use_local_server, parent):
|
||||
# The list of images combo box (Qemu support multiple images)
|
||||
self._images_combo_boxes = set()
|
||||
|
||||
# The list of radio button for existing image or new images
|
||||
self._radio_existing_images_buttons = set()
|
||||
|
||||
super().__init__(devices, use_local_server, parent)
|
||||
|
||||
def refreshImageStepsButtons(self):
|
||||
"""
|
||||
When changing the server type (remote or local)
|
||||
Refresh all the image selectors
|
||||
"""
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
radio_button.setChecked(radio_button.isChecked())
|
||||
|
||||
def _vmToggledSlot(self, checked):
|
||||
super()._vmToggledSlot(checked)
|
||||
if checked:
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
super()._remoteServerToggledSlot(checked)
|
||||
if checked:
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
def _localToggledSlot(self, checked):
|
||||
super()._localToggledSlot(checked)
|
||||
if checked:
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
def addImageSelector(self, radio_button, combo_box, line_edit, browser, image_selector, create_button=None, create_image_wizard=None, image_suffix=""):
|
||||
"""
|
||||
Add a remote image selector
|
||||
|
||||
:param radio_button: Radio button which toggle display of the listbox
|
||||
:param combo_box: The image choice combo box
|
||||
:param line_edit: The edit for the image
|
||||
:param browser: file upload browser button
|
||||
:param image_selector: function which display an image selector and return path
|
||||
:param create_button: Image create button None if you don't need one
|
||||
:param create_image_wizard: Wizard Class for creating a new image
|
||||
"""
|
||||
|
||||
combo_box.currentIndexChanged.connect(lambda index: self._imageListIndexChangedSlot(index, combo_box, line_edit))
|
||||
self._images_combo_boxes.add(combo_box)
|
||||
|
||||
browser.clicked.connect(lambda: self._imageBrowserSlot(line_edit, image_selector))
|
||||
|
||||
if create_button:
|
||||
assert create_image_wizard is not None
|
||||
create_button.clicked.connect(lambda: self._imageCreateSlot(line_edit, create_image_wizard, image_suffix))
|
||||
|
||||
self._existingImageToggledSlot(True, combo_box, line_edit, browser, create_button)
|
||||
radio_button.toggled.connect(lambda checked: self._existingImageToggledSlot(checked, combo_box, line_edit, browser, create_button))
|
||||
self._radio_existing_images_buttons.add(radio_button)
|
||||
|
||||
def _imageCreateSlot(self, line_edit, create_image_wizard, image_suffix):
|
||||
server = Servers.instance().getServerFromString(self.getSettings()["server"])
|
||||
|
||||
create_dialog = create_image_wizard(self, server, self.uiNameLineEdit.text() + image_suffix)
|
||||
if QtWidgets.QDialog.Accepted == create_dialog.exec_():
|
||||
line_edit.setText(create_dialog.uiLocationLineEdit.text())
|
||||
|
||||
def _imageBrowserSlot(self, line_edit, image_selector):
|
||||
"""
|
||||
Slot to open a file browser and select an image.
|
||||
"""
|
||||
|
||||
server = Servers.instance().getServerFromString(self.getSettings()["server"])
|
||||
path = image_selector(self, server)
|
||||
if not path:
|
||||
return
|
||||
line_edit.clear()
|
||||
line_edit.setText(path)
|
||||
|
||||
def _imageListIndexChangedSlot(self, index, combo_box, line_edit):
|
||||
"""
|
||||
User select a different image in the combo box
|
||||
"""
|
||||
item = combo_box.itemData(index)
|
||||
if item and item["path"]:
|
||||
line_edit.setText(item["path"])
|
||||
else:
|
||||
line_edit.setText("")
|
||||
|
||||
def _existingImageToggledSlot(self, checked, combo_box, line_edit, browser, create_button):
|
||||
"""
|
||||
User select the option of using an existing image
|
||||
"""
|
||||
|
||||
if create_button:
|
||||
create_button.hide()
|
||||
|
||||
if checked:
|
||||
combo_box.show()
|
||||
browser.hide()
|
||||
line_edit.hide()
|
||||
if combo_box.count() > 0:
|
||||
line_edit.setText(combo_box.itemData(combo_box.currentIndex())["path"])
|
||||
else:
|
||||
combo_box.hide()
|
||||
line_edit.setText("")
|
||||
line_edit.show()
|
||||
browser.show()
|
||||
if create_button:
|
||||
create_button.show()
|
||||
|
||||
def loadImagesList(self, endpoint):
|
||||
"""
|
||||
Fill the list box with available Images"
|
||||
|
||||
:param endpoint: server endpoint with the list of Images
|
||||
"""
|
||||
|
||||
self._server.get(endpoint, self._getImagesFromServerCallback)
|
||||
|
||||
def _getImagesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for loadImagesList.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Images", "Error while getting the VMs: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
if len(result) == 0:
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
if radio_button.isChecked():
|
||||
for button in radio_button.parent().findChildren(QtWidgets.QRadioButton):
|
||||
if button != radio_button:
|
||||
button.setChecked(True)
|
||||
button.hide()
|
||||
|
||||
for combo_box in self._images_combo_boxes:
|
||||
combo_box.clear()
|
||||
for vm in result:
|
||||
# Could be drop when 1.4 is out and user have upgraded
|
||||
if "path" not in vm:
|
||||
QtWidgets.QMessageBox.critical(self, "Images", "Please upgrade the gns3 server to a version >= 1.4.0b4")
|
||||
return
|
||||
combo_box.addItem(vm["path"], vm)
|
||||
158
gns3/dialogs/vm_wizard.py
Normal file
158
gns3/dialogs/vm_wizard.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.servers import Servers
|
||||
from gns3.gns3_vm import GNS3VM
|
||||
|
||||
|
||||
class VMWizard(QtWidgets.QWizard):
|
||||
"""
|
||||
Base class for VM wizard.
|
||||
|
||||
:param devices: List of existing device for this type
|
||||
:param use_local_server: Value the use_local_server settings for this module
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, devices, use_local_server, parent):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.setModal(True)
|
||||
|
||||
self._devices = devices
|
||||
self._use_local_server = use_local_server
|
||||
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
|
||||
|
||||
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
if hasattr(self, "uiLoadBalanceCheckBox"):
|
||||
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
|
||||
|
||||
# By default we use the local server
|
||||
self._server = Servers.instance().localServer()
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
self._localToggledSlot(True)
|
||||
|
||||
if Servers.instance().isNonLocalServerConfigured() is False:
|
||||
# skip the server page if we use the local server
|
||||
self.setStartId(1)
|
||||
|
||||
def _vmToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the VM radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the remote server radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(True)
|
||||
self.uiRemoteServersGroupBox.show()
|
||||
|
||||
def _localToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the local server radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
def setStartId(self, index):
|
||||
"""
|
||||
Which page should we use when starting the Wizard
|
||||
"""
|
||||
super().setStartId(index)
|
||||
# If we skip the initial page (choosing a server)
|
||||
# we check the settings
|
||||
if index != 0:
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
|
||||
def initializePage(self, page_id):
|
||||
|
||||
if self.page(page_id) == self.uiServerWizardPage:
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
for server in Servers.instance().remoteServers().values():
|
||||
self.uiRemoteServersComboBox.addItem(server.url(), server)
|
||||
if hasattr(self, "uiVMRadioButton") and not GNS3VM.instance().isRunning():
|
||||
self.uiVMRadioButton.setEnabled(False)
|
||||
if hasattr(self, "uiVMRadioButton") and GNS3VM.instance().isRunning():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
elif self._use_local_server and self.uiLocalRadioButton.isChecked():
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the server.
|
||||
"""
|
||||
|
||||
if hasattr(self, "uiNamePlatformWizardPage") and self.currentPage() == self.uiNamePlatformWizardPage:
|
||||
name = self.uiNameLineEdit.text()
|
||||
for device in self._devices.values():
|
||||
if device["name"] == name:
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "{} is already used, please choose another name".format(name))
|
||||
return False
|
||||
elif self.currentPage() == self.uiServerWizardPage:
|
||||
if self.uiRemoteRadioButton.isChecked():
|
||||
if not Servers.instance().remoteServers():
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote server registered in your preferences")
|
||||
return False
|
||||
self._server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
|
||||
gns3_vm_server = Servers.instance().vmServer()
|
||||
if gns3_vm_server is None:
|
||||
QtWidgets.QMessageBox.critical(self, "GNS3 VM", "The GNS3 VM is not running")
|
||||
return False
|
||||
self._server = gns3_vm_server
|
||||
else:
|
||||
self._server = Servers.instance().localServer()
|
||||
return True
|
||||
|
||||
def _loadBalanceToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the load balance checkbox is toggled.
|
||||
|
||||
:param checked: either the box is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersComboBox.setEnabled(False)
|
||||
else:
|
||||
self.uiRemoteServersComboBox.setEnabled(True)
|
||||
276
gns3/gns3_vm.py
Normal file
276
gns3/gns3_vm.py
Normal file
@@ -0,0 +1,276 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Manages the GNS3 VM.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
import codecs
|
||||
import shutil
|
||||
|
||||
from .qt import QtNetwork
|
||||
from collections import OrderedDict
|
||||
from .servers import Servers
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GNS3VM:
|
||||
|
||||
"""
|
||||
GNS3 VM management class.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._is_running = False
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns the GNS3 VM settings.
|
||||
|
||||
:returns: GNS3 VM settings (dict)
|
||||
"""
|
||||
|
||||
return Servers.instance().vmSettings()
|
||||
|
||||
def setSettings(self, settings):
|
||||
"""
|
||||
Set new GNS3 VM settings.
|
||||
|
||||
:param settings: GNS3 VM settings (dict)
|
||||
"""
|
||||
|
||||
Servers.instance().setVMsettings(settings)
|
||||
|
||||
@staticmethod
|
||||
def execute_vmrun(subcommand, args, timeout=60):
|
||||
|
||||
from gns3.modules.vmware import VMware
|
||||
vmware_settings = VMware.instance().settings()
|
||||
vmrun_path = vmware_settings["vmrun_path"]
|
||||
if sys.platform.startswith("darwin"):
|
||||
command = [vmrun_path, "-T", "fusion", subcommand]
|
||||
else:
|
||||
host_type = vmware_settings["host_type"]
|
||||
command = [vmrun_path, "-T", host_type, subcommand]
|
||||
command.extend(args)
|
||||
log.debug("Executing vmrun with command: {}".format(command))
|
||||
output = subprocess.check_output(command, timeout=timeout)
|
||||
return output.decode("utf-8", errors="ignore").strip()
|
||||
|
||||
@staticmethod
|
||||
def execute_vboxmanage(subcommand, args, timeout=60):
|
||||
|
||||
from gns3.modules.virtualbox import VirtualBox
|
||||
virtualbox_settings = VirtualBox.instance().settings()
|
||||
vboxmanage_path = virtualbox_settings["vboxmanage_path"]
|
||||
command = [vboxmanage_path, "--nologo", subcommand]
|
||||
command.extend(args)
|
||||
log.debug("Executing VBoxManage with command: {}".format(command))
|
||||
output = subprocess.check_output(command, timeout=timeout)
|
||||
return output.decode("utf-8", errors="ignore").strip()
|
||||
|
||||
@staticmethod
|
||||
def parse_vmx_file(path):
|
||||
"""
|
||||
Parses a VMX file.
|
||||
|
||||
:param path: path to the VMX file
|
||||
|
||||
:returns: dict
|
||||
"""
|
||||
|
||||
pairs = OrderedDict()
|
||||
encoding = "utf-8"
|
||||
# get the first line to read the .encoding value
|
||||
with open(path, "rb") as f:
|
||||
line = f.readline().decode(encoding, errors="ignore")
|
||||
if line.startswith("#!"):
|
||||
# skip the shebang
|
||||
line = f.readline().decode(encoding, errors="ignore")
|
||||
try:
|
||||
key, value = line.split('=', 1)
|
||||
if key.strip().lower() == ".encoding":
|
||||
file_encoding = value.strip('" ')
|
||||
try:
|
||||
codecs.lookup(file_encoding)
|
||||
encoding = file_encoding
|
||||
except LookupError:
|
||||
log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding))
|
||||
except ValueError:
|
||||
log.warning("Couldn't find file encoding in {}, using {}...".format(path, encoding))
|
||||
|
||||
# read the file with the correct encoding
|
||||
with open(path, encoding=encoding, errors="ignore") as f:
|
||||
for line in f.read().splitlines():
|
||||
try:
|
||||
key, value = line.split('=', 1)
|
||||
pairs[key.strip().lower()] = value.strip('" ')
|
||||
except ValueError:
|
||||
continue
|
||||
return pairs
|
||||
|
||||
@staticmethod
|
||||
def write_vmx_file(path, pairs):
|
||||
"""
|
||||
Write a VMware VMX file.
|
||||
|
||||
:param path: path to the VMX file
|
||||
:param pairs: settings to write
|
||||
"""
|
||||
|
||||
encoding = "utf-8"
|
||||
if ".encoding" in pairs:
|
||||
file_encoding = pairs[".encoding"]
|
||||
try:
|
||||
codecs.lookup(file_encoding)
|
||||
encoding = file_encoding
|
||||
except LookupError:
|
||||
log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding))
|
||||
with open(path, "w", encoding=encoding, errors="ignore") as f:
|
||||
if sys.platform.startswith("linux"):
|
||||
# write the shebang on the first line on Linux
|
||||
vmware_path = shutil.which("vmware")
|
||||
if vmware_path:
|
||||
f.write("#!{}\n".format(vmware_path))
|
||||
for key, value in pairs.items():
|
||||
entry = '{} = "{}"\n'.format(key, value)
|
||||
f.write(entry)
|
||||
|
||||
def autoStart(self):
|
||||
"""
|
||||
Automatically start the GNS3 VM at startup.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
vm_settings = Servers.instance().vmSettings()
|
||||
return vm_settings["auto_start"]
|
||||
|
||||
def adjustLocalServerIP(self):
|
||||
"""
|
||||
Adjust the local server IP address to be in the same subnet as the GNS3 VM.
|
||||
|
||||
:returns: the local server IP/host address
|
||||
"""
|
||||
|
||||
servers = Servers.instance()
|
||||
local_server_settings = servers.localServerSettings()
|
||||
if Servers.instance().vmSettings()["adjust_local_server_ip"]:
|
||||
vm_server = servers.vmServer()
|
||||
vm_ip_address = vm_server.host()
|
||||
log.debug("GNS3 VM IP address is {}".format(vm_ip_address))
|
||||
|
||||
for interface in QtNetwork.QNetworkInterface.allInterfaces():
|
||||
for address in interface.addressEntries():
|
||||
ip = address.ip().toString()
|
||||
prefix_length = address.prefixLength()
|
||||
subnet = QtNetwork.QHostAddress.parseSubnet("{}/{}".format(ip, prefix_length))
|
||||
if QtNetwork.QHostAddress(vm_ip_address).isInSubnet(subnet):
|
||||
if local_server_settings["host"] != ip:
|
||||
log.info("Adjust local server IP address to {}".format(ip))
|
||||
servers.setLocalServerSettings({"host": ip})
|
||||
servers.registerLocalServer()
|
||||
servers.save()
|
||||
return ip
|
||||
return local_server_settings["host"]
|
||||
|
||||
def setRunning(self, value):
|
||||
"""
|
||||
Sets either the GNS3 VM is running or not.
|
||||
|
||||
:param value: boolean
|
||||
"""
|
||||
|
||||
self._is_running = value
|
||||
|
||||
def isRunning(self):
|
||||
"""
|
||||
Returns either the GNS3 VM is running or not.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._is_running
|
||||
|
||||
def setvCPUandRAM(self, vcpus, ram):
|
||||
"""
|
||||
Set the vCPU cores and RAM amount for the GNS3 VM.
|
||||
|
||||
:param vcpus: number of vCPU cores
|
||||
:param ram: amount of memory
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
vm_settings = self.settings()
|
||||
if vm_settings["virtualization"] == "VMware":
|
||||
try:
|
||||
pairs = self.parse_vmx_file(vm_settings["vmx_path"])
|
||||
pairs["numvcpus"] = str(vcpus)
|
||||
pairs["memsize"] = str(ram)
|
||||
self.write_vmx_file(vm_settings["vmx_path"], pairs)
|
||||
except OSError as e:
|
||||
log.error('Could not read/write VMware VMX file "{}": {}'.format(vm_settings["vmx_path"], e))
|
||||
return False
|
||||
|
||||
elif vm_settings["virtualization"] == "VirtualBox":
|
||||
try:
|
||||
self.execute_vboxmanage("modifyvm", [vm_settings["vmname"], "--cpus", str(vcpus)], timeout=3)
|
||||
self.execute_vboxmanage("modifyvm", [vm_settings["vmname"], "--memory", str(ram)], timeout=3)
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.error("Could not execute VBoxManage: {}".format(e), True)
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
log.error("VBoxmanage timeout expired", True)
|
||||
return False
|
||||
log.info("GNS3 VM vCPU count set to {} and RAM to {} MB".format(vcpus, ram))
|
||||
return True
|
||||
|
||||
def shutdown(self, force=False):
|
||||
"""
|
||||
Gracefully shutdowns the GNS3 VM.
|
||||
"""
|
||||
|
||||
vm_settings = self.settings()
|
||||
if self._is_running and (vm_settings["auto_stop"] or force):
|
||||
try:
|
||||
if vm_settings["virtualization"] == "VMware":
|
||||
self.execute_vmrun("stop", [vm_settings["vmx_path"], "soft"])
|
||||
elif vm_settings["virtualization"] == "VirtualBox":
|
||||
self.execute_vboxmanage("controlvm", [vm_settings["vmname"], "acpipowerbutton"], timeout=3)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
except subprocess.TimeoutExpired:
|
||||
log.warning("Could not ACPI shutdown the VM (timeout expired)")
|
||||
self._is_running = False
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of GNS3VM
|
||||
|
||||
:returns: instance of GNS3VM
|
||||
"""
|
||||
|
||||
if not hasattr(GNS3VM, "_instance") or GNS3VM._instance is None:
|
||||
GNS3VM._instance = GNS3VM()
|
||||
return GNS3VM._instance
|
||||
@@ -22,18 +22,19 @@ Graphical view on the scene where items are drawn.
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import functools
|
||||
|
||||
from .qt import QtCore, QtGui, QtNetwork
|
||||
from .qt import QtCore, QtGui, QtSvg, QtNetwork, QtWidgets, qpartial
|
||||
from .servers import Servers
|
||||
from .items.node_item import NodeItem
|
||||
from .dialogs.node_configurator_dialog import NodeConfiguratorDialog
|
||||
from .items.svg_node_item import SvgNodeItem
|
||||
from .items.pixmap_node_item import PixmapNodeItem
|
||||
from .dialogs.node_properties_dialog import NodePropertiesDialog
|
||||
from .link import Link
|
||||
from .node import Node
|
||||
from .modules import MODULES
|
||||
from .modules.builtin.cloud import Cloud
|
||||
from .modules.module_error import ModuleError
|
||||
from .settings import GRAPHICS_VIEW_SETTINGS, GRAPHICS_VIEW_SETTING_TYPES
|
||||
from .settings import GRAPHICS_VIEW_SETTINGS
|
||||
from .topology import Topology
|
||||
from .ports.port import Port
|
||||
from .dialogs.style_editor_dialog import StyleEditorDialog
|
||||
@@ -41,6 +42,8 @@ from .dialogs.text_editor_dialog import TextEditorDialog
|
||||
from .dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from .dialogs.idlepc_dialog import IdlePCDialog
|
||||
from .local_config import LocalConfig
|
||||
from .progress import Progress
|
||||
from .utils.server_select import server_select
|
||||
|
||||
# link items
|
||||
from .items.link_item import LinkItem
|
||||
@@ -53,11 +56,13 @@ from .items.shape_item import ShapeItem
|
||||
from .items.rectangle_item import RectangleItem
|
||||
from .items.ellipse_item import EllipseItem
|
||||
from .items.image_item import ImageItem
|
||||
from .items.pixmap_image_item import PixmapImageItem
|
||||
from .items.svg_image_item import SvgImageItem
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GraphicsView(QtGui.QGraphicsView):
|
||||
class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
"""
|
||||
Graphics view that displays the scene.
|
||||
@@ -67,11 +72,10 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
# Our parent is the central widget which parent
|
||||
# is the main window.
|
||||
# Our parent is the central widget which parent is the main window.
|
||||
self._main_window = parent.parent()
|
||||
|
||||
QtGui.QGraphicsView.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self._settings = {}
|
||||
self._loadSettings()
|
||||
|
||||
@@ -83,20 +87,28 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
self._dragging = False
|
||||
self._last_mouse_position = None
|
||||
self._topology = Topology.instance()
|
||||
self._background_warning_msgbox = QtWidgets.QErrorMessage(self)
|
||||
self._background_warning_msgbox.setWindowTitle("Layer position")
|
||||
|
||||
# set the scene
|
||||
scene = QtGui.QGraphicsScene(parent=self)
|
||||
scene = QtWidgets.QGraphicsScene(parent=self)
|
||||
width = self._settings["scene_width"]
|
||||
height = self._settings["scene_height"]
|
||||
scene.setSceneRect(-(width / 2), -(height / 2), width, height)
|
||||
self.setScene(scene)
|
||||
|
||||
# set the custom flags for this view
|
||||
self.setDragMode(QtGui.QGraphicsView.RubberBandDrag)
|
||||
self.setCacheMode(QtGui.QGraphicsView.CacheBackground)
|
||||
self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
|
||||
self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground)
|
||||
self.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
|
||||
self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter)
|
||||
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
|
||||
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter)
|
||||
|
||||
# default directories for QFileDialog
|
||||
self._import_configs_from_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
|
||||
self._import_config_dir = ""
|
||||
self._export_configs_to_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
|
||||
self._export_config_dir = ""
|
||||
|
||||
self._local_addresses = ['0.0.0.0', '127.0.0.1', 'localhost', '::1', '0:0:0:0:0:0:0:1', '::', QtNetwork.QHostInfo.localHostName()]
|
||||
|
||||
@@ -131,21 +143,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
Loads the settings from the persistent settings file.
|
||||
"""
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
# restore the graphics view settings from QSettings (for backward compatibility)
|
||||
legacy_settings = {}
|
||||
settings = QtCore.QSettings()
|
||||
settings.beginGroup(self.__class__.__name__)
|
||||
for name in GRAPHICS_VIEW_SETTINGS.keys():
|
||||
if settings.contains(name):
|
||||
legacy_settings[name] = settings.value(name, type=GRAPHICS_VIEW_SETTING_TYPES[name])
|
||||
settings.remove("")
|
||||
settings.endGroup()
|
||||
|
||||
if legacy_settings:
|
||||
local_config.saveSectionSettings(self.__class__.__name__, legacy_settings)
|
||||
|
||||
self._settings = local_config.loadSectionSettings(self.__class__.__name__, GRAPHICS_VIEW_SETTINGS)
|
||||
self._settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, GRAPHICS_VIEW_SETTINGS)
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
@@ -178,9 +176,9 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
if enabled:
|
||||
self.setCursor(QtCore.Qt.CrossCursor)
|
||||
else:
|
||||
if self._newlink:
|
||||
if self._newlink and self._newlink in self.scene().items():
|
||||
self.scene().removeItem(self._newlink)
|
||||
self._newlink = None
|
||||
self._newlink = None
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_link = enabled
|
||||
|
||||
@@ -226,15 +224,19 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
self._adding_ellipse = False
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addImage(self, pixmap, image_path):
|
||||
def addImage(self, image, image_path):
|
||||
"""
|
||||
Adds an image.
|
||||
|
||||
:param pixmap: QPixmap instance
|
||||
:param image: QPixmap or QSvgRenderer instance
|
||||
:param image_path: path to the image
|
||||
"""
|
||||
|
||||
image_item = ImageItem(pixmap, image_path)
|
||||
if isinstance(image, QtSvg.QSvgRenderer):
|
||||
# use a SVG image item if this is a valid SVG file
|
||||
image_item = SvgImageItem(image, image_path)
|
||||
else:
|
||||
image_item = PixmapImageItem(image, image_path)
|
||||
# center the image on the scene
|
||||
x = image_item.pos().x() - (image_item.boundingRect().width() / 2)
|
||||
y = image_item.pos().y() - (image_item.boundingRect().height() / 2)
|
||||
@@ -250,15 +252,17 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
:param source_port: source Port instance
|
||||
:param destination_node: destination Node instance
|
||||
:param destination_port: destination Port instance
|
||||
:returns: Link
|
||||
"""
|
||||
|
||||
link = Link(source_node, source_port, destination_node, destination_port)
|
||||
|
||||
# connect the signals that let the graphics view knows about events such as
|
||||
# a new link creation or deletion.
|
||||
link.add_link_signal.connect(self.addLinkSlot)
|
||||
link.delete_link_signal.connect(self.deleteLinkSlot)
|
||||
self._topology.addLink(link)
|
||||
if self._topology.addLink(link):
|
||||
link.add_link_signal.connect(self.addLinkSlot)
|
||||
link.delete_link_signal.connect(self.deleteLinkSlot)
|
||||
return link
|
||||
|
||||
def addLinkSlot(self, link_id):
|
||||
"""
|
||||
@@ -355,10 +359,13 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
if not source_port:
|
||||
return
|
||||
if not source_item.node().initialized():
|
||||
QtGui.QMessageBox.critical(self, "Connection", "This node hasn't been initialized correctly")
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "This node hasn't been initialized correctly")
|
||||
return
|
||||
if not source_port.isFree():
|
||||
QtGui.QMessageBox.critical(self, "Connection", "Port {} isn't free".format(source_port.name()))
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Port {} isn't free".format(source_port.name()))
|
||||
return
|
||||
if not source_port.isHotPluggable() and source_item.node().status() == Node.started:
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "A new link cannot be added because {} is running".format(source_item.node().name()))
|
||||
return
|
||||
if source_port.linkType() == "Serial":
|
||||
self._newlink = SerialLinkItem(source_item, source_port, self.mapToScene(event.pos()), None, adding_flag=True)
|
||||
@@ -370,44 +377,56 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
source_port = self._newlink.sourcePort()
|
||||
destination_item = item
|
||||
if source_item == destination_item:
|
||||
QtGui.QMessageBox.critical(self, "Connection", "Cannot connect to itself!")
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Cannot connect to itself!")
|
||||
return
|
||||
destination_port = destination_item.connectToPort()
|
||||
if not destination_port:
|
||||
return
|
||||
if not destination_item.node().initialized():
|
||||
QtGui.QMessageBox.critical(self, "Connection", "This node hasn't been initialized correctly")
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "This node hasn't been initialized correctly")
|
||||
return
|
||||
if not destination_port.isFree():
|
||||
QtGui.QMessageBox.critical(self, "Connection", "Port {} isn't free".format(destination_port.name()))
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Port {} isn't free".format(destination_port.name()))
|
||||
return
|
||||
if not destination_port.isHotPluggable() and destination_item.node().status() == Node.started:
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "A new link cannot be added because {} is running".format(destination_item.node().name()))
|
||||
return
|
||||
if source_port.isStub() or destination_port.isStub():
|
||||
pass
|
||||
elif source_port.linkType() != destination_port.linkType():
|
||||
QtGui.QMessageBox.critical(self, "Connection", "Cannot connect this port!")
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Cannot connect this port!")
|
||||
return
|
||||
elif source_port.defaultNio() != destination_port.defaultNio():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "These nodes cannot be connected together ({} != {})".format(source_port.defaultNio().__name__,
|
||||
destination_port.defaultNio().__name__))
|
||||
return
|
||||
|
||||
if source_item.node().server().protocol() != destination_item.node().server().protocol():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Sorry, you cannot connect a device running on an insecure server to a device running on a secure server.")
|
||||
return
|
||||
|
||||
if isinstance(source_item.node(), Cloud) and isinstance(destination_item.node(), Cloud):
|
||||
QtGui.QMessageBox.critical(self, "Connection", "Sorry, you cannot connect a cloud to another cloud!")
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Sorry, you cannot connect a cloud to another cloud!")
|
||||
return
|
||||
|
||||
source_host = source_item.node().server().host
|
||||
destination_host = destination_item.node().server().host
|
||||
source_host = source_item.node().server().host()
|
||||
destination_host = destination_item.node().server().host()
|
||||
|
||||
# check that the node can be connected to a cloud
|
||||
if (isinstance(source_item.node(), Cloud) or isinstance(destination_item.node(), Cloud)) and source_host != destination_host:
|
||||
QtGui.QMessageBox.critical(self, "Connection", "This device can only be connected to a cloud on the same host")
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "This device can only be connected to a cloud on the same host")
|
||||
return
|
||||
|
||||
# check if the 2 nodes can communicate
|
||||
if (source_host in self._local_addresses and destination_host not in self._local_addresses) or \
|
||||
(destination_host in self._local_addresses and source_host not in self._local_addresses):
|
||||
QtGui.QMessageBox.critical(self, "Connection", "Server {} cannot communicate with server {}, most likely because your local server host binding is set to a local address".format(source_host, destination_host))
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Server {} cannot communicate with server {}, most likely because your local server host binding is set to a local address".format(source_host, destination_host))
|
||||
return
|
||||
|
||||
self.scene().removeItem(self._newlink)
|
||||
self.addLink(source_item.node(), source_port, destination_item.node(), destination_port)
|
||||
if self._newlink in self.scene().items():
|
||||
self.scene().removeItem(self._newlink)
|
||||
self._newlink = None
|
||||
self.addLink(source_item.node(), source_port, destination_item.node(), destination_port)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""
|
||||
@@ -458,6 +477,10 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
# when more than one item is selected display the contextual menu even if mouse is not above an item
|
||||
elif len(self.scene().selectedItems()) > 1:
|
||||
self._showDeviceContextualMenu(QtGui.QCursor.pos())
|
||||
elif is_not_link and self._adding_link and event.button() == QtCore.Qt.RightButton:
|
||||
# send a escape key to the main window to cancel the link addition
|
||||
key = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Escape, QtCore.Qt.NoModifier)
|
||||
QtWidgets.QApplication.sendEvent(self._main_window, key)
|
||||
elif item and isinstance(item, NodeItem) and self._adding_link and event.button() == QtCore.Qt.LeftButton:
|
||||
self._userNodeLinking(event, item)
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_note:
|
||||
@@ -487,7 +510,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_ellipse = False
|
||||
else:
|
||||
QtGui.QGraphicsView.mousePressEvent(self, event)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""
|
||||
@@ -505,7 +528,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
else:
|
||||
if item is not None and not event.modifiers() & QtCore.Qt.ControlModifier:
|
||||
item.setSelected(True)
|
||||
QtGui.QGraphicsView.mouseReleaseEvent(self, event)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
"""
|
||||
@@ -514,19 +537,22 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
:param: QWheelEvent instance
|
||||
"""
|
||||
|
||||
if event.modifiers() == QtCore.Qt.ControlModifier and event.orientation() == QtCore.Qt.Vertical:
|
||||
# CTRL is pressed then use the mouse wheel to zoom in or out.
|
||||
self.scaleView(pow(2.0, event.delta() / 240.0))
|
||||
if event.modifiers() == QtCore.Qt.ControlModifier:
|
||||
# event.delta() added for Qt4 compatibility
|
||||
delta = event.angleDelta() if hasattr(event, 'angleDelta') else event.delta()
|
||||
if delta is not None and delta.x() == 0:
|
||||
# CTRL is pressed then use the mouse wheel to zoom in or out.
|
||||
self.scaleView(pow(2.0, delta.y() / 240.0))
|
||||
else:
|
||||
QtGui.QGraphicsView.wheelEvent(self, event)
|
||||
super().wheelEvent(event)
|
||||
|
||||
def scaleView(self, scale_factor):
|
||||
"""
|
||||
Scales the view (zoom in and out).
|
||||
"""
|
||||
|
||||
factor = self.matrix().scale(scale_factor, scale_factor).mapRect(QtCore.QRectF(0, 0, 1, 1)).width()
|
||||
if (factor < 0.10 or factor > 10):
|
||||
factor = self.transform().scale(scale_factor, scale_factor).mapRect(QtCore.QRectF(0, 0, 1, 1)).width()
|
||||
if factor < 0.10 or factor > 10:
|
||||
return
|
||||
self.scale(scale_factor, scale_factor)
|
||||
|
||||
@@ -541,12 +567,10 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
# check if we are editing an NoteItem instance, then send the delete key event to it
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NoteItem) and item.hasFocus():
|
||||
QtGui.QGraphicsView.keyPressEvent(self, event)
|
||||
super().keyPressEvent(event)
|
||||
return
|
||||
self.deleteActionSlot()
|
||||
QtGui.QGraphicsView.keyPressEvent(self,event)
|
||||
else:
|
||||
QtGui.QGraphicsView.keyPressEvent(self, event)
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""
|
||||
@@ -563,10 +587,10 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
hBar = self.horizontalScrollBar()
|
||||
vBar = self.verticalScrollBar()
|
||||
delta = mapped_global_pos - self._last_mouse_position
|
||||
hBar.setValue(hBar.value() + (delta.x() if QtGui.QApplication.isRightToLeft() else -delta.x()))
|
||||
hBar.setValue(hBar.value() + (delta.x() if QtWidgets.QApplication.isRightToLeft() else -delta.x()))
|
||||
vBar.setValue(vBar.value() - delta.y())
|
||||
self._last_mouse_position = mapped_global_pos
|
||||
if self._adding_link and self._newlink:
|
||||
if self._adding_link and self._newlink and self._newlink in self.scene().items():
|
||||
# update the mouse position when the user is adding a link.
|
||||
self._newlink.setMousePoint(self.mapToScene(event.pos()))
|
||||
event.ignore()
|
||||
@@ -576,7 +600,12 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
# show item coords in the status bar
|
||||
coords = "X: {} Y: {} Z: {}".format(item.x(), item.y(), item.zValue())
|
||||
self._main_window.uiStatusBar.showMessage(coords, 2000)
|
||||
QtGui.QGraphicsView.mouseMoveEvent(self, event)
|
||||
|
||||
# force the children to redraw because of a problem with QGraphicsEffect
|
||||
for item in self.scene().selectedItems():
|
||||
for child in item.childItems():
|
||||
child.update()
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
"""
|
||||
@@ -586,28 +615,38 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
"""
|
||||
|
||||
item = self.itemAt(event.pos())
|
||||
if not self._adding_link and isinstance(item, NodeItem) and item.node().initialized():
|
||||
item.setSelected(True)
|
||||
if isinstance(item, NodeItem):
|
||||
self.consoleToNode(item.node())
|
||||
else:
|
||||
self.configureSlot()
|
||||
else:
|
||||
QtGui.QGraphicsView.mouseDoubleClickEvent(self, event)
|
||||
|
||||
if not self._adding_link:
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
item.setSelected(True)
|
||||
if item.node().status() == Node.stopped:
|
||||
self.configureSlot()
|
||||
return
|
||||
else:
|
||||
self.consoleFromItems(self.scene().selectedItems())
|
||||
return
|
||||
elif isinstance(item, NoteItem) and isinstance(item.parentItem(), NodeItem):
|
||||
if item.parentItem().node().initialized():
|
||||
item.parentItem().setSelected(True)
|
||||
self.changeHostnameActionSlot()
|
||||
return
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
def configureSlot(self, items=None):
|
||||
"""
|
||||
Opens the node configurator.
|
||||
Opens the node properties dialog.
|
||||
"""
|
||||
|
||||
if not items:
|
||||
items = self.scene().selectedItems()
|
||||
node_configurator = NodeConfiguratorDialog(items, self._main_window)
|
||||
node_configurator.setModal(True)
|
||||
node_configurator.show()
|
||||
node_configurator.exec_()
|
||||
for item in items:
|
||||
item.setSelected(False)
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
items.append(item)
|
||||
with Progress.instance().context(min_duration=0):
|
||||
node_properties = NodePropertiesDialog(items, self._main_window)
|
||||
node_properties.setModal(True)
|
||||
node_properties.show()
|
||||
node_properties.exec_()
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
"""
|
||||
@@ -640,7 +679,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
if event.keyboardModifiers() == QtCore.Qt.ShiftModifier:
|
||||
max_nodes_per_line = 10 # max number of nodes on a single line
|
||||
offset = 100 # spacing between elements
|
||||
integer, ok = QtGui.QInputDialog.getInteger(self, "Nodes", "Number of nodes:", 2, 1, 100, 1)
|
||||
integer, ok = QtWidgets.QInputDialog.getInt(self, "Nodes", "Number of nodes:", 2, 1, 100, 1)
|
||||
if ok:
|
||||
for node_number in range(integer):
|
||||
node_item = self.createNode(node_data, event.pos())
|
||||
@@ -653,12 +692,15 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
else:
|
||||
self.createNode(node_data, event.pos())
|
||||
elif event.mimeData().hasFormat("text/uri-list") and event.mimeData().hasUrls():
|
||||
# This should not arrive but we received bug report with it...
|
||||
if len(event.mimeData().urls()) == 0:
|
||||
return
|
||||
if len(event.mimeData().urls()) > 1:
|
||||
QtGui.QMessageBox.critical(self, "Project files", "Please drop only one file")
|
||||
QtWidgets.QMessageBox.critical(self, "Project files", "Please drop only one file")
|
||||
return
|
||||
path = event.mimeData().urls()[0].toLocalFile()
|
||||
if os.path.isfile(path) and self._main_window.checkForUnsavedChanges():
|
||||
self._main_window.loadProject(path)
|
||||
self._main_window.loadPath(path)
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
event.ignore()
|
||||
@@ -670,7 +712,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
:param pos: position where to display the menu
|
||||
"""
|
||||
|
||||
menu = QtGui.QMenu()
|
||||
menu = QtWidgets.QMenu()
|
||||
self.populateDeviceContextualMenu(menu)
|
||||
menu.exec_(pos)
|
||||
menu.clear()
|
||||
@@ -687,103 +729,116 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
return
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
|
||||
configure_action = QtGui.QAction("Configure", menu)
|
||||
configure_action = QtWidgets.QAction("Configure", menu)
|
||||
configure_action.setIcon(QtGui.QIcon(':/icons/configuration.svg'))
|
||||
configure_action.triggered.connect(self.configureActionSlot)
|
||||
menu.addAction(configure_action)
|
||||
|
||||
# Action: Change hostname
|
||||
change_hostname_action = QtGui.QAction("Change hostname", menu)
|
||||
change_hostname_action = QtWidgets.QAction("Change hostname", menu)
|
||||
change_hostname_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
|
||||
self.connect(change_hostname_action, QtCore.SIGNAL('triggered()'), self.changeHostnameActionSlot)
|
||||
change_hostname_action.triggered.connect(self.changeHostnameActionSlot)
|
||||
menu.addAction(change_hostname_action)
|
||||
|
||||
# Action: Change symbol
|
||||
change_symbol_action = QtGui.QAction("Change symbol", menu)
|
||||
change_symbol_action = QtWidgets.QAction("Change symbol", menu)
|
||||
change_symbol_action.setIcon(QtGui.QIcon(':/icons/node_conception.svg'))
|
||||
self.connect(change_symbol_action, QtCore.SIGNAL('triggered()'), self.changeSymbolActionSlot)
|
||||
change_symbol_action.triggered.connect(self.changeSymbolActionSlot)
|
||||
menu.addAction(change_symbol_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "vmDir"), items)):
|
||||
# Action: Show in file manager
|
||||
show_in_file_manager_action = QtWidgets.QAction("Show in file manager", menu)
|
||||
show_in_file_manager_action.setIcon(QtGui.QIcon(':/icons/open.svg'))
|
||||
show_in_file_manager_action.triggered.connect(self.showInFileManagerSlot)
|
||||
menu.addAction(show_in_file_manager_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "console"), items)):
|
||||
console_action = QtGui.QAction("Console", menu)
|
||||
console_action = QtWidgets.QAction("Console", menu)
|
||||
console_action.setIcon(QtGui.QIcon(':/icons/console.svg'))
|
||||
console_action.triggered.connect(self.consoleActionSlot)
|
||||
menu.addAction(console_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole"), items)):
|
||||
aux_console_action = QtGui.QAction("Auxiliary console", menu)
|
||||
aux_console_action = QtWidgets.QAction("Auxiliary console", menu)
|
||||
aux_console_action.setIcon(QtGui.QIcon(':/icons/aux-console.svg'))
|
||||
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
|
||||
menu.addAction(aux_console_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "importConfig"), items)):
|
||||
import_config_action = QtGui.QAction("Import config", menu)
|
||||
import_config_action = QtWidgets.QAction("Import config", menu)
|
||||
import_config_action.setIcon(QtGui.QIcon(':/icons/import_config.svg'))
|
||||
import_config_action.triggered.connect(self.importConfigActionSlot)
|
||||
menu.addAction(import_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "exportConfig"), items)):
|
||||
export_config_action = QtGui.QAction("Export config", menu)
|
||||
export_config_action = QtWidgets.QAction("Export config", menu)
|
||||
export_config_action.setIcon(QtGui.QIcon(':/icons/export_config.svg'))
|
||||
export_config_action.triggered.connect(self.exportConfigActionSlot)
|
||||
menu.addAction(export_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "saveConfig"), items)):
|
||||
save_config_action = QtGui.QAction("Save config", menu)
|
||||
save_config_action = QtWidgets.QAction("Save config", menu)
|
||||
save_config_action.setIcon(QtGui.QIcon(':/icons/save.svg'))
|
||||
save_config_action.triggered.connect(self.saveConfigActionSlot)
|
||||
menu.addAction(save_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "startPacketCapture"), items)):
|
||||
capture_action = QtGui.QAction("Capture", menu)
|
||||
capture_action = QtWidgets.QAction("Capture", menu)
|
||||
capture_action.setIcon(QtGui.QIcon(':/icons/inspect.svg'))
|
||||
capture_action.triggered.connect(self.captureActionSlot)
|
||||
menu.addAction(capture_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "idlepc"), items)):
|
||||
idlepc_action = QtGui.QAction("Idle-PC", menu)
|
||||
idlepc_action = QtWidgets.QAction("Idle-PC", menu)
|
||||
idlepc_action.setIcon(QtGui.QIcon(':/icons/calculate.svg'))
|
||||
idlepc_action.triggered.connect(self.idlepcActionSlot)
|
||||
menu.addAction(idlepc_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "idlepc"), items)):
|
||||
auto_idlepc_action = QtWidgets.QAction("Auto Idle-PC", menu)
|
||||
auto_idlepc_action.setIcon(QtGui.QIcon(':/icons/calculate.svg'))
|
||||
auto_idlepc_action.triggered.connect(self.autoIdlepcActionSlot)
|
||||
menu.addAction(auto_idlepc_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "start"), items)):
|
||||
start_action = QtGui.QAction("Start", menu)
|
||||
start_action = QtWidgets.QAction("Start", menu)
|
||||
start_action.setIcon(QtGui.QIcon(':/icons/start.svg'))
|
||||
start_action.triggered.connect(self.startActionSlot)
|
||||
menu.addAction(start_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "suspend"), items)):
|
||||
suspend_action = QtGui.QAction("Suspend", menu)
|
||||
suspend_action = QtWidgets.QAction("Suspend", menu)
|
||||
suspend_action.setIcon(QtGui.QIcon(':/icons/pause.svg'))
|
||||
suspend_action.triggered.connect(self.suspendActionSlot)
|
||||
menu.addAction(suspend_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "stop"), items)):
|
||||
stop_action = QtGui.QAction("Stop", menu)
|
||||
stop_action = QtWidgets.QAction("Stop", menu)
|
||||
stop_action.setIcon(QtGui.QIcon(':/icons/stop.svg'))
|
||||
stop_action.triggered.connect(self.stopActionSlot)
|
||||
menu.addAction(stop_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "reload"), items)):
|
||||
reload_action = QtGui.QAction("Reload", menu)
|
||||
reload_action = QtWidgets.QAction("Reload", menu)
|
||||
reload_action.setIcon(QtGui.QIcon(':/icons/reload.svg'))
|
||||
reload_action.triggered.connect(self.reloadActionSlot)
|
||||
menu.addAction(reload_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NoteItem) or isinstance(item, ShapeItem) or isinstance(item, ImageItem), items)):
|
||||
duplicate_action = QtGui.QAction("Duplicate", menu)
|
||||
duplicate_action = QtWidgets.QAction("Duplicate", menu)
|
||||
duplicate_action.setIcon(QtGui.QIcon(':/icons/new.svg'))
|
||||
duplicate_action.triggered.connect(self.duplicateActionSlot)
|
||||
menu.addAction(duplicate_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NoteItem), items)):
|
||||
text_edit_action = QtGui.QAction("Text edit", menu)
|
||||
text_edit_action = QtWidgets.QAction("Text edit", menu)
|
||||
text_edit_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg')) # TODO: change icon for text edit
|
||||
text_edit_action.triggered.connect(self.textEditActionSlot)
|
||||
menu.addAction(text_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, ShapeItem), items)):
|
||||
style_action = QtGui.QAction("Style", menu)
|
||||
style_action = QtWidgets.QAction("Style", menu)
|
||||
style_action.setIcon(QtGui.QIcon(':/icons/drawing.svg'))
|
||||
style_action.triggered.connect(self.styleActionSlot)
|
||||
menu.addAction(style_action)
|
||||
@@ -792,27 +847,27 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
if True in list(map(lambda item: item.parentItem() is None, items)):
|
||||
|
||||
if len(items) > 1:
|
||||
horizontal_align_action = QtGui.QAction("Align horizontally", menu)
|
||||
horizontal_align_action = QtWidgets.QAction("Align horizontally", menu)
|
||||
horizontal_align_action.setIcon(QtGui.QIcon(':/icons/horizontally.svg'))
|
||||
horizontal_align_action.triggered.connect(self.horizontalAlignmentSlot)
|
||||
menu.addAction(horizontal_align_action)
|
||||
|
||||
vertical_align_action = QtGui.QAction("Align vertically", menu)
|
||||
vertical_align_action = QtWidgets.QAction("Align vertically", menu)
|
||||
vertical_align_action.setIcon(QtGui.QIcon(':/icons/vertically.svg'))
|
||||
vertical_align_action.triggered.connect(self.verticalAlignmentSlot)
|
||||
menu.addAction(vertical_align_action)
|
||||
|
||||
raise_layer_action = QtGui.QAction("Raise one layer", menu)
|
||||
raise_layer_action = QtWidgets.QAction("Raise one layer", menu)
|
||||
raise_layer_action.setIcon(QtGui.QIcon(':/icons/raise_z_value.svg'))
|
||||
raise_layer_action.triggered.connect(self.raiseLayerActionSlot)
|
||||
menu.addAction(raise_layer_action)
|
||||
|
||||
lower_layer_action = QtGui.QAction("Lower one layer", menu)
|
||||
lower_layer_action = QtWidgets.QAction("Lower one layer", menu)
|
||||
lower_layer_action.setIcon(QtGui.QIcon(':/icons/lower_z_value.svg'))
|
||||
lower_layer_action.triggered.connect(self.lowerLayerActionSlot)
|
||||
menu.addAction(lower_layer_action)
|
||||
|
||||
delete_action = QtGui.QAction("Delete", menu)
|
||||
delete_action = QtWidgets.QAction("Delete", menu)
|
||||
delete_action.setIcon(QtGui.QIcon(':/icons/delete.svg'))
|
||||
delete_action.triggered.connect(self.deleteActionSlot)
|
||||
menu.addAction(delete_action)
|
||||
@@ -879,11 +934,11 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
new_hostname, ok = QtGui.QInputDialog.getText(self, "Change hostname", "Hostname:", QtGui.QLineEdit.Normal, item.node().name())
|
||||
new_hostname, ok = QtWidgets.QInputDialog.getText(self, "Change hostname", "Hostname:", QtWidgets.QLineEdit.Normal, item.node().name())
|
||||
if ok:
|
||||
if hasattr(item.node(), "validateHostname"):
|
||||
if not item.node().validateHostname(new_hostname):
|
||||
QtGui.QMessageBox.critical(self, "Change hostname", "Invalid name detected for this node: {}".format(new_hostname))
|
||||
QtWidgets.QMessageBox.critical(self, "Change hostname", "Invalid name detected for this node: {}".format(new_hostname))
|
||||
continue
|
||||
item.node().update({"name": new_hostname})
|
||||
|
||||
@@ -902,6 +957,29 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
def showInFileManagerSlot(self):
|
||||
"""
|
||||
Slot to receive events from the show in file manager action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "vmDir") and item.node().initialized():
|
||||
node = item.node()
|
||||
vm_dir = node.vmDir()
|
||||
if vm_dir is None:
|
||||
QtWidgets.QMessageBox.critical(self, "Show in file manager", "This VM has no working directory")
|
||||
break
|
||||
|
||||
if os.path.exists(vm_dir):
|
||||
log.debug("Open %s in file manage")
|
||||
if QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(vm_dir)) is False:
|
||||
QtWidgets.QMessageBox.critical(self, "Show in file manager", "Failed to open {}".format(vm_dir))
|
||||
break
|
||||
else:
|
||||
QtWidgets.QMessageBox.information(self, "Show in file manager", "The device directory is located in {} on {}".format(vm_dir, node.server().url()))
|
||||
break
|
||||
|
||||
def consoleToNode(self, node, aux=False):
|
||||
"""
|
||||
Start a console application to connect to a node.
|
||||
@@ -923,45 +1001,34 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
if hasattr(node, "serialConsole") and node.serialConsole():
|
||||
try:
|
||||
from .serial_console import serialConsole
|
||||
serialConsole(node.name())
|
||||
serialConsole(node.name(), node.serialPipe())
|
||||
except (OSError, ValueError) as e:
|
||||
QtGui.QMessageBox.critical(self, "Console", "Cannot start serial console application: {}".format(e))
|
||||
QtWidgets.QMessageBox.critical(self, "Console", "Cannot start serial console application: {}".format(e))
|
||||
return False
|
||||
else:
|
||||
name = node.name()
|
||||
if aux:
|
||||
console_port = node.auxConsole()
|
||||
if console_port is None:
|
||||
QtGui.QMessageBox.critical(self, "Console", "AUX console port not allocated for {}".format(name))
|
||||
QtWidgets.QMessageBox.critical(self, "Console", "AUX console port not allocated for {}".format(name))
|
||||
return False
|
||||
else:
|
||||
console_port = node.console()
|
||||
console_host = node.server().host
|
||||
|
||||
console_type = "telnet"
|
||||
if "console_type" in node.settings():
|
||||
console_type = node.settings()["console_type"]
|
||||
|
||||
try:
|
||||
from .telnet_console import telnetConsole
|
||||
from .telnet_console import nodeTelnetConsole
|
||||
from .vnc_console import vncConsole
|
||||
|
||||
telnet_callback = None
|
||||
|
||||
try:
|
||||
if node.server().isCloud():
|
||||
# override target host with localhost
|
||||
console_host = '127.0.0.1'
|
||||
ep = node.server().tunnel.add_endpoint(console_host, console_port)
|
||||
# override target port with local tunneled port
|
||||
local_addr, _ = ep.get()
|
||||
console_port = local_addr[1]
|
||||
|
||||
def cb(*args, **kwargs):
|
||||
node.server().tunnel.remove_endpoint(ep)
|
||||
log.debug('Console DONE on port {}'.format(args[2]))
|
||||
telnet_callback = cb
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
telnetConsole(name, console_host, console_port, telnet_callback)
|
||||
if console_type == "telnet":
|
||||
nodeTelnetConsole(name, node.server(), console_port)
|
||||
elif console_type == "vnc":
|
||||
vncConsole(node.server().host(), console_port)
|
||||
except (OSError, ValueError) as e:
|
||||
QtGui.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
|
||||
QtWidgets.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -982,7 +1049,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
counter = 0
|
||||
for name in sorted(nodes.keys()):
|
||||
node = nodes[name]
|
||||
callback = functools.partial(self.consoleToNode, node)
|
||||
callback = qpartial(self.consoleToNode, node)
|
||||
self._main_window.run_later(counter, callback)
|
||||
counter += delay
|
||||
|
||||
@@ -1011,7 +1078,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
counter = 0
|
||||
for name in sorted(nodes.keys()):
|
||||
node = nodes[name]
|
||||
callback = functools.partial(self.consoleToNode, node, aux=True)
|
||||
callback = qpartial(self.consoleToNode, node, aux=True)
|
||||
self._main_window.run_later(counter, callback)
|
||||
counter += delay
|
||||
|
||||
@@ -1038,25 +1105,31 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
return
|
||||
|
||||
if len(items) > 1:
|
||||
path = QtGui.QFileDialog.getExistingDirectory(self, "Import directory", ".", QtGui.QFileDialog.ShowDirsOnly)
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self, "Import directory", self._import_configs_from_dir, QtWidgets.QFileDialog.ShowDirsOnly)
|
||||
if path:
|
||||
self._import_configs_from_dir = os.path.dirname(path)
|
||||
for item in items:
|
||||
item.node().importConfigFromDirectory(path)
|
||||
else:
|
||||
if not self._import_config_dir:
|
||||
self._import_config_dir = self._main_window.project().filesDir()
|
||||
|
||||
item = items[0]
|
||||
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(self,
|
||||
"Import config",
|
||||
".",
|
||||
"All files (*.*);;Config files (*.cfg)",
|
||||
"Config files (*.cfg)")
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self,
|
||||
"Import config",
|
||||
self._import_config_dir,
|
||||
"All files (*.*);;Config files (*.cfg)",
|
||||
"Config files (*.cfg)")
|
||||
|
||||
if path:
|
||||
self._import_config_dir = os.path.dirname(path)
|
||||
item.node().importConfig(path)
|
||||
if hasattr(item.node(), "importPrivateConfig"):
|
||||
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(self,
|
||||
"Import private-config",
|
||||
".",
|
||||
"All files (*.*);;Config files (*.cfg)",
|
||||
"Config files (*.cfg)")
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self,
|
||||
"Import private-config",
|
||||
self._import_config_dir,
|
||||
"All files (*.*);;Config files (*.cfg)",
|
||||
"Config files (*.cfg)")
|
||||
if path:
|
||||
item.node().importPrivateConfig(path)
|
||||
|
||||
@@ -1075,17 +1148,24 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
return
|
||||
|
||||
if len(items) > 1:
|
||||
path = QtGui.QFileDialog.getExistingDirectory(self, "Export directory", ".", QtGui.QFileDialog.ShowDirsOnly)
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self, "Export directory", self._export_configs_to_dir, QtWidgets.QFileDialog.ShowDirsOnly)
|
||||
if path:
|
||||
self._export_configs_to_dir = os.path.dirname(path)
|
||||
for item in items:
|
||||
item.node().exportConfigToDirectory(path)
|
||||
else:
|
||||
if not self._export_config_dir:
|
||||
self._export_config_dir = self._main_window.project().filesDir()
|
||||
|
||||
item = items[0]
|
||||
config_path = QtGui.QFileDialog.getSaveFileName(self, "Export config")
|
||||
if hasattr(item.node(), "importPrivateConfig"):
|
||||
private_config_path = QtGui.QFileDialog.getSaveFileName(self, "Export private-config")
|
||||
config_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export startup-config", self._export_config_dir)
|
||||
self._export_config_dir = os.path.dirname(config_path)
|
||||
private_config_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export private-config", self._export_config_dir)
|
||||
item.node().exportConfig(config_path, private_config_path)
|
||||
else:
|
||||
config_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export config", self._export_config_dir)
|
||||
self._export_config_dir = os.path.dirname(config_path)
|
||||
item.node().exportConfig(config_path)
|
||||
|
||||
def saveConfigActionSlot(self):
|
||||
@@ -1114,13 +1194,16 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
key = "Port {} ({} encapsulation: {})".format(port.name(), dlt_name, dlt)
|
||||
ports[key] = [port, dlt]
|
||||
if ports:
|
||||
selection, ok = QtGui.QInputDialog.getItem(self, "Capture on {}".format(node.name()), "Please select a port:", list(ports.keys()), 0, False)
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(self, "Capture on {}".format(node.name()), "Please select a port:", list(ports.keys()), 0, False)
|
||||
if ok:
|
||||
if selection in ports:
|
||||
port, dlt = ports[selection]
|
||||
node.startPacketCapture(port, port.captureFileName(node.name()), dlt)
|
||||
try:
|
||||
node.startPacketCapture(port, port.captureFileName(node.name()), dlt)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Packet capture", "Cannot start Wireshark: {}".format(e))
|
||||
else:
|
||||
QtGui.QMessageBox.warning(self, "Capture", "No port available for packet capture on {}".format(node.name()))
|
||||
QtWidgets.QMessageBox.warning(self, "Capture", "No port available for packet capture on {}".format(node.name()))
|
||||
|
||||
def idlepcActionSlot(self):
|
||||
"""
|
||||
@@ -1130,16 +1213,11 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
|
||||
items = self.scene().selectedItems()
|
||||
if len(items) != 1:
|
||||
QtGui.QMessageBox.critical(self, "Idle-PC", "Please select only one router")
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC", "Please select only one router")
|
||||
return
|
||||
item = items[0]
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "idlepc") and item.node().initialized():
|
||||
router = item.node()
|
||||
#question = QtGui.QMessageBox.question(self, "Auto Idle-PC", "Would you like to automatically find a suitable Idle-PC value (but not optimal)?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
|
||||
|
||||
#if question == QtGui.QMessageBox.Yes:
|
||||
# router.computeAutoIdlepc(self._autoIdlepcCallback)
|
||||
#else:
|
||||
router.computeIdlepcs(self._idlepcCallback)
|
||||
|
||||
def _idlepcCallback(self, result, error=False, context={}, **kwargs):
|
||||
@@ -1148,7 +1226,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtGui.QMessageBox.critical(self, "Idle-PC", "Error: {}".format(result["message"]))
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC", "Error: {}".format(result["message"]))
|
||||
else:
|
||||
router = context["router"]
|
||||
log.info("{} has received Idle-PC proposals".format(router.name()))
|
||||
@@ -1158,7 +1236,22 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
else:
|
||||
QtGui.QMessageBox.critical(self, "Idle-PC", "Sorry no Idle-PC values could be computed, please check again with Cisco IOS in a different state")
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC", "Sorry no Idle-PC values could be computed, please check again with Cisco IOS in a different state")
|
||||
|
||||
def autoIdlepcActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the auto idlepc action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
items = self.scene().selectedItems()
|
||||
if len(items) != 1:
|
||||
QtWidgets.QMessageBox.critical(self, "Auto Idle-PC", "Please select only one router")
|
||||
return
|
||||
item = items[0]
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "idlepc") and item.node().initialized():
|
||||
router = item.node()
|
||||
router.computeAutoIdlepc(self._autoIdlepcCallback)
|
||||
|
||||
def _autoIdlepcCallback(self, result, error=False, context={}, **kwargs):
|
||||
"""
|
||||
@@ -1166,13 +1259,22 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtGui.QMessageBox.critical(self, "Auto Idle-PC", "Error: {}".format(result["message"]))
|
||||
QtWidgets.QMessageBox.critical(self, "Auto Idle-PC", "Error: {}".format(result["message"]))
|
||||
else:
|
||||
router = context["router"]
|
||||
idlepc = result["idlepc"]
|
||||
log.info("{} has received the auto idle-pc value: {}".format(router.name(), idlepc))
|
||||
router.setIdlepc(idlepc)
|
||||
QtGui.QMessageBox.information(self, "Auto Idle-PC", "Idle-PC value {} has been applied on {}".format(idlepc, router.name()))
|
||||
# apply Idle-PC to all routers with the same IOS image
|
||||
ios_image = os.path.basename(router.settings()["image"])
|
||||
for node in Topology.instance().nodes():
|
||||
if hasattr(node, "idlepc") and node.settings()["image"] == ios_image:
|
||||
node.setIdlepc(idlepc)
|
||||
# apply the idle-pc to templates with the same IOS image
|
||||
router.module().updateImageIdlepc(ios_image, idlepc)
|
||||
QtWidgets.QMessageBox.information(self, "Auto Idle-PC", "Idle-PC value {} has been applied on {} and all routers with IOS image {}".format(idlepc,
|
||||
router.name(),
|
||||
ios_image))
|
||||
|
||||
def duplicateActionSlot(self):
|
||||
"""
|
||||
@@ -1238,8 +1340,8 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
for item in self.scene().selectedItems():
|
||||
if item.parentItem() is None:
|
||||
if horizontal_pos is None:
|
||||
horizontal_pos = item.y()
|
||||
item.setPos(item.x(), horizontal_pos)
|
||||
horizontal_pos = item.y() + item.boundingRect().height() / 2
|
||||
item.setPos(item.x(), horizontal_pos - item.boundingRect().height() / 2)
|
||||
|
||||
def verticalAlignmentSlot(self):
|
||||
"""
|
||||
@@ -1251,8 +1353,8 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
for item in self.scene().selectedItems():
|
||||
if item.parentItem() is None:
|
||||
if vertical_position is None:
|
||||
vertical_position = item.x()
|
||||
item.setPos(vertical_position, item.y())
|
||||
vertical_position = item.x() + item.boundingRect().width() / 2
|
||||
item.setPos(vertical_position - item.boundingRect().width() / 2, item.y())
|
||||
|
||||
def raiseLayerActionSlot(self):
|
||||
"""
|
||||
@@ -1272,15 +1374,14 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
show_message = True
|
||||
for item in self.scene().selectedItems():
|
||||
if item.parentItem() is None:
|
||||
current_zvalue = item.zValue()
|
||||
item.setZValue(current_zvalue - 1)
|
||||
item.update()
|
||||
if item.zValue() == -1 and show_message:
|
||||
QtGui.QMessageBox.information(self, "Layer position", "Object moved to a background layer. You will now have to use the right-click action to select this object in the future and raise it to layer 0 to be able to move it")
|
||||
show_message = False
|
||||
|
||||
if item.zValue() == -1:
|
||||
self._background_warning_msgbox.showMessage("Object moved to a background layer. You will now have to use the right-click action to select this object in the future and raise it to layer 0 to be able to move it")
|
||||
|
||||
def deleteActionSlot(self):
|
||||
"""
|
||||
@@ -1297,9 +1398,9 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
question = "Do you want to permanently delete these {} nodes?".format(len(selected_nodes))
|
||||
else:
|
||||
question = "Do you want to permanently delete {}?".format(selected_nodes[0].name())
|
||||
reply = QtGui.QMessageBox.question(self, "Delete", question,
|
||||
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
|
||||
if reply == QtGui.QMessageBox.No:
|
||||
reply = QtWidgets.QMessageBox.question(self, "Delete", question,
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem):
|
||||
@@ -1308,6 +1409,21 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
elif item.parentItem() is None:
|
||||
item.delete()
|
||||
|
||||
@staticmethod
|
||||
def allocateServer():
|
||||
"""
|
||||
Allocates a server.
|
||||
|
||||
:returns: allocated server (HTTPClient instance)
|
||||
"""
|
||||
|
||||
from .main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
server = server_select(mainwindow)
|
||||
if server is None:
|
||||
raise ModuleError("Please select a server")
|
||||
return server
|
||||
|
||||
def createNode(self, node_data, pos):
|
||||
"""
|
||||
Creates a new node on the scene.
|
||||
@@ -1331,17 +1447,22 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
raise ModuleError("Could not find any module for {}".format(node_class))
|
||||
|
||||
if "server" not in node_data:
|
||||
server = node_module.allocateServer(node_class)
|
||||
server = self.allocateServer()
|
||||
elif node_data["server"] == "local":
|
||||
server = Servers.instance().localServer()
|
||||
elif node_data["server"] == "cloud":
|
||||
server = Servers.instance().anyCloudServer()
|
||||
elif node_data["server"] == "vm":
|
||||
server = Servers.instance().vmServer()
|
||||
if server is None:
|
||||
QtWidgets.QMessageBox.critical(self, "GNS3 VM", "The GNS3 VM is not running")
|
||||
return
|
||||
elif node_data["server"] == "load-balance":
|
||||
ram = node_data.get("ram", 0)
|
||||
server = Servers.instance().anyRemoteServer(ram)
|
||||
if server is None:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "Cannot load balance: no remote server configured")
|
||||
return
|
||||
else:
|
||||
try:
|
||||
host, port = node_data["server"].rsplit(":", 1)
|
||||
except ValueError:
|
||||
raise ModuleError("Wrong format for server: '{}', please recreate the node in preferences".format(node_data["server"]))
|
||||
server = Servers.instance().getRemoteServer(host, port)
|
||||
server = Servers.instance().getServerFromString(node_data["server"])
|
||||
|
||||
if server is None:
|
||||
return
|
||||
@@ -1350,10 +1471,13 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
node.error_signal.connect(self._main_window.uiConsoleTextEdit.writeError)
|
||||
node.warning_signal.connect(self._main_window.uiConsoleTextEdit.writeWarning)
|
||||
node.server_error_signal.connect(self._main_window.uiConsoleTextEdit.writeServerError)
|
||||
node_item = NodeItem(node, node_data["default_symbol"], node_data["hover_symbol"])
|
||||
if QtSvg.QSvgRenderer(node_data["symbol"]).isValid():
|
||||
node_item = SvgNodeItem(node, node_data["symbol"])
|
||||
else:
|
||||
node_item = PixmapNodeItem(node, node_data["symbol"])
|
||||
node_module.setupNode(node, node_data["name"])
|
||||
except ModuleError as e:
|
||||
QtGui.QMessageBox.critical(self, "Node creation", "{}".format(e))
|
||||
QtWidgets.QMessageBox.critical(self, "Node creation", "{}".format(e))
|
||||
return
|
||||
|
||||
node_item.setPos(self.mapToScene(pos))
|
||||
|
||||
@@ -18,24 +18,34 @@
|
||||
|
||||
import json
|
||||
import http
|
||||
import copy
|
||||
import ipaddress
|
||||
import uuid
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from functools import partial
|
||||
import pathlib
|
||||
import base64
|
||||
|
||||
from .version import __version__
|
||||
from .qt import QtCore, QtNetwork
|
||||
from .version import __version__, __version_info__
|
||||
from .qt import QtCore, QtNetwork, qpartial
|
||||
from .network_client import getNetworkUrl
|
||||
from .utils import parse_version
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HttpBadRequest(Exception):
|
||||
|
||||
"""We raise bad request exception for logging them in Sentry"""
|
||||
pass
|
||||
|
||||
|
||||
class HTTPClient(QtCore.QObject):
|
||||
|
||||
"""
|
||||
HTTP client.
|
||||
|
||||
:param url: URL to connect to the server
|
||||
:param settings: Dictionnary with connection information to the server
|
||||
:param network_manager: A QT network manager
|
||||
"""
|
||||
|
||||
@@ -47,33 +57,134 @@ class HTTPClient(QtCore.QObject):
|
||||
connected_signal = QtCore.Signal()
|
||||
connection_error_signal = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, url, network_manager):
|
||||
def __init__(self, settings, network_manager):
|
||||
|
||||
super().__init__()
|
||||
self._url = url
|
||||
self._version = ""
|
||||
|
||||
url_settings = urllib.parse.urlparse(url)
|
||||
self.scheme = url_settings.scheme
|
||||
self.host = url_settings.netloc.split(":")[0]
|
||||
self.port = url_settings.port
|
||||
|
||||
self._scheme = settings.get("protocol", "http")
|
||||
self._host = settings["host"]
|
||||
self._http_host = settings["host"]
|
||||
self._port = int(settings["port"])
|
||||
self._http_port = int(settings["port"])
|
||||
self._user = settings.get("user", None)
|
||||
self._password = settings.get("password", None)
|
||||
self._connected = False
|
||||
self._local = True
|
||||
self._cloud = False
|
||||
self._gns3_vm = False
|
||||
self._ram_limit = settings.get("ram_limit", 0)
|
||||
self._allocated_ram = 0
|
||||
self._accept_insecure_certificate = settings.get("accept_insecure_certificate", None)
|
||||
|
||||
self._network_manager = network_manager
|
||||
|
||||
# A buffer used by progress download
|
||||
self._buffer = {}
|
||||
|
||||
# create an unique ID
|
||||
self._id = HTTPClient._instance_count
|
||||
HTTPClient._instance_count += 1
|
||||
|
||||
def notify_progress_start_query(self, query_id):
|
||||
def getTunnel(self, port):
|
||||
"""
|
||||
Get a tunnel to the remote port.
|
||||
For HTTP standard client it's the same port. For SSH it will create a new tunnel.
|
||||
|
||||
:param port: Remote port
|
||||
:returns: Tuple host, port to connect
|
||||
"""
|
||||
return self._host, port
|
||||
|
||||
def releaseTunnel(self, port):
|
||||
"""
|
||||
Release a tunnel to the remote port.
|
||||
For HTTP standard client it's do nothing
|
||||
|
||||
:param port: Allocated remote port
|
||||
"""
|
||||
pass
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Return a dictionnary with server settings
|
||||
"""
|
||||
settings = {"protocol": self.protocol(),
|
||||
"ram_limit": self.RAMLimit(),
|
||||
"host": self.host(),
|
||||
"port": self.port(),
|
||||
"user": self.user(),
|
||||
"password": self._password}
|
||||
if self.protocol() == "https":
|
||||
settings["accept_insecure_certificate"] = self.acceptInsecureCertificate()
|
||||
return settings
|
||||
|
||||
def acceptInsecureCertificate(self, certificate=None):
|
||||
"""
|
||||
Does the server accept this insecure SSL certificate digest
|
||||
|
||||
:param: Certificate digest
|
||||
"""
|
||||
return self._accept_insecure_certificate
|
||||
|
||||
def setAcceptInsecureCertificate(self, certificate):
|
||||
"""
|
||||
Does the server accept this insecure SSL certificate digest
|
||||
|
||||
:param: Certificate digest
|
||||
"""
|
||||
self._accept_insecure_certificate = certificate
|
||||
|
||||
def host(self):
|
||||
"""
|
||||
Host display to user
|
||||
"""
|
||||
return self._host
|
||||
|
||||
def setHost(self, host):
|
||||
self._host = host
|
||||
self._http_host = host
|
||||
|
||||
def port(self):
|
||||
"""
|
||||
Port display to user
|
||||
"""
|
||||
return self._port
|
||||
|
||||
def setPort(self, port):
|
||||
self._port = port
|
||||
self._http_port = port
|
||||
|
||||
def protocol(self):
|
||||
"""
|
||||
Transport protocol
|
||||
"""
|
||||
return self._scheme
|
||||
|
||||
def user(self):
|
||||
"""
|
||||
User login display to GNS3 user
|
||||
"""
|
||||
return self._user
|
||||
|
||||
def setUser(self, user):
|
||||
self._user = user
|
||||
|
||||
def setPassword(self, password):
|
||||
self._password = password
|
||||
|
||||
def notify_progress_start_query(self, query_id, progress_text, response):
|
||||
"""
|
||||
Called when a query start
|
||||
"""
|
||||
if HTTPClient._progress_callback:
|
||||
HTTPClient._progress_callback.add_query_signal.emit(query_id, "Waiting for {scheme}://{host}:{port}".format(scheme=self.scheme, host=self.host, port=self.port))
|
||||
if progress_text:
|
||||
HTTPClient._progress_callback.add_query_signal.emit(query_id, progress_text, response)
|
||||
else:
|
||||
if self._local:
|
||||
HTTPClient._progress_callback.add_query_signal.emit(query_id, "Waiting for local GNS3 server", response)
|
||||
else:
|
||||
HTTPClient._progress_callback.add_query_signal.emit(query_id, "Waiting for {}".format(self.url()), response)
|
||||
|
||||
def notify_progress_end_query(cls, query_id):
|
||||
"""
|
||||
@@ -83,6 +194,20 @@ class HTTPClient(QtCore.QObject):
|
||||
if HTTPClient._progress_callback:
|
||||
HTTPClient._progress_callback.remove_query_signal.emit(query_id)
|
||||
|
||||
def notify_progress_upload(self, query_id, sent, total):
|
||||
"""
|
||||
Called when a query upload progress
|
||||
"""
|
||||
if HTTPClient._progress_callback:
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
|
||||
|
||||
def notify_progress_download(self, query_id, sent, total):
|
||||
"""
|
||||
Called when a query download progress
|
||||
"""
|
||||
if HTTPClient._progress_callback:
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
|
||||
|
||||
@classmethod
|
||||
def setProgressCallback(cls, progress_callback):
|
||||
"""
|
||||
@@ -100,7 +225,7 @@ class HTTPClient(QtCore.QObject):
|
||||
def url(self):
|
||||
"""Returns current server url"""
|
||||
|
||||
return "{scheme}://{host}:{port}".format(scheme=self.scheme, host=self.host, port=self.port)
|
||||
return getNetworkUrl(self.protocol(), self.host(), self.port(), self.user(), self.settings())
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
@@ -126,6 +251,24 @@ class HTTPClient(QtCore.QObject):
|
||||
|
||||
return self._local
|
||||
|
||||
def setGNS3VM(self, value):
|
||||
"""
|
||||
Sets either this is a connection to the GNS3 VM or not.
|
||||
|
||||
:param value: boolean
|
||||
"""
|
||||
|
||||
self._gns3_vm = value
|
||||
|
||||
def isGNS3VM(self):
|
||||
"""
|
||||
Returns either this is a connection to the GNS3 VM or not.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._gns3_vm
|
||||
|
||||
def connected(self):
|
||||
"""
|
||||
Returns if the client is connected.
|
||||
@@ -138,81 +281,114 @@ class HTTPClient(QtCore.QObject):
|
||||
"""
|
||||
Closes the connection with the server.
|
||||
"""
|
||||
|
||||
log.info("Connection to %s closed", self.url())
|
||||
self._connected = False
|
||||
|
||||
def isServerRunning(self):
|
||||
def isLocalServerRunning(self):
|
||||
"""
|
||||
Check if a server is already running on this host.
|
||||
Synchronous check if a server is already running on this host.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
try:
|
||||
url = "{scheme}://{host}:{port}/v1/version".format(scheme=self.scheme, host=self.host, port=self.port)
|
||||
response = urllib.request.urlopen(url, timeout=2)
|
||||
content_type = response.getheader("CONTENT-TYPE")
|
||||
if response.status == 200 and content_type == "application/json":
|
||||
content = response.read()
|
||||
json_data = json.loads(content.decode("utf-8"))
|
||||
version = json_data.get("version")
|
||||
local_server = json_data.get("local", False)
|
||||
if version != __version__:
|
||||
log.debug("Client version {} differs with server version {}".format(__version__, version))
|
||||
return False
|
||||
if not local_server:
|
||||
log.debug("Running server is not a GNS3 local server (not started with --local)")
|
||||
return False
|
||||
return True
|
||||
except (OSError, urllib.error.HTTPError, http.client.BadStatusLine) as e:
|
||||
log.debug("No GNS3 server is already running on {}:{}: {}".format(self.host, self.port, e))
|
||||
return False
|
||||
status, json_data = self.getSynchronous("version", timeout=2)
|
||||
if json_data is None or status != 200:
|
||||
return False
|
||||
else:
|
||||
version = json_data.get("version", None)
|
||||
if version is None:
|
||||
log.debug("Server is not a GNS3 server")
|
||||
return False
|
||||
return True
|
||||
|
||||
def get(self, path, callback, context={}):
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
"""
|
||||
Synchronous check if a server is running
|
||||
|
||||
:returns: Tuple (Status code, json of anwser). Status 0 is a non HTTP error
|
||||
"""
|
||||
try:
|
||||
url = "{protocol}://{host}:{port}/v1/{endpoint}".format(protocol=self._scheme, host=self._http_host, port=self._http_port, endpoint=endpoint)
|
||||
|
||||
log.debug("Synchronous get %s with user %s", url, self._user)
|
||||
if self._user is not None and len(self._user) > 0:
|
||||
auth_handler = urllib.request.HTTPBasicAuthHandler()
|
||||
auth_handler.add_password(realm="GNS3 server",
|
||||
uri=url,
|
||||
user=self._user,
|
||||
passwd=self._password)
|
||||
opener = urllib.request.build_opener(auth_handler)
|
||||
urllib.request.install_opener(opener)
|
||||
|
||||
response = urllib.request.urlopen(url, timeout=timeout)
|
||||
content_type = response.getheader("CONTENT-TYPE")
|
||||
if response.status == 200:
|
||||
if content_type == "application/json":
|
||||
content = response.read()
|
||||
json_data = json.loads(content.decode("utf-8"))
|
||||
return response.status, json_data
|
||||
else:
|
||||
return response.status, None
|
||||
except (http.client.InvalidURL) as e:
|
||||
log.warn("Invalid local server url: {}".format(e))
|
||||
return 0, None
|
||||
except (urllib.error.URLError):
|
||||
# Connection refused. It's a normal behavior if server is not started
|
||||
return 0, None
|
||||
except urllib.error.HTTPError as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return e.code, None
|
||||
except (OSError, http.client.BadStatusLine, ValueError) as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return 0, None
|
||||
|
||||
def get(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP GET on the remote server
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param context: Pass a context to the response callback
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
self.createHTTPQuery("GET", path, callback, context=context)
|
||||
self.createHTTPQuery("GET", path, callback, **kwargs)
|
||||
|
||||
def put(self, path, callback, body={}, context={}):
|
||||
def put(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP PUT on the remote server
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param context: Pass a context to the response callback
|
||||
:param body: params to send (dictionary)
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
self.createHTTPQuery("PUT", path, callback, context=context, body=body)
|
||||
self.createHTTPQuery("PUT", path, callback, **kwargs)
|
||||
|
||||
def post(self, path, callback, body={}, context={}):
|
||||
def post(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP POST on the remote server
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param context: Pass a context to the response callback
|
||||
:param body: params to send (dictionary)
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
self.createHTTPQuery("POST", path, callback, context=context, body=body)
|
||||
self.createHTTPQuery("POST", path, callback, **kwargs)
|
||||
|
||||
def delete(self, path, callback, context={}):
|
||||
def delete(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP DELETE on the remote server
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param context: Pass a context to the response callback
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
self.createHTTPQuery("DELETE", path, callback, context=context)
|
||||
self.createHTTPQuery("DELETE", path, callback, **kwargs)
|
||||
|
||||
def _request(self, url):
|
||||
"""
|
||||
@@ -225,51 +401,156 @@ class HTTPClient(QtCore.QObject):
|
||||
|
||||
return QtNetwork.QNetworkRequest(url)
|
||||
|
||||
def createHTTPQuery(self, method, path, callback, body={}, context={}):
|
||||
def _connect(self, query):
|
||||
"""
|
||||
Initialize the connection
|
||||
|
||||
:param query: The query to execute when all network stack is ready
|
||||
"""
|
||||
self.executeHTTPQuery("GET", "/version", query, {})
|
||||
|
||||
def createHTTPQuery(self, method, path, callback, body={}, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None):
|
||||
"""
|
||||
Call the remote server, if not connected, check connection before
|
||||
|
||||
:param method: HTTP method
|
||||
:param path: Remote path
|
||||
:param body: params to send (dictionary)
|
||||
:param body: params to send (dictionary or pathlib.Path)
|
||||
:param callback: callback method to call when the server replies
|
||||
:param context: Pass a context to the response callback
|
||||
:param downloadProgressCallback: Callback called when received something, it can be an incomplete response
|
||||
:param showProgress: Display progress to the user
|
||||
:params progressText: Text display to user in the progress dialog. None for auto generated
|
||||
:param ignoreErrors: Ignore connection error (usefull to not closing a connection when notification feed is broken)
|
||||
:returns: QNetworkReply
|
||||
"""
|
||||
|
||||
if self._connected:
|
||||
self.executeHTTPQuery(method, path, callback, body, context=context)
|
||||
return self.executeHTTPQuery(method, path, qpartial(callback), body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText)
|
||||
else:
|
||||
log.info("Connection to {}:{}".format(self.host, self.port))
|
||||
self.executeHTTPQuery("GET", "/version", partial(self._callbackConnect, method, path, callback, body, context), {})
|
||||
log.info("Connection to {}".format(self.url()))
|
||||
query = qpartial(self._callbackConnect, method, path, qpartial(callback), body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText)
|
||||
self._connect(query)
|
||||
|
||||
def _callbackConnect(self, method, path, callback, body, original_context, params, error=False, **kwargs):
|
||||
def _connectionError(self, callback, msg=""):
|
||||
"""
|
||||
Return an error to user if connection failed
|
||||
|
||||
:param callback: User callback
|
||||
:param msg: An optional additional message for the callback
|
||||
"""
|
||||
|
||||
if self.isLocal():
|
||||
server = "local server {}".format(self.url())
|
||||
else:
|
||||
server = "remote server {}".format(self.url())
|
||||
if len(msg) > 0:
|
||||
msg = "Cannot connect to {}: {}".format(server, msg)
|
||||
else:
|
||||
if self.isLocal():
|
||||
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall.".format(server)
|
||||
else:
|
||||
msg = "Cannot connect to {}".format(server)
|
||||
log.error(msg)
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=self)
|
||||
|
||||
def _callbackConnect(self, method, path, callback, body, original_context, params, error=False, server=None, **kwargs):
|
||||
"""
|
||||
Callback after /version response. Continue execution of query
|
||||
|
||||
:param method: HTTP method
|
||||
:param path: Remote path
|
||||
:param body: params to send (dictionary)
|
||||
:param body: params to send (dictionary or pathlib.Path)
|
||||
:param original_context: Original context
|
||||
:param callback: callback method to call when the server replies
|
||||
"""
|
||||
|
||||
if error is not False:
|
||||
msg = "Can't connect to server {}://{}:{}".format(self.scheme, self.host, self.port)
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=self)
|
||||
self._connectionError(callback)
|
||||
return
|
||||
if params["version"] != __version__:
|
||||
msg = "Client version {} differs with server version {}".format(__version__, params["version"])
|
||||
|
||||
if "version" not in params or "local" not in params:
|
||||
msg = "The remote server {} is not a GNS3 server".format(self.url())
|
||||
log.error(msg)
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=self)
|
||||
return
|
||||
|
||||
if params["version"] != __version__:
|
||||
msg = "Client version {} differs with server version {}".format(__version__, params["version"])
|
||||
log.error(msg)
|
||||
# Stable release
|
||||
if __version_info__[3] == 0:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=self)
|
||||
return
|
||||
# We don't allow different major version to interact even with dev build
|
||||
elif parse_version(__version__)[:2] != parse_version(params["version"])[:2]:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=self)
|
||||
return
|
||||
print(msg)
|
||||
print("WARNING: Use a different client and server version can create bugs. Use it at your own risk.")
|
||||
|
||||
if params["local"] != self.isLocal():
|
||||
if self.isLocal():
|
||||
msg = "Running server is not a GNS3 local server (not started with --local)"
|
||||
else:
|
||||
msg = "Remote running server is started with --local. It is forbidden for security reasons"
|
||||
log.error(msg)
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=self)
|
||||
return
|
||||
|
||||
self.executeHTTPQuery(method, path, callback, body, context=original_context)
|
||||
self._connected = True
|
||||
kwargs["context"] = original_context
|
||||
self.executeHTTPQuery(method, path, callback, body, **kwargs)
|
||||
self._version = params["version"]
|
||||
|
||||
def executeHTTPQuery(self, method, path, callback, body, context={}):
|
||||
def _addBodyToRequest(self, body, request):
|
||||
"""
|
||||
Add the require headers for sending the body.
|
||||
It detect the type of body for sending the corresponding headers
|
||||
and methods.
|
||||
|
||||
:param body: The body
|
||||
:returns: The body compatible with Qt
|
||||
"""
|
||||
|
||||
if body is None:
|
||||
return None
|
||||
|
||||
if isinstance(body, dict):
|
||||
body = json.dumps(body)
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
request.setRawHeader(b"Content-Length", str(len(body)).encode())
|
||||
data = QtCore.QByteArray(body.encode())
|
||||
body = QtCore.QBuffer(self)
|
||||
body.setData(data)
|
||||
body.open(QtCore.QIODevice.ReadOnly)
|
||||
return body
|
||||
elif isinstance(body, pathlib.Path):
|
||||
body = QtCore.QFile(str(body), self)
|
||||
body.open(QtCore.QFile.ReadOnly)
|
||||
request.setRawHeader(b"Content-Type", b"application/octet-stream")
|
||||
# QT is smart and will compute the Content-Lenght for us
|
||||
return body
|
||||
else:
|
||||
return None
|
||||
|
||||
def addAuth(self, request):
|
||||
"""
|
||||
If require add basic auth header
|
||||
"""
|
||||
if self._user:
|
||||
auth_string = "{}:{}".format(self._user, self._password)
|
||||
auth_string = base64.b64encode(auth_string.encode("utf-8"))
|
||||
auth_string = "Basic {}".format(auth_string.decode())
|
||||
request.setRawHeader(b"Authorization", auth_string.encode())
|
||||
return request
|
||||
|
||||
def executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None):
|
||||
"""
|
||||
Call the remote server
|
||||
|
||||
@@ -278,76 +559,153 @@ class HTTPClient(QtCore.QObject):
|
||||
:param body: params to send (dictionary)
|
||||
:param callback: callback method to call when the server replies
|
||||
:param context: Pass a context to the response callback
|
||||
:param downloadProgressCallback: Callback called when received something, it can be an incomplete response
|
||||
:param showProgress: Display progress to the user
|
||||
:param progressText: Text display to user in progress dialog. None for auto generated
|
||||
:param ignoreErrors: Ignore connection error (usefull to not closing a connection when notification feed is broken)
|
||||
:returns: QNetworkReply
|
||||
"""
|
||||
|
||||
import copy
|
||||
try:
|
||||
ip = self._http_host.rsplit('%', 1)[0]
|
||||
ipaddress.IPv6Address(ip) # remove any scope ID
|
||||
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
|
||||
host = "[{}]".format(ip)
|
||||
except ipaddress.AddressValueError:
|
||||
host = self._http_host
|
||||
|
||||
log.debug("{method} {protocol}://{host}:{port}/v1{path} {body}".format(method=method, protocol=self._scheme, host=host, port=self._http_port, path=path, body=body))
|
||||
if self._user:
|
||||
url = QtCore.QUrl("{protocol}://{user}@{host}:{port}/v1{path}".format(protocol=self._scheme, user=self._user, host=host, port=self._http_port, path=path))
|
||||
else:
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}/v1{path}".format(protocol=self._scheme, host=host, port=self._http_port, path=path))
|
||||
request = self._request(url)
|
||||
|
||||
request = self.addAuth(request)
|
||||
|
||||
request.setRawHeader(b"User-Agent", "GNS3 QT Client v{version}".format(version=__version__).encode())
|
||||
|
||||
# By default QT doesn't support GET with body even if it's in the RFC that's why we need to use sendCustomRequest
|
||||
body = self._addBodyToRequest(body, request)
|
||||
|
||||
response = self._network_manager.sendCustomRequest(request, method.encode(), body)
|
||||
|
||||
context = copy.copy(context)
|
||||
context["query_id"] = str(uuid.uuid4())
|
||||
self.notify_progress_start_query(context["query_id"])
|
||||
log.debug("{method} {scheme}://{host}:{port}/v1{path} {body}".format(method=method, scheme=self.scheme, host=self.host, port=self.port, path=path, body=body))
|
||||
url = QtCore.QUrl("{scheme}://{host}:{port}/v1{path}".format(scheme=self.scheme, host=self.host, port=self.port, path=path))
|
||||
request = self._request(url)
|
||||
request.setRawHeader("Content-Type", "application/json")
|
||||
request.setRawHeader("Content-Length", str(len(body)))
|
||||
request.setRawHeader("User-Agent", "GNS3 QT Client v{version}".format(version=__version__))
|
||||
|
||||
if method == "GET":
|
||||
response = self._network_manager.get(request)
|
||||
response.finished.connect(qpartial(self._processResponse, response, callback, context, body, ignoreErrors))
|
||||
|
||||
if method == "PUT":
|
||||
body = json.dumps(body)
|
||||
request.setRawHeader("Content-Type", "application/json")
|
||||
request.setRawHeader("Content-Length", str(len(body)))
|
||||
response = self._network_manager.put(request, body)
|
||||
if downloadProgressCallback is not None:
|
||||
response.downloadProgress.connect(qpartial(self._processDownloadProgress, response, downloadProgressCallback, context))
|
||||
|
||||
if method == "POST":
|
||||
body = json.dumps(body)
|
||||
request.setRawHeader("Content-Type", "application/json")
|
||||
request.setRawHeader("Content-Length", str(len(body)))
|
||||
response = self._network_manager.post(request, body)
|
||||
if showProgress:
|
||||
response.uploadProgress.connect(qpartial(self.notify_progress_upload, context["query_id"]))
|
||||
response.downloadProgress.connect(qpartial(self.notify_progress_download, context["query_id"]))
|
||||
# Should be the last operation otherwise we have race condition in Qt
|
||||
# where query start before finishing connect to everything
|
||||
self.notify_progress_start_query(context["query_id"], progressText, response)
|
||||
|
||||
if method == "DELETE":
|
||||
response = self._network_manager.deleteResource(request)
|
||||
return response
|
||||
|
||||
response.finished.connect(partial(self._processResponse, response, callback, context))
|
||||
def _processDownloadProgress(self, response, callback, context, bytesReceived, bytesTotal):
|
||||
"""
|
||||
Process a packet receive on the notification feed.
|
||||
The feed can contains qpartial JSON. If we found a
|
||||
part of a JSON we keep it for the next packet
|
||||
"""
|
||||
|
||||
if response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
return
|
||||
|
||||
# HTTP error
|
||||
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status >= 300:
|
||||
return
|
||||
|
||||
content = bytes(response.readAll())
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
if content_type == "application/json":
|
||||
content = content.decode("utf-8")
|
||||
if context["query_id"] in self._buffer:
|
||||
content = self._buffer[context["query_id"]] + content
|
||||
try:
|
||||
while True:
|
||||
content = content.lstrip(" \r\n\t")
|
||||
answer, index = json.JSONDecoder().raw_decode(content)
|
||||
callback(answer, server=self, context=context)
|
||||
content = content[index:]
|
||||
except ValueError: # Partial JSON
|
||||
self._buffer[context["query_id"]] = content
|
||||
else:
|
||||
callback(content, server=self, context=context)
|
||||
|
||||
if HTTPClient._progress_callback and HTTPClient._progress_callback.progress_dialog():
|
||||
request_canceled = partial(self._requestCanceled, response, context)
|
||||
request_canceled = qpartial(self._requestCanceled, response, context)
|
||||
HTTPClient._progress_callback.progress_dialog().canceled.connect(request_canceled)
|
||||
|
||||
def _requestCanceled(self, response, context):
|
||||
|
||||
if response.isRunning():
|
||||
response.finished.disconnect()
|
||||
response.abort()
|
||||
if "query_id" in context:
|
||||
self.notify_progress_end_query(context["query_id"])
|
||||
|
||||
def _processResponse(self, response, callback, context):
|
||||
def _processResponse(self, response, callback, context, request_body, ignore_errors):
|
||||
|
||||
if request_body is not None:
|
||||
request_body.close()
|
||||
|
||||
status = None
|
||||
body = None
|
||||
|
||||
if "query_id" in context:
|
||||
self.notify_progress_end_query(context["query_id"])
|
||||
|
||||
if response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
error_code = response.error()
|
||||
if error_code < 200:
|
||||
self._connected = False
|
||||
error_message = response.errorString()
|
||||
log.info("Response error: {}".format(error_message))
|
||||
body = bytes(response.readAll()).decode()
|
||||
|
||||
if not ignore_errors:
|
||||
log.info("Response error: %s (error: %d)", error_message, error_code)
|
||||
|
||||
if error_code < 200:
|
||||
if not ignore_errors:
|
||||
self.close()
|
||||
if callback is not None:
|
||||
callback({"message": error_message}, error=True, server=self, context=context)
|
||||
return
|
||||
else:
|
||||
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status == 401:
|
||||
print(error_message)
|
||||
|
||||
try:
|
||||
body = bytes(response.readAll()).decode("utf-8").strip("\0")
|
||||
# Some time antivirus intercept our query and reply with garbage content
|
||||
except UnicodeError:
|
||||
body = None
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
if callback is not None:
|
||||
if not body or content_type != "application/json":
|
||||
callback({"message": error_message}, error=True, server=self, context=context)
|
||||
else:
|
||||
log.debug(body)
|
||||
callback(json.loads(body), error=True, server=self, context=context)
|
||||
try:
|
||||
callback(json.loads(body), error=True, server=self, context=context)
|
||||
except ValueError:
|
||||
# It happens when an antivirus catch the communication and send is error page without changing the Content Type
|
||||
callback({"message": error_message}, error=True, server=self, context=context)
|
||||
else:
|
||||
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
log.debug("Decoding response from {} response {}".format(response.url().toString(), status))
|
||||
body = bytes(response.readAll()).decode()
|
||||
try:
|
||||
body = bytes(response.readAll()).decode("utf-8").strip("\0")
|
||||
# Some time anti-virus intercept our query and reply with garbage content
|
||||
except UnicodeDecodeError:
|
||||
body = None
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
log.debug(body)
|
||||
if body and content_type == "application/json":
|
||||
if body and len(body.strip(" \n\t")) > 0 and content_type == "application/json":
|
||||
params = json.loads(body)
|
||||
else:
|
||||
params = {}
|
||||
@@ -356,7 +714,56 @@ class HTTPClient(QtCore.QObject):
|
||||
callback(params, error=True, server=self, context=context)
|
||||
else:
|
||||
callback(params, server=self, context=context)
|
||||
response.deleteLater()
|
||||
# response.deleteLater()
|
||||
if status == 400:
|
||||
try:
|
||||
params = json.loads(body)
|
||||
e = HttpBadRequest(body)
|
||||
e.fingerprint = params["path"]
|
||||
# If something goes wrong for a any reason just raise the bad request
|
||||
except Exception:
|
||||
e = HttpBadRequest(body)
|
||||
raise e
|
||||
|
||||
def RAMLimit(self):
|
||||
"""
|
||||
Returns the RAM limit for this server (used for RAM usage load balancing).
|
||||
|
||||
:returns: RAM limit (integer)
|
||||
"""
|
||||
|
||||
return self._ram_limit
|
||||
|
||||
def allocatedRAM(self):
|
||||
"""
|
||||
Amount of allocated RAM on this server (used for RAM usage load balancing).
|
||||
|
||||
:returns: allocated RAM (integer)
|
||||
"""
|
||||
|
||||
return self._allocated_ram
|
||||
|
||||
def increaseAllocatedRAM(self, ram):
|
||||
"""
|
||||
Increase the amount of allocated RAM on this server (used for RAM usage load balancing).
|
||||
|
||||
:param ram: amount of RAM (integer)
|
||||
"""
|
||||
|
||||
log.info("RAM usage on {} has increased by {} MB (total load is now {} MB)".format(self.url(), ram, self._allocated_ram + ram))
|
||||
self._allocated_ram += ram
|
||||
|
||||
def decreaseAllocatedRAM(self, ram):
|
||||
"""
|
||||
Decrease the amount of allocated RAM on this server (used for RAM usage load balancing).
|
||||
|
||||
:param ram: amount of RAM (integer)
|
||||
"""
|
||||
|
||||
log.info("RAM usage on {} has decreased by {} MB (total load is now {} MB)".format(self.url(), ram, self._allocated_ram - ram))
|
||||
self._allocated_ram -= ram
|
||||
if self._allocated_ram < 0:
|
||||
self._allocated_ram = 0
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
@@ -364,11 +771,18 @@ class HTTPClient(QtCore.QObject):
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
return {"id": self._id,
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"local": self._local,
|
||||
"cloud": self._cloud}
|
||||
server = self.settings()
|
||||
server["id"] = self._id
|
||||
server["local"] = self._local
|
||||
server["vm"] = self._gns3_vm
|
||||
#server["cloud"] = self._cloud
|
||||
if "user" in server and self._local:
|
||||
del server["user"]
|
||||
if "password" in server:
|
||||
del server["password"]
|
||||
if server["protocol"] == "https":
|
||||
server["accept_insecure_certificate"] = self._accept_insecure_certificate
|
||||
return server
|
||||
|
||||
def isCloud(self):
|
||||
return False
|
||||
|
||||
188
gns3/image_manager.py
Normal file
188
gns3/image_manager.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import glob
|
||||
|
||||
from gns3.servers import Servers
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.utils.file_copy_worker import FileCopyWorker
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
|
||||
|
||||
class ImageManager:
|
||||
|
||||
def __init__(self):
|
||||
# Remember if we already ask the user about this image for this server
|
||||
self._asked_for_this_image = {}
|
||||
|
||||
def askCopyUploadImage(self, parent, path, server, vm_type):
|
||||
"""
|
||||
Ask user for copying the image to the default directory or upload
|
||||
it to remote server.
|
||||
|
||||
:param parent: Parent window
|
||||
:param path: File path on computer
|
||||
:param server: The server where the images should be located
|
||||
:param vm_type: Remote upload endpoint
|
||||
:returns path: Final path
|
||||
"""
|
||||
|
||||
if server and not server.isLocal():
|
||||
return self._uploadImageToRemoteServer(path, server, vm_type)
|
||||
else:
|
||||
destination_directory = self.getDirectoryForType(vm_type)
|
||||
if os.path.normpath(os.path.dirname(path)) != destination_directory:
|
||||
# the IOS image is not in the default images directory
|
||||
reply = QtWidgets.QMessageBox.question(parent,
|
||||
'Image',
|
||||
'Would you like to copy {} to the default images directory'.format(os.path.basename(path)),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
destination_path = os.path.join(destination_directory, os.path.basename(path))
|
||||
try:
|
||||
os.makedirs(destination_directory, exist_ok=True)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(parent, 'Image', 'Could not create destination directory {}: {}'.format(destination_directory, str(e)))
|
||||
return path
|
||||
worker = FileCopyWorker(path, destination_path)
|
||||
progress_dialog = ProgressDialog(worker, 'Image', 'Copying {}'.format(os.path.basename(path)), 'Cancel', busy=True, parent=parent)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
errors = progress_dialog.errors()
|
||||
if errors:
|
||||
QtWidgets.QMessageBox.critical(parent, 'Image', '{}'.format(''.join(errors)))
|
||||
return path
|
||||
else:
|
||||
path = destination_path
|
||||
return path
|
||||
|
||||
def _uploadImageToRemoteServer(self, path, server, vm_type):
|
||||
"""
|
||||
Upload image to remote server
|
||||
|
||||
:param path: File path on computer
|
||||
:param server: The server where the images should be located
|
||||
:param vm_type: Image vm_type
|
||||
:returns path: Final path
|
||||
"""
|
||||
|
||||
if vm_type == 'QEMU':
|
||||
upload_endpoint = '/qemu/vms'
|
||||
elif vm_type == 'IOU':
|
||||
upload_endpoint = '/iou/vms'
|
||||
elif vm_type == 'DYNAMIPS':
|
||||
upload_endpoint = '/dynamips/vms'
|
||||
else:
|
||||
raise Exception('Invalid image vm_type')
|
||||
|
||||
filename = self._getRelativeImagePath(path, vm_type).replace("\\", "/")
|
||||
server.post('{}/{}'.format(upload_endpoint, filename), None, body=pathlib.Path(path), progressText="Uploading {}".format(filename))
|
||||
return filename
|
||||
|
||||
def addMissingImage(self, filename, server, vm_type):
|
||||
"""
|
||||
Add a missing image to the queue of images require to be upload on remote server
|
||||
:param filename: Filename of the image
|
||||
:param server: Server where image should be uploaded
|
||||
:param vm_type: Type of the image
|
||||
"""
|
||||
|
||||
if self._asked_for_this_image.setdefault(server.id(), {}).setdefault(filename, False):
|
||||
return
|
||||
self._asked_for_this_image[server.id()][filename] = True
|
||||
|
||||
if server.isLocal():
|
||||
return
|
||||
path = os.path.join(self.getDirectoryForType(vm_type), filename)
|
||||
if os.path.exists(path):
|
||||
if self._askForUploadMissingImage(filename, server):
|
||||
|
||||
if filename.endswith(".vmdk"):
|
||||
# A vmdk file could be split in multiple vmdk file
|
||||
search = glob.escape(path).replace(".vmdk", "-*.vmdk")
|
||||
for file in glob.glob(search):
|
||||
self._uploadImageToRemoteServer(file, server, vm_type)
|
||||
|
||||
self._uploadImageToRemoteServer(path, server, vm_type)
|
||||
del self._asked_for_this_image[server.id()][filename]
|
||||
|
||||
def _askForUploadMissingImage(self, filename, server):
|
||||
from gns3.main_window import MainWindow
|
||||
parent = MainWindow.instance()
|
||||
reply = QtWidgets.QMessageBox.warning(parent,
|
||||
'Image',
|
||||
'{} is missing on server {} but exist on your computer. Do you want to upload it?'.format(filename, server.url()),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _getRelativeImagePath(self, path, vm_type):
|
||||
"""
|
||||
Get a path relative to images directory path
|
||||
or just filename if the path is not located inside
|
||||
image directory
|
||||
|
||||
:param path: file path
|
||||
:param vm_type: Type of vm
|
||||
:return: file path
|
||||
"""
|
||||
|
||||
if not path:
|
||||
return ""
|
||||
img_directory = self.getDirectoryForType(vm_type)
|
||||
path = os.path.abspath(path)
|
||||
if os.path.commonprefix([img_directory, path]) == img_directory:
|
||||
return os.path.relpath(path, img_directory)
|
||||
return os.path.basename(path)
|
||||
|
||||
def getDirectory(self):
|
||||
"""
|
||||
Returns the images directory path.
|
||||
|
||||
:returns: path to the default images directory
|
||||
"""
|
||||
|
||||
return Servers.instance().localServerSettings()['images_path']
|
||||
|
||||
def getDirectoryForType(self, vm_type):
|
||||
"""
|
||||
Return the path of local directory of the images
|
||||
of a specific vm_type
|
||||
|
||||
:param vm_type: Type of vm
|
||||
"""
|
||||
if vm_type == 'DYNAMIPS':
|
||||
return os.path.join(self.getDirectory(), 'IOS')
|
||||
else:
|
||||
return os.path.join(self.getDirectory(), vm_type)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of ImageManager.
|
||||
|
||||
:returns: instance of ImageManager
|
||||
"""
|
||||
|
||||
if not hasattr(ImageManager, '_instance') or ImageManager._instance is None:
|
||||
ImageManager._instance = ImageManager()
|
||||
return ImageManager._instance
|
||||
189
gns3/iouvm_converter.py
Normal file
189
gns3/iouvm_converter.py
Normal file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import sys
|
||||
import shutil
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
try:
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
except ImportError:
|
||||
raise SystemExit("Can't import Qt modules: Qt and/or PyQt is probably not installed correctly...")
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from gns3.version import __version__
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.ui.iouvm_converter_wizard_ui import Ui_IOUVMConverterWizard
|
||||
|
||||
|
||||
class IOUVMConverterWizard(QtWidgets.QWizard, Ui_IOUVMConverterWizard):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
|
||||
|
||||
# set the window icon
|
||||
self.setWindowIcon(QtGui.QIcon(":/images/gns3.ico")) # this info is necessary for QSettings
|
||||
|
||||
config = self._loadConfig()
|
||||
self.uiPushButtonBrowse.clicked.connect(self._browseTopologiesSlot)
|
||||
self.uiLineEditTopologiesPath.setText(config['Servers']['local_server']['projects_path'])
|
||||
|
||||
def _browseTopologiesSlot(self):
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select a directory')
|
||||
self.uiLineEditTopologiesPath.setText(path)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the settings.
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiWizardPageIOURCCheck:
|
||||
return self._checkIOURC()
|
||||
elif self.currentPage() == self.uiWizardUpdateConfiguration:
|
||||
return self._updateConfig()
|
||||
elif self.currentPage() == self.uiWizardPagePatchTopologies:
|
||||
return self._patchTopologies()
|
||||
return True
|
||||
|
||||
def _checkIOURC(self):
|
||||
"""
|
||||
Validate if the IOURC contain an entry for the IOUVM
|
||||
"""
|
||||
config = self._loadConfig()
|
||||
iourc_path = config.get("IOU", {}).get("iourc_path", "")
|
||||
if len(iourc_path) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Error", "The IOURC is not configured")
|
||||
return False
|
||||
try:
|
||||
with open(iourc_path) as f:
|
||||
if 'gns3vm' not in f.read():
|
||||
QtWidgets.QMessageBox.critical(self, "Error", "The gns3vm doesn't exist in your iourc file".format(iourc_path))
|
||||
except OSError:
|
||||
QtWidgets.QMessageBox.critical(self, "Error", "IOURC file {} doesn't exist or not accessible".format(iourc_path))
|
||||
return True
|
||||
|
||||
def _updateConfig(self):
|
||||
"""
|
||||
Update the config file to use the GNS3 VM instead of IOU VM
|
||||
"""
|
||||
config = self._loadConfig()
|
||||
if "devices" in config["IOU"]:
|
||||
for device in config["IOU"]["devices"]:
|
||||
device["path"] = os.path.basename(device["path"])
|
||||
device["server"] = "vm"
|
||||
config["Servers"]["remote_servers"] = []
|
||||
self._writeConfig(config)
|
||||
return True
|
||||
|
||||
def _patchTopologies(self):
|
||||
"""
|
||||
Patch topologies to use the GNS3 VM
|
||||
"""
|
||||
|
||||
path = self.uiLineEditTopologiesPath.text()
|
||||
try:
|
||||
for (dirpath, dirnames, filenames) in os.walk(path):
|
||||
for filename in filenames:
|
||||
if filename.endswith(".gns3"):
|
||||
self._patchTopology(os.path.join(dirpath, filename))
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Error", "Can't open {}: {}".format(path, str(e)))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _patchTopology(self, path):
|
||||
"""
|
||||
Path a specific topology
|
||||
"""
|
||||
try:
|
||||
shutil.copy(path, "{}.{}.backup".format(path, datetime.now().isoformat()))
|
||||
with open(path) as f:
|
||||
topo = json.load(f)
|
||||
if "topology" in topo and "servers" in topo["topology"]:
|
||||
for server in topo["topology"]["servers"]:
|
||||
if server["local"] is False:
|
||||
server["vm"] = True
|
||||
with open(path, 'w+') as f:
|
||||
topo = json.dump(topo, f)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Error", "Can't open {}: {}".format(path, str(e)))
|
||||
|
||||
def _loadConfig(self):
|
||||
with open(self._configurationFile()) as f:
|
||||
return json.load(f)
|
||||
|
||||
def _writeConfig(self, config):
|
||||
shutil.copy(self._configurationFile(), "{}.{}.backup".format(self._configurationFile(), datetime.now().isoformat()))
|
||||
with open(self._configurationFile(), 'w+') as f:
|
||||
json.dump(config, f, indent=4)
|
||||
|
||||
def _configurationFile(self):
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_gui.ini"
|
||||
else:
|
||||
filename = "gns3_gui.conf"
|
||||
directory = LocalConfig.configDirectory()
|
||||
return os.path.join(directory, filename)
|
||||
|
||||
|
||||
def main():
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
app.setOrganizationName("GNS3")
|
||||
app.setOrganizationDomain("gns3.net")
|
||||
app.setApplicationName("GNS3")
|
||||
app.setApplicationVersion(__version__)
|
||||
|
||||
# We force a full garbage collect before exit
|
||||
# for unknow reason otherwise Qt Segfault on OSX in some
|
||||
# conditions
|
||||
import gc
|
||||
gc.collect()
|
||||
|
||||
# Manage Ctrl + C or kill command
|
||||
def sigint_handler(*args):
|
||||
log.info("Signal received exiting the application")
|
||||
app.closeAllWindows()
|
||||
# signal.signal(signal.SIGINT, sigint_handler)
|
||||
# signal.signal(signal.SIGTERM, sigint_handler)
|
||||
|
||||
mainwindow = IOUVMConverterWizard()
|
||||
mainwindow.show()
|
||||
exit_code = mainwindow.exec_()
|
||||
|
||||
# We force a full garbage collect before exit
|
||||
# for unknow reason otherwise Qt Segfault on OSX in some
|
||||
# conditions
|
||||
import gc
|
||||
gc.collect()
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -19,11 +19,11 @@
|
||||
Graphical representation of an ellipse on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .shape_item import ShapeItem
|
||||
|
||||
|
||||
class EllipseItem(ShapeItem, QtGui.QGraphicsEllipseItem):
|
||||
class EllipseItem(QtWidgets.QGraphicsEllipseItem, ShapeItem):
|
||||
|
||||
"""
|
||||
Class to draw an ellipse on the scene.
|
||||
@@ -31,8 +31,8 @@ class EllipseItem(ShapeItem, QtGui.QGraphicsEllipseItem):
|
||||
|
||||
def __init__(self, pos=None, width=200, height=200):
|
||||
|
||||
QtGui.QGraphicsEllipseItem.__init__(self, 0, 0, width, height)
|
||||
ShapeItem.__init__(self)
|
||||
super().__init__()
|
||||
self.setRect(0, 0, width, height)
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.DashLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.setPen(pen)
|
||||
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
|
||||
@@ -58,7 +58,7 @@ class EllipseItem(ShapeItem, QtGui.QGraphicsEllipseItem):
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsEllipseItem.paint(self, painter, option, widget)
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def duplicate(self):
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
Graphical representation of an Ethernet link for QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .link_item import LinkItem
|
||||
from .note_item import NoteItem
|
||||
from ..ports.port import Port
|
||||
@@ -41,7 +41,7 @@ class EthernetLinkItem(LinkItem):
|
||||
|
||||
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
|
||||
|
||||
LinkItem.__init__(self, source_item, source_port, destination_item, destination_port, link, adding_flag, multilink)
|
||||
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag, multilink)
|
||||
self._source_collision_offset = 0.0
|
||||
self._destination_collision_offset = 0.0
|
||||
|
||||
@@ -75,7 +75,7 @@ class EthernetLinkItem(LinkItem):
|
||||
:returns: QPainterPath instance
|
||||
"""
|
||||
|
||||
path = QtGui.QGraphicsPathItem.shape(self)
|
||||
path = QtWidgets.QGraphicsPathItem.shape(self)
|
||||
offset = self._point_size / 2
|
||||
if not self._adding_flag:
|
||||
if self.length:
|
||||
@@ -106,7 +106,7 @@ class EthernetLinkItem(LinkItem):
|
||||
:param widget: QWidget instance.
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
if not self._adding_flag and self._settings["draw_link_status_points"]:
|
||||
|
||||
# points disappears if nodes are too close to each others.
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
Graphical representation of an image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore
|
||||
|
||||
|
||||
class ImageItem(QtGui.QGraphicsPixmapItem):
|
||||
class ImageItem():
|
||||
|
||||
"""
|
||||
Class to insert an image on the scene.
|
||||
@@ -30,11 +30,9 @@ class ImageItem(QtGui.QGraphicsPixmapItem):
|
||||
|
||||
show_layer = False
|
||||
|
||||
def __init__(self, pixmap, image_path, pos=None):
|
||||
def __init__(self, image_path, pos=None):
|
||||
|
||||
QtGui.QGraphicsPixmapItem.__init__(self, pixmap)
|
||||
self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)
|
||||
self.setTransformationMode(QtCore.Qt.SmoothTransformation)
|
||||
self._image_path = image_path
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
@@ -48,17 +46,6 @@ class ImageItem(QtGui.QGraphicsPixmapItem):
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeImage(self)
|
||||
|
||||
def duplicate(self):
|
||||
"""
|
||||
Duplicates this image item.
|
||||
|
||||
:return: ImageItem instance
|
||||
"""
|
||||
|
||||
image_item = ImageItem(self.pixmap(), self._image_path, QtCore.QPointF(self.x() + 20, self.y() + 20))
|
||||
image_item.setZValue(self.zValue())
|
||||
return image_item
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
Paints the contents of an item in local coordinates.
|
||||
@@ -68,7 +55,7 @@ class ImageItem(QtGui.QGraphicsPixmapItem):
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsPixmapItem.paint(self, painter, option, widget)
|
||||
super().paint(painter, option, widget)
|
||||
|
||||
if self.show_layer is False:
|
||||
return
|
||||
@@ -93,7 +80,7 @@ class ImageItem(QtGui.QGraphicsPixmapItem):
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsPixmapItem.setZValue(self, value)
|
||||
super().setZValue(value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
|
||||
@@ -23,10 +23,10 @@ Link items are graphical representation of a link on the QGraphicsScene
|
||||
import math
|
||||
import struct
|
||||
import sys
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class LinkItem(QtGui.QGraphicsPathItem):
|
||||
class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
"""
|
||||
Base class for link items.
|
||||
@@ -44,8 +44,8 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
|
||||
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
|
||||
|
||||
QtGui.QGraphicsPathItem.__init__(self)
|
||||
self.setAcceptsHoverEvents(True)
|
||||
super().__init__()
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setZValue(-1)
|
||||
self._link = None
|
||||
|
||||
@@ -181,33 +181,33 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
|
||||
if not self._source_port.capturing() or not self._destination_port.capturing():
|
||||
# start capture
|
||||
start_capture_action = QtGui.QAction("Start capture", menu)
|
||||
start_capture_action = QtWidgets.QAction("Start capture", menu)
|
||||
start_capture_action.setIcon(QtGui.QIcon(':/icons/capture-start.svg'))
|
||||
start_capture_action.triggered.connect(self._startCaptureActionSlot)
|
||||
menu.addAction(start_capture_action)
|
||||
|
||||
if self._source_port.capturing() or self._destination_port.capturing():
|
||||
# stop capture
|
||||
stop_capture_action = QtGui.QAction("Stop capture", menu)
|
||||
stop_capture_action = QtWidgets.QAction("Stop capture", menu)
|
||||
stop_capture_action.setIcon(QtGui.QIcon(':/icons/capture-stop.svg'))
|
||||
stop_capture_action.triggered.connect(self._stopCaptureActionSlot)
|
||||
menu.addAction(stop_capture_action)
|
||||
|
||||
# start wireshark
|
||||
start_wireshark_action = QtGui.QAction("Start Wireshark", menu)
|
||||
start_wireshark_action = QtWidgets.QAction("Start Wireshark", menu)
|
||||
start_wireshark_action.setIcon(QtGui.QIcon(":/icons/wireshark.png"))
|
||||
start_wireshark_action.triggered.connect(self._startWiresharkActionSlot)
|
||||
menu.addAction(start_wireshark_action)
|
||||
|
||||
if sys.platform.startswith("win") and struct.calcsize("P") * 8 == 64:
|
||||
# Windows 64-bit only (Solarwinds RTV limitation).
|
||||
analyze_action = QtGui.QAction("Analyze capture", menu)
|
||||
analyze_action = QtWidgets.QAction("Analyze capture", menu)
|
||||
analyze_action.setIcon(QtGui.QIcon(':/icons/rtv.png'))
|
||||
analyze_action.triggered.connect(self._analyzeCaptureActionSlot)
|
||||
menu.addAction(analyze_action)
|
||||
|
||||
# delete
|
||||
delete_action = QtGui.QAction("Delete", menu)
|
||||
delete_action = QtWidgets.QAction("Delete", menu)
|
||||
delete_action.setIcon(QtGui.QIcon(':/icons/delete.svg'))
|
||||
delete_action.triggered.connect(self._deleteActionSlot)
|
||||
menu.addAction(delete_action)
|
||||
@@ -224,15 +224,15 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
# send a escape key to the main window to cancel the link addition
|
||||
from ..main_window import MainWindow
|
||||
key = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Escape, QtCore.Qt.NoModifier)
|
||||
QtGui.QApplication.sendEvent(MainWindow.instance(), key)
|
||||
QtWidgets.QApplication.sendEvent(MainWindow.instance(), key)
|
||||
return
|
||||
|
||||
# create the contextual menu
|
||||
self.setAcceptsHoverEvents(False)
|
||||
menu = QtGui.QMenu()
|
||||
self.setAcceptHoverEvents(False)
|
||||
menu = QtWidgets.QMenu()
|
||||
self.populateLinkContextualMenu(menu)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
self.setAcceptsHoverEvents(True)
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._hovered = False
|
||||
self.adjust()
|
||||
|
||||
@@ -243,7 +243,7 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
#On pressing backspace or delete key, the selected link gets deleted
|
||||
# On pressing backspace or delete key, the selected link gets deleted
|
||||
if event.key() == QtCore.Qt.Key_Delete or event.key() == QtCore.Qt.Key_Backspace:
|
||||
self._deleteActionSlot()
|
||||
return
|
||||
@@ -274,10 +274,10 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
ports[port] = [self._destination_item.node(), self._destination_port, dlt]
|
||||
|
||||
if not ports:
|
||||
QtGui.QMessageBox.critical(self._main_window, "Packet capture", "Packet capture is not supported on this link")
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Packet capture", "Packet capture is not supported on this link")
|
||||
return
|
||||
|
||||
selection, ok = QtGui.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", list(ports.keys()), 0, False)
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", list(ports.keys()), 0, False)
|
||||
if ok:
|
||||
if selection in ports:
|
||||
node, port, dlt = ports[selection]
|
||||
@@ -295,7 +295,7 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
ports[source_port] = [self._source_item.node(), self._source_port]
|
||||
destination_port = "{} port {}".format(self._destination_item.node().name(), self._destination_port.name())
|
||||
ports[destination_port] = [self._destination_item.node(), self._destination_port]
|
||||
selection, ok = QtGui.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", list(ports.keys()), 0, False)
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", list(ports.keys()), 0, False)
|
||||
if ok:
|
||||
if selection in ports:
|
||||
node, port = ports[selection]
|
||||
@@ -315,18 +315,18 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
if self._source_port.capturing() and self._destination_port.capturing():
|
||||
ports = ["{} port {}".format(self._source_item.node().name(), self._source_port.name()),
|
||||
"{} port {}".format(self._destination_item.node().name(), self._destination_port.name())]
|
||||
selection, ok = QtGui.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", ports, 0, False)
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", ports, 0, False)
|
||||
if ok:
|
||||
if selection.endswith(self._source_port.name()):
|
||||
self._source_port.startPacketCaptureReader()
|
||||
self._source_port.startPacketCaptureReader(self._source_item.node().name())
|
||||
else:
|
||||
self._destination_port.startPacketCaptureReader()
|
||||
self._destination_port.startPacketCaptureReader(self._destination_item.node().name())
|
||||
elif self._source_port.capturing():
|
||||
self._source_port.startPacketCaptureReader()
|
||||
self._source_port.startPacketCaptureReader(self._source_item.node().name())
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_port.startPacketCaptureReader()
|
||||
self._destination_port.startPacketCaptureReader(self._destination_item.node().name())
|
||||
except OSError as e:
|
||||
QtGui.QMessageBox.critical(self._main_window, "Packet capture", "Cannot start Wireshark: {}".format(e))
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Packet capture", "Cannot start Wireshark: {}".format(e))
|
||||
|
||||
def _analyzeCaptureActionSlot(self):
|
||||
"""
|
||||
@@ -338,7 +338,7 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
if self._source_port.capturing() and self._destination_port.capturing():
|
||||
ports = ["{} port {}".format(self._source_item.node().name(), self._source_port.name()),
|
||||
"{} port {}".format(self._destination_item.node().name(), self._destination_port.name())]
|
||||
selection, ok = QtGui.QInputDialog.getItem(self._main_window, "Capture analyzer", "Please select a port:", ports, 0, False)
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Capture analyzer", "Please select a port:", ports, 0, False)
|
||||
if ok:
|
||||
if selection.endswith(self._source_port.name()):
|
||||
self._source_port.startPacketCaptureAnalyzer()
|
||||
@@ -349,7 +349,7 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_port.startPacketCaptureAnalyzer()
|
||||
except OSError as e:
|
||||
QtGui.QMessageBox.critical(self._main_window, "Capture analyzer", "Cannot start the packet capture analyzer program: {}".format(e))
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Capture analyzer", "Cannot start the packet capture analyzer program: {}".format(e))
|
||||
|
||||
def setHovered(self, value):
|
||||
"""
|
||||
|
||||
@@ -19,25 +19,24 @@
|
||||
Graphical representation of a node on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui, QtSvg
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .note_item import NoteItem
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
class NodeItem():
|
||||
|
||||
"""
|
||||
Node for the scene.
|
||||
|
||||
:param node: Node instance
|
||||
:param default_symbol: Default symbol for the node representation on the scene
|
||||
:param hover_symbol: Hover symbol when the node is hovered on the scene
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
|
||||
def __init__(self, node, default_symbol=None, hover_symbol=None):
|
||||
|
||||
QtSvg.QGraphicsSvgItem.__init__(self)
|
||||
def __init__(self, node):
|
||||
|
||||
# attached node
|
||||
self._node = node
|
||||
@@ -48,28 +47,23 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
# link items connected to this node item.
|
||||
self._links = []
|
||||
|
||||
# set graphical settings for this node
|
||||
self.setFlag(QtSvg.QGraphicsSvgItem.ItemIsMovable)
|
||||
self.setFlag(QtSvg.QGraphicsSvgItem.ItemIsSelectable)
|
||||
self.setFlag(QtSvg.QGraphicsSvgItem.ItemIsFocusable)
|
||||
self.setFlag(QtSvg.QGraphicsSvgItem.ItemSendsGeometryChanges)
|
||||
self.setAcceptsHoverEvents(True)
|
||||
self.setZValue(1)
|
||||
effect = QtWidgets.QGraphicsColorizeEffect()
|
||||
effect.setColor(QtGui.QColor("black"))
|
||||
effect.setStrength(0.8)
|
||||
#effect = QtWidgets.QGraphicsDropShadowEffect()
|
||||
# effect.setColor(QtGui.QColor("darkGray"))
|
||||
# effect.setBlurRadius(0)
|
||||
#effect.setOffset(3, 3)
|
||||
self.setGraphicsEffect(effect)
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
# create renderers using symbols paths/resources
|
||||
if default_symbol:
|
||||
self._default_renderer = QtSvg.QSvgRenderer(default_symbol)
|
||||
if default_symbol != node.defaultSymbol():
|
||||
self._default_renderer.setObjectName(default_symbol)
|
||||
else:
|
||||
self._default_renderer = QtSvg.QSvgRenderer(node.defaultSymbol())
|
||||
if hover_symbol:
|
||||
self._hover_renderer = QtSvg.QSvgRenderer(hover_symbol)
|
||||
if hover_symbol != node.hoverSymbol():
|
||||
self._hover_renderer.setObjectName(hover_symbol)
|
||||
else:
|
||||
self._hover_renderer = QtSvg.QSvgRenderer(node.hoverSymbol())
|
||||
self.setSharedRenderer(self._default_renderer)
|
||||
# set graphical settings for this node
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setZValue(1)
|
||||
|
||||
# connect signals to know about some events
|
||||
# e.g. when the node has been started, stopped or suspended etc.
|
||||
@@ -98,43 +92,6 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
self._main_window = MainWindow.instance()
|
||||
self._settings = self._main_window.uiGraphicsView.settings()
|
||||
|
||||
def defaultRenderer(self):
|
||||
"""
|
||||
Returns the default QSvgRenderer.
|
||||
|
||||
:return: QSvgRenderer instance
|
||||
"""
|
||||
|
||||
return self._default_renderer
|
||||
|
||||
def setDefaultRenderer(self, default_renderer):
|
||||
"""
|
||||
Sets new default QSvgRenderer.
|
||||
|
||||
:param default_renderer: QSvgRenderer instance
|
||||
"""
|
||||
|
||||
self._default_renderer = default_renderer
|
||||
self.setSharedRenderer(self._default_renderer)
|
||||
|
||||
def hoverRenderer(self):
|
||||
"""
|
||||
Returns the hover QSvgRenderer.
|
||||
|
||||
:return: QSvgRenderer instance
|
||||
"""
|
||||
|
||||
return self._hover_renderer
|
||||
|
||||
def setHoverRenderer(self, hover_renderer):
|
||||
"""
|
||||
Sets new hover QSvgRenderer.
|
||||
|
||||
:param hover_renderer: QSvgRenderer instance
|
||||
"""
|
||||
|
||||
self._hover_renderer = hover_renderer
|
||||
|
||||
def setUnsavedState(self):
|
||||
"""
|
||||
Indicates the project is in a unsaved state.
|
||||
@@ -255,6 +212,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
when the node has been deleted.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
self._node.removeAllocatedName()
|
||||
if self in self.scene().items():
|
||||
self.scene().removeItem(self)
|
||||
@@ -269,7 +228,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
self._last_error = "{message}".format(message=message)
|
||||
if self:
|
||||
self._last_error = "{message}".format(message=message)
|
||||
|
||||
def errorSlot(self, node_id, message):
|
||||
"""
|
||||
@@ -280,7 +240,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
self._last_error = "{message}".format(message=message)
|
||||
if self:
|
||||
self._last_error = "{message}".format(message=message)
|
||||
|
||||
def setCustomToolTip(self):
|
||||
"""
|
||||
@@ -348,10 +309,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
self._selected_port = None
|
||||
menu = QtGui.QMenu()
|
||||
menu = QtWidgets.QMenu()
|
||||
ports = self._node.ports()
|
||||
if not ports:
|
||||
QtGui.QMessageBox.critical(self.scene().parent(), "Link", "No port available, please configure this device")
|
||||
QtWidgets.QMessageBox.critical(self.scene().parent(), "Link", "No port available, please configure this device")
|
||||
return None
|
||||
|
||||
# sort the ports
|
||||
@@ -361,7 +322,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
# make the port number unique (special case with WICs).
|
||||
port_number = port.portNumber()
|
||||
if port_number >= 16:
|
||||
port_number *= 4
|
||||
port_number *= 8
|
||||
ports_dict[(port.adapterNumber() * 16) + port_number] = port
|
||||
elif port.portNumber()is not None:
|
||||
ports_dict[port.portNumber()] = port
|
||||
@@ -376,6 +337,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
# show a contextual menu for the user to choose a port
|
||||
for port in ports:
|
||||
port_object = ports_dict[port]
|
||||
log.debug("Node '{}' Port {} Type {}".format(self.node(), port_object.name(), type(port_object.name())))
|
||||
if port in unavailable_ports:
|
||||
# this port cannot be chosen by the user (grayed out)
|
||||
action = menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port_object.name())
|
||||
@@ -414,19 +376,19 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
# dynamically change the renderer when this node item is selected/unselected.
|
||||
if change == QtSvg.QGraphicsSvgItem.ItemSelectedChange:
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if value:
|
||||
self.setSharedRenderer(self._hover_renderer)
|
||||
self.graphicsEffect().setEnabled(True)
|
||||
else:
|
||||
self.setSharedRenderer(self._default_renderer)
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
# adjust link item positions when this node is moving or has changed.
|
||||
if change == QtSvg.QGraphicsSvgItem.ItemPositionChange or change == QtSvg.QGraphicsSvgItem.ItemPositionHasChanged:
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange or change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
|
||||
self.setUnsavedState()
|
||||
for link in self._links:
|
||||
link.adjust()
|
||||
|
||||
return QtGui.QGraphicsItem.itemChange(self, change, value)
|
||||
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -439,8 +401,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
# don't show the selection rectangle
|
||||
if not self._settings["draw_rectangle_selected_item"]:
|
||||
option.state = QtGui.QStyle.State_None
|
||||
QtSvg.QGraphicsSvgItem.paint(self, painter, option, widget)
|
||||
option.state = QtWidgets.QStyle.State_None
|
||||
super().paint(painter, option, widget)
|
||||
|
||||
if not self._initialized or self.show_layer:
|
||||
brect = self.boundingRect()
|
||||
@@ -464,7 +426,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtSvg.QGraphicsSvgItem.setZValue(self, value)
|
||||
super().setZValue(value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
@@ -488,13 +450,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
self.setCustomToolTip()
|
||||
# dynamically change the renderer when this node item is hovered.
|
||||
if not self.isSelected():
|
||||
self.setSharedRenderer(self._hover_renderer)
|
||||
# effect = QtGui.QGraphicsColorizeEffect()
|
||||
# effect.setColor(QtGui.QColor("black"))
|
||||
# effect.setStrength(0.8)
|
||||
# self.setGraphicsEffect(effect)
|
||||
self.graphicsEffect().setEnabled(True)
|
||||
|
||||
def hoverLeaveEvent(self, event):
|
||||
"""
|
||||
@@ -503,7 +460,5 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
# dynamically change the renderer back to the default when this node item is not hovered anymore.
|
||||
if not self.isSelected():
|
||||
self.setSharedRenderer(self._default_renderer)
|
||||
# self.graphicsEffect().setEnabled(False)
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
@@ -19,11 +19,10 @@
|
||||
Graphical representation of a note on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
|
||||
|
||||
class NoteItem(QtGui.QGraphicsTextItem):
|
||||
|
||||
class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
"""
|
||||
Text note for the QGraphicsView.
|
||||
|
||||
@@ -34,9 +33,10 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
QtGui.QGraphicsTextItem.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
|
||||
main_window = MainWindow.instance()
|
||||
view_settings = main_window.uiGraphicsView.settings()
|
||||
qt_font = QtGui.QFont()
|
||||
@@ -59,6 +59,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
|
||||
Topology.instance().removeNote(self)
|
||||
|
||||
def editable(self):
|
||||
@@ -101,7 +102,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
else:
|
||||
QtGui.QGraphicsTextItem.keyPressEvent(self, event)
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def editText(self):
|
||||
"""
|
||||
@@ -132,7 +133,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
:param event: QFocusEvent instance
|
||||
"""
|
||||
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsFocusable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False)
|
||||
cursor = self.textCursor()
|
||||
if cursor.hasSelection():
|
||||
cursor.clearSelection()
|
||||
@@ -142,7 +143,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
# delete the note if empty
|
||||
self.delete()
|
||||
return
|
||||
return QtGui.QGraphicsTextItem.focusOutEvent(self, event)
|
||||
return super().focusOutEvent(event)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -153,7 +154,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsTextItem.paint(self, painter, option, widget)
|
||||
super().paint(painter, option, widget)
|
||||
|
||||
if self.show_layer is False or self.parentItem():
|
||||
return
|
||||
@@ -178,7 +179,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsTextItem.setZValue(self, value)
|
||||
super().setZValue(value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
@@ -198,7 +199,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
"y": self.y()}
|
||||
|
||||
note_info["font"] = self.font().toString()
|
||||
note_info["color"] = self.defaultTextColor().name()
|
||||
note_info["color"] = self.defaultTextColor().name(QtGui.QColor.HexArgb)
|
||||
if self.rotation() != 0:
|
||||
note_info["rotation"] = self.rotation()
|
||||
if self.zValue() != 2:
|
||||
@@ -235,7 +236,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
if color:
|
||||
self.setDefaultTextColor(QtGui.QColor(color))
|
||||
if rotation is not None:
|
||||
self.setRotation(rotation)
|
||||
self.setRotation(float(rotation))
|
||||
if z is not None:
|
||||
self.setZValue(z)
|
||||
|
||||
|
||||
47
gns3/items/pixmap_image_item.py
Normal file
47
gns3/items/pixmap_image_item.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a Pixmap image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from .image_item import ImageItem
|
||||
|
||||
|
||||
class PixmapImageItem(ImageItem, QtWidgets.QGraphicsPixmapItem):
|
||||
|
||||
"""
|
||||
Class to insert an pixmap image on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, pixmap, image_path, pos=None):
|
||||
|
||||
QtWidgets.QGraphicsPixmapItem.__init__(self, pixmap)
|
||||
ImageItem.__init__(self, image_path, pos)
|
||||
self.setTransformationMode(QtCore.Qt.SmoothTransformation)
|
||||
|
||||
def duplicate(self):
|
||||
"""
|
||||
Duplicates this image item.
|
||||
|
||||
:return: PixmapImageItem instance
|
||||
"""
|
||||
|
||||
image_item = PixmapImageItem(self.pixmap(), self._image_path, QtCore.QPointF(self.x() + 20, self.y() + 20))
|
||||
image_item.setZValue(self.zValue())
|
||||
return image_item
|
||||
63
gns3/items/pixmap_node_item.py
Normal file
63
gns3/items/pixmap_node_item.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a pixmap node on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtGui, QtWidgets
|
||||
from .node_item import NodeItem
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PixmapNodeItem(NodeItem, QtWidgets.QGraphicsPixmapItem):
|
||||
|
||||
"""
|
||||
Pixmap node for the scene.
|
||||
|
||||
:param node: Node instance
|
||||
:param pixmap_symbol: symbol for the node representation on the scene
|
||||
"""
|
||||
|
||||
def __init__(self, node, pixmap_symbol_path):
|
||||
|
||||
QtWidgets.QGraphicsPixmapItem.__init__(self)
|
||||
NodeItem.__init__(self, node)
|
||||
|
||||
self._pixmap_symbol_path = pixmap_symbol_path
|
||||
pixmap = QtGui.QPixmap(pixmap_symbol_path)
|
||||
self.setPixmap(pixmap)
|
||||
|
||||
def setPixmapSymbolPath(self, path):
|
||||
"""
|
||||
Sets the pixmap path
|
||||
|
||||
:param path: path to the Pixmap file.
|
||||
"""
|
||||
|
||||
self._pixmap_symbol_path = path
|
||||
|
||||
def pixmapSymbolPath(self):
|
||||
"""
|
||||
Returns the pixmap path
|
||||
|
||||
:returns: path to the Pixmap file.
|
||||
"""
|
||||
|
||||
return self._pixmap_symbol_path
|
||||
@@ -19,11 +19,11 @@
|
||||
Graphical representation of a rectangle on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .shape_item import ShapeItem
|
||||
|
||||
|
||||
class RectangleItem(ShapeItem, QtGui.QGraphicsRectItem):
|
||||
class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
|
||||
"""
|
||||
Class to draw a rectangle on the scene.
|
||||
@@ -31,8 +31,8 @@ class RectangleItem(ShapeItem, QtGui.QGraphicsRectItem):
|
||||
|
||||
def __init__(self, pos=None, width=200, height=100):
|
||||
|
||||
QtGui.QGraphicsRectItem.__init__(self, 0, 0, width, height)
|
||||
ShapeItem.__init__(self)
|
||||
super().__init__()
|
||||
self.setRect(0, 0, width, height)
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.setPen(pen)
|
||||
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
|
||||
@@ -58,7 +58,7 @@ class RectangleItem(ShapeItem, QtGui.QGraphicsRectItem):
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsRectItem.paint(self, painter, option, widget)
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def duplicate(self):
|
||||
|
||||
@@ -20,7 +20,7 @@ Graphical representation of a Serial link on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import math
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .link_item import LinkItem
|
||||
from .note_item import NoteItem
|
||||
from ..ports.port import Port
|
||||
@@ -42,7 +42,7 @@ class SerialLinkItem(LinkItem):
|
||||
|
||||
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
|
||||
|
||||
LinkItem.__init__(self, source_item, source_port, destination_item, destination_port, link, adding_flag, multilink)
|
||||
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag, multilink)
|
||||
|
||||
def adjust(self):
|
||||
"""
|
||||
@@ -89,7 +89,7 @@ class SerialLinkItem(LinkItem):
|
||||
:returns: QPainterPath instance
|
||||
"""
|
||||
|
||||
path = QtGui.QGraphicsPathItem.shape(self)
|
||||
path = QtWidgets.QGraphicsPathItem.shape(self)
|
||||
offset = self._point_size / 2
|
||||
point = self.source
|
||||
path.addEllipse(point.x() - offset, point.y() - offset, self._point_size, self._point_size)
|
||||
@@ -106,7 +106,7 @@ class SerialLinkItem(LinkItem):
|
||||
:param widget: QWidget instance.
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
|
||||
if not self._adding_flag and self._settings["draw_link_status_points"]:
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
Base class for shape items (Rectangle, ellipse etc.).
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class ShapeItem:
|
||||
@@ -30,10 +30,10 @@ class ShapeItem:
|
||||
|
||||
show_layer = False
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, **kws):
|
||||
|
||||
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemIsFocusable | QtGui.QGraphicsItem.ItemIsSelectable)
|
||||
self.setAcceptsHoverEvents(True)
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable)
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._border = 5
|
||||
self._edge = None
|
||||
|
||||
@@ -58,7 +58,7 @@ class ShapeItem:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
else:
|
||||
QtGui.QGraphicsItem.keyPressEvent(self, event)
|
||||
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""
|
||||
@@ -69,22 +69,22 @@ class ShapeItem:
|
||||
|
||||
self.update()
|
||||
if event.pos().x() > (self.rect().right() - self._border):
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "right"
|
||||
|
||||
elif event.pos().x() < (self.rect().left() + self._border):
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "left"
|
||||
|
||||
elif event.pos().y() < (self.rect().top() + self._border):
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "top"
|
||||
|
||||
elif event.pos().y() > (self.rect().bottom() - self._border):
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "bottom"
|
||||
|
||||
QtGui.QGraphicsItem.mousePressEvent(self, event)
|
||||
QtWidgets.QGraphicsItem.mousePressEvent(self, event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""
|
||||
@@ -94,9 +94,9 @@ class ShapeItem:
|
||||
"""
|
||||
|
||||
self.update()
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
|
||||
self._edge = None
|
||||
QtGui.QGraphicsItem.mouseReleaseEvent(self, event)
|
||||
QtWidgets.QGraphicsItem.mouseReleaseEvent(self, event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""
|
||||
@@ -145,7 +145,7 @@ class ShapeItem:
|
||||
self.setPos(scenePos.x(), self.y())
|
||||
self._edge = "left"
|
||||
|
||||
QtGui.QGraphicsItem.mouseMoveEvent(self, event)
|
||||
QtWidgets.QGraphicsItem.mouseMoveEvent(self, event)
|
||||
|
||||
def hoverMoveEvent(self, event):
|
||||
"""
|
||||
@@ -208,7 +208,7 @@ class ShapeItem:
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsItem.setZValue(self, value)
|
||||
QtWidgets.QGraphicsItem.setZValue(self, value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
|
||||
47
gns3/items/svg_image_item.py
Normal file
47
gns3/items/svg_image_item.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a SVG image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtSvg
|
||||
from .image_item import ImageItem
|
||||
|
||||
|
||||
class SvgImageItem(ImageItem, QtSvg.QGraphicsSvgItem):
|
||||
|
||||
"""
|
||||
Class to insert a SVG image on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, renderer, image_path, pos=None):
|
||||
|
||||
QtSvg.QGraphicsSvgItem.__init__(self)
|
||||
ImageItem.__init__(self, image_path, pos)
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
def duplicate(self):
|
||||
"""
|
||||
Duplicates this image item.
|
||||
|
||||
:return: SvgImageItem instance
|
||||
"""
|
||||
|
||||
image_item = SvgImageItem(self.renderer(), self._image_path, QtCore.QPointF(self.x() + 20, self.y() + 20))
|
||||
image_item.setZValue(self.zValue())
|
||||
return image_item
|
||||
50
gns3/items/svg_node_item.py
Normal file
50
gns3/items/svg_node_item.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a SVG node on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtSvg
|
||||
from .node_item import NodeItem
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SvgNodeItem(NodeItem, QtSvg.QGraphicsSvgItem):
|
||||
|
||||
"""
|
||||
SVG node for the scene.
|
||||
|
||||
:param node: Node instance
|
||||
:param symbol: symbol for the node representation on the scene
|
||||
"""
|
||||
|
||||
def __init__(self, node, symbol=None):
|
||||
|
||||
QtSvg.QGraphicsSvgItem.__init__(self)
|
||||
NodeItem.__init__(self, node)
|
||||
|
||||
# create renderer using symbols path/resource
|
||||
if symbol:
|
||||
renderer = QtSvg.QSvgRenderer(symbol)
|
||||
if symbol != node.defaultSymbol():
|
||||
renderer.setObjectName(symbol)
|
||||
else:
|
||||
renderer = QtSvg.QSvgRenderer(node.defaultSymbol())
|
||||
self.setSharedRenderer(renderer)
|
||||
114
gns3/link.py
114
gns3/link.py
@@ -22,6 +22,7 @@ Manages and stores everything needed for a connection between 2 devices.
|
||||
|
||||
from .qt import QtCore
|
||||
from .nios.nio_udp import NIOUDP
|
||||
from .nios.nio_vmnet import NIOVMNET
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -48,7 +49,7 @@ class Link(QtCore.QObject):
|
||||
|
||||
def __init__(self, source_node, source_port, destination_node, destination_port):
|
||||
|
||||
super(Link, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
log.info("adding link from {} {} to {} {}".format(source_node.name(),
|
||||
source_port.name(),
|
||||
@@ -72,30 +73,33 @@ class Link(QtCore.QObject):
|
||||
self._stub = True
|
||||
else:
|
||||
self._stub = False
|
||||
|
||||
# we must request UDP information if the NIO is a NIO UDP and before
|
||||
# it can be created.
|
||||
if not self._stub:
|
||||
|
||||
# connect signals used when a NIO has been created by a node
|
||||
# and this NIO need to be attached to a port connected to this link
|
||||
source_node.nio_signal.connect(self.newNIOSlot)
|
||||
destination_node.nio_signal.connect(self.newNIOSlot)
|
||||
|
||||
# currently, we support only NIO_UDP for normal connections (non-stub).
|
||||
if not source_port.defaultNio() == NIOUDP:
|
||||
# currently, we support only NIO_UDP and NIO_VMNET for normal connections (non-stub).
|
||||
if source_port.defaultNio() == NIOUDP:
|
||||
assert destination_port.defaultNio() == NIOUDP
|
||||
self._source_udp = None
|
||||
self._destination_udp = None
|
||||
|
||||
# connect signals used to receive a UDP port and host allocated by a node
|
||||
source_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
|
||||
destination_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
|
||||
|
||||
# request the UDP info for each node
|
||||
source_node.allocateUDPPort(self._source_port.id())
|
||||
destination_node.allocateUDPPort(self._destination_port.id())
|
||||
elif source_port.defaultNio() == NIOVMNET:
|
||||
assert destination_port.defaultNio() == NIOVMNET
|
||||
source_node.allocate_vmnet_nio_signal.connect(self.VMnetInterfaceAllocatedSlot)
|
||||
source_node.allocateVMnetInterface(self._source_port.id())
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
self._source_udp = None
|
||||
self._destination_udp = None
|
||||
|
||||
# connect signals used to receive a UDP port and host allocated by a node
|
||||
source_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
|
||||
destination_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
|
||||
|
||||
# request the UDP info for each node
|
||||
source_node.allocateUDPPort(self._source_port.id())
|
||||
destination_node.allocateUDPPort(self._destination_port.id())
|
||||
else:
|
||||
# handle stub connections (to a cloud for instance).
|
||||
if not source_port.isStub() and destination_port.isStub():
|
||||
@@ -137,10 +141,12 @@ class Link(QtCore.QObject):
|
||||
self._destination_port.name()))
|
||||
|
||||
# delete the NIOs on both source and destination nodes
|
||||
self._source_node.deleteNIO(self._source_port)
|
||||
if self._source_port.nio():
|
||||
self._source_node.deleteNIO(self._source_port)
|
||||
self._source_port.setFree()
|
||||
self._source_node.updated_signal.emit()
|
||||
self._destination_node.deleteNIO(self._destination_port)
|
||||
if self._destination_port.nio():
|
||||
self._destination_node.deleteNIO(self._destination_port)
|
||||
self._destination_port.setFree()
|
||||
self._destination_node.updated_signal.emit()
|
||||
|
||||
@@ -201,10 +207,9 @@ class Link(QtCore.QObject):
|
||||
:param port_id: port identifier
|
||||
:param lport: local UDP port
|
||||
"""
|
||||
|
||||
# check that the node is connected to this link as a source
|
||||
if node_id == self._source_node.id() and port_id == self._source_port.id():
|
||||
laddr = self._source_node.server().host
|
||||
laddr = self._source_node.server().host()
|
||||
self._source_udp = (lport, laddr)
|
||||
# disconnect the signal has we don't expect new source UDP info for this link.
|
||||
self._source_node.allocate_udp_nio_signal.disconnect(self.UDPPortAllocatedSlot)
|
||||
@@ -215,7 +220,7 @@ class Link(QtCore.QObject):
|
||||
|
||||
# check that the node is connected to this link as a destination
|
||||
elif node_id == self._destination_node.id() and port_id == self._destination_port.id():
|
||||
laddr = self._destination_node.server().host
|
||||
laddr = self._destination_node.server().host()
|
||||
self._destination_udp = (lport, laddr)
|
||||
# disconnect the signal has we don't expect new source UDP info for this link.
|
||||
self._destination_node.allocate_udp_nio_signal.disconnect(self.UDPPortAllocatedSlot)
|
||||
@@ -225,7 +230,6 @@ class Link(QtCore.QObject):
|
||||
laddr))
|
||||
|
||||
if self._source_udp and self._destination_udp:
|
||||
|
||||
# we got UDP info from both source and destination nodes
|
||||
# meaning we can proceed with the creation of UDP NIOs
|
||||
lport, laddr = self._source_udp
|
||||
@@ -245,6 +249,30 @@ class Link(QtCore.QObject):
|
||||
self._destination_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._destination_node.addNIO(self._destination_port, self._destination_nio)
|
||||
|
||||
def VMnetInterfaceAllocatedSlot(self, node_id, port_id, vmnet):
|
||||
"""
|
||||
Slot to receive events from Node instances
|
||||
when a VMnet interface has been allocated in order to create a NIO VMNET.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param port_id: port identifier
|
||||
:param vmnet: vmnet interface name
|
||||
"""
|
||||
|
||||
# check that the node is connected to this link as a source
|
||||
# only the source is used to request the server for a vmnet interface
|
||||
# and then allocate a NIO VMNET to both the source and destination
|
||||
if node_id == self._source_node.id() and port_id == self._source_port.id():
|
||||
self._source_node.allocate_vmnet_nio_signal.disconnect(self.VMnetInterfaceAllocatedSlot)
|
||||
self._source_nio = NIOVMNET(vmnet)
|
||||
self._destination_nio = NIOVMNET(vmnet)
|
||||
|
||||
# add the VMnet NIOs to the nodes
|
||||
self._source_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._source_node.addNIO(self._source_port, self._source_nio)
|
||||
self._destination_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._destination_node.addNIO(self._destination_port, self._destination_nio)
|
||||
|
||||
def newNIOSlot(self, node_id, port_id):
|
||||
"""
|
||||
Slot to receive events from Node instances
|
||||
@@ -255,6 +283,10 @@ class Link(QtCore.QObject):
|
||||
:param port_id: port identifier
|
||||
"""
|
||||
|
||||
# in very rare cases link is already deleted
|
||||
if self is None:
|
||||
return
|
||||
|
||||
# check that the node is connected to this link as a source
|
||||
if node_id == self._source_node.id() and port_id == self._source_port.id():
|
||||
self._source_nio_active = True
|
||||
@@ -336,29 +368,19 @@ class Link(QtCore.QObject):
|
||||
"""
|
||||
|
||||
if not self._stub:
|
||||
if self._source_node.id() != node_id:
|
||||
try:
|
||||
# the destination node has canceled its NIO allocation
|
||||
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
except TypeError:
|
||||
# ignore TypeError: 'method' object is not connected
|
||||
pass
|
||||
try:
|
||||
# the destination node has canceled its NIO allocation
|
||||
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
except TypeError:
|
||||
# ignore TypeError: 'method' object is not connected
|
||||
pass
|
||||
|
||||
self._source_node.deleteNIO(self._source_port)
|
||||
self._source_port.setFree()
|
||||
self._source_node.updated_signal.emit()
|
||||
|
||||
elif self._destination_node.id() != node_id:
|
||||
try:
|
||||
# the source node has canceled its NIO allocation
|
||||
self._source_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
except TypeError:
|
||||
# ignore TypeError: 'method' object is not connected
|
||||
pass
|
||||
|
||||
self._destination_node.deleteNIO(self._destination_port)
|
||||
self._destination_port.setFree()
|
||||
self._destination_node.updated_signal.emit()
|
||||
try:
|
||||
# the source node has canceled its NIO allocation
|
||||
self._source_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
except TypeError:
|
||||
# ignore TypeError: 'method' object is not connected
|
||||
pass
|
||||
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
@@ -372,6 +394,7 @@ class Link(QtCore.QObject):
|
||||
|
||||
self._source_nio_active = False
|
||||
self._destination_nio_active = False
|
||||
self.deleteLink()
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
@@ -385,5 +408,4 @@ class Link(QtCore.QObject):
|
||||
"source_node_id": self._source_node.id(),
|
||||
"source_port_id": self._source_port.id(),
|
||||
"destination_node_id": self._destination_node.id(),
|
||||
"destination_port_id": self._destination_port.id(),
|
||||
}
|
||||
"destination_port_id": self._destination_port.id()}
|
||||
|
||||
@@ -18,56 +18,60 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import copy
|
||||
|
||||
import psutil
|
||||
|
||||
from .qt import QtCore
|
||||
from .version import __version__
|
||||
from .utils import parse_version
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalConfig:
|
||||
class LocalConfig(QtCore.QObject):
|
||||
|
||||
"""
|
||||
Handles the local GUI settings.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
config_changed_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, config_file=None):
|
||||
|
||||
super().__init__()
|
||||
self._settings = {}
|
||||
self._last_config_changed = None
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_gui.ini"
|
||||
else:
|
||||
filename = "gns3_gui.conf"
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
appname = "gns3.net"
|
||||
else:
|
||||
appname = "GNS3"
|
||||
self._migrateOldConfigPath()
|
||||
|
||||
appname = "GNS3"
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
|
||||
# On windows, the system wide configuration file location is %COMMON_APPDATA%/GNS3/gns3_gui.conf
|
||||
common_appdata = os.path.expandvars("%COMMON_APPDATA%")
|
||||
system_wide_config_file = os.path.join(common_appdata, appname, filename)
|
||||
|
||||
# On windows, the user specific configuration file location is %APPDATA%/GNS3/gns3_gui.conf
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
self._config_file = os.path.join(appdata, appname, filename)
|
||||
|
||||
else:
|
||||
|
||||
# On UNIX-like platforms, the system wide configuration file location is /etc/xdg/GNS3/gns3_gui.conf
|
||||
system_wide_config_file = os.path.join("/etc/xdg", appname, filename)
|
||||
|
||||
# On UNIX-like platforms, the user specific configuration file location is /etc/xdg/GNS3/gns3_gui.conf
|
||||
home = os.path.expanduser("~")
|
||||
self._config_file = os.path.join(home, ".config", appname, filename)
|
||||
if config_file:
|
||||
self._config_file = config_file
|
||||
else:
|
||||
self._config_file = os.path.join(LocalConfig.configDirectory(), filename)
|
||||
|
||||
# First load system wide settings
|
||||
if os.path.exists(system_wide_config_file):
|
||||
self._settings = self._readConfig(system_wide_config_file)
|
||||
if not self._settings:
|
||||
log.warning("No system wide settings loaded from {}".format(system_wide_config_file))
|
||||
self._readConfig(system_wide_config_file)
|
||||
|
||||
config_file_in_cwd = os.path.join(os.getcwd(), filename)
|
||||
if os.path.exists(config_file_in_cwd):
|
||||
@@ -77,7 +81,7 @@ class LocalConfig:
|
||||
try:
|
||||
# create the config file if it doesn't exist
|
||||
os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
|
||||
with open(self._config_file, "w") as f:
|
||||
with open(self._config_file, "w", encoding="utf-8") as f:
|
||||
json.dump({"version": __version__, "type": "settings"}, f)
|
||||
except OSError as e:
|
||||
log.error("Could not create the config file {}: {}".format(self._config_file, e))
|
||||
@@ -85,19 +89,83 @@ class LocalConfig:
|
||||
user_settings = self._readConfig(self._config_file)
|
||||
# overwrite system wide settings with user specific ones
|
||||
self._settings.update(user_settings)
|
||||
self._migrateOldConfig()
|
||||
self._writeConfig()
|
||||
|
||||
@staticmethod
|
||||
def configDirectory():
|
||||
"""
|
||||
Get the configuration directory
|
||||
"""
|
||||
if sys.platform.startswith("win"):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
path = os.path.join(appdata, "GNS3")
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
path = os.path.join(home, ".config", "GNS3")
|
||||
return os.path.normpath(path)
|
||||
|
||||
def _migrateOldConfigPath(self):
|
||||
"""
|
||||
Migrate pre 1.4 config path
|
||||
"""
|
||||
|
||||
# In < 1.4 on Mac the config was in a gns3.net directory
|
||||
# We have move to same location as Linux
|
||||
if sys.platform.startswith("darwin"):
|
||||
old_path = os.path.join(os.path.expanduser("~"), ".config", "gns3.net")
|
||||
new_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3")
|
||||
if os.path.exists(old_path) and not os.path.exists(new_path):
|
||||
try:
|
||||
shutil.copytree(old_path, new_path)
|
||||
except OSError as e:
|
||||
print("Can't copy the old config: %s", str(e))
|
||||
|
||||
def _migrateOldConfig(self):
|
||||
"""
|
||||
Migrate pre 1.4 config
|
||||
"""
|
||||
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.0alpha1"):
|
||||
|
||||
servers = self._settings.get("Servers", {})
|
||||
|
||||
if "LocalServer" in self._settings:
|
||||
servers["local_server"] = copy.copy(self._settings["LocalServer"])
|
||||
|
||||
# We migrate the server binary for OSX due to the change from py2app to CX freeze
|
||||
if servers["local_server"]["path"] == "/Applications/GNS3.app/Contents/Resources/server/Contents/MacOS/gns3server":
|
||||
servers["local_server"]["path"] = "/Applications/GNS3.app/Contents/MacOS/gns3server"
|
||||
|
||||
if "RemoteServers" in self._settings:
|
||||
servers["remote_servers"] = copy.copy(self._settings["RemoteServers"])
|
||||
|
||||
self._settings["Servers"] = servers
|
||||
|
||||
if "GUI" in self._settings:
|
||||
main_window = self._settings.get("MainWindow", {})
|
||||
main_window["hide_getting_started_dialog"] = self._settings["GUI"].get("hide_getting_started_dialog", False)
|
||||
self._settings["MainWindow"] = main_window
|
||||
|
||||
def _readConfig(self, config_path):
|
||||
"""
|
||||
Read the configuration file.
|
||||
"""
|
||||
|
||||
log.info("Load config from %s", config_path)
|
||||
try:
|
||||
with open(config_path, "r") as f:
|
||||
return json.load(f)
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
self._last_config_changed = os.stat(config_path).st_mtime
|
||||
config = json.load(f)
|
||||
self._settings.update(config)
|
||||
except (ValueError, OSError) as e:
|
||||
log.error("Could not read the config file {}: {}".format(self._config_file, e))
|
||||
|
||||
# Update already loaded section
|
||||
for section in self._settings.keys():
|
||||
if isinstance(self._settings[section], dict):
|
||||
self.loadSectionSettings(section, self._settings[section])
|
||||
|
||||
return dict()
|
||||
|
||||
def _writeConfig(self):
|
||||
@@ -105,12 +173,27 @@ class LocalConfig:
|
||||
Write the configuration file.
|
||||
"""
|
||||
|
||||
self._settings["version"] = __version__
|
||||
try:
|
||||
with open(self._config_file, "w") as f:
|
||||
temporary = os.path.join(os.path.dirname(self._config_file), "gns3_gui.tmp")
|
||||
with open(temporary, "w", encoding="utf-8") as f:
|
||||
json.dump(self._settings, f, sort_keys=True, indent=4)
|
||||
shutil.move(temporary, self._config_file)
|
||||
log.info("Configuration save to %s", self._config_file)
|
||||
self._last_config_changed = os.stat(self._config_file).st_mtime
|
||||
except (ValueError, OSError) as e:
|
||||
log.error("Could not write the config file {}: {}".format(self._config_file, e))
|
||||
|
||||
def checkConfigChanged(self):
|
||||
|
||||
try:
|
||||
if self._last_config_changed and self._last_config_changed < os.stat(self._config_file).st_mtime:
|
||||
log.info("Client config has changed, reloading it...")
|
||||
self._readConfig(self._config_file)
|
||||
self.config_changed_signal.emit()
|
||||
except OSError as e:
|
||||
log.error("Error when checking for changes {}: {}".format(self._config_file, str(e)))
|
||||
|
||||
def configFilePath(self):
|
||||
"""
|
||||
Returns the config file path.
|
||||
@@ -127,8 +210,8 @@ class LocalConfig:
|
||||
:returns: path to the config file.
|
||||
"""
|
||||
|
||||
self._settings = self._readConfig(self._config_file)
|
||||
self._config_file = config_file
|
||||
self._readConfig(self._config_file)
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
@@ -137,7 +220,7 @@ class LocalConfig:
|
||||
:returns: settings (dict)
|
||||
"""
|
||||
|
||||
return self._readConfig(self._config_file)
|
||||
return copy.deepcopy(self._settings)
|
||||
|
||||
def setSettings(self, settings):
|
||||
"""
|
||||
@@ -146,8 +229,9 @@ class LocalConfig:
|
||||
:param settings: settings to save (dict)
|
||||
"""
|
||||
|
||||
self._settings.update(settings)
|
||||
self._writeConfig()
|
||||
if self._settings != settings:
|
||||
self._settings.update(settings)
|
||||
self._writeConfig()
|
||||
|
||||
def loadSectionSettings(self, section, default_settings):
|
||||
"""
|
||||
@@ -159,16 +243,32 @@ class LocalConfig:
|
||||
"""
|
||||
|
||||
settings = self.settings().get(section, dict())
|
||||
changed = False
|
||||
|
||||
# use default values for missing settings
|
||||
for name, value in default_settings.items():
|
||||
if name not in settings:
|
||||
settings[name] = value
|
||||
def _copySettings(local, default):
|
||||
"""
|
||||
Copy only existing settings, ignore the other.
|
||||
Add default values if require.
|
||||
"""
|
||||
nonlocal changed
|
||||
|
||||
if section not in self._settings:
|
||||
self._settings[section] = {}
|
||||
self._settings[section].update(settings)
|
||||
return settings
|
||||
# use default values for missing settings
|
||||
for name, value in default.items():
|
||||
if name not in local:
|
||||
local[name] = value
|
||||
changed = True
|
||||
elif isinstance(value, dict):
|
||||
local[name] = _copySettings(local[name], default[name])
|
||||
return local
|
||||
|
||||
settings = _copySettings(settings, default_settings)
|
||||
self._settings[section] = settings
|
||||
|
||||
if changed:
|
||||
log.info("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
|
||||
self._writeConfig()
|
||||
|
||||
return copy.deepcopy(settings)
|
||||
|
||||
def saveSectionSettings(self, section, settings):
|
||||
"""
|
||||
@@ -180,17 +280,68 @@ class LocalConfig:
|
||||
|
||||
if section not in self._settings:
|
||||
self._settings[section] = {}
|
||||
self._settings[section].update(settings)
|
||||
self._writeConfig()
|
||||
|
||||
if self._settings[section] != settings:
|
||||
self._settings[section].update(copy.deepcopy(settings))
|
||||
log.info("Section %s has changed. Saving configuration", section)
|
||||
self._writeConfig()
|
||||
else:
|
||||
log.debug("Section %s has not changed. Skip saving configuration", section)
|
||||
|
||||
def experimental(self):
|
||||
"""
|
||||
:returns: Boolean. True if experimental features allowed
|
||||
"""
|
||||
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["experimental_features"]
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
def instance(config_file=None):
|
||||
"""
|
||||
Singleton to return only on instance of LocalConfig.
|
||||
|
||||
:returns: instance of LocalConfig
|
||||
"""
|
||||
|
||||
if not hasattr(LocalConfig, "_instance"):
|
||||
LocalConfig._instance = LocalConfig()
|
||||
if not hasattr(LocalConfig, "_instance") or LocalConfig._instance is None:
|
||||
LocalConfig._instance = LocalConfig(config_file=config_file)
|
||||
return LocalConfig._instance
|
||||
|
||||
@staticmethod
|
||||
def isMainGui():
|
||||
"""
|
||||
:returns: Return true if we are the main gui (first gui to start)
|
||||
"""
|
||||
|
||||
my_pid = os.getpid()
|
||||
pid_path = os.path.join(LocalConfig.configDirectory(), "gns3_gui.pid")
|
||||
|
||||
if os.path.exists(pid_path):
|
||||
try:
|
||||
with open(pid_path) as f:
|
||||
pid = int(f.read())
|
||||
if pid != my_pid:
|
||||
try:
|
||||
process = psutil.Process(pid=pid)
|
||||
ps_name = process.name()
|
||||
except (OSError, psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
else:
|
||||
if "gns3" in ps_name or "python" in ps_name:
|
||||
# Process run under the same user id
|
||||
if sys.platform.startswith("win") or process.uids()[0] == os.getuid():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
except (OSError, ValueError) as e:
|
||||
log.critical("Can't read pid file %s: %s", pid_path, str(e))
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(pid_path, 'w+') as f:
|
||||
f.write(str(my_pid))
|
||||
except OSError as e:
|
||||
log.critical("Can't write pid file %s: %s", pid_path, str(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
import os
|
||||
import sys
|
||||
import configparser
|
||||
from gns3.qt import QtCore
|
||||
|
||||
|
||||
import logging
|
||||
@@ -33,15 +32,24 @@ class LocalServerConfig:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._config = configparser.ConfigParser()
|
||||
appname = "GNS3"
|
||||
|
||||
self._config = configparser.RawConfigParser()
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_server.ini"
|
||||
else:
|
||||
filename = "gns3_server.conf"
|
||||
self._config_file = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), filename)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
self._config_file = os.path.join(appdata, appname, filename)
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
self._config_file = os.path.join(home, ".config", appname, filename)
|
||||
|
||||
try:
|
||||
# create the config file if it doesn't exist
|
||||
open(self._config_file, 'a').close()
|
||||
open(self._config_file, "a").close()
|
||||
except OSError as e:
|
||||
log.error("Could not create the local server configuration {}: {}".format(self._config_file, e))
|
||||
self.readConfig()
|
||||
@@ -52,8 +60,8 @@ class LocalServerConfig:
|
||||
"""
|
||||
|
||||
try:
|
||||
self._config.read(self._config_file)
|
||||
except (OSError, configparser.Error) as e:
|
||||
self._config.read(self._config_file, encoding="utf-8")
|
||||
except (OSError, configparser.Error, UnicodeEncodeError) as e:
|
||||
log.error("Could not read the local server configuration {}: {}".format(self._config_file, e))
|
||||
|
||||
def writeConfig(self):
|
||||
@@ -63,7 +71,7 @@ class LocalServerConfig:
|
||||
|
||||
try:
|
||||
log.debug("Write configuration file %s", self._config_file)
|
||||
with open(self._config_file, 'w') as fp:
|
||||
with open(self._config_file, "w", encoding="utf-8") as fp:
|
||||
self._config.write(fp)
|
||||
except (OSError, configparser.Error) as e:
|
||||
log.error("Could not write the local server configuration {}: {}".format(self._config_file, e))
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
class ColouredFormatter(logging.Formatter):
|
||||
@@ -34,7 +35,7 @@ class ColouredFormatter(logging.Formatter):
|
||||
|
||||
message = super().format(record)
|
||||
|
||||
if not colour:
|
||||
if not colour or sys.platform.startswith("win"):
|
||||
return message.replace("#RESET#", "")
|
||||
|
||||
level_no = record.levelno
|
||||
@@ -74,11 +75,14 @@ class ColouredStreamHandler(logging.StreamHandler):
|
||||
stream.write(msg)
|
||||
stream.write(self.terminator)
|
||||
self.flush()
|
||||
# On OSX when frozen flush raise a BrokenPipeError
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
def init_logger(level, quiet=False):
|
||||
def init_logger(level, logfile, quiet=False):
|
||||
if sys.platform.startswith("win"):
|
||||
stream_handler = logging.StreamHandler(sys.stdout)
|
||||
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
|
||||
@@ -86,5 +90,20 @@ def init_logger(level, quiet=False):
|
||||
stream_handler = ColouredStreamHandler(sys.stdout)
|
||||
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{")
|
||||
logging.basicConfig(level=level, handlers=[stream_handler])
|
||||
logging.getLogger().addHandler(stream_handler)
|
||||
log = logging.getLogger()
|
||||
log.addHandler(stream_handler)
|
||||
|
||||
try:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(logfile))
|
||||
except FileExistsError:
|
||||
pass
|
||||
handler = logging.FileHandler(logfile, "w")
|
||||
handler.formatter = logging.Formatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
|
||||
log.addHandler(handler)
|
||||
except OSError as e:
|
||||
log.warn("could not log to {}: {}".format(logfile, e))
|
||||
|
||||
log.info('Log level: {}'.format(logging.getLevelName(level)))
|
||||
|
||||
return logging.getLogger()
|
||||
|
||||
157
gns3/main.py
157
gns3/main.py
@@ -16,6 +16,19 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Try to install updates & restart application if an update is installed
|
||||
try:
|
||||
import gns3.update_manager
|
||||
if gns3.update_manager.UpdateManager().installDownloadedUpdates():
|
||||
print("Update installed restart the application")
|
||||
python = sys.executable
|
||||
os.execl(python, *sys.argv)
|
||||
except Exception as e:
|
||||
print("Fail update installation: {}".format(str(e)))
|
||||
|
||||
|
||||
# WARNING
|
||||
# Due to buggy user machines we choose to put this as the first loading modules
|
||||
@@ -27,28 +40,28 @@ from gns3.utils.get_resource import get_resource
|
||||
|
||||
|
||||
import datetime
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import time
|
||||
import locale
|
||||
import argparse
|
||||
import signal
|
||||
import re
|
||||
|
||||
try:
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, DEFAULT_BINDING
|
||||
except ImportError:
|
||||
raise SystemExit("Can't import Qt modules: Qt and/or PyQt is probably not installed correctly...")
|
||||
from gns3.main_window import MainWindow
|
||||
|
||||
from gns3.logger import init_logger
|
||||
from gns3.crash_report import CrashReport
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.application import Application
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
try:
|
||||
from gns3.qt import QtCore, QtGui, DEFAULT_BINDING
|
||||
except ImportError:
|
||||
raise RuntimeError("Can't import Qt modules: Qt and/or PyQt is probably not installed correctly...")
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.version import __version__
|
||||
|
||||
|
||||
@@ -96,15 +109,45 @@ def main():
|
||||
Entry point for GNS3 GUI.
|
||||
"""
|
||||
|
||||
# Sometimes (for example at first launch) the OSX app service launcher add
|
||||
# an extra argument starting with -psn_. We filter it
|
||||
if sys.platform.startswith("darwin"):
|
||||
sys.argv = [a for a in sys.argv if not a.startswith("-psn_")]
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("project", help="load a GNS3 project (.gns3)", metavar="path", nargs="?")
|
||||
parser.add_argument("--version", help="show the version", action="version", version=__version__)
|
||||
parser.add_argument("--debug", help="print out debug messages", action="store_true", default=False)
|
||||
parser.add_argument("--config", help="Configuration file")
|
||||
options = parser.parse_args()
|
||||
exception_file_path = "exceptions.log"
|
||||
|
||||
if options.project and hasattr(sys, "frozen"):
|
||||
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
|
||||
if options.config:
|
||||
LocalConfig.instance(config_file=options.config)
|
||||
else:
|
||||
LocalConfig.instance()
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
# We add to the path where the OS search executable our binary location starting by GNS3
|
||||
# packaged binary
|
||||
frozen_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||
if sys.platform.startswith("darwin"):
|
||||
frozen_dirs = [
|
||||
frozen_dir,
|
||||
os.path.normpath(os.path.join(frozen_dir, '..', 'Resources'))
|
||||
]
|
||||
elif sys.platform.startswith("win"):
|
||||
frozen_dirs = [
|
||||
frozen_dir,
|
||||
os.path.normpath(os.path.join(frozen_dir, 'dynamips')),
|
||||
os.path.normpath(os.path.join(frozen_dir, 'vpcs'))
|
||||
]
|
||||
|
||||
os.environ["PATH"] = os.pathsep.join(frozen_dirs) + os.pathsep + os.environ.get("PATH", "")
|
||||
|
||||
if options.project:
|
||||
options.project = os.path.abspath(options.project)
|
||||
os.chdir(frozen_dir)
|
||||
|
||||
def exceptionHook(exception, value, tb):
|
||||
|
||||
@@ -113,11 +156,11 @@ def main():
|
||||
|
||||
lines = traceback.format_exception(exception, value, tb)
|
||||
print("****** Exception detected, traceback information saved in {} ******".format(exception_file_path))
|
||||
print("\nPLEASE REPORT ON https://community.gns3.com/community/support/bug\n")
|
||||
print("\nPLEASE REPORT ON https://www.gns3.com\n")
|
||||
print("".join(lines))
|
||||
try:
|
||||
curdate = time.strftime("%d %b %Y %H:%M:%S")
|
||||
logfile = open(exception_file_path, "a")
|
||||
logfile = open(exception_file_path, "a", encoding="utf-8")
|
||||
logfile.write("=== GNS3 {} traceback on {} ===\n".format(__version__, curdate))
|
||||
logfile.write("".join(lines))
|
||||
logfile.close()
|
||||
@@ -128,7 +171,11 @@ def main():
|
||||
# if stdout is not a tty (redirected to the console view),
|
||||
# then print the exception on stderr too.
|
||||
print("".join(lines), file=sys.stderr)
|
||||
CrashReport.instance().captureException(exception, value, tb)
|
||||
|
||||
if exception is MemoryError:
|
||||
print("YOUR SYSTEM IS OUT OF MEMORY!")
|
||||
else:
|
||||
CrashReport.instance().captureException(exception, value, tb)
|
||||
|
||||
# catch exceptions to write them in a file
|
||||
sys.excepthook = exceptionHook
|
||||
@@ -137,24 +184,19 @@ def main():
|
||||
print("GNS3 GUI version {}".format(__version__))
|
||||
print("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
|
||||
|
||||
# we only support Python 2 version >= 2.7 and Python 3 version >= 3.3
|
||||
if sys.version_info < (2, 7):
|
||||
raise RuntimeError("Python 2.7 or higher is required")
|
||||
elif sys.version_info[0] == 3 and sys.version_info < (3, 3):
|
||||
raise RuntimeError("Python 3.3 or higher is required")
|
||||
# we only support Python 3 version >= 3.4
|
||||
if sys.version_info < (3, 4):
|
||||
raise SystemExit("Python 3.4 or higher is required")
|
||||
|
||||
def version(version_string):
|
||||
return [int(i) for i in version_string.split('.')]
|
||||
|
||||
if version(QtCore.QT_VERSION_STR) < version("4.6"):
|
||||
raise RuntimeError("Requirement is Qt version 4.6 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
return [int(i) for i in re.split(r'[^0-9]', version_string)]
|
||||
|
||||
# 4.8.3 because of QSettings (http://pyqt.sourceforge.net/Docs/PyQt4/pyqt_qsettings.html)
|
||||
if DEFAULT_BINDING == "PyQt" and version(QtCore.BINDING_VERSION_STR) < version("4.8.3"):
|
||||
raise RuntimeError("Requirement is PyQt version 4.8.3 or higher, got version {}".format(QtCore.BINDING_VERSION_STR))
|
||||
if DEFAULT_BINDING == "PyQt4" and version(QtCore.BINDING_VERSION_STR) < version("4.8.3"):
|
||||
raise SystemExit("Requirement is PyQt version 4.8.3 or higher, got version {}".format(QtCore.BINDING_VERSION_STR))
|
||||
|
||||
if DEFAULT_BINDING == "PySide" and version(QtCore.BINDING_VERSION_STR) < version("1.0"):
|
||||
raise RuntimeError("Requirement is PySide version 1.0 or higher, got version {}".format(QtCore.BINDING_VERSION_STR))
|
||||
if DEFAULT_BINDING == "PyQt5" and version(QtCore.BINDING_VERSION_STR) < version("5.0.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.0.0 or higher, got version {}".format(QtCore.BINDING_VERSION_STR))
|
||||
|
||||
# check for the correct locale
|
||||
# (UNIX/Linux only)
|
||||
@@ -176,7 +218,7 @@ def main():
|
||||
import win32con
|
||||
import win32gui
|
||||
except ImportError:
|
||||
raise RuntimeError("Python for Windows extensions must be installed.")
|
||||
raise SystemExit("Python for Windows extensions must be installed.")
|
||||
|
||||
if not options.debug:
|
||||
try:
|
||||
@@ -186,45 +228,48 @@ def main():
|
||||
except win32console.error as e:
|
||||
print("warning: could not allocate console: {}".format(e))
|
||||
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
global app
|
||||
app = Application(sys.argv)
|
||||
|
||||
# this info is necessary for QSettings
|
||||
app.setOrganizationName("GNS3")
|
||||
app.setOrganizationDomain("gns3.net")
|
||||
app.setApplicationName("GNS3")
|
||||
app.setApplicationVersion(__version__)
|
||||
|
||||
formatter = logging.Formatter("[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d] %(message)s",
|
||||
datefmt="%y%m%d %H:%M:%S")
|
||||
# save client logging info to a file
|
||||
logfile = os.path.join(LocalConfig.configDirectory(), "gns3_gui.log")
|
||||
|
||||
# on debug enable logging to stdout
|
||||
if options.debug:
|
||||
root_logger = init_logger(logging.DEBUG)
|
||||
root_logger = init_logger(logging.DEBUG, logfile)
|
||||
else:
|
||||
root_logger = init_logger(logging.INFO)
|
||||
|
||||
# save client logging info to a file
|
||||
logfile = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "gns3_gui.log")
|
||||
try:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(QtCore.QSettings().fileName()))
|
||||
except FileExistsError:
|
||||
pass
|
||||
handler = logging.FileHandler(logfile, "w")
|
||||
root_logger.addHandler(handler)
|
||||
except OSError as e:
|
||||
log.warn("could not log to {}: {}".format(logfile, e))
|
||||
|
||||
log.info('Log level: {}'.format(logging.getLevelName(log.getEffectiveLevel())))
|
||||
root_logger = init_logger(logging.INFO, logfile)
|
||||
|
||||
# update the exception file path to have it in the same directory as the settings file.
|
||||
exception_file_path = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), exception_file_path)
|
||||
exception_file_path = os.path.join(LocalConfig.configDirectory(), exception_file_path)
|
||||
|
||||
global mainwindow
|
||||
mainwindow = MainWindow()
|
||||
|
||||
# On OSX we can receive the file to open from a system event
|
||||
# loadPath is smart and will load only if a path is present
|
||||
mainwindow.ready_signal.connect(lambda: mainwindow.loadPath(app.open_file_at_startup))
|
||||
mainwindow.ready_signal.connect(lambda: mainwindow.loadPath(options.project))
|
||||
app.file_open_signal.connect(lambda path: mainwindow.loadPath(path))
|
||||
|
||||
# Manage Ctrl + C or kill command
|
||||
def sigint_handler(*args):
|
||||
log.info("Signal received exiting the application")
|
||||
mainwindow.setSoftExit(False)
|
||||
app.closeAllWindows()
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
signal.signal(signal.SIGTERM, sigint_handler)
|
||||
|
||||
mainwindow = MainWindow(options.project)
|
||||
mainwindow.show()
|
||||
|
||||
exit_code = app.exec_()
|
||||
delattr(MainWindow, "_instance")
|
||||
app.deleteLater()
|
||||
|
||||
# We force a full garbage collect before exit
|
||||
# for unknow reason otherwise Qt Segfault on OSX in some
|
||||
# conditions
|
||||
import gc
|
||||
gc.collect()
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
1173
gns3/main_window.py
1173
gns3/main_window.py
File diff suppressed because it is too large
Load Diff
@@ -21,5 +21,6 @@ from gns3.modules.iou import IOU
|
||||
from gns3.modules.vpcs import VPCS
|
||||
from gns3.modules.virtualbox import VirtualBox
|
||||
from gns3.modules.qemu import Qemu
|
||||
from gns3.modules.vmware import VMware
|
||||
|
||||
MODULES = [VPCS, Dynamips, IOU, VirtualBox, Qemu, Builtin]
|
||||
MODULES = [VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Builtin]
|
||||
|
||||
@@ -19,10 +19,8 @@
|
||||
Built-in module implementation.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui
|
||||
from gns3.servers import Servers
|
||||
from gns3.qt import QtWidgets
|
||||
from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
from .cloud import Cloud
|
||||
from .host import Host
|
||||
|
||||
@@ -38,10 +36,13 @@ class Builtin(Module):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Module.__init__(self)
|
||||
super().__init__()
|
||||
|
||||
self._nodes = []
|
||||
|
||||
def configChangedSlot(self):
|
||||
pass
|
||||
|
||||
def addNode(self, node):
|
||||
"""
|
||||
Adds a node to this module.
|
||||
@@ -69,57 +70,6 @@ class Builtin(Module):
|
||||
log.info("Built-in module reset")
|
||||
self._nodes.clear()
|
||||
|
||||
def allocateServer(self, node_class):
|
||||
"""
|
||||
Allocates a server.
|
||||
|
||||
:param node_class: Node object
|
||||
|
||||
:returns: allocated server (HTTPClient instance)
|
||||
"""
|
||||
|
||||
# check all other modules to find if they
|
||||
# are using a local server
|
||||
using_local_server = []
|
||||
from gns3.modules import MODULES
|
||||
for module in MODULES:
|
||||
instance = module.instance()
|
||||
if instance != self:
|
||||
module_settings = instance.settings()
|
||||
if "use_local_server" in module_settings:
|
||||
using_local_server.append(module_settings["use_local_server"])
|
||||
|
||||
# allocate a server for the node
|
||||
servers = Servers.instance()
|
||||
local_server = servers.localServer()
|
||||
remote_servers = servers.remoteServers()
|
||||
|
||||
if not all(using_local_server) and len(remote_servers):
|
||||
# a module is not using a local server
|
||||
|
||||
if True not in using_local_server and len(remote_servers) == 1:
|
||||
# no module is using a local server and there is only one
|
||||
# remote server available, so no need to ask the user.
|
||||
return next(iter(servers))
|
||||
|
||||
server_list = []
|
||||
server_list.append("Local server ({}:{})".format(local_server.host, local_server.port))
|
||||
for remote_server in remote_servers:
|
||||
server_list.append("{}".format(remote_server))
|
||||
|
||||
# TODO: move this to graphics_view
|
||||
from gns3.main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
(selection, ok) = QtGui.QInputDialog.getItem(mainwindow, "Server", "Please choose a server", server_list, 0, False)
|
||||
if ok:
|
||||
if selection.startswith("Local server"):
|
||||
return local_server
|
||||
else:
|
||||
return remote_servers[selection]
|
||||
else:
|
||||
raise ModuleError("Please select a server")
|
||||
return local_server
|
||||
|
||||
def createNode(self, node_class, server, project):
|
||||
"""
|
||||
Creates a new node.
|
||||
@@ -156,15 +106,15 @@ class Builtin(Module):
|
||||
available_interfaces.append(interface["name"])
|
||||
|
||||
if available_interfaces:
|
||||
selection, ok = QtGui.QInputDialog.getItem(mainwindow,
|
||||
"Cloud interfaces", "Interface {} could not be found\nPlease select an alternative from your existing interfaces:".format(missing_interface),
|
||||
available_interfaces, 0, False)
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(mainwindow,
|
||||
"Cloud interfaces", "Interface {} could not be found\nPlease select an alternative from your existing interfaces:".format(missing_interface),
|
||||
available_interfaces, 0, False)
|
||||
if ok:
|
||||
return selection
|
||||
QtGui.QMessageBox.warning(mainwindow, "Cloud interface", "No alternative interface chosen to replace {} on this host, this may lead to issues".format(missing_interface))
|
||||
QtWidgets.QMessageBox.warning(mainwindow, "Cloud interface", "No alternative interface chosen to replace {} on this host, this may lead to issues".format(missing_interface))
|
||||
return None
|
||||
else:
|
||||
QtGui.QMessageBox.critical(mainwindow, "Cloud interface", "Could not find interface {} on this host".format(missing_interface))
|
||||
QtWidgets.QMessageBox.critical(mainwindow, "Cloud interface", "Could not find interface {} on this host".format(missing_interface))
|
||||
return missing_interface
|
||||
|
||||
@staticmethod
|
||||
@@ -201,8 +151,7 @@ class Builtin(Module):
|
||||
{"class": node_class.__name__,
|
||||
"name": node_class.symbolName(),
|
||||
"categories": node_class.categories(),
|
||||
"default_symbol": node_class.defaultSymbol(),
|
||||
"hover_symbol": node_class.hoverSymbol()}
|
||||
"symbol": node_class.defaultSymbol()}
|
||||
)
|
||||
return nodes
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
"""
|
||||
NIO implementation on the client side (in the form of a pseudo node represented as a cloud).
|
||||
Asynchronously sends JSON messages to the GNS3 server and receives responses with callbacks.
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -25,6 +24,7 @@ from gns3.node import Node
|
||||
from gns3.ports.port import Port
|
||||
from gns3.nios.nio_generic_ethernet import NIOGenericEthernet
|
||||
from gns3.nios.nio_linux_ethernet import NIOLinuxEthernet
|
||||
from gns3.nios.nio_nat import NIONAT
|
||||
from gns3.nios.nio_udp import NIOUDP
|
||||
from gns3.nios.nio_tap import NIOTAP
|
||||
from gns3.nios.nio_unix import NIOUNIX
|
||||
@@ -48,7 +48,8 @@ class Cloud(Node):
|
||||
_name_instance_count = 1
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
Node.__init__(self, module, server, project)
|
||||
|
||||
super().__init__(module, server, project)
|
||||
|
||||
log.info("cloud is being created")
|
||||
# create an unique id and name
|
||||
@@ -57,8 +58,6 @@ class Cloud(Node):
|
||||
|
||||
name = "Cloud {}".format(self._name_id)
|
||||
self.setStatus(Node.started) # this is an always-on node
|
||||
self._defaults = {}
|
||||
self._ports = []
|
||||
self._initial_settings = None
|
||||
self._settings = {"name": name,
|
||||
"interfaces": {},
|
||||
@@ -73,7 +72,7 @@ class Cloud(Node):
|
||||
self.delete_links_signal.emit()
|
||||
self.deleted_signal.emit()
|
||||
|
||||
def setup(self, name=None, initial_settings={}):
|
||||
def setup(self, name=None, additional_settings={}):
|
||||
"""
|
||||
Setups this cloud.
|
||||
|
||||
@@ -83,8 +82,8 @@ class Cloud(Node):
|
||||
if name:
|
||||
self._settings["name"] = name
|
||||
|
||||
if initial_settings:
|
||||
self._initial_settings = initial_settings
|
||||
if additional_settings and "nios" in additional_settings:
|
||||
self._settings["nios"] = additional_settings["nios"]
|
||||
|
||||
self._server.get("/interfaces", self._setupCallback)
|
||||
|
||||
@@ -103,13 +102,16 @@ class Cloud(Node):
|
||||
else:
|
||||
self._settings["interfaces"] = result.copy()
|
||||
|
||||
if self._initial_settings and "nios" in self._initial_settings and self._initial_settings["nios"]:
|
||||
self._initial_settings["interfaces"] = {}
|
||||
self.update(self._initial_settings)
|
||||
if self._settings["nios"]:
|
||||
self._addPorts(self._settings["nios"])
|
||||
|
||||
if self._loading:
|
||||
self.loaded_signal.emit()
|
||||
else:
|
||||
log.info("cloud {} has been created".format(self.name()))
|
||||
self.setInitialized(True)
|
||||
log.info("cloud {} has been created".format(self.name()))
|
||||
self.created_signal.emit(self.id())
|
||||
self._module.addNode(self)
|
||||
|
||||
def _createNIOUDP(self, nio):
|
||||
"""
|
||||
@@ -152,6 +154,19 @@ class Cloud(Node):
|
||||
return NIOLinuxEthernet(linux_device)
|
||||
return None
|
||||
|
||||
def _createNIONAT(self, nio):
|
||||
"""
|
||||
Creates a NIO NAT.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_nat:(.+)$""", nio)
|
||||
if match:
|
||||
identifier = match.group(1)
|
||||
return NIONAT(identifier)
|
||||
return None
|
||||
|
||||
def _createNIOTAP(self, nio):
|
||||
"""
|
||||
Creates a NIO TAP.
|
||||
@@ -206,6 +221,56 @@ class Cloud(Node):
|
||||
return NIONull(identifier)
|
||||
return None
|
||||
|
||||
def _allocateNIO(self, nio):
|
||||
"""
|
||||
Allocate a new NIO object.
|
||||
|
||||
:param nio: NIO description
|
||||
|
||||
:returns: NIO instance
|
||||
"""
|
||||
|
||||
nio_object = None
|
||||
if nio.lower().startswith("nio_udp"):
|
||||
nio_object = self._createNIOUDP(nio)
|
||||
if nio.lower().startswith("nio_gen_eth"):
|
||||
nio_object = self._createNIOGenericEthernet(nio)
|
||||
if nio.lower().startswith("nio_gen_linux"):
|
||||
nio_object = self._createNIOLinuxEthernet(nio)
|
||||
if nio.lower().startswith("nio_nat"):
|
||||
nio_object = self._createNIONAT(nio)
|
||||
if nio.lower().startswith("nio_tap"):
|
||||
nio_object = self._createNIOTAP(nio)
|
||||
if nio.lower().startswith("nio_unix"):
|
||||
nio_object = self._createNIOUNIX(nio)
|
||||
if nio.lower().startswith("nio_vde"):
|
||||
nio_object = self._createNIOVDE(nio)
|
||||
if nio.lower().startswith("nio_null"):
|
||||
nio_object = self._createNIONull(nio)
|
||||
if nio_object is None:
|
||||
log.error("Could not create NIO object from {}".format(nio))
|
||||
return nio_object
|
||||
|
||||
def _addPorts(self, nios, ignore_existing_nio=False):
|
||||
"""
|
||||
Adds adapters.
|
||||
|
||||
:param adapters: number of adapters
|
||||
"""
|
||||
|
||||
# add ports
|
||||
for nio in nios:
|
||||
if ignore_existing_nio and nio in self._settings["nios"]:
|
||||
# port already created for this NIO
|
||||
continue
|
||||
nio_object = self._allocateNIO(nio)
|
||||
if nio_object is None:
|
||||
continue
|
||||
port = Port(nio, nio_object, stub=True)
|
||||
port.setStatus(Port.started)
|
||||
self._ports.append(port)
|
||||
log.debug("port {} has been added".format(nio))
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this cloud.
|
||||
@@ -216,35 +281,8 @@ class Cloud(Node):
|
||||
updated = False
|
||||
if "nios" in new_settings:
|
||||
nios = new_settings["nios"]
|
||||
|
||||
# add ports
|
||||
for nio in nios:
|
||||
if nio in self._settings["nios"]:
|
||||
# port already created for this NIO
|
||||
continue
|
||||
nio_object = None
|
||||
if nio.lower().startswith("nio_udp"):
|
||||
nio_object = self._createNIOUDP(nio)
|
||||
if nio.lower().startswith("nio_gen_eth"):
|
||||
nio_object = self._createNIOGenericEthernet(nio)
|
||||
if nio.lower().startswith("nio_gen_linux"):
|
||||
nio_object = self._createNIOLinuxEthernet(nio)
|
||||
if nio.lower().startswith("nio_tap"):
|
||||
nio_object = self._createNIOTAP(nio)
|
||||
if nio.lower().startswith("nio_unix"):
|
||||
nio_object = self._createNIOUNIX(nio)
|
||||
if nio.lower().startswith("nio_vde"):
|
||||
nio_object = self._createNIOVDE(nio)
|
||||
if nio.lower().startswith("nio_null"):
|
||||
nio_object = self._createNIONull(nio)
|
||||
if nio_object is None:
|
||||
log.error("Could not create NIO object from {}".format(nio))
|
||||
continue
|
||||
port = Port(nio, nio_object, stub=True)
|
||||
port.setStatus(Port.started)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been added".format(nio))
|
||||
self._addPorts(nios, ignore_existing_nio=True)
|
||||
updated = True
|
||||
|
||||
# delete ports
|
||||
for nio in self._settings["nios"]:
|
||||
@@ -312,8 +350,7 @@ This is a pseudo-device for external connections
|
||||
"description": str(self),
|
||||
"properties": {"name": self.name(),
|
||||
"nios": self._settings["nios"]},
|
||||
"server_id": self._server.id(),
|
||||
}
|
||||
"server_id": self._server.id()}
|
||||
|
||||
# add the ports
|
||||
if self._ports:
|
||||
@@ -331,22 +368,25 @@ This is a pseudo-device for external connections
|
||||
:param node_info: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
self.node_info = node_info
|
||||
settings = node_info["properties"]
|
||||
name = settings.pop("name")
|
||||
self.updated_signal.connect(self._updatePortSettings)
|
||||
log.info("cloud {} is loading".format(name))
|
||||
self.setup(name, settings)
|
||||
self.setName(name)
|
||||
self._loading = True
|
||||
self._node_info = node_info
|
||||
self.loaded_signal.connect(self._updatePortSettings)
|
||||
self.setup(name, additional_settings=settings)
|
||||
|
||||
def _updatePortSettings(self):
|
||||
"""
|
||||
Updates port settings when loading a topology.
|
||||
"""
|
||||
|
||||
self.updated_signal.disconnect(self._updatePortSettings)
|
||||
self.loaded_signal.disconnect(self._updatePortSettings)
|
||||
|
||||
# update the port with the correct IDs
|
||||
if "ports" in self.node_info:
|
||||
ports = self.node_info["ports"]
|
||||
if "ports" in self._node_info:
|
||||
ports = self._node_info["ports"]
|
||||
for topology_port in ports:
|
||||
for port in self._ports:
|
||||
if topology_port["name"] == port.name():
|
||||
@@ -365,12 +405,18 @@ This is a pseudo-device for external connections
|
||||
if topology_port["name"] in self._settings["nios"]:
|
||||
self._settings["nios"].remove(topology_port["name"])
|
||||
topology_port["name"] = topology_port["name"].replace(topology_port_name, alternative_interface)
|
||||
nio = self._allocateNIO(topology_port["name"])
|
||||
port.setDefaultNio(nio)
|
||||
port.setName(topology_port["name"])
|
||||
self._settings["nios"].append(topology_port["name"])
|
||||
|
||||
log.info("cloud {} has been created".format(self.name()))
|
||||
# now we can set the node as initialized and trigger the created signal
|
||||
self.setInitialized(True)
|
||||
log.info("cloud {} has been loaded".format(self.name()))
|
||||
self.created_signal.emit(self.id())
|
||||
self._module.addNode(self)
|
||||
self._loading = False
|
||||
self._node_info = None
|
||||
|
||||
def name(self):
|
||||
"""
|
||||
@@ -401,7 +447,7 @@ This is a pseudo-device for external connections
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node configurator.
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
@@ -417,17 +463,7 @@ This is a pseudo-device for external connections
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/cloud.normal.svg"
|
||||
|
||||
@staticmethod
|
||||
def hoverSymbol():
|
||||
"""
|
||||
Returns the symbol to use when the cloud is hovered.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/cloud.selected.svg"
|
||||
return ":/symbols/cloud.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
@@ -36,7 +36,7 @@ class Host(Cloud):
|
||||
_name_instance_count = 1
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
Cloud.__init__(self, module, server, project)
|
||||
super().__init__(module, server, project)
|
||||
|
||||
log.info("host is being created")
|
||||
# create an unique id and name
|
||||
@@ -46,7 +46,7 @@ class Host(Cloud):
|
||||
name = "Host{}".format(self._name_id)
|
||||
self._settings["name"] = name
|
||||
|
||||
def setup(self, name=None, initial_settings={}):
|
||||
def setup(self, name=None, additional_settings={}):
|
||||
"""
|
||||
Setups this host.
|
||||
|
||||
@@ -56,8 +56,8 @@ class Host(Cloud):
|
||||
if name:
|
||||
self._settings["name"] = name
|
||||
|
||||
if initial_settings:
|
||||
self._initial_settings = initial_settings
|
||||
if additional_settings and "nios" in additional_settings:
|
||||
self._settings["nios"] = additional_settings["nios"]
|
||||
else:
|
||||
self.created_signal.connect(self._autoConfigure)
|
||||
|
||||
@@ -76,7 +76,6 @@ class Host(Cloud):
|
||||
new_settings["nios"].append("nio_tap:{}".format(interface["name"]))
|
||||
else:
|
||||
new_settings["nios"].append("nio_gen_eth:{}".format(interface["name"]))
|
||||
|
||||
self.update(new_settings)
|
||||
|
||||
@staticmethod
|
||||
@@ -87,17 +86,7 @@ class Host(Cloud):
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/computer.normal.svg"
|
||||
|
||||
@staticmethod
|
||||
def hoverSymbol():
|
||||
"""
|
||||
Returns the symbol to use when the host is hovered.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/computer.selected.svg"
|
||||
return ":/symbols/computer.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
@@ -20,11 +20,11 @@ Configuration page for clouds.
|
||||
"""
|
||||
|
||||
import re
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
from ..ui.cloud_configuration_page_ui import Ui_cloudConfigPageWidget
|
||||
|
||||
|
||||
class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
|
||||
"""
|
||||
QWidget configuration page for clouds.
|
||||
@@ -32,7 +32,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
QtGui.QWidget.__init__(self)
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
self._nios = []
|
||||
|
||||
@@ -48,6 +48,12 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiAddLinuxEthernetPushButton.clicked.connect(self._linuxEthernetAddSlot)
|
||||
self.uiDeleteLinuxEthernetPushButton.clicked.connect(self._linuxEthernetDeleteSlot)
|
||||
|
||||
# connect NIO NAT slots
|
||||
self.uiNIONATListWidget.currentRowChanged.connect(self._NIONATSelectedSlot)
|
||||
self.uiNIONATListWidget.itemSelectionChanged.connect(self._NIONATChangedSlot)
|
||||
self.uiAddNIONATPushButton.clicked.connect(self._NIONATAddSlot)
|
||||
self.uiDeleteNIONATPushButton.clicked.connect(self._NIONATDeleteSlot)
|
||||
|
||||
# connect NIO UDP slots
|
||||
self.uiNIOUDPListWidget.currentRowChanged.connect(self._NIOUDPSelectedSlot)
|
||||
self.uiNIOUDPListWidget.itemSelectionChanged.connect(self._NIOUDPChangedSlot)
|
||||
@@ -122,7 +128,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtGui.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiGenericEthernetListWidget.takeItem(self.uiGenericEthernetListWidget.currentRow())
|
||||
@@ -171,14 +177,70 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtGui.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiLinuxEthernetListWidget.takeItem(self.uiLinuxEthernetListWidget.currentRow())
|
||||
|
||||
def _NIONATSelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected NAT NIO.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIONATListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_nat:(.+)$""", nio)
|
||||
if match:
|
||||
self.uiNIONATIdentiferLineEdit.setText(match.group(1))
|
||||
|
||||
def _NIONATChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiNIONATListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIONATPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteNIONATPushButton.setEnabled(False)
|
||||
|
||||
def _NIONATAddSlot(self):
|
||||
"""
|
||||
Adds a new NAT NIO.
|
||||
"""
|
||||
|
||||
identifier = self.uiNIONATIdentiferLineEdit.text()
|
||||
if identifier:
|
||||
nio = "nio_nat:{}".format(identifier)
|
||||
if nio not in self._nios:
|
||||
self.uiNIONATListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
|
||||
def _NIONATDeleteSlot(self):
|
||||
"""
|
||||
Deletes a NAT NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIONATListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
# check we can delete that NIO
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIONATListWidget.takeItem(self.uiNIONATListWidget.currentRow())
|
||||
|
||||
def _NIOUDPSelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected UDP.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIOUDPListWidget.currentItem()
|
||||
@@ -231,7 +293,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtGui.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIOUDPListWidget.takeItem(self.uiNIOUDPListWidget.currentRow())
|
||||
@@ -285,7 +347,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtGui.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIOTAPListWidget.takeItem(self.uiNIOTAPListWidget.currentRow())
|
||||
@@ -342,7 +404,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtGui.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIOUNIXListWidget.takeItem(self.uiNIOUNIXListWidget.currentRow())
|
||||
@@ -398,7 +460,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtGui.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIOVDEListWidget.takeItem(self.uiNIOVDEListWidget.currentRow())
|
||||
@@ -406,6 +468,8 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
def _NIONullSelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected NULL NIO.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIONullListWidget.currentItem()
|
||||
@@ -450,7 +514,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtGui.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIONullListWidget.takeItem(self.uiNIONullListWidget.currentRow())
|
||||
@@ -481,7 +545,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiGenericEthernetComboBox.addItem(interface["name"])
|
||||
self.uiGenericEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
|
||||
index += 1
|
||||
self.uiGenericEthernetComboBox.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents)
|
||||
self.uiGenericEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
|
||||
# load all network interfaces
|
||||
self.uiLinuxEthernetComboBox.clear()
|
||||
@@ -491,7 +555,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiLinuxEthernetComboBox.addItem(interface["name"])
|
||||
self.uiLinuxEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
|
||||
index += 1
|
||||
self.uiLinuxEthernetComboBox.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents)
|
||||
self.uiLinuxEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
|
||||
# populate the NIO lists
|
||||
self.nios = []
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>542</width>
|
||||
<height>500</height>
|
||||
<width>653</width>
|
||||
<height>478</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -15,13 +15,13 @@
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<widget class="QTabWidget" name="uiNIOsTabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab">
|
||||
<widget class="QWidget" name="NIOEthernetTab">
|
||||
<attribute name="title">
|
||||
<string>NIO Ethernet</string>
|
||||
<string>Ethernet</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
@@ -135,9 +135,104 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<widget class="QWidget" name="NIONATTab">
|
||||
<attribute name="title">
|
||||
<string>NIO UDP</string>
|
||||
<string>NAT</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="uiNIONATSettingsGroupBox">
|
||||
<property name="title">
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiNIONATIdentifierLabel">
|
||||
<property name="text">
|
||||
<string>Local identifier:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLineEdit" name="uiNIONATIdentiferLineEdit">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2" rowspan="3">
|
||||
<widget class="QGroupBox" name="uiNIONATListGroupBox">
|
||||
<property name="title">
|
||||
<string>NIOs</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="_3">
|
||||
<item>
|
||||
<widget class="QListWidget" name="uiNIONATListWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="uiAddNIONATPushButton">
|
||||
<property name="text">
|
||||
<string>&Add</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="uiDeleteNIONATPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Delete</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" rowspan="2">
|
||||
<spacer name="spacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>294</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<spacer name="spacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>194</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="NIOUDPTab">
|
||||
<attribute name="title">
|
||||
<string>UDP</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
@@ -265,9 +360,9 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_3">
|
||||
<widget class="QWidget" name="NIOTAPTab">
|
||||
<attribute name="title">
|
||||
<string>NIO TAP</string>
|
||||
<string>TAP</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
@@ -317,9 +412,9 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_4">
|
||||
<widget class="QWidget" name="NIOUnixTab">
|
||||
<attribute name="title">
|
||||
<string>NIO UNIX</string>
|
||||
<string>UNIX</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
@@ -440,9 +535,9 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_5">
|
||||
<widget class="QWidget" name="NIOVDETab">
|
||||
<attribute name="title">
|
||||
<string>NIO VDE</string>
|
||||
<string>VDE</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
@@ -563,9 +658,9 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_6">
|
||||
<widget class="QWidget" name="NIONullTab">
|
||||
<attribute name="title">
|
||||
<string>NIO NULL</string>
|
||||
<string>NULL</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
@@ -575,9 +670,9 @@
|
||||
</property>
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<widget class="QLabel" name="uiNIONullIdentifierLabel">
|
||||
<property name="text">
|
||||
<string>Identifier:</string>
|
||||
<string>Local identifier:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -658,7 +753,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_7">
|
||||
<widget class="QWidget" name="MiscTab">
|
||||
<attribute name="title">
|
||||
<string>Misc.</string>
|
||||
</attribute>
|
||||
|
||||
@@ -1,423 +1,459 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/workspace/git/gns3-gui/gns3/modules/dynamips/ui/cloud_configuration_page.ui'
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
|
||||
#
|
||||
# Created: Mon Mar 17 17:42:16 2014
|
||||
# by: PyQt4 UI code generator 4.10
|
||||
# Created by: PyQt5 UI code generator 5.4.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
def _fromUtf8(s):
|
||||
return s
|
||||
|
||||
try:
|
||||
_encoding = QtGui.QApplication.UnicodeUTF8
|
||||
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig, _encoding)
|
||||
except AttributeError:
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig)
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_cloudConfigPageWidget(object):
|
||||
|
||||
def setupUi(self, cloudConfigPageWidget):
|
||||
cloudConfigPageWidget.setObjectName(_fromUtf8("cloudConfigPageWidget"))
|
||||
cloudConfigPageWidget.resize(542, 500)
|
||||
self.vboxlayout = QtGui.QVBoxLayout(cloudConfigPageWidget)
|
||||
self.vboxlayout.setObjectName(_fromUtf8("vboxlayout"))
|
||||
self.tabWidget = QtGui.QTabWidget(cloudConfigPageWidget)
|
||||
self.tabWidget.setObjectName(_fromUtf8("tabWidget"))
|
||||
self.tab = QtGui.QWidget()
|
||||
self.tab.setObjectName(_fromUtf8("tab"))
|
||||
self.vboxlayout1 = QtGui.QVBoxLayout(self.tab)
|
||||
self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1"))
|
||||
self.uiGenericEthernetGroupBox = QtGui.QGroupBox(self.tab)
|
||||
self.uiGenericEthernetGroupBox.setObjectName(_fromUtf8("uiGenericEthernetGroupBox"))
|
||||
self.gridlayout = QtGui.QGridLayout(self.uiGenericEthernetGroupBox)
|
||||
self.gridlayout.setObjectName(_fromUtf8("gridlayout"))
|
||||
self.uiGenericEthernetComboBox = QtGui.QComboBox(self.uiGenericEthernetGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
cloudConfigPageWidget.setObjectName("cloudConfigPageWidget")
|
||||
cloudConfigPageWidget.resize(653, 478)
|
||||
self.vboxlayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
|
||||
self.vboxlayout.setObjectName("vboxlayout")
|
||||
self.uiNIOsTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
|
||||
self.uiNIOsTabWidget.setObjectName("uiNIOsTabWidget")
|
||||
self.NIOEthernetTab = QtWidgets.QWidget()
|
||||
self.NIOEthernetTab.setObjectName("NIOEthernetTab")
|
||||
self.vboxlayout1 = QtWidgets.QVBoxLayout(self.NIOEthernetTab)
|
||||
self.vboxlayout1.setObjectName("vboxlayout1")
|
||||
self.uiGenericEthernetGroupBox = QtWidgets.QGroupBox(self.NIOEthernetTab)
|
||||
self.uiGenericEthernetGroupBox.setObjectName("uiGenericEthernetGroupBox")
|
||||
self.gridlayout = QtWidgets.QGridLayout(self.uiGenericEthernetGroupBox)
|
||||
self.gridlayout.setObjectName("gridlayout")
|
||||
self.uiGenericEthernetComboBox = QtWidgets.QComboBox(self.uiGenericEthernetGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiGenericEthernetComboBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiGenericEthernetComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiGenericEthernetComboBox.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents)
|
||||
self.uiGenericEthernetComboBox.setObjectName(_fromUtf8("uiGenericEthernetComboBox"))
|
||||
self.uiGenericEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
self.uiGenericEthernetComboBox.setObjectName("uiGenericEthernetComboBox")
|
||||
self.gridlayout.addWidget(self.uiGenericEthernetComboBox, 0, 0, 1, 3)
|
||||
self.uiGenericEthernetLineEdit = QtGui.QLineEdit(self.uiGenericEthernetGroupBox)
|
||||
self.uiGenericEthernetLineEdit.setObjectName(_fromUtf8("uiGenericEthernetLineEdit"))
|
||||
self.uiGenericEthernetLineEdit = QtWidgets.QLineEdit(self.uiGenericEthernetGroupBox)
|
||||
self.uiGenericEthernetLineEdit.setObjectName("uiGenericEthernetLineEdit")
|
||||
self.gridlayout.addWidget(self.uiGenericEthernetLineEdit, 1, 0, 1, 1)
|
||||
self.uiAddGenericEthernetPushButton = QtGui.QPushButton(self.uiGenericEthernetGroupBox)
|
||||
self.uiAddGenericEthernetPushButton.setObjectName(_fromUtf8("uiAddGenericEthernetPushButton"))
|
||||
self.uiAddGenericEthernetPushButton = QtWidgets.QPushButton(self.uiGenericEthernetGroupBox)
|
||||
self.uiAddGenericEthernetPushButton.setObjectName("uiAddGenericEthernetPushButton")
|
||||
self.gridlayout.addWidget(self.uiAddGenericEthernetPushButton, 1, 1, 1, 1)
|
||||
self.uiDeleteGenericEthernetPushButton = QtGui.QPushButton(self.uiGenericEthernetGroupBox)
|
||||
self.uiDeleteGenericEthernetPushButton = QtWidgets.QPushButton(self.uiGenericEthernetGroupBox)
|
||||
self.uiDeleteGenericEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteGenericEthernetPushButton.setObjectName(_fromUtf8("uiDeleteGenericEthernetPushButton"))
|
||||
self.uiDeleteGenericEthernetPushButton.setObjectName("uiDeleteGenericEthernetPushButton")
|
||||
self.gridlayout.addWidget(self.uiDeleteGenericEthernetPushButton, 1, 2, 1, 1)
|
||||
self.uiGenericEthernetListWidget = QtGui.QListWidget(self.uiGenericEthernetGroupBox)
|
||||
self.uiGenericEthernetListWidget.setObjectName(_fromUtf8("uiGenericEthernetListWidget"))
|
||||
self.uiGenericEthernetListWidget = QtWidgets.QListWidget(self.uiGenericEthernetGroupBox)
|
||||
self.uiGenericEthernetListWidget.setObjectName("uiGenericEthernetListWidget")
|
||||
self.gridlayout.addWidget(self.uiGenericEthernetListWidget, 2, 0, 1, 3)
|
||||
self.vboxlayout1.addWidget(self.uiGenericEthernetGroupBox)
|
||||
self.uiLinuxEthernetGroupBox = QtGui.QGroupBox(self.tab)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
|
||||
self.uiLinuxEthernetGroupBox = QtWidgets.QGroupBox(self.NIOEthernetTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiLinuxEthernetGroupBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiLinuxEthernetGroupBox.setSizePolicy(sizePolicy)
|
||||
self.uiLinuxEthernetGroupBox.setObjectName(_fromUtf8("uiLinuxEthernetGroupBox"))
|
||||
self.gridlayout1 = QtGui.QGridLayout(self.uiLinuxEthernetGroupBox)
|
||||
self.gridlayout1.setObjectName(_fromUtf8("gridlayout1"))
|
||||
self.uiLinuxEthernetComboBox = QtGui.QComboBox(self.uiLinuxEthernetGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
self.uiLinuxEthernetGroupBox.setObjectName("uiLinuxEthernetGroupBox")
|
||||
self.gridlayout1 = QtWidgets.QGridLayout(self.uiLinuxEthernetGroupBox)
|
||||
self.gridlayout1.setObjectName("gridlayout1")
|
||||
self.uiLinuxEthernetComboBox = QtWidgets.QComboBox(self.uiLinuxEthernetGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiLinuxEthernetComboBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiLinuxEthernetComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiLinuxEthernetComboBox.setObjectName(_fromUtf8("uiLinuxEthernetComboBox"))
|
||||
self.uiLinuxEthernetComboBox.setObjectName("uiLinuxEthernetComboBox")
|
||||
self.gridlayout1.addWidget(self.uiLinuxEthernetComboBox, 0, 0, 1, 3)
|
||||
self.uiLinuxEthernetLineEdit = QtGui.QLineEdit(self.uiLinuxEthernetGroupBox)
|
||||
self.uiLinuxEthernetLineEdit.setObjectName(_fromUtf8("uiLinuxEthernetLineEdit"))
|
||||
self.uiLinuxEthernetLineEdit = QtWidgets.QLineEdit(self.uiLinuxEthernetGroupBox)
|
||||
self.uiLinuxEthernetLineEdit.setObjectName("uiLinuxEthernetLineEdit")
|
||||
self.gridlayout1.addWidget(self.uiLinuxEthernetLineEdit, 1, 0, 1, 1)
|
||||
self.uiAddLinuxEthernetPushButton = QtGui.QPushButton(self.uiLinuxEthernetGroupBox)
|
||||
self.uiAddLinuxEthernetPushButton.setObjectName(_fromUtf8("uiAddLinuxEthernetPushButton"))
|
||||
self.uiAddLinuxEthernetPushButton = QtWidgets.QPushButton(self.uiLinuxEthernetGroupBox)
|
||||
self.uiAddLinuxEthernetPushButton.setObjectName("uiAddLinuxEthernetPushButton")
|
||||
self.gridlayout1.addWidget(self.uiAddLinuxEthernetPushButton, 1, 1, 1, 1)
|
||||
self.uiDeleteLinuxEthernetPushButton = QtGui.QPushButton(self.uiLinuxEthernetGroupBox)
|
||||
self.uiDeleteLinuxEthernetPushButton = QtWidgets.QPushButton(self.uiLinuxEthernetGroupBox)
|
||||
self.uiDeleteLinuxEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteLinuxEthernetPushButton.setObjectName(_fromUtf8("uiDeleteLinuxEthernetPushButton"))
|
||||
self.uiDeleteLinuxEthernetPushButton.setObjectName("uiDeleteLinuxEthernetPushButton")
|
||||
self.gridlayout1.addWidget(self.uiDeleteLinuxEthernetPushButton, 1, 2, 1, 1)
|
||||
self.uiLinuxEthernetListWidget = QtGui.QListWidget(self.uiLinuxEthernetGroupBox)
|
||||
self.uiLinuxEthernetListWidget.setObjectName(_fromUtf8("uiLinuxEthernetListWidget"))
|
||||
self.uiLinuxEthernetListWidget = QtWidgets.QListWidget(self.uiLinuxEthernetGroupBox)
|
||||
self.uiLinuxEthernetListWidget.setObjectName("uiLinuxEthernetListWidget")
|
||||
self.gridlayout1.addWidget(self.uiLinuxEthernetListWidget, 2, 0, 1, 3)
|
||||
self.vboxlayout1.addWidget(self.uiLinuxEthernetGroupBox)
|
||||
spacerItem = QtGui.QSpacerItem(21, 16, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
|
||||
spacerItem = QtWidgets.QSpacerItem(21, 16, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
self.vboxlayout1.addItem(spacerItem)
|
||||
self.tabWidget.addTab(self.tab, _fromUtf8(""))
|
||||
self.tab_2 = QtGui.QWidget()
|
||||
self.tab_2.setObjectName(_fromUtf8("tab_2"))
|
||||
self.gridlayout2 = QtGui.QGridLayout(self.tab_2)
|
||||
self.gridlayout2.setObjectName(_fromUtf8("gridlayout2"))
|
||||
self.uiNIOUDPSettingsGroupBox = QtGui.QGroupBox(self.tab_2)
|
||||
self.uiNIOUDPSettingsGroupBox.setObjectName(_fromUtf8("uiNIOUDPSettingsGroupBox"))
|
||||
self.gridlayout3 = QtGui.QGridLayout(self.uiNIOUDPSettingsGroupBox)
|
||||
self.gridlayout3.setObjectName(_fromUtf8("gridlayout3"))
|
||||
self.uiLocalPortLabel = QtGui.QLabel(self.uiNIOUDPSettingsGroupBox)
|
||||
self.uiLocalPortLabel.setObjectName(_fromUtf8("uiLocalPortLabel"))
|
||||
self.uiNIOsTabWidget.addTab(self.NIOEthernetTab, "")
|
||||
self.NIONATTab = QtWidgets.QWidget()
|
||||
self.NIONATTab.setObjectName("NIONATTab")
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout(self.NIONATTab)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.uiNIONATSettingsGroupBox = QtWidgets.QGroupBox(self.NIONATTab)
|
||||
self.uiNIONATSettingsGroupBox.setObjectName("uiNIONATSettingsGroupBox")
|
||||
self._2 = QtWidgets.QGridLayout(self.uiNIONATSettingsGroupBox)
|
||||
self._2.setObjectName("_2")
|
||||
self.uiNIONATIdentifierLabel = QtWidgets.QLabel(self.uiNIONATSettingsGroupBox)
|
||||
self.uiNIONATIdentifierLabel.setObjectName("uiNIONATIdentifierLabel")
|
||||
self._2.addWidget(self.uiNIONATIdentifierLabel, 0, 0, 1, 1)
|
||||
self.uiNIONATIdentiferLineEdit = QtWidgets.QLineEdit(self.uiNIONATSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIONATIdentiferLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIONATIdentiferLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiNIONATIdentiferLineEdit.setObjectName("uiNIONATIdentiferLineEdit")
|
||||
self._2.addWidget(self.uiNIONATIdentiferLineEdit, 1, 0, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.uiNIONATSettingsGroupBox, 0, 0, 1, 2)
|
||||
self.uiNIONATListGroupBox = QtWidgets.QGroupBox(self.NIONATTab)
|
||||
self.uiNIONATListGroupBox.setObjectName("uiNIONATListGroupBox")
|
||||
self._3 = QtWidgets.QVBoxLayout(self.uiNIONATListGroupBox)
|
||||
self._3.setObjectName("_3")
|
||||
self.uiNIONATListWidget = QtWidgets.QListWidget(self.uiNIONATListGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIONATListWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIONATListWidget.setSizePolicy(sizePolicy)
|
||||
self.uiNIONATListWidget.setObjectName("uiNIONATListWidget")
|
||||
self._3.addWidget(self.uiNIONATListWidget)
|
||||
self.gridLayout_2.addWidget(self.uiNIONATListGroupBox, 0, 2, 3, 1)
|
||||
self.uiAddNIONATPushButton = QtWidgets.QPushButton(self.NIONATTab)
|
||||
self.uiAddNIONATPushButton.setObjectName("uiAddNIONATPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiAddNIONATPushButton, 1, 0, 1, 1)
|
||||
self.uiDeleteNIONATPushButton = QtWidgets.QPushButton(self.NIONATTab)
|
||||
self.uiDeleteNIONATPushButton.setEnabled(False)
|
||||
self.uiDeleteNIONATPushButton.setObjectName("uiDeleteNIONATPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiDeleteNIONATPushButton, 1, 1, 1, 1)
|
||||
spacerItem1 = QtWidgets.QSpacerItem(20, 294, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout_2.addItem(spacerItem1, 2, 0, 2, 1)
|
||||
spacerItem2 = QtWidgets.QSpacerItem(20, 194, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout_2.addItem(spacerItem2, 3, 2, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.NIONATTab, "")
|
||||
self.NIOUDPTab = QtWidgets.QWidget()
|
||||
self.NIOUDPTab.setObjectName("NIOUDPTab")
|
||||
self.gridlayout2 = QtWidgets.QGridLayout(self.NIOUDPTab)
|
||||
self.gridlayout2.setObjectName("gridlayout2")
|
||||
self.uiNIOUDPSettingsGroupBox = QtWidgets.QGroupBox(self.NIOUDPTab)
|
||||
self.uiNIOUDPSettingsGroupBox.setObjectName("uiNIOUDPSettingsGroupBox")
|
||||
self.gridlayout3 = QtWidgets.QGridLayout(self.uiNIOUDPSettingsGroupBox)
|
||||
self.gridlayout3.setObjectName("gridlayout3")
|
||||
self.uiLocalPortLabel = QtWidgets.QLabel(self.uiNIOUDPSettingsGroupBox)
|
||||
self.uiLocalPortLabel.setObjectName("uiLocalPortLabel")
|
||||
self.gridlayout3.addWidget(self.uiLocalPortLabel, 0, 0, 1, 1)
|
||||
self.uiLocalPortSpinBox = QtGui.QSpinBox(self.uiNIOUDPSettingsGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
self.uiLocalPortSpinBox = QtWidgets.QSpinBox(self.uiNIOUDPSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiLocalPortSpinBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiLocalPortSpinBox.setSizePolicy(sizePolicy)
|
||||
self.uiLocalPortSpinBox.setMaximum(65535)
|
||||
self.uiLocalPortSpinBox.setProperty("value", 30000)
|
||||
self.uiLocalPortSpinBox.setObjectName(_fromUtf8("uiLocalPortSpinBox"))
|
||||
self.uiLocalPortSpinBox.setObjectName("uiLocalPortSpinBox")
|
||||
self.gridlayout3.addWidget(self.uiLocalPortSpinBox, 0, 1, 1, 1)
|
||||
self.uiRemoteHostLabel = QtGui.QLabel(self.uiNIOUDPSettingsGroupBox)
|
||||
self.uiRemoteHostLabel.setObjectName(_fromUtf8("uiRemoteHostLabel"))
|
||||
self.uiRemoteHostLabel = QtWidgets.QLabel(self.uiNIOUDPSettingsGroupBox)
|
||||
self.uiRemoteHostLabel.setObjectName("uiRemoteHostLabel")
|
||||
self.gridlayout3.addWidget(self.uiRemoteHostLabel, 1, 0, 1, 1)
|
||||
self.uiRemoteHostLineEdit = QtGui.QLineEdit(self.uiNIOUDPSettingsGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
self.uiRemoteHostLineEdit = QtWidgets.QLineEdit(self.uiNIOUDPSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiRemoteHostLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiRemoteHostLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiRemoteHostLineEdit.setMinimumSize(QtCore.QSize(80, 0))
|
||||
self.uiRemoteHostLineEdit.setObjectName(_fromUtf8("uiRemoteHostLineEdit"))
|
||||
self.uiRemoteHostLineEdit.setObjectName("uiRemoteHostLineEdit")
|
||||
self.gridlayout3.addWidget(self.uiRemoteHostLineEdit, 1, 1, 1, 1)
|
||||
self.uiRemotePortLabel = QtGui.QLabel(self.uiNIOUDPSettingsGroupBox)
|
||||
self.uiRemotePortLabel.setObjectName(_fromUtf8("uiRemotePortLabel"))
|
||||
self.uiRemotePortLabel = QtWidgets.QLabel(self.uiNIOUDPSettingsGroupBox)
|
||||
self.uiRemotePortLabel.setObjectName("uiRemotePortLabel")
|
||||
self.gridlayout3.addWidget(self.uiRemotePortLabel, 2, 0, 1, 1)
|
||||
self.uiRemotePortSpinBox = QtGui.QSpinBox(self.uiNIOUDPSettingsGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
self.uiRemotePortSpinBox = QtWidgets.QSpinBox(self.uiNIOUDPSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiRemotePortSpinBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiRemotePortSpinBox.setSizePolicy(sizePolicy)
|
||||
self.uiRemotePortSpinBox.setMaximum(65535)
|
||||
self.uiRemotePortSpinBox.setProperty("value", 20000)
|
||||
self.uiRemotePortSpinBox.setObjectName(_fromUtf8("uiRemotePortSpinBox"))
|
||||
self.uiRemotePortSpinBox.setObjectName("uiRemotePortSpinBox")
|
||||
self.gridlayout3.addWidget(self.uiRemotePortSpinBox, 2, 1, 1, 1)
|
||||
self.gridlayout2.addWidget(self.uiNIOUDPSettingsGroupBox, 0, 0, 1, 2)
|
||||
self.uiNIOUDPListGroupBox = QtGui.QGroupBox(self.tab_2)
|
||||
self.uiNIOUDPListGroupBox.setObjectName(_fromUtf8("uiNIOUDPListGroupBox"))
|
||||
self.vboxlayout2 = QtGui.QVBoxLayout(self.uiNIOUDPListGroupBox)
|
||||
self.vboxlayout2.setObjectName(_fromUtf8("vboxlayout2"))
|
||||
self.uiNIOUDPListWidget = QtGui.QListWidget(self.uiNIOUDPListGroupBox)
|
||||
self.uiNIOUDPListWidget.setObjectName(_fromUtf8("uiNIOUDPListWidget"))
|
||||
self.uiNIOUDPListGroupBox = QtWidgets.QGroupBox(self.NIOUDPTab)
|
||||
self.uiNIOUDPListGroupBox.setObjectName("uiNIOUDPListGroupBox")
|
||||
self.vboxlayout2 = QtWidgets.QVBoxLayout(self.uiNIOUDPListGroupBox)
|
||||
self.vboxlayout2.setObjectName("vboxlayout2")
|
||||
self.uiNIOUDPListWidget = QtWidgets.QListWidget(self.uiNIOUDPListGroupBox)
|
||||
self.uiNIOUDPListWidget.setObjectName("uiNIOUDPListWidget")
|
||||
self.vboxlayout2.addWidget(self.uiNIOUDPListWidget)
|
||||
self.gridlayout2.addWidget(self.uiNIOUDPListGroupBox, 0, 2, 2, 1)
|
||||
self.uiAddNIOUDPPushButton = QtGui.QPushButton(self.tab_2)
|
||||
self.uiAddNIOUDPPushButton.setObjectName(_fromUtf8("uiAddNIOUDPPushButton"))
|
||||
self.uiAddNIOUDPPushButton = QtWidgets.QPushButton(self.NIOUDPTab)
|
||||
self.uiAddNIOUDPPushButton.setObjectName("uiAddNIOUDPPushButton")
|
||||
self.gridlayout2.addWidget(self.uiAddNIOUDPPushButton, 1, 0, 1, 1)
|
||||
self.uiDeleteNIOUDPPushButton = QtGui.QPushButton(self.tab_2)
|
||||
self.uiDeleteNIOUDPPushButton = QtWidgets.QPushButton(self.NIOUDPTab)
|
||||
self.uiDeleteNIOUDPPushButton.setEnabled(False)
|
||||
self.uiDeleteNIOUDPPushButton.setObjectName(_fromUtf8("uiDeleteNIOUDPPushButton"))
|
||||
self.uiDeleteNIOUDPPushButton.setObjectName("uiDeleteNIOUDPPushButton")
|
||||
self.gridlayout2.addWidget(self.uiDeleteNIOUDPPushButton, 1, 1, 1, 1)
|
||||
spacerItem1 = QtGui.QSpacerItem(20, 211, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridlayout2.addItem(spacerItem1, 2, 1, 1, 1)
|
||||
self.tabWidget.addTab(self.tab_2, _fromUtf8(""))
|
||||
self.tab_3 = QtGui.QWidget()
|
||||
self.tab_3.setObjectName(_fromUtf8("tab_3"))
|
||||
self.vboxlayout3 = QtGui.QVBoxLayout(self.tab_3)
|
||||
self.vboxlayout3.setObjectName(_fromUtf8("vboxlayout3"))
|
||||
self.uiNIOTAPGroupBox = QtGui.QGroupBox(self.tab_3)
|
||||
self.uiNIOTAPGroupBox.setObjectName(_fromUtf8("uiNIOTAPGroupBox"))
|
||||
self.gridlayout4 = QtGui.QGridLayout(self.uiNIOTAPGroupBox)
|
||||
self.gridlayout4.setObjectName(_fromUtf8("gridlayout4"))
|
||||
self.uiNIOTAPLineEdit = QtGui.QLineEdit(self.uiNIOTAPGroupBox)
|
||||
self.uiNIOTAPLineEdit.setObjectName(_fromUtf8("uiNIOTAPLineEdit"))
|
||||
spacerItem3 = QtWidgets.QSpacerItem(20, 211, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridlayout2.addItem(spacerItem3, 2, 1, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.NIOUDPTab, "")
|
||||
self.NIOTAPTab = QtWidgets.QWidget()
|
||||
self.NIOTAPTab.setObjectName("NIOTAPTab")
|
||||
self.vboxlayout3 = QtWidgets.QVBoxLayout(self.NIOTAPTab)
|
||||
self.vboxlayout3.setObjectName("vboxlayout3")
|
||||
self.uiNIOTAPGroupBox = QtWidgets.QGroupBox(self.NIOTAPTab)
|
||||
self.uiNIOTAPGroupBox.setObjectName("uiNIOTAPGroupBox")
|
||||
self.gridlayout4 = QtWidgets.QGridLayout(self.uiNIOTAPGroupBox)
|
||||
self.gridlayout4.setObjectName("gridlayout4")
|
||||
self.uiNIOTAPLineEdit = QtWidgets.QLineEdit(self.uiNIOTAPGroupBox)
|
||||
self.uiNIOTAPLineEdit.setObjectName("uiNIOTAPLineEdit")
|
||||
self.gridlayout4.addWidget(self.uiNIOTAPLineEdit, 0, 0, 1, 1)
|
||||
self.uiAddNIOTAPPushButton = QtGui.QPushButton(self.uiNIOTAPGroupBox)
|
||||
self.uiAddNIOTAPPushButton.setObjectName(_fromUtf8("uiAddNIOTAPPushButton"))
|
||||
self.uiAddNIOTAPPushButton = QtWidgets.QPushButton(self.uiNIOTAPGroupBox)
|
||||
self.uiAddNIOTAPPushButton.setObjectName("uiAddNIOTAPPushButton")
|
||||
self.gridlayout4.addWidget(self.uiAddNIOTAPPushButton, 0, 1, 1, 1)
|
||||
self.uiDeleteNIOTAPPushButton = QtGui.QPushButton(self.uiNIOTAPGroupBox)
|
||||
self.uiDeleteNIOTAPPushButton = QtWidgets.QPushButton(self.uiNIOTAPGroupBox)
|
||||
self.uiDeleteNIOTAPPushButton.setEnabled(False)
|
||||
self.uiDeleteNIOTAPPushButton.setObjectName(_fromUtf8("uiDeleteNIOTAPPushButton"))
|
||||
self.uiDeleteNIOTAPPushButton.setObjectName("uiDeleteNIOTAPPushButton")
|
||||
self.gridlayout4.addWidget(self.uiDeleteNIOTAPPushButton, 0, 2, 1, 1)
|
||||
self.uiNIOTAPListWidget = QtGui.QListWidget(self.uiNIOTAPGroupBox)
|
||||
self.uiNIOTAPListWidget.setObjectName(_fromUtf8("uiNIOTAPListWidget"))
|
||||
self.uiNIOTAPListWidget = QtWidgets.QListWidget(self.uiNIOTAPGroupBox)
|
||||
self.uiNIOTAPListWidget.setObjectName("uiNIOTAPListWidget")
|
||||
self.gridlayout4.addWidget(self.uiNIOTAPListWidget, 1, 0, 1, 3)
|
||||
self.vboxlayout3.addWidget(self.uiNIOTAPGroupBox)
|
||||
spacerItem2 = QtGui.QSpacerItem(20, 191, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.vboxlayout3.addItem(spacerItem2)
|
||||
self.tabWidget.addTab(self.tab_3, _fromUtf8(""))
|
||||
self.tab_4 = QtGui.QWidget()
|
||||
self.tab_4.setObjectName(_fromUtf8("tab_4"))
|
||||
self.gridlayout5 = QtGui.QGridLayout(self.tab_4)
|
||||
self.gridlayout5.setObjectName(_fromUtf8("gridlayout5"))
|
||||
self.uiNIOUNIXSettingsGroupBox = QtGui.QGroupBox(self.tab_4)
|
||||
self.uiNIOUNIXSettingsGroupBox.setObjectName(_fromUtf8("uiNIOUNIXSettingsGroupBox"))
|
||||
self.gridlayout6 = QtGui.QGridLayout(self.uiNIOUNIXSettingsGroupBox)
|
||||
self.gridlayout6.setObjectName(_fromUtf8("gridlayout6"))
|
||||
self.gridlayout7 = QtGui.QGridLayout()
|
||||
self.gridlayout7.setObjectName(_fromUtf8("gridlayout7"))
|
||||
self.uiLocalFileLabel = QtGui.QLabel(self.uiNIOUNIXSettingsGroupBox)
|
||||
self.uiLocalFileLabel.setObjectName(_fromUtf8("uiLocalFileLabel"))
|
||||
spacerItem4 = QtWidgets.QSpacerItem(20, 191, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.vboxlayout3.addItem(spacerItem4)
|
||||
self.uiNIOsTabWidget.addTab(self.NIOTAPTab, "")
|
||||
self.NIOUnixTab = QtWidgets.QWidget()
|
||||
self.NIOUnixTab.setObjectName("NIOUnixTab")
|
||||
self.gridlayout5 = QtWidgets.QGridLayout(self.NIOUnixTab)
|
||||
self.gridlayout5.setObjectName("gridlayout5")
|
||||
self.uiNIOUNIXSettingsGroupBox = QtWidgets.QGroupBox(self.NIOUnixTab)
|
||||
self.uiNIOUNIXSettingsGroupBox.setObjectName("uiNIOUNIXSettingsGroupBox")
|
||||
self.gridlayout6 = QtWidgets.QGridLayout(self.uiNIOUNIXSettingsGroupBox)
|
||||
self.gridlayout6.setObjectName("gridlayout6")
|
||||
self.gridlayout7 = QtWidgets.QGridLayout()
|
||||
self.gridlayout7.setObjectName("gridlayout7")
|
||||
self.uiLocalFileLabel = QtWidgets.QLabel(self.uiNIOUNIXSettingsGroupBox)
|
||||
self.uiLocalFileLabel.setObjectName("uiLocalFileLabel")
|
||||
self.gridlayout7.addWidget(self.uiLocalFileLabel, 0, 0, 1, 1)
|
||||
self.uiLocalFileLineEdit = QtGui.QLineEdit(self.uiNIOUNIXSettingsGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
self.uiLocalFileLineEdit = QtWidgets.QLineEdit(self.uiNIOUNIXSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiLocalFileLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiLocalFileLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiLocalFileLineEdit.setObjectName(_fromUtf8("uiLocalFileLineEdit"))
|
||||
self.uiLocalFileLineEdit.setObjectName("uiLocalFileLineEdit")
|
||||
self.gridlayout7.addWidget(self.uiLocalFileLineEdit, 1, 0, 1, 1)
|
||||
self.gridlayout6.addLayout(self.gridlayout7, 0, 0, 1, 1)
|
||||
self.gridlayout8 = QtGui.QGridLayout()
|
||||
self.gridlayout8.setObjectName(_fromUtf8("gridlayout8"))
|
||||
self.uiRemoteFileLabel = QtGui.QLabel(self.uiNIOUNIXSettingsGroupBox)
|
||||
self.uiRemoteFileLabel.setObjectName(_fromUtf8("uiRemoteFileLabel"))
|
||||
self.gridlayout8 = QtWidgets.QGridLayout()
|
||||
self.gridlayout8.setObjectName("gridlayout8")
|
||||
self.uiRemoteFileLabel = QtWidgets.QLabel(self.uiNIOUNIXSettingsGroupBox)
|
||||
self.uiRemoteFileLabel.setObjectName("uiRemoteFileLabel")
|
||||
self.gridlayout8.addWidget(self.uiRemoteFileLabel, 0, 0, 1, 1)
|
||||
self.uiRemoteFileLineEdit = QtGui.QLineEdit(self.uiNIOUNIXSettingsGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
self.uiRemoteFileLineEdit = QtWidgets.QLineEdit(self.uiNIOUNIXSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiRemoteFileLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiRemoteFileLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiRemoteFileLineEdit.setObjectName(_fromUtf8("uiRemoteFileLineEdit"))
|
||||
self.uiRemoteFileLineEdit.setObjectName("uiRemoteFileLineEdit")
|
||||
self.gridlayout8.addWidget(self.uiRemoteFileLineEdit, 1, 0, 1, 1)
|
||||
self.gridlayout6.addLayout(self.gridlayout8, 1, 0, 1, 1)
|
||||
self.gridlayout5.addWidget(self.uiNIOUNIXSettingsGroupBox, 0, 0, 1, 2)
|
||||
self.uiNIOUNIXListGroupBox = QtGui.QGroupBox(self.tab_4)
|
||||
self.uiNIOUNIXListGroupBox.setObjectName(_fromUtf8("uiNIOUNIXListGroupBox"))
|
||||
self.vboxlayout4 = QtGui.QVBoxLayout(self.uiNIOUNIXListGroupBox)
|
||||
self.vboxlayout4.setObjectName(_fromUtf8("vboxlayout4"))
|
||||
self.uiNIOUNIXListWidget = QtGui.QListWidget(self.uiNIOUNIXListGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.uiNIOUNIXListGroupBox = QtWidgets.QGroupBox(self.NIOUnixTab)
|
||||
self.uiNIOUNIXListGroupBox.setObjectName("uiNIOUNIXListGroupBox")
|
||||
self.vboxlayout4 = QtWidgets.QVBoxLayout(self.uiNIOUNIXListGroupBox)
|
||||
self.vboxlayout4.setObjectName("vboxlayout4")
|
||||
self.uiNIOUNIXListWidget = QtWidgets.QListWidget(self.uiNIOUNIXListGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIOUNIXListWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIOUNIXListWidget.setSizePolicy(sizePolicy)
|
||||
self.uiNIOUNIXListWidget.setObjectName(_fromUtf8("uiNIOUNIXListWidget"))
|
||||
self.uiNIOUNIXListWidget.setObjectName("uiNIOUNIXListWidget")
|
||||
self.vboxlayout4.addWidget(self.uiNIOUNIXListWidget)
|
||||
self.gridlayout5.addWidget(self.uiNIOUNIXListGroupBox, 0, 2, 3, 1)
|
||||
self.uiAddNIOUNIXPushButton = QtGui.QPushButton(self.tab_4)
|
||||
self.uiAddNIOUNIXPushButton.setObjectName(_fromUtf8("uiAddNIOUNIXPushButton"))
|
||||
self.uiAddNIOUNIXPushButton = QtWidgets.QPushButton(self.NIOUnixTab)
|
||||
self.uiAddNIOUNIXPushButton.setObjectName("uiAddNIOUNIXPushButton")
|
||||
self.gridlayout5.addWidget(self.uiAddNIOUNIXPushButton, 1, 0, 1, 1)
|
||||
self.uiDeleteNIOUNIXPushButton = QtGui.QPushButton(self.tab_4)
|
||||
self.uiDeleteNIOUNIXPushButton = QtWidgets.QPushButton(self.NIOUnixTab)
|
||||
self.uiDeleteNIOUNIXPushButton.setEnabled(False)
|
||||
self.uiDeleteNIOUNIXPushButton.setObjectName(_fromUtf8("uiDeleteNIOUNIXPushButton"))
|
||||
self.uiDeleteNIOUNIXPushButton.setObjectName("uiDeleteNIOUNIXPushButton")
|
||||
self.gridlayout5.addWidget(self.uiDeleteNIOUNIXPushButton, 1, 1, 1, 1)
|
||||
spacerItem3 = QtGui.QSpacerItem(160, 190, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
|
||||
self.gridlayout5.addItem(spacerItem3, 2, 0, 2, 2)
|
||||
spacerItem4 = QtGui.QSpacerItem(196, 132, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridlayout5.addItem(spacerItem4, 3, 2, 1, 1)
|
||||
self.tabWidget.addTab(self.tab_4, _fromUtf8(""))
|
||||
self.tab_5 = QtGui.QWidget()
|
||||
self.tab_5.setObjectName(_fromUtf8("tab_5"))
|
||||
self.gridlayout9 = QtGui.QGridLayout(self.tab_5)
|
||||
self.gridlayout9.setObjectName(_fromUtf8("gridlayout9"))
|
||||
self.uiNIOVDESettingsGroupBox = QtGui.QGroupBox(self.tab_5)
|
||||
self.uiNIOVDESettingsGroupBox.setObjectName(_fromUtf8("uiNIOVDESettingsGroupBox"))
|
||||
self.gridlayout10 = QtGui.QGridLayout(self.uiNIOVDESettingsGroupBox)
|
||||
self.gridlayout10.setObjectName(_fromUtf8("gridlayout10"))
|
||||
self.gridlayout11 = QtGui.QGridLayout()
|
||||
self.gridlayout11.setObjectName(_fromUtf8("gridlayout11"))
|
||||
self.uiVDEControlFileLabel = QtGui.QLabel(self.uiNIOVDESettingsGroupBox)
|
||||
self.uiVDEControlFileLabel.setObjectName(_fromUtf8("uiVDEControlFileLabel"))
|
||||
spacerItem5 = QtWidgets.QSpacerItem(160, 190, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
self.gridlayout5.addItem(spacerItem5, 2, 0, 2, 2)
|
||||
spacerItem6 = QtWidgets.QSpacerItem(196, 132, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridlayout5.addItem(spacerItem6, 3, 2, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.NIOUnixTab, "")
|
||||
self.NIOVDETab = QtWidgets.QWidget()
|
||||
self.NIOVDETab.setObjectName("NIOVDETab")
|
||||
self.gridlayout9 = QtWidgets.QGridLayout(self.NIOVDETab)
|
||||
self.gridlayout9.setObjectName("gridlayout9")
|
||||
self.uiNIOVDESettingsGroupBox = QtWidgets.QGroupBox(self.NIOVDETab)
|
||||
self.uiNIOVDESettingsGroupBox.setObjectName("uiNIOVDESettingsGroupBox")
|
||||
self.gridlayout10 = QtWidgets.QGridLayout(self.uiNIOVDESettingsGroupBox)
|
||||
self.gridlayout10.setObjectName("gridlayout10")
|
||||
self.gridlayout11 = QtWidgets.QGridLayout()
|
||||
self.gridlayout11.setObjectName("gridlayout11")
|
||||
self.uiVDEControlFileLabel = QtWidgets.QLabel(self.uiNIOVDESettingsGroupBox)
|
||||
self.uiVDEControlFileLabel.setObjectName("uiVDEControlFileLabel")
|
||||
self.gridlayout11.addWidget(self.uiVDEControlFileLabel, 0, 0, 1, 1)
|
||||
self.uiVDEControlFileLineEdit = QtGui.QLineEdit(self.uiNIOVDESettingsGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
self.uiVDEControlFileLineEdit = QtWidgets.QLineEdit(self.uiNIOVDESettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiVDEControlFileLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiVDEControlFileLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiVDEControlFileLineEdit.setObjectName(_fromUtf8("uiVDEControlFileLineEdit"))
|
||||
self.uiVDEControlFileLineEdit.setObjectName("uiVDEControlFileLineEdit")
|
||||
self.gridlayout11.addWidget(self.uiVDEControlFileLineEdit, 1, 0, 1, 1)
|
||||
self.gridlayout10.addLayout(self.gridlayout11, 0, 0, 1, 1)
|
||||
self.gridlayout12 = QtGui.QGridLayout()
|
||||
self.gridlayout12.setObjectName(_fromUtf8("gridlayout12"))
|
||||
self.uiVDELocalFileLabel = QtGui.QLabel(self.uiNIOVDESettingsGroupBox)
|
||||
self.uiVDELocalFileLabel.setObjectName(_fromUtf8("uiVDELocalFileLabel"))
|
||||
self.gridlayout12 = QtWidgets.QGridLayout()
|
||||
self.gridlayout12.setObjectName("gridlayout12")
|
||||
self.uiVDELocalFileLabel = QtWidgets.QLabel(self.uiNIOVDESettingsGroupBox)
|
||||
self.uiVDELocalFileLabel.setObjectName("uiVDELocalFileLabel")
|
||||
self.gridlayout12.addWidget(self.uiVDELocalFileLabel, 0, 0, 1, 1)
|
||||
self.uiVDELocalFileLineEdit = QtGui.QLineEdit(self.uiNIOVDESettingsGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
self.uiVDELocalFileLineEdit = QtWidgets.QLineEdit(self.uiNIOVDESettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiVDELocalFileLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiVDELocalFileLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiVDELocalFileLineEdit.setObjectName(_fromUtf8("uiVDELocalFileLineEdit"))
|
||||
self.uiVDELocalFileLineEdit.setObjectName("uiVDELocalFileLineEdit")
|
||||
self.gridlayout12.addWidget(self.uiVDELocalFileLineEdit, 1, 0, 1, 1)
|
||||
self.gridlayout10.addLayout(self.gridlayout12, 1, 0, 1, 1)
|
||||
self.gridlayout9.addWidget(self.uiNIOVDESettingsGroupBox, 0, 0, 1, 2)
|
||||
self.uiNIOVDEListGroupBox = QtGui.QGroupBox(self.tab_5)
|
||||
self.uiNIOVDEListGroupBox.setObjectName(_fromUtf8("uiNIOVDEListGroupBox"))
|
||||
self.vboxlayout5 = QtGui.QVBoxLayout(self.uiNIOVDEListGroupBox)
|
||||
self.vboxlayout5.setObjectName(_fromUtf8("vboxlayout5"))
|
||||
self.uiNIOVDEListWidget = QtGui.QListWidget(self.uiNIOVDEListGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.uiNIOVDEListGroupBox = QtWidgets.QGroupBox(self.NIOVDETab)
|
||||
self.uiNIOVDEListGroupBox.setObjectName("uiNIOVDEListGroupBox")
|
||||
self.vboxlayout5 = QtWidgets.QVBoxLayout(self.uiNIOVDEListGroupBox)
|
||||
self.vboxlayout5.setObjectName("vboxlayout5")
|
||||
self.uiNIOVDEListWidget = QtWidgets.QListWidget(self.uiNIOVDEListGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIOVDEListWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIOVDEListWidget.setSizePolicy(sizePolicy)
|
||||
self.uiNIOVDEListWidget.setObjectName(_fromUtf8("uiNIOVDEListWidget"))
|
||||
self.uiNIOVDEListWidget.setObjectName("uiNIOVDEListWidget")
|
||||
self.vboxlayout5.addWidget(self.uiNIOVDEListWidget)
|
||||
self.gridlayout9.addWidget(self.uiNIOVDEListGroupBox, 0, 2, 3, 1)
|
||||
self.uiAddNIOVDEPushButton = QtGui.QPushButton(self.tab_5)
|
||||
self.uiAddNIOVDEPushButton.setObjectName(_fromUtf8("uiAddNIOVDEPushButton"))
|
||||
self.uiAddNIOVDEPushButton = QtWidgets.QPushButton(self.NIOVDETab)
|
||||
self.uiAddNIOVDEPushButton.setObjectName("uiAddNIOVDEPushButton")
|
||||
self.gridlayout9.addWidget(self.uiAddNIOVDEPushButton, 1, 0, 1, 1)
|
||||
self.uiDeleteNIOVDEPushButton = QtGui.QPushButton(self.tab_5)
|
||||
self.uiDeleteNIOVDEPushButton = QtWidgets.QPushButton(self.NIOVDETab)
|
||||
self.uiDeleteNIOVDEPushButton.setEnabled(False)
|
||||
self.uiDeleteNIOVDEPushButton.setObjectName(_fromUtf8("uiDeleteNIOVDEPushButton"))
|
||||
self.uiDeleteNIOVDEPushButton.setObjectName("uiDeleteNIOVDEPushButton")
|
||||
self.gridlayout9.addWidget(self.uiDeleteNIOVDEPushButton, 1, 1, 1, 1)
|
||||
spacerItem5 = QtGui.QSpacerItem(161, 201, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
|
||||
self.gridlayout9.addItem(spacerItem5, 2, 0, 2, 2)
|
||||
spacerItem6 = QtGui.QSpacerItem(196, 132, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridlayout9.addItem(spacerItem6, 3, 2, 1, 1)
|
||||
self.tabWidget.addTab(self.tab_5, _fromUtf8(""))
|
||||
self.tab_6 = QtGui.QWidget()
|
||||
self.tab_6.setObjectName(_fromUtf8("tab_6"))
|
||||
self.gridlayout13 = QtGui.QGridLayout(self.tab_6)
|
||||
self.gridlayout13.setObjectName(_fromUtf8("gridlayout13"))
|
||||
self.uiNIONullSettingsGroupBox = QtGui.QGroupBox(self.tab_6)
|
||||
self.uiNIONullSettingsGroupBox.setObjectName(_fromUtf8("uiNIONullSettingsGroupBox"))
|
||||
self.gridlayout14 = QtGui.QGridLayout(self.uiNIONullSettingsGroupBox)
|
||||
self.gridlayout14.setObjectName(_fromUtf8("gridlayout14"))
|
||||
self.label_9 = QtGui.QLabel(self.uiNIONullSettingsGroupBox)
|
||||
self.label_9.setObjectName(_fromUtf8("label_9"))
|
||||
self.gridlayout14.addWidget(self.label_9, 0, 0, 1, 1)
|
||||
self.uiNIONullIdentiferLineEdit = QtGui.QLineEdit(self.uiNIONullSettingsGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
spacerItem7 = QtWidgets.QSpacerItem(161, 201, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
self.gridlayout9.addItem(spacerItem7, 2, 0, 2, 2)
|
||||
spacerItem8 = QtWidgets.QSpacerItem(196, 132, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridlayout9.addItem(spacerItem8, 3, 2, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.NIOVDETab, "")
|
||||
self.NIONullTab = QtWidgets.QWidget()
|
||||
self.NIONullTab.setObjectName("NIONullTab")
|
||||
self.gridlayout13 = QtWidgets.QGridLayout(self.NIONullTab)
|
||||
self.gridlayout13.setObjectName("gridlayout13")
|
||||
self.uiNIONullSettingsGroupBox = QtWidgets.QGroupBox(self.NIONullTab)
|
||||
self.uiNIONullSettingsGroupBox.setObjectName("uiNIONullSettingsGroupBox")
|
||||
self.gridlayout14 = QtWidgets.QGridLayout(self.uiNIONullSettingsGroupBox)
|
||||
self.gridlayout14.setObjectName("gridlayout14")
|
||||
self.uiNIONullIdentifierLabel = QtWidgets.QLabel(self.uiNIONullSettingsGroupBox)
|
||||
self.uiNIONullIdentifierLabel.setObjectName("uiNIONullIdentifierLabel")
|
||||
self.gridlayout14.addWidget(self.uiNIONullIdentifierLabel, 0, 0, 1, 1)
|
||||
self.uiNIONullIdentiferLineEdit = QtWidgets.QLineEdit(self.uiNIONullSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIONullIdentiferLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIONullIdentiferLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiNIONullIdentiferLineEdit.setObjectName(_fromUtf8("uiNIONullIdentiferLineEdit"))
|
||||
self.uiNIONullIdentiferLineEdit.setObjectName("uiNIONullIdentiferLineEdit")
|
||||
self.gridlayout14.addWidget(self.uiNIONullIdentiferLineEdit, 1, 0, 1, 1)
|
||||
self.gridlayout13.addWidget(self.uiNIONullSettingsGroupBox, 0, 0, 1, 2)
|
||||
self.uiNIONullListGroupBox = QtGui.QGroupBox(self.tab_6)
|
||||
self.uiNIONullListGroupBox.setObjectName(_fromUtf8("uiNIONullListGroupBox"))
|
||||
self.vboxlayout6 = QtGui.QVBoxLayout(self.uiNIONullListGroupBox)
|
||||
self.vboxlayout6.setObjectName(_fromUtf8("vboxlayout6"))
|
||||
self.uiNIONullListWidget = QtGui.QListWidget(self.uiNIONullListGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.uiNIONullListGroupBox = QtWidgets.QGroupBox(self.NIONullTab)
|
||||
self.uiNIONullListGroupBox.setObjectName("uiNIONullListGroupBox")
|
||||
self.vboxlayout6 = QtWidgets.QVBoxLayout(self.uiNIONullListGroupBox)
|
||||
self.vboxlayout6.setObjectName("vboxlayout6")
|
||||
self.uiNIONullListWidget = QtWidgets.QListWidget(self.uiNIONullListGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIONullListWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIONullListWidget.setSizePolicy(sizePolicy)
|
||||
self.uiNIONullListWidget.setObjectName(_fromUtf8("uiNIONullListWidget"))
|
||||
self.uiNIONullListWidget.setObjectName("uiNIONullListWidget")
|
||||
self.vboxlayout6.addWidget(self.uiNIONullListWidget)
|
||||
self.gridlayout13.addWidget(self.uiNIONullListGroupBox, 0, 2, 3, 1)
|
||||
self.uiAddNIONullPushButton = QtGui.QPushButton(self.tab_6)
|
||||
self.uiAddNIONullPushButton.setObjectName(_fromUtf8("uiAddNIONullPushButton"))
|
||||
self.uiAddNIONullPushButton = QtWidgets.QPushButton(self.NIONullTab)
|
||||
self.uiAddNIONullPushButton.setObjectName("uiAddNIONullPushButton")
|
||||
self.gridlayout13.addWidget(self.uiAddNIONullPushButton, 1, 0, 1, 1)
|
||||
self.uiDeleteNIONullPushButton = QtGui.QPushButton(self.tab_6)
|
||||
self.uiDeleteNIONullPushButton = QtWidgets.QPushButton(self.NIONullTab)
|
||||
self.uiDeleteNIONullPushButton.setEnabled(False)
|
||||
self.uiDeleteNIONullPushButton.setObjectName(_fromUtf8("uiDeleteNIONullPushButton"))
|
||||
self.uiDeleteNIONullPushButton.setObjectName("uiDeleteNIONullPushButton")
|
||||
self.gridlayout13.addWidget(self.uiDeleteNIONullPushButton, 1, 1, 1, 1)
|
||||
spacerItem7 = QtGui.QSpacerItem(20, 261, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridlayout13.addItem(spacerItem7, 2, 0, 2, 2)
|
||||
spacerItem8 = QtGui.QSpacerItem(20, 181, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridlayout13.addItem(spacerItem8, 3, 2, 1, 1)
|
||||
self.tabWidget.addTab(self.tab_6, _fromUtf8(""))
|
||||
self.tab_7 = QtGui.QWidget()
|
||||
self.tab_7.setObjectName(_fromUtf8("tab_7"))
|
||||
self.gridLayout = QtGui.QGridLayout(self.tab_7)
|
||||
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
|
||||
self.uiNameLabel = QtGui.QLabel(self.tab_7)
|
||||
self.uiNameLabel.setObjectName(_fromUtf8("uiNameLabel"))
|
||||
spacerItem9 = QtWidgets.QSpacerItem(20, 261, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridlayout13.addItem(spacerItem9, 2, 0, 2, 2)
|
||||
spacerItem10 = QtWidgets.QSpacerItem(20, 181, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridlayout13.addItem(spacerItem10, 3, 2, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.NIONullTab, "")
|
||||
self.MiscTab = QtWidgets.QWidget()
|
||||
self.MiscTab.setObjectName("MiscTab")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.MiscTab)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.uiNameLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.uiNameLabel.setObjectName("uiNameLabel")
|
||||
self.gridLayout.addWidget(self.uiNameLabel, 0, 0, 1, 1)
|
||||
self.uiNameLineEdit = QtGui.QLineEdit(self.tab_7)
|
||||
self.uiNameLineEdit.setObjectName(_fromUtf8("uiNameLineEdit"))
|
||||
self.uiNameLineEdit = QtWidgets.QLineEdit(self.MiscTab)
|
||||
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
|
||||
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 1, 1, 1)
|
||||
spacerItem9 = QtGui.QSpacerItem(20, 399, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridLayout.addItem(spacerItem9, 1, 1, 1, 1)
|
||||
self.tabWidget.addTab(self.tab_7, _fromUtf8(""))
|
||||
self.vboxlayout.addWidget(self.tabWidget)
|
||||
spacerItem11 = QtWidgets.QSpacerItem(20, 399, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout.addItem(spacerItem11, 1, 1, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.MiscTab, "")
|
||||
self.vboxlayout.addWidget(self.uiNIOsTabWidget)
|
||||
|
||||
self.retranslateUi(cloudConfigPageWidget)
|
||||
self.tabWidget.setCurrentIndex(0)
|
||||
self.uiNIOsTabWidget.setCurrentIndex(0)
|
||||
QtCore.QMetaObject.connectSlotsByName(cloudConfigPageWidget)
|
||||
|
||||
def retranslateUi(self, cloudConfigPageWidget):
|
||||
cloudConfigPageWidget.setWindowTitle(_translate("cloudConfigPageWidget", "Cloud configuration", None))
|
||||
self.uiGenericEthernetGroupBox.setTitle(_translate("cloudConfigPageWidget", "Generic Ethernet NIO (Administrator or root access required)", None))
|
||||
self.uiAddGenericEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add", None))
|
||||
self.uiDeleteGenericEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete", None))
|
||||
self.uiLinuxEthernetGroupBox.setTitle(_translate("cloudConfigPageWidget", "Linux Ethernet NIO (Linux only, root access required)", None))
|
||||
self.uiAddLinuxEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add", None))
|
||||
self.uiDeleteLinuxEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete", None))
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("cloudConfigPageWidget", "NIO Ethernet", None))
|
||||
self.uiNIOUDPSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings", None))
|
||||
self.uiLocalPortLabel.setText(_translate("cloudConfigPageWidget", "Local port:", None))
|
||||
self.uiRemoteHostLabel.setText(_translate("cloudConfigPageWidget", "Remote host:", None))
|
||||
self.uiRemoteHostLineEdit.setText(_translate("cloudConfigPageWidget", "127.0.0.1", None))
|
||||
self.uiRemotePortLabel.setText(_translate("cloudConfigPageWidget", "Remote port:", None))
|
||||
self.uiNIOUDPListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs", None))
|
||||
self.uiAddNIOUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Add", None))
|
||||
self.uiDeleteNIOUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete", None))
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("cloudConfigPageWidget", "NIO UDP", None))
|
||||
self.uiNIOTAPGroupBox.setTitle(_translate("cloudConfigPageWidget", "TAP interface (require root access)", None))
|
||||
self.uiAddNIOTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add", None))
|
||||
self.uiDeleteNIOTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete", None))
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), _translate("cloudConfigPageWidget", "NIO TAP", None))
|
||||
self.uiNIOUNIXSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings", None))
|
||||
self.uiLocalFileLabel.setText(_translate("cloudConfigPageWidget", "Local file:", None))
|
||||
self.uiRemoteFileLabel.setText(_translate("cloudConfigPageWidget", "Remote file:", None))
|
||||
self.uiNIOUNIXListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs", None))
|
||||
self.uiAddNIOUNIXPushButton.setText(_translate("cloudConfigPageWidget", "&Add", None))
|
||||
self.uiDeleteNIOUNIXPushButton.setText(_translate("cloudConfigPageWidget", "&Delete", None))
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), _translate("cloudConfigPageWidget", "NIO UNIX", None))
|
||||
self.uiNIOVDESettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings", None))
|
||||
self.uiVDEControlFileLabel.setText(_translate("cloudConfigPageWidget", "Control file:", None))
|
||||
self.uiVDELocalFileLabel.setText(_translate("cloudConfigPageWidget", "Local file:", None))
|
||||
self.uiNIOVDEListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs", None))
|
||||
self.uiAddNIOVDEPushButton.setText(_translate("cloudConfigPageWidget", "&Add", None))
|
||||
self.uiDeleteNIOVDEPushButton.setText(_translate("cloudConfigPageWidget", "&Delete", None))
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_5), _translate("cloudConfigPageWidget", "NIO VDE", None))
|
||||
self.uiNIONullSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings", None))
|
||||
self.label_9.setText(_translate("cloudConfigPageWidget", "Identifier:", None))
|
||||
self.uiNIONullListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs", None))
|
||||
self.uiAddNIONullPushButton.setText(_translate("cloudConfigPageWidget", "&Add", None))
|
||||
self.uiDeleteNIONullPushButton.setText(_translate("cloudConfigPageWidget", "&Delete", None))
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_6), _translate("cloudConfigPageWidget", "NIO NULL", None))
|
||||
self.uiNameLabel.setText(_translate("cloudConfigPageWidget", "Name:", None))
|
||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_7), _translate("cloudConfigPageWidget", "Misc.", None))
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
cloudConfigPageWidget.setWindowTitle(_translate("cloudConfigPageWidget", "Cloud configuration"))
|
||||
self.uiGenericEthernetGroupBox.setTitle(_translate("cloudConfigPageWidget", "Generic Ethernet NIO (Administrator or root access required)"))
|
||||
self.uiAddGenericEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteGenericEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiLinuxEthernetGroupBox.setTitle(_translate("cloudConfigPageWidget", "Linux Ethernet NIO (Linux only, root access required)"))
|
||||
self.uiAddLinuxEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteLinuxEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOEthernetTab), _translate("cloudConfigPageWidget", "Ethernet"))
|
||||
self.uiNIONATSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
|
||||
self.uiNIONATIdentifierLabel.setText(_translate("cloudConfigPageWidget", "Local identifier:"))
|
||||
self.uiNIONATListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
|
||||
self.uiAddNIONATPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIONATPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIONATTab), _translate("cloudConfigPageWidget", "NAT"))
|
||||
self.uiNIOUDPSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
|
||||
self.uiLocalPortLabel.setText(_translate("cloudConfigPageWidget", "Local port:"))
|
||||
self.uiRemoteHostLabel.setText(_translate("cloudConfigPageWidget", "Remote host:"))
|
||||
self.uiRemoteHostLineEdit.setText(_translate("cloudConfigPageWidget", "127.0.0.1"))
|
||||
self.uiRemotePortLabel.setText(_translate("cloudConfigPageWidget", "Remote port:"))
|
||||
self.uiNIOUDPListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
|
||||
self.uiAddNIOUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIOUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOUDPTab), _translate("cloudConfigPageWidget", "UDP"))
|
||||
self.uiNIOTAPGroupBox.setTitle(_translate("cloudConfigPageWidget", "TAP interface (require root access)"))
|
||||
self.uiAddNIOTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIOTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOTAPTab), _translate("cloudConfigPageWidget", "TAP"))
|
||||
self.uiNIOUNIXSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
|
||||
self.uiLocalFileLabel.setText(_translate("cloudConfigPageWidget", "Local file:"))
|
||||
self.uiRemoteFileLabel.setText(_translate("cloudConfigPageWidget", "Remote file:"))
|
||||
self.uiNIOUNIXListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
|
||||
self.uiAddNIOUNIXPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIOUNIXPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOUnixTab), _translate("cloudConfigPageWidget", "UNIX"))
|
||||
self.uiNIOVDESettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
|
||||
self.uiVDEControlFileLabel.setText(_translate("cloudConfigPageWidget", "Control file:"))
|
||||
self.uiVDELocalFileLabel.setText(_translate("cloudConfigPageWidget", "Local file:"))
|
||||
self.uiNIOVDEListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
|
||||
self.uiAddNIOVDEPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIOVDEPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOVDETab), _translate("cloudConfigPageWidget", "VDE"))
|
||||
self.uiNIONullSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
|
||||
self.uiNIONullIdentifierLabel.setText(_translate("cloudConfigPageWidget", "Local identifier:"))
|
||||
self.uiNIONullListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
|
||||
self.uiAddNIONullPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIONullPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIONullTab), _translate("cloudConfigPageWidget", "NULL"))
|
||||
self.uiNameLabel.setText(_translate("cloudConfigPageWidget", "Name:"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.MiscTab), _translate("cloudConfigPageWidget", "Misc."))
|
||||
|
||||
254
gns3/modules/docker/__init__.py
Normal file
254
gns3/modules/docker/__init__.py
Normal file
@@ -0,0 +1,254 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Docker module implementation."""
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
|
||||
from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
from .docker_vm import DockerVM
|
||||
from .settings import DOCKER_SETTINGS, DOCKER_CONTAINER_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Docker(Module):
|
||||
"""Docker module."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._settings = {}
|
||||
self._docker_images = {}
|
||||
self._nodes = []
|
||||
|
||||
# load the settings
|
||||
self._loadSettings()
|
||||
self._loadDockerImages()
|
||||
|
||||
def configChangedSlot(self):
|
||||
# load the settings
|
||||
self._loadSettings()
|
||||
|
||||
def _loadSettings(self):
|
||||
"""Loads the settings from the persistent settings file."""
|
||||
local_config = LocalConfig.instance()
|
||||
self._settings = local_config.loadSectionSettings(
|
||||
self.__class__.__name__, DOCKER_SETTINGS)
|
||||
|
||||
def _saveSettings(self):
|
||||
"""Saves the settings to the persistent settings file."""
|
||||
LocalConfig.instance().saveSectionSettings(
|
||||
self.__class__.__name__, self._settings)
|
||||
|
||||
def _loadDockerImages(self):
|
||||
"""Load the Docker images from the persistent settings file."""
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
settings = local_config.settings()
|
||||
if "images" in settings.get(self.__class__.__name__, {}):
|
||||
for image in settings[self.__class__.__name__]["images"]:
|
||||
name = image.get("name")
|
||||
server = image.get("server")
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
if key in self._docker_images or not name or not server:
|
||||
continue
|
||||
container_settings = DOCKER_CONTAINER_SETTINGS.copy()
|
||||
container_settings.update(image)
|
||||
self._docker_images[key] = container_settings
|
||||
|
||||
def _saveDockerImages(self):
|
||||
"""Saves the Docker images to the persistent settings file."""
|
||||
|
||||
self._settings["images"] = list(self._docker_images.values())
|
||||
self._saveSettings()
|
||||
|
||||
def dockerImages(self):
|
||||
"""
|
||||
Returns Docker images settings.
|
||||
|
||||
:returns: Docker images settings
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
return self._docker_images
|
||||
|
||||
def setDockerImages(self, new_docker_images):
|
||||
"""Sets Docker image settings.
|
||||
|
||||
:param new_iou_images: Docker images settings (dictionary)
|
||||
"""
|
||||
self._docker_images = new_docker_images.copy()
|
||||
self._saveDockerImages()
|
||||
|
||||
def addNode(self, node):
|
||||
"""Adds a node to this module.
|
||||
|
||||
:param node: Node instance
|
||||
"""
|
||||
self._nodes.append(node)
|
||||
|
||||
def removeNode(self, node):
|
||||
"""Removes a node from this module.
|
||||
|
||||
:param node: Node instance
|
||||
"""
|
||||
if node in self._nodes:
|
||||
self._nodes.remove(node)
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns the module settings
|
||||
|
||||
:returns: module settings (dictionary)
|
||||
"""
|
||||
return self._settings
|
||||
|
||||
def setSettings(self, settings):
|
||||
"""Sets the module settings
|
||||
|
||||
:param settings: module settings (dictionary)
|
||||
"""
|
||||
self._settings.update(settings)
|
||||
self._saveSettings()
|
||||
|
||||
def createNode(self, node_class, server, project):
|
||||
"""Creates a new node.
|
||||
|
||||
:param node_class: Node object
|
||||
:param server: HTTPClient instance
|
||||
"""
|
||||
log.info("creating node {}".format(node_class))
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def setupNode(self, node, node_name):
|
||||
"""Sets up a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
log.info("configuring node {} with id {}".format(node, node.id()))
|
||||
|
||||
image = None
|
||||
if node_name:
|
||||
for image_key, info in self._docker_images.items():
|
||||
if node_name == info["imagename"]:
|
||||
image = image_key
|
||||
if not image:
|
||||
selected_images = []
|
||||
for image, info in self._docker_images.items():
|
||||
if info["server"] == node.server().host() or (
|
||||
node.server().isLocal() and info["server"] == "local"):
|
||||
selected_images.append(image)
|
||||
|
||||
if not selected_images:
|
||||
raise ModuleError("No Docker VM on server {}".format(
|
||||
node.server().url()))
|
||||
elif len(selected_images) > 1:
|
||||
from gns3.main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
|
||||
(selection, ok) = QtWidgets.QInputDialog.getItem(
|
||||
mainwindow, "Docker Image", "Please choose an image",
|
||||
selected_images, 0, False)
|
||||
if ok:
|
||||
image = selection
|
||||
else:
|
||||
raise ModuleError("Please select a Docker Image")
|
||||
else:
|
||||
image = selected_images[0]
|
||||
|
||||
image_settings = {}
|
||||
for setting_name, value in self._docker_images[image].items():
|
||||
if setting_name in node.settings() and value != "" and value is not None:
|
||||
image_settings[setting_name] = value
|
||||
imagename = self._docker_images[image]["imagename"]
|
||||
node.setup(imagename, additional_settings=image_settings)
|
||||
|
||||
def reset(self):
|
||||
"""Resets the servers."""
|
||||
log.info("Docker module reset")
|
||||
self._nodes.clear()
|
||||
|
||||
def getDockerImagesFromServer(self, server, callback):
|
||||
"""Gets the Docker images list from a server.
|
||||
|
||||
:param server: server to send the request to
|
||||
:param callback: callback for the reply from the server
|
||||
"""
|
||||
server.get("/docker/images", callback)
|
||||
|
||||
@staticmethod
|
||||
def getNodeClass(name):
|
||||
"""
|
||||
Returns the object with the corresponding name.
|
||||
|
||||
:param name: object name
|
||||
"""
|
||||
if name in globals():
|
||||
return globals()[name]
|
||||
|
||||
@staticmethod
|
||||
def classes():
|
||||
"""Returns all the node classes supported by this module.
|
||||
|
||||
:returns: list of classes
|
||||
"""
|
||||
return [DockerVM]
|
||||
|
||||
def nodes(self):
|
||||
"""
|
||||
Returns all the node data necessary to represent a node
|
||||
in the nodes view and create a node on the scene.
|
||||
"""
|
||||
nodes = []
|
||||
for docker_image in self._docker_images.values():
|
||||
nodes.append({
|
||||
"class": DockerVM.__name__,
|
||||
"name": docker_image["imagename"],
|
||||
"server": docker_image["server"],
|
||||
"symbol": docker_image["symbol"],
|
||||
"categories": [docker_image["category"]]
|
||||
})
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
def preferencePages():
|
||||
"""
|
||||
:returns: QWidget object list
|
||||
"""
|
||||
from .pages.docker_preferences_page import DockerPreferencesPage
|
||||
from .pages.docker_vm_preferences_page import DockerVMPreferencesPage
|
||||
|
||||
from gns3.local_config import LocalConfig
|
||||
if not LocalConfig.instance().experimental():
|
||||
return []
|
||||
|
||||
return [DockerPreferencesPage, DockerVMPreferencesPage]
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""Singleton to return only one instance of Docker module.
|
||||
|
||||
:returns: instance of Docker"""
|
||||
if not hasattr(Docker, "_instance"):
|
||||
Docker._instance = Docker()
|
||||
return Docker._instance
|
||||
113
gns3/modules/docker/dialogs/docker_vm_wizard.py
Normal file
113
gns3/modules/docker/dialogs/docker_vm_wizard.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Wizard for Docker images."""
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.docker_vm_wizard_ui import Ui_DockerVMWizard
|
||||
from .. import Docker
|
||||
|
||||
|
||||
class DockerVMWizard(VMWizard, Ui_DockerVMWizard):
|
||||
"""Wizard to create a Docker image.
|
||||
|
||||
:param docker_images: existing Docker images
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, docker_images, parent):
|
||||
|
||||
super().__init__(parent=parent, devices=[], use_local_server=True)
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(
|
||||
":/icons/docker.png"))
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
|
||||
self._docker_images = docker_images
|
||||
|
||||
if Docker.instance().settings()["use_local_server"]:
|
||||
# skip the server page if we use the local server
|
||||
self.setStartId(1)
|
||||
|
||||
def initializePage(self, page_id):
|
||||
|
||||
super().initializePage(page_id)
|
||||
|
||||
if self.page(page_id) == self.uiImageWizardPage:
|
||||
self._server.get(
|
||||
"/docker/images", self._getDockerImagesFromServerCallback)
|
||||
|
||||
def _getDockerImagesFromServerCallback(
|
||||
self, result, error=False, **kwargs):
|
||||
"""Callback for getDockerImagesFromServer.
|
||||
|
||||
:param progress_dialog: QProgressDialog instance
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self, "Docker Images", "{}".format(result["message"]))
|
||||
else:
|
||||
self.uiImageListComboBox.clear()
|
||||
existing_images = []
|
||||
for existing_image in self._docker_images.values():
|
||||
existing_images.append(existing_image["imagename"])
|
||||
|
||||
for image in result:
|
||||
if image["imagename"] not in existing_images:
|
||||
self.uiImageListComboBox.addItem(image["imagename"], image)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""Validates the server."""
|
||||
|
||||
if super().validateCurrentPage() is False:
|
||||
return False
|
||||
|
||||
if self.currentPage() == self.uiImageWizardPage:
|
||||
if not self.uiImageListComboBox.count():
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self, "Docker images",
|
||||
"There are no Docker images available!")
|
||||
return False
|
||||
return True
|
||||
|
||||
def getSettings(self):
|
||||
"""Returns the settings set in this Wizard.
|
||||
|
||||
:return: settings
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
if self.uiLocalRadioButton.isChecked():
|
||||
server = "local"
|
||||
elif self.uiRemoteRadioButton.isChecked():
|
||||
server = self.uiRemoteServersComboBox.currentText()
|
||||
elif self.uiVMRadioButton.isChecked():
|
||||
server = "vm"
|
||||
|
||||
index = self.uiImageListComboBox.currentIndex()
|
||||
imagename = self.uiImageListComboBox.itemText(index)
|
||||
# FIXME: add some more configuration options for images
|
||||
imageinfo = self.uiImageListComboBox.itemData(index)
|
||||
|
||||
settings = {
|
||||
"imagename": imagename,
|
||||
"server": server,
|
||||
}
|
||||
return settings
|
||||
437
gns3/modules/docker/docker_vm.py
Normal file
437
gns3/modules/docker/docker_vm.py
Normal file
@@ -0,0 +1,437 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Docker VM implementation.
|
||||
"""
|
||||
|
||||
from gns3.vm import VM
|
||||
from gns3.node import Node
|
||||
from gns3.ports.port import Port
|
||||
from gns3.ports.ethernet_port import EthernetPort
|
||||
from gns3.nios.nio_generic_ethernet import NIOGenericEthernet
|
||||
from gns3.nios.nio_linux_ethernet import NIOLinuxEthernet
|
||||
from gns3.nios.nio_udp import NIOUDP
|
||||
from .settings import DOCKER_CONTAINER_SETTINGS
|
||||
|
||||
import logging
|
||||
import re
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DockerVM(VM):
|
||||
"""
|
||||
Docker Image.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
"""
|
||||
URL_PREFIX = "docker"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
super().__init__(module, server, project)
|
||||
|
||||
log.info("Docker image instance is being created")
|
||||
self._settings = {
|
||||
"name": "",
|
||||
"imagename": "",
|
||||
"console": DOCKER_CONTAINER_SETTINGS["console"],
|
||||
"adapters": DOCKER_CONTAINER_SETTINGS["adapters"],
|
||||
"adapter_type": DOCKER_CONTAINER_SETTINGS["adapter_type"],
|
||||
"startcmd": DOCKER_CONTAINER_SETTINGS["startcmd"]
|
||||
}
|
||||
|
||||
def _addAdapters(self, adapters):
|
||||
"""Adds adapters.
|
||||
|
||||
:param adapters: number of adapters
|
||||
"""
|
||||
for adapter_number in range(0, adapters):
|
||||
adapter_name = EthernetPort.longNameType() + str(adapter_number)
|
||||
short_name = EthernetPort.shortNameType() + str(adapter_number)
|
||||
new_port = EthernetPort(adapter_name)
|
||||
new_port.setShortName(short_name)
|
||||
new_port.setAdapterNumber(adapter_number)
|
||||
new_port.setPortNumber(0)
|
||||
self._ports.append(new_port)
|
||||
log.debug("Adapter {} has been added".format(adapter_name))
|
||||
|
||||
def setup(self, imagename, name=None, vm_id=None, additional_settings={}):
|
||||
"""Sets up this Docker container.
|
||||
|
||||
:param imagename: image name
|
||||
:param name: optional name
|
||||
:param additional_settings: additional settings for this VM
|
||||
"""
|
||||
# let's create a unique name if none has been chosen
|
||||
if not name:
|
||||
name = imagename.replace(":", "-").replace("/", "-")
|
||||
name = self.allocateName(name + "-")
|
||||
self.setName(name)
|
||||
|
||||
if not name:
|
||||
self.error_signal.emit(
|
||||
self.id(), "could not allocate a name for this container")
|
||||
return
|
||||
|
||||
self._settings["name"] = name
|
||||
params = {
|
||||
"name": name,
|
||||
"imagename": imagename
|
||||
}
|
||||
if vm_id:
|
||||
params["id"] = vm_id
|
||||
params.update(additional_settings)
|
||||
self.httpPost("/docker/images", self._setupCallback, body=params)
|
||||
|
||||
def _setupCallback(self, result, error=False, **kwargs):
|
||||
"""Callback for Docker container setup.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
if not super()._setupCallback(result, error=error, **kwargs):
|
||||
return
|
||||
|
||||
self._addAdapters(self._settings.get("adapters", 0))
|
||||
|
||||
if self._loading:
|
||||
self.loaded_signal.emit()
|
||||
else:
|
||||
self.setInitialized(True)
|
||||
log.info(
|
||||
"Docker container {} has been created".format(self.name()))
|
||||
self.created_signal.emit(self.id())
|
||||
self._module.addNode(self)
|
||||
|
||||
def _updateCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("error while deleting {}: {}".format(self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["message"])
|
||||
return
|
||||
|
||||
updated = False
|
||||
nb_adapters_changed = False
|
||||
for name, value in result.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
log.info("{}: updating {} from '{}' to '{}'".format(self.name(), name, self._settings[name], value))
|
||||
updated = True
|
||||
if name == "name":
|
||||
# update the node name
|
||||
self.updateAllocatedName(value)
|
||||
if name == "adapters":
|
||||
nb_adapters_changed = True
|
||||
self._settings[name] = value
|
||||
|
||||
if nb_adapters_changed:
|
||||
log.debug("number of adapters has changed to {}".format(self._settings["adapters"]))
|
||||
# TODO: dynamically add/remove adapters
|
||||
self._ports.clear()
|
||||
self._addAdapters(self._settings["adapters"])
|
||||
|
||||
if updated:
|
||||
log.info("Docker VM {} has been updated".format(self.name()))
|
||||
self.updated_signal.emit()
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this container.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
updated = False
|
||||
if "nios" in new_settings:
|
||||
nios = new_settings["nios"]
|
||||
# add ports
|
||||
for nio in nios:
|
||||
if nio in self._settings["nios"]:
|
||||
# port already created for this NIO
|
||||
continue
|
||||
nio_object = None
|
||||
if nio.lower().startswith("nio_udp"):
|
||||
nio_object = self._createNIOUDP(nio)
|
||||
print("nio_object")
|
||||
if nio_object is None:
|
||||
log.error("Could not create NIO object from {}".format(nio))
|
||||
continue
|
||||
port = Port(nio, nio_object, stub=True)
|
||||
port.setStatus(Port.started)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been added".format(nio))
|
||||
|
||||
# delete ports
|
||||
for nio in self._settings["nios"]:
|
||||
if nio not in nios:
|
||||
for port in self._ports.copy():
|
||||
if port.name() == nio:
|
||||
self._ports.remove(port)
|
||||
updated = True
|
||||
log.debug("port {} has been deleted".format(nio))
|
||||
break
|
||||
|
||||
self._settings["nios"] = new_settings["nios"].copy()
|
||||
|
||||
if "name" in new_settings and new_settings["name"] != self.name():
|
||||
self._settings["name"] = new_settings["name"]
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
log.info("cloud {} has been updated".format(self.name()))
|
||||
self.updated_signal.emit()
|
||||
|
||||
def suspend(self):
|
||||
"""Suspends this Docker container."""
|
||||
if self.status() == Node.suspended:
|
||||
log.debug("{} is already suspended".format(self.name()))
|
||||
return
|
||||
log.debug("{} is being suspended".format(self.name()))
|
||||
self.httpPost("/docker/images/{id}/suspend".format(
|
||||
id=self._vm_id), self._suspendCallback)
|
||||
|
||||
def _suspendCallback(self, result, error=False, **kwargs):
|
||||
"""Callback for container suspend.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
if error:
|
||||
log.error("error while suspending {}: {}".format(
|
||||
self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["message"])
|
||||
else:
|
||||
log.info("{} has suspended".format(self.name()))
|
||||
self.setStatus(Node.suspended)
|
||||
for port in self._ports:
|
||||
# set ports as suspended
|
||||
port.setStatus(Port.suspended)
|
||||
self.suspended_signal.emit()
|
||||
|
||||
def reload(self):
|
||||
"""Reloads this Docker container."""
|
||||
log.debug("{} is being reloaded".format(self.name()))
|
||||
self.httpPost("/docker/images/{id}/reload".format(
|
||||
id=self._vm_id), self._reloadCallback)
|
||||
|
||||
def _reloadCallback(self, result, error=False, **kwargs):
|
||||
"""Callback for Docker container reload.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
if error:
|
||||
log.error("error while reloading {}: {}".format(
|
||||
self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["message"])
|
||||
else:
|
||||
log.info("{} has reloaded".format(self.name()))
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this Docker VM instance.
|
||||
(to be saved in a topology file).
|
||||
|
||||
:returns: representation of the node (dictionary)
|
||||
"""
|
||||
container = {
|
||||
"id": self.id(),
|
||||
"vm_id": self._vm_id,
|
||||
"type": self.__class__.__name__,
|
||||
"description": str(self),
|
||||
"properties": {},
|
||||
"server_id": self._server.id()
|
||||
}
|
||||
|
||||
# add the properties
|
||||
for name, value in self._settings.items():
|
||||
if value is not None and value != "":
|
||||
container["properties"][name] = value
|
||||
|
||||
# add the ports
|
||||
if self._ports:
|
||||
ports = container["ports"] = []
|
||||
for port in self._ports:
|
||||
ports.append(port.dump())
|
||||
return container
|
||||
|
||||
def info(self):
|
||||
"""Returns information about this Docker container.
|
||||
|
||||
:returns: formated string
|
||||
:rtype: string
|
||||
"""
|
||||
if self.status() == Node.started:
|
||||
state = "started"
|
||||
else:
|
||||
state = "stopped"
|
||||
|
||||
info = """Docker container {name} is {state}
|
||||
Node ID is {id}, server's Docker container ID is {vm_id}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
vm_id=self._vm_id,
|
||||
state=state
|
||||
)
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " {port_name} is empty\n".format(
|
||||
port_name=port.name())
|
||||
else:
|
||||
port_info += " {port_name} {port_description}\n".format(
|
||||
port_name=port.name(),
|
||||
port_description=port.description())
|
||||
|
||||
return info + port_info
|
||||
|
||||
def load(self, node_info):
|
||||
"""
|
||||
Loads a cloud representation
|
||||
(from a topology file).
|
||||
|
||||
:param node_info: representation of the node (dictionary)
|
||||
"""
|
||||
settings = node_info["properties"]
|
||||
name = settings.pop("name")
|
||||
self.updated_signal.connect(self._updatePortSettings)
|
||||
log.info("Docker container {} is loading".format(name))
|
||||
self._node_info = node_info
|
||||
self.setup(name, settings)
|
||||
|
||||
def _updatePortSettings(self):
|
||||
"""
|
||||
Updates port settings when loading a topology.
|
||||
"""
|
||||
|
||||
self.updated_signal.disconnect(self._updatePortSettings)
|
||||
# update the port with the correct IDs
|
||||
if "ports" in self._node_info:
|
||||
ports = self._node_info["ports"]
|
||||
for topology_port in ports:
|
||||
for port in self._ports:
|
||||
if topology_port["name"] == port.name():
|
||||
port.setId(topology_port["id"])
|
||||
if topology_port["name"].startswith("nio_gen_eth") or topology_port["name"].startswith("nio_linux_eth"):
|
||||
# lookup if the interface exists
|
||||
available_interface = False
|
||||
topology_port_name = topology_port["name"].split(':', 1)[1]
|
||||
for interface in self._settings["interfaces"]:
|
||||
if interface["name"] == topology_port_name:
|
||||
available_interface = True
|
||||
break
|
||||
if not available_interface:
|
||||
alternative_interface = self._module.findAlternativeInterface(self, topology_port_name)
|
||||
if alternative_interface:
|
||||
if topology_port["name"] in self._settings["nios"]:
|
||||
self._settings["nios"].remove(topology_port["name"])
|
||||
topology_port["name"] = topology_port["name"].replace(topology_port_name, alternative_interface)
|
||||
port.setName(topology_port["name"])
|
||||
self._settings["nios"].append(topology_port["name"])
|
||||
|
||||
log.info("cloud {} has been created".format(self.name()))
|
||||
self.setInitialized(True)
|
||||
self.created_signal.emit(self.id())
|
||||
|
||||
def name(self):
|
||||
"""
|
||||
Returns the name of this Docker container.
|
||||
|
||||
:returns: name (string)
|
||||
"""
|
||||
return self._settings["name"]
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns all settings of this Docker container.
|
||||
|
||||
:returns: settings
|
||||
:rtype: dict
|
||||
"""
|
||||
return self._settings
|
||||
|
||||
def ports(self):
|
||||
"""
|
||||
Returns all the ports for this Docker VM instance.
|
||||
|
||||
:returns: list of Port instances
|
||||
"""
|
||||
return self._ports
|
||||
|
||||
def shellExecCmd(self):
|
||||
"""Returns the execution command.
|
||||
|
||||
:returns: execution cmd for shell console
|
||||
:rtype: string
|
||||
"""
|
||||
return "docker exec -it {} bash".format(self._settings['name'])
|
||||
|
||||
def console(self):
|
||||
"""
|
||||
Returns the console port for this Docker VM instance.
|
||||
|
||||
:returns: port (integer)
|
||||
"""
|
||||
return self._settings["console"]
|
||||
|
||||
def configPage(self):
|
||||
"""Returns the configuration page widget to be used by the node configurator.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
from .pages.docker_vm_configuration_page import DockerVMConfigurationPage
|
||||
return DockerVMConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""Returns the default symbol path for this node.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
return ":/symbols/docker_guest.normal.svg"
|
||||
|
||||
@staticmethod
|
||||
def hoverSymbol():
|
||||
"""Returns the symbol to use when this node is hovered.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
return ":/symbols/docker_guest.selected.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
return "Docker image"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node category (integer)
|
||||
"""
|
||||
return [Node.end_devices]
|
||||
|
||||
def __str__(self):
|
||||
return "Docker image"
|
||||
0
gns3/modules/docker/pages/__init__.py
Normal file
0
gns3/modules/docker/pages/__init__.py
Normal file
65
gns3/modules/docker/pages/docker_preferences_page.py
Normal file
65
gns3/modules/docker/pages/docker_preferences_page.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for Docker preferences.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from .. import Docker
|
||||
from ..ui.docker_preferences_page_ui import Ui_DockerPreferencesPageWidget
|
||||
from ..settings import DOCKER_SETTINGS
|
||||
|
||||
|
||||
class DockerPreferencesPage(QtWidgets.QWidget, Ui_DockerPreferencesPageWidget):
|
||||
"""QWidget preference page for Docker."""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
# connect signals
|
||||
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
|
||||
|
||||
if not sys.platform.startswith("linux"):
|
||||
# Docker is only supported on Linux
|
||||
self.uiUseLocalServercheckBox.setEnabled(False)
|
||||
|
||||
def _restoreDefaultsSlot(self):
|
||||
"""Slot to populate the page widgets with the default settings."""
|
||||
self._populateWidgets(DOCKER_SETTINGS)
|
||||
|
||||
def _populateWidgets(self, settings):
|
||||
"""Populates the widgets with the settings.
|
||||
|
||||
:param settings: Docker settings
|
||||
"""
|
||||
self.uiUseLocalServercheckBox.setChecked(settings["use_local_server"])
|
||||
|
||||
def loadPreferences(self):
|
||||
"""Loads Docker preferences."""
|
||||
docker_settings = Docker.instance().settings()
|
||||
self._populateWidgets(docker_settings)
|
||||
|
||||
def savePreferences(self):
|
||||
"""Saves Docker preferences."""
|
||||
new_settings = {}
|
||||
new_settings["use_local_server"] = self.uiUseLocalServercheckBox.isChecked()
|
||||
Docker.instance().setSettings(new_settings)
|
||||
87
gns3/modules/docker/pages/docker_vm_configuration_page.py
Normal file
87
gns3/modules/docker/pages/docker_vm_configuration_page.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for Docker images.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
|
||||
from ..ui.docker_vm_configuration_page_ui import Ui_dockerVMConfigPageWidget
|
||||
|
||||
|
||||
class DockerVMConfigurationPage(
|
||||
QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
|
||||
"""QWidget configuration page for Docker images."""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
# TODO: finish docker name change
|
||||
self.uiImageListLabel.hide()
|
||||
self.uiImageListComboBox.hide()
|
||||
|
||||
def loadSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Loads the VirtualBox VM settings.
|
||||
|
||||
:param settings: the settings (dictionary)
|
||||
:param node: Node instance
|
||||
:param group: indicates the settings apply to a group of images
|
||||
"""
|
||||
|
||||
if not group:
|
||||
# set the device name
|
||||
if "name" in settings:
|
||||
self.uiNameLineEdit.setText(settings["name"])
|
||||
else:
|
||||
self.uiNameLabel.hide()
|
||||
self.uiNameLineEdit.hide()
|
||||
if "startcmd" in settings:
|
||||
self.uiCMDLineEdit.setText(settings["startcmd"])
|
||||
else:
|
||||
self.uiCMDLabel.hide()
|
||||
self.uiCMDLineEdit.hide()
|
||||
else:
|
||||
self.uiNameLabel.hide()
|
||||
self.uiNameLineEdit.hide()
|
||||
self.uiCMDLabel.hide()
|
||||
self.uiCMDLineEdit.hide()
|
||||
self.uiImageListLabel.hide()
|
||||
self.uiImageListComboBox.hide()
|
||||
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
"""Saves the Docker container settings.
|
||||
|
||||
:param settings: the settings (dictionary)
|
||||
:param node: Node instance
|
||||
:param group: indicates the settings apply to a group of VMs
|
||||
"""
|
||||
if not group:
|
||||
if "startcmd" in settings:
|
||||
startcmd = self.uiCMDLineEdit.text()
|
||||
settings["startcmd"] = startcmd
|
||||
if "name" in settings:
|
||||
name = self.uiNameLineEdit.text()
|
||||
if not name:
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "VMware name cannot be empty!")
|
||||
else:
|
||||
settings["name"] = name
|
||||
else:
|
||||
del settings["startcmd"]
|
||||
del settings["name"]
|
||||
230
gns3/modules/docker/pages/docker_vm_preferences_page.py
Normal file
230
gns3/modules/docker/pages/docker_vm_preferences_page.py
Normal file
@@ -0,0 +1,230 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for Docker image preferences.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
|
||||
from .. import Docker
|
||||
from ..settings import DOCKER_CONTAINER_SETTINGS
|
||||
from ..ui.docker_vm_preferences_page_ui import Ui_DockerVMPreferencesPageWidget
|
||||
from ..pages.docker_vm_configuration_page import DockerVMConfigurationPage
|
||||
from ..dialogs.docker_vm_wizard import DockerVMWizard
|
||||
|
||||
|
||||
class DockerVMPreferencesPage(
|
||||
QtWidgets.QWidget, Ui_DockerVMPreferencesPageWidget):
|
||||
"""QWidget preference page for Docker image preferences."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = MainWindow.instance()
|
||||
self._docker_images = {}
|
||||
self._items = []
|
||||
|
||||
self.uiNewDockerVMPushButton.clicked.connect(self._dockerImageNewSlot)
|
||||
self.uiEditDockerVMPushButton.clicked.connect(
|
||||
self._dockerImageEditSlot)
|
||||
self.uiDeleteDockerVMPushButton.clicked.connect(
|
||||
self._dockerImageDeleteSlot)
|
||||
self.uiDockerVMsTreeWidget.itemSelectionChanged.connect(
|
||||
self._dockerImageChangedSlot)
|
||||
self.uiDockerVMsTreeWidget.itemPressed.connect(
|
||||
self._dockerImagePressedSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
section_item = QtWidgets.QTreeWidgetItem(self.uiDockerVMInfoTreeWidget)
|
||||
section_item.setText(0, name)
|
||||
font = section_item.font(0)
|
||||
font.setBold(True)
|
||||
section_item.setFont(0, font)
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, docker_image):
|
||||
|
||||
self.uiDockerVMInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(
|
||||
section_item, ["Image name:", docker_image["imagename"]])
|
||||
# FIXME: add more configuration options
|
||||
QtWidgets.QTreeWidgetItem(
|
||||
section_item, ["CMD:", str(docker_image["startcmd"])])
|
||||
# QtWidgets.QTreeWidgetItem(
|
||||
# section_item, ["Server:", docker_image["server"]])
|
||||
|
||||
# # fill out the Network section
|
||||
# section_item = self._createSectionItem("Network")
|
||||
# QtWidgets.QTreeWidgetItem(
|
||||
# section_item, ["Adapters:", str(docker_image["adapters"])])
|
||||
# QtWidgets.QTreeWidgetItem(
|
||||
# section_item, ["Use any adapter:", "{}".format(
|
||||
# docker_image["use_any_adapter"])])
|
||||
# QtWidgets.QTreeWidgetItem(
|
||||
# section_item, ["Type:", docker_image["adapter_type"]])
|
||||
|
||||
self.uiDockerVMInfoTreeWidget.expandAll()
|
||||
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(0)
|
||||
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(1)
|
||||
|
||||
def _dockerImageChangedSlot(self):
|
||||
"""Loads a selected Docker image from the tree widget."""
|
||||
|
||||
selection = self.uiDockerVMsTreeWidget.selectedItems()
|
||||
self.uiDeleteDockerVMPushButton.setEnabled(len(selection) != 0)
|
||||
single_selected = len(selection) == 1
|
||||
self.uiEditDockerVMPushButton.setEnabled(single_selected)
|
||||
|
||||
if single_selected:
|
||||
key = selection[0].data(0, QtCore.Qt.UserRole)
|
||||
docker_image = self._docker_images[key]
|
||||
self._refreshInfo(docker_image)
|
||||
else:
|
||||
self.uiDockerVMInfoTreeWidget.clear()
|
||||
|
||||
def _dockerImageNewSlot(self):
|
||||
"""Creates a new Docker image."""
|
||||
wizard = DockerVMWizard(self._docker_images, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_image_settings = wizard.getSettings()
|
||||
key = "{server}:{imagename}".format(
|
||||
server=new_image_settings["server"],
|
||||
imagename=new_image_settings["imagename"])
|
||||
self._docker_images[key] = DOCKER_CONTAINER_SETTINGS.copy()
|
||||
self._docker_images[key].update(new_image_settings)
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiDockerVMsTreeWidget)
|
||||
item.setText(0, self._docker_images[key]["imagename"])
|
||||
item.setIcon(
|
||||
0, QtGui.QIcon(self._docker_images[key]["symbol"]))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
self.uiDockerVMsTreeWidget.setCurrentItem(item)
|
||||
|
||||
def _dockerImageEditSlot(self):
|
||||
"""Edits a Docker image"""
|
||||
item = self.uiDockerVMsTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
docker_image = self._docker_images[key]
|
||||
dialog = ConfigurationDialog(
|
||||
docker_image["imagename"], docker_image,
|
||||
DockerVMConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
if docker_image["imagename"] != item.text(0):
|
||||
new_key = "{server}:{imagename}".format(
|
||||
server=docker_image["server"],
|
||||
name=docker_image["imagename"])
|
||||
if new_key in self._docker_images:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self, "Docker image",
|
||||
"Docker image name {} already exists for server {}".format(
|
||||
docker_image["imagename"],
|
||||
docker_image["server"]))
|
||||
docker_image["imagename"] = item.text(0)
|
||||
return
|
||||
self._docker_images[new_key] = self._docker_images[key]
|
||||
del self._docker_images[key]
|
||||
item.setText(0, docker_image["imagename"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(docker_image)
|
||||
|
||||
def _dockerImageDeleteSlot(self):
|
||||
"""Deletes a Docker image."""
|
||||
for item in self.uiDockerVMsTreeWidget.selectedItems():
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
del self._docker_images[key]
|
||||
self.uiDockerVMsTreeWidget.takeTopLevelItem(
|
||||
self.uiDockerVMsTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
def _dockerImagePressedSlot(self, item, column):
|
||||
"""Slot for item pressed.
|
||||
|
||||
:param item: ignored
|
||||
:param column: ignored
|
||||
"""
|
||||
if QtWidgets.QApplication.mouseButtons() & QtCore.Qt.RightButton:
|
||||
self._showContextualMenu()
|
||||
|
||||
def _showContextualMenu(self):
|
||||
"""Contextual menu."""
|
||||
menu = QtWidgets.QMenu()
|
||||
|
||||
change_symbol_action = QtWidgets.QAction("Change symbol", menu)
|
||||
change_symbol_action.setIcon(QtGui.QIcon(":/icons/node_conception.svg"))
|
||||
change_symbol_action.setEnabled(len(self.uiDockerVMsTreeWidget.selectedItems()) == 1)
|
||||
change_symbol_action.triggered.connect(self._changeSymbolSlot)
|
||||
menu.addAction(change_symbol_action)
|
||||
|
||||
delete_action = QtWidgets.QAction("Delete", menu)
|
||||
delete_action.triggered.connect(self._dockerImageDeleteSlot)
|
||||
menu.addAction(delete_action)
|
||||
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
def _changeSymbolSlot(self):
|
||||
"""Change a symbol for a Docker image."""
|
||||
item = self.uiDockerVMsTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
docker_image = self._docker_images[key]
|
||||
dialog = SymbolSelectionDialog(
|
||||
self, symbol=docker_image["symbol"],
|
||||
category=docker_image["category"])
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
normal_symbol, selected_symbol = dialog.getSymbols()
|
||||
category = dialog.getCategory()
|
||||
item.setIcon(0, QtGui.QIcon(normal_symbol))
|
||||
docker_image["symbol"] = normal_symbol
|
||||
docker_image["category"] = category
|
||||
|
||||
def loadPreferences(self):
|
||||
"""Loads the Docker VM preferences."""
|
||||
|
||||
docker_module = Docker.instance()
|
||||
self._docker_images = copy.deepcopy(docker_module.dockerImages())
|
||||
self._items.clear()
|
||||
|
||||
for key, docker_image in self._docker_images.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiDockerVMsTreeWidget)
|
||||
item.setText(0, docker_image["imagename"])
|
||||
item.setIcon(0, QtGui.QIcon(docker_image["symbol"]))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
|
||||
if self._items:
|
||||
self.uiDockerVMsTreeWidget.setCurrentItem(self._items[0])
|
||||
self.uiDockerVMsTreeWidget.sortByColumn(
|
||||
0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
def savePreferences(self):
|
||||
"""Saves the Docker image preferences."""
|
||||
Docker.instance().setDockerImages(self._docker_images)
|
||||
39
gns3/modules/docker/settings.py
Normal file
39
gns3/modules/docker/settings.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Default Docker settings.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from gns3.node import Node
|
||||
|
||||
DOCKER_SETTINGS = {
|
||||
"docker_url": "",
|
||||
"docker_user": "",
|
||||
"use_local_server": sys.platform.startswith("linux") # Docker only supported on Linux
|
||||
}
|
||||
|
||||
DOCKER_CONTAINER_SETTINGS = {
|
||||
"symbol": ":/symbols/vbox_guest.svg",
|
||||
"category": Node.end_devices,
|
||||
"adapters": 4,
|
||||
"adapter_type": "veth",
|
||||
"console": "gnome-terminal",
|
||||
"enable_remote_console": False,
|
||||
"startcmd": ""
|
||||
}
|
||||
0
gns3/modules/docker/ui/__init__.py
Normal file
0
gns3/modules/docker/ui/__init__.py
Normal file
102
gns3/modules/docker/ui/docker_preferences_page.ui
Normal file
102
gns3/modules/docker/ui/docker_preferences_page.ui
Normal file
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DockerPreferencesPageWidget</class>
|
||||
<widget class="QWidget" name="DockerPreferencesPageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>200</width>
|
||||
<height>200</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Docker</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="uiTabWidget">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="uiServerSettingsTabWidget">
|
||||
<attribute name="title">
|
||||
<string>General settings</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="uiUseLocalServercheckBox">
|
||||
<property name="text">
|
||||
<string>Use the local server</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>455</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>254</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiRestoreDefaultsPushButton">
|
||||
<property name="text">
|
||||
<string>Restore defaults</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
<designerdata>
|
||||
<property name="gridDeltaX">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="gridDeltaY">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="gridSnapX">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="gridSnapY">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="gridVisible">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</designerdata>
|
||||
</ui>
|
||||
52
gns3/modules/docker/ui/docker_preferences_page_ui.py
Normal file
52
gns3/modules/docker/ui/docker_preferences_page_ui.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/docker/ui/docker_preferences_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.4.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_DockerPreferencesPageWidget(object):
|
||||
|
||||
def setupUi(self, DockerPreferencesPageWidget):
|
||||
DockerPreferencesPageWidget.setObjectName("DockerPreferencesPageWidget")
|
||||
DockerPreferencesPageWidget.resize(200, 200)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(DockerPreferencesPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(DockerPreferencesPageWidget)
|
||||
self.uiTabWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.uiTabWidget.setObjectName("uiTabWidget")
|
||||
self.uiServerSettingsTabWidget = QtWidgets.QWidget()
|
||||
self.uiServerSettingsTabWidget.setObjectName("uiServerSettingsTabWidget")
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.uiServerSettingsTabWidget)
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.uiUseLocalServercheckBox = QtWidgets.QCheckBox(self.uiServerSettingsTabWidget)
|
||||
self.uiUseLocalServercheckBox.setChecked(True)
|
||||
self.uiUseLocalServercheckBox.setObjectName("uiUseLocalServercheckBox")
|
||||
self.verticalLayout_2.addWidget(self.uiUseLocalServercheckBox)
|
||||
spacerItem = QtWidgets.QSpacerItem(20, 455, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout_2.addItem(spacerItem)
|
||||
self.uiTabWidget.addTab(self.uiServerSettingsTabWidget, "")
|
||||
self.verticalLayout.addWidget(self.uiTabWidget)
|
||||
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||
spacerItem1 = QtWidgets.QSpacerItem(254, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_2.addItem(spacerItem1)
|
||||
self.uiRestoreDefaultsPushButton = QtWidgets.QPushButton(DockerPreferencesPageWidget)
|
||||
self.uiRestoreDefaultsPushButton.setObjectName("uiRestoreDefaultsPushButton")
|
||||
self.horizontalLayout_2.addWidget(self.uiRestoreDefaultsPushButton)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout_2)
|
||||
|
||||
self.retranslateUi(DockerPreferencesPageWidget)
|
||||
self.uiTabWidget.setCurrentIndex(0)
|
||||
QtCore.QMetaObject.connectSlotsByName(DockerPreferencesPageWidget)
|
||||
|
||||
def retranslateUi(self, DockerPreferencesPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
DockerPreferencesPageWidget.setWindowTitle(_translate("DockerPreferencesPageWidget", "Docker"))
|
||||
self.uiUseLocalServercheckBox.setText(_translate("DockerPreferencesPageWidget", "Use the local server"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("DockerPreferencesPageWidget", "General settings"))
|
||||
self.uiRestoreDefaultsPushButton.setText(_translate("DockerPreferencesPageWidget", "Restore defaults"))
|
||||
72
gns3/modules/docker/ui/docker_vm_configuration_page.ui
Normal file
72
gns3/modules/docker/ui/docker_vm_configuration_page.ui
Normal file
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dockerVMConfigPageWidget</class>
|
||||
<widget class="QWidget" name="dockerVMConfigPageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>509</width>
|
||||
<height>346</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Docker image configuration</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="uiTabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab">
|
||||
<attribute name="title">
|
||||
<string>General settings</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiNameLabel">
|
||||
<property name="text">
|
||||
<string>Name:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="uiNameLineEdit"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiCMDLabel">
|
||||
<property name="text">
|
||||
<string>CMD:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="uiCMDLineEdit"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="uiImageListLabel">
|
||||
<property name="text">
|
||||
<string>Image name:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="uiImageListComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
61
gns3/modules/docker/ui/docker_vm_configuration_page_ui.py
Normal file
61
gns3/modules/docker/ui/docker_vm_configuration_page_ui.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/cetko/projects/gns3/gns3-gui/gns3/modules/docker/ui/docker_vm_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.4.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_dockerVMConfigPageWidget(object):
|
||||
|
||||
def setupUi(self, dockerVMConfigPageWidget):
|
||||
dockerVMConfigPageWidget.setObjectName("dockerVMConfigPageWidget")
|
||||
dockerVMConfigPageWidget.resize(509, 346)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(dockerVMConfigPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(dockerVMConfigPageWidget)
|
||||
self.uiTabWidget.setObjectName("uiTabWidget")
|
||||
self.tab = QtWidgets.QWidget()
|
||||
self.tab.setObjectName("tab")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.tab)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.uiNameLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiNameLabel.setObjectName("uiNameLabel")
|
||||
self.gridLayout.addWidget(self.uiNameLabel, 0, 0, 1, 1)
|
||||
self.uiNameLineEdit = QtWidgets.QLineEdit(self.tab)
|
||||
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
|
||||
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 1, 1, 1)
|
||||
self.uiCMDLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiCMDLabel.setObjectName("uiCMDLabel")
|
||||
self.gridLayout.addWidget(self.uiCMDLabel, 1, 0, 1, 1)
|
||||
self.uiCMDLineEdit = QtWidgets.QLineEdit(self.tab)
|
||||
self.uiCMDLineEdit.setObjectName("uiCMDLineEdit")
|
||||
self.gridLayout.addWidget(self.uiCMDLineEdit, 1, 1, 1, 1)
|
||||
self.uiImageListLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiImageListLabel.setObjectName("uiImageListLabel")
|
||||
self.gridLayout.addWidget(self.uiImageListLabel, 2, 0, 1, 1)
|
||||
self.uiImageListComboBox = QtWidgets.QComboBox(self.tab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiImageListComboBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiImageListComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiImageListComboBox.setObjectName("uiImageListComboBox")
|
||||
self.gridLayout.addWidget(self.uiImageListComboBox, 2, 1, 1, 1)
|
||||
self.uiTabWidget.addTab(self.tab, "")
|
||||
self.verticalLayout.addWidget(self.uiTabWidget)
|
||||
|
||||
self.retranslateUi(dockerVMConfigPageWidget)
|
||||
self.uiTabWidget.setCurrentIndex(0)
|
||||
QtCore.QMetaObject.connectSlotsByName(dockerVMConfigPageWidget)
|
||||
|
||||
def retranslateUi(self, dockerVMConfigPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
dockerVMConfigPageWidget.setWindowTitle(_translate("dockerVMConfigPageWidget", "Docker image configuration"))
|
||||
self.uiNameLabel.setText(_translate("dockerVMConfigPageWidget", "Name:"))
|
||||
self.uiCMDLabel.setText(_translate("dockerVMConfigPageWidget", "CMD:"))
|
||||
self.uiImageListLabel.setText(_translate("dockerVMConfigPageWidget", "Image name:"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab), _translate("dockerVMConfigPageWidget", "General settings"))
|
||||
151
gns3/modules/docker/ui/docker_vm_preferences_page.ui
Normal file
151
gns3/modules/docker/ui/docker_vm_preferences_page.ui
Normal file
@@ -0,0 +1,151 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DockerVMPreferencesPageWidget</class>
|
||||
<widget class="QWidget" name="DockerVMPreferencesPageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>505</width>
|
||||
<height>350</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Docker Containers</string>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string>Docker VM templates</string>
|
||||
</property>
|
||||
<property name="accessibleDescription">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiNewDockerVMPushButton">
|
||||
<property name="text">
|
||||
<string>&New</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiEditDockerVMPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Edit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiDeleteDockerVMPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Delete</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QTreeWidget" name="uiDockerVMInfoTreeWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="indentation">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="allColumnsShowFocus">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>1</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>2</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" rowspan="2">
|
||||
<widget class="QTreeWidget" name="uiDockerVMsTreeWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>160</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string notr="true">1</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>uiNewDockerVMPushButton</tabstop>
|
||||
<tabstop>uiDeleteDockerVMPushButton</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
<designerdata>
|
||||
<property name="gridDeltaX">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="gridDeltaY">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="gridSnapX">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="gridSnapY">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="gridVisible">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</designerdata>
|
||||
</ui>
|
||||
77
gns3/modules/docker/ui/docker_vm_preferences_page_ui.py
Normal file
77
gns3/modules/docker/ui/docker_vm_preferences_page_ui.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/docker/ui/docker_vm_preferences_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.5
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_DockerVMPreferencesPageWidget(object):
|
||||
|
||||
def setupUi(self, DockerVMPreferencesPageWidget):
|
||||
DockerVMPreferencesPageWidget.setObjectName("DockerVMPreferencesPageWidget")
|
||||
DockerVMPreferencesPageWidget.resize(505, 350)
|
||||
DockerVMPreferencesPageWidget.setAccessibleDescription("")
|
||||
self.gridLayout = QtWidgets.QGridLayout(DockerVMPreferencesPageWidget)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
|
||||
self.uiNewDockerVMPushButton = QtWidgets.QPushButton(DockerVMPreferencesPageWidget)
|
||||
self.uiNewDockerVMPushButton.setObjectName("uiNewDockerVMPushButton")
|
||||
self.horizontalLayout_5.addWidget(self.uiNewDockerVMPushButton)
|
||||
self.uiEditDockerVMPushButton = QtWidgets.QPushButton(DockerVMPreferencesPageWidget)
|
||||
self.uiEditDockerVMPushButton.setEnabled(False)
|
||||
self.uiEditDockerVMPushButton.setObjectName("uiEditDockerVMPushButton")
|
||||
self.horizontalLayout_5.addWidget(self.uiEditDockerVMPushButton)
|
||||
self.uiDeleteDockerVMPushButton = QtWidgets.QPushButton(DockerVMPreferencesPageWidget)
|
||||
self.uiDeleteDockerVMPushButton.setEnabled(False)
|
||||
self.uiDeleteDockerVMPushButton.setObjectName("uiDeleteDockerVMPushButton")
|
||||
self.horizontalLayout_5.addWidget(self.uiDeleteDockerVMPushButton)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_5, 1, 1, 1, 1)
|
||||
self.uiDockerVMInfoTreeWidget = QtWidgets.QTreeWidget(DockerVMPreferencesPageWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiDockerVMInfoTreeWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiDockerVMInfoTreeWidget.setSizePolicy(sizePolicy)
|
||||
self.uiDockerVMInfoTreeWidget.setIndentation(10)
|
||||
self.uiDockerVMInfoTreeWidget.setAllColumnsShowFocus(True)
|
||||
self.uiDockerVMInfoTreeWidget.setObjectName("uiDockerVMInfoTreeWidget")
|
||||
self.uiDockerVMInfoTreeWidget.header().setVisible(False)
|
||||
self.gridLayout.addWidget(self.uiDockerVMInfoTreeWidget, 0, 1, 1, 1)
|
||||
self.uiDockerVMsTreeWidget = QtWidgets.QTreeWidget(DockerVMPreferencesPageWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiDockerVMsTreeWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiDockerVMsTreeWidget.setSizePolicy(sizePolicy)
|
||||
self.uiDockerVMsTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
|
||||
font = QtGui.QFont()
|
||||
font.setPointSize(11)
|
||||
font.setBold(True)
|
||||
font.setWeight(75)
|
||||
self.uiDockerVMsTreeWidget.setFont(font)
|
||||
self.uiDockerVMsTreeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.uiDockerVMsTreeWidget.setIconSize(QtCore.QSize(32, 32))
|
||||
self.uiDockerVMsTreeWidget.setRootIsDecorated(False)
|
||||
self.uiDockerVMsTreeWidget.setObjectName("uiDockerVMsTreeWidget")
|
||||
self.uiDockerVMsTreeWidget.headerItem().setText(0, "1")
|
||||
self.uiDockerVMsTreeWidget.header().setVisible(False)
|
||||
self.gridLayout.addWidget(self.uiDockerVMsTreeWidget, 0, 0, 2, 1)
|
||||
|
||||
self.retranslateUi(DockerVMPreferencesPageWidget)
|
||||
QtCore.QMetaObject.connectSlotsByName(DockerVMPreferencesPageWidget)
|
||||
DockerVMPreferencesPageWidget.setTabOrder(self.uiNewDockerVMPushButton, self.uiDeleteDockerVMPushButton)
|
||||
|
||||
def retranslateUi(self, DockerVMPreferencesPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
DockerVMPreferencesPageWidget.setWindowTitle(_translate("DockerVMPreferencesPageWidget", "Docker Containers"))
|
||||
DockerVMPreferencesPageWidget.setAccessibleName(_translate("DockerVMPreferencesPageWidget", "Docker VM templates"))
|
||||
self.uiNewDockerVMPushButton.setText(_translate("DockerVMPreferencesPageWidget", "&New"))
|
||||
self.uiEditDockerVMPushButton.setText(_translate("DockerVMPreferencesPageWidget", "&Edit"))
|
||||
self.uiDeleteDockerVMPushButton.setText(_translate("DockerVMPreferencesPageWidget", "&Delete"))
|
||||
self.uiDockerVMInfoTreeWidget.headerItem().setText(0, _translate("DockerVMPreferencesPageWidget", "1"))
|
||||
self.uiDockerVMInfoTreeWidget.headerItem().setText(1, _translate("DockerVMPreferencesPageWidget", "2"))
|
||||
147
gns3/modules/docker/ui/docker_vm_wizard.ui
Normal file
147
gns3/modules/docker/ui/docker_vm_wizard.ui
Normal file
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DockerVMWizard</class>
|
||||
<widget class="QWizard" name="DockerVMWizard">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>585</width>
|
||||
<height>424</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>New Docker VM template</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWizardPage" name="uiServerWizardPage">
|
||||
<property name="title">
|
||||
<string>Server</string>
|
||||
</property>
|
||||
<property name="subTitle">
|
||||
<string>Please choose a server type to run your new Docker VM.</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="uiServerTypeGroupBox">
|
||||
<property name="title">
|
||||
<string>Server type</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="uiRemoteRadioButton">
|
||||
<property name="text">
|
||||
<string>Remote</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="uiVMRadioButton">
|
||||
<property name="text">
|
||||
<string>GNS3 VM</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="uiLocalRadioButton">
|
||||
<property name="text">
|
||||
<string>Local</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="uiRemoteServersGroupBox">
|
||||
<property name="title">
|
||||
<string>Remote servers</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_7">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiLoadBalanceCheckBox">
|
||||
<property name="text">
|
||||
<string>Load balance across all available remote servers</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiRemoteServersLabel">
|
||||
<property name="text">
|
||||
<string>Run on server:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="uiRemoteServersComboBox">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWizardPage" name="uiImageWizardPage">
|
||||
<property name="title">
|
||||
<string>Docker Virtual Machine</string>
|
||||
</property>
|
||||
<property name="subTitle">
|
||||
<string>Please choose a Docker virtual machine from the list.</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiImageListLabel">
|
||||
<property name="text">
|
||||
<string>Image list:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="uiImageListComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>uiImageListComboBox</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
95
gns3/modules/docker/ui/docker_vm_wizard_ui.py
Normal file
95
gns3/modules/docker/ui/docker_vm_wizard_ui.py
Normal file
@@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/docker/ui/docker_vm_wizard.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.5
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_DockerVMWizard(object):
|
||||
|
||||
def setupUi(self, DockerVMWizard):
|
||||
DockerVMWizard.setObjectName("DockerVMWizard")
|
||||
DockerVMWizard.resize(585, 424)
|
||||
DockerVMWizard.setModal(True)
|
||||
self.uiServerWizardPage = QtWidgets.QWizardPage()
|
||||
self.uiServerWizardPage.setObjectName("uiServerWizardPage")
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(self.uiServerWizardPage)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiServerTypeGroupBox = QtWidgets.QGroupBox(self.uiServerWizardPage)
|
||||
self.uiServerTypeGroupBox.setObjectName("uiServerTypeGroupBox")
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout(self.uiServerTypeGroupBox)
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.uiRemoteRadioButton = QtWidgets.QRadioButton(self.uiServerTypeGroupBox)
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
self.uiRemoteRadioButton.setObjectName("uiRemoteRadioButton")
|
||||
self.horizontalLayout.addWidget(self.uiRemoteRadioButton)
|
||||
self.uiVMRadioButton = QtWidgets.QRadioButton(self.uiServerTypeGroupBox)
|
||||
self.uiVMRadioButton.setObjectName("uiVMRadioButton")
|
||||
self.horizontalLayout.addWidget(self.uiVMRadioButton)
|
||||
self.uiLocalRadioButton = QtWidgets.QRadioButton(self.uiServerTypeGroupBox)
|
||||
self.uiLocalRadioButton.setObjectName("uiLocalRadioButton")
|
||||
self.horizontalLayout.addWidget(self.uiLocalRadioButton)
|
||||
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout.addItem(spacerItem)
|
||||
self.verticalLayout.addWidget(self.uiServerTypeGroupBox)
|
||||
self.uiRemoteServersGroupBox = QtWidgets.QGroupBox(self.uiServerWizardPage)
|
||||
self.uiRemoteServersGroupBox.setObjectName("uiRemoteServersGroupBox")
|
||||
self.gridLayout_7 = QtWidgets.QGridLayout(self.uiRemoteServersGroupBox)
|
||||
self.gridLayout_7.setObjectName("gridLayout_7")
|
||||
self.uiLoadBalanceCheckBox = QtWidgets.QCheckBox(self.uiRemoteServersGroupBox)
|
||||
self.uiLoadBalanceCheckBox.setChecked(True)
|
||||
self.uiLoadBalanceCheckBox.setObjectName("uiLoadBalanceCheckBox")
|
||||
self.gridLayout_7.addWidget(self.uiLoadBalanceCheckBox, 0, 0, 1, 2)
|
||||
self.uiRemoteServersLabel = QtWidgets.QLabel(self.uiRemoteServersGroupBox)
|
||||
self.uiRemoteServersLabel.setObjectName("uiRemoteServersLabel")
|
||||
self.gridLayout_7.addWidget(self.uiRemoteServersLabel, 1, 0, 1, 1)
|
||||
self.uiRemoteServersComboBox = QtWidgets.QComboBox(self.uiRemoteServersGroupBox)
|
||||
self.uiRemoteServersComboBox.setEnabled(False)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiRemoteServersComboBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiRemoteServersComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiRemoteServersComboBox.setObjectName("uiRemoteServersComboBox")
|
||||
self.gridLayout_7.addWidget(self.uiRemoteServersComboBox, 1, 1, 1, 1)
|
||||
self.verticalLayout.addWidget(self.uiRemoteServersGroupBox)
|
||||
DockerVMWizard.addPage(self.uiServerWizardPage)
|
||||
self.uiImageWizardPage = QtWidgets.QWizardPage()
|
||||
self.uiImageWizardPage.setObjectName("uiImageWizardPage")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.uiImageWizardPage)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.uiImageListLabel = QtWidgets.QLabel(self.uiImageWizardPage)
|
||||
self.uiImageListLabel.setObjectName("uiImageListLabel")
|
||||
self.gridLayout.addWidget(self.uiImageListLabel, 0, 0, 1, 1)
|
||||
self.uiImageListComboBox = QtWidgets.QComboBox(self.uiImageWizardPage)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiImageListComboBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiImageListComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiImageListComboBox.setObjectName("uiImageListComboBox")
|
||||
self.gridLayout.addWidget(self.uiImageListComboBox, 0, 1, 1, 1)
|
||||
DockerVMWizard.addPage(self.uiImageWizardPage)
|
||||
|
||||
self.retranslateUi(DockerVMWizard)
|
||||
QtCore.QMetaObject.connectSlotsByName(DockerVMWizard)
|
||||
|
||||
def retranslateUi(self, DockerVMWizard):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
DockerVMWizard.setWindowTitle(_translate("DockerVMWizard", "New Docker VM template"))
|
||||
self.uiServerWizardPage.setTitle(_translate("DockerVMWizard", "Server"))
|
||||
self.uiServerWizardPage.setSubTitle(_translate("DockerVMWizard", "Please choose a server type to run your new Docker VM."))
|
||||
self.uiServerTypeGroupBox.setTitle(_translate("DockerVMWizard", "Server type"))
|
||||
self.uiRemoteRadioButton.setText(_translate("DockerVMWizard", "Remote"))
|
||||
self.uiVMRadioButton.setText(_translate("DockerVMWizard", "GNS3 VM"))
|
||||
self.uiLocalRadioButton.setText(_translate("DockerVMWizard", "Local"))
|
||||
self.uiRemoteServersGroupBox.setTitle(_translate("DockerVMWizard", "Remote servers"))
|
||||
self.uiLoadBalanceCheckBox.setText(_translate("DockerVMWizard", "Load balance across all available remote servers"))
|
||||
self.uiRemoteServersLabel.setText(_translate("DockerVMWizard", "Run on server:"))
|
||||
self.uiImageWizardPage.setTitle(_translate("DockerVMWizard", "Docker Virtual Machine"))
|
||||
self.uiImageWizardPage.setSubTitle(_translate("DockerVMWizard", "Please choose a Docker virtual machine from the list."))
|
||||
self.uiImageListLabel.setText(_translate("DockerVMWizard", "Image list:"))
|
||||
@@ -19,13 +19,13 @@
|
||||
Dynamips module implementation.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import hashlib
|
||||
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.servers import Servers
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.image_manager import ImageManager
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
|
||||
from ..module import Module
|
||||
@@ -43,9 +43,9 @@ from .nodes.ethernet_switch import EthernetSwitch
|
||||
from .nodes.ethernet_hub import EthernetHub
|
||||
from .nodes.frame_relay_switch import FrameRelaySwitch
|
||||
from .nodes.atm_switch import ATMSwitch
|
||||
from .settings import DYNAMIPS_SETTINGS, DYNAMIPS_SETTING_TYPES
|
||||
from .settings import IOS_ROUTER_SETTINGS, IOS_ROUTER_SETTING_TYPES
|
||||
from .settings import PLATFORMS_DEFAULT_RAM
|
||||
from .settings import DYNAMIPS_SETTINGS
|
||||
from .settings import IOS_ROUTER_SETTINGS
|
||||
from .settings import DEFAULT_IDLEPC
|
||||
|
||||
PLATFORM_TO_CLASS = {
|
||||
"c1700": C1700,
|
||||
@@ -68,61 +68,63 @@ class Dynamips(Module):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Module.__init__(self)
|
||||
super().__init__()
|
||||
|
||||
self._settings = {}
|
||||
self._ios_routers = {}
|
||||
self._nodes = []
|
||||
self._ios_images_cache = {}
|
||||
|
||||
self.configChangedSlot()
|
||||
|
||||
def configChangedSlot(self):
|
||||
# load the settings and IOS images.
|
||||
self._loadSettings()
|
||||
self._loadIOSRouters()
|
||||
|
||||
@staticmethod
|
||||
def _findDynamips(self):
|
||||
def getDefaultIdlePC(path):
|
||||
"""
|
||||
Finds the Dynamips path.
|
||||
|
||||
:return: path to Dynamips
|
||||
Return the default IDLE PC for an image if the image
|
||||
exists or None otherwise
|
||||
"""
|
||||
if not os.path.isfile(path):
|
||||
path = os.path.join(ImageManager.instance().getDirectoryForType("DYNAMIPS"), path)
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
try:
|
||||
md5sum = Dynamips._md5sum(path)
|
||||
log.debug("Get idlePC for %s. md5sum %s", path, md5sum)
|
||||
if md5sum in DEFAULT_IDLEPC:
|
||||
log.debug("IDLEPC found for %s", path)
|
||||
return DEFAULT_IDLEPC[md5sum]
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
if sys.platform.startswith("win") and hasattr(sys, "frozen"):
|
||||
dynamips_path = os.path.join(os.getcwd(), "dynamips", "dynamips.exe")
|
||||
elif sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
|
||||
dynamips_path = os.path.join(os.getcwd(), "dynamips")
|
||||
else:
|
||||
dynamips_path = shutil.which("dynamips")
|
||||
|
||||
if dynamips_path is None:
|
||||
return ""
|
||||
return dynamips_path
|
||||
@staticmethod
|
||||
def _md5sum(path):
|
||||
with open(path, "rb") as fd:
|
||||
m = hashlib.md5()
|
||||
while True:
|
||||
data = fd.read(8192)
|
||||
if not data:
|
||||
break
|
||||
m.update(data)
|
||||
return m.hexdigest()
|
||||
|
||||
def _loadSettings(self):
|
||||
"""
|
||||
Loads the settings from the persistent settings file.
|
||||
"""
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
# restore the Dynamips settings from QSettings (for backward compatibility)
|
||||
legacy_settings = {}
|
||||
settings = QtCore.QSettings()
|
||||
settings.beginGroup(self.__class__.__name__)
|
||||
for name in DYNAMIPS_SETTINGS.keys():
|
||||
if settings.contains(name):
|
||||
legacy_settings[name] = settings.value(name, type=DYNAMIPS_SETTING_TYPES[name])
|
||||
settings.remove("")
|
||||
settings.endGroup()
|
||||
|
||||
if legacy_settings:
|
||||
local_config.saveSectionSettings(self.__class__.__name__, legacy_settings)
|
||||
self._settings = local_config.loadSectionSettings(self.__class__.__name__, DYNAMIPS_SETTINGS)
|
||||
|
||||
self._settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, DYNAMIPS_SETTINGS)
|
||||
if not os.path.exists(self._settings["dynamips_path"]):
|
||||
self._settings["dynamips_path"] = self._findDynamips(self)
|
||||
dynamips_path = shutil.which("dynamips")
|
||||
if dynamips_path:
|
||||
self._settings["dynamips_path"] = os.path.abspath(dynamips_path)
|
||||
else:
|
||||
self._settings["dynamips_path"] = ""
|
||||
|
||||
# keep the config file sync
|
||||
self._saveSettings()
|
||||
self._loadIOSRouters()
|
||||
|
||||
def _saveSettings(self):
|
||||
"""
|
||||
@@ -150,88 +152,30 @@ class Dynamips(Module):
|
||||
Load the IOS routers from the persistent settings file.
|
||||
"""
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
|
||||
# restore the Dynamips VM settings from QSettings (for backward compatibility)
|
||||
ios_routers = []
|
||||
# load the settings
|
||||
settings = QtCore.QSettings()
|
||||
settings.beginGroup("IOSRouters")
|
||||
# load the VMs
|
||||
size = settings.beginReadArray("ios_router")
|
||||
for index in range(0, size):
|
||||
settings.setArrayIndex(index)
|
||||
router = {}
|
||||
for setting_name, default_value in IOS_ROUTER_SETTINGS.items():
|
||||
router[setting_name] = settings.value(setting_name, default_value, IOS_ROUTER_SETTING_TYPES[setting_name])
|
||||
|
||||
for slot_id in range(0, 7):
|
||||
slot = "slot{}".format(slot_id)
|
||||
if settings.contains(slot):
|
||||
router[slot] = settings.value(slot, "")
|
||||
|
||||
for wic_id in range(0, 3):
|
||||
wic = "wic{}".format(wic_id)
|
||||
if settings.contains(wic):
|
||||
router[wic] = settings.value(wic, "")
|
||||
|
||||
platform = router["platform"]
|
||||
chassis = router["chassis"]
|
||||
|
||||
if platform == "c7200":
|
||||
router["midplane"] = settings.value("midplane", "vxr")
|
||||
router["npe"] = settings.value("npe", "npe-400")
|
||||
router["slot0"] = settings.value("slot0", "C7200-IO-FE")
|
||||
else:
|
||||
router["iomem"] = 5
|
||||
|
||||
if platform in ("c3725", "c3725", "c2691"):
|
||||
router["slot0"] = settings.value("slot0", "GT96100-FE")
|
||||
elif platform == "c3600" and chassis == "3660":
|
||||
router["slot0"] = settings.value("slot0", "Leopard-2FE")
|
||||
elif platform == "c2600" and chassis == "2610":
|
||||
router["slot0"] = settings.value("slot0", "C2600-MB-1E")
|
||||
elif platform == "c2600" and chassis == "2611":
|
||||
router["slot0"] = settings.value("slot0", "C2600-MB-2E")
|
||||
elif platform == "c2600" and chassis in ("2620", "2610XM", "2620XM", "2650XM"):
|
||||
router["slot0"] = settings.value("slot0", "C2600-MB-1FE")
|
||||
elif platform == "c2600" and chassis in ("2621", "2611XM", "2621XM", "2651XM"):
|
||||
router["slot0"] = settings.value("slot0", "C2600-MB-2FE")
|
||||
elif platform == "c1700" and chassis in ("1720", "1721", "1750", "1751", "1760"):
|
||||
router["slot0"] = settings.value("slot0", "C1700-MB-1FE")
|
||||
elif platform == "c1700" and chassis in ("1751", "1760"):
|
||||
router["slot0"] = settings.value("slot0", "C1700-MB-WIC1")
|
||||
|
||||
ios_routers.append(router)
|
||||
|
||||
settings.endArray()
|
||||
settings.remove("")
|
||||
settings.endGroup()
|
||||
|
||||
if ios_routers:
|
||||
local_config.saveSectionSettings(self.__class__.__name__, {"routers": ios_routers})
|
||||
|
||||
settings = local_config.settings()
|
||||
settings = LocalConfig.instance().settings()
|
||||
if "routers" in settings.get(self.__class__.__name__, {}):
|
||||
for router in settings[self.__class__.__name__]["routers"]:
|
||||
name = router.get("name")
|
||||
server = router.get("server")
|
||||
router["image"] = router.get("path", router["image"]) # for compatibility before version 1.3
|
||||
router["image"] = router.get("path", router["image"]) # for backward compatibility before version 1.3
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
if key in self._ios_routers or not name or not server:
|
||||
continue
|
||||
self._ios_routers[key] = router
|
||||
|
||||
# keep things sync
|
||||
self._saveIOSRouters()
|
||||
router_settings = IOS_ROUTER_SETTINGS.copy()
|
||||
router_settings.update(router)
|
||||
# for backward compatibility before version 1.4
|
||||
if "symbol" not in router_settings:
|
||||
router_settings["symbol"] = router_settings["default_symbol"]
|
||||
router_settings["symbol"] = router_settings["symbol"][:-11] + ".svg" if router_settings["symbol"].endswith("normal.svg") else router_settings["symbol"]
|
||||
self._ios_routers[key] = router_settings
|
||||
|
||||
def _saveIOSRouters(self):
|
||||
"""
|
||||
Saves the IOS routers to the persistent settings file.
|
||||
"""
|
||||
|
||||
# save the settings
|
||||
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, {"routers": list(self._ios_routers.values())})
|
||||
self._settings["routers"] = list(self._ios_routers.values())
|
||||
self._saveSettings()
|
||||
|
||||
def addNode(self, node):
|
||||
"""
|
||||
@@ -250,6 +194,8 @@ class Dynamips(Module):
|
||||
"""
|
||||
|
||||
if node in self._nodes:
|
||||
if "ram" in node.settings():
|
||||
node.server().decreaseAllocatedRAM(node.settings()["ram"])
|
||||
self._nodes.remove(node)
|
||||
|
||||
def iosRouters(self):
|
||||
@@ -290,34 +236,6 @@ class Dynamips(Module):
|
||||
self._settings.update(settings)
|
||||
self._saveSettings()
|
||||
|
||||
def allocateServer(self, node_class, use_cloud=False):
|
||||
"""
|
||||
Allocates a server.
|
||||
|
||||
:param node_class: Node object
|
||||
|
||||
:returns: allocated server (HTTPClient instance)
|
||||
"""
|
||||
|
||||
# allocate a server for the node
|
||||
servers = Servers.instance()
|
||||
|
||||
if use_cloud:
|
||||
from ...topology import Topology
|
||||
topology = Topology.instance()
|
||||
top_instance = topology.anyInstance()
|
||||
server = servers.getCloudServer(top_instance.host, top_instance.port, top_instance.ssl_ca_file)
|
||||
else:
|
||||
if self._settings["use_local_server"]:
|
||||
# use the local server
|
||||
server = servers.localServer()
|
||||
else:
|
||||
# pick up a remote server (round-robin method)
|
||||
server = next(iter(servers))
|
||||
if not server:
|
||||
raise ModuleError("No remote server is configured")
|
||||
return server
|
||||
|
||||
def createNode(self, node_class, server, project):
|
||||
"""
|
||||
Creates a new node.
|
||||
@@ -343,7 +261,6 @@ class Dynamips(Module):
|
||||
log.info("configuring node {}".format(node))
|
||||
|
||||
if isinstance(node, Router):
|
||||
|
||||
ios_router = None
|
||||
if node_name:
|
||||
for ios_key, info in self._ios_routers.items():
|
||||
@@ -354,47 +271,29 @@ class Dynamips(Module):
|
||||
if not ios_router:
|
||||
raise ModuleError("No IOS router for platform {}".format(node.settings()["platform"]))
|
||||
|
||||
# TODO: improve this part
|
||||
settings = {}
|
||||
# set initial settings like the chassis or an Idle-PC value etc.
|
||||
if "chassis" in ios_router and ios_router["chassis"]:
|
||||
settings["chassis"] = ios_router["chassis"]
|
||||
if "idlepc" in ios_router and ios_router["idlepc"]:
|
||||
settings["idlepc"] = ios_router["idlepc"]
|
||||
if ios_router["startup_config"]:
|
||||
settings["startup_config"] = ios_router["startup_config"]
|
||||
if "private_config" in ios_router and ios_router["private_config"]:
|
||||
settings["private_config"] = ios_router["private_config"]
|
||||
|
||||
if ios_router["platform"] == "c7200":
|
||||
settings["midplane"] = ios_router["midplane"]
|
||||
settings["npe"] = ios_router["npe"]
|
||||
elif "iomem" in ios_router:
|
||||
settings["iomem"] = ios_router["iomem"]
|
||||
|
||||
if "nvram" in ios_router and ios_router["nvram"]:
|
||||
settings["nvram"] = ios_router["nvram"]
|
||||
|
||||
if "disk0" in ios_router:
|
||||
settings["disk0"] = ios_router["disk0"]
|
||||
|
||||
if "disk1" in ios_router:
|
||||
settings["disk1"] = ios_router["disk1"]
|
||||
|
||||
for slot_id in range(0, 7):
|
||||
slot = "slot{}".format(slot_id)
|
||||
if slot in ios_router:
|
||||
settings[slot] = ios_router[slot]
|
||||
for wic_id in range(0, 3):
|
||||
wic = "wic{}".format(wic_id)
|
||||
if wic in ios_router:
|
||||
settings[wic] = ios_router[wic]
|
||||
vm_settings = {}
|
||||
for setting_name, value in ios_router.items():
|
||||
if setting_name in node.settings() and setting_name != "name" and value != "" and value is not None:
|
||||
vm_settings[setting_name] = value
|
||||
|
||||
base_name = "R"
|
||||
if "slot1" in settings and settings["slot1"] == "NM-16ESW":
|
||||
if "slot1" in vm_settings and vm_settings["slot1"] == "NM-16ESW":
|
||||
# must be an EtherSwitch router
|
||||
base_name = "ESW"
|
||||
node.setup(ios_router["image"], ios_router["ram"], additional_settings=settings, base_name=base_name)
|
||||
|
||||
# Older GNS3 versions may have the following invalid settings in the VM template
|
||||
if "console" in vm_settings:
|
||||
del vm_settings["console"]
|
||||
if "sensors" in vm_settings:
|
||||
del vm_settings["sensors"]
|
||||
if "power_supplies" in vm_settings:
|
||||
del vm_settings["power_supplies"]
|
||||
|
||||
ram = vm_settings.pop("ram")
|
||||
image = vm_settings.pop("image", None)
|
||||
if image is None:
|
||||
raise ModuleError("No IOS image has been associated with this IOS router")
|
||||
node.setup(image, ram, additional_settings=vm_settings, base_name=base_name)
|
||||
else:
|
||||
node.setup()
|
||||
|
||||
@@ -407,11 +306,11 @@ class Dynamips(Module):
|
||||
"""
|
||||
|
||||
for ios_router in self._ios_routers.values():
|
||||
if ios_router["image"] == image_path:
|
||||
if os.path.basename(ios_router["image"]) == image_path:
|
||||
if ios_router["idlepc"] != idlepc:
|
||||
ios_router["idlepc"] = idlepc
|
||||
log.info("Idle-PC value {} saved into '{}' template".format(idlepc, ios_router["name"]))
|
||||
self._saveIOSRouters()
|
||||
break
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
@@ -470,11 +369,10 @@ class Dynamips(Module):
|
||||
candidate_ios_images[ios_router["image"]] = ios_router
|
||||
|
||||
if candidate_ios_images:
|
||||
selection, ok = QtGui.QInputDialog.getItem(mainwindow,
|
||||
"IOS image", "IOS image {} could not be found\nPlease select an alternative from your existing images:".format(image),
|
||||
list(candidate_ios_images.keys()), 0, False)
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(mainwindow,
|
||||
"IOS image", "IOS image {} could not be found\nPlease select an alternative from your existing images:".format(image),
|
||||
list(candidate_ios_images.keys()), 0, False)
|
||||
if ok:
|
||||
ios_image = candidate_ios_images[selection]
|
||||
alternative_image["image"] = ios_router["image"]
|
||||
alternative_image["ram"] = ios_router["ram"]
|
||||
alternative_image["idlepc"] = ios_router["idlepc"]
|
||||
@@ -482,9 +380,11 @@ class Dynamips(Module):
|
||||
return alternative_image
|
||||
|
||||
# no registered IOS image is used, let's just ask for an IOS image path
|
||||
QtGui.QMessageBox.critical(mainwindow, "IOS image", "Could not find the {} IOS image \nPlease select a similar IOS image!".format(image))
|
||||
msg = "Could not find the {} IOS image \nPlease select a similar IOS image!".format(image)
|
||||
log.error(msg)
|
||||
QtWidgets.QMessageBox.critical(mainwindow, "IOS image", msg)
|
||||
from .pages.ios_router_preferences_page import IOSRouterPreferencesPage
|
||||
image_path = IOSRouterPreferencesPage.getIOSImage(mainwindow)
|
||||
image_path = IOSRouterPreferencesPage.getIOSImage(mainwindow, None)
|
||||
if image_path:
|
||||
alternative_image["image"] = image_path
|
||||
self._ios_images_cache[image] = alternative_image
|
||||
@@ -518,22 +418,13 @@ class Dynamips(Module):
|
||||
in the nodes view and create a node on the scene.
|
||||
"""
|
||||
|
||||
server = "local"
|
||||
if not self._settings["use_local_server"]:
|
||||
# pick up a remote server (round-robin method)
|
||||
remote_server = next(iter(Servers.instance()))
|
||||
if remote_server:
|
||||
server = "{}:{}".format(remote_server.host, remote_server.port)
|
||||
|
||||
nodes = []
|
||||
for node_class in [EthernetSwitch, EthernetHub, FrameRelaySwitch, ATMSwitch]:
|
||||
nodes.append(
|
||||
{"class": node_class.__name__,
|
||||
"name": node_class.symbolName(),
|
||||
"server": server,
|
||||
"categories": node_class.categories(),
|
||||
"default_symbol": node_class.defaultSymbol(),
|
||||
"hover_symbol": node_class.hoverSymbol()}
|
||||
"symbol": node_class.defaultSymbol()}
|
||||
)
|
||||
|
||||
for ios_router in self._ios_routers.values():
|
||||
@@ -541,9 +432,9 @@ class Dynamips(Module):
|
||||
nodes.append(
|
||||
{"class": node_class.__name__,
|
||||
"name": ios_router["name"],
|
||||
"ram": ios_router["ram"],
|
||||
"server": ios_router["server"],
|
||||
"default_symbol": ios_router["default_symbol"],
|
||||
"hover_symbol": ios_router["hover_symbol"],
|
||||
"symbol": ios_router["symbol"],
|
||||
"categories": [ios_router["category"]]}
|
||||
)
|
||||
|
||||
|
||||
@@ -19,21 +19,19 @@
|
||||
Wizard for IOS routers.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets
|
||||
from gns3.servers import Servers
|
||||
from gns3.node import Node
|
||||
from gns3.utils.run_in_terminal import RunInTerminal
|
||||
from gns3.utils.get_resource import get_resource
|
||||
from gns3.utils.get_default_base_config import get_default_base_config
|
||||
from gns3.dialogs.vm_with_images_wizard import VMWithImagesWizard
|
||||
|
||||
from ....settings import ENABLE_CLOUD
|
||||
from ..ui.ios_router_wizard_ui import Ui_IOSRouterWizard
|
||||
from ..settings import PLATFORMS_DEFAULT_RAM, PLATFORMS_DEFAULT_NVRAM, DEFAULT_IDLEPC, CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
|
||||
from ..settings import PLATFORMS_DEFAULT_RAM, PLATFORMS_DEFAULT_NVRAM, CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
|
||||
from .. import Dynamips
|
||||
from ..nodes.c1700 import C1700
|
||||
from ..nodes.c2600 import C2600
|
||||
@@ -56,7 +54,8 @@ PLATFORM_TO_CLASS = {
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
|
||||
class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
|
||||
|
||||
"""
|
||||
Wizard to create an IOS router.
|
||||
@@ -67,17 +66,9 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
|
||||
def __init__(self, ios_routers, parent):
|
||||
|
||||
QtGui.QWizard.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/router.normal.svg"))
|
||||
self.setWizardStyle(QtGui.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtGui.QWizard.NoDefaultButton)
|
||||
super().__init__(ios_routers, Dynamips.instance().settings()["use_local_server"], parent)
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/router.svg"))
|
||||
|
||||
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
|
||||
self.uiIOSImageToolButton.clicked.connect(self._iosImageBrowserSlot)
|
||||
self.uiTestIOSImagePushButton.clicked.connect(self._testIOSImageSlot)
|
||||
self.uiIdlePCFinderPushButton.clicked.connect(self._idlePCFinderSlot)
|
||||
self.uiEtherSwitchCheckBox.stateChanged.connect(self._etherSwitchSlot)
|
||||
@@ -117,38 +108,43 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
1: self.uiWic1comboBox,
|
||||
2: self.uiWic2comboBox}
|
||||
|
||||
self._ios_routers = ios_routers
|
||||
from ..pages.ios_router_preferences_page import IOSRouterPreferencesPage
|
||||
self.addImageSelector(self.uiIOSExistingImageRadioButton, self.uiIOSImageListComboBox, self.uiIOSImageLineEdit, self.uiIOSImageToolButton, IOSRouterPreferencesPage.getIOSImage)
|
||||
|
||||
if Dynamips.instance().settings()["use_local_server"]:
|
||||
# skip the server page if we use the local server
|
||||
self.setStartId(1)
|
||||
|
||||
if not ENABLE_CLOUD:
|
||||
self.uiCloudRadioButton.hide()
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
def _prefillPlatform(self):
|
||||
"""
|
||||
Slot for when the remote server radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
Try to guess the platform based on image name
|
||||
"""
|
||||
# try to guess the platform
|
||||
image = os.path.basename(self.uiIOSImageLineEdit.text())
|
||||
match = re.match("^(c[0-9]+)p?\\-\w+", image.lower())
|
||||
if not match:
|
||||
QtWidgets.QMessageBox.warning(self, "IOS image", "Could not detect the platform, make sure this is a valid IOS image!")
|
||||
return
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(True)
|
||||
else:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
detected_platform = match.group(1)
|
||||
detected_chassis = ""
|
||||
# IOS images for the 3600 platform start with the chassis name (c3620 etc.)
|
||||
for platform, chassis in CHASSIS.items():
|
||||
if detected_platform[1:] in chassis:
|
||||
detected_chassis = detected_platform[1:]
|
||||
detected_platform = platform
|
||||
break
|
||||
|
||||
def _loadBalanceToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the load balance checkbox is toggled.
|
||||
if detected_platform not in PLATFORMS_DEFAULT_RAM:
|
||||
QtWidgets.QMessageBox.warning(self, "IOS image", "This IOS image is for the {} platform/chassis and is not supported by this application!".format(detected_platform))
|
||||
return
|
||||
|
||||
:param checked: either the box is checked or not
|
||||
"""
|
||||
if image.lower().startswith("c7200p"):
|
||||
QtWidgets.QMessageBox.warning(self, "IOS image", "This IOS image is for c7200 PowerPC routers and is not recommended. Please use an IOS image that do not start with c7200p.")
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersComboBox.setEnabled(False)
|
||||
else:
|
||||
self.uiRemoteServersComboBox.setEnabled(True)
|
||||
index = self.uiPlatformComboBox.findText(detected_platform)
|
||||
if index != -1:
|
||||
self.uiPlatformComboBox.setCurrentIndex(index)
|
||||
|
||||
index = self.uiChassisComboBox.findText(detected_chassis)
|
||||
if index != -1:
|
||||
self.uiChassisComboBox.setCurrentIndex(index)
|
||||
|
||||
def _platformChangedSlot(self, platform):
|
||||
"""
|
||||
@@ -176,7 +172,7 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
ios_image = self.uiIOSImageLineEdit.text()
|
||||
dynamips = os.path.realpath(Dynamips.instance().settings()["image"])
|
||||
if not os.path.exists(dynamips):
|
||||
QtGui.QMessageBox.critical(self, "IOS image", "Could not find Dynamips executable: {}".format(dynamips))
|
||||
QtWidgets.QMessageBox.critical(self, "IOS image", "Could not find Dynamips executable: {}".format(dynamips))
|
||||
return
|
||||
command = '"{path}" -P {platform} -r {ram} "{ios_image}"'.format(path=dynamips,
|
||||
platform=platform[1:],
|
||||
@@ -185,7 +181,7 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
try:
|
||||
RunInTerminal(command)
|
||||
except OSError as e:
|
||||
QtGui.QMessageBox.critical(self, "IOS image", "Could not test the IOS image: {}".format(e))
|
||||
QtWidgets.QMessageBox.critical(self, "IOS image", "Could not test the IOS image: {}".format(e))
|
||||
|
||||
def _idlePCValidateSlot(self):
|
||||
"""
|
||||
@@ -256,7 +252,7 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
:param message: error message from the server.
|
||||
"""
|
||||
|
||||
QtGui.QMessageBox.critical(self, "Idle-PC finder", "Could not create IOS router: {}".format(message))
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC finder", "Could not create IOS router: {}".format(message))
|
||||
|
||||
def _computeAutoIdlepcCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -270,11 +266,11 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
self._router.delete()
|
||||
self._router = None
|
||||
if error:
|
||||
QtGui.QMessageBox.critical(self, "Idle-PC finder", "Error: {}".format(result["message"]))
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC finder", "Error: {}".format(result["message"]))
|
||||
else:
|
||||
idlepc = result["idlepc"]
|
||||
self.uiIdlepcLineEdit.setText(idlepc)
|
||||
QtGui.QMessageBox.information(self, "Idle-PC finder", "Idle-PC value {} has been found suitable for your IOS image".format(idlepc))
|
||||
QtWidgets.QMessageBox.information(self, "Idle-PC finder", "Idle-PC value {} has been found suitable for your IOS image".format(idlepc))
|
||||
|
||||
def _iosImageBrowserSlot(self):
|
||||
"""
|
||||
@@ -282,7 +278,8 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
"""
|
||||
|
||||
from ..pages.ios_router_preferences_page import IOSRouterPreferencesPage
|
||||
path = IOSRouterPreferencesPage.getIOSImage(self)
|
||||
server = Servers.instance().getServerFromString(self.getSettings()["server"])
|
||||
path = IOSRouterPreferencesPage.getIOSImage(self, server)
|
||||
if not path:
|
||||
return
|
||||
self.uiIOSImageLineEdit.clear()
|
||||
@@ -292,7 +289,7 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
image = os.path.basename(path)
|
||||
match = re.match("^(c[0-9]+)p?\\-\w+", image.lower())
|
||||
if not match:
|
||||
QtGui.QMessageBox.warning(self, "IOS image", "Could not detect the platform, make sure this is a valid IOS image!")
|
||||
QtWidgets.QMessageBox.warning(self, "IOS image", "Could not detect the platform, make sure this is a valid IOS image!")
|
||||
return
|
||||
|
||||
detected_platform = match.group(1)
|
||||
@@ -305,11 +302,11 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
break
|
||||
|
||||
if detected_platform not in PLATFORMS_DEFAULT_RAM:
|
||||
QtGui.QMessageBox.warning(self, "IOS image", "This IOS image is for the {} platform/chassis and is not supported by this application!".format(detected_platform))
|
||||
QtWidgets.QMessageBox.warning(self, "IOS image", "This IOS image is for the {} platform/chassis and is not supported by this application!".format(detected_platform))
|
||||
return
|
||||
|
||||
if image.lower().startswith("c7200p"):
|
||||
QtGui.QMessageBox.warning(self, "IOS image", "This IOS image is for c7200 PowerPC routers and is not recommended. Please use an IOS image that do not start with c7200p.")
|
||||
QtWidgets.QMessageBox.warning(self, "IOS image", "This IOS image is for c7200 PowerPC routers and is not recommended. Please use an IOS image that do not start with c7200p.")
|
||||
|
||||
index = self.uiPlatformComboBox.findText(detected_platform)
|
||||
if index != -1:
|
||||
@@ -323,7 +320,7 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
|
||||
if self._router:
|
||||
self._router.delete()
|
||||
QtGui.QWizard.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
def _populateAdapters(self, platform, chassis):
|
||||
"""
|
||||
@@ -362,25 +359,14 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
wic_list = list(wics)
|
||||
self._widget_wics[wic_number].addItems([""] + wic_list)
|
||||
|
||||
def _md5sum(self, filename):
|
||||
|
||||
with open(filename, 'rb') as fd:
|
||||
m = hashlib.md5()
|
||||
while True:
|
||||
data = fd.read(8192)
|
||||
if not data:
|
||||
break
|
||||
m.update(data)
|
||||
return m.hexdigest()
|
||||
|
||||
def initializePage(self, page_id):
|
||||
|
||||
if self.page(page_id) == self.uiServerWizardPage:
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
for server in Servers.instance().remoteServers().values():
|
||||
self.uiRemoteServersComboBox.addItem("{}:{}".format(server.host, server.port), server)
|
||||
super().initializePage(page_id)
|
||||
|
||||
if self.page(page_id) == self.uiIOSImageWizardPage:
|
||||
self.loadImagesList("/dynamips/vms")
|
||||
elif self.page(page_id) == self.uiNamePlatformWizardPage:
|
||||
self._prefillPlatform()
|
||||
self.uiNameLineEdit.setText(self.uiPlatformComboBox.currentText())
|
||||
ios_image = self.uiIOSImageLineEdit.text()
|
||||
self.setWindowTitle("New IOS router - {}".format(os.path.basename(ios_image)))
|
||||
@@ -411,37 +397,26 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
|
||||
elif self.page(page_id) == self.uiIdlePCWizardPage:
|
||||
path = self.uiIOSImageLineEdit.text()
|
||||
if os.path.isfile(path):
|
||||
try:
|
||||
md5sum = self._md5sum(path)
|
||||
if md5sum in DEFAULT_IDLEPC:
|
||||
self.uiIdlepcLineEdit.setText(DEFAULT_IDLEPC[md5sum])
|
||||
except OSError:
|
||||
pass
|
||||
idle_pc = Dynamips.getDefaultIdlePC(path)
|
||||
if idle_pc is not None:
|
||||
self.uiIdlepcLineEdit.setText(idle_pc)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the IOS name and checks validation state for Idle-PC value
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiNamePlatformWizardPage:
|
||||
name = self.uiNameLineEdit.text()
|
||||
for ios_router in self._ios_routers.values():
|
||||
if ios_router["name"] == name:
|
||||
QtGui.QMessageBox.critical(self, "Name", "{} is already used, please choose another name".format(name))
|
||||
return False
|
||||
if super().validateCurrentPage() is False:
|
||||
return False
|
||||
|
||||
if self.currentPage() == self.uiMemoryWizardPage and self.uiPlatformComboBox.currentText() == "c7200":
|
||||
if self.uiRamSpinBox.value() > 512:
|
||||
QtGui.QMessageBox.critical(self, "c7200 RAM requirement", "c7200 routers with NPE-400 are limited to 512MB of RAM")
|
||||
QtWidgets.QMessageBox.critical(self, "c7200 RAM requirement", "c7200 routers with NPE-400 are limited to 512MB of RAM")
|
||||
return False
|
||||
if self.currentPage() == self.uiIdlePCWizardPage:
|
||||
if not self._idle_valid:
|
||||
idle_pc = self.uiIdlepcLineEdit.text()
|
||||
QtGui.QMessageBox.critical(self, "Idle-PC", "{} is not a valid Idle-PC value ".format(idle_pc))
|
||||
return False
|
||||
if self.currentPage() == self.uiServerWizardPage and self.uiRemoteRadioButton.isChecked():
|
||||
if not Servers.instance().remoteServers():
|
||||
QtGui.QMessageBox.critical(self, "Remote server", "There is no remote server registered in Dynamips preferences")
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC", "{} is not a valid Idle-PC value ".format(idle_pc))
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -453,23 +428,21 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
"""
|
||||
|
||||
image = self.uiIOSImageLineEdit.text()
|
||||
if Dynamips.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
|
||||
if self.uiLocalRadioButton.isChecked():
|
||||
server = "local"
|
||||
elif self.uiRemoteRadioButton.isChecked():
|
||||
if self.uiLoadBalanceCheckBox.isChecked():
|
||||
server = next(iter(Servers.instance()))
|
||||
server = "{}:{}".format(server.host, server.port)
|
||||
server = "load-balance"
|
||||
else:
|
||||
server = self.uiRemoteServersComboBox.currentText()
|
||||
else: # Cloud is selected
|
||||
server = "cloud"
|
||||
elif self.uiVMRadioButton.isChecked():
|
||||
server = "vm"
|
||||
|
||||
platform = self.uiPlatformComboBox.currentText()
|
||||
settings = {
|
||||
"name": self.uiNameLineEdit.text(),
|
||||
"image": image,
|
||||
"startup_config": get_default_base_config(self._base_startup_config_template),
|
||||
"private_config": get_default_base_config(self._base_private_config_template),
|
||||
"ram": self.uiRamSpinBox.value(),
|
||||
"nvram": PLATFORMS_DEFAULT_NVRAM[platform],
|
||||
"idlepc": self.uiIdlepcLineEdit.text(),
|
||||
@@ -480,10 +453,11 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
|
||||
if self.uiEtherSwitchCheckBox.isChecked():
|
||||
settings["startup_config"] = get_default_base_config(self._base_etherswitch_startup_config_template)
|
||||
settings["default_symbol"] = ":/symbols/multilayer_switch.normal.svg"
|
||||
settings["hover_symbol"] = ":/symbols/multilayer_switch.selected.svg"
|
||||
settings["symbol"] = ":/symbols/multilayer_switch.svg"
|
||||
settings["disk0"] = 1 # adds 1MB disk to store vlan.dat
|
||||
settings["category"] = Node.switches
|
||||
else:
|
||||
settings["auto_delete_disks"] = True
|
||||
|
||||
image_file = os.path.basename(image)
|
||||
if image_file.lower().startswith("c7200p"):
|
||||
@@ -510,4 +484,4 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
if platform not in WIC_MATRIX:
|
||||
# skip the WIC modules page if the platform doesn't support any.
|
||||
return self.uiNetworkAdaptersWizardPage.nextId() + 1
|
||||
return QtGui.QWizard.nextId(self)
|
||||
return QtWidgets.QWizard.nextId(self)
|
||||
|
||||
@@ -41,7 +41,7 @@ class ATMSwitch(Device):
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
Device.__init__(self, module, server, project)
|
||||
super().__init__(module, server, project)
|
||||
self.setStatus(Node.started) # this is an always-on node
|
||||
self._ports = []
|
||||
self._settings = {"name": "",
|
||||
@@ -128,8 +128,7 @@ class ATMSwitch(Device):
|
||||
if self.hasAllocatedName(new_settings["name"]):
|
||||
self.error_signal.emit(self.id(), 'Name "{}" is already used by another node'.format(new_settings["name"]))
|
||||
return
|
||||
params = {"id": self._atmsw_id,
|
||||
"name": new_settings["name"]}
|
||||
params["name"] = new_settings["name"]
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
@@ -200,8 +199,8 @@ class ATMSwitch(Device):
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
device_id=self._device_id,
|
||||
host=self._server.host,
|
||||
port=self._server.port)
|
||||
host=self._server.host(),
|
||||
port=self._server.port())
|
||||
|
||||
port_info = ""
|
||||
mapping = re.compile(r"""^([0-9]*):([0-9]*):([0-9]*)$""")
|
||||
@@ -334,7 +333,7 @@ class ATMSwitch(Device):
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node configurator.
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
@@ -350,17 +349,7 @@ class ATMSwitch(Device):
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/atm_switch.normal.svg"
|
||||
|
||||
@staticmethod
|
||||
def hoverSymbol():
|
||||
"""
|
||||
Returns the symbol to use when this node is hovered.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/atm_switch.selected.svg"
|
||||
return ":/symbols/atm_switch.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
@@ -34,7 +34,7 @@ class C1700(Router):
|
||||
|
||||
def __init__(self, module, server, project, chassis="1720"):
|
||||
|
||||
Router.__init__(self, module, server, project, platform="c1700")
|
||||
super().__init__(module, server, project, platform="c1700")
|
||||
c1700_settings = {"ram": 128,
|
||||
"nvram": 32,
|
||||
"disk0": 0,
|
||||
|
||||
@@ -46,7 +46,7 @@ class C2600(Router):
|
||||
|
||||
def __init__(self, module, server, project, chassis="2610"):
|
||||
|
||||
Router.__init__(self, module, server, project, platform="c2600")
|
||||
super().__init__(module, server, project, platform="c2600")
|
||||
c2600_settings = {"ram": 128,
|
||||
"nvram": 128,
|
||||
"disk0": 0,
|
||||
|
||||
@@ -34,7 +34,7 @@ class C2691(Router):
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
Router.__init__(self, module, server, project, platform="c2691")
|
||||
super().__init__(module, server, project, platform="c2691")
|
||||
c2691_settings = {"ram": 192,
|
||||
"nvram": 112,
|
||||
"disk0": 16,
|
||||
|
||||
@@ -34,7 +34,7 @@ class C3600(Router):
|
||||
|
||||
def __init__(self, module, server, project, chassis="3640"):
|
||||
|
||||
Router.__init__(self, module, server, project, platform="c3600")
|
||||
super().__init__(module, server, project, platform="c3600")
|
||||
c3600_settings = {"ram": 192,
|
||||
"nvram": 128,
|
||||
"disk0": 0,
|
||||
|
||||
@@ -34,7 +34,7 @@ class C3725(Router):
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
Router.__init__(self, module, server, project, platform="c3725")
|
||||
super().__init__(module, server, project, platform="c3725")
|
||||
c3725_settings = {"ram": 128,
|
||||
"nvram": 112,
|
||||
"disk0": 16,
|
||||
|
||||
@@ -34,7 +34,7 @@ class C3745(Router):
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
Router.__init__(self, module, server, project, platform="c3745")
|
||||
super().__init__(module, server, project, platform="c3745")
|
||||
c3745_settings = {"ram": 256,
|
||||
"nvram": 304,
|
||||
"disk0": 16,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user