mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-06-03 01:02:03 +03:00
Compare commits
2203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b87f2b2952 | ||
|
|
9e4b5ad02b | ||
|
|
a31586fac9 | ||
|
|
2a6327b2f2 | ||
|
|
d4d8adf4ac | ||
|
|
b1da0b8279 | ||
|
|
21fba1c4f7 | ||
|
|
41f6119118 | ||
|
|
0be4f31162 | ||
|
|
6bc8428dd0 | ||
|
|
fd92e92a4f | ||
|
|
9dc7a4447b | ||
|
|
eaab3c3f5e | ||
|
|
ee6e2b41f7 | ||
|
|
8eab44349f | ||
|
|
1a3a17e480 | ||
|
|
b3a7d42f9d | ||
|
|
17ed1f9806 | ||
|
|
bead888c67 | ||
|
|
07fcd66d8d | ||
|
|
0f4cac1b76 | ||
|
|
f0ebdf295f | ||
|
|
c6df492852 | ||
|
|
c0dbf95b94 | ||
|
|
d24a0312d8 | ||
|
|
aa5d8b9377 | ||
|
|
e9703e03cd | ||
|
|
13a8d27349 | ||
|
|
939f8f52c1 | ||
|
|
cb1e062f9b | ||
|
|
a1d1bc5aea | ||
|
|
1d81c0521f | ||
|
|
d3ef916b23 | ||
|
|
c9b7259cd7 | ||
|
|
266eb77eb5 | ||
|
|
c1cac82081 | ||
|
|
323c787d91 | ||
|
|
41070495ba | ||
|
|
cd2f897ff2 | ||
|
|
c877d4b1d7 | ||
|
|
dc6032aa43 | ||
|
|
fec9431ae5 | ||
|
|
bf1b7e640b | ||
|
|
ff79e7ad36 | ||
|
|
ebfdac96ae | ||
|
|
e2a85885be | ||
|
|
c6b0fb4d65 | ||
|
|
b5d879139a | ||
|
|
0e05918631 | ||
|
|
c3fee8d323 | ||
|
|
b5743d9902 | ||
|
|
44974c04ad | ||
|
|
afcf2a9400 | ||
|
|
26d918e218 | ||
|
|
23e1097f89 | ||
|
|
6d1e2d9fab | ||
|
|
e9384676e1 | ||
|
|
88bf51c066 | ||
|
|
fbbe8aff54 | ||
|
|
e3d441d19f | ||
|
|
4f3d20a7c4 | ||
|
|
b59d31855e | ||
|
|
a2211cfa46 | ||
|
|
e9ec42be02 | ||
|
|
0801d9bf65 | ||
|
|
21e03e8318 | ||
|
|
fa1b53682c | ||
|
|
443e338cc3 | ||
|
|
9cab049696 | ||
|
|
e30e869025 | ||
|
|
d2ff73b579 | ||
|
|
c31d9dfbb2 | ||
|
|
d7ed734ffb | ||
|
|
c3f33acdb3 | ||
|
|
58501c205a | ||
|
|
47f23884b4 | ||
|
|
acc0a2ec67 | ||
|
|
088d022d5e | ||
|
|
d7190b0602 | ||
|
|
4cf769e7b6 | ||
|
|
7125fb285e | ||
|
|
0454868958 | ||
|
|
f18e7295bd | ||
|
|
d6a6343aa8 | ||
|
|
7750720f4d | ||
|
|
950281caa6 | ||
|
|
b5202b5591 | ||
|
|
4aa01acce4 | ||
|
|
c58e788eba | ||
|
|
e7b60a1f27 | ||
|
|
1e4bbc4ecf | ||
|
|
e599da7033 | ||
|
|
341b5cd947 | ||
|
|
fa35f3f9e4 | ||
|
|
2e30a96389 | ||
|
|
3561c55174 | ||
|
|
5195c647f6 | ||
|
|
f3a0d1daac | ||
|
|
dcad6e2d23 | ||
|
|
52335bddbc | ||
|
|
05acf724a8 | ||
|
|
71319a0a7c | ||
|
|
c341c55258 | ||
|
|
cf40e641a6 | ||
|
|
ad0af16fa3 | ||
|
|
0f00e206bf | ||
|
|
27cdaf1ed5 | ||
|
|
32e8a45e4e | ||
|
|
34f35aff27 | ||
|
|
9b0101321a | ||
|
|
a4c9487192 | ||
|
|
6d3b4db760 | ||
|
|
2f71480849 | ||
|
|
c8519188a1 | ||
|
|
bf9f782970 | ||
|
|
a443e3dcde | ||
|
|
5496c6c8af | ||
|
|
b96d5e765e | ||
|
|
cee5fb915a | ||
|
|
54888ff278 | ||
|
|
ab3f3d72ab | ||
|
|
8451b4b14e | ||
|
|
ca85d5e8c0 | ||
|
|
9f7cf16335 | ||
|
|
e09353b0fe | ||
|
|
56ace4dd31 | ||
|
|
3cfd1a0957 | ||
|
|
3bd91dc9cb | ||
|
|
aa805a611a | ||
|
|
b46109a086 | ||
|
|
141b102129 | ||
|
|
2a03953f6c | ||
|
|
0ff3bb1a34 | ||
|
|
45d4c26972 | ||
|
|
c05aeffbbb | ||
|
|
b37b07bb06 | ||
|
|
83bb38b857 | ||
|
|
6ac398f11d | ||
|
|
774c210097 | ||
|
|
173aa53cbe | ||
|
|
be128bc12a | ||
|
|
305975bb3b | ||
|
|
e6726eb69d | ||
|
|
2988bae855 | ||
|
|
d65e1087f9 | ||
|
|
03744a7606 | ||
|
|
65e2a1c8aa | ||
|
|
1e8ef4b208 | ||
|
|
2a636481e8 | ||
|
|
9efc424462 | ||
|
|
ad9db64e8b | ||
|
|
0cf04e34c7 | ||
|
|
f932f96097 | ||
|
|
4c5dac5e13 | ||
|
|
abd838de00 | ||
|
|
cd92f69804 | ||
|
|
9d4cddb4a0 | ||
|
|
4f105ced0e | ||
|
|
983a69ed5d | ||
|
|
e17b6aa5c0 | ||
|
|
c73c302d77 | ||
|
|
bdd40ec59d | ||
|
|
d78064daa6 | ||
|
|
7683f7820f | ||
|
|
c6b88d1fcd | ||
|
|
dfaae1df1a | ||
|
|
58efa8411b | ||
|
|
95f000252b | ||
|
|
2cf5880940 | ||
|
|
88c948f117 | ||
|
|
89a369165e | ||
|
|
9fc53329b5 | ||
|
|
8765b7b3bd | ||
|
|
c4710b4bd2 | ||
|
|
43bd08a58f | ||
|
|
8a78cc2f5e | ||
|
|
186429890e | ||
|
|
85d9988d79 | ||
|
|
25ed2b794d | ||
|
|
b43a94b3c7 | ||
|
|
fc5cb3f0ad | ||
|
|
fc83a9e905 | ||
|
|
6635f2f9c1 | ||
|
|
dc054d7e6b | ||
|
|
555d464f8f | ||
|
|
c8a8336dc7 | ||
|
|
228c39719d | ||
|
|
5207fedd61 | ||
|
|
ae9c082cb7 | ||
|
|
b302f16f65 | ||
|
|
bf0bb0519a | ||
|
|
c9db57fb7f | ||
|
|
ca0ace0832 | ||
|
|
32217db357 | ||
|
|
a2ddfc5674 | ||
|
|
a418af0aad | ||
|
|
1e63fc14cb | ||
|
|
6c00ef65af | ||
|
|
19055ba004 | ||
|
|
74f4ae03f3 | ||
|
|
5894cec3e4 | ||
|
|
e66c411989 | ||
|
|
f74920fd1b | ||
|
|
c1c98cc7b6 | ||
|
|
29a73b183c | ||
|
|
830c7556b8 | ||
|
|
f6c9ab0068 | ||
|
|
e44b34062c | ||
|
|
320ae611a1 | ||
|
|
e54a87c436 | ||
|
|
608cc363a2 | ||
|
|
f9609c5871 | ||
|
|
cc422a6b1d | ||
|
|
638d75c388 | ||
|
|
b02495dd3d | ||
|
|
90ee8033b0 | ||
|
|
3f5d8fe2a1 | ||
|
|
32176d3e2f | ||
|
|
c65f55b22a | ||
|
|
c9d221404b | ||
|
|
ee73961832 | ||
|
|
ef39c174ed | ||
|
|
962d8f77dd | ||
|
|
bbc7abc50d | ||
|
|
00f1258032 | ||
|
|
beb297967f | ||
|
|
00c913fd19 | ||
|
|
a38a8c4ba4 | ||
|
|
56fafba8e9 | ||
|
|
d0396b3da9 | ||
|
|
180eaa2ce5 | ||
|
|
d8de60afb9 | ||
|
|
d5248e8472 | ||
|
|
a7c199b195 | ||
|
|
97a5351a52 | ||
|
|
e0b4452007 | ||
|
|
2e4a532b3c | ||
|
|
e54266d3a5 | ||
|
|
422ed0a5e2 | ||
|
|
59e17738cc | ||
|
|
50644cf3c4 | ||
|
|
4a3ceb710d | ||
|
|
5ab640c380 | ||
|
|
ba1afca4dd | ||
|
|
4d4ffdb86c | ||
|
|
0c6002a861 | ||
|
|
3ebdd8da14 | ||
|
|
31eb689635 | ||
|
|
c3d66f243a | ||
|
|
5c108635d0 | ||
|
|
365808eff2 | ||
|
|
5c654e99e4 | ||
|
|
709c47d40d | ||
|
|
8ff8fb9c92 | ||
|
|
e3cdc5d3ff | ||
|
|
69851d1596 | ||
|
|
c5330246b1 | ||
|
|
0f2b46b56a | ||
|
|
6580ea5891 | ||
|
|
1cfd5ae4f0 | ||
|
|
339beeabaf | ||
|
|
3a4b9e2e31 | ||
|
|
3055eeaa4f | ||
|
|
e517fa6000 | ||
|
|
3946ebcb92 | ||
|
|
a34fa04e4f | ||
|
|
2108f3209d | ||
|
|
8d2ae5e254 | ||
|
|
5092bc571d | ||
|
|
b013e8af50 | ||
|
|
3af8c4d28f | ||
|
|
1f1860e53c | ||
|
|
21015bccb5 | ||
|
|
b27fabea12 | ||
|
|
932c708538 | ||
|
|
adf241c146 | ||
|
|
b27a62c625 | ||
|
|
132596a17e | ||
|
|
f0359dcde9 | ||
|
|
08a005b271 | ||
|
|
f7fa47026c | ||
|
|
36c3fe6a27 | ||
|
|
7c67f08362 | ||
|
|
5d71a828c4 | ||
|
|
3e9392b4b7 | ||
|
|
02d9d7c22c | ||
|
|
8d60c65e5b | ||
|
|
0418cd0a95 | ||
|
|
a464295e5b | ||
|
|
95ec16fa92 | ||
|
|
2bd43cdc62 | ||
|
|
37e7222371 | ||
|
|
254c766883 | ||
|
|
e82a8ad63e | ||
|
|
39b4b233c9 | ||
|
|
3e8208a117 | ||
|
|
fbc7fd1de3 | ||
|
|
d533733d4b | ||
|
|
082716e21a | ||
|
|
5ffdecab9e | ||
|
|
62c87b4f87 | ||
|
|
ab3a50f22f | ||
|
|
71d3e8dd04 | ||
|
|
aa8bbc32c5 | ||
|
|
05646c03cc | ||
|
|
1453b30e41 | ||
|
|
07144659b1 | ||
|
|
69b7eb43f6 | ||
|
|
2a8b59b79a | ||
|
|
71fa0dff4b | ||
|
|
b58abf2a5c | ||
|
|
f7911701b1 | ||
|
|
f9d4b58588 | ||
|
|
1269aa273b | ||
|
|
8894c26748 | ||
|
|
9bb7e3a541 | ||
|
|
235cba5ba5 | ||
|
|
6c04b3936a | ||
|
|
a15635d953 | ||
|
|
04d9f3808b | ||
|
|
494724c795 | ||
|
|
71cadad05a | ||
|
|
30d204dddc | ||
|
|
cc19748fd2 | ||
|
|
48f197b7ea | ||
|
|
99b0ab5f50 | ||
|
|
db02b4443b | ||
|
|
8e35500269 | ||
|
|
74628642ad | ||
|
|
f41edf284c | ||
|
|
f740fde834 | ||
|
|
db35c28607 | ||
|
|
b1aae4a85a | ||
|
|
f4435c255c | ||
|
|
5aaec02af0 | ||
|
|
7f4b3edd84 | ||
|
|
14cc7fcfeb | ||
|
|
95c0afd5dd | ||
|
|
306ea31f0b | ||
|
|
18b7989e03 | ||
|
|
559eef594e | ||
|
|
7e6d2c6586 | ||
|
|
9fd22a92ac | ||
|
|
e9470f4c94 | ||
|
|
cceb4bb324 | ||
|
|
f14b4f4429 | ||
|
|
23db719c36 | ||
|
|
8b88a17836 | ||
|
|
f1599d6e69 | ||
|
|
df0c72ab0a | ||
|
|
c68395549f | ||
|
|
d69addc3af | ||
|
|
d96e67e850 | ||
|
|
321d9f376e | ||
|
|
e3450352c5 | ||
|
|
4ba8be67fe | ||
|
|
abff997179 | ||
|
|
55216e1de7 | ||
|
|
194f3352a1 | ||
|
|
9e0ae5dc96 | ||
|
|
ba88fd5306 | ||
|
|
179529214b | ||
|
|
b27a7a1c31 | ||
|
|
1a5b5327dc | ||
|
|
d30ac79a77 | ||
|
|
a1461d9ea6 | ||
|
|
783248d58b | ||
|
|
d13d77e39a | ||
|
|
bf333e1964 | ||
|
|
1833e8683b | ||
|
|
e4a336cd67 | ||
|
|
1810956185 | ||
|
|
af7f0f49d1 | ||
|
|
d6ebf8ea04 | ||
|
|
13721c9811 | ||
|
|
eafde17259 | ||
|
|
02810e84a9 | ||
|
|
ac3214fedc | ||
|
|
c376689ad4 | ||
|
|
a12a686a68 | ||
|
|
16ec69bb8c | ||
|
|
82cb95aff7 | ||
|
|
1eb3693f0a | ||
|
|
50925536d1 | ||
|
|
bfd9c654aa | ||
|
|
1104ce3176 | ||
|
|
19ef5b7e1d | ||
|
|
b1477f5fb5 | ||
|
|
8613a89264 | ||
|
|
6a90bac196 | ||
|
|
cda6398010 | ||
|
|
e502f1dcc4 | ||
|
|
13b699a183 | ||
|
|
0cd1f314f6 | ||
|
|
0c894ee48a | ||
|
|
f6f1d4a97c | ||
|
|
c85820e685 | ||
|
|
445f721ceb | ||
|
|
5fa24247c6 | ||
|
|
7ee76fdd2b | ||
|
|
2acfe7f1bf | ||
|
|
b33f660f90 | ||
|
|
91509888af | ||
|
|
fa3657f736 | ||
|
|
09638633c1 | ||
|
|
11a6f1124f | ||
|
|
98ba58f39b | ||
|
|
b6fa4f3242 | ||
|
|
baa7436e43 | ||
|
|
4a9b649e9f | ||
|
|
42143f77c5 | ||
|
|
e2bdd216bb | ||
|
|
856fde79ec | ||
|
|
91987b2e06 | ||
|
|
36c6eeabc9 | ||
|
|
549e364da3 | ||
|
|
1cc2d6c6b7 | ||
|
|
d0321ce0fa | ||
|
|
0d4cca3aa7 | ||
|
|
d77177a98f | ||
|
|
13bcac7e22 | ||
|
|
0046d8ba90 | ||
|
|
4475a93fc5 | ||
|
|
041b609f99 | ||
|
|
bb6653cecb | ||
|
|
d84ce07038 | ||
|
|
c9fec9ad51 | ||
|
|
e953ea7212 | ||
|
|
d1d7cd8186 | ||
|
|
d3e4f17dab | ||
|
|
344fde0d6f | ||
|
|
e27c8955c5 | ||
|
|
27fa234888 | ||
|
|
59ae047359 | ||
|
|
a847a2ff91 | ||
|
|
8445637dd1 | ||
|
|
7f339ede4e | ||
|
|
e54faa3079 | ||
|
|
4d2a4f3433 | ||
|
|
ddd6de24ce | ||
|
|
19f1b969ea | ||
|
|
48beb69103 | ||
|
|
a0be08d62a | ||
|
|
a750c7df2a | ||
|
|
950986c69e | ||
|
|
cc7bddefa1 | ||
|
|
1c4b735880 | ||
|
|
0862c135d0 | ||
|
|
01e3cf1b1c | ||
|
|
519bd389f6 | ||
|
|
ee6aac2614 | ||
|
|
fa319b6529 | ||
|
|
905be4130e | ||
|
|
814e973f9a | ||
|
|
d84596860c | ||
|
|
5d9414e728 | ||
|
|
016e279d43 | ||
|
|
d432047ac1 | ||
|
|
9938e60e05 | ||
|
|
6e751bbfaf | ||
|
|
8bdc07185b | ||
|
|
8872aceb0b | ||
|
|
f7b14c7da7 | ||
|
|
86cd732453 | ||
|
|
ae6571b18b | ||
|
|
0faf773f71 | ||
|
|
3178fb1a46 | ||
|
|
a917e80774 | ||
|
|
9bde5fcad6 | ||
|
|
e33f423733 | ||
|
|
cc35408607 | ||
|
|
10d91b50ec | ||
|
|
7dd06b5659 | ||
|
|
86087bf505 | ||
|
|
cb4923815a | ||
|
|
43258d64d2 | ||
|
|
6d3109be67 | ||
|
|
a25e0cc23f | ||
|
|
026a78b1b6 | ||
|
|
380a4a0395 | ||
|
|
f6c2a6387f | ||
|
|
27f65a92e9 | ||
|
|
858f33f782 | ||
|
|
fa072bf387 | ||
|
|
154ea7354d | ||
|
|
53dbac1d5c | ||
|
|
a780866cd8 | ||
|
|
bf6f1af217 | ||
|
|
29c85f3c65 | ||
|
|
ba51d3ebf5 | ||
|
|
e4f3ea1da9 | ||
|
|
f037452188 | ||
|
|
3d35601d47 | ||
|
|
146e642097 | ||
|
|
e2c53443e3 | ||
|
|
965a98d5a7 | ||
|
|
41a68e7b0e | ||
|
|
20e62b824c | ||
|
|
36e9fcbddf | ||
|
|
bd3d33d67b | ||
|
|
caf77a24f1 | ||
|
|
729f9e103d | ||
|
|
7261debb79 | ||
|
|
b4d0deddfc | ||
|
|
d62a32c7d7 | ||
|
|
cb94fc4d1e | ||
|
|
554888b21d | ||
|
|
4ae01282d7 | ||
|
|
020e62ddf5 | ||
|
|
c1dce98595 | ||
|
|
676187061c | ||
|
|
14f4f26791 | ||
|
|
57cf10c251 | ||
|
|
70b4fae62c | ||
|
|
5c3b9e7fae | ||
|
|
9ea39a78db | ||
|
|
8f77697482 | ||
|
|
1b5e53ddc6 | ||
|
|
86ee9595f4 | ||
|
|
dd7937dba4 | ||
|
|
a4c646152e | ||
|
|
3d7db9a9c7 | ||
|
|
06ed266278 | ||
|
|
5df16db823 | ||
|
|
4f23706b19 | ||
|
|
e9ceadfc34 | ||
|
|
267cdc365d | ||
|
|
6dfbaccee1 | ||
|
|
f97f48d428 | ||
|
|
70b3bc680e | ||
|
|
c125579a38 | ||
|
|
9bc2a7e7bc | ||
|
|
a5358dec14 | ||
|
|
c84f554f36 | ||
|
|
0dd9803f74 | ||
|
|
2baaa26a42 | ||
|
|
3fd62f906e | ||
|
|
41e8b44dde | ||
|
|
09cbd9b606 | ||
|
|
3ab8d79ff7 | ||
|
|
97eaed6d2c | ||
|
|
805aa23948 | ||
|
|
a25c45a502 | ||
|
|
6ebda209ac | ||
|
|
d226b31fab | ||
|
|
27a3009ce1 | ||
|
|
654ad61105 | ||
|
|
31a461195f | ||
|
|
331eef3164 | ||
|
|
cb0d576ade | ||
|
|
32978381b2 | ||
|
|
a7f7a688d3 | ||
|
|
5d87726397 | ||
|
|
b9601cb54a | ||
|
|
bb66555896 | ||
|
|
37726630ce | ||
|
|
3d12f85f66 | ||
|
|
e26762fce3 | ||
|
|
32e59fcce4 | ||
|
|
9b43330e95 | ||
|
|
b3b94243b4 | ||
|
|
fba1032388 | ||
|
|
ccc0803c5b | ||
|
|
f0340bcc98 | ||
|
|
8d19b8fcbf | ||
|
|
dbf66d5a9a | ||
|
|
0fa8d61b19 | ||
|
|
02d326419b | ||
|
|
79b8baac9f | ||
|
|
8a6df8abc7 | ||
|
|
8b8d763fb7 | ||
|
|
608e80a80b | ||
|
|
d90f11eb86 | ||
|
|
dc39187091 | ||
|
|
e4cd418533 | ||
|
|
8559469c73 | ||
|
|
ac8d2beb80 | ||
|
|
5e6384074e | ||
|
|
e6ba7bdd98 | ||
|
|
784055689f | ||
|
|
c937811f45 | ||
|
|
79c8021faa | ||
|
|
a428730f59 | ||
|
|
2522bd44d6 | ||
|
|
3cad8ea046 | ||
|
|
76131f1cc7 | ||
|
|
54fb5dc765 | ||
|
|
4110af56e7 | ||
|
|
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 | ||
|
|
27eca480d0 | ||
|
|
311ec1d5df | ||
|
|
a66beecd8d | ||
|
|
f833c8c598 | ||
|
|
828c3b3c9c | ||
|
|
d5f432d07c | ||
|
|
0f8404f686 | ||
|
|
ff46870bf1 | ||
|
|
1b55a5a399 | ||
|
|
afb06cab99 | ||
|
|
d26ac237e9 | ||
|
|
41c200a011 | ||
|
|
fecabca368 | ||
|
|
4afb864bef | ||
|
|
7b8690cbb7 | ||
|
|
9a97b4b754 | ||
|
|
ad4e1f216d | ||
|
|
d6264dfc0b | ||
|
|
675228d841 | ||
|
|
a28567b48a | ||
|
|
317ba23e5a | ||
|
|
26a30da072 | ||
|
|
b35e87780b | ||
|
|
f9aab38575 | ||
|
|
01f8815413 | ||
|
|
963aabb8f5 | ||
|
|
884fe3c7d9 | ||
|
|
8866e2aa13 | ||
|
|
07ac9710a8 | ||
|
|
9177962454 | ||
|
|
a66545dac5 | ||
|
|
0614dcb3e2 | ||
|
|
359d6c4dba | ||
|
|
242018d0d3 | ||
|
|
d9a81d1d14 | ||
|
|
eeb3b70328 | ||
|
|
ca6820f91b | ||
|
|
257eed01df | ||
|
|
f08475605a | ||
|
|
64d480be48 | ||
|
|
f240bca81a | ||
|
|
3b67dc4ac0 | ||
|
|
06fba3a816 | ||
|
|
1586ba83dc | ||
|
|
ab3a5df71d | ||
|
|
9d91629ba7 | ||
|
|
0921d51262 | ||
|
|
4bf898c9d9 | ||
|
|
d346c69033 | ||
|
|
cd68e387c9 | ||
|
|
ee3d51cb84 | ||
|
|
8e584dbe12 | ||
|
|
90f955e471 | ||
|
|
702fc5ac31 | ||
|
|
746a84b50b | ||
|
|
e8cc49fe14 | ||
|
|
f5b9c97edb | ||
|
|
376178b18a | ||
|
|
76521218ae | ||
|
|
f9a9c5c00f | ||
|
|
5779e164c6 | ||
|
|
e17693905a | ||
|
|
92add6b14d | ||
|
|
3493de7bb7 | ||
|
|
c0557f7974 | ||
|
|
f3e8473f60 | ||
|
|
d88a104237 | ||
|
|
932c2162ad | ||
|
|
6f3cdfcc4a | ||
|
|
b311c7991c | ||
|
|
2d25ff399f | ||
|
|
66c78f44c2 | ||
|
|
6f68947815 | ||
|
|
c24460962a | ||
|
|
8a0512c956 | ||
|
|
8f146c5161 | ||
|
|
d5076999fd | ||
|
|
69df0b6837 | ||
|
|
52bef05107 | ||
|
|
261f70c034 | ||
|
|
c5e53125b5 | ||
|
|
304e84f901 | ||
|
|
ed4e4a1d93 | ||
|
|
ae00038493 | ||
|
|
d6a60db1b7 | ||
|
|
a7fa3762aa | ||
|
|
53a2b37c08 | ||
|
|
161f7cd514 | ||
|
|
6816dc772b | ||
|
|
b72df57201 | ||
|
|
1bfdfc4535 | ||
|
|
4149ed361b | ||
|
|
2e1b7e940b | ||
|
|
683c75d308 | ||
|
|
8e8efbb805 | ||
|
|
67da57e7f6 | ||
|
|
1be5f9f748 | ||
|
|
1ee5260d8c | ||
|
|
c017586694 | ||
|
|
ce8df3c833 | ||
|
|
b2a93c04c6 | ||
|
|
2b19ba9572 | ||
|
|
3649ea612f | ||
|
|
4dd9d3b18d | ||
|
|
db307ca37d | ||
|
|
5769927760 | ||
|
|
e827e9be39 | ||
|
|
b799e04c39 | ||
|
|
0a1c45ef3b | ||
|
|
d06b6df34d | ||
|
|
b01854ec72 | ||
|
|
89d2d28ccb | ||
|
|
77f9bd931c | ||
|
|
9ae0779523 | ||
|
|
8d3952fcc9 | ||
|
|
0f759d763d | ||
|
|
22c7ae1053 | ||
|
|
f937aa374f | ||
|
|
2a72cb9700 | ||
|
|
200bc94870 | ||
|
|
168fad78a4 | ||
|
|
741cd4a857 | ||
|
|
68102d1523 | ||
|
|
be72b8c9fd | ||
|
|
0d000cf19b | ||
|
|
0ecf21813e | ||
|
|
e840fbd67d | ||
|
|
e85ea6a3ef | ||
|
|
f80c6ae6f3 | ||
|
|
5050cfa7e8 | ||
|
|
23d87cd926 | ||
|
|
c5a2974d53 | ||
|
|
3aa0736478 | ||
|
|
92f6aab290 | ||
|
|
12322b4be0 | ||
|
|
7a1b0e7a48 | ||
|
|
1914522c3a | ||
|
|
ad07bd9bb9 | ||
|
|
75d2c6686d | ||
|
|
9253b7f841 | ||
|
|
60970134e6 | ||
|
|
ba7136f70a | ||
|
|
dc2c592ee1 | ||
|
|
f910eb7946 | ||
|
|
d72db0db77 | ||
|
|
a251ec7739 | ||
|
|
a55d0e3b04 | ||
|
|
5dc5f31d07 | ||
|
|
f97efabcdb | ||
|
|
ba7995ab3f | ||
|
|
bb1723e2bb | ||
|
|
95866fa5c2 | ||
|
|
8a19456657 | ||
|
|
4e24a9f4dd | ||
|
|
7a5f2412c0 | ||
|
|
cf334b1ea4 | ||
|
|
eb5a723991 | ||
|
|
02e71a79bf | ||
|
|
8eae452765 | ||
|
|
05dea91c93 | ||
|
|
3f4dd04461 | ||
|
|
c2bbf98947 | ||
|
|
65be634999 | ||
|
|
c027648cb7 | ||
|
|
96ff09885d | ||
|
|
cc08b18e9e | ||
|
|
73186a771e | ||
|
|
c7a266cdfc | ||
|
|
a74b749421 | ||
|
|
37a87dbdee | ||
|
|
054b9e3dd2 | ||
|
|
9694eb5940 | ||
|
|
dafb36e3e9 | ||
|
|
364c771b96 | ||
|
|
93ba13b6a9 | ||
|
|
1969ad8dd9 | ||
|
|
0f1713ef32 | ||
|
|
4ddda18d5b | ||
|
|
7df1e92a36 | ||
|
|
998794e11e | ||
|
|
06036c41cd | ||
|
|
cab6b7783b | ||
|
|
8abfe5ff7d | ||
|
|
9ce152bc42 | ||
|
|
fb8ce37e4d | ||
|
|
b16d756941 | ||
|
|
72ad516140 | ||
|
|
56ed695544 | ||
|
|
f567ab8b11 | ||
|
|
968b879b0b | ||
|
|
05ffa8bc03 | ||
|
|
ac4bfcb098 | ||
|
|
2be612e042 | ||
|
|
1b1d7d9481 | ||
|
|
12ab8612cb | ||
|
|
71696ab48b | ||
|
|
6d7e05a9e0 | ||
|
|
82e13c7bed | ||
|
|
df27824e3f | ||
|
|
fac0ccfe68 | ||
|
|
d38536ff75 | ||
|
|
56e0cafe0d | ||
|
|
c55bbe339f | ||
|
|
84ed6cd722 | ||
|
|
dbcf8beda9 | ||
|
|
dbe37d6bc0 | ||
|
|
73b5652b5c | ||
|
|
2fea220ed8 | ||
|
|
8457854483 | ||
|
|
f00ad62b96 | ||
|
|
cd65f4c4d7 | ||
|
|
f9f4c59e55 | ||
|
|
402505e2cb | ||
|
|
4b13e4f7c6 | ||
|
|
d0e9cc44e0 | ||
|
|
9af4e0e15e | ||
|
|
c2f77ba39f | ||
|
|
7db4e397f0 | ||
|
|
399db108f3 | ||
|
|
9de5e5f984 | ||
|
|
30505269a6 | ||
|
|
7b1f1268fc | ||
|
|
fd23957e2e | ||
|
|
98dc5890fd | ||
|
|
4a1eeeedd1 | ||
|
|
bf3d11cbdc | ||
|
|
4e055a3eb0 | ||
|
|
e703f220ca | ||
|
|
9e4ec60d47 | ||
|
|
6ea960f2b7 | ||
|
|
2d98d34137 | ||
|
|
42c2aec975 | ||
|
|
b0f062234b | ||
|
|
16b3cb581b | ||
|
|
65e5bc8da0 | ||
|
|
82815cd697 | ||
|
|
87cabfeaeb | ||
|
|
4dcd5712e8 | ||
|
|
04a63d5e17 | ||
|
|
6b330eccef | ||
|
|
3b5a0a3067 | ||
|
|
9c9c216f6c | ||
|
|
e4cdecf653 | ||
|
|
544e11ac9c | ||
|
|
3fe6f27c57 | ||
|
|
c35a911d36 | ||
|
|
e7bb823677 | ||
|
|
f79d7b8550 | ||
|
|
11e5ebeacf | ||
|
|
2ed3da2328 | ||
|
|
3d0a30e38c | ||
|
|
a8a8cd8158 | ||
|
|
b518520dd6 | ||
|
|
9e7662c255 | ||
|
|
0b70872163 | ||
|
|
cc3999e9f6 | ||
|
|
aa3e137c9a | ||
|
|
c5dbf2d54d | ||
|
|
5770cfad3b | ||
|
|
9204ffae78 | ||
|
|
5340ea400c | ||
|
|
990ccee91d | ||
|
|
837885cb04 | ||
|
|
af00609edc | ||
|
|
8cf974701a | ||
|
|
82cd586950 | ||
|
|
e7f74373dd | ||
|
|
a9bb8f7130 | ||
|
|
68a084911d | ||
|
|
d02bb86083 | ||
|
|
408291403b | ||
|
|
5afe8d2805 | ||
|
|
1482b65c64 | ||
|
|
84173e2933 | ||
|
|
a0bd0c422e | ||
|
|
6ebbc369ca | ||
|
|
72fa14cb8f | ||
|
|
7bed153cc8 | ||
|
|
07f0ccacb6 | ||
|
|
b95f75354e | ||
|
|
15bf587616 | ||
|
|
e6dc1b5b09 | ||
|
|
0839b06ec2 | ||
|
|
67e9410f43 | ||
|
|
789409f74f | ||
|
|
141d588596 | ||
|
|
3a6a11bab3 | ||
|
|
bc17f8c865 | ||
|
|
9b434d4928 | ||
|
|
56bbe64b35 | ||
|
|
5504e0e7b7 | ||
|
|
890c211fe2 | ||
|
|
14be61b5d8 | ||
|
|
5455689f9b | ||
|
|
68e10ea066 | ||
|
|
c2d3a2d94f | ||
|
|
3905b9e089 | ||
|
|
ce15caed4f | ||
|
|
1c51219f40 | ||
|
|
85d8446e93 | ||
|
|
83b5812ade | ||
|
|
488e26af6d | ||
|
|
41549ae60d | ||
|
|
1ede748d81 | ||
|
|
5913ba9491 | ||
|
|
caaad2da85 | ||
|
|
143318dd34 | ||
|
|
1d48e27c80 | ||
|
|
c6ea09a031 | ||
|
|
d75eca55ad | ||
|
|
e8432d0000 | ||
|
|
276f9dbfdb | ||
|
|
cf1a5ba382 | ||
|
|
c551db1556 | ||
|
|
0caf3ea31a | ||
|
|
a1bd6e34cc | ||
|
|
c8d1c60bd8 | ||
|
|
41b2f64db4 | ||
|
|
3fc011e75e | ||
|
|
42a31a4479 | ||
|
|
b044ff5f35 | ||
|
|
843463a97f | ||
|
|
c14765f69d | ||
|
|
35e1b194c1 | ||
|
|
912c2aeb03 | ||
|
|
25eeea20c7 | ||
|
|
67f5964aff | ||
|
|
0bf7933d4d | ||
|
|
c5e868dc1b | ||
|
|
5f7e80643a | ||
|
|
bcd85eec82 | ||
|
|
55667b78df | ||
|
|
0ac429a0b6 | ||
|
|
bbc51b83b8 | ||
|
|
d0c709fa2f | ||
|
|
0dfcbb778b | ||
|
|
6ed70380b6 | ||
|
|
4d844a1608 | ||
|
|
9af650def7 | ||
|
|
1935bbf510 | ||
|
|
eadfd7fd48 | ||
|
|
1c33ad4527 | ||
|
|
ac6cc76d67 | ||
|
|
6adc8014da | ||
|
|
138df7c2c0 | ||
|
|
efb8cbc9f4 | ||
|
|
f0d03f2196 | ||
|
|
ed4077b1ff | ||
|
|
20fe982df2 | ||
|
|
25c33b69f3 | ||
|
|
722c8f38d1 | ||
|
|
3bb01d8eaf | ||
|
|
d4d7c64442 | ||
|
|
4c559f20a3 | ||
|
|
81fa1bc43f | ||
|
|
0c63f37a8e | ||
|
|
90d20b7e1a | ||
|
|
1cad2a94ec | ||
|
|
92d95bd585 | ||
|
|
39da7cbe22 | ||
|
|
061c603831 | ||
|
|
8fb92a316a | ||
|
|
527a571bf6 | ||
|
|
b2e81e3070 | ||
|
|
1a389d68c5 | ||
|
|
5908b4aaf2 | ||
|
|
684781aadd | ||
|
|
2b771772e2 | ||
|
|
ff439a2b0b | ||
|
|
ea8bcae586 | ||
|
|
3e7fff0c35 | ||
|
|
81bd3b4893 | ||
|
|
68e9842bd9 | ||
|
|
54552031d8 | ||
|
|
bfabccc60d | ||
|
|
17fe1df029 | ||
|
|
e10ccea86c | ||
|
|
fa470bd8bf | ||
|
|
9b7a2cda15 | ||
|
|
d0d60d26da | ||
|
|
b2c14c1218 | ||
|
|
2d59dc2c72 | ||
|
|
bb48cdf0a0 | ||
|
|
e00dcbcd92 | ||
|
|
5ab7d6e94e | ||
|
|
d1da9adc88 | ||
|
|
1455babd62 | ||
|
|
fc8529323c | ||
|
|
f9130794ee | ||
|
|
e62d6c0edd | ||
|
|
8d1dc4b090 | ||
|
|
acf6cf6ea2 | ||
|
|
d9ee44f90a | ||
|
|
bd6da5db9a | ||
|
|
4b3ade9b48 | ||
|
|
9daed7e0d4 | ||
|
|
4ef61e7af1 | ||
|
|
851f2d0517 | ||
|
|
5e5e04de8e | ||
|
|
e8b2f952af | ||
|
|
85b5d10e5a | ||
|
|
b916ca7bfb | ||
|
|
46190154f6 | ||
|
|
194552d2d3 | ||
|
|
a736cbc4d5 | ||
|
|
0310ecdfd0 | ||
|
|
28bbc8bbe9 | ||
|
|
f36ef66623 | ||
|
|
16ba51aa8c | ||
|
|
52847d1fad | ||
|
|
4733cc8a3e | ||
|
|
aecf61135f | ||
|
|
252d86eb70 | ||
|
|
438b0fe9d3 | ||
|
|
f6c58b5a28 | ||
|
|
dee2f94c38 | ||
|
|
1fc4dec5ca | ||
|
|
d7ab12bc61 | ||
|
|
1f50612a16 | ||
|
|
43715f1e34 | ||
|
|
1bf0ff69d8 | ||
|
|
abf992dfb8 | ||
|
|
707dfee696 | ||
|
|
e8c4f059c0 | ||
|
|
1a6f80f2df | ||
|
|
47ae310ac7 | ||
|
|
923c61f9c7 | ||
|
|
18b8c558cd | ||
|
|
84a091e380 | ||
|
|
1399098e30 | ||
|
|
593f8add5d | ||
|
|
2f26624f29 | ||
|
|
14c901d219 | ||
|
|
2b2e45ca45 | ||
|
|
868e9a322e | ||
|
|
d2f3f58de1 | ||
|
|
a69b3fcd11 | ||
|
|
a3505ee7f2 | ||
|
|
efe5e0e2c4 | ||
|
|
8745527c5d | ||
|
|
78b40df71e | ||
|
|
9675619ab9 | ||
|
|
68ca2c2be6 | ||
|
|
835ecbb410 | ||
|
|
22756a3c13 | ||
|
|
3c2fb04ed8 | ||
|
|
db5fb840a6 | ||
|
|
65b05d707b | ||
|
|
1b972df7e2 | ||
|
|
40266c275d | ||
|
|
197db35c80 | ||
|
|
8771e1ace7 | ||
|
|
e6e1275ad2 | ||
|
|
417dc0859b | ||
|
|
806e1f29bb | ||
|
|
9724f5769d | ||
|
|
5a0c8914c4 | ||
|
|
c2e0a30da8 | ||
|
|
45ae4f20a0 | ||
|
|
398826d7ef | ||
|
|
14e5f3bf74 | ||
|
|
57b1cbf41b | ||
|
|
51a0d88b9b | ||
|
|
16d4d7d1ea | ||
|
|
2affb1513d | ||
|
|
214d4b2a9e | ||
|
|
2f486d979b | ||
|
|
023c5fb99a | ||
|
|
329ed371f9 | ||
|
|
0577bfbde3 | ||
|
|
3ce5c35143 | ||
|
|
9258ef4bb3 | ||
|
|
e02facc170 | ||
|
|
4dc76926ea | ||
|
|
ba6be7e987 | ||
|
|
6b2da1ec97 | ||
|
|
6aa1b515c7 | ||
|
|
5d64ec1c8f | ||
|
|
e1b81fb931 | ||
|
|
c91752598c | ||
|
|
708515925f | ||
|
|
a61745f868 | ||
|
|
2ca7af34df | ||
|
|
bd7e6f4e3e | ||
|
|
32e86d461e | ||
|
|
66ea5ad979 | ||
|
|
ebfa80d444 | ||
|
|
4247cc819d | ||
|
|
1f9f5bd734 | ||
|
|
426d791eea | ||
|
|
30d99b812c | ||
|
|
981e55bc4d | ||
|
|
bab1090a25 | ||
|
|
84b5181fd8 | ||
|
|
222ebb58c0 | ||
|
|
f5ad3a6a2e | ||
|
|
422af60827 | ||
|
|
9127321540 | ||
|
|
dcae059480 | ||
|
|
6d6603e013 | ||
|
|
90fe8078c3 | ||
|
|
367fb32114 | ||
|
|
b2b82afd71 |
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
gns3/version.py merge=ours
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -37,6 +37,7 @@ nosetests.xml
|
||||
|
||||
# PyCharm
|
||||
.idea
|
||||
/.eggs
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
@@ -47,3 +48,15 @@ nosetests.xml
|
||||
|
||||
# Gedit Temp Files
|
||||
*~
|
||||
|
||||
# Qt creator
|
||||
*.autosave
|
||||
|
||||
# Licence keys
|
||||
keys
|
||||
|
||||
# Custom config
|
||||
/gns3_server.ini
|
||||
updates
|
||||
.cache
|
||||
__pycache__
|
||||
|
||||
33
.travis.yml
33
.travis.yml
@@ -1,24 +1,19 @@
|
||||
language: python
|
||||
sudo: required
|
||||
|
||||
python:
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
|
||||
install:
|
||||
- "pip install -r requirements.txt --use-mirrors"
|
||||
- "pip install tox"
|
||||
|
||||
script: "python setup.py test"
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
services:
|
||||
- docker
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#gns3"
|
||||
on_success: change
|
||||
on_failure: always
|
||||
#email:
|
||||
# - julien@gns3.net
|
||||
#irc:
|
||||
# channels:
|
||||
# - "chat.freenode.net#gns3"
|
||||
# on_success: change
|
||||
# on_failure: always
|
||||
|
||||
script:
|
||||
- docker build -t gns3-gui-test .
|
||||
- docker run gns3-gui-test
|
||||
|
||||
|
||||
54
CONTRIBUTING.md
Normal file
54
CONTRIBUTING.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 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
|
||||
|
||||
## Security issues
|
||||
|
||||
For security issues please keep it private and send an email to developers@gns3.net
|
||||
|
||||
## 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.
|
||||
499
COPYING
Normal file
499
COPYING
Normal file
@@ -0,0 +1,499 @@
|
||||
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 gns3-converter
|
||||
---------------------------------
|
||||
https://github.com/dlintott/gns3-converter/blob/master/COPYING
|
||||
|
||||
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/
|
||||
|
||||
License notice for BusyBox
|
||||
---------------------------
|
||||
BusyBox is distributed under version 2 of the General Public License
|
||||
https://busybox.net/license.html
|
||||
|
||||
Source code is available here:
|
||||
https://github.com/GNS3/busybox
|
||||
|
||||
|
||||
Licence notice for zipstream
|
||||
-----------------------------
|
||||
zipstream is distributed under version 3 of the General Public License
|
||||
https://github.com/allanlei/python-zipstream/blob/master/LICENSE
|
||||
|
||||
Source code is available here:
|
||||
https://pypi.python.org/pypi/zipstream
|
||||
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
|
||||
@@ -3,9 +3,10 @@ include AUTHORS
|
||||
include INSTALL
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
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]
|
||||
|
||||
75
README.rst
75
README.rst
@@ -1,69 +1,42 @@
|
||||
GNS3-gui
|
||||
========
|
||||
|
||||
GNS3 GUI repository (beta stage).
|
||||
.. image:: https://travis-ci.org/GNS3/gns3-gui.svg?branch=master
|
||||
:target: https://travis-ci.org/GNS3/gns3-gui
|
||||
|
||||
Linux (Debian based)
|
||||
--------------------
|
||||
.. image:: https://img.shields.io/pypi/v/gns3-gui.svg
|
||||
:target: https://pypi.python.org/pypi/gns3-gui
|
||||
|
||||
The following instructions have been tested with Ubuntu and Mint.
|
||||
You must be connected to the Internet in order to install the dependencies.
|
||||
|
||||
Dependencies:
|
||||
GNS3 GUI repository.
|
||||
|
||||
- Python 3.3 or above
|
||||
- Setuptools
|
||||
- PyQt libraries
|
||||
- Apache Libcloud library
|
||||
- Requests library
|
||||
- Paramiko library
|
||||
Installation
|
||||
------------
|
||||
|
||||
The following commands will install some of these dependencies:
|
||||
https://gns3.com/support/docs
|
||||
|
||||
Development
|
||||
-------------
|
||||
|
||||
If you want to update the interface, modify the .ui files using QT tools. And:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
sudo apt-get install python3-setuptools
|
||||
sudo apt-get install python3-pyqt4
|
||||
cd scripts
|
||||
python build_pyqt.py
|
||||
|
||||
Finally these commands will install the GUI as well as the rest of the dependencies:
|
||||
Debug
|
||||
"""""
|
||||
|
||||
If you want to see the full logs in the internal shell you can type:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
debug 2
|
||||
|
||||
cd gns3-gui-master
|
||||
sudo python3 setup.py install
|
||||
gns3
|
||||
|
||||
Windows
|
||||
-------
|
||||
Or start the app with --debug flag.
|
||||
|
||||
Please use our all-in-one installer.
|
||||
Due to the fact PyQT intercept you can use a web debugger for inspecting stuff:
|
||||
https://github.com/Kozea/wdb
|
||||
|
||||
Mac OS X
|
||||
--------
|
||||
|
||||
Please use our DMG package or you can manually install using the following steps (experimental):
|
||||
|
||||
`First install homebrew <http://brew.sh/>`_.
|
||||
|
||||
Then install the GNS3 dependencies.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
brew install python3
|
||||
brew install qt
|
||||
brew install sip --without-python --with-python3
|
||||
brew install pyqt --without-python --with-python3
|
||||
|
||||
Finally, install both the GUI & server from the source.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd gns3-gui-master
|
||||
python3 setup.py install
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd gns3-server-master
|
||||
python3 setup.py install
|
||||
|
||||
Or follow this `HOWTO that uses MacPorts <http://binarynature.blogspot.ca/2014/05/install-gns3-early-release-on-mac-os-x.html>`_.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
-rrequirements.txt
|
||||
|
||||
pep8
|
||||
pytest
|
||||
pytest-pythonpath # useful for running tests outside tox
|
||||
pytest-timeout
|
||||
pytest-capturelog
|
||||
|
||||
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()
|
||||
29
gns3-gui.appdata.xml
Normal file
29
gns3-gui.appdata.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright 2016 Athmane Madjoudj <athmane@fedoraproject.org> -->
|
||||
<component type="desktop">
|
||||
<id>gns3-gui.desktop</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<name>GNS3</name>
|
||||
<summary>Graphical Network Simulator 3</summary>
|
||||
<description>
|
||||
<p>
|
||||
GNS3 is a graphical network simulator that allows you to design complex network
|
||||
topologies. You may run simulations or configure devices ranging from simple
|
||||
workstations to powerful routers.
|
||||
</p>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://a.fsdn.com/con/app/proj/gns-3/screenshots/127765.jpg</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://a.fsdn.com/con/app/proj/gns-3/screenshots/127755.jpg</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://a.fsdn.com/con/app/proj/gns-3/screenshots/127755.jpg</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<url type="homepage">http://gns3.com/</url>
|
||||
<update_contact>athmane_at_fedoraproject.org</update_contact>
|
||||
</component>
|
||||
9
gns3-gui.desktop
Normal file
9
gns3-gui.desktop
Normal file
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Name=GNS3
|
||||
GenericName=Graphical Network Simulator 3
|
||||
Comment=Graphical Network Simulator 3
|
||||
Exec=gns3
|
||||
Icon=gns3
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Application;Network;Qt;
|
||||
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()
|
||||
60
gns3/application.py
Normal file
60
gns3/application.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/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 gns3.utils import parse_version
|
||||
from .version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Application(QtWidgets.QApplication):
|
||||
file_open_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, argv):
|
||||
|
||||
self.setStyle(QtWidgets.QStyleFactory.create("Fusion"))
|
||||
# both Qt and PyQt must be version >= 5.6 in order to enable high DPI scaling
|
||||
if parse_version(QtCore.QT_VERSION_STR) >= parse_version("5.6") and parse_version(QtCore.PYQT_VERSION_STR) >= parse_version("5.6"):
|
||||
# only available starting Qt version 5.6
|
||||
self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
|
||||
self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
|
||||
|
||||
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,341 +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))
|
||||
|
||||
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. """
|
||||
|
||||
try:
|
||||
return self.driver.list_nodes()
|
||||
except Exception as e:
|
||||
log.error("list_instances returned an error: {}".format(e))
|
||||
|
||||
|
||||
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,45 +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,311 +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, gns3_ias_url):
|
||||
super(RackspaceCtrl, self).__init__(username, api_key)
|
||||
|
||||
self.gns3_ias_url = gns3_ias_url
|
||||
|
||||
# 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_shared_images(self, username, region, gns3_version):
|
||||
"""
|
||||
Given a GNS3 version, ask gns3-ias to share compatible images
|
||||
|
||||
Response:
|
||||
[{"created_at": "", "schema": "", "status": "", "member_id": "", "image_id": "", "updated_at": ""},]
|
||||
or, if access was already asked
|
||||
[{"image_id": "", "member_id": "", "status": "ALREADYREQUESTED"},]
|
||||
"""
|
||||
endpoint = self.gns3_ias_url+"/images/grant_access"
|
||||
params = {
|
||||
"user_id": username,
|
||||
"user_region": region.upper(),
|
||||
"gns3_version": gns3_version,
|
||||
}
|
||||
try:
|
||||
response = requests.get(endpoint, params=params)
|
||||
except requests.ConnectionError:
|
||||
raise ApiError("Unable to connect to IAS")
|
||||
|
||||
status = response.status_code
|
||||
|
||||
if status == 200:
|
||||
return response.json()
|
||||
elif status == 404:
|
||||
raise ItemNotFound()
|
||||
else:
|
||||
raise ApiError("IAS status code: %d" % status)
|
||||
|
||||
def list_images(self):
|
||||
"""
|
||||
Return a dictionary containing RackSpace server images
|
||||
retrieved from gns3-ias server
|
||||
"""
|
||||
if not (self.tenant_id and self.region):
|
||||
return {}
|
||||
|
||||
try:
|
||||
shared_images = self._get_shared_images(self.tenant_id, self.region, __version__)
|
||||
images = {}
|
||||
for i in shared_images:
|
||||
images[i['image_id']] = i['image_name']
|
||||
return images
|
||||
except ItemNotFound:
|
||||
return {}
|
||||
except ApiError as e:
|
||||
log.error('Error while retrieving image list: %s' % e)
|
||||
return {}
|
||||
|
||||
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']
|
||||
ias_url = cloud_settings['gns3_ias_url']
|
||||
except KeyError as e:
|
||||
log.error("Unable to create cloud provider: {}".format(e))
|
||||
return
|
||||
|
||||
provider = RackspaceCtrl(username, apikey, ias_url)
|
||||
|
||||
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,466 +0,0 @@
|
||||
from contextlib import contextmanager
|
||||
import io
|
||||
import json
|
||||
from socket import error as socket_error
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
from PyQt4.QtCore import QThread
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from .exceptions import KeyPairExists
|
||||
from .rackspace_ctrl import RackspaceCtrl, 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.error("SSH connection error to {}: {}".format(host, e))
|
||||
yield None
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
class ListInstancesThread(QThread):
|
||||
"""
|
||||
Helper class to retrieve data from the provider in a separate thread,
|
||||
avoid freezing the gui
|
||||
"""
|
||||
instancesReady = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent, provider):
|
||||
super(QThread, self).__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(QThread):
|
||||
"""
|
||||
Helper class to create instances in a separate thread
|
||||
"""
|
||||
instanceCreated = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, parent, provider, name, flavor_id, image_id):
|
||||
super(QThread, self).__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(QThread):
|
||||
"""
|
||||
Helper class to remove an instance in a separate thread
|
||||
"""
|
||||
instanceDeleted = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent, provider, instance):
|
||||
super(QThread, self).__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(QThread):
|
||||
"""
|
||||
Perform an SSH connection to the instances in a separate thread,
|
||||
outside the GUI event loop, and start GNS3 server
|
||||
"""
|
||||
gns3server_started = 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.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
|
||||
killall python3 gns3server gns3dms
|
||||
'''
|
||||
|
||||
def __init__(self, parent, host, private_key_string, server_id, username, api_key, region, dead_time):
|
||||
super(QThread, self).__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(QThread):
|
||||
"""
|
||||
Establish a websocket connection with the remote gns3server
|
||||
instance. Run outside the GUI event loop.
|
||||
"""
|
||||
established = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent, provider, server_id, host, port, ca_file,
|
||||
auth_user, auth_password, ssh_pkey, instance_id):
|
||||
super(QThread, self).__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(QThread):
|
||||
"""
|
||||
Zip and Upload project to the cloud
|
||||
"""
|
||||
|
||||
# signals to update the progress dialog.
|
||||
error = pyqtSignal(str, bool)
|
||||
completed = pyqtSignal()
|
||||
update = pyqtSignal(int)
|
||||
|
||||
def __init__(self, cloud_settings, project_path, images_path):
|
||||
super().__init__()
|
||||
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(str(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(QThread):
|
||||
"""
|
||||
Upload multiple files to cloud files
|
||||
|
||||
uploads - A list of 2-tuples of (local_src_path, remote_dst_path)
|
||||
"""
|
||||
|
||||
completed = pyqtSignal()
|
||||
|
||||
def __init__(self, parent, cloud_settings, uploads):
|
||||
super(QThread, self).__init__(parent)
|
||||
self._cloud_settings = cloud_settings
|
||||
self._uploads = uploads
|
||||
|
||||
def run(self):
|
||||
for src, dst in self._uploads:
|
||||
log.debug('Upload from {} to {}'.format(src, dst))
|
||||
provider = get_provider(self._cloud_settings)
|
||||
provider.upload_file(src, dst)
|
||||
log.debug('Upload image completed')
|
||||
self.completed.emit()
|
||||
|
||||
|
||||
class DownloadProjectThread(QThread):
|
||||
"""
|
||||
Downloads project from cloud storage
|
||||
"""
|
||||
|
||||
# signals to update the progress dialog.
|
||||
error = pyqtSignal(str, bool)
|
||||
completed = pyqtSignal()
|
||||
update = pyqtSignal(int)
|
||||
|
||||
def __init__(self, cloud_project_file_name, project_dest_path, images_dest_path, cloud_settings):
|
||||
super().__init__()
|
||||
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(str(e)), True)
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class DeleteProjectThread(QThread):
|
||||
"""
|
||||
Deletes project from cloud storage
|
||||
"""
|
||||
|
||||
# signals to update the progress dialog.
|
||||
error = pyqtSignal(str, bool)
|
||||
completed = pyqtSignal()
|
||||
update = pyqtSignal(int)
|
||||
|
||||
def __init__(self, project_file_name, cloud_settings):
|
||||
super().__init__()
|
||||
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(str(e)), True)
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_cloud_projects(cloud_settings):
|
||||
provider = get_provider(cloud_settings)
|
||||
return provider.list_projects()
|
||||
@@ -1,443 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import ast
|
||||
import logging
|
||||
import os
|
||||
from PyQt4.QtGui import QWidget
|
||||
from PyQt4.QtGui import QIcon
|
||||
from PyQt4.QtGui import QMenu
|
||||
from PyQt4.QtGui import QAction
|
||||
from PyQt4.QtGui import QInputDialog
|
||||
from PyQt4.QtCore import QAbstractTableModel
|
||||
from PyQt4.QtCore import QModelIndex
|
||||
from PyQt4.QtCore import QTimer
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
from PyQt4.Qt import Qt
|
||||
|
||||
from .cloud.utils import (ListInstancesThread, CreateInstanceThread, DeleteInstanceThread,
|
||||
StartGNS3ServerThread, WSConnectThread)
|
||||
from libcloud.compute.types import NodeState
|
||||
from .topology import Topology
|
||||
|
||||
# this widget was promoted on Creator, must use absolute imports
|
||||
from gns3.ui.cloud_inspector_view_ui import Ui_CloudInspectorView
|
||||
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 = 10
|
||||
GNS3SERVER_STARTED = 11
|
||||
WS_CONNECTED = 12
|
||||
|
||||
|
||||
class InstanceTableModel(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 == Qt.DecorationRole:
|
||||
if col == 1:
|
||||
# status
|
||||
return QIcon(self._get_status_icon_path(instance))
|
||||
|
||||
elif role == 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 == Qt.Horizontal and role == 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(QModelIndex(), self.rowCount(), self.rowCount())
|
||||
if not len(self._instances):
|
||||
self.beginInsertColumns(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(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(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 = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent):
|
||||
super(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(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 = QTimer(self)
|
||||
self._pollingTimer.timeout.connect(self._polling_slot)
|
||||
|
||||
# map flavor ids to combobox indexes
|
||||
self.flavor_index_id = []
|
||||
|
||||
# TODO: Delete me
|
||||
self._running = {}
|
||||
|
||||
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, instances):
|
||||
"""
|
||||
Fill the model data layer with instances retrieved through libcloud
|
||||
"""
|
||||
self._main_window = main_win
|
||||
self._provider = main_win.cloudProvider
|
||||
self._settings = main_win.cloudSettings()
|
||||
log.info('CloudInspectorView.load')
|
||||
|
||||
for i in instances:
|
||||
self._project_instances_id.append(i["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 = QAction("Delete", self)
|
||||
delete_action.triggered.connect(self._deleteSelectedInstance)
|
||||
# create context menu and add actions
|
||||
menu = 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)
|
||||
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 _gns3server_started_slot(self, id, host_ip, start_response):
|
||||
"""
|
||||
This slot is called when the StartGNS3ServerThread succesfully started
|
||||
the server.
|
||||
|
||||
:param id: the id of the instance
|
||||
:param host_ip: the host ip of the instance
|
||||
:param start_response: the output of the server start script on the remote host
|
||||
"""
|
||||
# instance state transition: GNS3SERVER_STARTING --> GNS3SERVER_STARTED
|
||||
instance = self._model.getInstanceById(id)
|
||||
instance.state = RunningInstanceState.GNS3SERVER_STARTED
|
||||
self._model.updateInstanceFields(instance, ['state'])
|
||||
|
||||
data = ast.literal_eval(start_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(host_ip)
|
||||
# TODO: Move this directory into projectSettings.
|
||||
ca_dir = os.path.join(self._main_window.projectSettings()["project_files_dir"], "keys")
|
||||
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(id)
|
||||
top_instance.set_later_attributes(host_ip, port, ssl_cert, ca_file)
|
||||
ssh_pkey = top_instance.private_key
|
||||
|
||||
log.debug('Cloud server gns3server started.')
|
||||
wss_thread = WSConnectThread(self, self._provider, id, host_ip, port, ca_file,
|
||||
username, password, ssh_pkey, id)
|
||||
wss_thread.established.connect(self._wss_connected_slot)
|
||||
wss_thread.start()
|
||||
|
||||
def _wss_connected_slot(self, id):
|
||||
"""
|
||||
This slot is called when the WSConnectThread successfully connected to
|
||||
the websocket on the remote host
|
||||
"""
|
||||
# instance state transition: GNS3SERVER_STARTED --> WS_CONNECTED
|
||||
instance = self._model.getInstanceById(id)
|
||||
instance.state = RunningInstanceState.WS_CONNECTED
|
||||
self._model.updateInstanceFields(instance, ['state'])
|
||||
|
||||
def _get_public_ip(self, ip_list):
|
||||
"""
|
||||
Pick the ipv4 address from the list of ip addresses that the instance
|
||||
has.
|
||||
"""
|
||||
for ip in ip_list:
|
||||
log.debug('Cloud server ip {}'.format(ip))
|
||||
# Don't use the ipv6 address
|
||||
if ':' not in ip:
|
||||
log.debug('Chose {} as public ip'.format(ip))
|
||||
return ip
|
||||
return None
|
||||
|
||||
def _update_model(self, instances):
|
||||
if not instances:
|
||||
return
|
||||
|
||||
# populate underlying model if this is the first call
|
||||
if self._model.rowCount() == 0 and len(instances) > 0:
|
||||
self._populate_model(instances)
|
||||
|
||||
instance_manager = CloudInstances.instance()
|
||||
instance_manager.update_instances(instances)
|
||||
|
||||
# filter instances to only those in the current project
|
||||
project_instances = [i for i in instances if i.id in self._project_instances_id]
|
||||
for i in project_instances:
|
||||
if i.state != RunningInstanceState.RUNNING:
|
||||
self._model.updateInstanceFields(i, ['state'])
|
||||
|
||||
# cleanup 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()
|
||||
|
||||
# start gns3server if needed
|
||||
for i in project_instances:
|
||||
# get the real instance state from self._model
|
||||
model_instance = self._model.getInstanceById(i.id)
|
||||
|
||||
if model_instance.state == RunningInstanceState.RUNNING:
|
||||
# instance state transition: RUNNING --> GNS3SERVER_STARTING
|
||||
model_instance.state = RunningInstanceState.GNS3SERVER_STARTING
|
||||
self._model.updateInstanceFields(model_instance, ['state'])
|
||||
|
||||
# start GNS3 server and deadman switch
|
||||
public_ip = self._get_public_ip(i.public_ips)
|
||||
instance_manager.update_host_for_instance(i.id, public_ip)
|
||||
topology_instance = instance_manager.get_instance(i.id)
|
||||
ssh_thread = StartGNS3ServerThread(
|
||||
self, public_ip, topology_instance.private_key, i.id,
|
||||
self._provider.username, self._provider.api_key, self._provider.region,
|
||||
1800)
|
||||
ssh_thread.gns3server_started.connect(self._gns3server_started_slot)
|
||||
ssh_thread.start()
|
||||
|
||||
def _populate_model(self, instances):
|
||||
log.info('CloudInspectorView._populate_model')
|
||||
self._model.flavors = self._provider.list_flavors()
|
||||
# filter instances for current project
|
||||
project_instances = [i for i in instances if i.id in self._project_instances_id]
|
||||
for i in project_instances:
|
||||
self._model.addInstance(i)
|
||||
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 = 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:
|
||||
if not name.endswith("-gns3"):
|
||||
name += "-gns3"
|
||||
|
||||
create_thread = CreateInstanceThread(self, self._provider, name, flavor_id, image_id)
|
||||
create_thread.instanceCreated.connect(self._main_window.add_instance_to_project)
|
||||
create_thread.instanceCreated.connect(CloudInstances.instance().add_instance)
|
||||
create_thread.start()
|
||||
@@ -1,145 +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
|
||||
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):
|
||||
save_needed = False
|
||||
# Look for instances that have been deleted
|
||||
for static in self._instances:
|
||||
found = False
|
||||
for dynamic in instances:
|
||||
if static.id == dynamic.id:
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
self._instances.remove(static)
|
||||
save_needed = True
|
||||
|
||||
if save_needed:
|
||||
self.save()
|
||||
|
||||
def update_host_for_instance(self, instance_id, host):
|
||||
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
|
||||
@@ -1,21 +0,0 @@
|
||||
!
|
||||
|
||||
kerberos password
|
||||
crypto RSA-key-pair %h.mydomain.com 0 1014940935
|
||||
30820155 02010030 0D06092A 864886F7 0D010101 05000482 013F3082 013B0201
|
||||
00024100 A7EA2920 73033037 689F8166 B6AEA7FF 91015466 7379FA4F D7B175C3
|
||||
8D5D1E56 89B00E73 D5553491 06D651DA 71213D18 3E4EAF44 8C5F05F1 E8C1FE47
|
||||
B07D5A1B 02030100 01024049 FE964106 6DD14199 8930ACE2 B3F4B45A 620B9F5A
|
||||
23D67A78 C26AF2D1 C8C72504 987ADD3E 2755DCC4 70AADB86 679171D7 54A9038F
|
||||
0EB080E7 8B514EB8 8A038102 2100D588 DF0A6D31 AEF5C231 5A4A3459 5D3FD973
|
||||
F1A13EA8 2C25D210 6ACD4733 39AF0221 00C94EC2 9428B371 2599E7EA 8C89E86C
|
||||
E188F689 3AFCFE7A 59B42810 E83DABBD 55022100 944FB792 D75ACDC9 96328F22
|
||||
C10F5CAC 2F4DCF83 0E30E250 F6813E9D 0B99F1B3 02204863 D126D428 0B05197E
|
||||
4362FC68 9F56CF18 D0AA6CB5 DA2B8DD4 66980D2D 47ED0221 00991914 B6CDC66E
|
||||
60AF0332 D5FB2771 B9F0317B 886E6E48 B86CDFDF 3FC1D48E CA
|
||||
quit
|
||||
305C300D 06092A86 4886F70D 01010105 00034B00 30480241 00A7EA29 20730330
|
||||
37689F81 66B6AEA7 FF910154 667379FA 4FD7B175 C38D5D1E 5689B00E 73D55534
|
||||
9106D651 DA71213D 183E4EAF 448C5F05 F1E8C1FE 47B07D5A 1B020301 0001
|
||||
quit
|
||||
end
|
||||
@@ -6,7 +6,7 @@ no service password-encryption
|
||||
hostname %h
|
||||
!
|
||||
ip cef
|
||||
no ip domain lookup
|
||||
no ip domain-lookup
|
||||
no ip icmp rate-limit unreachable
|
||||
ip tcp synwait 5
|
||||
no cdp log mismatch duplex
|
||||
|
||||
@@ -8,7 +8,7 @@ hostname %h
|
||||
!
|
||||
ip cef
|
||||
no ip routing
|
||||
no ip domain lookup
|
||||
no ip domain-lookup
|
||||
no ip icmp rate-limit unreachable
|
||||
ip tcp synwait 5
|
||||
no cdp log mismatch duplex
|
||||
|
||||
@@ -30,83 +30,83 @@ ip tcp synwait-time 5
|
||||
!
|
||||
interface Ethernet0/0
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/1
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/2
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/3
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/0
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/1
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/2
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/3
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial2/0
|
||||
interface Ethernet2/0
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial2/1
|
||||
interface Ethernet2/1
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial2/2
|
||||
interface Ethernet2/2
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial2/3
|
||||
interface Ethernet2/3
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial3/0
|
||||
interface Ethernet3/0
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial3/1
|
||||
interface Ethernet3/1
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial3/2
|
||||
interface Ethernet3/2
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial3/3
|
||||
interface Ethernet3/3
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Vlan1
|
||||
no ip address
|
||||
@@ -13,7 +13,7 @@ no ip icmp rate-limit unreachable
|
||||
!
|
||||
!
|
||||
ip cef
|
||||
no ip domain lookup
|
||||
no ip domain-lookup
|
||||
!
|
||||
!
|
||||
ip tcp synwait-time 5
|
||||
@@ -28,14 +28,14 @@ import json
|
||||
from .qt import QtCore
|
||||
from .node import Node
|
||||
from .version import __version__
|
||||
try:
|
||||
from gns3converter import __version__ as gns3converter_version
|
||||
except ImportError:
|
||||
gns3converter_version = "Not installed"
|
||||
|
||||
|
||||
class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
cmd.Cmd.__init__(self)
|
||||
|
||||
def do_version(self, args):
|
||||
"""
|
||||
Show the version of GNS3 and its dependencies.
|
||||
@@ -45,6 +45,7 @@ class ConsoleCmd(cmd.Cmd):
|
||||
if hasattr(sys, "frozen"):
|
||||
compiled = "(compiled)"
|
||||
print("GNS3 version is {} {}".format(__version__, compiled))
|
||||
print("GNS3 Converter version is {}".format(gns3converter_version))
|
||||
print("Python version is {}.{}.{} ({}-bit) with {} encoding".format(sys.version_info[0],
|
||||
sys.version_info[1],
|
||||
sys.version_info[2],
|
||||
@@ -189,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)
|
||||
@@ -199,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() == "":
|
||||
@@ -207,19 +208,24 @@ class ConsoleCmd(cmd.Cmd):
|
||||
return
|
||||
|
||||
root = logging.getLogger()
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
|
||||
if len(args) == 1:
|
||||
try:
|
||||
level = int(args[0])
|
||||
if level == 0:
|
||||
print("Deactivating debugging")
|
||||
root.removeHandler(ch)
|
||||
else:
|
||||
level = int(args[0])
|
||||
if level == 0:
|
||||
print("Deactivating debugging")
|
||||
for handler in root.handlers:
|
||||
if isinstance(handler, logging.StreamHandler):
|
||||
root.removeHandler(handler)
|
||||
root.setLevel(logging.INFO)
|
||||
else:
|
||||
root.addHandler(logging.StreamHandler(sys.stdout))
|
||||
if level == 1:
|
||||
print("Activating debugging")
|
||||
root.addHandler(ch)
|
||||
except:
|
||||
print(self.do_debug.__doc__)
|
||||
else:
|
||||
print("Activating full debugging")
|
||||
root.setLevel(logging.DEBUG)
|
||||
from .main_window import MainWindow
|
||||
MainWindow.instance().setSettings({"debug_level": level})
|
||||
else:
|
||||
print(self.do_debug.__doc__)
|
||||
|
||||
@@ -259,6 +265,10 @@ class ConsoleCmd(cmd.Cmd):
|
||||
:param params: list of parameters
|
||||
"""
|
||||
|
||||
if self._topology.project is None:
|
||||
print("Sorry, the project hasn't been saved yet")
|
||||
return
|
||||
|
||||
topology = self._topology.dump()
|
||||
if len(params) == 1:
|
||||
# print out whole topology
|
||||
@@ -286,6 +296,17 @@ class ConsoleCmd(cmd.Cmd):
|
||||
print(json.dumps(node, sort_keys=True, indent=4))
|
||||
break
|
||||
|
||||
def _show_gnsvm(self, params):
|
||||
"""
|
||||
Handles the 'show gns3vm' command.
|
||||
|
||||
:param params: list of parameters
|
||||
"""
|
||||
from gns3.gns3_vm import GNS3VM
|
||||
vm = GNS3VM.instance()
|
||||
print("Running: {}".format(vm.isRunning()))
|
||||
print("Settings: {}".format(vm.settings()))
|
||||
|
||||
def do_show(self, args):
|
||||
"""
|
||||
Show detail information about every device in current lab:
|
||||
@@ -299,6 +320,9 @@ class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
Show topology info of a device:
|
||||
show run <device_name>
|
||||
|
||||
Show the GNS3 VM status
|
||||
show gns3vm
|
||||
"""
|
||||
|
||||
if '?' in args or args.strip() == "":
|
||||
@@ -310,6 +334,8 @@ class ConsoleCmd(cmd.Cmd):
|
||||
self._show_device(params)
|
||||
elif params[0] == "run":
|
||||
self._show_run(params)
|
||||
elif params[0] == "gns3vm":
|
||||
self._show_gnsvm(params)
|
||||
else:
|
||||
print(self.do_show.__doc__)
|
||||
|
||||
|
||||
@@ -19,11 +19,15 @@ import platform
|
||||
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):
|
||||
@@ -36,12 +40,18 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
|
||||
# Set introduction message
|
||||
bitness = struct.calcsize("P") * 8
|
||||
self.intro = "GNS3 management console. Running GNS3 version {} on {} ({}-bit).\n" \
|
||||
"Copyright (c) 2006-2014 GNS3 Technologies.".format(__version__, platform.system(), bitness)
|
||||
current_year = datetime.date.today().year
|
||||
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {}.\n" \
|
||||
"Copyright (c) 2006-{} GNS3 Technologies.\n" \
|
||||
"Use Help -> GNS3 Doctor to detect common issues." \
|
||||
"".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)
|
||||
@@ -69,7 +79,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
For exception handling purposes
|
||||
(see exception hook in the program entry point).
|
||||
"""
|
||||
|
||||
|
||||
return False
|
||||
|
||||
def onKeyPress_Tab(self):
|
||||
@@ -83,7 +93,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:
|
||||
@@ -170,7 +180,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
self.write(text, warning=True)
|
||||
self.write("\n")
|
||||
|
||||
def writeServerError(self, node_id, code, message):
|
||||
def writeServerError(self, node_id, message):
|
||||
"""
|
||||
Write server error messages coming from the server.
|
||||
|
||||
@@ -181,15 +191,14 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
|
||||
node = Topology.instance().getNode(node_id)
|
||||
server = name = ""
|
||||
if node and node.name():
|
||||
name = " {}:".format(node.name())
|
||||
server = "from {}:{}".format(node.server().host,
|
||||
node.server().port)
|
||||
if node:
|
||||
if node.name():
|
||||
name = " {}:".format(node.name())
|
||||
server = "from {}".format(node.server().url())
|
||||
|
||||
text = "Server error [{code}] {server}:{name} {message}".format(code=code,
|
||||
server=server,
|
||||
name=name,
|
||||
message=message)
|
||||
text = "Server error {server}:{name} {message}".format(server=server,
|
||||
name=name,
|
||||
message=message)
|
||||
self.write(text, error=True)
|
||||
self.write("\n")
|
||||
|
||||
@@ -203,12 +212,12 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
self.pointer = 0
|
||||
if len(self.line):
|
||||
self.history.append(self.line)
|
||||
try:
|
||||
self.lines.append(self.line)
|
||||
source = "\n".join(self.lines)
|
||||
self.more = self.onecmd(source)
|
||||
except Exception as e:
|
||||
print("Unknown error: {}".format(e))
|
||||
try:
|
||||
self.lines.append(self.line)
|
||||
source = "\n".join(self.lines)
|
||||
self.more = self.onecmd(source)
|
||||
except Exception as e:
|
||||
print("Unknown error: {}".format(e))
|
||||
|
||||
self.write(self.prompt)
|
||||
self.lines = []
|
||||
|
||||
123
gns3/crash_report.py
Normal file
123
gns3/crash_report.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# -*- 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 psutil
|
||||
import os
|
||||
import platform
|
||||
import struct
|
||||
|
||||
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://03a292b244b941d9abee28365c999972:038f2bb8fc9145468c3428b17e5f1e44@app.getsentry.com/38506"
|
||||
if hasattr(sys, "frozen"):
|
||||
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):
|
||||
# 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 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()),
|
||||
"os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]),
|
||||
"os:linux": " ".join(platform.linux_distribution()),
|
||||
"python:version": "{}.{}.{}".format(sys.version_info[0],
|
||||
sys.version_info[1],
|
||||
sys.version_info[2]),
|
||||
"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 = 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(client.get_ident(report)))
|
||||
|
||||
def _add_qt_information(self, context):
|
||||
try:
|
||||
from .qt import QtCore
|
||||
import sip
|
||||
except ImportError:
|
||||
return context
|
||||
context["psutil:version"] = psutil.__version__
|
||||
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):
|
||||
if cls._instance is None:
|
||||
cls._instance = CrashReport()
|
||||
return cls._instance
|
||||
@@ -15,19 +15,20 @@
|
||||
# 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.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
# dynamically add the current version number
|
||||
|
||||
515
gns3/dialogs/appliance_wizard.py
Normal file
515
gns3/dialogs/appliance_wizard.py
Normal file
@@ -0,0 +1,515 @@
|
||||
#!/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.uiCreateVersionPushButton.clicked.connect(self._createVersionPushButtonClickedSlot)
|
||||
|
||||
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
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 "docker" in self._appliance:
|
||||
type = "docker"
|
||||
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"),
|
||||
("Architecture", "qemu/arch"),
|
||||
("KVM", "qemu/kvm")
|
||||
)
|
||||
|
||||
self.uiInfoTreeWidget.clear()
|
||||
for (name, key) in info:
|
||||
if "/" in key:
|
||||
key, subkey = key.split("/")
|
||||
value = self._appliance.get(key, {}).get(subkey, None)
|
||||
else:
|
||||
value = self._appliance.get(key, None)
|
||||
if value is None:
|
||||
continue
|
||||
item = QtWidgets.QTreeWidgetItem([name + ":", value])
|
||||
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)
|
||||
|
||||
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")):
|
||||
if type == "qemu":
|
||||
# Qemu has issues on OSX and Windows we disallow usage of the local server
|
||||
if not LocalConfig.instance().experimental():
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
elif type != "dynamips":
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
|
||||
if GNS3VM.instance().isRunning():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
elif Servers.instance().localServer().isLocalServerRunning() and self.uiLocalRadioButton.isEnabled():
|
||||
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", ""))
|
||||
)
|
||||
|
||||
elif self.page(page_id) == self.uiCheckServerWizardPage:
|
||||
self.uiCheckServerLabel.setText("Please wait while checking server capacities...")
|
||||
if 'qemu' in self._appliance:
|
||||
if self._appliance['qemu'].get('kvm', 'require') == 'require':
|
||||
self._server_check = False # If the server as the capacities for running the appliance
|
||||
Qemu.instance().getQemuCapabilitiesFromServer(self._server, qpartial(self._qemuServerCapabilitiesCallback))
|
||||
return
|
||||
self.uiCheckServerLabel.setText("")
|
||||
self._server_check = True
|
||||
self.next()
|
||||
|
||||
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
|
||||
"""
|
||||
Check if server support KVM or not
|
||||
"""
|
||||
if error is None and "kvm" in result and self._appliance["qemu"]["arch"] in result["kvm"]:
|
||||
self._server_check = True
|
||||
self.uiCheckServerLabel.setText("GNS3 server requirements is OK you can continue the installation")
|
||||
else:
|
||||
if error:
|
||||
msg = result["message"]
|
||||
else:
|
||||
msg = "The remote server doesn't support KVM. You need a Linux server or the GNS3 VM with VMware and CPU virtualization instructions."
|
||||
self.uiCheckServerLabel.setText(msg)
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu", msg)
|
||||
self._server_check = False
|
||||
|
||||
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 files...", 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.get("filesize", 0)
|
||||
image_widget = QtWidgets.QTreeWidgetItem(
|
||||
[
|
||||
"",
|
||||
image["filename"],
|
||||
human_filesize(image.get("filesize", 0)),
|
||||
image["status"],
|
||||
image["version"],
|
||||
image.get("md5sum", "")
|
||||
])
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
# Docker do not have versions
|
||||
if not "versions" in self._appliance:
|
||||
return
|
||||
|
||||
for version in self._appliance["versions"]:
|
||||
for image in version["images"].values():
|
||||
img = self._registry.search_image_file(image["filename"], image.get("md5sum"), image.get("filesize"))
|
||||
if img:
|
||||
image["status"] = "Found"
|
||||
image["md5sum"] = img.md5sum
|
||||
image["filesize"] = img.filesize
|
||||
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()
|
||||
self.uiExplainDownloadLabel.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()
|
||||
|
||||
if current is None:
|
||||
return
|
||||
|
||||
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 file is compressed with {} you need to uncompress it before using it.".format(data["compression"]))
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "Add appliance", "Download will redirect you where the required file can be downloaded, you may have to be registered with the vendor in order to download the file.")
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["download_url"]))
|
||||
|
||||
def _createVersionPushButtonClickedSlot(self):
|
||||
"""
|
||||
Allow user to create a new version of an appliance
|
||||
"""
|
||||
|
||||
new_version, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Creating a new version allows to import unknown files to use with this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.Normal)
|
||||
if ok:
|
||||
self._appliance.create_new_version(new_version)
|
||||
self._refreshVersions()
|
||||
|
||||
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 "md5sum" in disk and image.md5sum != disk["md5sum"]:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "This is not the correct file. The MD5 sum is {} and should be {}. For OVA you need to import the OVA/OVF not the file inside the archive.".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, ValueError])
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Importing 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
|
||||
|
||||
if version is None:
|
||||
appliance_configuration = self._appliance.copy()
|
||||
else:
|
||||
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()
|
||||
|
||||
if "qemu" in appliance_configuration:
|
||||
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 nextId(self):
|
||||
if self.currentPage() == self.uiServerWizardPage:
|
||||
if "docker" in self._appliance:
|
||||
return super().nextId() + 3
|
||||
elif "qemu" not in self._appliance:
|
||||
return super().nextId() + 1
|
||||
elif self.currentPage() == self.uiFilesWizardPage:
|
||||
if "qemu" not in self._appliance:
|
||||
return super().nextId() + 1
|
||||
return super().nextId()
|
||||
|
||||
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)
|
||||
if not self._appliance.is_version_installable(version["name"]):
|
||||
QtWidgets.QMessageBox.warning(self, "Appliance", "Sorry, you cannot install {} with missing files".format(appliance["name"]))
|
||||
return False
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Would you like to install {} version {}?".format(appliance["name"], version["name"]),
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
|
||||
elif self.currentPage() == self.uiUsageWizardPage:
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
if current:
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
return self._install(version["name"])
|
||||
else:
|
||||
return self._install(None)
|
||||
|
||||
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")):
|
||||
if "qemu" in self._appliance:
|
||||
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
|
||||
|
||||
elif self.currentPage() == self.uiCheckServerWizardPage:
|
||||
return self._server_check
|
||||
|
||||
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()
|
||||
@@ -19,11 +19,13 @@
|
||||
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(QtWidgets.QDialog, Ui_configurationDialog):
|
||||
|
||||
class ConfigurationDialog(QtGui.QDialog, Ui_configurationDialog):
|
||||
"""
|
||||
Configuration dialog implementation.
|
||||
|
||||
@@ -35,13 +37,14 @@ 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)
|
||||
self.setWindowTitle(configuration_page.windowTitle())
|
||||
self.uiConfigStackedWidget.addWidget(configuration_page)
|
||||
self.uiConfigStackedWidget.setCurrentWidget(configuration_page)
|
||||
self.setModal(True)
|
||||
configuration_page.loadSettings(settings)
|
||||
self._settings = settings
|
||||
self._configuration_page = configuration_page
|
||||
@@ -53,12 +56,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)
|
||||
|
||||
135
gns3/dialogs/console_command_dialog.py
Normal file
135
gns3/dialogs/console_command_dialog.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 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 copy
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.ui.console_command_dialog_ui import Ui_uiConsoleCommandDialog
|
||||
from gns3.settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, \
|
||||
PRECONFIGURED_SERIAL_CONSOLE_COMMANDS, \
|
||||
PRECONFIGURED_VNC_CONSOLE_COMMANDS, \
|
||||
CUSTOM_CONSOLE_COMMANDS_SETTINGS
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
|
||||
"""
|
||||
This dialog allow user to select the command used to start a
|
||||
console.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, console_type="telnet", current=None):
|
||||
"""
|
||||
:params console_type: telnet, serial or vnc
|
||||
:params current: Current console command
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self._console_type = console_type
|
||||
self._current = current
|
||||
|
||||
self._settings = LocalConfig.instance().loadSectionSettings("CustomConsoleCommands", CUSTOM_CONSOLE_COMMANDS_SETTINGS)
|
||||
|
||||
self.uiCommandComboBox.currentIndexChanged.connect(self.commandComboBoxCurrentIndexChangedSlot)
|
||||
self.uiCommandPlainTextEdit.textChanged.connect(self.textChangedSlot)
|
||||
self.uiSavePushButton.clicked.connect(self.savePushButtonClickedSlot)
|
||||
self.uiRemovePushButton.clicked.connect(self.removePushButtonClickedSlot)
|
||||
|
||||
self._refreshList()
|
||||
|
||||
def _refreshList(self):
|
||||
if self._console_type == "telnet":
|
||||
self._consoles = copy.copy(PRECONFIGURED_TELNET_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
elif self._console_type == "vnc":
|
||||
self._consoles = copy.copy(PRECONFIGURED_VNC_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
else:
|
||||
self._consoles = copy.copy(PRECONFIGURED_SERIAL_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
|
||||
self.uiCommandComboBox.clear()
|
||||
self.uiCommandComboBox.addItem("Custom", "")
|
||||
for name, cmd in sorted(self._consoles.items(), key=(lambda item: item[0].lower())):
|
||||
self.uiCommandComboBox.addItem(name, cmd)
|
||||
|
||||
if self._current:
|
||||
self.uiCommandPlainTextEdit.setPlainText(self._current)
|
||||
else:
|
||||
self.uiCommandComboBox.setCurrentIndex(1)
|
||||
|
||||
def removePushButtonClickedSlot(self):
|
||||
"""
|
||||
Remove the custom command from the custom list
|
||||
"""
|
||||
self._settings[self._console_type].pop(self.uiCommandComboBox.currentText())
|
||||
LocalConfig.instance().saveSectionSettings("CustomConsoleCommands", self._settings)
|
||||
self._current = None
|
||||
self._refreshList()
|
||||
|
||||
def savePushButtonClickedSlot(self):
|
||||
"""
|
||||
Save a custom command to the list
|
||||
"""
|
||||
name, ok = QtWidgets.QInputDialog.getText(self, "Add a command", "Command name:", QtWidgets.QLineEdit.Normal)
|
||||
command = self.uiCommandPlainTextEdit.toPlainText().strip()
|
||||
if ok and len(command) > 0:
|
||||
if command not in self._consoles.values():
|
||||
self._settings[self._console_type][name] = command
|
||||
self._current = command
|
||||
LocalConfig.instance().saveSectionSettings("CustomConsoleCommands", self._settings)
|
||||
self._refreshList()
|
||||
|
||||
def textChangedSlot(self):
|
||||
index = self.uiCommandComboBox.findData(self.uiCommandPlainTextEdit.toPlainText())
|
||||
if index == -1:
|
||||
index = 0
|
||||
self.uiCommandComboBox.setCurrentIndex(index)
|
||||
|
||||
def commandComboBoxCurrentIndexChangedSlot(self, index):
|
||||
self.uiRemovePushButton.hide()
|
||||
# Ignore custom command
|
||||
if index != 0:
|
||||
self.uiCommandPlainTextEdit.setPlainText(self.uiCommandComboBox.currentData())
|
||||
self.uiSavePushButton.hide()
|
||||
if self.uiCommandComboBox.currentText() in self._settings[self._console_type].keys():
|
||||
self.uiRemovePushButton.show()
|
||||
else:
|
||||
self.uiSavePushButton.show()
|
||||
|
||||
@staticmethod
|
||||
def getCommand(parent, console_type="telnet", current=None):
|
||||
dialog = ConsoleCommandDialog(parent, console_type=console_type, current=current)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
return (True, dialog.uiCommandPlainTextEdit.toPlainText().replace("\n", " "))
|
||||
return (False, None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
(ok, command) = ConsoleCommandDialog.getCommand(main, console_type="telnet", current=list(PRECONFIGURED_TELNET_CONSOLE_COMMANDS.items())[0][1])
|
||||
print(ok)
|
||||
print(command)
|
||||
|
||||
224
gns3/dialogs/doctor_dialog.py
Normal file
224
gns3/dialogs/doctor_dialog.py
Normal file
@@ -0,0 +1,224 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 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 psutil
|
||||
import platform
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import struct
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.doctor_dialog_ui import Ui_DoctorDialog
|
||||
from gns3.servers import Servers
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3 import version
|
||||
from gns3.modules.vmware import VMware
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
"""
|
||||
This dialog allow user to detect error in his GNS3 installation.
|
||||
|
||||
If you want to add a test add a method starting by check. The
|
||||
check return a tuple result and a message in case of failure.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, console=False):
|
||||
|
||||
super().__init__(parent)
|
||||
self._console = console
|
||||
self.setupUi(self)
|
||||
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
|
||||
for method in sorted(dir(self)):
|
||||
if method.startswith('check'):
|
||||
try:
|
||||
self.write(getattr(self, method).__doc__ + "...")
|
||||
(res, msg) = getattr(self, method)()
|
||||
if res == 0:
|
||||
self.write('<span style="color: green"><strong>OK</strong></span>')
|
||||
elif res == 1:
|
||||
self.write('<span style="color: orange"><strong>WARNING</strong> {}</span>'.format(msg))
|
||||
elif res == 2:
|
||||
self.write('<span style="color: red"><strong>ERROR</strong> {}</span>'.format(msg))
|
||||
except Exception as e:
|
||||
log.error("GNS3 doctor exception detected: {}".format(e), exc_info=1)
|
||||
self.write('<span style="color: red"><strong>FAIL</strong> The doctor failed during this test with error: {} Please check on the forum.</span>'.format(str(e)))
|
||||
self.write("<br/>")
|
||||
|
||||
def write(self, text):
|
||||
"""
|
||||
Add text to the text windows
|
||||
"""
|
||||
if self._console:
|
||||
print(text)
|
||||
self.uiDoctorResultTextEdit.setHtml(self.uiDoctorResultTextEdit.toHtml() + text)
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
self.accept()
|
||||
|
||||
def checkLocalServerEnabled(self):
|
||||
"""Checking if the local server is enabled"""
|
||||
if Servers.instance().shouldLocalServerAutoStart() is False:
|
||||
return (2, "The local server is disabled. Go to Preferences -> Server -> Local Server and enable the local server.")
|
||||
return (0, None)
|
||||
|
||||
def checkDevVersionOfGNS3(self):
|
||||
"""Checking for stable GNS3 version"""
|
||||
if version.__version_info__[3] != 0:
|
||||
return (1, "You are using a unstable version of GNS3.")
|
||||
return (0, None)
|
||||
|
||||
def checkExperimentalFeaturesEnabled(self):
|
||||
"""Checking if experimental features are not enabled"""
|
||||
if LocalConfig.instance().experimental():
|
||||
return (1, "Experimental features are enabled. Turn them off by going to Preferences -> General -> Miscellaneous.")
|
||||
return (0, None)
|
||||
|
||||
def checkAVGInstalled(self):
|
||||
"""Checking if AVG software is not installed"""
|
||||
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
psinfo = proc.as_dict(["exe"])
|
||||
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
|
||||
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
return (0, None)
|
||||
|
||||
def checkFreeRam(self):
|
||||
"""Checking for amount of free virtual memory"""
|
||||
|
||||
if int(psutil.virtual_memory().available / (1024 * 1024)) < 600:
|
||||
return (2, "You have less than 600MB of available virtual memory, this could prevent nodes to start")
|
||||
return (0, None)
|
||||
|
||||
def checkVmrun(self):
|
||||
"""Checking if vmrun is installed"""
|
||||
vmrun = VMware.instance().findVmrun()
|
||||
if len(vmrun) == 0:
|
||||
return (1, "The vmrun executable could not be found, VMware VMs cannot be used")
|
||||
return (0, None)
|
||||
|
||||
def check64Bit(self):
|
||||
"""Check if processor is 64 bit"""
|
||||
if platform.architecture()[0] != "64bit":
|
||||
return (2, "The architecture {} is not supported.".format(platform.architecture()[0]))
|
||||
return (0, None)
|
||||
|
||||
def checkUbridgePermission(self):
|
||||
"""Check if ubridge has the correct permission"""
|
||||
if not sys.platform.startswith("win") and os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return (0, None)
|
||||
|
||||
path = Servers.instance().localServerSettings().get("ubridge_path")
|
||||
if path is None:
|
||||
return (0, None)
|
||||
if not os.path.exists(path):
|
||||
return (2, "Ubridge path {path} doesn't exists".format(path=path))
|
||||
|
||||
request_setuid = False
|
||||
if sys.platform.startswith("linux"):
|
||||
if "security.capability" in os.listxattr(path):
|
||||
caps = os.getxattr(path, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
return(2, "Ubridge require CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
|
||||
else:
|
||||
# capabilities not supported
|
||||
request_setuid = True
|
||||
|
||||
if sys.platform.startswith("darwin") or request_setuid:
|
||||
if os.stat(path).st_uid != 0 or not os.stat(path).st_mode & stat.S_ISUID:
|
||||
return (2, "Ubridge should be setuid. Run sudo chown root {path} and sudo chmod 4755 {path}".format(path=path))
|
||||
return (0, None)
|
||||
|
||||
def checkDynamipsPermission(self):
|
||||
"""Check if dynamips has the correct permission"""
|
||||
if not sys.platform.startswith("win") and os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return (0, None)
|
||||
|
||||
path = Servers.instance().localServerSettings().get("dynamips_path")
|
||||
if path is None:
|
||||
return (0, None)
|
||||
if not os.path.exists(path):
|
||||
return (2, "Dynamips path {path} doesn't exists".format(path=path))
|
||||
|
||||
if sys.platform.startswith("linux") and "security.capability" in os.listxattr(path):
|
||||
caps = os.getxattr(path, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
return (2, "Dynamips require CAP_NET_RAW. Run sudo setcap cap_net_raw,cap_net_admin+eip {path}".format(path=path))
|
||||
return (0, None)
|
||||
|
||||
def checkGNS3InstalledTwice(self):
|
||||
"""Check if gns3 is not installed twice"""
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
return (0, None)
|
||||
|
||||
try:
|
||||
if os.path.exists("/usr/local/bin/gns3server") and os.path.exists("/usr/bin/gns3server"):
|
||||
return (2, "GNS3 is installed twice please remove it from /usr/local/bin")
|
||||
except OSError:
|
||||
pass
|
||||
return (0, None)
|
||||
|
||||
def _checkWindowsService(self, service_name):
|
||||
|
||||
import pywintypes
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
try:
|
||||
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
|
||||
return False
|
||||
except pywintypes.error as e:
|
||||
if e.winerror == 1060:
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
return True
|
||||
|
||||
def checkRPFServiceIsRunning(self):
|
||||
"""Check if the RPF service is running (required to use Ethernet NIOs)"""
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
return (0, None)
|
||||
|
||||
import pywintypes
|
||||
try:
|
||||
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
|
||||
return (2, "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot")
|
||||
except pywintypes.error as e:
|
||||
return (2, "Could not check if the NPF or Npcap service is running: {}".format(e.strerror))
|
||||
|
||||
return (0, None)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = DoctorDialog(main, console=True)
|
||||
#dialog.show()
|
||||
#exit_code = app.exec_()
|
||||
@@ -15,18 +15,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/>.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, command, params):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.setWindowTitle("Executing {}".format(command))
|
||||
@@ -56,4 +57,4 @@ class ExecCommandDialog(QtGui.QDialog, Ui_ExecCommandDialog):
|
||||
|
||||
self._process.kill()
|
||||
self._process.waitForFinished()
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
122
gns3/dialogs/export_debug_dialog.py
Normal file
122
gns3/dialogs/export_debug_dialog.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# -*- 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}
|
||||
PyQt: {pyqt}
|
||||
CPU: {cpu}
|
||||
Memory: {memory}
|
||||
|
||||
Networks:
|
||||
{addrs}
|
||||
|
||||
Open connections:
|
||||
{connections}
|
||||
|
||||
Processus:
|
||||
""".format(
|
||||
version=__version__,
|
||||
qt=QtCore.QT_VERSION_STR,
|
||||
pyqt=QtCore.PYQT_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())
|
||||
63
gns3/dialogs/file_editor_dialog.py
Normal file
63
gns3/dialogs/file_editor_dialog.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 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
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.file_editor_dialog_ui import Ui_FileEditorDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
|
||||
"""
|
||||
This dialog allow user to detect error in his GNS3 installation.
|
||||
|
||||
If you want to add a test add a method starting by check. The
|
||||
check return a tuple result and a message in case of failure.
|
||||
"""
|
||||
|
||||
def __init__(self, node, path, parent=None):
|
||||
|
||||
if parent is None:
|
||||
from gns3.main_window import MainWindow
|
||||
parent = MainWindow.instance()
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._node = node
|
||||
self._path = path
|
||||
|
||||
self.setWindowTitle(node.name() + " " + os.path.basename(path))
|
||||
|
||||
self.uiRefreshButton.pressed.connect(self._refreshSlot)
|
||||
self.accepted.connect(self._acceptedCallback)
|
||||
|
||||
self._refreshSlot()
|
||||
|
||||
def _acceptedCallback(self):
|
||||
text = self.uiFileTextEdit.toPlainText()
|
||||
self._node.httpPost("/files" + self._path, None, body=text)
|
||||
|
||||
def _refreshSlot(self):
|
||||
self._node.httpGet("/files" + self._path, self._getCallback)
|
||||
|
||||
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
|
||||
if not error:
|
||||
self.uiFileTextEdit.setText(raw_body)
|
||||
@@ -1,102 +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
|
||||
import pkg_resources
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWebKit
|
||||
from ..ui.getting_started_dialog_ui import Ui_GettingStartedDialog
|
||||
|
||||
|
||||
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.uiWebView.loadFinished.connect(self._loadFinishedSlot)
|
||||
self.uiCheckBox.setChecked(QtCore.QSettings().value("GUI/hide_getting_started_dialog", False, type=bool))
|
||||
self._timer = QtCore.QTimer(self)
|
||||
self._timer.timeout.connect(self._loadFinishedSlot)
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.start(5000)
|
||||
self.uiWebView.load(QtCore.QUrl("http://start.gns3.net"))
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
QtCore.QSettings().setValue("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))
|
||||
|
||||
def _loadFinishedSlot(self, result=False):
|
||||
"""
|
||||
Slot called when the web page has been loaded.
|
||||
|
||||
:param result: boolean
|
||||
"""
|
||||
|
||||
self.uiWebView.loadFinished.disconnect(self._loadFinishedSlot)
|
||||
self._timer.stop()
|
||||
self._timer.timeout.disconnect()
|
||||
if result is False:
|
||||
# load a local resource if the page is not available
|
||||
resource_name = os.path.join("static", "getting_started.html")
|
||||
getting_started = None
|
||||
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
|
||||
getting_started = os.path.normpath(resource_name)
|
||||
elif pkg_resources.resource_exists("gns3", resource_name):
|
||||
getting_started_page = pkg_resources.resource_filename("gns3", resource_name)
|
||||
getting_started = os.path.normpath(getting_started_page)
|
||||
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("file://{}".format(getting_started)))
|
||||
else:
|
||||
self.uiCheckBox.setChecked(True)
|
||||
self.accept()
|
||||
@@ -15,24 +15,26 @@
|
||||
# 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.
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -51,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):
|
||||
"""
|
||||
@@ -63,17 +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, "idlepcs") and node.settings()["image"] == ios_image:
|
||||
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,5 +89,4 @@ class IdlePCDialog(QtGui.QDialog, Ui_IdlePCDialog):
|
||||
|
||||
if result:
|
||||
self._applySlot()
|
||||
QtGui.QDialog.done(self, result)
|
||||
|
||||
super().done(result)
|
||||
|
||||
@@ -1,68 +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(
|
||||
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(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()
|
||||
97
gns3/dialogs/new_appliance_dialog.py
Normal file
97
gns3/dialogs/new_appliance_dialog.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 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 gns3.qt import QtWidgets, QtCore
|
||||
from gns3.ui.new_appliance_dialog_ui import Ui_NewApplianceDialog
|
||||
from gns3.dialogs.preferences_dialog import PreferencesDialog
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NewApplianceDialog(QtWidgets.QDialog, Ui_NewApplianceDialog):
|
||||
"""
|
||||
This dialog allow user to create a new appliance by opening
|
||||
the correct creation dialog
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiImportApplianceTemplatePushButton.clicked.connect(self._importApplianceTemplatePushButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpButtonClickedSlot)
|
||||
|
||||
def _importApplianceTemplatePushButtonClickedSlot(self):
|
||||
|
||||
self.accept()
|
||||
from gns3.main_window import MainWindow
|
||||
MainWindow.instance().openApplianceActionSlot()
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
|
||||
self.accept()
|
||||
dialog = PreferencesDialog(self.parent())
|
||||
if self.uiAddIOSRouterRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Dynamips").uiNewIOSRouterPushButton.clicked.emit(False)
|
||||
elif self.uiAddIOUDeviceRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "IOS on UNIX").uiNewIOUDevicePushButton.clicked.emit(False)
|
||||
elif self.uiAddQemuVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "QEMU").uiNewQemuVMPushButton.clicked.emit(False)
|
||||
elif self.uiAddVirtualBoxVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "VirtualBox").uiNewVirtualBoxVMPushButton.clicked.emit(False)
|
||||
elif self.uiAddVMwareVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "VMware").uiNewVMwareVMPushButton.clicked.emit(False)
|
||||
elif self.uiAddDockerVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Docker").uiNewDockerVMPushButton.clicked.emit(False)
|
||||
else:
|
||||
return
|
||||
dialog.exec_()
|
||||
|
||||
def _helpButtonClickedSlot(self):
|
||||
|
||||
help_text = """<html><p>This dialog helps you to add an appliance template in GNS3. In all cases you must provide your own images.</p>
|
||||
<p>You can download appliance template files (.gns3appliance) from <a href="https://gns3.com/marketplace/appliances">the GNS3 website</a></p>
|
||||
<p>A template file provides community tested settings to run a specific appliance in GNS3.</p></html>
|
||||
"""
|
||||
QtWidgets.QMessageBox.information(self, "Help for adding a new appliance template", help_text)
|
||||
|
||||
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()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = NewApplianceDialog(main, console=True)
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
@@ -16,29 +16,28 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import shutil
|
||||
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.
|
||||
|
||||
:param parent: parent widget.
|
||||
:param showed_from_startup: boolean to indicate if this dialog
|
||||
:param default_project_name: Project name by default
|
||||
has been opened automatically when GNS3 started.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, showed_from_startup=False):
|
||||
def __init__(self, parent, showed_from_startup=False, default_project_name="untitled"):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = parent
|
||||
self._project_settings = parent.projectSettings().copy()
|
||||
default_project_name = "untitled"
|
||||
self._project_settings = {}
|
||||
self.uiNameLineEdit.setText(default_project_name)
|
||||
self.uiLocationLineEdit.setText(os.path.join(self._main_window.projectsDirPath(), default_project_name))
|
||||
|
||||
@@ -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,34 +113,28 @@ 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
|
||||
self._project_settings["project_path"] = os.path.join(project_location, project_name + ".gns3")
|
||||
self._project_settings["project_files_dir"] = os.path.join(project_location, project_name + "-files")
|
||||
self._project_settings["project_files_dir"] = project_location
|
||||
self._project_settings["project_type"] = project_type
|
||||
|
||||
# delete all the project files
|
||||
shutil.rmtree(self._project_settings["project_files_dir"], ignore_errors=True)
|
||||
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
93
gns3/dialogs/new_server_dialog.py
Normal file
93
gns3/dialogs/new_server_dialog.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# -*- 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 re
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.new_server_dialog_ui import Ui_NewServerDialog
|
||||
from gns3.servers import Servers
|
||||
|
||||
|
||||
class NewServerDialog(QtWidgets.QDialog, Ui_NewServerDialog):
|
||||
|
||||
"""
|
||||
New server dialog.
|
||||
|
||||
:param parent: parent widget.
|
||||
has been opened automatically when GNS3 started.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiEnableAuthenticationCheckBox.stateChanged.connect(self._enableAuthenticationSlot)
|
||||
|
||||
def _enableAuthenticationSlot(self, state):
|
||||
"""
|
||||
Slot to enable or not the authentication.
|
||||
"""
|
||||
|
||||
if state:
|
||||
self.uiServerUserLineEdit.setEnabled(True)
|
||||
self.uiServerPasswordLineEdit.setEnabled(True)
|
||||
else:
|
||||
self.uiServerUserLineEdit.setEnabled(False)
|
||||
self.uiServerPasswordLineEdit.setEnabled(False)
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
Adds a new remote server.
|
||||
"""
|
||||
|
||||
protocol = self.uiServerProtocolComboBox.currentText().lower()
|
||||
host = self.uiServerHostLineEdit.text().strip()
|
||||
port = self.uiServerPortSpinBox.value()
|
||||
if self.uiEnableAuthenticationCheckBox.isChecked():
|
||||
user = self.uiServerUserLineEdit.text().strip()
|
||||
password = self.uiServerPasswordLineEdit.text().strip()
|
||||
else:
|
||||
user = password = ""
|
||||
|
||||
if not re.match(r"^[a-zA-Z0-9\.{}-]+$".format("\u0370-\u1CDF\u2C00-\u30FF\u4E00-\u9FBF"), host):
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "Invalid remote server hostname {}".format(host))
|
||||
return
|
||||
if port is None or port < 1:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "Invalid remote server port {}".format(port))
|
||||
return
|
||||
|
||||
servers = Servers.instance()
|
||||
remote_servers = servers.remoteServers()
|
||||
|
||||
# check if the remote server is already defined
|
||||
for server in remote_servers.values():
|
||||
if server.protocol() == protocol and server.host() == host and server.port() == port and server.user() == user:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "Remote server is already defined.")
|
||||
return
|
||||
|
||||
servers.getRemoteServer(protocol, host, port, user, settings={"password": password})
|
||||
servers.save()
|
||||
super().accept()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = NewServerDialog(main)
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
@@ -19,13 +19,14 @@
|
||||
Dialog to configure and update node settings using widget pages.
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -33,40 +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)
|
||||
|
||||
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 not parent in self._parent_items:
|
||||
item = QtGui.QTreeWidgetItem(self.uiNodesTreeWidget, [group_name])
|
||||
if parent not in self._parent_items:
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiNodesTreeWidget, [group_name])
|
||||
item.setIcon(0, QtGui.QIcon(node_item.node().defaultSymbol()))
|
||||
item.setExpanded(True)
|
||||
self._parent_items[parent] = item
|
||||
@@ -76,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.
|
||||
@@ -117,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):
|
||||
"""
|
||||
@@ -131,15 +144,15 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
"""
|
||||
|
||||
try:
|
||||
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):
|
||||
QtGui.QDialog.reject(self)
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
|
||||
QtWidgets.QDialog.reject(self)
|
||||
else:
|
||||
self.applySettings()
|
||||
QtGui.QDialog.accept(self)
|
||||
QtWidgets.QDialog.accept(self)
|
||||
except ConfigurationError:
|
||||
pass
|
||||
|
||||
@@ -165,7 +178,7 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
page.saveSettings(settings, node, group=True)
|
||||
for index in range(0, item.childCount()):
|
||||
child = item.child(index)
|
||||
#child.node().update(settings) #TODO: delete
|
||||
# child.node().update(settings) #TODO: delete
|
||||
child.settings().update(settings)
|
||||
|
||||
# update the nodes with the settings
|
||||
@@ -200,7 +213,8 @@ 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.
|
||||
Store temporary node settings configured in a page widget.
|
||||
@@ -212,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()
|
||||
@@ -233,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
|
||||
"""
|
||||
@@ -269,10 +283,11 @@ class ConfigurationPageItem(QtGui.QTreeWidgetItem):
|
||||
|
||||
|
||||
class ConfigurationError(Exception):
|
||||
|
||||
"""
|
||||
Exception to be raised when a configuration error occurs.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
Exception.__init__(self)
|
||||
super().__init__()
|
||||
@@ -19,17 +19,16 @@
|
||||
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
|
||||
|
||||
|
||||
class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
|
||||
"""
|
||||
Preferences dialog implementation.
|
||||
|
||||
@@ -38,17 +37,40 @@ 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
|
||||
# We need to manually do that otherwise on small screen the windows
|
||||
# could be bigger than the screen instead of displaying scrollbars
|
||||
height = QtWidgets.QDesktopWidget().screenGeometry().height() - 100
|
||||
width = QtWidgets.QDesktopWidget().screenGeometry().width() - 100
|
||||
|
||||
# 980 is the default width
|
||||
if self.width() > width:
|
||||
self.resize(width, self.height())
|
||||
# 680 is the default height
|
||||
if self.height() > height:
|
||||
self.resize(self.width(), 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])
|
||||
|
||||
# set the maximum width based on the content of column 0
|
||||
self.uiTreeWidget.setMaximumWidth(self.uiTreeWidget.sizeHintForColumn(0) + 10)
|
||||
|
||||
# Something has change?
|
||||
self._modified = False
|
||||
|
||||
def _loadPreferencePages(self):
|
||||
"""
|
||||
Loads all preference pages (built-ins and from modules).
|
||||
@@ -60,18 +82,17 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
ServerPreferencesPage,
|
||||
PacketCapturePreferencesPage,
|
||||
]
|
||||
if ENABLE_CLOUD:
|
||||
pages.append(CloudPreferencesPage)
|
||||
|
||||
for page in pages:
|
||||
preferences_page = page()
|
||||
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:
|
||||
@@ -81,17 +102,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.
|
||||
@@ -104,11 +151,15 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
current = previous
|
||||
|
||||
preferences_page = current.data(0, QtCore.Qt.UserRole)
|
||||
name = preferences_page.windowTitle()
|
||||
self.uiTitleLabel.setText("{} preferences".format(name))
|
||||
accessible_name = preferences_page.accessibleName()
|
||||
if accessible_name:
|
||||
self.uiTitleLabel.setText(accessible_name)
|
||||
else:
|
||||
name = preferences_page.windowTitle()
|
||||
self.uiTitleLabel.setText("{} preferences".format(name))
|
||||
index = self.uiStackedWidget.indexOf(preferences_page)
|
||||
widget = self.uiStackedWidget.widget(index)
|
||||
self.uiStackedWidget.setMinimumSize(widget.size())
|
||||
#self.uiStackedWidget.setMinimumSize(widget.size())
|
||||
self.uiStackedWidget.resize(widget.size())
|
||||
self.uiStackedWidget.setCurrentIndex(index)
|
||||
|
||||
@@ -124,6 +175,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):
|
||||
@@ -131,7 +185,15 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
Closes this dialog.
|
||||
"""
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -139,9 +201,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():
|
||||
QtGui.QDialog.accept(self)
|
||||
QtWidgets.QDialog.accept(self)
|
||||
|
||||
291
gns3/dialogs/setup_wizard.py
Normal file
291
gns3/dialogs/setup_wizard.py
Normal file
@@ -0,0 +1,291 @@
|
||||
# -*- 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
|
||||
from ..version import __version__
|
||||
|
||||
|
||||
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.uiGNS3VMDownloadLinkUrlLabel.setText('')
|
||||
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.
|
||||
"""
|
||||
|
||||
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{version}.zip".format(version=__version__)
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('The GNS3 VM can <a href="{download_url}">downloaded here</a>.<br>Import the VM in your virtualization software and hit refresh.'.format(download_url=download_url))
|
||||
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. You can download it from https://www.vmware.com/support/developer/vix-api/")
|
||||
return
|
||||
self._refreshVMListSlot()
|
||||
|
||||
def _listVirtualBoxVMsSlot(self):
|
||||
"""
|
||||
Slot to refresh the VirtualBox VMs list.
|
||||
"""
|
||||
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM on VirtualBox", "VirtualBox doesn't support nested virtualization, this means running Qemu based VM could be very slow")
|
||||
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format(version=__version__)
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('If you don\'t have the GNS3 Virtual Machine you can <a href="{download_url}">download it here</a>.<br>And import the VM in the virtualization software and hit refresh.'.format(download_url=download_url))
|
||||
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 _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:
|
||||
# limit the number of vCPUs to the number of physical cores (hyper thread CPUs are excluded)
|
||||
# because this is likely to degrade performances.
|
||||
cpu_count = psutil.cpu_count(logical=False)
|
||||
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, delay=5)
|
||||
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)
|
||||
if self.uiAddDockerVMCheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "Docker").uiNewDockerVMPushButton.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,16 @@ 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.
|
||||
|
||||
@@ -41,11 +42,11 @@ 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
|
||||
self._project_files_dir = project_files_dir
|
||||
self._project_files_dir = os.path.join(project_files_dir, "project-files")
|
||||
|
||||
self.uiCreatePushButton.clicked.connect(self._createSnapshotSlot)
|
||||
self.uiDeletePushButton.clicked.connect(self._deleteSnapshotSlot)
|
||||
@@ -63,17 +64,22 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
if not os.path.isdir(snapshot_dir):
|
||||
return
|
||||
|
||||
snapshots = []
|
||||
for snapshot in os.listdir(snapshot_dir):
|
||||
match = re.search(r"^(.*)_([0-9]+)_([0-9]+)", snapshot)
|
||||
if match:
|
||||
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.setText("{} on {} at {}".format(snapshot_name, snapshot_date, snapshot_time))
|
||||
item.setData(QtCore.Qt.UserRole, os.path.join(snapshot_dir, snapshot))
|
||||
snapshots.append((snapshot_name, snapshot_date, snapshot_time))
|
||||
|
||||
# Sort by date
|
||||
snapshots = sorted(snapshots, key=(lambda v: v[1] + v[2]))
|
||||
for snapshot_name, snapshot_date, snapshot_time in snapshots:
|
||||
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))
|
||||
|
||||
self.uiSnapshotsList.sortItems(QtCore.Qt.AscendingOrder)
|
||||
|
||||
if self.uiSnapshotsList.count():
|
||||
self.uiSnapshotsList.setCurrentRow(0)
|
||||
@@ -88,15 +94,14 @@ 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"])
|
||||
thread.deleteLater()
|
||||
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_()
|
||||
self._listSnaphosts()
|
||||
@@ -134,9 +139,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
|
||||
@@ -145,15 +150,37 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
if hasattr(node, "start") and node.status() == Node.started:
|
||||
node.stop()
|
||||
|
||||
#FIXME: problably a bug when restoring a snapshot and the project name has changed.
|
||||
thread = ProcessFilesThread(snapshot_path, os.path.dirname(self._project_path), skip_dirs=["snapshots"])
|
||||
thread.deleteLater()
|
||||
progress_dialog = ProgressDialog(thread, "Restoring snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
project_name, _ = os.path.splitext(os.path.basename(self._project_path))
|
||||
legacy_project_files_dir = os.path.join(snapshot_path, "{}-files".format(project_name))
|
||||
if os.path.exists(legacy_project_files_dir):
|
||||
# support for pre 1.3 snapshots
|
||||
for root, dirs, _ in os.walk(self._project_files_dir):
|
||||
dirs[:] = [d for d in dirs if d not in "snapshots"]
|
||||
for project_subdir in dirs:
|
||||
project_subdir_path = os.path.join(root, project_subdir)
|
||||
shutil.rmtree(project_subdir_path, ignore_errors=True)
|
||||
|
||||
dirs = os.listdir(legacy_project_files_dir)
|
||||
for snapshot_subdir in dirs:
|
||||
snapshot_subdir_path = os.path.join(legacy_project_files_dir, snapshot_subdir)
|
||||
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_()
|
||||
|
||||
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:
|
||||
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_()
|
||||
|
||||
from ..main_window import MainWindow
|
||||
MainWindow.instance().loadProject(self._project_path)
|
||||
MainWindow.instance().loadSnapshot(self._project_path)
|
||||
self.accept()
|
||||
|
||||
def _snapshotDoubleClickedSlot(self, item):
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
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.
|
||||
|
||||
@@ -33,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)
|
||||
@@ -73,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(),
|
||||
@@ -86,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(),
|
||||
@@ -117,4 +118,4 @@ class StyleEditorDialog(QtGui.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
if result:
|
||||
self._applyPreferencesSlot()
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
@@ -16,15 +16,22 @@
|
||||
# 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 QtCore, QtGui, QtWidgets
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
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.
|
||||
|
||||
@@ -32,69 +39,158 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
|
||||
:param items: list of items
|
||||
"""
|
||||
|
||||
def __init__(self, parent, items=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"]
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for name, category in categories.items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
else:
|
||||
self.uiCategoryLabel.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
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)
|
||||
item.setIcon(QtGui.QIcon(':/symbols/' + symbol))
|
||||
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
|
||||
if os.path.exists(os.path.join(self._symbols_path, symbol)):
|
||||
svg_renderer = QImageSvgRenderer(os.path.join(self._symbols_path, symbol))
|
||||
else:
|
||||
resource_path = ":/symbols/" + symbol
|
||||
svg_renderer = QImageSvgRenderer(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)
|
||||
renderer = QImageSvgRenderer(symbol_path)
|
||||
renderer.setObjectName(symbol_path)
|
||||
if renderer.isValid():
|
||||
item.setSharedRenderer(renderer)
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Custom pixmap symbol", "Invalid image")
|
||||
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 *.gif);;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):
|
||||
"""
|
||||
@@ -103,6 +199,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,11 +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.
|
||||
|
||||
@@ -33,32 +33,49 @@ 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)
|
||||
|
||||
if len(self._items) == 1:
|
||||
self.uiApplyColorToAllItemsCheckBox.setChecked(True)
|
||||
self.uiApplyColorToAllItemsCheckBox.hide()
|
||||
self.uiApplyRotationToAllItemsCheckBox.setChecked(True)
|
||||
self.uiApplyRotationToAllItemsCheckBox.hide()
|
||||
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)
|
||||
|
||||
@@ -67,11 +84,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):
|
||||
"""
|
||||
@@ -79,10 +94,12 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
"""
|
||||
|
||||
for item in self._items:
|
||||
item.setDefaultTextColor(self._color)
|
||||
item.setFont(self.uiPlainTextEdit.font())
|
||||
item.setRotation(self.uiRotationSpinBox.value())
|
||||
if item.editable():
|
||||
if self.uiApplyColorToAllItemsCheckBox.isChecked():
|
||||
item.setDefaultTextColor(self._color)
|
||||
if self.uiApplyRotationToAllItemsCheckBox.isChecked():
|
||||
item.setRotation(self.uiRotationSpinBox.value())
|
||||
if item.editable() and self.uiApplyTextToAllItemsCheckBox.isChecked():
|
||||
item.setPlainText(self.uiPlainTextEdit.toPlainText())
|
||||
|
||||
def done(self, result):
|
||||
@@ -94,4 +111,4 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
|
||||
if result:
|
||||
self._applyPreferencesSlot()
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
194
gns3/dialogs/vm_with_images_wizard.py
Normal file
194
gns3/dialogs/vm_with_images_wizard.py
Normal file
@@ -0,0 +1,194 @@
|
||||
#!/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
|
||||
|
||||
# Wizard is closed
|
||||
if self.currentPage() is None:
|
||||
return
|
||||
|
||||
if len(result) == 0:
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
if radio_button.isChecked() and self._widgetOnCurrentPage(radio_button):
|
||||
for button in radio_button.parent().findChildren(QtWidgets.QRadioButton):
|
||||
if button != radio_button:
|
||||
button.setChecked(True)
|
||||
button.hide()
|
||||
else:
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
if self._widgetOnCurrentPage(radio_button):
|
||||
for button in radio_button.parent().findChildren(QtWidgets.QRadioButton):
|
||||
if button == radio_button:
|
||||
button.setChecked(True)
|
||||
button.show()
|
||||
|
||||
for combo_box in self._images_combo_boxes:
|
||||
if self._widgetOnCurrentPage(combo_box):
|
||||
combo_box.clear()
|
||||
for vm in result:
|
||||
combo_box.addItem(vm["path"], vm)
|
||||
|
||||
|
||||
def _widgetOnCurrentPage(self, widget):
|
||||
"""
|
||||
:returns Boolean True if widget is current active Wizard page
|
||||
"""
|
||||
return self.currentPage().findChild(widget.__class__, widget.objectName()) is not None
|
||||
|
||||
153
gns3/dialogs/vm_wizard.py
Normal file
153
gns3/dialogs/vm_wizard.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# -*- 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)
|
||||
|
||||
# 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.uiRemoteServersComboBox.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()
|
||||
|
||||
if len(Servers.instance().remoteServers().values()) == 0:
|
||||
self.uiRemoteRadioButton.setEnabled(False)
|
||||
else:
|
||||
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.isEnabled():
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
else:
|
||||
if self.uiRemoteRadioButton.isEnabled():
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the server.
|
||||
"""
|
||||
|
||||
if hasattr(self, "uiNameWizardPage") and self.currentPage() == self.uiNameWizardPage:
|
||||
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
|
||||
325
gns3/gns3_vm.py
Normal file
325
gns3/gns3_vm.py
Normal file
@@ -0,0 +1,325 @@
|
||||
# -*- 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 os
|
||||
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
|
||||
# The current running vboxmanage and vmrun process
|
||||
self._running_process = None
|
||||
|
||||
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)
|
||||
|
||||
def killRunningProcess(self):
|
||||
"""
|
||||
Kill the VBoxManage or vmrun process if running
|
||||
"""
|
||||
|
||||
if self._running_process is not None:
|
||||
self._running_process.kill()
|
||||
self._running_process.wait()
|
||||
self._running_process = None
|
||||
|
||||
def _process_check_output(self, command, timeout=None):
|
||||
# Original code from Python's subprocess.check_output
|
||||
# https://github.com/python/cpython/blob/3.4/Lib/subprocess.py
|
||||
with subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=os.environ) as process:
|
||||
self._running_process = process
|
||||
try:
|
||||
output, unused_err = process.communicate(None, timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
output, unused_err = process.communicate()
|
||||
self._running_process = None
|
||||
raise subprocess.TimeoutExpired(process.args, timeout, output=output)
|
||||
except:
|
||||
self.killRunningProcess()
|
||||
raise
|
||||
retcode = process.poll()
|
||||
if retcode:
|
||||
self._running_process = None
|
||||
raise subprocess.CalledProcessError(retcode, process.args, output=output)
|
||||
self._running_process = None
|
||||
return output.decode("utf-8", errors="ignore").strip()
|
||||
|
||||
def execute_vmrun(self, 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))
|
||||
return self._process_check_output(command, timeout=timeout)
|
||||
|
||||
def execute_vboxmanage(self, 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))
|
||||
return self._process_check_output(command, timeout=timeout)
|
||||
|
||||
@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 isRemote(self):
|
||||
"""
|
||||
Checks if the GNS3 VM is remote.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
vm_settings = Servers.instance().vmSettings()
|
||||
if vm_settings["virtualization"] == "remote":
|
||||
return True
|
||||
return False
|
||||
|
||||
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 as e:
|
||||
log.error("Could not execute VBoxManage: {}".format(e), True)
|
||||
return False
|
||||
except subprocess.SubprocessError as e:
|
||||
log.error("Could not execute VBoxManage: {} with output '{}'".format(e, e.output.decode("utf-8", errors="ignore").strip()), 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":
|
||||
if vm_settings["vmx_path"] is None:
|
||||
log.error("No vm path configured, can't stop the VM")
|
||||
return
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
768
gns3/http_client.py
Normal file
768
gns3/http_client.py
Normal file
@@ -0,0 +1,768 @@
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
import json
|
||||
import http
|
||||
import copy
|
||||
import ipaddress
|
||||
import uuid
|
||||
import urllib.request
|
||||
import pathlib
|
||||
import base64
|
||||
|
||||
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 settings: Dictionnary with connection information to the server
|
||||
:param network_manager: A QT network manager
|
||||
"""
|
||||
|
||||
_instance_count = 1
|
||||
|
||||
# Callback class used for displaying progress
|
||||
_progress_callback = None
|
||||
|
||||
connection_connected_signal = QtCore.Signal()
|
||||
connection_closed_signal = QtCore.Signal()
|
||||
system_usage_updated_signal = QtCore.Signal()
|
||||
connection_error_signal = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, settings, network_manager):
|
||||
|
||||
super().__init__()
|
||||
self._version = ""
|
||||
|
||||
self._scheme = settings.get("protocol", "http")
|
||||
self._host = settings["host"]
|
||||
if "http_host" in settings:
|
||||
self._http_host = settings["http_host"]
|
||||
else:
|
||||
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._accept_insecure_certificate = settings.get("accept_insecure_certificate", None)
|
||||
self._usage = 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 settings(self):
|
||||
"""
|
||||
Return a dictionnary with server settings
|
||||
"""
|
||||
settings = {"protocol": self.protocol(),
|
||||
"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 password(self):
|
||||
return self._password
|
||||
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
Called when a query is over
|
||||
"""
|
||||
|
||||
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):
|
||||
"""
|
||||
:param progress_callback: A progress callback instance
|
||||
"""
|
||||
|
||||
cls._progress_callback = progress_callback
|
||||
|
||||
@staticmethod
|
||||
def reset():
|
||||
"""Reset HTTP client internal variables"""
|
||||
|
||||
HTTPClient._instance_count = 0
|
||||
|
||||
def url(self):
|
||||
"""Returns current server url"""
|
||||
|
||||
return getNetworkUrl(self.protocol(), self.host(), self.port(), self.user(), self.settings())
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
Returns this HTTP Client identifier.
|
||||
:returns: HTTP client identifier (string)
|
||||
"""
|
||||
|
||||
return self._id
|
||||
|
||||
def setLocal(self, value):
|
||||
"""
|
||||
Sets either this is a connection to a local server or not.
|
||||
:param value: boolean
|
||||
"""
|
||||
|
||||
self._local = value
|
||||
|
||||
def isLocal(self):
|
||||
"""
|
||||
Returns either this is a connection to a local server or not.
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
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.
|
||||
:returns: True or False
|
||||
"""
|
||||
|
||||
return self._connected
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the connection with the server.
|
||||
"""
|
||||
log.info("Connection to %s closed", self.url())
|
||||
self._connected = False
|
||||
self.connection_closed_signal.emit()
|
||||
|
||||
def isLocalServerRunning(self):
|
||||
"""
|
||||
Synchronous check if a server is already running on this host.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
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 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)
|
||||
|
||||
if self._user is not None and len(self._user) > 0:
|
||||
log.debug("Synchronous get {} with user '{}'".format(url, self._user))
|
||||
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)
|
||||
else:
|
||||
log.debug("Synchronous get {} (no authentication)".format(url))
|
||||
|
||||
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
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
return self.createHTTPQuery("GET", path, callback, **kwargs)
|
||||
|
||||
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
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
self.createHTTPQuery("PUT", path, callback, **kwargs)
|
||||
|
||||
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
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
self.createHTTPQuery("POST", path, callback, **kwargs)
|
||||
|
||||
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
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
self.createHTTPQuery("DELETE", path, callback, **kwargs)
|
||||
|
||||
def _request(self, url):
|
||||
"""
|
||||
Get a QNetworkRequest object. You can mock this
|
||||
if you want low level mocking.
|
||||
|
||||
:param url: Url of remote ressource (QtCore.QUrl)
|
||||
:returns: QT Network request (QtNetwork.QNetworkRequest)
|
||||
"""
|
||||
|
||||
return QtNetwork.QNetworkRequest(url)
|
||||
|
||||
def _connect(self, query):
|
||||
"""
|
||||
Initialize the connection
|
||||
|
||||
:param query: The query to execute when all network stack is ready
|
||||
"""
|
||||
self.executeHTTPQuery("GET", "/version", query, {}, timeout=5)
|
||||
|
||||
def createHTTPQuery(self, method, path, callback, body={}, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, timeout=120, **kwargs):
|
||||
"""
|
||||
Call the remote server, if not connected, check connection before
|
||||
|
||||
:param method: HTTP method
|
||||
:param path: Remote path
|
||||
: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:
|
||||
return self.executeHTTPQuery(method, path, qpartial(callback), body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText, timeout=timeout)
|
||||
else:
|
||||
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, timeout=timeout)
|
||||
self._connect(query)
|
||||
|
||||
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 or pathlib.Path)
|
||||
:param original_context: Original context
|
||||
:param callback: callback method to call when the server replies
|
||||
"""
|
||||
|
||||
if error is not False:
|
||||
self._connectionError(callback)
|
||||
return
|
||||
|
||||
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._connected = True
|
||||
self.connection_connected_signal.emit()
|
||||
kwargs["context"] = original_context
|
||||
self.executeHTTPQuery(method, path, callback, body, **kwargs)
|
||||
self._version = params["version"]
|
||||
|
||||
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
|
||||
elif isinstance(body, str):
|
||||
request.setRawHeader(b"Content-Type", b"application/octet-stream")
|
||||
data = QtCore.QByteArray(body.encode())
|
||||
body = QtCore.QBuffer(self)
|
||||
body.setData(data)
|
||||
body.open(QtCore.QIODevice.ReadOnly)
|
||||
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, timeout=120, **kwargs):
|
||||
"""
|
||||
Call the remote server
|
||||
|
||||
:param method: HTTP method
|
||||
:param path: Remote path
|
||||
: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)
|
||||
:param timeout: Delay in seconds before raising a timeout
|
||||
:returns: QNetworkReply
|
||||
"""
|
||||
|
||||
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())
|
||||
|
||||
response.finished.connect(qpartial(self._processResponse, response, callback, context, body, ignoreErrors))
|
||||
|
||||
if downloadProgressCallback is not None:
|
||||
response.downloadProgress.connect(qpartial(self._processDownloadProgress, response, downloadProgressCallback, context))
|
||||
|
||||
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 timeout is not None:
|
||||
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response))
|
||||
|
||||
return response
|
||||
|
||||
def _timeoutSlot(self, response):
|
||||
"""
|
||||
Beware it's call for all request you need to check the status of the response
|
||||
"""
|
||||
# We check if we received HTTP headers
|
||||
if not len(response.rawHeaderList()) > 0:
|
||||
response.abort()
|
||||
|
||||
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 = qpartial(self._requestCanceled, response, context)
|
||||
HTTPClient._progress_callback.progress_dialog().canceled.connect(request_canceled)
|
||||
|
||||
def _requestCanceled(self, response, context):
|
||||
|
||||
if response.isRunning():
|
||||
log.warn("Aborting request for {}".format(response.url()))
|
||||
response.abort()
|
||||
if "query_id" in context:
|
||||
self.notify_progress_end_query(context["query_id"])
|
||||
|
||||
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()
|
||||
error_message = response.errorString()
|
||||
|
||||
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)
|
||||
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))
|
||||
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 len(body.strip(" \n\t")) > 0 and content_type == "application/json":
|
||||
params = json.loads(body)
|
||||
else:
|
||||
params = {}
|
||||
if callback is not None:
|
||||
if status >= 400:
|
||||
callback(params, error=True, server=self, context=context)
|
||||
else:
|
||||
callback(params, server=self, context=context, raw_body=body)
|
||||
# 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 dump(self):
|
||||
"""
|
||||
Returns a representation of this server.
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
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 systemUsage(self):
|
||||
"""
|
||||
Get information about current system usage
|
||||
|
||||
:returns: None or dict
|
||||
"""
|
||||
return self._usage
|
||||
|
||||
def setSystemUsage(self, usage):
|
||||
self._usage = usage
|
||||
self.system_usage_updated_signal.emit()
|
||||
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), timeout=None)
|
||||
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,19 +19,20 @@
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -57,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,13 +19,14 @@
|
||||
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
|
||||
|
||||
|
||||
class EthernetLinkItem(LinkItem):
|
||||
|
||||
"""
|
||||
Ethernet link for the scene.
|
||||
|
||||
@@ -40,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
|
||||
|
||||
@@ -74,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:
|
||||
@@ -105,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.
|
||||
@@ -199,3 +200,5 @@ class EthernetLinkItem(LinkItem):
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(point2)
|
||||
|
||||
self._drawCaptureSymbol()
|
||||
@@ -19,25 +19,30 @@
|
||||
Graphical representation of an image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtWidgets, QtCore
|
||||
|
||||
|
||||
class ImageItem(QtGui.QGraphicsPixmapItem):
|
||||
class ImageItem():
|
||||
|
||||
"""
|
||||
Class to insert an image on the scene.
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
def filePath(self):
|
||||
"""
|
||||
Return image file
|
||||
"""
|
||||
return self._image_path
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this image item.
|
||||
@@ -45,18 +50,10 @@ class ImageItem(QtGui.QGraphicsPixmapItem):
|
||||
|
||||
self.scene().removeItem(self)
|
||||
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
|
||||
try:
|
||||
Topology.instance().removeImage(self)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Image", "Cannot delete the image: {}".format(str(e)))
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -67,7 +64,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
|
||||
@@ -92,7 +89,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,25 @@ 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, QtSvg
|
||||
|
||||
from ..node import Node
|
||||
|
||||
|
||||
class LinkItem(QtGui.QGraphicsPathItem):
|
||||
class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
def __init__(self, symbol, parent):
|
||||
|
||||
QtSvg.QGraphicsSvgItem.__init__(self, symbol, parent)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
|
||||
self.parentItem().mousePressEvent(event)
|
||||
event.accept()
|
||||
|
||||
|
||||
class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
"""
|
||||
Base class for link items.
|
||||
|
||||
@@ -43,8 +58,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
|
||||
|
||||
@@ -75,6 +90,9 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
# indicates if the link is being hovered
|
||||
self._hovered = False
|
||||
|
||||
# QGraphicsSvgItem to indicate a capture
|
||||
self._capturing_item = None
|
||||
|
||||
if not self._adding_flag:
|
||||
# there is a destination
|
||||
self._link = link
|
||||
@@ -94,6 +112,14 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
Delete this link
|
||||
"""
|
||||
|
||||
if not self._source_port.isHotPluggable() and self._source_item.node().status() == Node.started:
|
||||
self._source_item.node().stop()
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Connection", "{} has been stopped because it doesn't support hot unlink.".format(self._source_item.node().name()))
|
||||
if not self._destination_port.isHotPluggable() and self._destination_item.node().status() == Node.started:
|
||||
self._destination_item.node().stop()
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Connection", "{} has been stopped because it doesn't support hot unlink.".format(self._destination_item.node().name()))
|
||||
|
||||
|
||||
# first delete the port labels if any
|
||||
if self._source_port.label():
|
||||
self._source_port.label().setParentItem(None)
|
||||
@@ -171,6 +197,14 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
|
||||
cls._draw_port_labels = state
|
||||
|
||||
def resetPortLabels(self):
|
||||
"""
|
||||
Resets the port label positions.
|
||||
"""
|
||||
|
||||
self._source_port.deleteLabel()
|
||||
self._destination_port.deleteLabel()
|
||||
|
||||
def populateLinkContextualMenu(self, menu):
|
||||
"""
|
||||
Adds device actions to the link contextual menu.
|
||||
@@ -180,33 +214,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)
|
||||
@@ -223,18 +257,30 @@ 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()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
# 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
|
||||
|
||||
def _deleteActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the delete action in the
|
||||
@@ -261,10 +307,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]
|
||||
@@ -282,7 +328,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]
|
||||
@@ -302,18 +348,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):
|
||||
"""
|
||||
@@ -325,7 +371,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()
|
||||
@@ -336,7 +382,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):
|
||||
"""
|
||||
@@ -416,3 +462,20 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
self.destination = scene_point
|
||||
self.adjust()
|
||||
self.update()
|
||||
|
||||
def _drawCaptureSymbol(self):
|
||||
"""
|
||||
Draws a capture symbol in the middle of the link to indicate a capture is active.
|
||||
"""
|
||||
|
||||
if not self._adding_flag:
|
||||
if (self._source_port.capturing() or self._destination_port.capturing()) and self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._capturing_item is None:
|
||||
self._capturing_item = SvgCaptureItem(':/icons/inspect.svg', self)
|
||||
self._capturing_item.setScale(0.6)
|
||||
self._capturing_item.setPos(link_center)
|
||||
if not self._capturing_item.isVisible():
|
||||
self._capturing_item.show()
|
||||
elif self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
|
||||
@@ -19,24 +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():
|
||||
|
||||
class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
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
|
||||
@@ -47,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.
|
||||
@@ -93,42 +88,9 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
# from the server.
|
||||
self._last_error = None
|
||||
|
||||
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
|
||||
from ..main_window import MainWindow
|
||||
self._main_window = MainWindow.instance()
|
||||
self._settings = self._main_window.uiGraphicsView.settings()
|
||||
|
||||
def setUnsavedState(self):
|
||||
"""
|
||||
@@ -187,6 +149,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param node_id: node identifier (integer)
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
self._initialized = True
|
||||
self.update()
|
||||
self._showLabel()
|
||||
@@ -197,6 +161,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
when a the node has started.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
for link in self._links:
|
||||
link.update()
|
||||
|
||||
@@ -206,6 +172,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
when a the node has stopped.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
for link in self._links:
|
||||
link.update()
|
||||
|
||||
@@ -215,6 +183,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
when a the node has suspended.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
for link in self._links:
|
||||
link.update()
|
||||
|
||||
@@ -224,8 +194,12 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
when a the node has been updated.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
if self._node_label:
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
if self._node_label.toPlainText() != self._node.name():
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
self._centerLabel()
|
||||
self.setUnsavedState()
|
||||
|
||||
# update the link tooltips in case the
|
||||
@@ -239,6 +213,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
when a all the links must be deleted.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
for link in self._links.copy():
|
||||
link.delete()
|
||||
|
||||
@@ -248,22 +224,24 @@ 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)
|
||||
self.setUnsavedState()
|
||||
|
||||
def serverErrorSlot(self, node_id, code, message):
|
||||
def serverErrorSlot(self, node_id, message):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when the node has received an error from the server.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param code: error code
|
||||
: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):
|
||||
"""
|
||||
@@ -274,7 +252,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):
|
||||
"""
|
||||
@@ -308,6 +287,19 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
self._node_label = label
|
||||
|
||||
def _centerLabel(self):
|
||||
"""
|
||||
Centers the node label.
|
||||
"""
|
||||
|
||||
text_rect = self._node_label.boundingRect()
|
||||
text_middle = text_rect.topRight() / 2
|
||||
node_rect = self.boundingRect()
|
||||
node_middle = node_rect.topRight() / 2
|
||||
label_x_pos = node_middle.x() - text_middle.x()
|
||||
label_y_pos = -25
|
||||
self._node_label.setPos(label_x_pos, label_y_pos)
|
||||
|
||||
def _showLabel(self):
|
||||
"""
|
||||
Shows the node label on the scene.
|
||||
@@ -317,13 +309,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
self._node_label = NoteItem(self)
|
||||
self._node_label.setEditable(False)
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
text_rect = self._node_label.boundingRect()
|
||||
text_middle = text_rect.topRight() / 2
|
||||
node_rect = self.boundingRect()
|
||||
node_middle = node_rect.topRight() / 2
|
||||
label_x_pos = node_middle.x() - text_middle.x()
|
||||
label_y_pos = -25
|
||||
self._node_label.setPos(label_x_pos, label_y_pos)
|
||||
self._centerLabel()
|
||||
|
||||
def connectToPort(self, unavailable_ports=[]):
|
||||
"""
|
||||
@@ -335,35 +321,43 @@ 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 by port name
|
||||
port_names = {}
|
||||
# sort the ports
|
||||
ports_dict = {}
|
||||
for port in ports:
|
||||
port_names[port.name()] = port
|
||||
if port.adapterNumber() is not None:
|
||||
# make the port number unique (special case with WICs).
|
||||
port_number = port.portNumber()
|
||||
if port_number >= 16:
|
||||
port_number *= 8
|
||||
ports_dict[(port.adapterNumber() * 16) + port_number] = port
|
||||
elif port.portNumber()is not None:
|
||||
ports_dict[port.portNumber()] = port
|
||||
else:
|
||||
ports_dict[port.name()] = port
|
||||
|
||||
try:
|
||||
# try a numeric sort first
|
||||
ports = sorted(port_names.keys(), key=int)
|
||||
ports = sorted(ports_dict.keys(), key=int)
|
||||
except ValueError:
|
||||
# fall back to a classic sort
|
||||
ports = sorted(port_names.keys())
|
||||
ports = sorted(ports_dict.keys())
|
||||
|
||||
# show a contextual menu for the user to choose a port
|
||||
for port in ports:
|
||||
port_object = port_names[port]
|
||||
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)
|
||||
action = menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port_object.name())
|
||||
action.setDisabled(True)
|
||||
elif port_object.isFree():
|
||||
menu.addAction(QtGui.QIcon(':/icons/led_red.svg'), port)
|
||||
menu.addAction(QtGui.QIcon(':/icons/led_red.svg'), port_object.name())
|
||||
else:
|
||||
menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port)
|
||||
menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port_object.name())
|
||||
|
||||
menu.triggered.connect(self.selectedPortSlot)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
@@ -393,20 +387,29 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param value: value of the change
|
||||
"""
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
GRID_SIZE = 75
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
tmp_x = (GRID_SIZE * round((self.x() + mid_x) / GRID_SIZE)) - mid_x
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
tmp_y = (GRID_SIZE * round((self.y() + mid_y) / GRID_SIZE)) - mid_y
|
||||
if tmp_x != self.x() and tmp_y != self.y():
|
||||
self.setPos(tmp_x, tmp_y)
|
||||
|
||||
# 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):
|
||||
"""
|
||||
@@ -418,8 +421,9 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
# don't show the selection rectangle
|
||||
option.state = QtGui.QStyle.State_None
|
||||
QtSvg.QGraphicsSvgItem.paint(self, painter, option, widget)
|
||||
if not self._settings["draw_rectangle_selected_item"]:
|
||||
option.state = QtWidgets.QStyle.State_None
|
||||
super().paint(painter, option, widget)
|
||||
|
||||
if not self._initialized or self.show_layer:
|
||||
brect = self.boundingRect()
|
||||
@@ -443,7 +447,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)
|
||||
@@ -467,13 +471,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):
|
||||
"""
|
||||
@@ -482,7 +481,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,10 +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.
|
||||
|
||||
@@ -33,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()
|
||||
@@ -58,6 +59,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
|
||||
Topology.instance().removeNote(self)
|
||||
|
||||
def editable(self):
|
||||
@@ -77,9 +79,9 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
"""
|
||||
|
||||
self._editable = value
|
||||
#if not self._editable:
|
||||
# if not self._editable:
|
||||
# self.setFlag(self.ItemIsSelectable, enabled=False)
|
||||
#else:
|
||||
# else:
|
||||
# self.setFlag(self.ItemIsSelectable)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
@@ -100,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):
|
||||
"""
|
||||
@@ -131,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()
|
||||
@@ -141,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):
|
||||
"""
|
||||
@@ -152,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
|
||||
@@ -177,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)
|
||||
@@ -197,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:
|
||||
@@ -234,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
|
||||
@@ -19,19 +19,20 @@
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -57,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,13 +20,14 @@ 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
|
||||
|
||||
|
||||
class SerialLinkItem(LinkItem):
|
||||
|
||||
"""
|
||||
Serial link for the scene.
|
||||
|
||||
@@ -41,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):
|
||||
"""
|
||||
@@ -78,8 +79,8 @@ class SerialLinkItem(LinkItem):
|
||||
scale_vect_diag = math.sqrt(scale_vect.x() ** 2 + scale_vect.y() ** 2)
|
||||
scale_coef = scale_vect_diag / 40.0
|
||||
|
||||
self.source = QtCore.QPointF(self.source.x() + scale_vect.x() / scale_coef, self.source.y() + scale_vect.y() / scale_coef)
|
||||
self.destination = QtCore.QPointF(self.destination.x() - scale_vect.x() / scale_coef, self.destination.y() - scale_vect.y() / scale_coef)
|
||||
self.source_point = QtCore.QPointF(self.source.x() + scale_vect.x() / scale_coef, self.source.y() + scale_vect.y() / scale_coef)
|
||||
self.destination_point = QtCore.QPointF(self.destination.x() - scale_vect.x() / scale_coef, self.destination.y() - scale_vect.y() / scale_coef)
|
||||
|
||||
def shape(self):
|
||||
"""
|
||||
@@ -88,11 +89,11 @@ class SerialLinkItem(LinkItem):
|
||||
:returns: QPainterPath instance
|
||||
"""
|
||||
|
||||
path = QtGui.QGraphicsPathItem.shape(self)
|
||||
path = QtWidgets.QGraphicsPathItem.shape(self)
|
||||
offset = self._point_size / 2
|
||||
point = self.source
|
||||
point = self.source_point
|
||||
path.addEllipse(point.x() - offset, point.y() - offset, self._point_size, self._point_size)
|
||||
point = self.destination
|
||||
point = self.destination_point
|
||||
path.addEllipse(point.x() - offset, point.y() - offset, self._point_size, self._point_size)
|
||||
return path
|
||||
|
||||
@@ -105,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"]:
|
||||
|
||||
@@ -144,7 +145,7 @@ class SerialLinkItem(LinkItem):
|
||||
elif source_port_label:
|
||||
source_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.source)
|
||||
painter.drawPoint(self.source_point)
|
||||
|
||||
# destination point color
|
||||
if self._destination_port.status() == Port.started:
|
||||
@@ -177,4 +178,6 @@ class SerialLinkItem(LinkItem):
|
||||
elif destination_port_label:
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.destination)
|
||||
painter.drawPoint(self.destination_point)
|
||||
|
||||
self._drawCaptureSymbol()
|
||||
|
||||
@@ -19,20 +19,21 @@
|
||||
Base class for shape items (Rectangle, ellipse etc.).
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class ShapeItem:
|
||||
|
||||
"""
|
||||
Base class to draw shapes on the scene.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
@@ -57,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):
|
||||
"""
|
||||
@@ -68,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):
|
||||
"""
|
||||
@@ -93,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):
|
||||
"""
|
||||
@@ -144,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):
|
||||
"""
|
||||
@@ -207,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)
|
||||
@@ -269,6 +270,9 @@ class ShapeItem:
|
||||
# load optional properties
|
||||
z = shape_info.get("z")
|
||||
color = shape_info.get("color")
|
||||
if not color and shape_info.get("fill_color"):
|
||||
# compatibility with old 1.0 projects
|
||||
color = shape_info.get("fill_color")
|
||||
transparency = shape_info.get("transparency")
|
||||
border_color = shape_info.get("border_color")
|
||||
border_transparency = shape_info.get("border_transparency")
|
||||
@@ -280,7 +284,7 @@ class ShapeItem:
|
||||
color = QtGui.QColor(color)
|
||||
else:
|
||||
color = QtGui.QColor(255, 255, 255)
|
||||
if transparency:
|
||||
if transparency is not None:
|
||||
color.setAlpha(transparency)
|
||||
self.setBrush(QtGui.QBrush(color))
|
||||
|
||||
@@ -294,7 +298,7 @@ class ShapeItem:
|
||||
pen.setColor(border_color)
|
||||
if border_width is not None:
|
||||
pen.setWidth(int(border_width))
|
||||
if border_style:
|
||||
if border_style is not None:
|
||||
pen.setStyle(QtCore.Qt.PenStyle(border_style))
|
||||
self.setPen(pen)
|
||||
|
||||
|
||||
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
|
||||
51
gns3/items/svg_node_item.py
Normal file
51
gns3/items/svg_node_item.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# -*- 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 ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
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 = QImageSvgRenderer(symbol)
|
||||
if symbol != node.defaultSymbol():
|
||||
renderer.setObjectName(symbol)
|
||||
else:
|
||||
renderer = QImageSvgRenderer(node.defaultSymbol())
|
||||
self.setSharedRenderer(renderer)
|
||||
184
gns3/jsonrpc.py
184
gns3/jsonrpc.py
@@ -1,184 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2013 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/>.
|
||||
|
||||
"""
|
||||
JSON-RPC protocol implementation.
|
||||
http://www.jsonrpc.org/specification
|
||||
"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
|
||||
class JSONRPCObject(object):
|
||||
"""
|
||||
Base object for JSON-RPC requests, responses,
|
||||
notifications and errors.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
return JSONRPCEncoder().default(self)
|
||||
|
||||
def __str__(self, *args, **kwargs):
|
||||
return json.dumps(self, cls=JSONRPCEncoder)
|
||||
|
||||
def __call__(self):
|
||||
return JSONRPCEncoder().default(self)
|
||||
|
||||
|
||||
class JSONRPCEncoder(json.JSONEncoder):
|
||||
"""
|
||||
Creates the JSON-RPC message.
|
||||
"""
|
||||
|
||||
def default(self, obj):
|
||||
"""
|
||||
Returns a Python dictionary corresponding to a JSON-RPC message.
|
||||
"""
|
||||
|
||||
if isinstance(obj, JSONRPCObject):
|
||||
message = {"jsonrpc": 2.0}
|
||||
for field in dir(obj):
|
||||
if not field.startswith('_'):
|
||||
value = getattr(obj, field)
|
||||
message[field] = value
|
||||
return message
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class JSONRPCInvalidRequest(JSONRPCObject):
|
||||
"""
|
||||
Error response for an invalid request.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = None
|
||||
self.error = {"code": -32600, "message": "Invalid Request"}
|
||||
|
||||
|
||||
class JSONRPCMethodNotFound(JSONRPCObject):
|
||||
"""
|
||||
Error response for an method not found.
|
||||
|
||||
:param request_id: JSON-RPC identifier
|
||||
"""
|
||||
|
||||
def __init__(self, request_id):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = request_id
|
||||
self.error = {"code": -32601, "message": "Method not found"}
|
||||
|
||||
|
||||
class JSONRPCInvalidParams(JSONRPCObject):
|
||||
"""
|
||||
Error response for invalid parameters.
|
||||
|
||||
:param request_id: JSON-RPC identifier
|
||||
"""
|
||||
|
||||
def __init__(self, request_id):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = request_id
|
||||
self.error = {"code": -32602, "message": "Invalid params"}
|
||||
|
||||
|
||||
class JSONRPCInternalError(JSONRPCObject):
|
||||
"""
|
||||
Error response for an internal error.
|
||||
|
||||
:param request_id: JSON-RPC identifier (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, request_id=None):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = request_id
|
||||
self.error = {"code": -32603, "message": "Internal error"}
|
||||
|
||||
|
||||
class JSONRPCParseError(JSONRPCObject):
|
||||
"""
|
||||
Error response for parsing error.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = None
|
||||
self.error = {"code": -32700, "message": "Parse error"}
|
||||
|
||||
|
||||
class JSONRPCCustomError(JSONRPCObject):
|
||||
"""
|
||||
Error response for an custom error.
|
||||
|
||||
:param code: JSON-RPC error code
|
||||
:param message: JSON-RPC error message
|
||||
:param request_id: JSON-RPC identifier (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, code, message, request_id=None):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = request_id
|
||||
self.error = {"code": code, "message": message}
|
||||
|
||||
|
||||
class JSONRPCResponse(JSONRPCObject):
|
||||
"""
|
||||
JSON-RPC successful response.
|
||||
|
||||
:param result: JSON-RPC result
|
||||
:param request_id: JSON-RPC identifier
|
||||
"""
|
||||
|
||||
def __init__(self, result, request_id):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = request_id
|
||||
self.result = result
|
||||
|
||||
|
||||
class JSONRPCRequest(JSONRPCObject):
|
||||
"""
|
||||
JSON-RPC request.
|
||||
|
||||
:param method: JSON-RPC destination method
|
||||
:param params: JSON-RPC params for the corresponding method (optional)
|
||||
:param request_id: JSON-RPC identifier (generated by default)
|
||||
"""
|
||||
|
||||
def __init__(self, method, params=None, request_id=None):
|
||||
JSONRPCObject.__init__(self)
|
||||
if request_id == None:
|
||||
request_id = str(uuid.uuid4())
|
||||
self.id = request_id
|
||||
self.method = method
|
||||
if params:
|
||||
self.params = params
|
||||
|
||||
|
||||
class JSONRPCNotification(JSONRPCObject):
|
||||
"""
|
||||
JSON-RPC notification.
|
||||
|
||||
:param method: JSON-RPC destination method
|
||||
:param params: JSON-RPC params for the corresponding method (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, method, params=None):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.method = method
|
||||
if params:
|
||||
self.params = params
|
||||
120
gns3/link.py
120
gns3/link.py
@@ -22,12 +22,14 @@ 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__)
|
||||
|
||||
|
||||
class Link(QtCore.QObject):
|
||||
|
||||
"""
|
||||
Link implementation.
|
||||
|
||||
@@ -47,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(),
|
||||
@@ -71,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():
|
||||
@@ -136,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()
|
||||
|
||||
@@ -200,10 +207,12 @@ class Link(QtCore.QObject):
|
||||
:param port_id: port identifier
|
||||
:param lport: local UDP port
|
||||
"""
|
||||
if not self:
|
||||
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():
|
||||
laddr = self._source_node.server().host
|
||||
if self._source_node and node_id == self._source_node.id() and port_id == self._source_port.id():
|
||||
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)
|
||||
@@ -213,8 +222,8 @@ class Link(QtCore.QObject):
|
||||
laddr))
|
||||
|
||||
# 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
|
||||
elif self._destination_node and node_id == self._destination_node.id() and port_id == self._destination_port.id():
|
||||
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)
|
||||
@@ -224,7 +233,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
|
||||
@@ -244,6 +252,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
|
||||
@@ -254,6 +286,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
|
||||
@@ -335,29 +371,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)
|
||||
@@ -371,6 +397,7 @@ class Link(QtCore.QObject):
|
||||
|
||||
self._source_nio_active = False
|
||||
self._destination_nio_active = False
|
||||
self.deleteLink()
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
@@ -384,5 +411,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()}
|
||||
|
||||
355
gns3/local_config.py
Normal file
355
gns3/local_config.py
Normal file
@@ -0,0 +1,355 @@
|
||||
# -*- 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/>.
|
||||
|
||||
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(QtCore.QObject):
|
||||
|
||||
"""
|
||||
Handles the local GUI settings.
|
||||
"""
|
||||
|
||||
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"
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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._readConfig(system_wide_config_file)
|
||||
|
||||
config_file_in_cwd = os.path.join(os.getcwd(), filename)
|
||||
if os.path.exists(config_file_in_cwd):
|
||||
# use any config file present in the current working directory
|
||||
self._config_file = config_file_in_cwd
|
||||
elif not os.path.exists(self._config_file):
|
||||
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", 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))
|
||||
|
||||
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
|
||||
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.1dev2"):
|
||||
if sys.platform.startswith("darwin"):
|
||||
from .settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, DEFAULT_TELNET_CONSOLE_COMMAND
|
||||
|
||||
if "MainWindow" in self._settings:
|
||||
if self._settings["MainWindow"]["telnet_console_command"] not in PRECONFIGURED_TELNET_CONSOLE_COMMANDS.values():
|
||||
self._settings["MainWindow"]["telnet_console_command"] = DEFAULT_TELNET_CONSOLE_COMMAND
|
||||
|
||||
|
||||
def _readConfig(self, config_path):
|
||||
"""
|
||||
Read the configuration file.
|
||||
"""
|
||||
|
||||
log.info("Load config from %s", config_path)
|
||||
try:
|
||||
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):
|
||||
"""
|
||||
Write the configuration file.
|
||||
"""
|
||||
|
||||
self._settings["version"] = __version__
|
||||
try:
|
||||
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.
|
||||
|
||||
:returns: path to the config file.
|
||||
"""
|
||||
|
||||
return self._config_file
|
||||
|
||||
def setConfigFilePath(self, config_file):
|
||||
"""
|
||||
Set a new config file
|
||||
|
||||
:returns: path to the config file.
|
||||
"""
|
||||
|
||||
self._config_file = config_file
|
||||
self._readConfig(self._config_file)
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Get the settings.
|
||||
|
||||
:returns: settings (dict)
|
||||
"""
|
||||
|
||||
return copy.deepcopy(self._settings)
|
||||
|
||||
def setSettings(self, settings):
|
||||
"""
|
||||
Save the settings.
|
||||
|
||||
:param settings: settings to save (dict)
|
||||
"""
|
||||
|
||||
if self._settings != settings:
|
||||
self._settings.update(settings)
|
||||
self._writeConfig()
|
||||
|
||||
def loadSectionSettings(self, section, default_settings):
|
||||
"""
|
||||
Get all the settings from a given section.
|
||||
|
||||
:param default_settings: setting names and default values (dict)
|
||||
|
||||
:returns: settings (dict)
|
||||
"""
|
||||
|
||||
settings = self.settings().get(section, dict())
|
||||
changed = False
|
||||
|
||||
def _copySettings(local, default):
|
||||
"""
|
||||
Copy only existing settings, ignore the other.
|
||||
Add default values if require.
|
||||
"""
|
||||
nonlocal changed
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Save all the settings in a given section.
|
||||
|
||||
:param section: section name
|
||||
:param settings: settings to save (dict)
|
||||
"""
|
||||
|
||||
if section not in self._settings:
|
||||
self._settings[section] = {}
|
||||
|
||||
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(config_file=None):
|
||||
"""
|
||||
Singleton to return only on instance of LocalConfig.
|
||||
|
||||
:returns: instance of 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
|
||||
139
gns3/local_server_config.py
Normal file
139
gns3/local_server_config.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# -*- 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/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import configparser
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalServerConfig:
|
||||
|
||||
"""
|
||||
Local server configuration.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
appname = "GNS3"
|
||||
|
||||
self._config = configparser.RawConfigParser()
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_server.ini"
|
||||
else:
|
||||
filename = "gns3_server.conf"
|
||||
|
||||
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()
|
||||
except OSError as e:
|
||||
log.error("Could not create the local server configuration {}: {}".format(self._config_file, e))
|
||||
self.readConfig()
|
||||
|
||||
def readConfig(self):
|
||||
"""
|
||||
Read the configuration file.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._config.read(self._config_file, encoding="utf-8")
|
||||
except (OSError, configparser.Error, UnicodeEncodeError, UnicodeDecodeError) as e:
|
||||
log.error("Could not read the local server configuration {}: {}".format(self._config_file, e))
|
||||
|
||||
def writeConfig(self):
|
||||
"""
|
||||
Write the configuration file.
|
||||
"""
|
||||
|
||||
try:
|
||||
log.debug("Write configuration file %s", self._config_file)
|
||||
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))
|
||||
|
||||
def loadSettings(self, section, default_settings, types):
|
||||
"""
|
||||
Get all the settings from a given section.
|
||||
|
||||
:param section: section name
|
||||
:param default_settings: setting names and default values (dict)
|
||||
:param types: setting types (dict)
|
||||
|
||||
:returns: settings (dict)
|
||||
"""
|
||||
|
||||
if section not in self._config:
|
||||
self._config[section] = {}
|
||||
|
||||
settings = {}
|
||||
for name, default in default_settings.items():
|
||||
if types[name] is int:
|
||||
settings[name] = self._config[section].getint(name, default)
|
||||
elif types[name] is bool:
|
||||
settings[name] = self._config[section].getboolean(name, default)
|
||||
elif types[name] is float:
|
||||
settings[name] = self._config[section].getfloat(name, default)
|
||||
else:
|
||||
settings[name] = self._config[section].get(name, default)
|
||||
|
||||
# sync with the config file
|
||||
self.saveSettings(section, settings)
|
||||
return settings
|
||||
|
||||
def saveSettings(self, section, settings):
|
||||
"""
|
||||
Save all the settings in a given section.
|
||||
|
||||
:param section: section name
|
||||
:param settings: settings to save (dict)
|
||||
"""
|
||||
|
||||
changed = False
|
||||
if section not in self._config:
|
||||
self._config[section] = {}
|
||||
changed = True
|
||||
|
||||
for name, value in settings.items():
|
||||
if name not in self._config[section] or self._config[section][name] != str(value):
|
||||
self._config[section][name] = str(value)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
self.writeConfig()
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of LocalServerConfig.
|
||||
|
||||
:returns: instance of Config
|
||||
"""
|
||||
|
||||
if not hasattr(LocalServerConfig, "_instance"):
|
||||
LocalServerConfig._instance = LocalServerConfig()
|
||||
return LocalServerConfig._instance
|
||||
109
gns3/logger.py
Normal file
109
gns3/logger.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- 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/>.
|
||||
|
||||
"""Provide a pretty logging on console"""
|
||||
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
class ColouredFormatter(logging.Formatter):
|
||||
RESET = '\x1B[0m'
|
||||
RED = '\x1B[31m'
|
||||
YELLOW = '\x1B[33m'
|
||||
GREEN = '\x1B[32m'
|
||||
PINK = '\x1b[35m'
|
||||
|
||||
def format(self, record, colour=False):
|
||||
|
||||
message = super().format(record)
|
||||
|
||||
if not colour or sys.platform.startswith("win"):
|
||||
return message.replace("#RESET#", "")
|
||||
|
||||
level_no = record.levelno
|
||||
if level_no >= logging.CRITICAL:
|
||||
colour = self.RED
|
||||
elif level_no >= logging.ERROR:
|
||||
colour = self.RED
|
||||
elif level_no >= logging.WARNING:
|
||||
colour = self.YELLOW
|
||||
elif level_no >= logging.INFO:
|
||||
colour = self.GREEN
|
||||
elif level_no >= logging.DEBUG:
|
||||
colour = self.PINK
|
||||
else:
|
||||
colour = self.RESET
|
||||
|
||||
message = message.replace("#RESET#", self.RESET)
|
||||
message = '{colour}{message}{reset}'.format(colour=colour, message=message, reset=self.RESET)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
class ColouredStreamHandler(logging.StreamHandler):
|
||||
|
||||
def format(self, record, colour=False):
|
||||
|
||||
if not isinstance(self.formatter, ColouredFormatter):
|
||||
self.formatter = ColouredFormatter()
|
||||
|
||||
return self.formatter.format(record, colour)
|
||||
|
||||
def emit(self, record):
|
||||
|
||||
stream = self.stream
|
||||
try:
|
||||
msg = self.format(record, stream.isatty())
|
||||
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, 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", "{")
|
||||
else:
|
||||
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])
|
||||
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()
|
||||
233
gns3/main.py
233
gns3/main.py
@@ -16,24 +16,53 @@
|
||||
# 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 datetime
|
||||
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
|
||||
# otherwise the egg cache is initialized in his standard location and
|
||||
# if is not writetable the application crash. It's the user fault
|
||||
# because one day the user as used sudo to run an egg and break his
|
||||
# filesystem permissions, but it's a common mistake.
|
||||
from gns3.utils.get_resource import get_resource
|
||||
|
||||
|
||||
import datetime
|
||||
import traceback
|
||||
import time
|
||||
import locale
|
||||
import argparse
|
||||
import signal
|
||||
import psutil
|
||||
|
||||
try:
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets
|
||||
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
|
||||
from gns3.utils import parse_version
|
||||
|
||||
|
||||
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__
|
||||
|
||||
|
||||
@@ -81,11 +110,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('--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("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 = "exception.log"
|
||||
exception_file_path = "exceptions.log"
|
||||
|
||||
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):
|
||||
|
||||
@@ -94,22 +157,27 @@ 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()
|
||||
except OSError as e:
|
||||
print("Could not save traceback to {}: {}".format(exception_file_path, e))
|
||||
print("Could not save traceback to {}: {}".format(os.path.normpath(exception_file_path), e))
|
||||
|
||||
if not sys.stdout.isatty():
|
||||
# if stdout is not a tty (redirected to the console view),
|
||||
# then print the exception on stderr too.
|
||||
print("".join(lines), file=sys.stderr)
|
||||
|
||||
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
|
||||
|
||||
@@ -117,30 +185,15 @@ 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")
|
||||
|
||||
version = lambda version_string: [int(i) for i in version_string.split('.')]
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.0.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.0.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
|
||||
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))
|
||||
|
||||
# 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 == "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))
|
||||
|
||||
try:
|
||||
# if tornado is present then enable pretty logging.
|
||||
import tornado.log
|
||||
tornado.log.enable_pretty_logging()
|
||||
except ImportError:
|
||||
pass
|
||||
if parse_version(psutil.__version__) < parse_version("2.2.1"):
|
||||
raise SystemExit("Requirement is psutil version 2.2.1 or higher, got version {}".format(psutil.__version__))
|
||||
|
||||
# check for the correct locale
|
||||
# (UNIX/Linux only)
|
||||
@@ -156,69 +209,71 @@ def main():
|
||||
if sys.platform.startswith('win') or sys.platform.startswith('darwin'):
|
||||
QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat)
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if sys.platform.startswith('win') and hasattr(sys, "frozen"):
|
||||
try:
|
||||
import win32console
|
||||
import win32con
|
||||
import win32gui
|
||||
except ImportError:
|
||||
raise RuntimeError("Python for Windows extensions must be installed.")
|
||||
raise SystemExit("Python for Windows extensions must be installed.")
|
||||
|
||||
try:
|
||||
win32console.AllocConsole()
|
||||
console_window = win32console.GetConsoleWindow()
|
||||
win32gui.ShowWindow(console_window, win32con.SW_HIDE)
|
||||
except win32console.error as e:
|
||||
print("warning: could not allocate console: {}".format(e))
|
||||
|
||||
exit_code = MainWindow.exit_code_reboot
|
||||
while exit_code == MainWindow.exit_code_reboot:
|
||||
|
||||
exit_code = 0
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
|
||||
# this info is necessary for QSettings
|
||||
app.setOrganizationName("GNS3")
|
||||
app.setOrganizationDomain("gns3.net")
|
||||
app.setApplicationName("GNS3")
|
||||
app.setApplicationVersion(__version__)
|
||||
|
||||
# save client logging info to a file
|
||||
logfile = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "GNS3_client.log") # FIXME: does it work?
|
||||
try:
|
||||
if not options.debug:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(QtCore.QSettings().fileName()))
|
||||
except FileExistsError:
|
||||
pass
|
||||
handler = logging.FileHandler(logfile, "w")
|
||||
if options.debug:
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
if len(root_logger.handlers) > 0:
|
||||
root_handler = root_logger.handlers[0]
|
||||
else:
|
||||
root_handler = logging.StreamHandler()
|
||||
root_logger.addHandler(root_handler)
|
||||
root_handler.setLevel(logging.DEBUG)
|
||||
else:
|
||||
handler.setLevel(logging.INFO)
|
||||
log.info('Log level: {}'.format(logging.getLevelName(log.getEffectiveLevel())))
|
||||
# hide the console
|
||||
console_window = win32console.GetConsoleWindow()
|
||||
win32gui.ShowWindow(console_window, win32con.SW_HIDE)
|
||||
except win32console.error as e:
|
||||
print("warning: could not allocate console: {}".format(e))
|
||||
|
||||
formatter = logging.Formatter("[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d] %(message)s",
|
||||
datefmt="%y%m%d %H:%M:%S")
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
except OSError as e:
|
||||
log.warn("could not log to {}: {}".format(logfile, e))
|
||||
global app
|
||||
app = Application(sys.argv)
|
||||
|
||||
# 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)
|
||||
# save client logging info to a file
|
||||
logfile = os.path.join(LocalConfig.configDirectory(), "gns3_gui.log")
|
||||
|
||||
mainwindow = MainWindow.instance()
|
||||
mainwindow.show()
|
||||
exit_code = app.exec_()
|
||||
delattr(MainWindow, "_instance")
|
||||
app.deleteLater()
|
||||
# on debug enable logging to stdout
|
||||
if options.debug:
|
||||
root_logger = init_logger(logging.DEBUG, logfile)
|
||||
else:
|
||||
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(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()
|
||||
orig_sigint = signal.signal(signal.SIGINT, sigint_handler)
|
||||
orig_sigterm = signal.signal(signal.SIGTERM, sigint_handler)
|
||||
|
||||
mainwindow.show()
|
||||
|
||||
exit_code = app.exec_()
|
||||
|
||||
signal.signal(signal.SIGINT, orig_sigint)
|
||||
signal.signal(signal.SIGTERM, orig_sigterm)
|
||||
|
||||
|
||||
delattr(MainWindow, "_instance")
|
||||
|
||||
# We force deleting the app object otherwise it's segfault on Fedora
|
||||
del app
|
||||
# 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)
|
||||
|
||||
|
||||
1523
gns3/main_window.py
1523
gns3/main_window.py
File diff suppressed because it is too large
Load Diff
@@ -21,5 +21,7 @@ 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
|
||||
from gns3.modules.docker import Docker
|
||||
|
||||
MODULES = [Builtin, VPCS, Dynamips, IOU, VirtualBox, Qemu]
|
||||
MODULES = [VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker, Builtin]
|
||||
|
||||
@@ -19,11 +19,8 @@
|
||||
Built-in module implementation.
|
||||
"""
|
||||
|
||||
import os
|
||||
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
|
||||
|
||||
@@ -33,62 +30,18 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Builtin(Module):
|
||||
|
||||
"""
|
||||
Built-in module.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Module.__init__(self)
|
||||
super().__init__()
|
||||
|
||||
self._nodes = []
|
||||
self._servers = []
|
||||
|
||||
def setProjectFilesDir(self, path):
|
||||
"""
|
||||
Sets the project files directory path this module.
|
||||
|
||||
:param path: path to the local project files directory
|
||||
"""
|
||||
|
||||
pass # not used by this module
|
||||
|
||||
def setImageFilesDir(self, path):
|
||||
"""
|
||||
Sets the image files directory path this module.
|
||||
|
||||
:param path: path to the local image files directory
|
||||
"""
|
||||
|
||||
pass # not used by this module
|
||||
|
||||
def addServer(self, server):
|
||||
"""
|
||||
Adds a server to be used by this module.
|
||||
|
||||
:param server: WebSocketClient instance
|
||||
"""
|
||||
|
||||
log.info("adding server {}:{} to built-in module".format(server.host, server.port))
|
||||
self._servers.append(server)
|
||||
|
||||
def removeServer(self, server):
|
||||
"""
|
||||
Removes a server from being used by this module.
|
||||
|
||||
:param server: WebSocketClient instance
|
||||
"""
|
||||
|
||||
log.info("removing server {}:{} from built-in module".format(server.host, server.port))
|
||||
self._servers.remove(server)
|
||||
|
||||
def servers(self):
|
||||
"""
|
||||
Returns all the servers used by this module.
|
||||
|
||||
:returns: list of WebSocketClient instances
|
||||
"""
|
||||
|
||||
return self._servers
|
||||
def configChangedSlot(self):
|
||||
pass
|
||||
|
||||
def addNode(self, node):
|
||||
"""
|
||||
@@ -109,80 +62,27 @@ class Builtin(Module):
|
||||
if node in self._nodes:
|
||||
self._nodes.remove(node)
|
||||
|
||||
def allocateServer(self, node_class):
|
||||
def reset(self):
|
||||
"""
|
||||
Allocates a server.
|
||||
|
||||
:param node_class: Node object
|
||||
|
||||
:returns: allocated server (WebSocketClient instance)
|
||||
Resets the module.
|
||||
"""
|
||||
|
||||
# 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"])
|
||||
log.info("Built-in module reset")
|
||||
self._nodes.clear()
|
||||
|
||||
# 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 not True 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):
|
||||
def createNode(self, node_class, server, project):
|
||||
"""
|
||||
Creates a new node.
|
||||
|
||||
:param node_class: Node object
|
||||
:param server: WebSocketClient instance
|
||||
:param server: HTTPClient instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("creating node {}".format(node_class))
|
||||
|
||||
if not server.connected():
|
||||
try:
|
||||
log.info("reconnecting to server {}:{}".format(server.host, server.port))
|
||||
server.reconnect()
|
||||
except OSError as e:
|
||||
raise ModuleError("Could not connect to server {}:{}: {}".format(server.host,
|
||||
server.port,
|
||||
e))
|
||||
if server not in self._servers:
|
||||
self.addServer(server)
|
||||
|
||||
# create an instance of the node class
|
||||
return node_class(self, server)
|
||||
return node_class(self, server, project)
|
||||
|
||||
def setupNode(self, node, node_name):
|
||||
"""
|
||||
@@ -195,13 +95,6 @@ class Builtin(Module):
|
||||
log.info("configuring node {}".format(node))
|
||||
node.setup()
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the servers.
|
||||
"""
|
||||
|
||||
self._servers.clear()
|
||||
|
||||
@staticmethod
|
||||
def findAlternativeInterface(node, missing_interface):
|
||||
|
||||
@@ -213,13 +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
|
||||
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
|
||||
@@ -256,8 +151,8 @@ 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(),
|
||||
"builtin": True}
|
||||
)
|
||||
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
|
||||
@@ -36,17 +36,20 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Cloud(Node):
|
||||
|
||||
"""
|
||||
Dynamips cloud.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
_name_instance_count = 1
|
||||
|
||||
def __init__(self, module, server):
|
||||
Node.__init__(self, server)
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
|
||||
log.info("cloud is being created")
|
||||
# create an unique id and name
|
||||
@@ -55,13 +58,10 @@ class Cloud(Node):
|
||||
|
||||
name = "Cloud {}".format(self._name_id)
|
||||
self.setStatus(Node.started) # this is an always-on node
|
||||
self._defaults = {}
|
||||
self._ports = []
|
||||
self._module = module
|
||||
self._initial_settings = None
|
||||
self._settings = {"nios": [],
|
||||
self._settings = {"name": name,
|
||||
"interfaces": {},
|
||||
"name": name}
|
||||
"nios": []}
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
@@ -72,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.
|
||||
|
||||
@@ -82,11 +82,12 @@ class Cloud(Node):
|
||||
if name:
|
||||
self._settings["name"] = name
|
||||
|
||||
if initial_settings:
|
||||
self._initial_settings = initial_settings
|
||||
self._server.send_message("builtin.interfaces", None, self._setupCallback)
|
||||
if additional_settings and "nios" in additional_settings:
|
||||
self._settings["nios"] = additional_settings["nios"]
|
||||
|
||||
def _setupCallback(self, result, error=False):
|
||||
self._server.get("/interfaces", self._setupCallback)
|
||||
|
||||
def _setupCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for setup.
|
||||
|
||||
@@ -101,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):
|
||||
"""
|
||||
@@ -150,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.
|
||||
@@ -204,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.
|
||||
@@ -211,53 +278,28 @@ class Cloud(Node):
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
nios = new_settings["nios"]
|
||||
|
||||
updated = False
|
||||
# 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 == 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)
|
||||
if "nios" in new_settings:
|
||||
nios = new_settings["nios"]
|
||||
self._addPorts(nios, ignore_existing_nio=True)
|
||||
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
|
||||
# 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
|
||||
|
||||
self._settings["nios"] = new_settings["nios"].copy()
|
||||
if updated:
|
||||
log.info("cloud {} has been updated".format(self.name()))
|
||||
self.updated_signal.emit()
|
||||
@@ -308,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:
|
||||
@@ -327,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():
|
||||
@@ -357,15 +401,22 @@ This is a pseudo-device for external connections
|
||||
break
|
||||
if not available_interface:
|
||||
alternative_interface = self._module.findAlternativeInterface(self, topology_port_name)
|
||||
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"])
|
||||
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)
|
||||
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):
|
||||
"""
|
||||
@@ -396,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
|
||||
"""
|
||||
@@ -412,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():
|
||||
|
||||
@@ -24,27 +24,44 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Host(Cloud):
|
||||
|
||||
"""
|
||||
Pseudo host based on a Dynamips Cloud.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
_name_instance_count = 1
|
||||
|
||||
def __init__(self, module, server):
|
||||
Cloud.__init__(self, module, server)
|
||||
def __init__(self, module, server, project):
|
||||
super().__init__(module, server, project)
|
||||
|
||||
log.info("host is being created")
|
||||
# create an unique id and name
|
||||
self._name_id = Host._name_instance_count
|
||||
Host._name_instance_count += 1
|
||||
|
||||
name = "Host {}".format(self._name_id)
|
||||
name = "Host{}".format(self._name_id)
|
||||
self._settings["name"] = name
|
||||
|
||||
self.created_signal.connect(self._autoConfigure)
|
||||
def setup(self, name=None, additional_settings={}):
|
||||
"""
|
||||
Setups this host.
|
||||
|
||||
:param name: optional name for this host
|
||||
"""
|
||||
|
||||
if name:
|
||||
self._settings["name"] = name
|
||||
|
||||
if additional_settings and "nios" in additional_settings:
|
||||
self._settings["nios"] = additional_settings["nios"]
|
||||
else:
|
||||
self.created_signal.connect(self._autoConfigure)
|
||||
|
||||
self._server.get("/interfaces", self._setupCallback)
|
||||
|
||||
def _autoConfigure(self, node_id):
|
||||
"""
|
||||
@@ -59,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
|
||||
@@ -70,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,20 +20,21 @@ 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.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
QtGui.QWidget.__init__(self)
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
self._nios = []
|
||||
self._nios = set()
|
||||
|
||||
# connect NIO generic Ethernet slots
|
||||
self.uiGenericEthernetComboBox.currentIndexChanged.connect(self._genericEthernetSelectedSlot)
|
||||
@@ -47,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)
|
||||
@@ -105,9 +112,9 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
interface = self.uiGenericEthernetLineEdit.text()
|
||||
if interface:
|
||||
nio = "nio_gen_eth:{interface}".format(interface=interface)
|
||||
if not nio in self._nios:
|
||||
if nio not in self._nios:
|
||||
self.uiGenericEthernetListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _genericEthernetDeleteSlot(self):
|
||||
"""
|
||||
@@ -121,9 +128,12 @@ 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)
|
||||
try:
|
||||
self._nios.remove(nio)
|
||||
except KeyError: # If you ask for delete multiple time with race condition you could have a missing key https://github.com/GNS3/gns3-gui/issues/1352
|
||||
return
|
||||
self.uiGenericEthernetListWidget.takeItem(self.uiGenericEthernetListWidget.currentRow())
|
||||
|
||||
def _linuxEthernetSelectedSlot(self, index):
|
||||
@@ -154,9 +164,9 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
interface = self.uiLinuxEthernetLineEdit.text()
|
||||
if interface:
|
||||
nio = "nio_gen_linux:{interface}".format(interface=interface)
|
||||
if not nio in self._nios:
|
||||
if nio not in self._nios:
|
||||
self.uiLinuxEthernetListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _linuxEthernetDeleteSlot(self):
|
||||
"""
|
||||
@@ -170,14 +180,74 @@ 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
|
||||
try:
|
||||
self._nios.remove(nio)
|
||||
# An interface could have been removed and no longer exists in nios
|
||||
except KeyError:
|
||||
pass
|
||||
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.add(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.uiLinuxEthernetListWidget.takeItem(self.uiLinuxEthernetListWidget.currentRow())
|
||||
self.uiNIONATListWidget.takeItem(self.uiNIONATListWidget.currentRow())
|
||||
|
||||
def _NIOUDPSelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected UDP.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIOUDPListWidget.currentItem()
|
||||
@@ -212,9 +282,9 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
nio = "nio_udp:{lport}:{rhost}:{rport}".format(lport=local_port,
|
||||
rhost=remote_host,
|
||||
rport=remote_port)
|
||||
if not nio in self._nios:
|
||||
if nio not in self._nios:
|
||||
self.uiNIOUDPListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
self._nios.add(nio)
|
||||
self.uiLocalPortSpinBox.setValue(local_port + 1)
|
||||
self.uiRemotePortSpinBox.setValue(remote_port + 1)
|
||||
|
||||
@@ -230,7 +300,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())
|
||||
@@ -268,9 +338,9 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
tap_interface = self.uiNIOTAPLineEdit.text()
|
||||
if tap_interface:
|
||||
nio = "nio_tap:{}".format(tap_interface.lower())
|
||||
if not nio in self._nios:
|
||||
if nio not in self._nios:
|
||||
self.uiNIOTAPListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _NIOTAPDeleteSlot(self):
|
||||
"""
|
||||
@@ -284,7 +354,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())
|
||||
@@ -325,9 +395,9 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
if local_file and remote_file:
|
||||
nio = "nio_unix:{local}:{remote}".format(local=local_file,
|
||||
remote=remote_file)
|
||||
if not nio in self._nios:
|
||||
if nio not in self._nios:
|
||||
self.uiNIOUNIXListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _NIOUNIXDeleteSlot(self):
|
||||
"""
|
||||
@@ -341,7 +411,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())
|
||||
@@ -381,9 +451,9 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
local_file = self.uiVDELocalFileLineEdit.text()
|
||||
if local_file and control_file:
|
||||
nio = "nio_vde:{control}:{local}".format(control=control_file, local=local_file)
|
||||
if not nio in self._nios:
|
||||
if nio not in self._nios:
|
||||
self.uiNIOVDEListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _NIOVDEDeleteSlot(self):
|
||||
"""
|
||||
@@ -397,7 +467,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())
|
||||
@@ -405,6 +475,8 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
def _NIONullSelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected NULL NIO.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIONullListWidget.currentItem()
|
||||
@@ -433,9 +505,9 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
identifier = self.uiNIONullIdentiferLineEdit.text()
|
||||
if identifier:
|
||||
nio = "nio_null:{}".format(identifier)
|
||||
if not nio in self._nios:
|
||||
if nio not in self._nios:
|
||||
self.uiNIONullListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _NIONullDeleteSlot(self):
|
||||
"""
|
||||
@@ -449,7 +521,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())
|
||||
@@ -480,7 +552,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()
|
||||
@@ -490,10 +562,10 @@ 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 = []
|
||||
self.nios = set()
|
||||
self.uiGenericEthernetListWidget.clear()
|
||||
self.uiLinuxEthernetListWidget.clear()
|
||||
self.uiNIOUDPListWidget.clear()
|
||||
@@ -503,7 +575,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiNIONullListWidget.clear()
|
||||
|
||||
for nio in settings["nios"]:
|
||||
self._nios.append(nio)
|
||||
self._nios.add(nio)
|
||||
if nio.lower().startswith("nio_gen_eth"):
|
||||
self.uiGenericEthernetListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_gen_linux"):
|
||||
|
||||
@@ -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,19 +15,19 @@
|
||||
</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>
|
||||
<widget class="QGroupBox" name="uiGenericEthernetGroupBox">
|
||||
<property name="title">
|
||||
<string>Generic Ethernet NIO (Administrator or root access required)</string>
|
||||
<string>Generic Ethernet NIO</string>
|
||||
</property>
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0" colspan="3">
|
||||
@@ -78,7 +78,7 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Linux Ethernet NIO (Linux only, root access required)</string>
|
||||
<string>Linux Ethernet NIO (Linux only)</string>
|
||||
</property>
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0" colspan="3">
|
||||
@@ -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,15 +360,15 @@
|
||||
</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>
|
||||
<widget class="QGroupBox" name="uiNIOTAPGroupBox">
|
||||
<property name="title">
|
||||
<string>TAP interface (require root access)</string>
|
||||
<string>TAP interface</string>
|
||||
</property>
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
@@ -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,421 +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 '/home/grossmj/PycharmProjects/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: Fri May 27 22:47:08 2016
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
#
|
||||
# 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"))
|
||||
self.uiAddGenericEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteGenericEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiLinuxEthernetGroupBox.setTitle(_translate("cloudConfigPageWidget", "Linux Ethernet NIO (Linux only)"))
|
||||
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"))
|
||||
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."))
|
||||
|
||||
|
||||
255
gns3/modules/docker/__init__.py
Normal file
255
gns3/modules/docker/__init__.py
Normal file
@@ -0,0 +1,255 @@
|
||||
# -*- 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_containers = {}
|
||||
self._nodes = []
|
||||
|
||||
# load the settings
|
||||
self._loadSettings()
|
||||
|
||||
def configChangedSlot(self):
|
||||
# load the settings
|
||||
self._loadSettings()
|
||||
|
||||
def _saveSettings(self):
|
||||
"""Saves the settings to the persistent settings file."""
|
||||
LocalConfig.instance().saveSectionSettings(
|
||||
self.__class__.__name__, self._settings)
|
||||
|
||||
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)
|
||||
|
||||
if "containers" in self._settings:
|
||||
for image in self._settings["containers"]:
|
||||
name = image.get("name")
|
||||
server = image.get("server")
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
if key in self._docker_containers or not name or not server:
|
||||
continue
|
||||
container_settings = DOCKER_CONTAINER_SETTINGS.copy()
|
||||
container_settings.update(image)
|
||||
self._docker_containers[key] = container_settings
|
||||
|
||||
def _saveDockerImages(self):
|
||||
"""Saves the Docker containers to the persistent settings file."""
|
||||
|
||||
self._settings["containers"] = list(self._docker_containers.values())
|
||||
self._saveSettings()
|
||||
|
||||
def VMs(self):
|
||||
"""
|
||||
Returns Docker images settings.
|
||||
|
||||
:returns: Docker images settings
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
return self._docker_containers
|
||||
|
||||
def setVMs(self, new_docker_containers):
|
||||
"""Sets Docker image settings.
|
||||
|
||||
:param new_iou_images: Docker images settings (dictionary)
|
||||
"""
|
||||
self._docker_containers = new_docker_containers.copy()
|
||||
self._saveDockerImages()
|
||||
|
||||
@staticmethod
|
||||
def vmConfigurationPage():
|
||||
from .pages.docker_vm_configuration_page import DockerVMConfigurationPage
|
||||
return DockerVMConfigurationPage
|
||||
|
||||
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_containers.items():
|
||||
if node_name == info["name"]:
|
||||
image = image_key
|
||||
if not image:
|
||||
selected_images = []
|
||||
for image, info in self._docker_containers.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_containers[image].items():
|
||||
if setting_name in node.settings() and value != "" and value is not None:
|
||||
if setting_name not in ['name', 'image']:
|
||||
image_settings[setting_name] = value
|
||||
|
||||
default_name_format = DOCKER_CONTAINER_SETTINGS["default_name_format"]
|
||||
if self._docker_containers[image]["default_name_format"]:
|
||||
default_name_format = self._docker_containers[image]["default_name_format"]
|
||||
|
||||
image = self._docker_containers[image]["image"]
|
||||
node.setup(image, base_name=node_name, additional_settings=image_settings, default_name_format=default_name_format)
|
||||
|
||||
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_containers.values():
|
||||
nodes.append({
|
||||
"class": DockerVM.__name__,
|
||||
"name": docker_image["name"],
|
||||
"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
|
||||
|
||||
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
|
||||
143
gns3/modules/docker/dialogs/docker_vm_wizard.py
Normal file
143
gns3/modules/docker/dialogs/docker_vm_wizard.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# -*- 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."""
|
||||
|
||||
import sys
|
||||
|
||||
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_containers: existing Docker images
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, docker_containers, parent):
|
||||
|
||||
super().__init__(docker_containers, Docker.instance().settings()["use_local_server"], parent)
|
||||
self._docker_containers = docker_containers
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/icons/docker.png"))
|
||||
|
||||
self.uiNewImageRadioButton.setChecked(True)
|
||||
self._existingImageRadioButtonToggledSlot(False)
|
||||
self.uiExistingImageRadioButton.toggled.connect(self._existingImageRadioButtonToggledSlot)
|
||||
|
||||
if sys.platform.startswith("win") or sys.platform.startswith("darwin"):
|
||||
# Cannot use Docker locally on Windows and Mac
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
|
||||
def _existingImageRadioButtonToggledSlot(self, status):
|
||||
if self.uiExistingImageRadioButton.isChecked():
|
||||
self.uiImageLineEdit.hide()
|
||||
self.uiImageNameLabel.hide()
|
||||
self.uiImageListLabel.show()
|
||||
self.uiImageListComboBox.show()
|
||||
else:
|
||||
self.uiImageNameLabel.show()
|
||||
self.uiImageLineEdit.show()
|
||||
self.uiImageListLabel.hide()
|
||||
self.uiImageListComboBox.hide()
|
||||
|
||||
def initializePage(self, page_id):
|
||||
|
||||
super().initializePage(page_id)
|
||||
|
||||
if self.page(page_id) == self.uiImageWizardPage:
|
||||
Docker.instance().getDockerImagesFromServer(self._server, 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()
|
||||
if len(result) == 0:
|
||||
self.uiNewImageRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiExistingImageRadioButton.setChecked(True)
|
||||
for image in result:
|
||||
self.uiImageListComboBox.addItem(image["image"], image)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""Validates the server."""
|
||||
|
||||
if super().validateCurrentPage() is False:
|
||||
return False
|
||||
|
||||
if self.currentPage() == self.uiImageWizardPage:
|
||||
if self.uiImageListComboBox.currentIndex() < 0 and self.uiExistingImageRadioButton.isChecked():
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self, "Docker images",
|
||||
"There are no Docker images selected!")
|
||||
return False
|
||||
self.uiNameLineEdit.setText(self._getImageName().split(":")[0])
|
||||
|
||||
if self.currentPage() == self.uiNameWizardPage:
|
||||
if self.uiNameLineEdit.text() in [ d["name"] for d in self._docker_containers.values() ]:
|
||||
QtWidgets.QMessageBox.critical(self, "Container name", "This name already exist!")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _getImageName(self):
|
||||
if self.uiExistingImageRadioButton.isChecked():
|
||||
index = self.uiImageListComboBox.currentIndex()
|
||||
return self.uiImageListComboBox.itemText(index)
|
||||
else:
|
||||
name = self.uiImageLineEdit.text()
|
||||
return name
|
||||
|
||||
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"
|
||||
|
||||
image = self._getImageName()
|
||||
start_command = self.uiStartCommandLineEdit.text()
|
||||
name = self.uiNameLineEdit.text()
|
||||
|
||||
settings = {
|
||||
"image": image,
|
||||
"server": server,
|
||||
"adapters": self.uiAdaptersSpinBox.value(),
|
||||
"name": name,
|
||||
"environment": self.uiEnvironmentTextEdit.toPlainText(),
|
||||
"start_command": start_command,
|
||||
"console_type": self.uiConsoleTypeComboBox.currentText()
|
||||
}
|
||||
return settings
|
||||
390
gns3/modules/docker/docker_vm.py
Normal file
390
gns3/modules/docker/docker_vm.py
Normal file
@@ -0,0 +1,390 @@
|
||||
# -*- 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 .settings import DOCKER_CONTAINER_SETTINGS
|
||||
|
||||
import logging
|
||||
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": "",
|
||||
"image": "",
|
||||
"adapters": DOCKER_CONTAINER_SETTINGS["adapters"],
|
||||
"start_command": DOCKER_CONTAINER_SETTINGS["start_command"],
|
||||
"environment": DOCKER_CONTAINER_SETTINGS["environment"],
|
||||
"console": None,
|
||||
"aux": None,
|
||||
"console_type": DOCKER_CONTAINER_SETTINGS["console_type"],
|
||||
"console_resolution": DOCKER_CONTAINER_SETTINGS["console_resolution"],
|
||||
"console_http_port": DOCKER_CONTAINER_SETTINGS["console_http_port"],
|
||||
"console_http_path": DOCKER_CONTAINER_SETTINGS["console_http_path"]
|
||||
}
|
||||
|
||||
def _addAdapters(self, adapters):
|
||||
"""Adds adapters.
|
||||
|
||||
:param adapters: number of adapters
|
||||
"""
|
||||
for adapter_number in range(0, adapters):
|
||||
adapter_name = "eth" + 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)
|
||||
new_port.setHotPluggable(True)
|
||||
new_port.setPacketCaptureSupported(True)
|
||||
self._ports.append(new_port)
|
||||
log.debug("Adapter {} has been added".format(adapter_name))
|
||||
|
||||
def setup(self, image, name=None, base_name=None, vm_id=None, additional_settings={}, default_name_format="{name}-{0}"):
|
||||
"""Sets up this Docker container.
|
||||
|
||||
:param image: 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 = self.allocateName(default_name_format.replace('{name}', base_name))
|
||||
|
||||
if not name:
|
||||
self.error_signal.emit(self.id(), "could not allocate a name for this container")
|
||||
return
|
||||
|
||||
self.setName(name)
|
||||
self._settings["name"] = name
|
||||
self._settings["image"] = image
|
||||
params = {
|
||||
"name": name,
|
||||
"image": image,
|
||||
"adapters": self._settings["adapters"]
|
||||
}
|
||||
if vm_id:
|
||||
params["vm_id"] = vm_id
|
||||
params.update(additional_settings)
|
||||
|
||||
self.httpPost("/docker/vms", self._setupCallback, body=params, timeout=None)
|
||||
|
||||
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 VPCS device.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
if "name" in new_settings and new_settings["name"] != self.name() and self.hasAllocatedName(new_settings["name"]):
|
||||
self.error_signal.emit(self.id(), 'Name "{}" is already used by another node'.format(new_settings["name"]))
|
||||
return
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
|
||||
log.debug("{} is updating settings: {}".format(self.name(), params))
|
||||
self.httpPut("/docker/vms/{vm_id}".format(project_id=self._project.id(), vm_id=self._vm_id), self._updateCallback, body=params)
|
||||
|
||||
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/vms/{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 dump(self):
|
||||
"""
|
||||
Returns a representation of this Docker VM instance.
|
||||
(to be saved in a topology file).
|
||||
|
||||
:returns: representation of the node (dictionary)
|
||||
"""
|
||||
docker = super().dump()
|
||||
docker["id"] = self.id()
|
||||
docker["vm_id"] = self._vm_id
|
||||
|
||||
# add the properties
|
||||
for name, value in self._settings.items():
|
||||
if value is not None and value != "":
|
||||
docker["properties"][name] = value
|
||||
|
||||
# add the ports
|
||||
if self._ports:
|
||||
ports = docker["ports"] = []
|
||||
for port in self._ports:
|
||||
ports.append(port.dump())
|
||||
return docker
|
||||
|
||||
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 Docker representation
|
||||
(from a topology file).
|
||||
|
||||
:param node_info: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
super().load(node_info)
|
||||
|
||||
settings = node_info["properties"]
|
||||
name = settings.pop("name")
|
||||
image = settings.pop("image")
|
||||
vm_id = node_info["vm_id"]
|
||||
log.info("Docker container {} is loading".format(name))
|
||||
self.setup(image, name=name, vm_id=vm_id, additional_settings=settings)
|
||||
|
||||
def _updatePortSettings(self):
|
||||
"""
|
||||
Updates port settings when loading a topology.
|
||||
"""
|
||||
|
||||
self.loaded_signal.disconnect(self._updatePortSettings)
|
||||
|
||||
# assign the correct names and IDs to the ports
|
||||
if "ports" in self._node_info:
|
||||
ports = self._node_info["ports"]
|
||||
for topology_port in ports:
|
||||
for port in self._ports:
|
||||
adapter_number = topology_port.get("adapter_number")
|
||||
if adapter_number == port.adapterNumber():
|
||||
port.setName(topology_port["name"])
|
||||
port.setId(topology_port["id"])
|
||||
|
||||
# now we can set the node as initialized and trigger the created signal
|
||||
self.setInitialized(True)
|
||||
log.info("Docker container {} 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):
|
||||
"""
|
||||
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 console(self):
|
||||
"""
|
||||
Returns the console port for this Docker VM instance.
|
||||
|
||||
:returns: port (integer)
|
||||
"""
|
||||
return self._settings["console"]
|
||||
|
||||
def consoleHttpPath(self):
|
||||
"""
|
||||
Returns the path of the web ui
|
||||
|
||||
:returns: string
|
||||
"""
|
||||
return self._settings["console_http_path"]
|
||||
|
||||
def auxConsole(self):
|
||||
"""
|
||||
Returns the console port for this Docker VM instance.
|
||||
|
||||
:returns: port (integer)
|
||||
"""
|
||||
return self._settings["aux"]
|
||||
|
||||
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.svg"
|
||||
|
||||
def networkInterfacesPath(self):
|
||||
"""
|
||||
Return path of the /etc/network/interfaces
|
||||
"""
|
||||
return "/project-files/docker/{}/etc/network/interfaces".format(self._vm_id)
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
return "Docker container"
|
||||
|
||||
@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 container"
|
||||
|
||||
|
||||
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)
|
||||
182
gns3/modules/docker/pages/docker_vm_configuration_page.py
Normal file
182
gns3/modules/docker/pages/docker_vm_configuration_page.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# -*- 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, QtGui
|
||||
|
||||
from ..ui.docker_vm_configuration_page_ui import Ui_dockerVMConfigPageWidget
|
||||
from ....dialogs.file_editor_dialog import FileEditorDialog
|
||||
from ....dialogs.node_properties_dialog import ConfigurationError
|
||||
from ....dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
|
||||
|
||||
class DockerVMConfigurationPage(
|
||||
QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
|
||||
"""QWidget configuration page for Docker images."""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
self.uiNetworkConfigEditButton.released.connect(self._networkConfigEditSlot)
|
||||
|
||||
def _symbolBrowserSlot(self):
|
||||
"""
|
||||
Slot to open the symbol browser and select a new symbol.
|
||||
"""
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
new_symbol_path = dialog.getSymbol()
|
||||
self.uiSymbolLineEdit.setText(new_symbol_path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
|
||||
|
||||
def loadSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Loads the Docker VM settings.
|
||||
|
||||
:param settings: the settings (dictionary)
|
||||
:param node: Node instance
|
||||
:param group: indicates the settings apply to a group of images
|
||||
"""
|
||||
|
||||
self.uiCMDLineEdit.setText(settings["start_command"])
|
||||
self.uiEnvironmentTextEdit.setText(settings["environment"])
|
||||
self.uiConsoleTypeComboBox.setCurrentIndex(self.uiConsoleTypeComboBox.findText(settings["console_type"]))
|
||||
self.uiConsoleResolutionComboBox.setCurrentIndex(self.uiConsoleResolutionComboBox.findText(settings["console_resolution"]))
|
||||
self.uiConsoleHttpPortSpinBox.setValue(settings["console_http_port"])
|
||||
self.uiHttpConsolePathLineEdit.setText(settings["console_http_path"])
|
||||
|
||||
if not group:
|
||||
self.uiNameLineEdit.setText(settings["name"])
|
||||
self.uiAdapterSpinBox.setValue(settings["adapters"])
|
||||
else:
|
||||
self.uiNameLabel.hide()
|
||||
self.uiNameLineEdit.hide()
|
||||
self.uiCMDLabel.hide()
|
||||
self.uiCMDLineEdit.hide()
|
||||
self.uiAdapterLabel.hide()
|
||||
self.uiAdapterSpinBox.hide()
|
||||
self.uiConsolePortLabel.hide()
|
||||
self.uiConsolePortSpinBox.hide()
|
||||
self.uiAuxPortLabel.hide()
|
||||
self.uiAuxPortSpinBox.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# rename the label from "Name" to "Template name"
|
||||
self.uiNameLabel.setText("Template name:")
|
||||
|
||||
# load the default name format
|
||||
self.uiDefaultNameFormatLineEdit.setText(settings["default_name_format"])
|
||||
|
||||
# load the symbol
|
||||
self.uiSymbolLineEdit.setText(settings["symbol"])
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(settings["symbol"]))
|
||||
|
||||
self.uiCategoryComboBox.setCurrentIndex(settings["category"])
|
||||
self.uiConsolePortLabel.hide()
|
||||
self.uiConsolePortSpinBox.hide()
|
||||
self.uiAuxPortLabel.hide()
|
||||
self.uiAuxPortSpinBox.hide()
|
||||
self.uiNetworkConfigEditButton.hide()
|
||||
self.uiNetworkConfigLabel.hide()
|
||||
else:
|
||||
self._node = node
|
||||
self.uiConsolePortSpinBox.setValue(settings["console"])
|
||||
self.uiAuxPortSpinBox.setValue(settings["aux"])
|
||||
self.uiCategoryComboBox.hide()
|
||||
self.uiCategoryLabel.hide()
|
||||
|
||||
self.uiDefaultNameFormatLabel.hide()
|
||||
self.uiDefaultNameFormatLineEdit.hide()
|
||||
|
||||
self.uiSymbolLabel.hide()
|
||||
self.uiSymbolLineEdit.hide()
|
||||
self.uiSymbolToolButton.hide()
|
||||
|
||||
def _networkConfigEditSlot(self):
|
||||
dialog = FileEditorDialog(self._node, self._node.networkInterfacesPath())
|
||||
dialog.setModal(True)
|
||||
self.stackUnder(dialog)
|
||||
dialog.show()
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
settings["start_command"] = self.uiCMDLineEdit.text()
|
||||
settings["environment"] = self.uiEnvironmentTextEdit.toPlainText()
|
||||
settings["console_type"] = self.uiConsoleTypeComboBox.currentText()
|
||||
settings["console_resolution"] = self.uiConsoleResolutionComboBox.currentText()
|
||||
settings["console_http_port"] = self.uiConsoleHttpPortSpinBox.value()
|
||||
settings["console_http_path"] = self.uiHttpConsolePathLineEdit.text()
|
||||
|
||||
if not group:
|
||||
adapters = self.uiAdapterSpinBox.value()
|
||||
if node:
|
||||
if settings["adapters"] != adapters:
|
||||
# check if the adapters settings have changed
|
||||
node_ports = node.ports()
|
||||
for node_port in node_ports:
|
||||
if not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, node.name(), "Changing the number of adapters while links are connected isn't supported yet! Please delete all the links first.")
|
||||
raise ConfigurationError()
|
||||
|
||||
settings["adapters"] = adapters
|
||||
|
||||
name = self.uiNameLineEdit.text()
|
||||
if not name:
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "Docker name cannot be empty!")
|
||||
else:
|
||||
settings["name"] = name
|
||||
|
||||
|
||||
if not node:
|
||||
# these are template settings
|
||||
settings["category"] = self.uiCategoryComboBox.currentIndex()
|
||||
|
||||
# save the default name format
|
||||
default_name_format = self.uiDefaultNameFormatLineEdit.text().strip()
|
||||
if '{0}' not in default_name_format and '{id}' not in default_name_format:
|
||||
QtWidgets.QMessageBox.critical(self, "Default name format", "The default name format must contain at least {0} or {id}")
|
||||
else:
|
||||
settings["default_name_format"] = default_name_format
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
pixmap = QtGui.QPixmap(symbol_path)
|
||||
if pixmap.isNull():
|
||||
QtWidgets.QMessageBox.critical(self, "Symbol", "Invalid file or format not supported")
|
||||
else:
|
||||
settings["symbol"] = symbol_path
|
||||
else:
|
||||
settings["console"] = self.uiConsolePortSpinBox.value()
|
||||
settings["aux"] = self.uiAuxPortSpinBox.value()
|
||||
|
||||
|
||||
189
gns3/modules/docker/pages/docker_vm_preferences_page.py
Normal file
189
gns3/modules/docker/pages/docker_vm_preferences_page.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# -*- 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.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_containers = {}
|
||||
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)
|
||||
|
||||
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["image"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", str(docker_image["server"])])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Console type:", str(docker_image["console_type"])])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", docker_image["default_name_format"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Adapters:", str(docker_image["adapters"])])
|
||||
if docker_image["start_command"]:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Start command:", str(docker_image["start_command"])])
|
||||
if docker_image["environment"]:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Environment:", str(docker_image["environment"])])
|
||||
|
||||
self.uiDockerVMInfoTreeWidget.expandAll()
|
||||
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(0)
|
||||
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(1)
|
||||
self.uiDockerVMsTreeWidget.setMaximumWidth(self.uiDockerVMsTreeWidget.sizeHintForColumn(0) + 10)
|
||||
|
||||
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_containers[key]
|
||||
self._refreshInfo(docker_image)
|
||||
else:
|
||||
self.uiDockerVMInfoTreeWidget.clear()
|
||||
|
||||
def _dockerImageNewSlot(self):
|
||||
"""
|
||||
Creates a new Docker image.
|
||||
"""
|
||||
|
||||
wizard = DockerVMWizard(self._docker_containers, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_image_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_image_settings["server"], name=new_image_settings["name"])
|
||||
self._docker_containers[key] = DOCKER_CONTAINER_SETTINGS.copy()
|
||||
self._docker_containers[key].update(new_image_settings)
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiDockerVMsTreeWidget)
|
||||
item.setText(0, self._docker_containers[key]["name"])
|
||||
item.setIcon(
|
||||
0, QtGui.QIcon(self._docker_containers[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_containers[key]
|
||||
dialog = ConfigurationDialog(docker_image["name"], docker_image, DockerVMConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
item.setIcon(0, QtGui.QIcon(docker_image["symbol"]))
|
||||
if docker_image["name"] != item.text(0):
|
||||
new_key = "{server}:{name}".format(
|
||||
server=docker_image["server"],
|
||||
name=docker_image["name"])
|
||||
if new_key in self._docker_containers:
|
||||
QtWidgets.QMessageBox.critical(self, "Docker image", "Docker container name {} already exists for server {}".format(docker_image["name"],
|
||||
docker_image["server"]))
|
||||
docker_image["name"] = item.text(0)
|
||||
return
|
||||
self._docker_containers[new_key] = self._docker_containers[key]
|
||||
del self._docker_containers[key]
|
||||
item.setText(0, docker_image["name"])
|
||||
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_containers[key]
|
||||
self.uiDockerVMsTreeWidget.takeTopLevelItem(
|
||||
self.uiDockerVMsTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
def loadPreferences(self):
|
||||
"""
|
||||
Loads the Docker VM preferences.
|
||||
"""
|
||||
|
||||
docker_module = Docker.instance()
|
||||
self._docker_containers = copy.deepcopy(docker_module.VMs())
|
||||
self._items.clear()
|
||||
|
||||
for key, docker_image in self._docker_containers.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiDockerVMsTreeWidget)
|
||||
item.setText(0, docker_image["name"])
|
||||
icon = QtGui.QIcon()
|
||||
icon.addPixmap(QtGui.QPixmap(docker_image["symbol"]))
|
||||
item.setIcon(0, icon)
|
||||
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)
|
||||
self.uiDockerVMsTreeWidget.setMaximumWidth(self.uiDockerVMsTreeWidget.sizeHintForColumn(0) + 10)
|
||||
|
||||
def savePreferences(self):
|
||||
"""
|
||||
Saves the Docker image preferences.
|
||||
"""
|
||||
|
||||
Docker.instance().setVMs(self._docker_containers)
|
||||
43
gns3/modules/docker/settings.py
Normal file
43
gns3/modules/docker/settings.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- 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 = {
|
||||
"use_local_server": sys.platform.startswith("linux"), # Docker only supported on Linux
|
||||
"containers": []
|
||||
}
|
||||
|
||||
DOCKER_CONTAINER_SETTINGS = {
|
||||
"default_name_format": "{name}-{0}",
|
||||
"symbol": ":/symbols/docker_guest.svg",
|
||||
"category": Node.end_devices,
|
||||
"start_command": "",
|
||||
"name": "",
|
||||
"image": "",
|
||||
"adapters": 1,
|
||||
"environment": "",
|
||||
"console_type": "telnet",
|
||||
"console_resolution": "1024x768",
|
||||
"console_http_port": 80,
|
||||
"console_http_path": "/"
|
||||
}
|
||||
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>330</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>5</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 '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/docker/ui/docker_preferences_page.ui'
|
||||
#
|
||||
# Created: Thu May 5 18:51:18 2016
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
#
|
||||
# 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(330, 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, 5, 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"))
|
||||
|
||||
315
gns3/modules/docker/ui/docker_vm_configuration_page.ui
Normal file
315
gns3/modules/docker/ui/docker_vm_configuration_page.ui
Normal file
@@ -0,0 +1,315 @@
|
||||
<?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>656</width>
|
||||
<height>605</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="1">
|
||||
<widget class="QLineEdit" name="uiNameLineEdit"/>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="uiConsoleTypeLabel">
|
||||
<property name="text">
|
||||
<string>Console type:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="uiAdapterLabel">
|
||||
<property name="text">
|
||||
<string>Adapters:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="uiCMDLineEdit"/>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>HTTP port in the container:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="1">
|
||||
<widget class="QSpinBox" name="uiAuxPortSpinBox">
|
||||
<property name="minimum">
|
||||
<number>1024</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>65535</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QSpinBox" name="uiConsoleHttpPortSpinBox">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>65535</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="uiSymbolLineEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="uiSymbolToolButton">
|
||||
<property name="text">
|
||||
<string>&Browse...</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextOnly</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QSpinBox" name="uiConsolePortSpinBox">
|
||||
<property name="minimum">
|
||||
<number>1024</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>65535</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiDefaultNameFormatLabel">
|
||||
<property name="text">
|
||||
<string>Default name format</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="0">
|
||||
<widget class="QLabel" name="uiEnvironmentLabel">
|
||||
<property name="text">
|
||||
<string>Environment (KEY=VALUE one by line):</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="1">
|
||||
<widget class="QPushButton" name="uiNetworkConfigEditButton">
|
||||
<property name="text">
|
||||
<string>Edit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QComboBox" name="uiConsoleTypeComboBox">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>telnet</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>vnc</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>http</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>https</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="0">
|
||||
<widget class="QLabel" name="uiNetworkConfigLabel">
|
||||
<property name="text">
|
||||
<string>Network configuration</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="uiDefaultNameFormatLineEdit"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiNameLabel">
|
||||
<property name="text">
|
||||
<string>Name:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QSpinBox" name="uiAdapterSpinBox">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="uiConsolePortLabel">
|
||||
<property name="text">
|
||||
<string>Console port:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="1">
|
||||
<widget class="QTextEdit" name="uiEnvironmentTextEdit"/>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="uiCategoryComboBox">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Router</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Switch</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Guest</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Security device</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="uiCategoryLabel">
|
||||
<property name="text">
|
||||
<string>Category</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="uiCMDLabel">
|
||||
<property name="text">
|
||||
<string>Start command:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QComboBox" name="uiConsoleResolutionComboBox">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1920x1080</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1366x768</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1280x1024</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1280x800</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1024x768</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>800x600</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>640x480</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="uiSymbolLabel">
|
||||
<property name="text">
|
||||
<string>Symbol:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="uiConsoleResolutionLabel">
|
||||
<property name="text">
|
||||
<string>Console resolution (for VNC):</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="0">
|
||||
<widget class="QLabel" name="uiAuxPortLabel">
|
||||
<property name="text">
|
||||
<string>Aux port:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>HTTP path:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<widget class="QLineEdit" name="uiHttpConsolePathLineEdit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
182
gns3/modules/docker/ui/docker_vm_configuration_page_ui.py
Normal file
182
gns3/modules/docker/ui/docker_vm_configuration_page_ui.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/docker/ui/docker_vm_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.5.1
|
||||
#
|
||||
# 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(656, 605)
|
||||
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.uiNameLineEdit = QtWidgets.QLineEdit(self.tab)
|
||||
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
|
||||
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 1, 1, 1)
|
||||
self.uiConsoleTypeLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiConsoleTypeLabel.setObjectName("uiConsoleTypeLabel")
|
||||
self.gridLayout.addWidget(self.uiConsoleTypeLabel, 7, 0, 1, 1)
|
||||
self.uiAdapterLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiAdapterLabel.setObjectName("uiAdapterLabel")
|
||||
self.gridLayout.addWidget(self.uiAdapterLabel, 5, 0, 1, 1)
|
||||
self.uiCMDLineEdit = QtWidgets.QLineEdit(self.tab)
|
||||
self.uiCMDLineEdit.setObjectName("uiCMDLineEdit")
|
||||
self.gridLayout.addWidget(self.uiCMDLineEdit, 4, 1, 1, 1)
|
||||
self.label = QtWidgets.QLabel(self.tab)
|
||||
self.label.setObjectName("label")
|
||||
self.gridLayout.addWidget(self.label, 9, 0, 1, 1)
|
||||
self.uiAuxPortSpinBox = QtWidgets.QSpinBox(self.tab)
|
||||
self.uiAuxPortSpinBox.setMinimum(1024)
|
||||
self.uiAuxPortSpinBox.setMaximum(65535)
|
||||
self.uiAuxPortSpinBox.setObjectName("uiAuxPortSpinBox")
|
||||
self.gridLayout.addWidget(self.uiAuxPortSpinBox, 11, 1, 1, 1)
|
||||
self.uiConsoleHttpPortSpinBox = QtWidgets.QSpinBox(self.tab)
|
||||
self.uiConsoleHttpPortSpinBox.setMinimum(1)
|
||||
self.uiConsoleHttpPortSpinBox.setMaximum(65535)
|
||||
self.uiConsoleHttpPortSpinBox.setObjectName("uiConsoleHttpPortSpinBox")
|
||||
self.gridLayout.addWidget(self.uiConsoleHttpPortSpinBox, 9, 1, 1, 1)
|
||||
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
|
||||
self.uiSymbolLineEdit = QtWidgets.QLineEdit(self.tab)
|
||||
self.uiSymbolLineEdit.setObjectName("uiSymbolLineEdit")
|
||||
self.horizontalLayout_7.addWidget(self.uiSymbolLineEdit)
|
||||
self.uiSymbolToolButton = QtWidgets.QToolButton(self.tab)
|
||||
self.uiSymbolToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
|
||||
self.uiSymbolToolButton.setObjectName("uiSymbolToolButton")
|
||||
self.horizontalLayout_7.addWidget(self.uiSymbolToolButton)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_7, 3, 1, 1, 1)
|
||||
self.uiConsolePortSpinBox = QtWidgets.QSpinBox(self.tab)
|
||||
self.uiConsolePortSpinBox.setMinimum(1024)
|
||||
self.uiConsolePortSpinBox.setMaximum(65535)
|
||||
self.uiConsolePortSpinBox.setObjectName("uiConsolePortSpinBox")
|
||||
self.gridLayout.addWidget(self.uiConsolePortSpinBox, 6, 1, 1, 1)
|
||||
self.uiDefaultNameFormatLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiDefaultNameFormatLabel.setObjectName("uiDefaultNameFormatLabel")
|
||||
self.gridLayout.addWidget(self.uiDefaultNameFormatLabel, 1, 0, 1, 1)
|
||||
self.uiEnvironmentLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiEnvironmentLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
|
||||
self.uiEnvironmentLabel.setWordWrap(False)
|
||||
self.uiEnvironmentLabel.setObjectName("uiEnvironmentLabel")
|
||||
self.gridLayout.addWidget(self.uiEnvironmentLabel, 12, 0, 1, 1)
|
||||
self.uiNetworkConfigEditButton = QtWidgets.QPushButton(self.tab)
|
||||
self.uiNetworkConfigEditButton.setObjectName("uiNetworkConfigEditButton")
|
||||
self.gridLayout.addWidget(self.uiNetworkConfigEditButton, 13, 1, 1, 1)
|
||||
self.uiConsoleTypeComboBox = QtWidgets.QComboBox(self.tab)
|
||||
self.uiConsoleTypeComboBox.setObjectName("uiConsoleTypeComboBox")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.gridLayout.addWidget(self.uiConsoleTypeComboBox, 7, 1, 1, 1)
|
||||
self.uiNetworkConfigLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiNetworkConfigLabel.setObjectName("uiNetworkConfigLabel")
|
||||
self.gridLayout.addWidget(self.uiNetworkConfigLabel, 13, 0, 1, 1)
|
||||
self.uiDefaultNameFormatLineEdit = QtWidgets.QLineEdit(self.tab)
|
||||
self.uiDefaultNameFormatLineEdit.setObjectName("uiDefaultNameFormatLineEdit")
|
||||
self.gridLayout.addWidget(self.uiDefaultNameFormatLineEdit, 1, 1, 1, 1)
|
||||
self.uiNameLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiNameLabel.setObjectName("uiNameLabel")
|
||||
self.gridLayout.addWidget(self.uiNameLabel, 0, 0, 1, 1)
|
||||
self.uiAdapterSpinBox = QtWidgets.QSpinBox(self.tab)
|
||||
self.uiAdapterSpinBox.setMinimum(1)
|
||||
self.uiAdapterSpinBox.setObjectName("uiAdapterSpinBox")
|
||||
self.gridLayout.addWidget(self.uiAdapterSpinBox, 5, 1, 1, 1)
|
||||
self.uiConsolePortLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiConsolePortLabel.setObjectName("uiConsolePortLabel")
|
||||
self.gridLayout.addWidget(self.uiConsolePortLabel, 6, 0, 1, 1)
|
||||
self.uiEnvironmentTextEdit = QtWidgets.QTextEdit(self.tab)
|
||||
self.uiEnvironmentTextEdit.setObjectName("uiEnvironmentTextEdit")
|
||||
self.gridLayout.addWidget(self.uiEnvironmentTextEdit, 12, 1, 1, 1)
|
||||
self.uiCategoryComboBox = QtWidgets.QComboBox(self.tab)
|
||||
self.uiCategoryComboBox.setObjectName("uiCategoryComboBox")
|
||||
self.uiCategoryComboBox.addItem("")
|
||||
self.uiCategoryComboBox.addItem("")
|
||||
self.uiCategoryComboBox.addItem("")
|
||||
self.uiCategoryComboBox.addItem("")
|
||||
self.gridLayout.addWidget(self.uiCategoryComboBox, 2, 1, 1, 1)
|
||||
self.uiCategoryLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiCategoryLabel.setObjectName("uiCategoryLabel")
|
||||
self.gridLayout.addWidget(self.uiCategoryLabel, 2, 0, 1, 1)
|
||||
self.uiCMDLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiCMDLabel.setObjectName("uiCMDLabel")
|
||||
self.gridLayout.addWidget(self.uiCMDLabel, 4, 0, 1, 1)
|
||||
self.uiConsoleResolutionComboBox = QtWidgets.QComboBox(self.tab)
|
||||
self.uiConsoleResolutionComboBox.setObjectName("uiConsoleResolutionComboBox")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.gridLayout.addWidget(self.uiConsoleResolutionComboBox, 8, 1, 1, 1)
|
||||
self.uiSymbolLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiSymbolLabel.setObjectName("uiSymbolLabel")
|
||||
self.gridLayout.addWidget(self.uiSymbolLabel, 3, 0, 1, 1)
|
||||
self.uiConsoleResolutionLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiConsoleResolutionLabel.setObjectName("uiConsoleResolutionLabel")
|
||||
self.gridLayout.addWidget(self.uiConsoleResolutionLabel, 8, 0, 1, 1)
|
||||
self.uiAuxPortLabel = QtWidgets.QLabel(self.tab)
|
||||
self.uiAuxPortLabel.setObjectName("uiAuxPortLabel")
|
||||
self.gridLayout.addWidget(self.uiAuxPortLabel, 11, 0, 1, 1)
|
||||
self.label_2 = QtWidgets.QLabel(self.tab)
|
||||
self.label_2.setObjectName("label_2")
|
||||
self.gridLayout.addWidget(self.label_2, 10, 0, 1, 1)
|
||||
self.uiHttpConsolePathLineEdit = QtWidgets.QLineEdit(self.tab)
|
||||
self.uiHttpConsolePathLineEdit.setObjectName("uiHttpConsolePathLineEdit")
|
||||
self.gridLayout.addWidget(self.uiHttpConsolePathLineEdit, 10, 1, 1, 1)
|
||||
self.uiTabWidget.addTab(self.tab, "")
|
||||
self.verticalLayout.addWidget(self.uiTabWidget)
|
||||
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout.addItem(spacerItem)
|
||||
|
||||
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.uiConsoleTypeLabel.setText(_translate("dockerVMConfigPageWidget", "Console type:"))
|
||||
self.uiAdapterLabel.setText(_translate("dockerVMConfigPageWidget", "Adapters:"))
|
||||
self.label.setText(_translate("dockerVMConfigPageWidget", "HTTP port in the container:"))
|
||||
self.uiSymbolToolButton.setText(_translate("dockerVMConfigPageWidget", "&Browse..."))
|
||||
self.uiDefaultNameFormatLabel.setText(_translate("dockerVMConfigPageWidget", "Default name format"))
|
||||
self.uiEnvironmentLabel.setText(_translate("dockerVMConfigPageWidget", "Environment (KEY=VALUE one by line):"))
|
||||
self.uiNetworkConfigEditButton.setText(_translate("dockerVMConfigPageWidget", "Edit"))
|
||||
self.uiConsoleTypeComboBox.setItemText(0, _translate("dockerVMConfigPageWidget", "telnet"))
|
||||
self.uiConsoleTypeComboBox.setItemText(1, _translate("dockerVMConfigPageWidget", "vnc"))
|
||||
self.uiConsoleTypeComboBox.setItemText(2, _translate("dockerVMConfigPageWidget", "http"))
|
||||
self.uiConsoleTypeComboBox.setItemText(3, _translate("dockerVMConfigPageWidget", "https"))
|
||||
self.uiNetworkConfigLabel.setText(_translate("dockerVMConfigPageWidget", "Network configuration"))
|
||||
self.uiNameLabel.setText(_translate("dockerVMConfigPageWidget", "Name:"))
|
||||
self.uiConsolePortLabel.setText(_translate("dockerVMConfigPageWidget", "Console port:"))
|
||||
self.uiCategoryComboBox.setItemText(0, _translate("dockerVMConfigPageWidget", "Router"))
|
||||
self.uiCategoryComboBox.setItemText(1, _translate("dockerVMConfigPageWidget", "Switch"))
|
||||
self.uiCategoryComboBox.setItemText(2, _translate("dockerVMConfigPageWidget", "Guest"))
|
||||
self.uiCategoryComboBox.setItemText(3, _translate("dockerVMConfigPageWidget", "Security device"))
|
||||
self.uiCategoryLabel.setText(_translate("dockerVMConfigPageWidget", "Category"))
|
||||
self.uiCMDLabel.setText(_translate("dockerVMConfigPageWidget", "Start command:"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(0, _translate("dockerVMConfigPageWidget", "1920x1080"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(1, _translate("dockerVMConfigPageWidget", "1366x768"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(2, _translate("dockerVMConfigPageWidget", "1280x1024"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(3, _translate("dockerVMConfigPageWidget", "1280x800"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(4, _translate("dockerVMConfigPageWidget", "1024x768"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(5, _translate("dockerVMConfigPageWidget", "800x600"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(6, _translate("dockerVMConfigPageWidget", "640x480"))
|
||||
self.uiSymbolLabel.setText(_translate("dockerVMConfigPageWidget", "Symbol:"))
|
||||
self.uiConsoleResolutionLabel.setText(_translate("dockerVMConfigPageWidget", "Console resolution (for VNC):"))
|
||||
self.uiAuxPortLabel.setText(_translate("dockerVMConfigPageWidget", "Aux port:"))
|
||||
self.label_2.setText(_translate("dockerVMConfigPageWidget", "HTTP path:"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab), _translate("dockerVMConfigPageWidget", "General settings"))
|
||||
|
||||
160
gns3/modules/docker/ui/docker_vm_preferences_page.ui
Normal file
160
gns3/modules/docker/ui/docker_vm_preferences_page.ui
Normal file
@@ -0,0 +1,160 @@
|
||||
<?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>546</width>
|
||||
<height>455</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="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QTreeWidget" name="uiDockerVMsTreeWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" 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>
|
||||
<widget class="QWidget" name="">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<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>
|
||||
<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>
|
||||
</layout>
|
||||
</widget>
|
||||
</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>
|
||||
85
gns3/modules/docker/ui/docker_vm_preferences_page_ui.py
Normal file
85
gns3/modules/docker/ui/docker_vm_preferences_page_ui.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/docker/ui/docker_vm_preferences_page.ui'
|
||||
#
|
||||
# Created: Sun Mar 27 12:03:40 2016
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
#
|
||||
# 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(546, 455)
|
||||
DockerVMPreferencesPageWidget.setAccessibleDescription("")
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout(DockerVMPreferencesPageWidget)
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.splitter = QtWidgets.QSplitter(DockerVMPreferencesPageWidget)
|
||||
self.splitter.setOrientation(QtCore.Qt.Horizontal)
|
||||
self.splitter.setObjectName("splitter")
|
||||
self.uiDockerVMsTreeWidget = QtWidgets.QTreeWidget(self.splitter)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, 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.widget = QtWidgets.QWidget(self.splitter)
|
||||
self.widget.setObjectName("widget")
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(self.widget)
|
||||
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiDockerVMInfoTreeWidget = QtWidgets.QTreeWidget(self.widget)
|
||||
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.verticalLayout.addWidget(self.uiDockerVMInfoTreeWidget)
|
||||
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
|
||||
self.uiNewDockerVMPushButton = QtWidgets.QPushButton(self.widget)
|
||||
self.uiNewDockerVMPushButton.setObjectName("uiNewDockerVMPushButton")
|
||||
self.horizontalLayout_5.addWidget(self.uiNewDockerVMPushButton)
|
||||
self.uiEditDockerVMPushButton = QtWidgets.QPushButton(self.widget)
|
||||
self.uiEditDockerVMPushButton.setEnabled(False)
|
||||
self.uiEditDockerVMPushButton.setObjectName("uiEditDockerVMPushButton")
|
||||
self.horizontalLayout_5.addWidget(self.uiEditDockerVMPushButton)
|
||||
self.uiDeleteDockerVMPushButton = QtWidgets.QPushButton(self.widget)
|
||||
self.uiDeleteDockerVMPushButton.setEnabled(False)
|
||||
self.uiDeleteDockerVMPushButton.setObjectName("uiDeleteDockerVMPushButton")
|
||||
self.horizontalLayout_5.addWidget(self.uiDeleteDockerVMPushButton)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout_5)
|
||||
self.verticalLayout_2.addWidget(self.splitter)
|
||||
|
||||
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.uiDockerVMInfoTreeWidget.headerItem().setText(0, _translate("DockerVMPreferencesPageWidget", "1"))
|
||||
self.uiDockerVMInfoTreeWidget.headerItem().setText(1, _translate("DockerVMPreferencesPageWidget", "2"))
|
||||
self.uiNewDockerVMPushButton.setText(_translate("DockerVMPreferencesPageWidget", "&New"))
|
||||
self.uiEditDockerVMPushButton.setText(_translate("DockerVMPreferencesPageWidget", "&Edit"))
|
||||
self.uiDeleteDockerVMPushButton.setText(_translate("DockerVMPreferencesPageWidget", "&Delete"))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user