mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-05-30 07:20:31 +03:00
Compare commits
1219 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e172fc7e3 | ||
|
|
8ed8a2c115 | ||
|
|
073665a75d | ||
|
|
4ccc67aa46 | ||
|
|
6e2632e91f | ||
|
|
38ddcde902 | ||
|
|
436563afcb | ||
|
|
7eaab3e38b | ||
|
|
0927a2a8c9 | ||
|
|
87e6159ff6 | ||
|
|
effdcf5e24 | ||
|
|
021cdd2e65 | ||
|
|
363c4a9966 | ||
|
|
7082c75511 | ||
|
|
16c4a837d7 | ||
|
|
fd42ac410c | ||
|
|
6efc177804 | ||
|
|
6f9e6c9b92 | ||
|
|
0411c68150 | ||
|
|
4246e731e5 | ||
|
|
50cca71279 | ||
|
|
c78ef8f348 | ||
|
|
c03a5a9e0a | ||
|
|
c93b7836d8 | ||
|
|
690b22cc24 | ||
|
|
3560251816 | ||
|
|
90e861289f | ||
|
|
6e144d6122 | ||
|
|
2f168193d1 | ||
|
|
0fe5559564 | ||
|
|
37f0744d7c | ||
|
|
000f4a4790 | ||
|
|
a09b7d6738 | ||
|
|
a1fa8f9ec2 | ||
|
|
cd6b0b793e | ||
|
|
6453932421 | ||
|
|
37a23d9682 | ||
|
|
e18e10c701 | ||
|
|
a9240e2e46 | ||
|
|
4df0c33013 | ||
|
|
4096316ceb | ||
|
|
e09292c647 | ||
|
|
31c37161fa | ||
|
|
a047cd7f4c | ||
|
|
87cde665a8 | ||
|
|
6361c94bbe | ||
|
|
8a0aeff0bb | ||
|
|
508f8b3ad5 | ||
|
|
bfe942c029 | ||
|
|
b3a86594ff | ||
|
|
debe88bd37 | ||
|
|
a948fd07b1 | ||
|
|
072f714e21 | ||
|
|
a2059d3e7c | ||
|
|
c4cc819d50 | ||
|
|
0796d9aae5 | ||
|
|
c8148c1877 | ||
|
|
d6021afa0f | ||
|
|
9018bdac07 | ||
|
|
a48b898d92 | ||
|
|
949c1bbe37 | ||
|
|
0089edecc0 | ||
|
|
91da1d492b | ||
|
|
0242f53c17 | ||
|
|
bab7a1016f | ||
|
|
2ec91e1ef7 | ||
|
|
38af30ec15 | ||
|
|
3f8a0cb527 | ||
|
|
fd1a86df03 | ||
|
|
c954a16ead | ||
|
|
9418b997eb | ||
|
|
9682cad4f4 | ||
|
|
60ee9e1374 | ||
|
|
dbd5b9366a | ||
|
|
f63314d4d0 | ||
|
|
96eaec0100 | ||
|
|
5d554976e2 | ||
|
|
d1d8390b73 | ||
|
|
5f3f6462c2 | ||
|
|
821809bf6f | ||
|
|
a712fab7a4 | ||
|
|
4ab21ea9f9 | ||
|
|
1a4902279b | ||
|
|
35412171b9 | ||
|
|
73939847b9 | ||
|
|
5acfc5bc56 | ||
|
|
d67d0d146f | ||
|
|
23da6a4c31 | ||
|
|
fd5f999756 | ||
|
|
c05304b86f | ||
|
|
6361980591 | ||
|
|
d5ea98ba2a | ||
|
|
3ca01384c9 | ||
|
|
f35bb6a281 | ||
|
|
0c4a7693e6 | ||
|
|
20506525c5 | ||
|
|
bb00a9f64c | ||
|
|
ef36792379 | ||
|
|
873fd409bd | ||
|
|
03221e8ab7 | ||
|
|
55f9836dc9 | ||
|
|
3ab5144fe2 | ||
|
|
c91a22c9f8 | ||
|
|
a8ba909568 | ||
|
|
d33e9ed833 | ||
|
|
aa31af1dca | ||
|
|
4d07a7391f | ||
|
|
417395718d | ||
|
|
212048c4d1 | ||
|
|
11c27063b4 | ||
|
|
b8696fa54e | ||
|
|
ba9785f83f | ||
|
|
4ae742529c | ||
|
|
92cc335708 | ||
|
|
2099a4ae9a | ||
|
|
5038a72610 | ||
|
|
b9e320996b | ||
|
|
9c86cd71c8 | ||
|
|
7fdf42b442 | ||
|
|
61d86e919e | ||
|
|
bc71985ee3 | ||
|
|
6bebf2d14d | ||
|
|
490021aa47 | ||
|
|
c8db5e7e49 | ||
|
|
8efaacbc3d | ||
|
|
c5da24b954 | ||
|
|
4456cfd68e | ||
|
|
3468909db1 | ||
|
|
212728eb94 | ||
|
|
cf4f73a7e1 | ||
|
|
5948e5c4cb | ||
|
|
791aa27158 | ||
|
|
0b8ab56ffc | ||
|
|
12dcfda756 | ||
|
|
ef44beac41 | ||
|
|
97a71904f7 | ||
|
|
94f4059d67 | ||
|
|
14ed2546bc | ||
|
|
908258c163 | ||
|
|
704191ba25 | ||
|
|
be7fc9abe2 | ||
|
|
e0daf1dbb1 | ||
|
|
a8877d4f8a | ||
|
|
2db850a3f3 | ||
|
|
ec7cdedb86 | ||
|
|
606fa15a55 | ||
|
|
3eca9b0e54 | ||
|
|
3abaf74580 | ||
|
|
9713748633 | ||
|
|
405e86aff4 | ||
|
|
4da824dc2b | ||
|
|
0ec177c644 | ||
|
|
9ef4f86050 | ||
|
|
96b830817e | ||
|
|
0db8b00fb9 | ||
|
|
005dde6c2b | ||
|
|
260f9b352e | ||
|
|
852b0bc498 | ||
|
|
3a503a5fc0 | ||
|
|
88b408695a | ||
|
|
776b45363b | ||
|
|
adb270b64c | ||
|
|
5329b8fd72 | ||
|
|
d6379e4bb9 | ||
|
|
6ca18d5b29 | ||
|
|
50c008ddec | ||
|
|
4f56f100fa | ||
|
|
0c16a5b0d1 | ||
|
|
8f33ad3c70 | ||
|
|
7dca1b404c | ||
|
|
99ff98ff47 | ||
|
|
083d6e1298 | ||
|
|
71716c451e | ||
|
|
3c4a244b75 | ||
|
|
85892a3bd6 | ||
|
|
1d3d721cf3 | ||
|
|
37a72af75f | ||
|
|
c93713b9e7 | ||
|
|
77b3118cbc | ||
|
|
bc0cbfd040 | ||
|
|
0cfc66b4d4 | ||
|
|
1b9b1cbe3c | ||
|
|
407187c826 | ||
|
|
a6eb1e65ac | ||
|
|
efbb19a862 | ||
|
|
2a94816b58 | ||
|
|
aab307a519 | ||
|
|
1279e16484 | ||
|
|
c2e20f9bd6 | ||
|
|
4acdaf6b5a | ||
|
|
25313cbcde | ||
|
|
0de0eb12eb | ||
|
|
7d0fe52600 | ||
|
|
ba1d3b2423 | ||
|
|
cae6afe85d | ||
|
|
5865ac267b | ||
|
|
162993839c | ||
|
|
4359b490cc | ||
|
|
e4a8e67229 | ||
|
|
6605270e64 | ||
|
|
09e2bfeed0 | ||
|
|
b24733466d | ||
|
|
c31be48f20 | ||
|
|
a9ed27f42c | ||
|
|
1440caa532 | ||
|
|
513eb21940 | ||
|
|
de348d39da | ||
|
|
0a3697962b | ||
|
|
e0edcf3d23 | ||
|
|
57dbff6a8e | ||
|
|
0149bc90f2 | ||
|
|
f7292deb0f | ||
|
|
be01448c36 | ||
|
|
484617ce25 | ||
|
|
8ec53a6004 | ||
|
|
cfd1bbd9d1 | ||
|
|
25ae214b6b | ||
|
|
456160beb1 | ||
|
|
9affe2d9f4 | ||
|
|
3ed2f89b3b | ||
|
|
9bb353fdbd | ||
|
|
c414ea28e4 | ||
|
|
cb0253f7cb | ||
|
|
f28663c626 | ||
|
|
3ef018f90e | ||
|
|
e5ae7f77fa | ||
|
|
b6bac0cd3b | ||
|
|
6a8e435210 | ||
|
|
442318af49 | ||
|
|
ad93b46c94 | ||
|
|
f7a9cc09ea | ||
|
|
0d5507cf3d | ||
|
|
f885e33cbd | ||
|
|
4813a8681f | ||
|
|
829f750c76 | ||
|
|
08b9f4a6d2 | ||
|
|
7fdf022b36 | ||
|
|
f12935076e | ||
|
|
7a472d1574 | ||
|
|
4a82dc8705 | ||
|
|
52acaadfce | ||
|
|
ef1967ff00 | ||
|
|
6584f3b2d4 | ||
|
|
8ad55290b1 | ||
|
|
8ff34d63c0 | ||
|
|
da88947028 | ||
|
|
854dc5db71 | ||
|
|
e8ac144011 | ||
|
|
8b20f4d568 | ||
|
|
cb88a4f6d9 | ||
|
|
270f12dc1e | ||
|
|
46ff586055 | ||
|
|
78b999be57 | ||
|
|
4f702d9339 | ||
|
|
6f210c0e91 | ||
|
|
a677cff0a2 | ||
|
|
5929e3b56d | ||
|
|
5b1050e427 | ||
|
|
271e987972 | ||
|
|
80402185a5 | ||
|
|
1df34148b2 | ||
|
|
dc2070b24e | ||
|
|
7dcbb14b75 | ||
|
|
36c90e0d96 | ||
|
|
53b0bef527 | ||
|
|
8227cf1aba | ||
|
|
7e09d9042b | ||
|
|
49688d6c0e | ||
|
|
962ce3e3d8 | ||
|
|
8cbae911e9 | ||
|
|
10c94baab7 | ||
|
|
2f2c0ba3ba | ||
|
|
879647de5e | ||
|
|
cd2c9d6b0c | ||
|
|
95c443e127 | ||
|
|
d403b26b44 | ||
|
|
0f4d4e2071 | ||
|
|
b51e772664 | ||
|
|
83c7be0f60 | ||
|
|
0063f7d97f | ||
|
|
0a65eeeee2 | ||
|
|
a7e69e7260 | ||
|
|
cea15dab4c | ||
|
|
7646b59078 | ||
|
|
a3c996a3d8 | ||
|
|
e6017ea102 | ||
|
|
e7db81c277 | ||
|
|
a182f5e4b6 | ||
|
|
da73357763 | ||
|
|
09e37725f7 | ||
|
|
5ab9f95379 | ||
|
|
6731909971 | ||
|
|
284948e377 | ||
|
|
1e55c974eb | ||
|
|
b186dee326 | ||
|
|
3ae0abc19e | ||
|
|
dde660ae15 | ||
|
|
d9fbd04552 | ||
|
|
a20c15cbac | ||
|
|
5a54b9f5ca | ||
|
|
3528d85501 | ||
|
|
b773599c56 | ||
|
|
73346c8de7 | ||
|
|
d33b21fdc1 | ||
|
|
8139010796 | ||
|
|
f0bc1a4abb | ||
|
|
9d0d5da3fc | ||
|
|
26d188f228 | ||
|
|
c7af6c2ae2 | ||
|
|
8707113db8 | ||
|
|
e88b4814e7 | ||
|
|
97926fe6d5 | ||
|
|
4fb9d46953 | ||
|
|
c066f7f3df | ||
|
|
5117ef8a58 | ||
|
|
35ebb39e22 | ||
|
|
66f8c19478 | ||
|
|
e07ea40b24 | ||
|
|
149bc38b2b | ||
|
|
23ca81ccc4 | ||
|
|
b4ab559b00 | ||
|
|
ad88735e63 | ||
|
|
66ec4f9de5 | ||
|
|
35d6b424e6 | ||
|
|
eb6b099fa1 | ||
|
|
b8c1a7a4c9 | ||
|
|
bc07bdaf74 | ||
|
|
3109befe73 | ||
|
|
044683de21 | ||
|
|
e607663575 | ||
|
|
abcff7192b | ||
|
|
bc43c43c53 | ||
|
|
dbc7c4675f | ||
|
|
3f0e23f34d | ||
|
|
ef7bd7c77c | ||
|
|
77950c8a2c | ||
|
|
a801b5b21f | ||
|
|
d637a6dcac | ||
|
|
8370a22966 | ||
|
|
7ea8c8b8f1 | ||
|
|
11ab87245b | ||
|
|
cf63b49b82 | ||
|
|
030384c990 | ||
|
|
49c734b52c | ||
|
|
2a7b6144d6 | ||
|
|
f1ecc0cc15 | ||
|
|
adb7663d03 | ||
|
|
118b0a85d2 | ||
|
|
5fcf1e156d | ||
|
|
5272befb60 | ||
|
|
b10a524496 | ||
|
|
de7417787b | ||
|
|
a00b80529b | ||
|
|
b1fa44d176 | ||
|
|
8251b460ad | ||
|
|
4403044584 | ||
|
|
39c7a56041 | ||
|
|
383dbb5fca | ||
|
|
fc94e784cc | ||
|
|
37937b0096 | ||
|
|
3e9d303ec4 | ||
|
|
68908e17d3 | ||
|
|
026f482b1b | ||
|
|
879784cd67 | ||
|
|
8b8cbb4d74 | ||
|
|
40783eba15 | ||
|
|
e63d20ce48 | ||
|
|
f6ecc4d0bb | ||
|
|
64339e6846 | ||
|
|
2fe73056f0 | ||
|
|
95051035a9 | ||
|
|
af6294fe7e | ||
|
|
2fd53be083 | ||
|
|
93199e3695 | ||
|
|
d0a3051656 | ||
|
|
516e84d328 | ||
|
|
a06e4cb7ba | ||
|
|
a1dd5fee9f | ||
|
|
7e00ac4e50 | ||
|
|
59201e8af6 | ||
|
|
fbde9e0746 | ||
|
|
f714add057 | ||
|
|
ce2724a892 | ||
|
|
aff475cd5b | ||
|
|
b18d7c7cc5 | ||
|
|
687088cbd7 | ||
|
|
31fc18b150 | ||
|
|
dd960d5556 | ||
|
|
004d9f90e3 | ||
|
|
f8f7ee686e | ||
|
|
c695e565ea | ||
|
|
d32c3ebebe | ||
|
|
b863bae4e7 | ||
|
|
8f033e8bd3 | ||
|
|
c8776486c5 | ||
|
|
679e9ad4bf | ||
|
|
877b255f23 | ||
|
|
e404716f88 | ||
|
|
4e58df60ea | ||
|
|
e7f761c8d6 | ||
|
|
50ca85b7ce | ||
|
|
9659c29dd0 | ||
|
|
535f3193db | ||
|
|
7fae1eac48 | ||
|
|
4cc2975e97 | ||
|
|
149d1ad590 | ||
|
|
abc900a764 | ||
|
|
14084192da | ||
|
|
2f6603070b | ||
|
|
c3aac9f0a6 | ||
|
|
afde80dab5 | ||
|
|
b7f68afbf1 | ||
|
|
08634f330c | ||
|
|
83e35e6aa0 | ||
|
|
dc3bfef038 | ||
|
|
c13bb77b08 | ||
|
|
fab621bb40 | ||
|
|
bb760cd861 | ||
|
|
59e7b3fd93 | ||
|
|
315870aa09 | ||
|
|
5ed17acd6b | ||
|
|
d0dde822dd | ||
|
|
e3023532ed | ||
|
|
2a4c229c9d | ||
|
|
7b77627db7 | ||
|
|
fd5f32bcc4 | ||
|
|
7e7eecfa3e | ||
|
|
8325339b58 | ||
|
|
b39185ceb3 | ||
|
|
d93100f3c3 | ||
|
|
1691558989 | ||
|
|
17cabba48f | ||
|
|
c9a2cc05ea | ||
|
|
f9ac5302ca | ||
|
|
7c8c8dfc20 | ||
|
|
6622bc7560 | ||
|
|
3df7bd99eb | ||
|
|
da1636af60 | ||
|
|
2378d7ff78 | ||
|
|
c2f357cc0e | ||
|
|
f2bd85d803 | ||
|
|
ec3a856c59 | ||
|
|
1a55487e7b | ||
|
|
0f0ac33345 | ||
|
|
d0b4e5045b | ||
|
|
393cff0343 | ||
|
|
8e56b59bb8 | ||
|
|
a939dfe37f | ||
|
|
9b7e06bd54 | ||
|
|
cc2f7920b8 | ||
|
|
66a69dd22d | ||
|
|
d3f7eaee1c | ||
|
|
3200f4cd28 | ||
|
|
fc04b30b7e | ||
|
|
716f65786d | ||
|
|
0c397ba0a8 | ||
|
|
bff8a09147 | ||
|
|
f65ea13c6f | ||
|
|
3975b09898 | ||
|
|
6525d3130c | ||
|
|
5d8ad83cbe | ||
|
|
13a819ee87 | ||
|
|
cd1beb5191 | ||
|
|
483259ba2c | ||
|
|
3d828c42de | ||
|
|
d70dbe82d7 | ||
|
|
243c5a0f82 | ||
|
|
f4470d8190 | ||
|
|
b435163a3c | ||
|
|
26f6315b69 | ||
|
|
b81a02fbb4 | ||
|
|
b19988784f | ||
|
|
5caf576f83 | ||
|
|
e61b132c93 | ||
|
|
41b826e9ac | ||
|
|
4079f19e25 | ||
|
|
04f108cbdf | ||
|
|
e222e3e7c2 | ||
|
|
cb687205a4 | ||
|
|
4ef19056bb | ||
|
|
8a4fb55cdf | ||
|
|
805b573370 | ||
|
|
3f87719d02 | ||
|
|
2a27bb560c | ||
|
|
73dbe68301 | ||
|
|
c2bd4c8984 | ||
|
|
3021cfb164 | ||
|
|
794f317f83 | ||
|
|
e357581587 | ||
|
|
e46b699fcb | ||
|
|
c426ea6e03 | ||
|
|
b0a1fdb65a | ||
|
|
4515620259 | ||
|
|
c36695c59a | ||
|
|
090caa967b | ||
|
|
e9ac774464 | ||
|
|
de2de45196 | ||
|
|
64dd0e0be3 | ||
|
|
6cd5438c89 | ||
|
|
df5a09b46d | ||
|
|
cba03a7539 | ||
|
|
069d02d908 | ||
|
|
a207ba61de | ||
|
|
496db6427d | ||
|
|
1bcf6699f0 | ||
|
|
fa12721c3c | ||
|
|
65ad43431c | ||
|
|
081544e778 | ||
|
|
a30daf03d4 | ||
|
|
f80af230af | ||
|
|
71125a4b58 | ||
|
|
c1dfaf13fb | ||
|
|
03a3081361 | ||
|
|
b5715e46d2 | ||
|
|
3aa9ed61c6 | ||
|
|
0b6cc588b6 | ||
|
|
ed4af4a8e7 | ||
|
|
c1f6c3ddeb | ||
|
|
d57652815e | ||
|
|
ea3191739b | ||
|
|
378b4f973b | ||
|
|
6ecbe59011 | ||
|
|
2fcaa1f6cc | ||
|
|
f0a7582fd2 | ||
|
|
2da076a501 | ||
|
|
f31cc4806f | ||
|
|
ae228988a5 | ||
|
|
185f0463d4 | ||
|
|
cf668e774b | ||
|
|
b933cdd950 | ||
|
|
f38d3bdb8e | ||
|
|
2c5ed9c884 | ||
|
|
f2e457de6c | ||
|
|
6b146fc7a7 | ||
|
|
25b44a2070 | ||
|
|
ac8ef05b06 | ||
|
|
663185d93e | ||
|
|
61ac6ed6e0 | ||
|
|
0fa51bcd36 | ||
|
|
f000425350 | ||
|
|
8df07808a9 | ||
|
|
7f169261d4 | ||
|
|
a86c728a99 | ||
|
|
11d56d1ea7 | ||
|
|
9c52cf0b0b | ||
|
|
cd96492dff | ||
|
|
43ab1deb24 | ||
|
|
646bf10017 | ||
|
|
6a23874054 | ||
|
|
b6fe18b975 | ||
|
|
84125fe463 | ||
|
|
12732715bd | ||
|
|
ece0b94ae8 | ||
|
|
84d0532039 | ||
|
|
ab68c1f1ab | ||
|
|
b360d8d931 | ||
|
|
f9681f2766 | ||
|
|
e8a09eef72 | ||
|
|
3290639e54 | ||
|
|
02d3275475 | ||
|
|
fd92049cda | ||
|
|
f48a5655ed | ||
|
|
22267b4123 | ||
|
|
b06230ef3d | ||
|
|
87b37e2839 | ||
|
|
0f3dd2e05d | ||
|
|
0495429df8 | ||
|
|
2e97f1e037 | ||
|
|
8133ba61a5 | ||
|
|
4cce5cd4ff | ||
|
|
1f1f95e3da | ||
|
|
7c9c66470d | ||
|
|
da5bead39c | ||
|
|
68a0c74a1f | ||
|
|
949d1a900a | ||
|
|
88c7a472b1 | ||
|
|
4b49501630 | ||
|
|
9eb71b870a | ||
|
|
5d46560427 | ||
|
|
d7856af6db | ||
|
|
c0a093d044 | ||
|
|
03c7df9dad | ||
|
|
d847316914 | ||
|
|
190e17b445 | ||
|
|
ce5d1b56f0 | ||
|
|
cb7cbc15b3 | ||
|
|
dc6756da04 | ||
|
|
e4eeb437eb | ||
|
|
81b94070ac | ||
|
|
de36a04d88 | ||
|
|
1287b77dfe | ||
|
|
146bb004c0 | ||
|
|
d26abecea7 | ||
|
|
d2da14a951 | ||
|
|
4ae17a1f66 | ||
|
|
473f60167d | ||
|
|
5377590520 | ||
|
|
58fe1f6e2b | ||
|
|
c0564c89c8 | ||
|
|
34d27dc120 | ||
|
|
d907af46bc | ||
|
|
a8055471cd | ||
|
|
317c28eaf2 | ||
|
|
d2b6aeddbd | ||
|
|
4285d01a19 | ||
|
|
802c59a1d1 | ||
|
|
f1879c8d4b | ||
|
|
97ad294623 | ||
|
|
6147dcd304 | ||
|
|
d900842363 | ||
|
|
e6ac92abb4 | ||
|
|
944b5fc6c9 | ||
|
|
bf6c645281 | ||
|
|
14f04e5f88 | ||
|
|
ecbf7bd661 | ||
|
|
de15edcd0a | ||
|
|
a5d619d6a8 | ||
|
|
271811d376 | ||
|
|
6ed9652a2a | ||
|
|
0d62811a17 | ||
|
|
615f1f2b5d | ||
|
|
721bff01b0 | ||
|
|
69f671106c | ||
|
|
0c59970974 | ||
|
|
d161a75ab2 | ||
|
|
b87f2b2952 | ||
|
|
a0b4c38a44 | ||
|
|
c9cc98ae39 | ||
|
|
ad492e2b90 | ||
|
|
a977042017 | ||
|
|
9e4b5ad02b | ||
|
|
90b9b2d29c | ||
|
|
54b6efce59 | ||
|
|
1f77a825b3 | ||
|
|
283d787c8d | ||
|
|
47be4d39c2 | ||
|
|
8a4fab9528 | ||
|
|
a31586fac9 | ||
|
|
3b88c72778 | ||
|
|
0e4a5da71a | ||
|
|
2a6327b2f2 | ||
|
|
d4d8adf4ac | ||
|
|
8492a31dd7 | ||
|
|
bb91402d2c | ||
|
|
35d8b4f848 | ||
|
|
073dc1afe7 | ||
|
|
b61e6dabd4 | ||
|
|
316fa688c3 | ||
|
|
0724387257 | ||
|
|
8e87c8cdbe | ||
|
|
9553d3ba25 | ||
|
|
4bccd6de25 | ||
|
|
dd5bafca0a | ||
|
|
6d3e28226a | ||
|
|
0fc040773a | ||
|
|
1f2294f9bf | ||
|
|
bed3f1d8fa | ||
|
|
b1da0b8279 | ||
|
|
32f3137f4d | ||
|
|
df10bca2c0 | ||
|
|
21fba1c4f7 | ||
|
|
f56e7e8dd8 | ||
|
|
41f6119118 | ||
|
|
7732f2a27e | ||
|
|
0be4f31162 | ||
|
|
6bc8428dd0 | ||
|
|
fd92e92a4f | ||
|
|
9dc7a4447b | ||
|
|
c2a597ffcf | ||
|
|
fc4850afab | ||
|
|
6c31de36ac | ||
|
|
4082aa8d77 | ||
|
|
71a835ff5f | ||
|
|
b156df6fc2 | ||
|
|
d077621ee9 | ||
|
|
3624502a23 | ||
|
|
c9f12fece7 | ||
|
|
0f43fd4560 | ||
|
|
1c85f980d3 | ||
|
|
49428d3ead | ||
|
|
2f48752ff2 | ||
|
|
48ac89abc9 | ||
|
|
bd8bad5e4c | ||
|
|
13c189fb00 | ||
|
|
eaab3c3f5e | ||
|
|
61fb8246f0 | ||
|
|
b09249b384 | ||
|
|
7d6b98766c | ||
|
|
96bcf55942 | ||
|
|
7bb6078b13 | ||
|
|
c8d6a4640a | ||
|
|
27be2b7a1d | ||
|
|
b05d682aa3 | ||
|
|
636b26b0e8 | ||
|
|
ae2a111536 | ||
|
|
33796a8bd3 | ||
|
|
695e5d3daa | ||
|
|
9d805d5d42 | ||
|
|
ec3fd63138 | ||
|
|
8e5e2d4a0c | ||
|
|
ee6e2b41f7 | ||
|
|
42c54ef02f | ||
|
|
227cbfc79a | ||
|
|
0fd5a1a91d | ||
|
|
4406c940b5 | ||
|
|
8eab44349f | ||
|
|
f50f7153dc | ||
|
|
a994f65d79 | ||
|
|
1a3a17e480 | ||
|
|
840e4aec54 | ||
|
|
74b660af61 | ||
|
|
cc0c56087a | ||
|
|
990e6c0eed | ||
|
|
3bc6cd8b4d | ||
|
|
515119e1fa | ||
|
|
570303273c | ||
|
|
1739cc58d4 | ||
|
|
b3a7d42f9d | ||
|
|
17ed1f9806 | ||
|
|
1e3883674e | ||
|
|
bead888c67 | ||
|
|
07fcd66d8d | ||
|
|
0f4cac1b76 | ||
|
|
89fbc537bf | ||
|
|
f0ebdf295f | ||
|
|
d396cb911a | ||
|
|
9064487a3e | ||
|
|
8b03f32f95 | ||
|
|
3295cc514e | ||
|
|
c6df492852 | ||
|
|
565c71cb80 | ||
|
|
30bd710650 | ||
|
|
c0dbf95b94 | ||
|
|
7f58837111 | ||
|
|
53f609c4d7 | ||
|
|
26790fd80d | ||
|
|
cfcb24a732 | ||
|
|
e31746b676 | ||
|
|
154435d5a5 | ||
|
|
d24a0312d8 | ||
|
|
aa5d8b9377 | ||
|
|
e9703e03cd | ||
|
|
13a8d27349 | ||
|
|
939f8f52c1 | ||
|
|
cb1e062f9b | ||
|
|
a1d1bc5aea | ||
|
|
1d81c0521f | ||
|
|
d3ef916b23 | ||
|
|
c9b7259cd7 | ||
|
|
fa8c135b22 | ||
|
|
9d53d806fd | ||
|
|
6f499e6c56 | ||
|
|
e66bdc936a | ||
|
|
be34e062e7 | ||
|
|
5664b32cc5 | ||
|
|
6bf0ea63d4 | ||
|
|
628970e588 | ||
|
|
fb68ccad15 | ||
|
|
d6b394500f | ||
|
|
32508d60b1 | ||
|
|
0ae23c30c4 | ||
|
|
e5f18c5e22 | ||
|
|
df0f25b234 | ||
|
|
dc1d9e59b0 | ||
|
|
266eb77eb5 | ||
|
|
c1cac82081 | ||
|
|
323c787d91 | ||
|
|
41070495ba | ||
|
|
037e531b22 | ||
|
|
54713b5d68 | ||
|
|
d94f9a91db | ||
|
|
516b8e848f | ||
|
|
6d1d1705b2 | ||
|
|
666a527aa3 | ||
|
|
f296c7fdad | ||
|
|
58e62da913 | ||
|
|
ca364d4d56 | ||
|
|
2232680ded | ||
|
|
b662c54a07 | ||
|
|
cd2f897ff2 | ||
|
|
c877d4b1d7 | ||
|
|
dc6032aa43 | ||
|
|
fec9431ae5 | ||
|
|
bf1b7e640b | ||
|
|
780ab5b14f | ||
|
|
7371aebb76 | ||
|
|
c227f39a03 | ||
|
|
ff794f1578 | ||
|
|
f26c342e82 | ||
|
|
edacb88ff5 | ||
|
|
4854eac2da | ||
|
|
60cd105b82 | ||
|
|
184db222c5 | ||
|
|
abcfb9ee12 | ||
|
|
5f3ba669eb | ||
|
|
942d4756c7 | ||
|
|
d9b6dfd8d0 | ||
|
|
ff79e7ad36 | ||
|
|
ebfdac96ae | ||
|
|
e2a85885be | ||
|
|
c6b0fb4d65 | ||
|
|
fb5c4df4db | ||
|
|
9cb4eb775b | ||
|
|
8c349e4669 | ||
|
|
b5d879139a | ||
|
|
0e05918631 | ||
|
|
c3fee8d323 | ||
|
|
b5743d9902 | ||
|
|
44974c04ad | ||
|
|
336f8d525b | ||
|
|
f76b6afe6a | ||
|
|
602b58d1df | ||
|
|
030edccc90 | ||
|
|
dd3317f4f6 | ||
|
|
f29d0e45b7 | ||
|
|
c038ed3db4 | ||
|
|
afcf2a9400 | ||
|
|
6bcc4c86e6 | ||
|
|
ded32730bf | ||
|
|
2e1b6aef9f | ||
|
|
26d918e218 | ||
|
|
23e1097f89 | ||
|
|
6d1e2d9fab | ||
|
|
7c37284901 | ||
|
|
e9384676e1 | ||
|
|
88bf51c066 | ||
|
|
fbbe8aff54 | ||
|
|
e3d441d19f | ||
|
|
4f3d20a7c4 | ||
|
|
222ea18bcd | ||
|
|
df7c91f17f | ||
|
|
99331fcc54 | ||
|
|
b59d31855e | ||
|
|
a2211cfa46 | ||
|
|
e9ec42be02 | ||
|
|
0801d9bf65 | ||
|
|
379b7a56ef | ||
|
|
8e9062c812 | ||
|
|
21e03e8318 | ||
|
|
fa1b53682c | ||
|
|
ec68deb7e4 | ||
|
|
1d51f3eed5 | ||
|
|
e8e189d5f3 | ||
|
|
443e338cc3 | ||
|
|
9cab049696 | ||
|
|
e30e869025 | ||
|
|
b106be2ed5 | ||
|
|
842519d7d0 | ||
|
|
d2ff73b579 | ||
|
|
c31d9dfbb2 | ||
|
|
d7ed734ffb | ||
|
|
b5a04bfe63 | ||
|
|
077e6a110e | ||
|
|
66d87e8b12 | ||
|
|
6bf5e7abcc | ||
|
|
39979a411d | ||
|
|
fdd5c71711 | ||
|
|
d6e20fe166 | ||
|
|
7988b13281 | ||
|
|
8395865b75 | ||
|
|
c3f33acdb3 | ||
|
|
6d9167c30f | ||
|
|
f8d698aea9 | ||
|
|
0cbde5046e | ||
|
|
7a137a68ae | ||
|
|
09f7e6ce99 | ||
|
|
305cc72485 | ||
|
|
9b04901754 | ||
|
|
d262f429c4 | ||
|
|
6bb1223614 | ||
|
|
1d97b217cd | ||
|
|
dfe48466e0 | ||
|
|
6fed45e7a8 | ||
|
|
d97b75a3e1 | ||
|
|
87f2e08b3a | ||
|
|
f2c517a4a4 | ||
|
|
b8b810cdb1 | ||
|
|
c21900100e | ||
|
|
50222f5083 | ||
|
|
594b596cf9 | ||
|
|
3494a4875c | ||
|
|
a5d880e411 | ||
|
|
e9f445380b | ||
|
|
ea51f15253 | ||
|
|
58501c205a | ||
|
|
47f23884b4 | ||
|
|
35ff7fd83e | ||
|
|
1375d7922c | ||
|
|
acc0a2ec67 | ||
|
|
eecf1f4a54 | ||
|
|
088d022d5e | ||
|
|
22fcb14f9a | ||
|
|
6f2294f9b9 | ||
|
|
d7190b0602 | ||
|
|
4cf769e7b6 | ||
|
|
3889c8c1fa | ||
|
|
d122e10703 | ||
|
|
05f1fa0ecb | ||
|
|
3974629e34 | ||
|
|
dfd8147873 | ||
|
|
bb503d9cc7 | ||
|
|
913cb1a3cd | ||
|
|
7125fb285e | ||
|
|
0454868958 | ||
|
|
8523e3d1a4 | ||
|
|
e2415b68d3 | ||
|
|
0850a3428e | ||
|
|
6f44a8b6ee | ||
|
|
0cc16c232b | ||
|
|
57bd21d346 | ||
|
|
f18e7295bd | ||
|
|
d6a6343aa8 | ||
|
|
7397d2da50 | ||
|
|
f70c457e88 | ||
|
|
7750720f4d | ||
|
|
950281caa6 | ||
|
|
b5202b5591 | ||
|
|
4aa01acce4 | ||
|
|
c58e788eba | ||
|
|
e7b60a1f27 | ||
|
|
1e4bbc4ecf | ||
|
|
e599da7033 | ||
|
|
341b5cd947 | ||
|
|
fa35f3f9e4 | ||
|
|
1375578b52 | ||
|
|
1e2326913b | ||
|
|
82c41e09b5 | ||
|
|
16f3b71af4 | ||
|
|
03373f3cda | ||
|
|
2c9c01b991 | ||
|
|
8a44b6fdb7 | ||
|
|
3ceb886ca9 | ||
|
|
fb3df39263 | ||
|
|
829e8ed745 | ||
|
|
ed5c52a807 | ||
|
|
8afc5afadf | ||
|
|
b26401203f | ||
|
|
c127548dd1 | ||
|
|
f8c1a48350 | ||
|
|
55f634bec3 | ||
|
|
8c14e42a09 | ||
|
|
2e30a96389 | ||
|
|
3fc4898904 | ||
|
|
3561c55174 | ||
|
|
5195c647f6 | ||
|
|
f3a0d1daac | ||
|
|
dcad6e2d23 | ||
|
|
6a402fe544 | ||
|
|
310ae5905f | ||
|
|
23aa820cdf | ||
|
|
74f702cea6 | ||
|
|
52335bddbc | ||
|
|
05acf724a8 | ||
|
|
71319a0a7c | ||
|
|
c341c55258 | ||
|
|
cf40e641a6 | ||
|
|
7cb6af85a8 | ||
|
|
358ef34918 | ||
|
|
a66d194e12 | ||
|
|
57b3ce4666 | ||
|
|
aaa2b6f817 | ||
|
|
97b56e5620 | ||
|
|
79850176c3 | ||
|
|
8e1896ef5b | ||
|
|
8cf911bb15 | ||
|
|
ad0af16fa3 | ||
|
|
2aada61af3 | ||
|
|
66b9b4c68c | ||
|
|
60d6151ce9 | ||
|
|
bfb4b0b9da | ||
|
|
0f00e206bf | ||
|
|
71536ef9d3 | ||
|
|
354e73b4e7 | ||
|
|
30121e3617 | ||
|
|
4d422e716b | ||
|
|
8aa0f8d070 | ||
|
|
7c03c0cbcf | ||
|
|
c13a4835b2 | ||
|
|
412d9b7645 | ||
|
|
27cdaf1ed5 | ||
|
|
32e8a45e4e | ||
|
|
34f35aff27 | ||
|
|
9b0101321a | ||
|
|
a4c9487192 | ||
|
|
6449973ddc | ||
|
|
bd9a168667 | ||
|
|
5c0b03f133 | ||
|
|
5708f039c0 | ||
|
|
ba0809159c | ||
|
|
4aeb4238b2 | ||
|
|
119bc8207f | ||
|
|
5e7dc27e1f | ||
|
|
6d3b4db760 | ||
|
|
ccba9aa4d5 | ||
|
|
47cbc91b02 | ||
|
|
7a10fa157d | ||
|
|
a27ed4051c | ||
|
|
dbbde4b098 | ||
|
|
2f71480849 | ||
|
|
7e2284e094 | ||
|
|
0ce5c198aa | ||
|
|
c8519188a1 | ||
|
|
bf9f782970 | ||
|
|
72f580efb8 | ||
|
|
a443e3dcde | ||
|
|
5496c6c8af | ||
|
|
b96d5e765e | ||
|
|
cee5fb915a | ||
|
|
54888ff278 | ||
|
|
ab3f3d72ab | ||
|
|
b0eb0d74fb | ||
|
|
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 | ||
|
|
43bd08a58f | ||
|
|
8a78cc2f5e | ||
|
|
186429890e | ||
|
|
1e3d216961 | ||
|
|
fc83a9e905 | ||
|
|
dc054d7e6b | ||
|
|
555d464f8f | ||
|
|
c8a8336dc7 | ||
|
|
228c39719d | ||
|
|
32217db357 | ||
|
|
a2ddfc5674 | ||
|
|
1ae7be4f6a | ||
|
|
50a92e9ea0 | ||
|
|
1e63fc14cb | ||
|
|
5c29d42d8c | ||
|
|
19055ba004 | ||
|
|
0825ae8cb5 | ||
|
|
c032c9f458 | ||
|
|
fa5a9621e0 | ||
|
|
b2db2cc719 | ||
|
|
0f76819936 | ||
|
|
66d1597312 | ||
|
|
74f4ae03f3 | ||
|
|
5894cec3e4 | ||
|
|
d217d9a291 | ||
|
|
29a73b183c | ||
|
|
79c64f0e38 | ||
|
|
b8a3deeb02 | ||
|
|
108c774c0f | ||
|
|
830c7556b8 | ||
|
|
5470add29a | ||
|
|
f6c9ab0068 | ||
|
|
e44b34062c | ||
|
|
e54a87c436 | ||
|
|
608cc363a2 | ||
|
|
f9609c5871 | ||
|
|
cc422a6b1d | ||
|
|
638d75c388 | ||
|
|
b02495dd3d | ||
|
|
90ee8033b0 | ||
|
|
3f5d8fe2a1 | ||
|
|
c65f55b22a | ||
|
|
c9d221404b | ||
|
|
ee73961832 | ||
|
|
ef39c174ed | ||
|
|
962d8f77dd | ||
|
|
bbc7abc50d | ||
|
|
00f1258032 | ||
|
|
beb297967f | ||
|
|
00c913fd19 | ||
|
|
a38a8c4ba4 | ||
|
|
56fafba8e9 | ||
|
|
180eaa2ce5 | ||
|
|
d8de60afb9 | ||
|
|
d5248e8472 | ||
|
|
a7c199b195 | ||
|
|
97a5351a52 | ||
|
|
e0b4452007 | ||
|
|
2e4a532b3c | ||
|
|
e54266d3a5 | ||
|
|
422ed0a5e2 | ||
|
|
59e17738cc | ||
|
|
50644cf3c4 | ||
|
|
4a3ceb710d | ||
|
|
5ab640c380 | ||
|
|
5c108635d0 | ||
|
|
365808eff2 | ||
|
|
5c654e99e4 | ||
|
|
709c47d40d | ||
|
|
8ff8fb9c92 | ||
|
|
e3cdc5d3ff | ||
|
|
69851d1596 | ||
|
|
c5330246b1 | ||
|
|
6580ea5891 | ||
|
|
1cfd5ae4f0 | ||
|
|
339beeabaf | ||
|
|
3a4b9e2e31 | ||
|
|
3055eeaa4f | ||
|
|
e517fa6000 | ||
|
|
3946ebcb92 | ||
|
|
a34fa04e4f | ||
|
|
2108f3209d | ||
|
|
5092bc571d | ||
|
|
b013e8af50 | ||
|
|
932c708538 | ||
|
|
b27a62c625 | ||
|
|
f0359dcde9 | ||
|
|
08a005b271 | ||
|
|
1b873acd72 | ||
|
|
a76ac9b5e3 | ||
|
|
4f575fda73 | ||
|
|
14b6c70f47 | ||
|
|
8f4e9ac48f | ||
|
|
30069e719b | ||
|
|
40f3a78795 | ||
|
|
0d11c71bb7 | ||
|
|
758480dd5f | ||
|
|
2ca84501ba | ||
|
|
f9756e0977 | ||
|
|
9bb7e3a541 | ||
|
|
31ba460553 | ||
|
|
57f519db65 | ||
|
|
edf6c65e38 | ||
|
|
349cf1981a | ||
|
|
a15635d953 | ||
|
|
04d9f3808b | ||
|
|
494724c795 | ||
|
|
71cadad05a | ||
|
|
30d204dddc | ||
|
|
cc19748fd2 | ||
|
|
48f197b7ea | ||
|
|
f740fde834 | ||
|
|
b1aae4a85a | ||
|
|
7f4b3edd84 | ||
|
|
14cc7fcfeb | ||
|
|
9e0ae5dc96 | ||
|
|
ba88fd5306 | ||
|
|
eafde17259 | ||
|
|
ac3214fedc | ||
|
|
c376689ad4 | ||
|
|
50925536d1 | ||
|
|
1104ce3176 | ||
|
|
19ef5b7e1d | ||
|
|
8613a89264 | ||
|
|
6a90bac196 | ||
|
|
e502f1dcc4 | ||
|
|
f6f1d4a97c | ||
|
|
7ee76fdd2b | ||
|
|
11a6f1124f | ||
|
|
b6fa4f3242 | ||
|
|
1cc2d6c6b7 | ||
|
|
0046d8ba90 | ||
|
|
bb6653cecb | ||
|
|
a847a2ff91 | ||
|
|
7f339ede4e | ||
|
|
d84596860c | ||
|
|
5d9414e728 | ||
|
|
ae6571b18b | ||
|
|
0faf773f71 | ||
|
|
858f33f782 | ||
|
|
fa072bf387 | ||
|
|
154ea7354d | ||
|
|
06ed266278 | ||
|
|
5df16db823 | ||
|
|
4f23706b19 | ||
|
|
b9601cb54a | ||
|
|
3d12f85f66 | ||
|
|
79b8baac9f | ||
|
|
8a6df8abc7 | ||
|
|
8b8d763fb7 | ||
|
|
ac8d2beb80 | ||
|
|
5e6384074e | ||
|
|
2522bd44d6 | ||
|
|
76131f1cc7 | ||
|
|
54fb5dc765 | ||
|
|
4110af56e7 |
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
gns3/version.py merge=ours
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -59,3 +59,4 @@ keys
|
||||
/gns3_server.ini
|
||||
updates
|
||||
.cache
|
||||
__pycache__
|
||||
|
||||
23
.travis.yml
23
.travis.yml
@@ -1,19 +1,16 @@
|
||||
sudo: required
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
- docker
|
||||
notifications:
|
||||
email: false
|
||||
#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
|
||||
|
||||
- docker run gns3-gui-test
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: noplay
|
||||
password:
|
||||
secure: FofcqlJjgqf2jaDaXpLHeigVoexbrOz3WwnDuiJpwJxeFUlPY8s2cQs/Bm+dzxzZaOaGiVE0A83v/Xa10yD5tflThHt4sqYJK3iQCinA7wgeAlDimB4xrWUNplfNJZ/Eod5Ssa++E02W+3i29PxpXY//mjCY7qDxaoxul1gnFJY=
|
||||
on:
|
||||
tags: true
|
||||
repo: GNS3/gns3-gui
|
||||
|
||||
605
CHANGELOG
605
CHANGELOG
@@ -1,4 +1,549 @@
|
||||
# Change Log
|
||||
# Change Log
|
||||
|
||||
## 2.0.2 30/05/2017
|
||||
|
||||
* Show a default symbol in case of corrupted file
|
||||
* When another gui is already running exit instead of proper close to avoid any issue
|
||||
* Fix duplicate on remote server use wrong location
|
||||
* Display the location of settings when we disallow opening due to old release
|
||||
* Improve search for dynamips in development on OSX
|
||||
* Fix error display when loading a .png custom symbol
|
||||
* Fix a crash in the progress dialog
|
||||
* Fix a race condition when exporting a closed project
|
||||
* Fix RuntimeError: wrapped C/C++ object of type NodeItem has been deleted
|
||||
|
||||
## 2.0.1 16/05/2017
|
||||
|
||||
* Improve inline help. Fixes #1999. Add a warning about wifi interfaces in the cloud. Fixes #1902.
|
||||
* Copy remote directory path into clipboard in "Show in FileManager". Fixes #1966.
|
||||
* Fix display of error in progress dialog when we don't have thread
|
||||
* Fix lost slot and port in dynamips settings
|
||||
* Do not run import / export of project in seperate thread
|
||||
* Assert when running an HTTP query outside the main thread
|
||||
* Proper error when you try to load the pid file as config file
|
||||
* Log malformed svg text item
|
||||
* Fix a race condition when right click and delete a node at the same time
|
||||
* Fix a race condition when snapshoting a closed project
|
||||
* Update doctor_dialog.py
|
||||
* Catch remaining missing function listxattr on some Linux host.
|
||||
* Fix a race condition when creating node and closing project
|
||||
* Fix error if you put a path in a .gns3a file for qemu
|
||||
* Fix AttributeError: 'NoneType' object has no attribute '_refreshVisibleWidgets'
|
||||
* Do not crash if the logging code raise an exception
|
||||
* Fix some crash in dynamips device preference page
|
||||
* Fix warning when loading IOU images on Windows
|
||||
* Do not crash if you don't have configure a packet capture program on Windows
|
||||
* Ignore error when we can't kill the packet capture
|
||||
* Fix AttributeError: 'NoneType' object has no attribute 'wasCanceled'
|
||||
* Fix RuntimeError: wrapped C/C++ object of type QComboBox has been deleted
|
||||
* Fix RuntimeError: wrapped C/C++ object of type QTreeWidgetItem has been deleted
|
||||
* Fix detection of https when use for the local server
|
||||
* Silent the _COMPIZ_TOOLKIT_ACTION warning
|
||||
* Cacth TypeError: native Qt signal is not callable
|
||||
* Fix AttributeError: 'C7200' object has no attribute 'warning_signal'
|
||||
* Catch missing function listxattr on some linux host
|
||||
* Disallow opening a .gns3 on a remote server
|
||||
* Fix project closing when we have multiple client connected
|
||||
|
||||
## 2.0.0 02/05/2017
|
||||
|
||||
* Clarify that we don't override vmware custom adapters
|
||||
* Strip space from path at project creation
|
||||
|
||||
## 2.0.0rc4 20/04/2017
|
||||
|
||||
* Catch all error during the generation of log messages.
|
||||
* Catch a rare node creation error
|
||||
* Fix missing menu text at application startup
|
||||
* Fix a race condition in the drawing item
|
||||
* Catch system error when connecting to local server
|
||||
* Catch a rare error when killing the capture
|
||||
* Improve pcap streaming speed
|
||||
* Upgrade to 5.7.1
|
||||
* Recent projects list bug
|
||||
* Fix a race condition in the preferences dialog
|
||||
* Try to fix some windows Z issues
|
||||
* Catch a garbage collection issue in the right click on a link
|
||||
* Fix a compatibility issue with Python 3.4
|
||||
|
||||
## 1.5.4 13/04/2017
|
||||
|
||||
* Limit ubridge permission to the admin group on OSX
|
||||
* Upgrade to Qt 5.7.1 on Windows
|
||||
|
||||
## 2.0.0rc3 31/03/2017
|
||||
|
||||
* Improve timeout handling
|
||||
* Improve logging when we display a qt message box
|
||||
* Try to detect computer hibernation
|
||||
* Fix crash when we send some errors to the user console
|
||||
* Use QtFile for managing file capture
|
||||
* Allow to delete a profile from the profile select dialog
|
||||
* Filter hidden folder in the profil directory
|
||||
* Prevent user putting port in the remote host name
|
||||
* Fix RuntimeError: wrapped C/C++ object of type EllipseItem has been deleted
|
||||
* Fix a rare error in LinkItem
|
||||
* Fix Image field in nodes list is stale after changing an image
|
||||
* Fix RuntimeError: Set changed size during iteration
|
||||
* Better detection of remote server changes
|
||||
* Add a notice about the fact you need to apply server settings
|
||||
* Check python version only for setup.py install
|
||||
* Catch appliance error when creating an appliance new version
|
||||
* If a node can't be deleted do not remove it
|
||||
* If something is wrong during packet capture do not disconnect us from the server
|
||||
* Fix saving dynamips
|
||||
* Try to fix the hang dialog on some computers
|
||||
* Fix a rare crash in progress dialog
|
||||
* If we pass --profile skip the profile select dialog
|
||||
* Raise an error if the progress dialog is not created from the main thread
|
||||
* Log qt log to python log
|
||||
* Fix image are not uploaded to remote main server
|
||||
* Fix race condition when editing a project
|
||||
* Poll settings each 5 seconds
|
||||
* Avoid progress dialog not disapear
|
||||
* Remove wrong mention about the fact super putty is include
|
||||
* Avoid a crash when an ios router don't have a chassis
|
||||
* Fix a potentatial crash in the progress dialog
|
||||
* Support official docker images in appliances
|
||||
|
||||
## 2.0.0rc2 10/03/2017
|
||||
|
||||
* Deploy on pypi when we tag
|
||||
* Fix rare crash in GNS3 VM preference page
|
||||
* Fix an error on Windows when loading SVG files
|
||||
* Prevent a potential crash
|
||||
* Workaround a rare crash when sending analytics
|
||||
* Catch error when you try to create a node a not existing server
|
||||
* Fix an error when your local server crash and computer return non unicode
|
||||
* Fix KeyError: 'slot1'
|
||||
* Fix a rare crash in import appliance
|
||||
* Rollback to PyQT 5.8 because 5.8.1 seem to have trouble at install
|
||||
* Update pyqt5 from 5.8 to 5.8.1
|
||||
|
||||
## 2.0.0 RC 1 06/03/2017
|
||||
|
||||
* UltraVNC support
|
||||
* Display less noisy dialog when we can't connect to the remote server
|
||||
* Prevent the usage of gns3vm as a remote server name
|
||||
* Fix the VMware wizard for not using a remote server by default
|
||||
* Prevent the GNS3 VM to appear in remote compute in the VM wizard
|
||||
* Remove iouyap settings
|
||||
* Fix missing permission error management
|
||||
* Avoid a crash when create a new dynamips version in the appliance wizard
|
||||
* Disallow user to add the same server as a remote server and as local server
|
||||
* Fix 'module' object has no attribute 'run'
|
||||
* Monitor and display local server stderr
|
||||
* Fix some import errors
|
||||
* Remove placeholder string from appliance wizard
|
||||
* Avoiding calling multiple time /computes at the same time. And reduce timeout
|
||||
* Support for appliance v4
|
||||
* Some tweaks for enabling/disabling HDPI mode.
|
||||
* Do not display error at first step of the setup wizard
|
||||
* Disable HDPI by default on Linux and allow to configure it
|
||||
* Fix an issue when you edit a VPCS node from the node view
|
||||
* Catch a race condition in managing error static assets download
|
||||
* Handle error if you try to import an appliance without having the images
|
||||
* Improve crash proof code of the progress dialog
|
||||
|
||||
## 2.0.0 beta 4 19/01/2017
|
||||
|
||||
* Update pyqt5 from 5.7.1 to 5.8
|
||||
* Drop from console view the show command not supported by 2.0
|
||||
* Try to avoid segfault in some PyQT version
|
||||
* Support for strike and underline
|
||||
* Do not use native font selector on mac it could crash
|
||||
* Use a dedicated QNetwork manager for notification
|
||||
* Fix a display error in console error message
|
||||
* Use signal for writting on console to avoid some potential segfault
|
||||
* Fix a rare warning
|
||||
* Add more debug when we have an http error
|
||||
* Disable timeout on project open
|
||||
* Support for gvncviewer
|
||||
* Fix a rare crash in the file editor dialog
|
||||
* Fix a race condition when we display the error
|
||||
* Fix an issue with invalid hostname detected as an IPV6
|
||||
* When you update a a node from the node view send settings to controller
|
||||
* Fix error when permission on the loaded image is broken
|
||||
* Fix crash with invalid image file in appliance wizard
|
||||
* Fix error when loading an handmade appliance file
|
||||
* Fix no error if your VNC client is not configured
|
||||
* Avoid high cpu usage when connection is lost
|
||||
* Support {name} in cloud template
|
||||
* Fix text of the export dialog
|
||||
* Fix error message when a project is already open
|
||||
* Fix missing info in tooltip of ethernet switch
|
||||
* The server manage the vmname when we update the linked virtual box VM
|
||||
* Fix z value for text
|
||||
* Avoid a segfault when display an error
|
||||
* Add sata options in the appliance schema
|
||||
* Fix a rare crash when exporting IOU configurations
|
||||
* Allow additionnal properties in registry files
|
||||
* Fix a potential crash when a symbol is not found
|
||||
* Strip unused code for OVA support in the registry
|
||||
* Increase the timeout for killing local server
|
||||
* Fix error when changing the layer of a drawing item
|
||||
* Fix double click for open file on OSX
|
||||
* Add debug to see the arguments use to start the application
|
||||
* Put the selected engine in the first position of the listbox
|
||||
* Fix rare crash with dynamips
|
||||
* Fix rare crash in the progress dialog
|
||||
* Fix a rare crash in console view
|
||||
* Fix crash when you drag a file inside GNS3
|
||||
|
||||
## 2.0.0 beta 3 19/01/2017
|
||||
|
||||
* Fix error if you already have an image with a different name on remote server
|
||||
* Drop gns3 converter from requirements
|
||||
* Show correct server name in tooltip
|
||||
* Menu item to open controller webpage
|
||||
* Fixes potential exception when adding network module to an IOS router. Fixes #1774.
|
||||
* Do not export a file config file if empty
|
||||
* Allow to set console type in qemu wizard
|
||||
* Fix overwrite of projects
|
||||
* Fix creation of new appliance version when filename is different
|
||||
* Fix you can't configure port 0 on ethernet switch
|
||||
* Fix a race condition when saving as a project and closing it
|
||||
* Reorder multi link when you delete one
|
||||
* Ensure we can't connect to occupy port
|
||||
* Fix AttributeError: 'QImageSvgRenderer' object has no attribute '_svg'
|
||||
* Fix Unsaved preferences in GNS3 VM warning
|
||||
* Force margins in configuration tabs.
|
||||
* Sata disk interface support for Qemu VMs.
|
||||
* Remove "sata" disk interface. Does not exist in Qemu. Ref #1749
|
||||
* Add SATA and none disk interfaces on Qemu VM configuration page. Fixes #1749.
|
||||
* Update pyqt5 from 5.7 to 5.7.1
|
||||
* Fix TypeError: argument of type 'NoneType' is not iterable
|
||||
* Fix an error when you edit readme and no projet is opened
|
||||
* Upgrade Qt 5.7
|
||||
|
||||
## 1.5.3 12/01/2017
|
||||
|
||||
* Upgrade Qt 5.7
|
||||
|
||||
## 2.0.0 beta 2 20/12/2016
|
||||
|
||||
* AUX console button text change in MainWindow.
|
||||
* Fix GNS3 Client not connecting to remote controller
|
||||
* Delete from project list deleted projects
|
||||
* Keep a shared list of projects internally
|
||||
* Fix recent files in new project dialog
|
||||
* Move recent projects to the file menu
|
||||
* Fix Tail process for wireshark trace not killed when we change project
|
||||
* Move project menu items. Ref #1713.
|
||||
* Display recent files for local controller, recent project for remote controller
|
||||
* Do not display the remote server if the server is use as a GNS3 VM
|
||||
* If the notification stream is stopped by something we auto reconnect
|
||||
* Ignore system proxy to avoid trouble with "Security Suites"
|
||||
* Avoid close and delete a project at the same time
|
||||
* Alpha sort of servers summaries
|
||||
* Fix new remote server doesn't show up in compute summary
|
||||
* Fix interface number for Switch & Hub templates
|
||||
* Fix sync of node alignements with the server
|
||||
* Fix rare condition when you close a project and add a node
|
||||
* Options -q for quiet startup
|
||||
* Fix an error when apply permission on OSX
|
||||
* Support Qemu cpus in GNS3A
|
||||
* Support for BIOS images
|
||||
* Fix IdlePC can't be found during setup wizard
|
||||
|
||||
## 2.0.0 beta 1 07/12/2016
|
||||
|
||||
* Use osascript on OSX for asking admin permission
|
||||
* Change the method for creating the tmpdir for symbols cache
|
||||
* Fix a connection error at the end of the setup wizard
|
||||
* Change how some tabs are organized or named.
|
||||
* General settings => local settings
|
||||
* Drop more reference to use local server
|
||||
* Remove local server checkbox from preferences
|
||||
* Make sure to not start local server during setup wizard remote server
|
||||
* Fix Error when editing IOS image created using .gns3a file
|
||||
* Fix test suites around sip deleted
|
||||
* Do not auto start the local server in setup wizard
|
||||
* On OSX execute all sudo in a single operation
|
||||
* Catch key Compute is missing during conversion error
|
||||
* Fix rare crash in gns3.dialogs.appliance_wizard in validateCurrentPage
|
||||
* Fix AttributeError: 'Nat' object has no attribute 'configPage'
|
||||
* Catch one more RuntimeError: wrapped C/C++
|
||||
* Fix a rare crash in port
|
||||
* Fix a rare crash when set symbol
|
||||
* Fix a potential crash
|
||||
* Fix a potential crash at exit
|
||||
* Fix crashes
|
||||
* Remove unused settings from general preferences
|
||||
* Catch error when you try to import a IOU bin as a licence
|
||||
* Fix rare crash when exiting
|
||||
* Fix crash when freeing some ressources
|
||||
* Fix timeout when exporting large project
|
||||
* Avoid a rare crash when we free a port
|
||||
* Fix you can't download symbols after you got an error
|
||||
|
||||
## 2.0.0 alpha 4 24/11/2016
|
||||
* Mark preferences changes when you change a QPlainTextEdit
|
||||
* Force the VPCS config initial file
|
||||
* Replace the IOU licence path by an input text
|
||||
* Fix 403 when loading a remote project
|
||||
* Fix some possible server not starting on Windows
|
||||
* Hide the connection refused dialog when we success to reconnect
|
||||
* Avoid a rare crash when changing topology
|
||||
* When loading another project disconnect from current project
|
||||
* Do not crash if we can't list remote list of GNS3 VM engines
|
||||
* Init the VPCS base config
|
||||
* Fix invalid ressource path on OSX
|
||||
* Fix segfault when deleting a node
|
||||
* Do not download multiple time the same symbol
|
||||
* Kill tail process when capture stop
|
||||
* Fix Topology summary contain non existing links
|
||||
* Fix a rare crash when deleting a link
|
||||
* Fix export of debug informations when not connected to the controller
|
||||
* Fix AttributeError: 'DockerVM' object has no attribute 'server'
|
||||
* Fix error message if you double click on builtin switch
|
||||
* Fix a rare crash in packet capture
|
||||
* Restrict ubridge to admin users on OSX
|
||||
* Natural sort of Nodes in topology summary
|
||||
* Drop serial console type
|
||||
* Display an error if you try to open a 0.8.x file
|
||||
* Fix tab order when editing a compute
|
||||
* Fix a crash in ethernet switch settings
|
||||
* Dissallow unknown extensions
|
||||
|
||||
## 2.0.0 alpha 3 28/10/2016
|
||||
* Fix error when opening a project from the cli with a gns3 installed via setup.py
|
||||
* Fix a rare crash in snapshot dialog
|
||||
* Fix crash when importing project on a remote server
|
||||
* Fix crash in appliance wizard
|
||||
* Fix crash when local server is not available
|
||||
* Disallow to overwrite a running project
|
||||
* Fix a rare crash when deleting a link
|
||||
* Fix appliance with wrong file name after import
|
||||
* Fix a crash at startup on Mac when coming from old GNS3 version
|
||||
* Fix key error in settings if a compute no longer exists
|
||||
* All check for vmware linked base are already made server side
|
||||
* Fix Save as is not switching to the saved project
|
||||
* Auto reopen a project if connection is lost
|
||||
* Empty the list of computes nodes when connection is lost
|
||||
* Try to fix duplicate nodes after snapshot restore on some user computer
|
||||
* Allow only IPV4 in setup wizard
|
||||
* Catch error if user tmp directory is read only
|
||||
* Raise a proper error if packet capture program is invalid
|
||||
* Fix AttributeError: 'NoneType' object has no attribute 'upper'
|
||||
* Fix rare crash when killing wireshark
|
||||
* Export debug informations also from the controller
|
||||
* Fix a crash in vm wizard
|
||||
* Fix error when uploading an images from preferences
|
||||
* Fix snap to grid when initialy drop a node in the topology
|
||||
* Optimize snap-to-grid code
|
||||
* Fix a crash with linked clone
|
||||
* Move prevent using twice the same VM when linked clone is not enable
|
||||
* Fix If you show interface label and delete the link ghost interface label will appear
|
||||
* Display short interface label instead of long version
|
||||
* Fix error AttributeError: 'NoneType' object has no attribute 'capabilities'
|
||||
* Fix PermissionError when killing local server
|
||||
* Handle empty color
|
||||
* Fix rare crash in save as
|
||||
* Fix crash in restore default server settings
|
||||
* Fix an error during import of some 0.8x projects
|
||||
|
||||
## 2.0.0 alpha 2 20/10/2016
|
||||
|
||||
* Support pure remote server for importing appliance
|
||||
* Dissallow binding GNS3 server to an IPV6 (not supported by some emulators)
|
||||
* Drop vmware host type choice in client
|
||||
* Ask user to restart GNS3 after VMware installation
|
||||
* Improve duplicate prevention in topology summary
|
||||
* Add a duplicate button in the project library dialog
|
||||
* Fix error introduce in previous commits
|
||||
* Fix duplicates in recent project list
|
||||
* Fix a project override error
|
||||
* Fix Duplicated node in node summary when restoring a snapshot
|
||||
* Fix a crash in the VMware / VirtualBox wizard
|
||||
* If console host is 0.0.0.0 use controller address
|
||||
* Fix save issue when importing an appliance
|
||||
* Strip HTML in console view logs and log files
|
||||
* Fix TypeError: _expandAllSlot() takes 1 positional argument but 2 were given
|
||||
* Fix Cannot open created project by using Recents projects
|
||||
* Update edit project Ui.
|
||||
* Update crash report key
|
||||
* Fix a crash when exporting debug without project open
|
||||
* Fix a crash in rare condition when logging informations to the console
|
||||
* Fix a crash in compute summary view
|
||||
* Add a text about how to change the topology size in 2.0 in general preferences
|
||||
* Improve warning when connection issue to GNS3 VM
|
||||
* Fix crash in setup wizard
|
||||
* Fix the wizard for creating appliance template doesn't support remote main server
|
||||
* Appliance wizard support remote controller
|
||||
* Fix Browse button is not working in the local server page in the setup wizard
|
||||
* Check if local server is running in the setup wizard
|
||||
* Hide setup wizard after first successful run
|
||||
* Import appliance and New project are display at the same time
|
||||
* Support remote controller in the setup wizard
|
||||
* Fix When importing a gns3a the correct qemu binary is not selected
|
||||
* Increase creation timeout for docker container
|
||||
* Make WaitForLambdaWorker more crash proof
|
||||
* Fix a crash when importing appliance
|
||||
* Fix error in import appliances
|
||||
* Try to fix the a segfault when importing appliance
|
||||
* Fix crash in upload images
|
||||
* Trust the server for link creation error (avoid sync issue)
|
||||
* Fix an Error in server preference page
|
||||
* Fix compatibility with remote server of 1.X
|
||||
* New appliance dialog should not be display if you cancel the setup wizard
|
||||
|
||||
## 2.0.0 alpha 1 29/09/2016
|
||||
* Save as you go
|
||||
* Smart packet capture
|
||||
* Capture on any link between any node
|
||||
* Select where to run a VPCS node
|
||||
* Delete a project from the GUI
|
||||
* Project options
|
||||
* The cloud is a real node
|
||||
* Cloud templates
|
||||
* New cloud interface
|
||||
* VPCS / Ethernet Switch / Ethernet Hub templates
|
||||
* Search OS images in multiple locations
|
||||
* Periodic extraction of startup configs for Dynamips and IOU
|
||||
* Custom cloud, Ethernet hub and Ethernet switch templates
|
||||
* Snap to grid for all objects
|
||||
* Synchronize the node templates when using multiple GUI
|
||||
* Link label style
|
||||
* New place holders in command line for opening consoles
|
||||
* %i will be replaced by the project UUID
|
||||
* %c will be replaced by the connection string
|
||||
* Export a portable project from multiple remote servers
|
||||
* New save as
|
||||
* Snapshots with remote servers
|
||||
* Better start / stop / suspend all nodes
|
||||
* Edit config
|
||||
* NAT node
|
||||
* Support for colorblind users
|
||||
* Support for non local server
|
||||
* Support for profiles
|
||||
* Suspend the GNS3VM when closing GNS3
|
||||
* Edit the scene size
|
||||
* New API
|
||||
|
||||
|
||||
## 1.5.3 rc1 20/12/2016
|
||||
|
||||
* Fix Error when editing IOS image created using .gns3a file
|
||||
* Fix error when opening a project from the cli with a gns3 installed via setup.py
|
||||
* Fix a crash at startup on Mac when coming from old GNS3 version
|
||||
* Fix an error during import of some 0.8x projects
|
||||
* Ask for restart after installing vmrun
|
||||
* Improve warning when connection issue to GNS3 VM
|
||||
* Changes wording in VM wizards.
|
||||
* Changed sentence.
|
||||
* Display an error if settings come from a more recent version of GNS3
|
||||
* Fix Error when no GNS3 VM is configured and you click on new Docker or IOU
|
||||
* Disallow / in docker container name
|
||||
* Update iTerm3 console settings
|
||||
* Fix rename ethernet switch doesn't release the name
|
||||
* Support for VNC display number in command line replacement
|
||||
* Fix a crash when a directory with image is not accessible at gns3a import
|
||||
|
||||
## 1.5.2 18/08/2016
|
||||
|
||||
* Make more clear that VMware VM are not ESXi
|
||||
* Add AppData and Desktop files
|
||||
* Fix you can not select the server for VPCS
|
||||
* Fix error when removing an interface from a cloud
|
||||
* Fix crash when scanning a directory for image and you don't have permission on a file
|
||||
* Bring back the warning dialog when no router is configured
|
||||
* Fix rare crash in server summary
|
||||
* Fix crash during export
|
||||
|
||||
## 1.5.1 06/07/2016
|
||||
|
||||
* Try to fix a crash when reseting interface label
|
||||
* Fix a crash with broken file system
|
||||
* Fix EtherSwitch default name format
|
||||
* Fix crash when you have utf-8 char in the README
|
||||
* Fix rare crash when creating a link
|
||||
* Stop node before hot unlink
|
||||
* Prevent a crash due to issue in Qt
|
||||
* Add another security to prevent client to send empty hostname
|
||||
* Fix rare crash when deleting interface from the cloud
|
||||
* Fix rare crash in topology summary view
|
||||
* Ask user to send explanation if they cross a rare error
|
||||
* Fix rare crash when deleting a node
|
||||
* Hotlink support for Docker
|
||||
* Fix typo in the a warning dialog
|
||||
* Fix Remote GNS3 VM requires local server
|
||||
* Fix AttributeError: 'NoneType' object has no attribute '_server'
|
||||
* No timeout when importing a .gns3project
|
||||
|
||||
## 1.5.0 27/06/2016
|
||||
|
||||
* Fix double extension of portable project
|
||||
* Disallow export of project with a cloud
|
||||
* Change view grid -> show the grid.
|
||||
* Check if a link can be removed from a running node. Fixes #1320.
|
||||
* Hide non implemented console options in general preferences. Ref #1315.
|
||||
* Improve snap to grid
|
||||
* Change grid color
|
||||
* Avoid a crash with snap to grid and ostinato logo
|
||||
* Add a view grid
|
||||
* Fix you can no longer capture if you start stop capture multiple time
|
||||
* A button to open the file browser with the configuration file location
|
||||
* Add snap to grid feature
|
||||
|
||||
## 1.5.0rc2 15/06/2016
|
||||
|
||||
* Ethernet0 => eth0 for docker
|
||||
* Validate appliance schema before loading it
|
||||
* Fix a rare crash when loading images
|
||||
* Fixes doctor failure with 1.5rc1. Fixes #1290.
|
||||
* Check for template name collisions.
|
||||
* Log GNS3 doctor exceptions.
|
||||
* Option to hide the new appliance template button. Fixes #1277.
|
||||
|
||||
## 1.5.0rc1 01/06/2016
|
||||
|
||||
* Avoid a segfault when exiting with debug enabled
|
||||
* Fix the GNS3 VM is visible even if deactivated
|
||||
* Do not automatically stop the GNS3 VM by default.
|
||||
* Block VMnet host traffic by default. Solves the traffic loop issue on Windows.
|
||||
* Remove tooltip for Qemu VM base mac address.
|
||||
* Fix you cannot select the remote server of your choice in qemu wizard
|
||||
* Fix issue when deleting a running container
|
||||
* Allow to block network traffic originating from the host OS for vmnet interfaces (Windows only).
|
||||
* Change tooltip for Qemu VM base MAC address.
|
||||
* Improve image import
|
||||
* Support dragging an image in the GNS3 topology from the system file browser
|
||||
* Fix an issue with import with no GNS3 VM
|
||||
* Fix error when using {} in the node name
|
||||
* Display the progress dialog after 250ms
|
||||
* Fix a crash when exporting a project with virtualbox or VMware VM
|
||||
* Set default VMware VM adapter type to e1000.
|
||||
|
||||
|
||||
## 1.5.0b1 23/05/2016
|
||||
|
||||
* Remote server selector not enabled in import appliance wizard
|
||||
* New server dialog is now windows modal
|
||||
* Fixes issue when UDPPortAllocatedSlot() is called multiple times.
|
||||
* Private-config is optional.
|
||||
* Fixes alternative IOS image selection when loading a project.
|
||||
* Accept fill_color property for rectangle/ellipse objects. Compatibility for old 1.0 projects.
|
||||
* Fixes check for NPF service and add check for NPCAP service on Windows.
|
||||
* :latest for docker image is managed server side
|
||||
* Remove unbreakable space
|
||||
* Fix Checkbox and radio button are not readable with charcoal style
|
||||
* Fix existing remotez server is not recognised
|
||||
* Fix Cannot change docker image adapter number from docker image configuration
|
||||
* Fix got an unexpected keyword argument 'ram_limit'
|
||||
* Check that both Qt and PyQt version >= 5.6 to enable high DPI scaling.
|
||||
* Check Qt version, not PyQt. Fixes #1232.
|
||||
* Fix you can not turn off the GNS3VM with remote server
|
||||
|
||||
## 1.5.0a2 10/05/2016
|
||||
|
||||
* Fix issue with PyPi
|
||||
|
||||
## 1.5.0a1 10/05/2016
|
||||
|
||||
* Rebase Qcow2 disks when starting a VM if needed
|
||||
* Docker support
|
||||
* import / export portable projects (.gns3project)
|
||||
|
||||
## 1.4.6 28/04/2016
|
||||
|
||||
@@ -11,7 +556,7 @@
|
||||
* Improve the vmrun error message
|
||||
* If we can not read the registry try to guess vmware type from vmrun path
|
||||
* Ensure that you can not duplicate an interface in a cloud
|
||||
* Dissallow removal of link of running emulator without support of hotlink
|
||||
* Disallow removal of link of running emulator without support of hotlink
|
||||
* Check PyQT version support dev version
|
||||
* Show server CPU usage if it's 0
|
||||
* Clear warnings about using linked clones with VMware Player.
|
||||
@@ -53,7 +598,7 @@
|
||||
* Allow to show a message box for test without starting GNS3
|
||||
* Drop licence for paramiko since we no longer use it
|
||||
|
||||
## 1.4.4 23/02/2016
|
||||
## 1.4.4 23/02/2016
|
||||
|
||||
* Fix crash when selecting no image in GNS3A but clicking on Download
|
||||
* Fix crash when you have a file size None (testing a new gns3a)
|
||||
@@ -79,9 +624,9 @@
|
||||
* Make VM configuration dialog modal
|
||||
* Cannot take GIF screenshots (write is not supported by Qt).
|
||||
|
||||
## 1.4.2 17/02/2016
|
||||
## 1.4.2 17/02/2016
|
||||
|
||||
* Allow gif image (not animated) since pattent expire in 2004
|
||||
* Allow gif image (not animated) since patent expire in 2004
|
||||
* Countdown before starting the GNS3 VM
|
||||
* Prevent IOU GNS3A install on Windows
|
||||
* Set timeout from 1 to 3 seconds when waiting for GNS3 VM server. Ref #1034.
|
||||
@@ -101,7 +646,7 @@
|
||||
* Check if GNS3 is not installed twice in doctor
|
||||
* Allow to add custom command to the list
|
||||
* Update Readme Python 3.4 is require
|
||||
* Allow to import unknow files via GNS3A
|
||||
* Allow to import unknown files via GNS3A
|
||||
* Fix a problem with gns3 running in background after exit
|
||||
* Move common code for ports dump to vm.py
|
||||
* Move common code _updatePortSettings to vm.py
|
||||
@@ -109,7 +654,7 @@
|
||||
* Fix a crash with corrupted topology from 1.0
|
||||
* Remove all the docker code from 1.4 gui to avoid confusion
|
||||
* Create a dialog for choosing the console command.
|
||||
* Catch error if dynamips is disabled for local and no remote available
|
||||
* Catch error if Dynamips is disabled for local and no remote available
|
||||
* Put a link for the GNS3 VM in the setup wizard
|
||||
* When importing appliance explain why options is gray
|
||||
* User configurable default name format for VMware and VirtualBox. Ref #748
|
||||
@@ -119,10 +664,10 @@
|
||||
* Fixes ValueError: cannot mmap an empty file. Fixes #723.
|
||||
* Saves the "show port names" state in topology files. Fixes #778.
|
||||
* Fix KeyError: 'midplane' when loading 7200 in some cases
|
||||
* Hide the server select box for builtin switch if dynamips local is off
|
||||
* Fix an issue where the Existing image button can disapear from wizard
|
||||
* Hide the server select box for builtin switch if Dynamips local is off
|
||||
* Fix an issue where the Existing image button can disappear from wizard
|
||||
* Fix a race condition when you ask for image list but close the windows
|
||||
* Fix alignements of VMware and VirtualBox in VM choice type
|
||||
* Fix alignments of VMware and VirtualBox in VM choice type
|
||||
* Better explanation during server choice
|
||||
* Disabled remote button when we have no remote in server wizard
|
||||
* Improved lookup for VMware host type. Fixes #970.
|
||||
@@ -136,7 +681,7 @@
|
||||
* Fix a crash with doctor on windows
|
||||
* Fix crash in doctor if ubridge path is empty
|
||||
|
||||
## 1.4.1 01/02/2016
|
||||
## 1.4.1 01/02/2016
|
||||
|
||||
* Improvement to detect VMware Player on Linux. Ref #970.
|
||||
* You can move Dock widgets everywhere
|
||||
@@ -179,7 +724,7 @@
|
||||
* Fix crash on Windows when a gui is already running
|
||||
* Add default idle-pc value for c7200-adventerprisek9-mz.155-2.XB. Fixes #389.
|
||||
|
||||
## 1.4.0rc3 05/01/2016
|
||||
## 1.4.0rc3 05/01/2016
|
||||
|
||||
* Add information about antivirus and firewall in case of connection fail
|
||||
* Change link to doc for missing router image
|
||||
@@ -271,7 +816,7 @@
|
||||
* Change text for export debug information.
|
||||
* Add informations about GNS3 VM
|
||||
|
||||
## 1.4.0rc1 12/15/2015
|
||||
## 1.4.0rc1 12/15/2015
|
||||
|
||||
* Rename an appliance if the default name is already taken
|
||||
* Existing image option should be hidden when none is available
|
||||
@@ -283,7 +828,7 @@
|
||||
* Log to console the Qt Message Boxes
|
||||
* Drops securecrt.vbs
|
||||
|
||||
## 1.4.0b5 02/11/2015
|
||||
## 1.4.0b5 02/11/2015
|
||||
|
||||
* Fix crash when loading invalid appliance file
|
||||
* Show a message is starting or is stopping in progress dialog
|
||||
@@ -305,7 +850,7 @@
|
||||
* Fix crash when using an old version of 1.4 server
|
||||
* Ensure default settings are saved when starting the app
|
||||
|
||||
## 1.4.0b4 19/10/2015
|
||||
## 1.4.0b4 19/10/2015
|
||||
|
||||
* Mockup of appliances wizard
|
||||
* Fix tests
|
||||
@@ -385,7 +930,7 @@
|
||||
* Search image by default also in the download directory
|
||||
* Fixes issue when Telnet doesn't let you to login to an appliance on Linux.
|
||||
|
||||
## 1.3.11 07/10/2015
|
||||
## 1.3.11 07/10/2015
|
||||
|
||||
* Display the version of Qt in the console
|
||||
* Catch errors when we have an infinite recursion when copying a folder
|
||||
@@ -520,7 +1065,7 @@
|
||||
* Fix issue with file upload and Qt 5.5
|
||||
* Improves the symbol dialog. Implements #514.
|
||||
|
||||
## 1.3.9 03/08/2015
|
||||
## 1.3.9 03/08/2015
|
||||
|
||||
* Catch exception when trying to launch Wireshark.
|
||||
* Backport: fixes migration of cloud interfaces.
|
||||
@@ -538,7 +1083,7 @@
|
||||
* Write GNS3 upgrade to appdata
|
||||
* Fix windows asking for upgrade to the wrong version
|
||||
|
||||
## 1.3.8 27/07/2015
|
||||
## 1.3.8 27/07/2015
|
||||
|
||||
* Fixes rare issue when adding a link. Fixes #573.
|
||||
* Backport: option to drop nvram & disk files for IOS routers in order to save disk space.
|
||||
@@ -565,7 +1110,7 @@
|
||||
* Remove ram as a mandatory dynamips settings
|
||||
* Force UTF-8 when reading server configuration file
|
||||
|
||||
## 1.4.0alpha1 09/07/2015
|
||||
## 1.4.0alpha1 09/07/2015
|
||||
|
||||
* Remove unused cloud code from the 1.4
|
||||
* Setup Wizard (to be tweaked). Implements #402.
|
||||
@@ -642,7 +1187,7 @@
|
||||
* Fixes WICs are not displayed correctly. Fixes #434.
|
||||
* Do not load settings that the GUI doesn't use.
|
||||
|
||||
## 1.3.6 16/06/2015
|
||||
## 1.3.6 16/06/2015
|
||||
|
||||
* Fix an issue with 1.4dev compatibility
|
||||
|
||||
@@ -663,7 +1208,7 @@
|
||||
* Raise error if we pass non string to Port name
|
||||
* Add basic auth support for local server
|
||||
|
||||
## 1.3.4 02/06/2015
|
||||
## 1.3.4 02/06/2015
|
||||
|
||||
* Check if an IOS image is set in the IOS router template
|
||||
* Ensure the version number is written in configuration file
|
||||
@@ -680,7 +1225,7 @@
|
||||
* Fix a rare crash in completion
|
||||
* Fix crash when loading topology in rare conditions
|
||||
|
||||
## 1.3.3 14/05/2015
|
||||
## 1.3.3 14/05/2015
|
||||
|
||||
* New inline help text for the idle-pc dialog.
|
||||
* Reactivate auto idle-pc in device contextual menu + save a chosen idle-pc value in template.
|
||||
@@ -825,7 +1370,7 @@
|
||||
* Fix issues with progress dialog
|
||||
* Fix save as
|
||||
|
||||
## 1.3.0rc2 23/03/2015
|
||||
## 1.3.0rc2 23/03/2015
|
||||
|
||||
* Fix crash when in same occasion the project name is missing
|
||||
* Update sentry key
|
||||
@@ -842,7 +1387,7 @@
|
||||
* Del key deletes selected link
|
||||
* Fix crash is no remote servers is available
|
||||
|
||||
## 1.3.0rc1 19/03/2015
|
||||
## 1.3.0rc1 19/03/2015
|
||||
|
||||
* Handle legacy snapshots
|
||||
* Add server informations for Qemu, VirtualBox and VPCS info boxes
|
||||
@@ -855,7 +1400,7 @@
|
||||
* Display a warning on console if server port is already in used
|
||||
* Display an error if server version is incorrect
|
||||
|
||||
## 1.3.0beta2 13/03/2015
|
||||
## 1.3.0beta2 13/03/2015
|
||||
|
||||
* Alternative local server shutdown (faster GUI closing on Windows).
|
||||
* Grey out local server preferences if the local server is not activated.
|
||||
@@ -864,7 +1409,7 @@
|
||||
* Support RAM setting for VirtualBox VMs.
|
||||
* Fixed duplicate VM template entries for Qemu, VirtualBox and IOU.
|
||||
|
||||
## 1.3.0beta1 11/03/2015
|
||||
## 1.3.0beta1 11/03/2015
|
||||
|
||||
* New title for VMs/Devices/routers preference pages.
|
||||
* Deactivate auto idle-pc in contextual menu while we think about a better implementation.
|
||||
@@ -893,7 +1438,7 @@
|
||||
* Fixed adapter bug with VirtualBox.
|
||||
* Fixed various errors when a project was not initialized.
|
||||
|
||||
## 1.3.0alpha1 03/03/2015
|
||||
## 1.3.0alpha1 03/03/2015
|
||||
|
||||
* No more console port and UDP tunneling settings by type of module
|
||||
* Fixe save
|
||||
@@ -914,7 +1459,7 @@
|
||||
|
||||
## 1.2.2 2015/01/16
|
||||
|
||||
### Small improvements / new features
|
||||
### Small improvements / new features
|
||||
|
||||
* EtherSwitch routers can be added and configured like other IOS routers.
|
||||
* Change hostname option in the contextual device menu.
|
||||
@@ -935,7 +1480,7 @@
|
||||
* Console switching from local/remote to remote/local while a VirtualBox VM is running.
|
||||
* Default Jungle dock location is now bottom right corner.
|
||||
|
||||
### Bug fixes
|
||||
### Bug fixes
|
||||
|
||||
* Fixed the default jungle news loading on Windows.
|
||||
* Fixed SuperPutty integration (not the default, still have to select it in the preferences).
|
||||
@@ -982,7 +1527,7 @@ Prevent GNS3 to crash on Windows when importing GNS3 config file.
|
||||
* Fix SecureCRT issue when disconnecting from an IOU device on Windows.
|
||||
* Update VPCS to version 0.6 in the all-in-one installer.
|
||||
|
||||
## 1.1 2014/11/20
|
||||
## 1.1 2014/11/20
|
||||
|
||||
* Fixed broken cloud.
|
||||
* Fixed broken remote server.
|
||||
|
||||
@@ -18,13 +18,17 @@ it on https://github.com/GNS3/gns3-gui we will take care of the triage.
|
||||
|
||||
For bugs specific to the GNS3 VM, please report on https://github.com/GNS3/gns3-vm
|
||||
|
||||
## Asking for new features
|
||||
## 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
|
||||
## 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.
|
||||
@@ -45,6 +49,6 @@ The reason we do this is to ensure, to the extent possible, that we don’t “t
|
||||
|
||||
More information there: https://github.com/GNS3/cla
|
||||
|
||||
### Pull requests
|
||||
### 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.
|
||||
|
||||
66
COPYING
66
COPYING
@@ -272,46 +272,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License notice for Apache Libcloud
|
||||
----------------------------------
|
||||
https://github.com/apache/libcloud/blob/trunk/LICENSE
|
||||
|
||||
Copyright (c) 2010-2015 The Apache Software Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
License notice for requests
|
||||
---------------------------
|
||||
https://github.com/kennethreitz/requests/blob/master/LICENSE
|
||||
|
||||
Copyright (c) 2015 Kenneth Reitz
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
License notice for gns3-converter
|
||||
---------------------------------
|
||||
https://github.com/dlintott/gns3-converter/blob/master/COPYING
|
||||
|
||||
License notice for pywin32
|
||||
--------------------------
|
||||
https://github.com/SublimeText/Pywin32/blob/master/License.txt
|
||||
@@ -516,3 +476,29 @@ 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
|
||||
|
||||
|
||||
Licence notice for aiohttp_cors
|
||||
-------------------------------
|
||||
Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>.
|
||||
|
||||
Licensed under the Apache License, Version 2.0, see LICENSE file for details.
|
||||
|
||||
https://github.com/aio-libs/aiohttp_cors
|
||||
|
||||
@@ -3,6 +3,7 @@ include AUTHORS
|
||||
include INSTALL
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
include tox.ini
|
||||
recursive-include tests *
|
||||
recursive-include gns3 *
|
||||
|
||||
15
README.rst
15
README.rst
@@ -40,15 +40,8 @@ Or start the app with --debug flag.
|
||||
Due to the fact PyQT intercept you can use a web debugger for inspecting stuff:
|
||||
https://github.com/Kozea/wdb
|
||||
|
||||
|
||||
Test with PyQT4
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to simulate a user with PyQT4:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
export GNS3_QT4=1
|
||||
python gns3/main.py
|
||||
|
||||
Security issues
|
||||
----------------
|
||||
Please contact us using contact informations available here:
|
||||
http://docs.gns3.com/1ON9JBXSeR7Nt2-Qum2o3ZX0GU86BZwlmNSUgvmqNWGY/index.html
|
||||
|
||||
|
||||
19
appveyor.yml
Normal file
19
appveyor.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
version: '{build}-{branch}'
|
||||
|
||||
image: Visual Studio 2015
|
||||
|
||||
platform: x64
|
||||
|
||||
environment:
|
||||
PYTHON: "C:\\Python36-x64"
|
||||
DISTUTILS_USE_SDK: "1"
|
||||
|
||||
install:
|
||||
- cinst nmap
|
||||
- "%PYTHON%\\python.exe -m pip install -r dev-requirements.txt"
|
||||
- "%PYTHON%\\python.exe -m pip install -r win-requirements.txt"
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- "%PYTHON%\\python.exe -m pytest -v"
|
||||
@@ -1,7 +1,6 @@
|
||||
-rrequirements.txt
|
||||
|
||||
pep8
|
||||
pytest
|
||||
pytest-pythonpath # useful for running tests outside tox
|
||||
pytest-timeout
|
||||
pytest-capturelog
|
||||
pep8==1.7.0
|
||||
pytest==3.1.0
|
||||
pytest-pythonpath==0.7.1 # useful for running tests outside tox
|
||||
pytest-timeout==1.2.0
|
||||
|
||||
@@ -33,6 +33,8 @@ 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
|
||||
sys.executable = "/Applications/GNS3.app/Contents/MacOS/gns3"
|
||||
os.environ["_"] = "/Applications/GNS3.app/Contents/MacOS/gns3"
|
||||
|
||||
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;
|
||||
@@ -19,6 +19,7 @@
|
||||
import sys
|
||||
|
||||
from .qt import QtWidgets, QtGui, QtCore
|
||||
from gns3.utils import parse_version
|
||||
from .version import __version__
|
||||
|
||||
import logging
|
||||
@@ -28,8 +29,23 @@ log = logging.getLogger(__name__)
|
||||
class Application(QtWidgets.QApplication):
|
||||
file_open_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, argv):
|
||||
def __init__(self, argv, hdpi=True):
|
||||
|
||||
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
|
||||
if hdpi:
|
||||
if sys.platform.startswith("linux"):
|
||||
log.warning("HDPI mode is enabled. HDPI support on Linux is not fully stable and GNS3 may crash depending of your version of Linux. To disabled HDPI mode please edit ~/.config/GNS3/gns3_gui.conf and set 'hdpi' to 'false'")
|
||||
self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
|
||||
self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
|
||||
else:
|
||||
log.info("HDPI mode is disabled")
|
||||
self.setAttribute(QtCore.Qt.AA_DisableHighDpiScaling)
|
||||
|
||||
super().__init__(argv)
|
||||
|
||||
# this info is necessary for QSettings
|
||||
self.setOrganizationName("GNS3")
|
||||
self.setOrganizationDomain("gns3.net")
|
||||
@@ -41,7 +57,7 @@ class Application(QtWidgets.QApplication):
|
||||
self.open_file_at_startup = None
|
||||
|
||||
def event(self, event):
|
||||
# When you double click file you receive an 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):
|
||||
|
||||
328
gns3/base_node.py
Normal file
328
gns3/base_node.py
Normal file
@@ -0,0 +1,328 @@
|
||||
# -*- 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 class for node classes.
|
||||
"""
|
||||
|
||||
from .qt import QtCore
|
||||
from .ports.port import Port
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseNode(QtCore.QObject):
|
||||
|
||||
"""
|
||||
BaseNode implementation.
|
||||
|
||||
:param module: Module instance
|
||||
:param server: client connection to a server
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
# signals used to let the GUI know about some events.
|
||||
created_signal = QtCore.Signal(int)
|
||||
started_signal = QtCore.Signal()
|
||||
stopped_signal = QtCore.Signal()
|
||||
suspended_signal = QtCore.Signal()
|
||||
updated_signal = QtCore.Signal()
|
||||
loaded_signal = QtCore.Signal()
|
||||
deleted_signal = QtCore.Signal()
|
||||
error_signal = QtCore.Signal(int, str)
|
||||
server_error_signal = QtCore.Signal(int, str)
|
||||
|
||||
_instance_count = 1
|
||||
_allocated_names = set()
|
||||
|
||||
# node statuses
|
||||
stopped = 0
|
||||
started = 1
|
||||
suspended = 2
|
||||
|
||||
# node categories
|
||||
routers = 0
|
||||
switches = 1
|
||||
end_devices = 2
|
||||
security_devices = 3
|
||||
|
||||
def __init__(self, module, compute, project):
|
||||
|
||||
super().__init__()
|
||||
# create an unique ID
|
||||
self._id = BaseNode._instance_count
|
||||
BaseNode._instance_count += 1
|
||||
|
||||
self._module = module
|
||||
self._compute = compute
|
||||
assert project is not None
|
||||
self._project = project
|
||||
self._initialized = False
|
||||
self._loading = False
|
||||
self._status = BaseNode.stopped
|
||||
self._ports = []
|
||||
self._links = set()
|
||||
|
||||
def links(self):
|
||||
"""
|
||||
Links connected to the node
|
||||
"""
|
||||
return self._links
|
||||
|
||||
def addLink(self, link):
|
||||
self._links.add(link)
|
||||
|
||||
def deleteLink(self, link):
|
||||
try:
|
||||
self._links.remove(link)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
"""
|
||||
Reset the instance count.
|
||||
"""
|
||||
|
||||
cls._instance_count = 1
|
||||
|
||||
def module(self):
|
||||
"""
|
||||
Returns this node module.
|
||||
|
||||
:returns: Module instance
|
||||
"""
|
||||
|
||||
return self._module
|
||||
|
||||
def compute(self):
|
||||
"""
|
||||
Returns this node compute.
|
||||
|
||||
:returns: Compute instance
|
||||
"""
|
||||
return self._compute
|
||||
|
||||
def project(self):
|
||||
"""
|
||||
Returns this node project.
|
||||
|
||||
:returns: Project instance
|
||||
"""
|
||||
|
||||
return self._project
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
Returns this node identifier.
|
||||
|
||||
:returns: node identifier (integer)
|
||||
"""
|
||||
|
||||
return self._id
|
||||
|
||||
def setId(self, new_id):
|
||||
"""
|
||||
Sets an identifier for this node.
|
||||
|
||||
:param new_id: node identifier (integer)
|
||||
"""
|
||||
|
||||
self._id = new_id
|
||||
|
||||
# update the instance count to avoid conflicts
|
||||
if new_id >= BaseNode._instance_count:
|
||||
BaseNode._instance_count = new_id + 1
|
||||
|
||||
def status(self):
|
||||
"""
|
||||
Returns the status of this node.
|
||||
0 = stopped, 1 = started, 2 = suspended.
|
||||
|
||||
:returns: node status (integer)
|
||||
"""
|
||||
|
||||
return self._status
|
||||
|
||||
def setStatus(self, status):
|
||||
"""
|
||||
Sets a status for this node.
|
||||
0 = stopped, 1 = started, 2 = suspended.
|
||||
|
||||
:param status: node status (integer)
|
||||
"""
|
||||
|
||||
if status == self._status:
|
||||
return
|
||||
self._status = status
|
||||
if status == self.started:
|
||||
for port in self._ports:
|
||||
# set ports as started
|
||||
port.setStatus(Port.started)
|
||||
self.started_signal.emit()
|
||||
log.info("{} has started".format(self.name()))
|
||||
elif status == self.stopped:
|
||||
for port in self._ports:
|
||||
# set ports as stopped
|
||||
port.setStatus(Port.stopped)
|
||||
self.stopped_signal.emit()
|
||||
log.info("{} has stopped".format(self.name()))
|
||||
elif status == self.suspended:
|
||||
for port in self._ports:
|
||||
# set ports as suspended
|
||||
port.setStatus(Port.suspended)
|
||||
self.suspended_signal.emit()
|
||||
log.info("{} has suspended".format(self.name()))
|
||||
|
||||
def initialized(self):
|
||||
"""
|
||||
Returns if the node has been initialized
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._initialized
|
||||
|
||||
def setInitialized(self, initialized):
|
||||
"""
|
||||
Sets if the node has been initialized
|
||||
|
||||
:param initialized: boolean
|
||||
"""
|
||||
|
||||
self._initialized = initialized
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this node.
|
||||
Must be overloaded.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def ports(self):
|
||||
"""
|
||||
Returns all the ports for this node.
|
||||
|
||||
:returns: list of Port instances
|
||||
"""
|
||||
|
||||
return self._ports
|
||||
|
||||
@staticmethod
|
||||
def defaultCategories():
|
||||
"""
|
||||
Returns the default categories.
|
||||
|
||||
:returns: dict
|
||||
"""
|
||||
|
||||
categories = {"Routers": BaseNode.routers,
|
||||
"Switches": BaseNode.switches,
|
||||
"End devices": BaseNode.end_devices,
|
||||
"Security devices": BaseNode.security_devices}
|
||||
|
||||
return categories
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
Must be overloaded.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
"""
|
||||
Returns the symbol name (for the nodes view).
|
||||
|
||||
:returns: name (string)
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def categories(self):
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node category (integer)
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Must be overloaded.
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def controllerHttpPost(self, path, callback, body={}, context={}, **kwargs):
|
||||
"""
|
||||
POST on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param body: params to send (dictionary)
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.post(path, callback, body=body, context=context, **kwargs)
|
||||
|
||||
def controllerHttpPut(self, path, callback, body={}, context={}, **kwargs):
|
||||
"""
|
||||
PUT on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param body: params to send (dictionary)
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.put(path, callback, body=body, context=context, **kwargs)
|
||||
|
||||
def controllerHttpGet(self, path, callback, context={}, **kwargs):
|
||||
"""
|
||||
Get on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param body: params to send (dictionary)
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.get(path, callback, context=context, **kwargs)
|
||||
|
||||
def controllerHttpDelete(self, path, callback, context={}, **kwargs):
|
||||
"""
|
||||
Delete on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.delete(path, callback, context=context, **kwargs)
|
||||
120
gns3/compute.py
Normal file
120
gns3/compute.py
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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 uuid
|
||||
|
||||
|
||||
class Compute:
|
||||
"""
|
||||
A compute node on the remote server
|
||||
"""
|
||||
|
||||
def __init__(self, compute_id=None):
|
||||
if compute_id is None:
|
||||
compute_id = str(uuid.uuid4())
|
||||
self._compute_id = compute_id
|
||||
self._name = compute_id
|
||||
self._connected = False
|
||||
self._protocol = "http"
|
||||
self._host = None
|
||||
self._port = 3080
|
||||
self._user = None
|
||||
self._password = None
|
||||
self._cpu_usage_percent = None
|
||||
self._memory_usage_percent = None
|
||||
self._capabilities = {
|
||||
"node_types": []
|
||||
}
|
||||
|
||||
def id(self):
|
||||
return self._compute_id
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def setName(self, name):
|
||||
self._name = name
|
||||
|
||||
def connected(self):
|
||||
return self._connected
|
||||
|
||||
def setConnected(self, value):
|
||||
self._connected = value
|
||||
|
||||
def port(self):
|
||||
return self._port
|
||||
|
||||
def setPort(self, port):
|
||||
self._port = port
|
||||
|
||||
def user(self):
|
||||
return self._user
|
||||
|
||||
def setUser(self, user):
|
||||
self._user = user
|
||||
|
||||
def setPassword(self, password):
|
||||
self._password = password
|
||||
|
||||
def protocol(self):
|
||||
return self._protocol
|
||||
|
||||
def setProtocol(self, protocol):
|
||||
self._protocol = protocol
|
||||
|
||||
def host(self):
|
||||
return self._host
|
||||
|
||||
def setHost(self, host):
|
||||
self._host = host
|
||||
|
||||
def setCpuUsagePercent(self, usage):
|
||||
self._cpu_usage_percent = usage
|
||||
|
||||
def cpuUsagePercent(self):
|
||||
return self._cpu_usage_percent
|
||||
|
||||
def setMemoryUsagePercent(self, usage):
|
||||
self._memory_usage_percent = usage
|
||||
|
||||
def memoryUsagePercent(self):
|
||||
return self._memory_usage_percent
|
||||
|
||||
def capabilities(self):
|
||||
return self._capabilities
|
||||
|
||||
def setCapabilities(self, val):
|
||||
self._capabilities = val
|
||||
|
||||
def __str__(self):
|
||||
return self._compute_id
|
||||
|
||||
def __json__(self):
|
||||
return {
|
||||
"host": self._host,
|
||||
"port": self._port,
|
||||
"protocol": self._protocol,
|
||||
"user": self._user,
|
||||
"password": self._password,
|
||||
"name": self._name,
|
||||
"compute_id": self._compute_id
|
||||
}
|
||||
|
||||
def __eq__(self, v):
|
||||
if isinstance(v, Compute):
|
||||
return self.__json__() == v.__json__()
|
||||
return False
|
||||
225
gns3/compute_manager.py
Normal file
225
gns3/compute_manager.py
Normal file
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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 .qt import QtCore
|
||||
|
||||
from .compute import Compute
|
||||
from .controller import Controller
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import urllib
|
||||
import datetime
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComputeManager(QtCore.QObject):
|
||||
created_signal = QtCore.Signal(str)
|
||||
updated_signal = QtCore.Signal(str)
|
||||
deleted_signal = QtCore.Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._computes = {}
|
||||
self._controller = Controller.instance()
|
||||
self._controller.connected_signal.connect(self._controllerConnectedSlot)
|
||||
self._controller.disconnected_signal.connect(self._controllerDisconnectedSlot)
|
||||
self._controllerConnectedSlot()
|
||||
|
||||
# If we receive fresh data from the notification feed no need to refresh via an API call
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
|
||||
self._timer = QtCore.QTimer()
|
||||
self._timer.setInterval(1000)
|
||||
self._refreshingComputes = False
|
||||
self._timer.timeout.connect(self._refreshComputesSlot)
|
||||
self._timer.start()
|
||||
|
||||
def _refreshComputesSlot(self):
|
||||
if self._refreshingComputes:
|
||||
return
|
||||
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 5:
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
self._refreshingComputes = True
|
||||
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=15)
|
||||
|
||||
def _controllerConnectedSlot(self):
|
||||
if self._controller.connected():
|
||||
self._refreshingComputes = True
|
||||
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=15)
|
||||
|
||||
def _controllerDisconnectedSlot(self):
|
||||
for compute_id in list(self._computes):
|
||||
del self._computes[compute_id]
|
||||
self.deleted_signal.emit(compute_id)
|
||||
|
||||
def _listComputesCallback(self, result, error=False, **kwargs):
|
||||
self._refreshingComputes = False
|
||||
if error is True:
|
||||
log.error("Error while getting compute list: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
for compute in result:
|
||||
self.computeDataReceivedCallback(compute)
|
||||
|
||||
def computeDataReceivedCallback(self, compute):
|
||||
"""
|
||||
Called when we received data from a compute
|
||||
node.
|
||||
"""
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
|
||||
new_node = False
|
||||
compute_id = compute["compute_id"]
|
||||
if compute_id not in self._computes:
|
||||
new_node = True
|
||||
self._computes[compute_id] = Compute(compute_id)
|
||||
|
||||
self._computes[compute_id].setName(compute["name"])
|
||||
self._computes[compute_id].setConnected(compute["connected"])
|
||||
self._computes[compute_id].setProtocol(compute["protocol"])
|
||||
self._computes[compute_id].setHost(compute["host"])
|
||||
self._computes[compute_id].setPort(compute["port"])
|
||||
self._computes[compute_id].setUser(compute["user"])
|
||||
self._computes[compute_id].setCpuUsagePercent(compute["cpu_usage_percent"])
|
||||
self._computes[compute_id].setMemoryUsagePercent(compute["memory_usage_percent"])
|
||||
self._computes[compute_id].setCapabilities(compute["capabilities"])
|
||||
|
||||
if new_node:
|
||||
self.created_signal.emit(compute_id)
|
||||
else:
|
||||
self.updated_signal.emit(compute_id)
|
||||
|
||||
def computeIsTheRemoteGNS3VM(self, compute):
|
||||
"""
|
||||
:returns: Boolean True if the remote server is the remote GNS3 VM
|
||||
"""
|
||||
if compute.id() != "local" and compute.id() != "vm":
|
||||
if self.vmCompute() and "GNS3 VM ({})".format(compute.name()) == self.vmCompute().name():
|
||||
return True
|
||||
return False
|
||||
|
||||
def computes(self):
|
||||
"""
|
||||
:returns: List of computes nodes
|
||||
"""
|
||||
computes = []
|
||||
for compute in self._computes.values():
|
||||
# We filter the remote GNS3 VM compute from the computes list
|
||||
if not self.computeIsTheRemoteGNS3VM(compute):
|
||||
computes.append(compute)
|
||||
return computes
|
||||
|
||||
def vmCompute(self):
|
||||
"""
|
||||
:returns: The GNS3 VM compute node or None
|
||||
"""
|
||||
try:
|
||||
return self._computes["vm"]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def localCompute(self):
|
||||
"""
|
||||
:returns: The local compute node or None
|
||||
"""
|
||||
try:
|
||||
return self._computes["local"]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def localPlatform(self):
|
||||
"""
|
||||
Return the platform of the local compute.
|
||||
|
||||
With a remote controller it could be different of our local platform
|
||||
"""
|
||||
c = self.localCompute()
|
||||
if c is None:
|
||||
return sys.platform
|
||||
return c.capabilities().get("platform", sys.platform)
|
||||
|
||||
def remoteComputes(self):
|
||||
"""
|
||||
:returns: List of non local and non VM computes
|
||||
"""
|
||||
return [c for c in self._computes.values() if c.id() != "local" and c.id() != "vm"]
|
||||
|
||||
def getCompute(self, compute_id):
|
||||
if compute_id.startswith("http:") or compute_id.startswith("https:"):
|
||||
u = urllib.parse.urlsplit(compute_id)
|
||||
for compute in self._computes.values():
|
||||
if "{}:{}".format(compute.host(), compute.port()) == u.netloc:
|
||||
return compute
|
||||
raise KeyError("Compute {} is missing.".format(compute_id))
|
||||
if compute_id not in self._computes:
|
||||
self._computes[compute_id] = Compute(compute_id)
|
||||
self.created_signal.emit(compute_id)
|
||||
return self._computes[compute_id]
|
||||
|
||||
def deleteCompute(self, compute_id):
|
||||
if compute_id in self._computes:
|
||||
compute = self._computes[compute_id]
|
||||
del self._computes[compute_id]
|
||||
self._controller.delete("/computes/" + compute_id, None)
|
||||
self.deleted_signal.emit(compute_id)
|
||||
|
||||
def updateList(self, computes):
|
||||
"""
|
||||
Sync an array of compute server with remote
|
||||
"""
|
||||
for compute_id in copy.copy(self._computes):
|
||||
# Delete compute on controller not in the new computes
|
||||
if compute_id in ["local", "vm"]:
|
||||
continue
|
||||
|
||||
if compute_id not in [c.id() for c in computes]:
|
||||
log.debug("Delete compute %s", compute_id)
|
||||
self.deleteCompute(compute_id)
|
||||
else:
|
||||
# Update the changed nodes
|
||||
for c in computes:
|
||||
if c.id() == compute_id and c != self._computes[compute_id]:
|
||||
log.debug("Update compute %s", compute_id)
|
||||
self._controller.put("/computes/" + compute_id, None, body=c.__json__())
|
||||
self._computes[compute_id] = c
|
||||
self.updated_signal.emit(compute_id)
|
||||
# Create the new nodes
|
||||
for compute in computes:
|
||||
if compute.id() not in self._computes:
|
||||
log.debug("Create compute %s", compute.id())
|
||||
self._controller.post("/computes", None, body=compute.__json__())
|
||||
self._computes[compute.id()] = compute
|
||||
self.created_signal.emit(compute.id())
|
||||
|
||||
@staticmethod
|
||||
def reset():
|
||||
ComputeManager._instance = None
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of ComputeManager.
|
||||
:returns: instance of ComputeManager
|
||||
"""
|
||||
|
||||
if not hasattr(ComputeManager, '_instance') or ComputeManager._instance is None:
|
||||
ComputeManager._instance = ComputeManager()
|
||||
return ComputeManager._instance
|
||||
140
gns3/compute_summary_view.py
Normal file
140
gns3/compute_summary_view.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Compute summary view that list all the compute, their status.
|
||||
"""
|
||||
|
||||
import sip
|
||||
|
||||
from .qt import QtGui, QtCore, QtWidgets
|
||||
from .compute_manager import ComputeManager
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComputeItem(QtWidgets.QTreeWidgetItem):
|
||||
|
||||
"""
|
||||
Custom item for the QTreeWidget instance
|
||||
(topology summary view).
|
||||
|
||||
:param parent: parent widget
|
||||
:param compute: Compute instance
|
||||
"""
|
||||
|
||||
def __init__(self, parent, compute):
|
||||
|
||||
super().__init__(parent)
|
||||
self._compute = compute
|
||||
self._parent = parent
|
||||
self._status = "unknown"
|
||||
|
||||
self._refreshStatusSlot()
|
||||
|
||||
def _refreshStatusSlot(self):
|
||||
"""
|
||||
Changes the icon to show the node status (started, stopped etc.)
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
|
||||
usage = None
|
||||
text = self._compute.name()
|
||||
|
||||
if self._compute.cpuUsagePercent() is not None:
|
||||
text = "{} CPU {}%, RAM {}%".format(text, self._compute.cpuUsagePercent(), self._compute.memoryUsagePercent())
|
||||
|
||||
self.setText(0, text)
|
||||
self.setToolTip(0, text + " on " + self._compute.capabilities().get("platform", ""))
|
||||
|
||||
if self._compute.connected():
|
||||
self._status = "connected"
|
||||
if usage is None or (self._compute.cpuUsagePercent() < 90 and self._compute.memoryUsagePercent() < 90):
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_green.svg'))
|
||||
else:
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_yellow.svg'))
|
||||
else:
|
||||
if self._status == "unknown":
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_gray.svg'))
|
||||
else:
|
||||
self._status = "stopped"
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
|
||||
self._parent.sortItems(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
|
||||
class ComputeSummaryView(QtWidgets.QTreeWidget):
|
||||
|
||||
"""
|
||||
Compute summary view implementation.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self._computes = {}
|
||||
|
||||
ComputeManager.instance().created_signal.connect(self._computeAddedSlot)
|
||||
ComputeManager.instance().updated_signal.connect(self._computeUpdatedSlot)
|
||||
ComputeManager.instance().deleted_signal.connect(self._computeRemovedSlot)
|
||||
for compute in ComputeManager.instance().computes():
|
||||
self._computeAddedSlot(compute.id())
|
||||
|
||||
def _computeAddedSlot(self, compute_id):
|
||||
"""
|
||||
Called when a compute is added to the list of computes
|
||||
|
||||
:params url: URL of the compute
|
||||
"""
|
||||
|
||||
compute = ComputeManager.instance().getCompute(compute_id)
|
||||
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
|
||||
return
|
||||
self._computes[compute_id] = ComputeItem(self, compute)
|
||||
|
||||
def _computeUpdatedSlot(self, compute_id):
|
||||
"""
|
||||
Called when a compute is updated
|
||||
|
||||
:params url: URL of the compute
|
||||
"""
|
||||
|
||||
if compute_id in self._computes:
|
||||
compute = ComputeManager.instance().getCompute(compute_id)
|
||||
# We hide the remote GNS3 VM
|
||||
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
|
||||
self._computeRemovedSlot(compute_id)
|
||||
else:
|
||||
self._computes[compute_id]._refreshStatusSlot()
|
||||
else:
|
||||
self._computeAddedSlot(compute_id)
|
||||
|
||||
def _computeRemovedSlot(self, compute_id):
|
||||
"""
|
||||
Called when a compute is removed to the list of computes
|
||||
|
||||
:params url: URL of the compute
|
||||
"""
|
||||
|
||||
if compute_id in self._computes:
|
||||
self.takeTopLevelItem(self.indexOfTopLevelItem(self._computes[compute_id]))
|
||||
del self._computes[compute_id]
|
||||
@@ -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
|
||||
@@ -25,13 +25,10 @@ import logging
|
||||
import struct
|
||||
import sip
|
||||
import json
|
||||
from .qt import QtCore
|
||||
|
||||
from .node import Node
|
||||
from .qt import QtCore
|
||||
from .version import __version__
|
||||
try:
|
||||
from gns3converter import __version__ as gns3converter_version
|
||||
except ImportError:
|
||||
gns3converter_version = "Not installed"
|
||||
|
||||
|
||||
class ConsoleCmd(cmd.Cmd):
|
||||
@@ -45,7 +42,6 @@ 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],
|
||||
@@ -188,14 +184,9 @@ class ConsoleCmd(cmd.Cmd):
|
||||
:param node: Node instance
|
||||
"""
|
||||
|
||||
name = node.name()
|
||||
console_port = node.console()
|
||||
console_host = node.server().host()
|
||||
try:
|
||||
from .telnet_console import telnetConsole
|
||||
telnetConsole(name, console_host, console_port)
|
||||
except (OSError, ValueError) as e:
|
||||
print("Cannot start console application: {}".format(e))
|
||||
from .telnet_console import nodeTelnetConsole
|
||||
nodeTelnetConsole(node, console_port)
|
||||
|
||||
def do_debug(self, args):
|
||||
"""
|
||||
@@ -258,55 +249,6 @@ class ConsoleCmd(cmd.Cmd):
|
||||
print("{}: no such device".format(node_name))
|
||||
continue
|
||||
|
||||
def _show_run(self, params):
|
||||
"""
|
||||
Handles the 'show run' command.
|
||||
|
||||
: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
|
||||
print(json.dumps(topology, sort_keys=True, indent=4))
|
||||
elif len(params) >= 2:
|
||||
# this is a 'show run <device_name>'
|
||||
params.pop(0)
|
||||
for param in params:
|
||||
node_name = param
|
||||
node_id = None
|
||||
|
||||
# get the node ID
|
||||
for node in self._topology.nodes():
|
||||
if node.name() == node_name:
|
||||
node_id = node.id()
|
||||
break
|
||||
|
||||
if node_id is None:
|
||||
print("{}: no such device".format(node_name))
|
||||
continue
|
||||
|
||||
if "nodes" in topology["topology"]:
|
||||
for node in topology["topology"]["nodes"]:
|
||||
if node["id"] == node_id:
|
||||
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:
|
||||
@@ -314,15 +256,6 @@ class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
Show detail information about a device:
|
||||
show device <device_name>
|
||||
|
||||
Show the whole topology:
|
||||
show run
|
||||
|
||||
Show topology info of a device:
|
||||
show run <device_name>
|
||||
|
||||
Show the GNS3 VM status
|
||||
show gns3vm
|
||||
"""
|
||||
|
||||
if '?' in args or args.strip() == "":
|
||||
@@ -332,10 +265,6 @@ class ConsoleCmd(cmd.Cmd):
|
||||
params = args.split()
|
||||
if params[0] == "device":
|
||||
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__)
|
||||
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
# 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 platform
|
||||
import sys
|
||||
import sip
|
||||
import struct
|
||||
import inspect
|
||||
import datetime
|
||||
import platform
|
||||
|
||||
from .qt import QtCore
|
||||
from .qt import QtCore, Qt
|
||||
from .topology import Topology
|
||||
from .version import __version__
|
||||
from .console_cmd import ConsoleCmd
|
||||
@@ -29,9 +30,39 @@ from .pycutext import PyCutExt
|
||||
from .modules import MODULES
|
||||
from .local_config import LocalConfig
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleLogHandler(logging.StreamHandler):
|
||||
"""
|
||||
Display log event to the console
|
||||
"""
|
||||
|
||||
def emit(self, record):
|
||||
if sip.isdeleted(self._console_view):
|
||||
return
|
||||
|
||||
message = self.format(record)
|
||||
level_no = record.levelno
|
||||
if level_no >= logging.ERROR:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "error")
|
||||
elif level_no >= logging.WARNING:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "warning")
|
||||
elif level_no >= logging.INFO:
|
||||
# To avoid noise on console we display all event only if log level is debug
|
||||
# or if we force the display in the log record
|
||||
if "show" in record.__dict__ or logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "debug")
|
||||
elif level_no >= logging.DEBUG:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "debug")
|
||||
|
||||
|
||||
class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
|
||||
# Emit this signal to write a message on console
|
||||
write_message_signal = QtCore.Signal(str, str)
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
# Set the prompt PyCutExt
|
||||
@@ -41,13 +72,10 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
# Set introduction message
|
||||
bitness = struct.calcsize("P") * 8
|
||||
current_year = datetime.date.today().year
|
||||
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {}.\n" \
|
||||
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {} and PyQt {}.\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."
|
||||
"".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, Qt.PYQT_VERSION_STR, current_year)
|
||||
|
||||
# Parent class initialization
|
||||
try:
|
||||
@@ -66,14 +94,39 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
except Exception as e:
|
||||
sys.stderr.write(e)
|
||||
|
||||
self._handleLogs()
|
||||
|
||||
if LocalConfig.instance().experimental():
|
||||
log.warning("WARNING: Experimental features enable. You can use some unfinished features and lost data.")
|
||||
|
||||
for module in MODULES:
|
||||
instance = module.instance()
|
||||
instance.notification_signal.connect(self.writeNotification)
|
||||
|
||||
self.write_message_signal.connect(self._writeMessageSlot)
|
||||
|
||||
# required for Cmd module (do_help etc.)
|
||||
self.stdout = sys.stdout
|
||||
self._topology = Topology.instance()
|
||||
|
||||
def _writeMessageSlot(self, message, level):
|
||||
if level == "error":
|
||||
self.write(message, error=True)
|
||||
elif level == "warning":
|
||||
self.write(message, warning=True)
|
||||
else:
|
||||
self.write(message)
|
||||
|
||||
def _handleLogs(self):
|
||||
"""
|
||||
Catch log message and display them
|
||||
"""
|
||||
|
||||
log = logging.getLogger()
|
||||
log_handler = ConsoleLogHandler()
|
||||
log_handler._console_view = self
|
||||
log.addHandler(log_handler)
|
||||
|
||||
def isatty(self):
|
||||
"""
|
||||
For exception handling purposes
|
||||
@@ -138,69 +191,64 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
"""
|
||||
|
||||
text = "Server notification: {}".format(message)
|
||||
self.write(text, error=True)
|
||||
self.write("\n")
|
||||
if details:
|
||||
self.write(details)
|
||||
self.write("\n")
|
||||
text += "\n" + details
|
||||
self.write_message_signal.emit(text, "info")
|
||||
|
||||
def writeError(self, node_id, message):
|
||||
def writeError(self, base_node_id, message):
|
||||
"""
|
||||
Write error messages.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: base node identifier
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
node = Topology.instance().getNode(node_id)
|
||||
node = Topology.instance().getNode(base_node_id)
|
||||
name = ""
|
||||
if node and node.name():
|
||||
name = " {}:".format(node.name())
|
||||
|
||||
text = "Error:{name} {message}".format(name=name,
|
||||
message=message)
|
||||
self.write(text, error=True)
|
||||
self.write("\n")
|
||||
self.write_message_signal.emit(text, "error")
|
||||
|
||||
def writeWarning(self, node_id, message):
|
||||
def writeWarning(self, base_node_id, message):
|
||||
"""
|
||||
Write warning messages.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: base node identifier
|
||||
:param message: warning message
|
||||
"""
|
||||
|
||||
node = Topology.instance().getNode(node_id)
|
||||
node = Topology.instance().getNode(base_node_id)
|
||||
name = ""
|
||||
if node and node.name():
|
||||
name = " {}:".format(node.name())
|
||||
|
||||
text = "Warning:{name} {message}".format(name=name,
|
||||
message=message)
|
||||
self.write(text, warning=True)
|
||||
self.write("\n")
|
||||
self.write_message_signal.emit(text, "warning")
|
||||
|
||||
def writeServerError(self, node_id, message):
|
||||
def writeServerError(self, base_node_id, message):
|
||||
"""
|
||||
Write server error messages coming from the server.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: Base node identifier
|
||||
:param code: error code
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
node = Topology.instance().getNode(node_id)
|
||||
node = Topology.instance().getNode(base_node_id)
|
||||
server = name = ""
|
||||
if node:
|
||||
if node.name():
|
||||
name = " {}:".format(node.name())
|
||||
server = "from {}".format(node.server().url())
|
||||
server = "from {}".format(node.compute().name())
|
||||
|
||||
text = "Server error {server}:{name} {message}".format(server=server,
|
||||
name=name,
|
||||
message=message)
|
||||
self.write(text, error=True)
|
||||
self.write("\n")
|
||||
self.write_message_signal.emit(text.strip(), "error")
|
||||
|
||||
def _run(self):
|
||||
"""
|
||||
|
||||
292
gns3/controller.py
Normal file
292
gns3/controller.py
Normal file
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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
|
||||
import hashlib
|
||||
import tempfile
|
||||
|
||||
from .qt import QtCore, QtGui, QtWidgets, qpartial, qslot
|
||||
from .symbol import Symbol
|
||||
from .local_server_config import LocalServerConfig
|
||||
from .settings import LOCAL_SERVER_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Controller(QtCore.QObject):
|
||||
"""
|
||||
An instance of the GNS3 server controller
|
||||
"""
|
||||
connected_signal = QtCore.Signal()
|
||||
disconnected_signal = QtCore.Signal()
|
||||
connection_failed_signal = QtCore.Signal()
|
||||
project_list_updated_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__()
|
||||
self._connected = False
|
||||
self._connecting = False
|
||||
self._cache_directory = tempfile.mkdtemp()
|
||||
self._http_client = None
|
||||
# If it's the first error we display an alert box to the user
|
||||
self._first_error = True
|
||||
self._error_dialog = None
|
||||
self._display_error = True
|
||||
self._projects = []
|
||||
|
||||
# If we do multiple call in order to download the same symbol we queue them
|
||||
self._static_asset_download_queue = {}
|
||||
|
||||
def host(self):
|
||||
return self._http_client.host()
|
||||
|
||||
def isRemote(self):
|
||||
"""
|
||||
:returns Boolean: True if the controller is remote
|
||||
"""
|
||||
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
|
||||
return not settings["auto_start"]
|
||||
|
||||
def connecting(self):
|
||||
"""
|
||||
:returns: True if connection is in progress
|
||||
"""
|
||||
return self._connecting
|
||||
|
||||
def connected(self):
|
||||
"""
|
||||
Is the controller connected
|
||||
"""
|
||||
return self._connected
|
||||
|
||||
def httpClient(self):
|
||||
"""
|
||||
:returns: HTTP client for connected to the controller
|
||||
"""
|
||||
return self._http_client
|
||||
|
||||
def setHttpClient(self, http_client):
|
||||
"""
|
||||
:param http_client: Instance of HTTP client to communicate with the server
|
||||
"""
|
||||
self._http_client = http_client
|
||||
if self._http_client:
|
||||
if self.isRemote():
|
||||
self._http_client.setMaxTimeDifferenceBetweenQueries(120)
|
||||
self._http_client.connection_connected_signal.connect(self._httpClientConnectedSlot)
|
||||
self._http_client.connection_disconnected_signal.connect(self._httpClientDisconnectedSlot)
|
||||
self._connectingToServer()
|
||||
|
||||
def setDisplayError(self, val):
|
||||
"""
|
||||
Allow error to be visible or not
|
||||
"""
|
||||
self._display_error = val
|
||||
self._first_error = True
|
||||
|
||||
def _connectingToServer(self):
|
||||
"""
|
||||
Connection process as started
|
||||
"""
|
||||
self._connected = False
|
||||
self._connecting = True
|
||||
self.get('/version', self._versionGetSlot)
|
||||
|
||||
def _httpClientDisconnectedSlot(self):
|
||||
if self._connected:
|
||||
self._connected = False
|
||||
self.disconnected_signal.emit()
|
||||
self._connectingToServer()
|
||||
|
||||
def _versionGetSlot(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Called after the inital version get
|
||||
"""
|
||||
if error:
|
||||
if self._first_error:
|
||||
self._connecting = False
|
||||
self.connection_failed_signal.emit()
|
||||
if "message" in result and self._display_error:
|
||||
self._error_dialog = QtWidgets.QMessageBox(self.parent())
|
||||
self._error_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
self._error_dialog.setWindowTitle("Connection to server")
|
||||
self._error_dialog.setText("Error when connecting to the GNS3 server:\n{}".format(result["message"]))
|
||||
self._error_dialog.setIcon(QtWidgets.QMessageBox.Critical)
|
||||
self._error_dialog.show()
|
||||
# Try to connect again in x seconds
|
||||
QtCore.QTimer.singleShot(5000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_error))
|
||||
self._first_error = False
|
||||
else:
|
||||
self._first_error = True
|
||||
if self._error_dialog:
|
||||
self._error_dialog.reject()
|
||||
self._error_dialog = None
|
||||
|
||||
def _httpClientConnectedSlot(self):
|
||||
if not self._connected:
|
||||
self._connected = True
|
||||
self._connecting = False
|
||||
self.connected_signal.emit()
|
||||
self.refreshProjectList()
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("GET", *args, **kwargs)
|
||||
|
||||
def getCompute(self, path, compute_id, *args, **kwargs):
|
||||
"""
|
||||
API get on a specific compute
|
||||
"""
|
||||
compute_id = self.__fix_compute_id(compute_id)
|
||||
path = "/computes/{}{}".format(compute_id, path)
|
||||
return self.get(path, *args, **kwargs)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("POST", *args, **kwargs)
|
||||
|
||||
def postCompute(self, path, compute_id, *args, **kwargs):
|
||||
"""
|
||||
API post on a specific compute
|
||||
"""
|
||||
compute_id = self.__fix_compute_id(compute_id)
|
||||
path = "/computes/{}{}".format(compute_id, path)
|
||||
return self.post(path, *args, **kwargs)
|
||||
|
||||
def __fix_compute_id(self, compute_id):
|
||||
"""
|
||||
Support for remote server <= 1.5
|
||||
This fix should be not require after the 2.1
|
||||
when all the appliance template will be managed
|
||||
on server
|
||||
"""
|
||||
if compute_id.startswith("http:") or compute_id.startswith("https:"):
|
||||
from .compute_manager import ComputeManager
|
||||
try:
|
||||
return ComputeManager.instance().getCompute(compute_id).id()
|
||||
except KeyError:
|
||||
return compute_id
|
||||
return compute_id
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("PUT", *args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("DELETE", *args, **kwargs)
|
||||
|
||||
def createHTTPQuery(self, method, path, *args, **kwargs):
|
||||
"""
|
||||
Forward the query to the HTTP client or controller depending of the path
|
||||
"""
|
||||
if self._http_client:
|
||||
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
|
||||
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
return self._http_client.getSynchronous(endpoint, timeout)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of Controller.
|
||||
:returns: instance of Controller
|
||||
"""
|
||||
|
||||
if not hasattr(Controller, '_instance') or Controller._instance is None:
|
||||
Controller._instance = Controller()
|
||||
return Controller._instance
|
||||
|
||||
def getStatic(self, url, callback):
|
||||
"""
|
||||
Get a URL from the /static on controller and cache it on disk
|
||||
|
||||
:param url: URL without the protocol and host part
|
||||
:param callback: Callback to call when file is ready
|
||||
"""
|
||||
|
||||
if not self._http_client:
|
||||
return
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(url.encode())
|
||||
if ".svg" in url:
|
||||
extension = ".svg"
|
||||
else:
|
||||
extension = ".png"
|
||||
path = os.path.join(self._cache_directory, m.hexdigest() + extension)
|
||||
if os.path.exists(path):
|
||||
callback(path)
|
||||
elif path in self._static_asset_download_queue:
|
||||
self._static_asset_download_queue[path].append(callback)
|
||||
else:
|
||||
self._static_asset_download_queue[path] = [callback]
|
||||
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, url, path))
|
||||
|
||||
def _getStaticCallback(self, url, path, result, error=False, raw_body=None, **kwargs):
|
||||
if error:
|
||||
log.error("Error while downloading file: {}".format(url))
|
||||
if path in self._static_asset_download_queue:
|
||||
del self._static_asset_download_queue[path]
|
||||
return
|
||||
try:
|
||||
with open(path, "wb+") as f:
|
||||
f.write(raw_body)
|
||||
except OSError as e:
|
||||
log.error("Can't write to {}: {}".format(path, str(e)))
|
||||
return
|
||||
log.debug("File stored {} for {}".format(path, url))
|
||||
for callback in self._static_asset_download_queue[path]:
|
||||
callback(path)
|
||||
del self._static_asset_download_queue[path]
|
||||
|
||||
def getSymbolIcon(self, symbol_id, callback):
|
||||
"""
|
||||
Get a QIcon for a symbol from the controller
|
||||
|
||||
:param url: URL without the protocol and host part
|
||||
:param callback: Callback to call when file is ready
|
||||
"""
|
||||
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback))
|
||||
|
||||
def _getIconCallback(self, callback, path):
|
||||
icon = QtGui.QIcon()
|
||||
icon.addFile(path)
|
||||
callback(icon)
|
||||
|
||||
def deleteProject(self, project_id, callback=None):
|
||||
Controller.instance().delete("/projects/{}".format(project_id), qpartial(self._deleteProjectCallback, callback=callback, project_id=project_id))
|
||||
|
||||
def _deleteProjectCallback(self, result, error=False, project_id=None, callback=None, **kwargs):
|
||||
if error:
|
||||
log.error("Error while deleting project: {}".format(result["message"]))
|
||||
else:
|
||||
self.refreshProjectList()
|
||||
|
||||
self._projects = [p for p in self._projects if p["project_id"] != project_id]
|
||||
|
||||
if callback:
|
||||
callback(result, error=error, **kwargs)
|
||||
|
||||
@qslot
|
||||
def refreshProjectList(self, *args):
|
||||
self.get("/projects", self._projectListCallback)
|
||||
|
||||
def _projectListCallback(self, result, error=False, **kwargs):
|
||||
if not error:
|
||||
self._projects = result
|
||||
self.project_list_updated_signal.emit()
|
||||
|
||||
def projects(self):
|
||||
return self._projects
|
||||
@@ -23,6 +23,7 @@ import struct
|
||||
|
||||
try:
|
||||
import raven
|
||||
from raven.transport.http import HTTPTransport
|
||||
RAVEN_AVAILABLE = True
|
||||
except ImportError:
|
||||
# raven is not installed with deb package in order to simplify packaging
|
||||
@@ -35,7 +36,7 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Dev build
|
||||
# Dev build
|
||||
if __version_info__[3] != 0:
|
||||
import faulthandler
|
||||
# Display a traceback in case of segfault crash. Usefull when frozen
|
||||
@@ -50,7 +51,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "sync+https://3d44e34021504514a5fb0539ae6f8f92:af41562761754b4c9beca492d1b9115d@app.getsentry.com/38506"
|
||||
DSN = "sync+https://063691a489374eda912ad454a1d80777:5ddb34d6b23c4a08b040efce23aaac78@sentry.io/38506"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = get_resource("cacert.pem")
|
||||
if cacert is not None and os.path.isfile(cacert):
|
||||
@@ -68,20 +69,21 @@ class CrashReport:
|
||||
sentry_uncaught.disabled = True
|
||||
|
||||
def captureException(self, exception, value, tb):
|
||||
from .servers import Servers
|
||||
from .local_server import LocalServer
|
||||
|
||||
local_server = Servers.instance().localServerSettings()
|
||||
local_server = LocalServer.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")
|
||||
log.warning("A .git directory exist crash report is turn off for developers. Instant exit")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
if hasattr(exception, "fingerprint"):
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, fingerprint=['{{ default }}', exception.fingerprint])
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, fingerprint=['{{ default }}', exception.fingerprint], transport=HTTPTransport)
|
||||
else:
|
||||
client = raven.Client(CrashReport.DSN, release=__version__)
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, transport=HTTPTransport)
|
||||
context = {
|
||||
"os:name": platform.system(),
|
||||
"os:release": platform.release(),
|
||||
|
||||
@@ -16,42 +16,51 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sip
|
||||
|
||||
from ..qt import QtWidgets, QtCore, QtGui, qpartial
|
||||
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
|
||||
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
|
||||
from ..image_manager import ImageManager
|
||||
from ..modules import Qemu
|
||||
from ..registry.appliance import Appliance
|
||||
from ..registry.appliance import Appliance, ApplianceError
|
||||
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 ..compute_manager import ComputeManager
|
||||
from ..controller import Controller
|
||||
from ..local_config import LocalConfig
|
||||
|
||||
|
||||
class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
images_changed_signal = QtCore.Signal()
|
||||
versions_changed_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent, path):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.images_changed_signal.connect(self._refreshVersions)
|
||||
self.versions_changed_signal.connect(self._versionRefreshedSlot)
|
||||
|
||||
self._refreshing = False
|
||||
|
||||
self._path = path
|
||||
self.setupUi(self)
|
||||
# Count how many images are curently uploading
|
||||
self._image_uploading_count = 0
|
||||
|
||||
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._registry.image_list_changed_signal.connect(self.images_changed_signal.emit)
|
||||
|
||||
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.uiRefreshPushButton.clicked.connect(self.images_changed_signal.emit)
|
||||
self.uiDownloadPushButton.clicked.connect(self._downloadPushButtonClickedSlot)
|
||||
self.uiImportPushButton.clicked.connect(self._importPushButtonClickedSlot)
|
||||
self.uiCreateVersionPushButton.clicked.connect(self._createVersionPushButtonClickedSlot)
|
||||
@@ -61,8 +70,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
if hasattr(self, "uiLoadBalanceCheckBox"):
|
||||
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
|
||||
if Controller.instance().isRemote():
|
||||
self.uiLocalRadioButton.setText("Run the appliance on the main server")
|
||||
|
||||
self.uiServerWizardPage.isComplete = self._uiServerWizardPage_isComplete
|
||||
|
||||
@@ -84,6 +93,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
type = "qemu"
|
||||
elif "iou" in self._appliance:
|
||||
type = "iou"
|
||||
elif "docker" in self._appliance:
|
||||
type = "docker"
|
||||
elif "dynamips" in self._appliance:
|
||||
type = "dynamips"
|
||||
|
||||
@@ -118,13 +129,19 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
elif self.page(page_id) == self.uiServerWizardPage:
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
for server in Servers.instance().remoteServers().values():
|
||||
self.uiRemoteServersComboBox.addItem(server.url(), server)
|
||||
if len(ComputeManager.instance().remoteComputes()) == 0:
|
||||
self.uiRemoteRadioButton.setEnabled(False)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setEnabled(True)
|
||||
for compute in ComputeManager.instance().remoteComputes():
|
||||
self.uiRemoteServersComboBox.addItem(compute.name(), compute)
|
||||
|
||||
if not GNS3VM.instance().isRunning():
|
||||
if not ComputeManager.instance().vmCompute():
|
||||
self.uiVMRadioButton.setEnabled(False)
|
||||
|
||||
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")):
|
||||
if ComputeManager.instance().localPlatform() is None:
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
elif (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
|
||||
if type == "qemu":
|
||||
# Qemu has issues on OSX and Windows we disallow usage of the local server
|
||||
if not LocalConfig.instance().experimental():
|
||||
@@ -132,20 +149,20 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
elif type != "dynamips":
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
|
||||
if GNS3VM.instance().isRunning():
|
||||
if ComputeManager.instance().vmCompute():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
elif Servers.instance().localServer().isLocalServerRunning() and self.uiLocalRadioButton.isEnabled():
|
||||
elif ComputeManager.instance().localCompute() and self.uiLocalRadioButton.isEnabled():
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
elif len(Servers.instance().remoteServers().values()) > 0:
|
||||
elif self.uiRemoteRadioButton.isEnabled():
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setChecked(False)
|
||||
|
||||
elif self.page(page_id) == self.uiFilesWizardPage:
|
||||
self._refreshVersions()
|
||||
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
|
||||
|
||||
elif self.page(page_id) == self.uiQemuWizardPage:
|
||||
Qemu.instance().getQemuBinariesFromServer(self._server, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
|
||||
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
|
||||
|
||||
elif self.page(page_id) == self.uiSummaryWizardPage:
|
||||
self.uiSummaryTreeWidget.clear()
|
||||
@@ -168,12 +185,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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))
|
||||
self._server_check = False # If the server as the capacities for running the appliance
|
||||
self.uiCheckServerLabel.setText("")
|
||||
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
|
||||
return
|
||||
self.uiCheckServerLabel.setText("")
|
||||
self.uiCheckServerLabel.setText("GNS3 server requirements is OK you can continue the installation")
|
||||
self._server_check = True
|
||||
self.next()
|
||||
|
||||
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
|
||||
"""
|
||||
@@ -194,86 +211,111 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
def _uiServerWizardPage_isComplete(self):
|
||||
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
|
||||
|
||||
def _refreshVersions(self):
|
||||
def _imageUploadedCallback(self, result, error=False, **kwargs):
|
||||
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
|
||||
|
||||
@qslot
|
||||
def _refreshVersions(self, *args):
|
||||
"""
|
||||
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()
|
||||
if self._refreshing:
|
||||
return
|
||||
self._refreshing = True
|
||||
|
||||
worker = WaitForLambdaWorker(lambda: self._resfreshDialogWorker())
|
||||
self.uiFilesWizardPage.setSubTitle("The following versions are available for " + self._appliance["product_name"] + ". Check the status of files required to install.")
|
||||
|
||||
worker = WaitForLambdaWorker(lambda: self._refreshDialogWorker())
|
||||
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"
|
||||
@qslot
|
||||
def _versionRefreshedSlot(self, *args):
|
||||
"""
|
||||
Called when we finish to scan the disk for new versions
|
||||
"""
|
||||
if self._refreshing or self.currentPage() != self.uiFilesWizardPage:
|
||||
return
|
||||
self._refreshing = True
|
||||
self.uiApplianceVersionTreeWidget.clear()
|
||||
|
||||
size += image.get("filesize", 0)
|
||||
image_widget = QtWidgets.QTreeWidgetItem(
|
||||
[
|
||||
"",
|
||||
image["filename"],
|
||||
human_filesize(image.get("filesize", 0)),
|
||||
image["status"],
|
||||
image["version"],
|
||||
image.get("md5sum", "")
|
||||
])
|
||||
for version in self._appliance["versions"]:
|
||||
top = QtWidgets.QTreeWidgetItem(self.uiApplianceVersionTreeWidget, ["{} {}".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"
|
||||
|
||||
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")))
|
||||
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:
|
||||
expand = False
|
||||
top.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
|
||||
image_widget.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)
|
||||
# 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)
|
||||
# self.uiApplianceVersionTreeWidget.setCurrentItem(top)
|
||||
if expand:
|
||||
top.setExpanded(True)
|
||||
|
||||
if len(self._appliance["versions"]) > 0:
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(0)
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(1)
|
||||
self.uiApplianceVersionTreeWidget.setCurrentItem(self.uiApplianceVersionTreeWidget.topLevelItem(0))
|
||||
self._refreshing = False
|
||||
|
||||
def _resfreshDialogWorker(self):
|
||||
def _refreshDialogWorker(self):
|
||||
"""
|
||||
Scan local directory in order to found the images on disk
|
||||
"""
|
||||
|
||||
# Docker do not have versions
|
||||
if "versions" not 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"))
|
||||
img = self._registry.search_image_file(self._appliance.emulator(), 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"
|
||||
self._refreshing = False
|
||||
self.versions_changed_signal.emit()
|
||||
|
||||
@qslot
|
||||
def _applianceVersionCurrentItemChangedSlot(self, current, previous):
|
||||
"""
|
||||
Called when user select a different item in the list of appliance files
|
||||
@@ -282,7 +324,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiImportPushButton.hide()
|
||||
self.uiExplainDownloadLabel.hide()
|
||||
|
||||
if current is None:
|
||||
if current is None or sip.isdeleted(current):
|
||||
return
|
||||
|
||||
image = current.data(1, QtCore.Qt.UserRole)
|
||||
@@ -291,14 +333,18 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiDownloadPushButton.show()
|
||||
self.uiImportPushButton.show()
|
||||
|
||||
def _downloadPushButtonClickedSlot(self):
|
||||
@qslot
|
||||
def _downloadPushButtonClickedSlot(self, *args):
|
||||
"""
|
||||
Called when user want to download an appliance images.
|
||||
He should have selected the file before.
|
||||
"""
|
||||
if self._refreshing:
|
||||
return False
|
||||
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
|
||||
if current is None:
|
||||
if current is None or sip.isdeleted(current):
|
||||
return
|
||||
|
||||
data = current.data(1, QtCore.Qt.UserRole)
|
||||
@@ -311,40 +357,48 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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):
|
||||
@qslot
|
||||
def _createVersionPushButtonClickedSlot(self, *args):
|
||||
"""
|
||||
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()
|
||||
try:
|
||||
self._appliance.create_new_version(new_version)
|
||||
except ApplianceError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Create new version", str(e))
|
||||
return
|
||||
self.images_changed_signal.emit()
|
||||
|
||||
def _importPushButtonClickedSlot(self):
|
||||
@qslot
|
||||
def _importPushButtonClickedSlot(self, *args):
|
||||
"""
|
||||
Called when user want to import an appliance images.
|
||||
He should have selected the file before.
|
||||
"""
|
||||
if self._refreshing:
|
||||
return False
|
||||
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
if not current:
|
||||
return
|
||||
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"]))
|
||||
image = Image(self._appliance.emulator(), path, filename=disk["filename"])
|
||||
try:
|
||||
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 {}.".format(image.md5sum, disk["md5sum"]))
|
||||
return
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
|
||||
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()
|
||||
image.upload(self._compute_id, callback=self._imageUploadedCallback)
|
||||
|
||||
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -365,6 +419,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiQemuListComboBox.addItem("{path}".format(path=qemu["path"]), qemu["path"])
|
||||
if self.uiQemuListComboBox.count() == 1:
|
||||
self.next()
|
||||
else:
|
||||
i = self.uiQemuListComboBox.findText(self._appliance["qemu"]["arch"], QtCore.Qt.MatchContains)
|
||||
if i != -1:
|
||||
self.uiQemuListComboBox.setCurrentIndex(i)
|
||||
|
||||
def _install(self, version):
|
||||
"""
|
||||
@@ -379,14 +437,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
||||
return False
|
||||
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
|
||||
if self._server.isLocal():
|
||||
server_string = "local"
|
||||
elif self._server.isGNS3VM():
|
||||
server_string = "vm"
|
||||
if version is None:
|
||||
appliance_configuration = self._appliance.copy()
|
||||
else:
|
||||
server_string = self._server.url()
|
||||
try:
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
except ApplianceError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
||||
return False
|
||||
|
||||
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"]))
|
||||
@@ -396,7 +454,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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])
|
||||
worker = WaitForLambdaWorker(lambda: config.add_appliance(appliance_configuration, self._compute_id), 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_():
|
||||
@@ -409,9 +467,26 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
|
||||
return True
|
||||
|
||||
def _uploadImages(self, version):
|
||||
"""
|
||||
Upload an image to the compute
|
||||
"""
|
||||
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
for image in appliance_configuration["images"]:
|
||||
if image["location"] == "local":
|
||||
image = Image(self._appliance.emulator(), image["path"], filename=image["filename"])
|
||||
image.upload(self._compute_id, self._applianceImageUploadedCallback)
|
||||
self._image_uploading_count += 1
|
||||
|
||||
def _applianceImageUploadedCallback(self, result, error=False, **kwargs):
|
||||
self._image_uploading_count -= 1
|
||||
|
||||
def nextId(self):
|
||||
if self.currentPage() == self.uiServerWizardPage:
|
||||
if "qemu" not in self._appliance:
|
||||
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:
|
||||
@@ -424,8 +499,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiFilesWizardPage:
|
||||
if self._refreshing:
|
||||
return False
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
if current is None or sip.isdeleted(current):
|
||||
return False
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
if version is None:
|
||||
return False
|
||||
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"]))
|
||||
@@ -434,32 +515,37 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
self._uploadImages(version["name"])
|
||||
|
||||
elif self.currentPage() == self.uiUsageWizardPage:
|
||||
if self._image_uploading_count > 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for image uploading")
|
||||
return False
|
||||
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
return self._install(version["name"])
|
||||
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():
|
||||
if len(ComputeManager.instance().remoteComputes()) == 0:
|
||||
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())
|
||||
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
|
||||
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
|
||||
self._compute_id = "vm"
|
||||
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
|
||||
if ComputeManager.instance().localPlatform():
|
||||
if (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().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()
|
||||
self._compute_id = "local"
|
||||
|
||||
elif self.currentPage() == self.uiQemuWizardPage:
|
||||
if self.uiQemuListComboBox.currentIndex() == -1:
|
||||
@@ -471,6 +557,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
return True
|
||||
|
||||
@qslot
|
||||
def _vmToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the VM radio button is toggled.
|
||||
@@ -481,6 +568,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
@qslot
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the remote server radio button is toggled.
|
||||
@@ -492,6 +580,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiRemoteServersGroupBox.setEnabled(True)
|
||||
self.uiRemoteServersGroupBox.show()
|
||||
|
||||
@qslot
|
||||
def _localToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the local server radio button is toggled.
|
||||
@@ -501,15 +590,3 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
def _loadBalanceToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the load balance checkbox is toggled.
|
||||
|
||||
:param checked: either the box is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersComboBox.setEnabled(False)
|
||||
else:
|
||||
self.uiRemoteServersComboBox.setEnabled(True)
|
||||
|
||||
75
gns3/dialogs/capture_dialog.py
Normal file
75
gns3/dialogs/capture_dialog.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- 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
|
||||
from gns3.ui.capture_dialog_ui import Ui_CaptureDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CaptureDialog(QtWidgets.QDialog, Ui_CaptureDialog):
|
||||
"""
|
||||
This dialog allow configure the packet capture
|
||||
"""
|
||||
|
||||
def __init__(self, parent, file_name, auto_start, ethernet_link=True):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
||||
|
||||
if ethernet_link:
|
||||
self.uiDataLinkTypeComboBox.addItem("Ethernet", "DLT_EN10MB")
|
||||
else:
|
||||
self.uiDataLinkTypeComboBox.addItem("Cisco HDLC", "DLT_C_HDLC")
|
||||
self.uiDataLinkTypeComboBox.addItem("Cisco PPP", "DLT_PPP_SERIAL")
|
||||
self.uiDataLinkTypeComboBox.addItem("Frame Relay", "DLT_FRELAY")
|
||||
self.uiDataLinkTypeComboBox.addItem("ATM", "DLT_ATM_RFC1483")
|
||||
|
||||
self.uiCaptureFileNameLineEdit.setText(file_name)
|
||||
self.uiStartCommandCheckBox.setChecked(auto_start)
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
if len(self.fileName()) == 0:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Packet capture", "Please provide a file name for the capture")
|
||||
return
|
||||
self.accept()
|
||||
|
||||
def fileName(self):
|
||||
return self.uiCaptureFileNameLineEdit.text()
|
||||
|
||||
def dataLink(self):
|
||||
"""
|
||||
Type of link for capture
|
||||
"""
|
||||
return self.uiDataLinkTypeComboBox.currentData()
|
||||
|
||||
def commandAutoStart(self):
|
||||
return self.uiStartCommandCheckBox.isChecked()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = CaptureDialog(main, "test.pcap")
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
print(dialog.dataLink())
|
||||
print(dialog.fileName())
|
||||
@@ -49,6 +49,10 @@ class ConfigurationDialog(QtWidgets.QDialog, Ui_configurationDialog):
|
||||
self._settings = settings
|
||||
self._configuration_page = configuration_page
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return self._settings
|
||||
|
||||
def on_uiButtonBox_clicked(self, button):
|
||||
"""
|
||||
Slot called when a button of the uiButtonBox is clicked.
|
||||
|
||||
@@ -22,7 +22,6 @@ 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
|
||||
|
||||
@@ -63,9 +62,6 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
|
||||
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", "")
|
||||
|
||||
@@ -24,7 +24,7 @@ import struct
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.doctor_dialog_ui import Ui_DoctorDialog
|
||||
from gns3.servers import Servers
|
||||
from gns3.local_server import LocalServer
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3 import version
|
||||
from gns3.modules.vmware import VMware
|
||||
@@ -49,14 +49,18 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
|
||||
for method in sorted(dir(self)):
|
||||
if method.startswith('check'):
|
||||
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))
|
||||
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):
|
||||
@@ -72,7 +76,7 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
|
||||
def checkLocalServerEnabled(self):
|
||||
"""Checking if the local server is enabled"""
|
||||
if Servers.instance().shouldLocalServerAutoStart() is False:
|
||||
if LocalServer.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)
|
||||
|
||||
@@ -126,7 +130,7 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
# we are root, so we should have privileged access.
|
||||
return (0, None)
|
||||
|
||||
path = Servers.instance().localServerSettings().get("ubridge_path")
|
||||
path = LocalServer.instance().localServerSettings().get("ubridge_path")
|
||||
if path is None:
|
||||
return (0, None)
|
||||
if not os.path.exists(path):
|
||||
@@ -134,18 +138,22 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
|
||||
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
|
||||
try:
|
||||
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 requires CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
|
||||
else:
|
||||
# capabilities not supported
|
||||
request_setuid = True
|
||||
except AttributeError:
|
||||
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
|
||||
return (1, "Could not determine if CAP_NET_RAW capability is set for uBridge (Python bug)".format(path=path))
|
||||
|
||||
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))
|
||||
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:admin {path} and sudo chmod 4750 {path}".format(path=path))
|
||||
return (0, None)
|
||||
|
||||
def checkDynamipsPermission(self):
|
||||
@@ -154,17 +162,21 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
# we are root, so we should have privileged access.
|
||||
return (0, None)
|
||||
|
||||
path = Servers.instance().localServerSettings().get("dynamips_path")
|
||||
path = LocalServer.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))
|
||||
try:
|
||||
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 requires CAP_NET_RAW. Run sudo setcap cap_net_raw,cap_net_admin+eip {path}".format(path=path))
|
||||
except AttributeError:
|
||||
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
|
||||
return (1, "Could not determine if CAP_NET_RAW capability is set for Dynamips (Python bug)".format(path=path))
|
||||
return (0, None)
|
||||
|
||||
def checkGNS3InstalledTwice(self):
|
||||
@@ -180,6 +192,22 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
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)"""
|
||||
|
||||
@@ -187,22 +215,18 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
return (0, None)
|
||||
|
||||
import pywintypes
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
try:
|
||||
if win32serviceutil.QueryServiceStatus("npf", None)[1] != win32service.SERVICE_RUNNING:
|
||||
return (2, "The NPF service is not running: open cmd.exe as an Administrator and type 'sc config npf start= auto && net start npf'")
|
||||
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:
|
||||
if e[0] == 1060:
|
||||
return (2, "The NPF service is not installed, please install Winpcap and reboot")
|
||||
else:
|
||||
return (2, "Could not check if the NPF service is running: {}".format(e[2]))
|
||||
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()
|
||||
# dialog.show()
|
||||
#exit_code = app.exec_()
|
||||
|
||||
124
gns3/dialogs/edit_compute_dialog.py
Normal file
124
gns3/dialogs/edit_compute_dialog.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# -*- 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.compute import Compute
|
||||
from gns3.ui.edit_compute_dialog_ui import Ui_EditComputeDialog
|
||||
|
||||
|
||||
class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
|
||||
"""
|
||||
New compute dialog.
|
||||
|
||||
:param parent: parent widget.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, compute=None):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiEnableAuthenticationCheckBox.toggled.connect(self._enableAuthenticationSlot)
|
||||
self._compute = compute
|
||||
if self._compute:
|
||||
self.uiServerNameLineEdit.setText(self._compute.name())
|
||||
self.uiServerHostLineEdit.setText(self._compute.host())
|
||||
self.uiServerPortSpinBox.setValue(self._compute.port())
|
||||
|
||||
index = self.uiServerProtocolComboBox.findText(self._compute.protocol().upper())
|
||||
self.uiServerProtocolComboBox.setCurrentIndex(index)
|
||||
|
||||
if self._compute.user():
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(True)
|
||||
self.uiServerUserLineEdit.setText(self._compute.user())
|
||||
else:
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(False)
|
||||
self.uiWarningLabel.setVisible(False)
|
||||
else:
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(False)
|
||||
self.uiWarningLabel.setVisible(False)
|
||||
self._enableAuthenticationSlot(self.uiEnableAuthenticationCheckBox.isChecked())
|
||||
|
||||
def _enableAuthenticationSlot(self, state):
|
||||
"""
|
||||
Slot to enable or not the authentication.
|
||||
"""
|
||||
|
||||
if self.uiEnableAuthenticationCheckBox.isChecked():
|
||||
self.uiServerUserLineEdit.setVisible(True)
|
||||
self.uiServerPasswordLineEdit.setVisible(True)
|
||||
self.uiServerUserLabel.setVisible(True)
|
||||
self.uiServerPasswordLabel.setVisible(True)
|
||||
else:
|
||||
self.uiServerUserLineEdit.setVisible(False)
|
||||
self.uiServerPasswordLineEdit.setVisible(False)
|
||||
self.uiServerUserLabel.setVisible(False)
|
||||
self.uiServerPasswordLabel.setVisible(False)
|
||||
|
||||
def compute(self):
|
||||
return self._compute
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
Adds a new remote compute.
|
||||
"""
|
||||
|
||||
host = self.uiServerHostLineEdit.text().strip()
|
||||
name = self.uiServerNameLineEdit.text().strip()
|
||||
protocol = self.uiServerProtocolComboBox.currentText().lower()
|
||||
port = self.uiServerPortSpinBox.value()
|
||||
user = self.uiServerUserLineEdit.text().strip()
|
||||
password = self.uiServerPasswordLineEdit.text().strip()
|
||||
|
||||
if not re.match(r"^[a-zA-Z0-9\.{}-]+$".format("\u0370-\u1CDF\u2C00-\u30FF\u4E00-\u9FBF"), host):
|
||||
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server hostname {}".format(host))
|
||||
return
|
||||
if name == "gns3vm":
|
||||
QtWidgets.QMessageBox.critical(self, "Remote compute", "{} is a reserved name".format(name))
|
||||
return
|
||||
if len(name) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server name {}".format(name))
|
||||
return
|
||||
if port is None or port < 1:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server port {}".format(port))
|
||||
return
|
||||
|
||||
if not self._compute:
|
||||
self._compute = Compute()
|
||||
self._compute.setName(name)
|
||||
self._compute.setProtocol(protocol)
|
||||
self._compute.setHost(host)
|
||||
self._compute.setPort(port)
|
||||
if self.uiEnableAuthenticationCheckBox.isChecked():
|
||||
self._compute.setUser(user)
|
||||
self._compute.setPassword(password)
|
||||
else:
|
||||
self._compute.setUser(None)
|
||||
self._compute.setPassword(None)
|
||||
|
||||
super().accept()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = EditComputeDialog(main)
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
55
gns3/dialogs/edit_project_dialog.py
Normal file
55
gns3/dialogs/edit_project_dialog.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtWidgets
|
||||
from ..topology import Topology
|
||||
from ..ui.edit_project_dialog_ui import Ui_EditProjectDialog
|
||||
|
||||
|
||||
class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
"""
|
||||
Edit current project settings
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self._project = Topology.instance().project()
|
||||
self.uiProjectNameLineEdit.setText(self._project.name())
|
||||
self.uiProjectAutoOpenCheckBox.setChecked(self._project.autoOpen())
|
||||
self.uiProjectAutoCloseCheckBox.setChecked(not self._project.autoClose())
|
||||
self.uiProjectAutoStartCheckBox.setChecked(self._project.autoStart())
|
||||
self.uiSceneWidthSpinBox.setValue(self._project.sceneWidth())
|
||||
self.uiSceneHeightSpinBox.setValue(self._project.sceneHeight())
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
Called when the dialog is closed.
|
||||
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
|
||||
if result:
|
||||
self._project.setName(self.uiProjectNameLineEdit.text())
|
||||
self._project.setAutoOpen(self.uiProjectAutoOpenCheckBox.isChecked())
|
||||
self._project.setAutoClose(not self.uiProjectAutoCloseCheckBox.isChecked())
|
||||
self._project.setAutoStart(self.uiProjectAutoStartCheckBox.isChecked())
|
||||
self._project.setSceneHeight(self.uiSceneHeightSpinBox.value())
|
||||
self._project.setSceneWidth(self.uiSceneWidthSpinBox.value())
|
||||
self._project.update()
|
||||
super().done(result)
|
||||
@@ -25,10 +25,12 @@ 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
|
||||
from gns3.controller import Controller
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
"""
|
||||
This dialog allow user to export useful information
|
||||
@@ -43,29 +45,48 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
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:
|
||||
if Controller.instance().isRemote():
|
||||
QtWidgets.QMessageBox.critical(self, "Debug", "Export debug information from a remote server is not supported")
|
||||
self.reject()
|
||||
return
|
||||
|
||||
log.info("Export debug information to %s", path)
|
||||
self._path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export debug file", None, "Zip file (*.zip)", "Zip file (*.zip)")
|
||||
|
||||
if len(self._path) == 0:
|
||||
self.reject()
|
||||
return
|
||||
|
||||
if Controller.instance().connected():
|
||||
Controller.instance().post("/debug", self._exportDebugCallback)
|
||||
else:
|
||||
self._exportDebugCallback({}, error=True)
|
||||
|
||||
def _exportDebugCallback(self, result, error=False, **kwargs):
|
||||
log.info("Export debug information to %s", self._path)
|
||||
|
||||
try:
|
||||
with ZipFile(path, 'w') as zip:
|
||||
with ZipFile(self._path, 'w') as zip:
|
||||
zip.writestr("debug.txt", self._getDebugData())
|
||||
dir = LocalConfig.configDirectory()
|
||||
dir = LocalConfig.instance().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:
|
||||
dir = os.path.join(LocalConfig.instance().configDirectory(), "debug")
|
||||
if os.path.exists(dir):
|
||||
for filename in os.listdir(dir):
|
||||
path = os.path.join(dir, filename)
|
||||
if os.path.isfile(path):
|
||||
zip.write(path, filename)
|
||||
|
||||
if self._project:
|
||||
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()
|
||||
@@ -86,6 +107,7 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
OS: {os}
|
||||
Python: {python}
|
||||
Qt: {qt}
|
||||
PyQt: {pyqt}
|
||||
CPU: {cpu}
|
||||
Memory: {memory}
|
||||
|
||||
@@ -98,7 +120,8 @@ Open connections:
|
||||
Processus:
|
||||
""".format(
|
||||
version=__version__,
|
||||
qt=QtCore.BINDING_VERSION_STR,
|
||||
qt=QtCore.QT_VERSION_STR,
|
||||
pyqt=QtCore.PYQT_VERSION_STR,
|
||||
os=platform.platform(),
|
||||
python=platform.python_version(),
|
||||
memory=psutil.virtual_memory(),
|
||||
|
||||
72
gns3/dialogs/file_editor_dialog.py
Normal file
72
gns3/dialogs/file_editor_dialog.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- 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, target, path, parent=None, default=""):
|
||||
|
||||
if parent is None:
|
||||
from gns3.main_window import MainWindow
|
||||
parent = MainWindow.instance()
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._target = target
|
||||
self._path = path
|
||||
self._default = default
|
||||
|
||||
self.setWindowTitle(target.name() + " " + os.path.basename(path))
|
||||
|
||||
self.uiRefreshButton.pressed.connect(self._refreshSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Save).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
||||
|
||||
self._refreshSlot()
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
text = self.uiFileTextEdit.toPlainText()
|
||||
self._target.post("/files/" + self._path, self._saveCallback, body=text)
|
||||
|
||||
def _saveCallback(self, result, error=False, **kwargs):
|
||||
if not error:
|
||||
self.accept()
|
||||
|
||||
def _refreshSlot(self):
|
||||
self._target.get("/files/" + self._path, self._getCallback)
|
||||
|
||||
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
|
||||
if not error:
|
||||
self.uiFileTextEdit.setText(raw_body.decode("utf-8"))
|
||||
elif result.get("status") == 404:
|
||||
if self._default:
|
||||
self.uiFileTextEdit.setText(self._default)
|
||||
121
gns3/dialogs/new_appliance_dialog.py
Normal file
121
gns3/dialogs/new_appliance_dialog.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- 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)
|
||||
elif self.uiAddVPCSRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "VPCS").uiNewVPCSPushButton.clicked.emit(False)
|
||||
elif self.uiAddCloudRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Cloud nodes").uiNewCloudNodePushButton.clicked.emit(False)
|
||||
elif self.uiAddEthernetHubRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Ethernet hubs").uiNewEthernetHubPushButton.clicked.emit(False)
|
||||
elif self.uiAddEthernetSwitchRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Ethernet switches").uiNewEthernetSwitchPushButton.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
|
||||
"""
|
||||
|
||||
panes = dialog.uiTreeWidget.findItems(name, QtCore.Qt.MatchFixedString)
|
||||
if len(panes) > 0:
|
||||
child_pane = panes[0].child(0)
|
||||
dialog.uiTreeWidget.setCurrentItem(child_pane)
|
||||
else:
|
||||
i = 0
|
||||
root = dialog.uiTreeWidget.invisibleRootItem()
|
||||
while i < root.childCount():
|
||||
root_item = root.child(i)
|
||||
x = 0
|
||||
while x < root_item.childCount():
|
||||
item = root_item.child(x)
|
||||
x += 1
|
||||
if item.text(0) == name:
|
||||
dialog.uiTreeWidget.setCurrentItem(item)
|
||||
i += 1
|
||||
dialog.addModifiedPage(dialog.uiStackedWidget.currentWidget())
|
||||
return dialog.uiStackedWidget.currentWidget()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = NewApplianceDialog(main)
|
||||
dialog._setPreferencesPane(PreferencesDialog(main), "Ethernet hubs").uiNewEthernetHubPushButton.clicked.emit(False)
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
@@ -1,140 +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
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..ui.new_project_dialog_ui import 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
|
||||
has been opened automatically when GNS3 started.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, showed_from_startup=False):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = parent
|
||||
self._project_settings = {}
|
||||
default_project_name = "untitled"
|
||||
self.uiNameLineEdit.setText(default_project_name)
|
||||
self.uiLocationLineEdit.setText(os.path.join(self._main_window.projectsDirPath(), default_project_name))
|
||||
|
||||
self.uiNameLineEdit.textEdited.connect(self._projectNameSlot)
|
||||
self.uiLocationBrowserToolButton.clicked.connect(self._projectPathSlot)
|
||||
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
|
||||
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
|
||||
|
||||
if not showed_from_startup:
|
||||
self.uiOpenProjectPushButton.hide()
|
||||
self.uiRecentProjectsPushButton.hide()
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
"""
|
||||
Event handler in order to properly handle escape.
|
||||
"""
|
||||
|
||||
if e.key() == QtCore.Qt.Key_Escape:
|
||||
self.close()
|
||||
|
||||
def _projectNameSlot(self, text):
|
||||
|
||||
project_dir = self._main_window.projectsDirPath()
|
||||
if os.path.dirname(self.uiLocationLineEdit.text()) == project_dir:
|
||||
self.uiLocationLineEdit.setText(os.path.join(project_dir, text))
|
||||
|
||||
def _projectPathSlot(self):
|
||||
"""
|
||||
Slot to select the a new project location.
|
||||
"""
|
||||
|
||||
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Project location", os.path.join(self._main_window.projectsDirPath(),
|
||||
self.uiNameLineEdit.text()))
|
||||
|
||||
if path:
|
||||
self.uiLocationLineEdit.setText(path)
|
||||
|
||||
def getNewProjectSettings(self):
|
||||
|
||||
return self._project_settings
|
||||
|
||||
def _menuTriggeredSlot(self, action):
|
||||
"""
|
||||
Closes this dialog when a recent project
|
||||
has been opened.
|
||||
|
||||
:param action: ignored.
|
||||
"""
|
||||
|
||||
self.reject()
|
||||
|
||||
def _openProjectActionSlot(self):
|
||||
"""
|
||||
Opens a project and closes this dialog.
|
||||
"""
|
||||
|
||||
self._main_window.openProjectActionSlot()
|
||||
self.reject()
|
||||
|
||||
def _showRecentProjectsSlot(self):
|
||||
"""
|
||||
lot to show all the recent projects in a menu.
|
||||
"""
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.triggered.connect(self._menuTriggeredSlot)
|
||||
for action in self._main_window._recent_file_actions:
|
||||
menu.addAction(action)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
def done(self, result):
|
||||
|
||||
if result:
|
||||
project_name = self.uiNameLineEdit.text()
|
||||
project_location = self.uiLocationLineEdit.text()
|
||||
project_type = "local"
|
||||
|
||||
if not project_name:
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project name is empty")
|
||||
return
|
||||
|
||||
if not project_location:
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project location is empty")
|
||||
return
|
||||
|
||||
if os.path.isdir(project_location):
|
||||
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"] = project_location
|
||||
self._project_settings["project_type"] = project_type
|
||||
|
||||
super().done(result)
|
||||
@@ -93,6 +93,11 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
|
||||
self.uiNodesTreeWidget.setCurrentItem(item)
|
||||
self.showConfigurationPageSlot(item, 0)
|
||||
self.splitter.setSizes([0, 600])
|
||||
elif len(self._parent_items) > 0:
|
||||
# We have multiple node we select the first group
|
||||
item = next(iter(self._parent_items.values()))
|
||||
self.uiNodesTreeWidget.setCurrentItem(item)
|
||||
self.showConfigurationPageSlot(item, 0)
|
||||
|
||||
def showConfigurationPageSlot(self, item, column):
|
||||
"""
|
||||
@@ -132,9 +137,17 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
|
||||
if page != self.uiEmptyPageWidget:
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).setEnabled(True)
|
||||
else:
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).setEnabled(False)
|
||||
|
||||
# hide the contextual help button if there is no help text
|
||||
if page.whatsThis():
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).show()
|
||||
else:
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).hide()
|
||||
|
||||
def on_uiButtonBox_clicked(self, button):
|
||||
"""
|
||||
@@ -148,6 +161,8 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
|
||||
self.applySettings()
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset):
|
||||
self.resetSettings()
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help):
|
||||
self.showHelp()
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
|
||||
QtWidgets.QDialog.reject(self)
|
||||
else:
|
||||
@@ -173,12 +188,10 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
|
||||
# all children for that group
|
||||
self.previousItem = None
|
||||
self.previousNode = None
|
||||
settings = item.child(0).settings().copy()
|
||||
node = item.child(0).node()
|
||||
page.saveSettings(settings, node, group=True)
|
||||
settings = page.saveSettings({}, node, group=True)
|
||||
for index in range(0, item.childCount()):
|
||||
child = item.child(index)
|
||||
# child.node().update(settings) #TODO: delete
|
||||
child.settings().update(settings)
|
||||
|
||||
# update the nodes with the settings
|
||||
@@ -212,6 +225,14 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
|
||||
child = item.child(index)
|
||||
child.setSettings(child.node().settings().copy())
|
||||
|
||||
def showHelp(self):
|
||||
"""
|
||||
Show contextual help for the current page.
|
||||
"""
|
||||
|
||||
page = self.uiConfigStackedWidget.currentWidget()
|
||||
if page != self.uiEmptyPageWidget and page.whatsThis():
|
||||
QtWidgets.QMessageBox.information(self, "{} help".format(page.windowTitle()), page.whatsThis().strip())
|
||||
|
||||
class ConfigurationPageItem(QtWidgets.QTreeWidgetItem):
|
||||
|
||||
|
||||
@@ -24,8 +24,12 @@ from ..ui.preferences_dialog_ui import Ui_PreferencesDialog
|
||||
from ..pages.server_preferences_page import ServerPreferencesPage
|
||||
from ..pages.general_preferences_page import GeneralPreferencesPage
|
||||
from ..pages.packet_capture_preferences_page import PacketCapturePreferencesPage
|
||||
from ..pages.gns3_vm_preferences_page import GNS3VMPreferencesPage
|
||||
from ..modules import MODULES
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
|
||||
@@ -40,18 +44,20 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi(self)
|
||||
self._modified_pages = set()
|
||||
|
||||
# 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
|
||||
# could be bigger than the screen instead of displaying scrollbars
|
||||
height = QtWidgets.QDesktopWidget().screenGeometry().height() - 100
|
||||
width = QtWidgets.QDesktopWidget().screenGeometry().width() - 100
|
||||
|
||||
self.setMaximumSize(QtCore.QSize(width, height))
|
||||
if width > 900 and self.width() < 900:
|
||||
self.resize(900, self.height())
|
||||
if height > 768 and self.height() < 768:
|
||||
self.resize(self.width(), 768)
|
||||
# 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._applyButton = self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply)
|
||||
@@ -64,10 +70,11 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
# 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
|
||||
|
||||
|
||||
self._modified_pages = set()
|
||||
|
||||
def _loadPreferencePages(self):
|
||||
"""
|
||||
@@ -78,6 +85,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
pages = [
|
||||
GeneralPreferencesPage,
|
||||
ServerPreferencesPage,
|
||||
GNS3VMPreferencesPage,
|
||||
PacketCapturePreferencesPage,
|
||||
]
|
||||
|
||||
@@ -98,6 +106,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
parent = self.uiTreeWidget
|
||||
for cls in preference_pages:
|
||||
preferences_page = cls()
|
||||
preferences_page.setParent(self)
|
||||
preferences_page.loadPreferences()
|
||||
name = preferences_page.windowTitle()
|
||||
item = QtWidgets.QTreeWidgetItem(parent)
|
||||
@@ -120,7 +129,8 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
# Class name, changed signal
|
||||
widget_to_watch = {
|
||||
QtWidgets.QLineEdit: "textChanged",
|
||||
QtWidgets.QTreeWidget: "itemChanged",
|
||||
QtWidgets.QPlainTextEdit: "textChanged",
|
||||
# QtWidgets.QTreeWidget: "itemChanged",
|
||||
QtWidgets.QComboBox: "currentIndexChanged",
|
||||
QtWidgets.QSpinBox: "valueChanged",
|
||||
QtWidgets.QAbstractButton: "pressed"
|
||||
@@ -132,10 +142,27 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
|
||||
def _preferenceChangeSlot(self, *args):
|
||||
"""
|
||||
Called when somthing change in the preference dialog
|
||||
Called when something change in the preference dialog
|
||||
"""
|
||||
self._applyButton.setEnabled(True)
|
||||
self._modified = True
|
||||
|
||||
# Found the page with the change
|
||||
widget = sender = self.sender()
|
||||
while widget.parent() != self.uiStackedWidget:
|
||||
widget = widget.parent()
|
||||
|
||||
if self.addModifiedPage(widget):
|
||||
log.debug("%s value has changed", sender.objectName())
|
||||
|
||||
def addModifiedPage(self, widget):
|
||||
"""
|
||||
:returns: True is the page is initialized and element added
|
||||
"""
|
||||
# The widget can trigger signal before the end of init due to async api call
|
||||
if not hasattr(widget, 'pageInitialized') or widget.pageInitialized():
|
||||
self._applyButton.setEnabled(True)
|
||||
self._modified_pages.add(widget)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _showPreferencesPageSlot(self, current, previous):
|
||||
"""
|
||||
@@ -157,7 +184,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
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)
|
||||
|
||||
@@ -167,15 +194,14 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
"""
|
||||
|
||||
success = True
|
||||
for item in self._items:
|
||||
preferences_page = item.data(0, QtCore.Qt.UserRole)
|
||||
for preferences_page in list(self._modified_pages):
|
||||
ok = preferences_page.savePreferences()
|
||||
# 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
|
||||
self._modified_pages = set()
|
||||
return success
|
||||
|
||||
def reject(self):
|
||||
@@ -183,10 +209,12 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
Closes this dialog.
|
||||
"""
|
||||
|
||||
if self._modified:
|
||||
if len(self._modified_pages) > 0:
|
||||
# Get the title of pages with modifications
|
||||
pages_title = ', '.join([page.windowTitle() for page in self._modified_pages])
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"Preferences",
|
||||
"You have unsaved preferences.\n\nContinue without saving?",
|
||||
"You have unsaved preferences in {}.\n\nContinue without saving?".format(pages_title),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
|
||||
104
gns3/dialogs/profile_select.py
Normal file
104
gns3/dialogs/profile_select.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- 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
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.ui.profile_select_dialog_ui import Ui_ProfileSelectDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
"""
|
||||
This dialog allow user to choose a profile of settings
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
if parent is None:
|
||||
self._main = QtWidgets.QMainWindow()
|
||||
self._main.hide()
|
||||
parent = self._main
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.uiNewPushButton.clicked.connect(self._newPushButtonSlot)
|
||||
self.uiDeletePushButton.clicked.connect(self._deletePushButtonSlot)
|
||||
|
||||
# Center on screen
|
||||
screen = QtWidgets.QApplication.desktop().screenGeometry()
|
||||
self.move(screen.center() - self.rect().center())
|
||||
|
||||
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")
|
||||
self.profiles_path = os.path.join(path, "profiles")
|
||||
|
||||
self.uiShowAtStartupCheckBox.setChecked(LocalConfig.instance().multiProfiles())
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self):
|
||||
self.uiProfileSelectComboBox.clear()
|
||||
self.uiProfileSelectComboBox.addItem("default")
|
||||
|
||||
try:
|
||||
if os.path.exists(self.profiles_path):
|
||||
for profil in sorted(os.listdir(self.profiles_path)):
|
||||
if not profil.startswith("."):
|
||||
self.uiProfileSelectComboBox.addItem(profil)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def profile(self):
|
||||
return self.uiProfileSelectComboBox.currentText()
|
||||
|
||||
def accept(self):
|
||||
LocalConfig.instance().setMultiProfiles(self.uiShowAtStartupCheckBox.isChecked())
|
||||
super().accept()
|
||||
|
||||
def _newPushButtonSlot(self):
|
||||
profile, ok = QtWidgets.QInputDialog.getText(self.parent(), "New profile", "Profile name:")
|
||||
if ok:
|
||||
self.uiProfileSelectComboBox.addItem(profile)
|
||||
self.uiProfileSelectComboBox.setCurrentText(profile)
|
||||
self.accept()
|
||||
|
||||
def _deletePushButtonSlot(self):
|
||||
profile = self.uiProfileSelectComboBox.currentText()
|
||||
if profile == "default":
|
||||
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", "You can't delete the default profile")
|
||||
else:
|
||||
try:
|
||||
shutil.rmtree(os.path.join(self.profiles_path, profile))
|
||||
self._refresh()
|
||||
except (OSError, PermissionError) as e:
|
||||
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", str(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
dialog = ProfileSelectDialog()
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
308
gns3/dialogs/project_dialog.py
Normal file
308
gns3/dialogs/project_dialog.py
Normal file
@@ -0,0 +1,308 @@
|
||||
# -*- 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
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qslot
|
||||
from ..ui.project_dialog_ui import Ui_ProjectDialog
|
||||
from ..controller import Controller
|
||||
from ..topology import Topology
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
|
||||
"""
|
||||
New project dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, default_project_name="untitled", show_open_options=True):
|
||||
"""
|
||||
:param parent: parent widget.
|
||||
:param default_project_name: Project name by default
|
||||
:param show_open_options: If true allow to open a project from the dialog
|
||||
otherwise it's just for create a project
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = parent
|
||||
self._project_settings = {}
|
||||
self.uiNameLineEdit.setText(default_project_name)
|
||||
self.uiLocationLineEdit.setText(os.path.join(Topology.instance().projectsDirPath(), default_project_name))
|
||||
|
||||
self.uiNameLineEdit.textEdited.connect(self._projectNameSlot)
|
||||
self.uiLocationBrowserToolButton.clicked.connect(self._projectPathSlot)
|
||||
self.uiSettingsPushButton.clicked.connect(self._settingsClickedSlot)
|
||||
|
||||
if show_open_options:
|
||||
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
|
||||
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
|
||||
else:
|
||||
self.uiOpenProjectGroupBox.hide()
|
||||
self.uiProjectTabWidget.removeTab(1)
|
||||
|
||||
# If the controller is remote we hide option for local file system
|
||||
if Controller.instance().isRemote():
|
||||
self.uiLocationLabel.setVisible(False)
|
||||
self.uiLocationLineEdit.setVisible(False)
|
||||
self.uiLocationBrowserToolButton.setVisible(False)
|
||||
self.uiOpenProjectPushButton.setVisible(False)
|
||||
|
||||
self.uiProjectsTreeWidget.itemDoubleClicked.connect(self._projectsTreeWidgetDoubleClickedSlot)
|
||||
self.uiDeleteProjectButton.clicked.connect(self._deleteProjectSlot)
|
||||
self.uiDuplicateProjectPushButton.clicked.connect(self._duplicateProjectSlot)
|
||||
self.uiRefreshProjectsPushButton.clicked.connect(Controller.instance().refreshProjectList)
|
||||
Controller.instance().project_list_updated_signal.connect(self._updateProjectListSlot)
|
||||
self._updateProjectListSlot()
|
||||
Controller.instance().refreshProjectList()
|
||||
|
||||
def _settingsClickedSlot(self):
|
||||
"""
|
||||
When the user click on the settings button
|
||||
"""
|
||||
self.reject()
|
||||
self._main_window.preferencesActionSlot()
|
||||
|
||||
def _projectsTreeWidgetDoubleClickedSlot(self, item, column):
|
||||
self.done(True)
|
||||
|
||||
@qslot
|
||||
def _deleteProjectSlot(self, *args):
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Delete project", "No project selected")
|
||||
return
|
||||
|
||||
projects_to_delete = set()
|
||||
for project in self.uiProjectsTreeWidget.selectedItems():
|
||||
project_id = project.data(0, QtCore.Qt.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"Delete project",
|
||||
'Delete project "{}"?\nThis cannot be reverted.'.format(project_name),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
projects_to_delete.add(project_id)
|
||||
|
||||
for project_id in projects_to_delete:
|
||||
Controller.instance().deleteProject(project_id)
|
||||
|
||||
def _duplicateProjectSlot(self):
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Duplicate project", "No project selected")
|
||||
return
|
||||
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) > 1:
|
||||
QtWidgets.QMessageBox.critical(self, "Duplicate project", "Please select only one project to duplicate")
|
||||
return
|
||||
|
||||
for project in self.uiProjectsTreeWidget.selectedItems():
|
||||
project_id = project.data(0, QtCore.Qt.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
new_project_name = project_name + "-1"
|
||||
existing_project_name = [p["name"] for p in Controller.instance().projects()]
|
||||
i = 1
|
||||
while new_project_name in existing_project_name:
|
||||
new_project_name = "{}-{}".format(project_name, i)
|
||||
i += 1
|
||||
|
||||
name, reply = QtWidgets.QInputDialog.getText(self,
|
||||
"Duplicate project",
|
||||
'Duplicate project "{}"?.'.format(project_name),
|
||||
QtWidgets.QLineEdit.Normal,
|
||||
new_project_name)
|
||||
name = name.strip()
|
||||
if reply and len(name) > 0:
|
||||
if Controller.instance().isRemote():
|
||||
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
|
||||
self._duplicateCallback,
|
||||
body={"name": name})
|
||||
else:
|
||||
project_location = os.path.join(Topology.instance().projectsDirPath(), name)
|
||||
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
|
||||
self._duplicateCallback,
|
||||
body={"name": name, "path": project_location})
|
||||
|
||||
def _duplicateCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while duplicate project: {}".format(result["message"]))
|
||||
return
|
||||
Controller.instance().refreshProjectList()
|
||||
|
||||
@qslot
|
||||
def _updateProjectListSlot(self, *args):
|
||||
self.uiProjectsTreeWidget.clear()
|
||||
self.uiDeleteProjectButton.setEnabled(False)
|
||||
self.uiProjectsTreeWidget.setUpdatesEnabled(False)
|
||||
items = []
|
||||
for project in Controller.instance().projects():
|
||||
path = os.path.join(project["path"], project["filename"])
|
||||
item = QtWidgets.QTreeWidgetItem([project["name"], project["status"], path])
|
||||
item.setData(0, QtCore.Qt.UserRole, project["project_id"])
|
||||
item.setData(1, QtCore.Qt.UserRole, project["name"])
|
||||
item.setData(2, QtCore.Qt.UserRole, path)
|
||||
items.append(item)
|
||||
self.uiProjectsTreeWidget.addTopLevelItems(items)
|
||||
|
||||
if len(Controller.instance().projects()):
|
||||
self.uiDeleteProjectButton.setEnabled(True)
|
||||
|
||||
self.uiProjectsTreeWidget.header().setResizeContentsPrecision(100) # How many row is checked for the resize for performance reason
|
||||
self.uiProjectsTreeWidget.resizeColumnToContents(0)
|
||||
self.uiProjectsTreeWidget.resizeColumnToContents(1)
|
||||
self.uiProjectsTreeWidget.resizeColumnToContents(2)
|
||||
self.uiProjectsTreeWidget.sortItems(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiProjectsTreeWidget.setUpdatesEnabled(True)
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
"""
|
||||
Event handler in order to properly handle escape.
|
||||
"""
|
||||
|
||||
if e.key() == QtCore.Qt.Key_Escape:
|
||||
self.close()
|
||||
|
||||
def _projectNameSlot(self, text):
|
||||
|
||||
project_dir = Topology.instance().projectsDirPath()
|
||||
if os.path.dirname(self.uiLocationLineEdit.text()) == project_dir:
|
||||
self.uiLocationLineEdit.setText(os.path.join(project_dir, text))
|
||||
|
||||
def _projectPathSlot(self):
|
||||
"""
|
||||
Slot to select the a new project location.
|
||||
"""
|
||||
|
||||
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Project location", os.path.join(Topology.instance().projectsDirPath(),
|
||||
self.uiNameLineEdit.text()))
|
||||
|
||||
if path:
|
||||
self.uiNameLineEdit.setText(os.path.basename(path))
|
||||
self.uiLocationLineEdit.setText(path)
|
||||
|
||||
def getProjectSettings(self):
|
||||
|
||||
return self._project_settings
|
||||
|
||||
def _menuTriggeredSlot(self, action):
|
||||
"""
|
||||
Closes this dialog when a recent project
|
||||
has been opened.
|
||||
|
||||
:param action: ignored.
|
||||
"""
|
||||
|
||||
self.reject()
|
||||
|
||||
def _openProjectActionSlot(self):
|
||||
"""
|
||||
Opens a project and closes this dialog.
|
||||
"""
|
||||
|
||||
self._main_window.openProjectActionSlot()
|
||||
self.reject()
|
||||
|
||||
def _showRecentProjectsSlot(self):
|
||||
"""
|
||||
lot to show all the recent projects in a menu.
|
||||
"""
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.triggered.connect(self._menuTriggeredSlot)
|
||||
if Controller.instance().isRemote():
|
||||
for action in self._main_window.recent_project_actions:
|
||||
menu.addAction(action)
|
||||
else:
|
||||
for action in self._main_window.recent_file_actions:
|
||||
menu.addAction(action)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
def _overwriteProjectCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
# A 404 could arrive if someone else as deleted the project
|
||||
if "status" not in result or result["status"] != 404:
|
||||
return
|
||||
elif "message" in result:
|
||||
QtWidgets.QMessageBox.critical(self,
|
||||
"New Project",
|
||||
"Error while overwrite project: {}".format(result["message"]))
|
||||
Controller.instance().refreshProjectList()
|
||||
self.done(True)
|
||||
|
||||
def _newProject(self):
|
||||
self._project_settings["project_name"] = self.uiNameLineEdit.text().strip()
|
||||
if Controller.instance().isRemote():
|
||||
self._project_settings.pop("project_path", None)
|
||||
self._project_settings.pop("project_files_dir", None)
|
||||
else:
|
||||
project_location = self.uiLocationLineEdit.text().strip()
|
||||
if not project_location:
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project location is empty")
|
||||
return False
|
||||
|
||||
self._project_settings["project_path"] = os.path.join(project_location, self._project_settings["project_name"] + ".gns3")
|
||||
self._project_settings["project_files_dir"] = project_location
|
||||
|
||||
if len(self._project_settings["project_name"]) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project name is empty")
|
||||
return False
|
||||
|
||||
for existing_project in Controller.instance().projects():
|
||||
if self._project_settings["project_name"] == existing_project["name"] \
|
||||
or ("project_files_dir" in self._project_settings and self._project_settings["project_files_dir"] == existing_project["path"]):
|
||||
|
||||
if existing_project["status"] == "opened":
|
||||
QtWidgets.QMessageBox.critical(self,
|
||||
"New project",
|
||||
"Project {} is open you can not overwrite it".format(self._project_settings["project_name"]))
|
||||
return False
|
||||
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"New project",
|
||||
"Project {} already exists, overwrite it?".format(existing_project["name"]),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
Controller.instance().deleteProject(existing_project["project_id"], self._overwriteProjectCallback)
|
||||
|
||||
# In all cases we cancel the new project and if project success to delete
|
||||
# we will call done again
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def done(self, result):
|
||||
|
||||
if result:
|
||||
if self.uiProjectTabWidget.currentIndex() == 0:
|
||||
if not self._newProject():
|
||||
return
|
||||
else:
|
||||
current = self.uiProjectsTreeWidget.currentItem()
|
||||
if current is None:
|
||||
QtWidgets.QMessageBox.critical(self, "Open project", "No project selected")
|
||||
return
|
||||
|
||||
self._project_settings["project_id"] = current.data(0, QtCore.Qt.UserRole)
|
||||
self._project_settings["project_name"] = current.data(1, QtCore.Qt.UserRole)
|
||||
super().done(result)
|
||||
@@ -17,19 +17,23 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import psutil
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets, QtGui
|
||||
from gns3.servers import Servers
|
||||
from ..gns3_vm import GNS3VM
|
||||
from ..dialogs.preferences_dialog import PreferencesDialog
|
||||
from gns3.qt import QtCore, QtWidgets, QtGui, QtNetwork, qslot
|
||||
from gns3.controller import Controller
|
||||
from gns3.local_server import LocalServer
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
|
||||
|
||||
from ..settings import DEFAULT_LOCAL_SERVER_HOST
|
||||
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__
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
"""
|
||||
@@ -41,12 +45,23 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._gns3_vm_settings = {
|
||||
"enable": True,
|
||||
"headless": False,
|
||||
"when_exit": "stop",
|
||||
"engine": "vmware",
|
||||
"vcpus": 1,
|
||||
"ram": 2048,
|
||||
"vmname": "GNS3 VM"
|
||||
}
|
||||
|
||||
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.uiLocalServerToolButton.clicked.connect(self._localServerBrowserSlot)
|
||||
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('')
|
||||
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
|
||||
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
|
||||
@@ -61,11 +76,44 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
self.uiVmwareRadioButton.setChecked(False)
|
||||
self.uiVirtualBoxRadioButton.setChecked(False)
|
||||
|
||||
# Mandatory fields
|
||||
self.uiLocalServerWizardPage.registerField("path*", self.uiLocalServerPathLineEdit)
|
||||
|
||||
# load all available addresses
|
||||
for address in QtNetwork.QNetworkInterface.allAddresses():
|
||||
address_string = address.toString()
|
||||
if address.protocol() != QtNetwork.QAbstractSocket.IPv6Protocol:
|
||||
self.uiLocalServerHostComboBox.addItem(address_string, address.toString())
|
||||
|
||||
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"))
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
self.uiVMRadioButton.setText("Run the topologies in an isolated and standard VM")
|
||||
self.uiLocalRadioButton.setText("Run the topologies on my computer")
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
self.uiLocalLabel.setVisible(False)
|
||||
|
||||
Controller.instance().connected_signal.connect(self._refreshLocalServerStatusSlot)
|
||||
Controller.instance().connection_failed_signal.connect(self._refreshLocalServerStatusSlot)
|
||||
|
||||
def _localServerBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select a local server.
|
||||
"""
|
||||
|
||||
filter = ""
|
||||
if sys.platform.startswith("win"):
|
||||
filter = "Executable (*.exe);;All files (*.*)"
|
||||
server_path = shutil.which("gns3server")
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select the local server", server_path, filter)
|
||||
if not path:
|
||||
return
|
||||
|
||||
self.uiLocalServerPathLineEdit.setText(path)
|
||||
|
||||
def _VMwareBannerButtonClickedSlot(self):
|
||||
if sys.platform.startswith("darwin"):
|
||||
url = "http://send.onenetworkdirect.net/z/616461/CD225091/"
|
||||
@@ -79,12 +127,12 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
"""
|
||||
|
||||
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{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.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/")
|
||||
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/. After installation you need to restart GNS3.")
|
||||
return
|
||||
self._refreshVMListSlot()
|
||||
|
||||
@@ -93,13 +141,14 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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")
|
||||
QtWidgets.QMessageBox.critical(self, "VirtualBox", "VBoxManage could not be found, VirtualBox is probably not installed. After installation you need to restart GNS3.")
|
||||
return
|
||||
self._refreshVMListSlot()
|
||||
|
||||
@@ -118,6 +167,13 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
dialog.uiTreeWidget.setCurrentItem(child_pane)
|
||||
return dialog.uiStackedWidget.currentWidget()
|
||||
|
||||
def _getSettingsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while get gettings: {}".format(result["message"]))
|
||||
return
|
||||
self._gns3_vm_settings = result
|
||||
|
||||
def initializePage(self, page_id):
|
||||
"""
|
||||
Initialize Wizard pages.
|
||||
@@ -126,105 +182,172 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
"""
|
||||
|
||||
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)
|
||||
if self.page(page_id) == self.uiServerWizardPage:
|
||||
Controller.instance().setDisplayError(False)
|
||||
Controller.instance().get("/gns3vm", self._getSettingsCallback)
|
||||
elif self.page(page_id) == self.uiVMWizardPage:
|
||||
if self._GNS3VMSettings()["engine"] == "vmware":
|
||||
self.uiVmwareRadioButton.setChecked(True)
|
||||
self._listVMwareVMsSlot()
|
||||
elif self._GNS3VMSettings()["engine"] == "virtualbox":
|
||||
self.uiVirtualBoxRadioButton.setChecked(True)
|
||||
self._listVirtualBoxVMsSlot()
|
||||
self.uiCPUSpinBox.setValue(self._GNS3VMSettings()["vcpus"])
|
||||
self.uiRAMSpinBox.setValue(self._GNS3VMSettings()["ram"])
|
||||
|
||||
elif self.page(page_id) == self.uiLocalServerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self.uiLocalServerPathLineEdit.setText(local_server_settings["path"])
|
||||
index = self.uiLocalServerHostComboBox.findData(local_server_settings["host"])
|
||||
if index != -1:
|
||||
self.uiLocalServerHostComboBox.setCurrentIndex(index)
|
||||
self.uiLocalServerPortSpinBox.setValue(local_server_settings["port"])
|
||||
|
||||
elif self.page(page_id) == self.uiRemoteControllerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
if local_server_settings["host"] is None:
|
||||
self.uiRemoteMainServerHostLineEdit.setText(DEFAULT_LOCAL_SERVER_HOST)
|
||||
self.uiRemoteMainServerAuthCheckBox.setChecked(False)
|
||||
self.uiRemoteMainServerUserLineEdit.setText("")
|
||||
self.uiRemoteMainServerPasswordLineEdit.setText("")
|
||||
else:
|
||||
self.uiRemoteMainServerHostLineEdit.setText(local_server_settings["host"])
|
||||
self.uiRemoteMainServerAuthCheckBox.setChecked(local_server_settings["auth"])
|
||||
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["user"])
|
||||
self.uiRemoteMainServerPasswordLineEdit.setText(local_server_settings["password"])
|
||||
self.uiRemoteMainServerPortSpinBox.setValue(local_server_settings["port"])
|
||||
self.uiRemoteMainServerProtocolComboBox.setCurrentText(local_server_settings["protocol"])
|
||||
elif self.page(page_id) == self.uiLocalServerStatusWizardPage:
|
||||
self._refreshLocalServerStatusSlot()
|
||||
|
||||
elif self.page(page_id) == self.uiSummaryWizardPage:
|
||||
self.uiSummaryTreeWidget.clear()
|
||||
if self.uiLocalRadioButton.isChecked():
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self._addSummaryEntry("Server type:", "Local")
|
||||
self._addSummaryEntry("Path:", local_server_settings["path"])
|
||||
self._addSummaryEntry("Host:", local_server_settings["host"])
|
||||
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
|
||||
elif self.uiRemoteControllerRadioButton.isChecked():
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self._addSummaryEntry("Server type:", "Remote")
|
||||
self._addSummaryEntry("Host:", local_server_settings["host"])
|
||||
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
|
||||
self._addSummaryEntry("User:", local_server_settings["user"])
|
||||
else:
|
||||
self._addSummaryEntry("Server type:", "GNS3 Virtual Machine")
|
||||
self._addSummaryEntry("VM engine:", self._GNS3VMSettings()["engine"].capitalize())
|
||||
self._addSummaryEntry("VM name:", self._GNS3VMSettings()["vmname"])
|
||||
self._addSummaryEntry("VM vCPUs:", str(self._GNS3VMSettings()["vcpus"]))
|
||||
self._addSummaryEntry("VM RAM:", str(self._GNS3VMSettings()["ram"]) + " MB")
|
||||
|
||||
@qslot
|
||||
def _refreshLocalServerStatusSlot(self):
|
||||
"""
|
||||
Refresh the local server status page
|
||||
"""
|
||||
if Controller.instance().connected():
|
||||
self.uiLocalServerStatusLabel.setText("Connection to local server successful")
|
||||
Controller.instance().get("/gns3vm", self._getSettingsCallback)
|
||||
elif Controller.instance().connecting():
|
||||
self.uiLocalServerStatusLabel.setText("Please wait connection to the GNS3 server")
|
||||
else:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self.uiLocalServerStatusLabel.setText("Connection to local server failed.\n* Make sure GNS3 is allowed in your firewall.\n* Go back and try to change the server port\n* Please check with a browser if you can connect to {protocol}://{host}:{port}.\n* Try to run {path} in a terminal to see if you have an error if the above does not work.".format(protocol=local_server_settings["protocol"], host=local_server_settings["host"], port=local_server_settings["port"], path=local_server_settings["path"]))
|
||||
|
||||
def _GNS3VMSettings(self):
|
||||
return self._gns3_vm_settings
|
||||
|
||||
def _setGNS3VMSettings(self, settings):
|
||||
Controller.instance().put("/gns3vm", self._saveSettingsCallback, settings, timeout=60 * 5)
|
||||
|
||||
def _saveSettingsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while save settings: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def _addSummaryEntry(self, name, value):
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiSummaryTreeWidget, [name, value])
|
||||
item.setText(0, name)
|
||||
font = item.font(0)
|
||||
font.setBold(True)
|
||||
item.setFont(0, font)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the settings.
|
||||
"""
|
||||
|
||||
gns3_vm = GNS3VM.instance()
|
||||
servers = Servers.instance()
|
||||
Controller.instance().setDisplayError(True)
|
||||
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()}
|
||||
vm_settings = self._GNS3VMSettings()
|
||||
vm_settings["enable"] = True
|
||||
vm_settings["vmname"] = vmname
|
||||
|
||||
if self.uiVmwareRadioButton.isChecked():
|
||||
vm_settings["virtualization"] = "VMware"
|
||||
vm_settings["engine"] = "vmware"
|
||||
elif self.uiVirtualBoxRadioButton.isChecked():
|
||||
vm_settings["virtualization"] = "VirtualBox"
|
||||
gns3_vm.setSettings(vm_settings)
|
||||
servers.save()
|
||||
vm_settings["engine"] = "virtualbox"
|
||||
|
||||
# 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")
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM memory", "It is recommended to allocate a minimum of 1024 MB of memory to the GNS3 VM")
|
||||
vm_settings["vcpus"] = vpcus
|
||||
vm_settings["ram"] = ram
|
||||
|
||||
# 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_()
|
||||
self._setGNS3VMSettings(vm_settings)
|
||||
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:
|
||||
elif self.currentPage() == self.uiLocalServerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
local_server_settings["auto_start"] = True
|
||||
local_server_settings["path"] = self.uiLocalServerPathLineEdit.text().strip()
|
||||
local_server_settings["host"] = self.uiLocalServerHostComboBox.itemData(self.uiLocalServerHostComboBox.currentIndex())
|
||||
local_server_settings["port"] = self.uiLocalServerPortSpinBox.value()
|
||||
|
||||
use_local_server = self.uiLocalRadioButton.isChecked()
|
||||
if use_local_server:
|
||||
if not os.path.isfile(local_server_settings["path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "Local server", "Could not find local server {}".format(local_server_settings["path"]))
|
||||
return False
|
||||
if not os.access(local_server_settings["path"], os.X_OK):
|
||||
QtWidgets.QMessageBox.critical(self, "Local server", "{} is not an executable".format(local_server_settings["path"]))
|
||||
return False
|
||||
|
||||
LocalServer.instance().updateLocalServerSettings(local_server_settings)
|
||||
LocalServer.instance().localServerAutoStartIfRequire()
|
||||
|
||||
elif self.currentPage() == self.uiRemoteControllerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
local_server_settings["auto_start"] = False
|
||||
local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
|
||||
local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
|
||||
local_server_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText()
|
||||
local_server_settings["user"] = self.uiRemoteMainServerUserLineEdit.text()
|
||||
local_server_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
|
||||
local_server_settings["auth"] = self.uiRemoteMainServerAuthCheckBox.isChecked()
|
||||
LocalServer.instance().updateLocalServerSettings(local_server_settings)
|
||||
|
||||
elif self.currentPage() == self.uiSummaryWizardPage:
|
||||
if self.uiLocalRadioButton.isChecked():
|
||||
# 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)
|
||||
vm_settings = self._GNS3VMSettings()
|
||||
vm_settings["enable"] = False
|
||||
self._setGNS3VMSettings(vm_settings)
|
||||
|
||||
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})
|
||||
elif self.currentPage() == self.uiLocalServerStatusWizardPage:
|
||||
if not Controller.instance().connected():
|
||||
return False
|
||||
|
||||
dialog = PreferencesDialog(self)
|
||||
if self.uiAddIOSRouterCheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "Dynamips").uiNewIOSRouterPushButton.clicked.emit(False)
|
||||
if self.uiAddIOUDeviceCheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "IOS on UNIX").uiNewIOUDevicePushButton.clicked.emit(False)
|
||||
if self.uiAddQemuVMcheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "QEMU").uiNewQemuVMPushButton.clicked.emit(False)
|
||||
if self.uiAddVirtualBoxVMcheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "VirtualBox").uiNewVirtualBoxVMPushButton.clicked.emit(False)
|
||||
if self.uiAddVMwareVMcheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "VMware").uiNewVMwareVMPushButton.clicked.emit(False)
|
||||
dialog.exec_()
|
||||
return True
|
||||
|
||||
def _refreshVMListSlot(self):
|
||||
@@ -232,11 +355,10 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
Refresh the list of VM available in VMware or VirtualBox.
|
||||
"""
|
||||
|
||||
server = Servers.instance().localServer()
|
||||
if self.uiVmwareRadioButton.isChecked():
|
||||
server.get("/vmware/vms", self._getVMsFromServerCallback)
|
||||
Controller.instance().get("/gns3vm/engines/vmware/vms", self._getVMsFromServerCallback, progressText="Retrieving VMware VM list from server...")
|
||||
elif self.uiVirtualBoxRadioButton.isChecked():
|
||||
server.get("/virtualbox/vms", self._getVMsFromServerCallback)
|
||||
Controller.instance().get("/gns3vm/engines/virtualbox/vms", self._getVMsFromServerCallback, progressText="Retrieving VirtualBox VM list from server...")
|
||||
|
||||
def _getVMsFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -252,9 +374,9 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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"])
|
||||
self.uiVMListComboBox.addItem(vm["vmname"])
|
||||
|
||||
index = self.uiVMListComboBox.findText(self._GNS3VMSettings()["vmname"])
|
||||
if index != -1:
|
||||
self.uiVMListComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
@@ -271,8 +393,17 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
:param result: ignored
|
||||
"""
|
||||
|
||||
Controller.instance().setDisplayError(True)
|
||||
settings = self.parentWidget().settings()
|
||||
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
|
||||
if result:
|
||||
settings["hide_setup_wizard"] = True
|
||||
else:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
if local_server_settings["host"] is None:
|
||||
local_server_settings["host"] = DEFAULT_LOCAL_SERVER_HOST
|
||||
LocalServer.instance().updateLocalServerSettings(local_server_settings)
|
||||
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
|
||||
|
||||
self.parentWidget().setSettings(settings)
|
||||
super().done(result)
|
||||
|
||||
@@ -282,7 +413,21 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
"""
|
||||
|
||||
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
|
||||
if self.page(current_id) == self.uiLocalServerStatusWizardPage and not self.uiVMRadioButton.isChecked():
|
||||
return self._pageId(self.uiSummaryWizardPage)
|
||||
|
||||
if self.page(current_id) == self.uiServerWizardPage and self.uiRemoteControllerRadioButton.isChecked():
|
||||
return self._pageId(self.uiRemoteControllerWizardPage)
|
||||
|
||||
if self.page(current_id) == self.uiVMWizardPage:
|
||||
return self._pageId(self.uiSummaryWizardPage)
|
||||
return QtWidgets.QWizard.nextId(self)
|
||||
|
||||
def _pageId(self, page):
|
||||
"""
|
||||
Return id of the page
|
||||
"""
|
||||
for id in self.pageIds():
|
||||
if self.page(id) == page:
|
||||
return id
|
||||
raise KeyError
|
||||
|
||||
@@ -19,17 +19,14 @@
|
||||
Dialog to manage the snapshots.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import re
|
||||
import time
|
||||
import os
|
||||
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..utils.process_files_worker import ProcessFilesWorker
|
||||
from ..ui.snapshots_dialog_ui import Ui_SnapshotsDialog
|
||||
from ..topology import Topology
|
||||
from ..node import Node
|
||||
from ..controller import Controller
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
@@ -40,46 +37,38 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, parent, project_path, project_files_dir):
|
||||
def __init__(self, parent, project):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._project_path = project_path
|
||||
self._project_files_dir = os.path.join(project_files_dir, "project-files")
|
||||
self._project = project
|
||||
|
||||
self.uiCreatePushButton.clicked.connect(self._createSnapshotSlot)
|
||||
self.uiDeletePushButton.clicked.connect(self._deleteSnapshotSlot)
|
||||
self.uiRestorePushButton.clicked.connect(self._restoreSnapshotSlot)
|
||||
self.uiSnapshotsList.itemDoubleClicked.connect(self._snapshotDoubleClickedSlot)
|
||||
self._listSnaphosts()
|
||||
self._listSnapshots()
|
||||
|
||||
def _listSnaphosts(self):
|
||||
def _listSnapshots(self):
|
||||
"""
|
||||
Lists all available snapshots.
|
||||
"""
|
||||
|
||||
self.uiSnapshotsList.clear()
|
||||
snapshot_dir = os.path.join(self._project_files_dir, "snapshots")
|
||||
if not os.path.isdir(snapshot_dir):
|
||||
if self._project:
|
||||
Controller.instance().get("/projects/{}/snapshots".format(self._project.id()), self._listSnapshotsCallback)
|
||||
|
||||
def _listSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
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:]
|
||||
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:
|
||||
for snapshot in result:
|
||||
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))
|
||||
|
||||
item.setText("{} on {}".format(snapshot["name"], datetime.fromtimestamp(snapshot["created_at"]).strftime("%d/%m/%y at %H:%M:%S")))
|
||||
item.setData(QtCore.Qt.UserRole, snapshot["snapshot_id"])
|
||||
|
||||
if self.uiSnapshotsList.count():
|
||||
self.uiSnapshotsList.setCurrentRow(0)
|
||||
@@ -95,16 +84,15 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
"""
|
||||
|
||||
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)
|
||||
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()
|
||||
if ok and snapshot_name and self._project:
|
||||
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()), self._createSnapshotsCallback, {"name": snapshot_name})
|
||||
|
||||
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
return
|
||||
self._listSnapshots()
|
||||
|
||||
def _deleteSnapshotSlot(self):
|
||||
"""
|
||||
@@ -113,9 +101,15 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
item = self.uiSnapshotsList.currentItem()
|
||||
if item:
|
||||
snapshot_path = item.data(QtCore.Qt.UserRole)
|
||||
shutil.rmtree(snapshot_path, ignore_errors=True)
|
||||
self._listSnaphosts()
|
||||
snapshot_id = item.data(QtCore.Qt.UserRole)
|
||||
Controller.instance().delete("/projects/{}/snapshots/{}".format(self._project.id(), snapshot_id), self._deleteSnapshotsCallback)
|
||||
|
||||
def _deleteSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
return
|
||||
self._listSnapshots()
|
||||
|
||||
def _restoreSnapshotSlot(self):
|
||||
"""
|
||||
@@ -124,63 +118,26 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
item = self.uiSnapshotsList.currentItem()
|
||||
if item:
|
||||
snapshot_path = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_path)
|
||||
snapshot_id = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_id)
|
||||
|
||||
def _restoreSnapshot(self, snapshot_path):
|
||||
def _restoreSnapshot(self, snapshot_id):
|
||||
"""
|
||||
Restores a snapshot.
|
||||
|
||||
:param snapshot_path: path to the snapshot
|
||||
:param snapshot_id: id of the snapshot
|
||||
"""
|
||||
|
||||
match = re.search(r"^(.*)_([0-9]+)_([0-9]+)", os.path.basename(snapshot_path))
|
||||
if match:
|
||||
snapshot_name = match.group(1)
|
||||
else:
|
||||
snapshot_name = "Unknown"
|
||||
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)
|
||||
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
|
||||
if reply == QtWidgets.QMessageBox.Cancel:
|
||||
return
|
||||
|
||||
# stop all the nodes
|
||||
topology = Topology.instance()
|
||||
for node in topology.nodes():
|
||||
if hasattr(node, "start") and node.status() == Node.started:
|
||||
node.stop()
|
||||
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id), self._restoreSnapshotsCallback)
|
||||
|
||||
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().loadSnapshot(self._project_path)
|
||||
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
return
|
||||
self.accept()
|
||||
|
||||
def _snapshotDoubleClickedSlot(self, item):
|
||||
@@ -188,5 +145,5 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
Slot to restore a snapshot when it is double clicked.
|
||||
"""
|
||||
|
||||
snapshot_path = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_path)
|
||||
snapshot_id = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_id)
|
||||
|
||||
@@ -20,11 +20,15 @@ Dialog to change node symbols.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from ..ui.symbol_selection_dialog_ui import Ui_SymbolSelectionDialog
|
||||
from ..servers import Servers
|
||||
from ..local_server import LocalServer
|
||||
from ..controller import Controller
|
||||
from ..symbol import Symbol
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -39,6 +43,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
:param items: list of items
|
||||
"""
|
||||
|
||||
_symbols_dir = None
|
||||
|
||||
def __init__(self, parent, items=None, symbol=None):
|
||||
|
||||
super().__init__(parent)
|
||||
@@ -51,8 +57,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
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 SymbolSelectionDialog._symbols_dir:
|
||||
SymbolSelectionDialog._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
|
||||
|
||||
if not self._items:
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).hide()
|
||||
@@ -60,38 +66,40 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
self.uiBuiltInSymbolRadioButton.setChecked(True)
|
||||
self.uiSymbolListWidget.setFocus()
|
||||
self.uiSymbolListWidget.setIconSize(QtCore.QSize(64, 64))
|
||||
symbol_resources = QtCore.QResource(":/symbols")
|
||||
self._symbol_items = []
|
||||
symbols = symbol_resources.children()
|
||||
|
||||
try:
|
||||
for file in os.listdir(self._symbols_path):
|
||||
symbols.append(file)
|
||||
except OSError:
|
||||
pass
|
||||
Controller.instance().get("/symbols", self._listSymbolsCallback)
|
||||
|
||||
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)
|
||||
def _listSymbolsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while listing symbols: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
self._symbol_items = []
|
||||
for symbol in result:
|
||||
symbol = Symbol(**symbol)
|
||||
name = os.path.splitext(symbol.filename())[0]
|
||||
item = QtWidgets.QListWidgetItem(self.uiSymbolListWidget)
|
||||
item.setData(QtCore.Qt.UserRole, symbol)
|
||||
self._symbol_items.append(item)
|
||||
item.setText(name)
|
||||
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
|
||||
item.setIcon(icon)
|
||||
|
||||
def render(item, path):
|
||||
svg_renderer = QImageSvgRenderer(path)
|
||||
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)
|
||||
|
||||
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
|
||||
self.adjustSize()
|
||||
|
||||
def _builtinSymbolOnlyToggledSlot(self, checked):
|
||||
@@ -106,7 +114,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
"""
|
||||
text = self.uiSearchLineEdit.text()
|
||||
for item in self._symbol_items:
|
||||
if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not QtCore.QResource(":/symbols/{}.svg".format(item.text())).isValid():
|
||||
if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not item.data(QtCore.Qt.UserRole).builtin():
|
||||
item.setHidden(True)
|
||||
else:
|
||||
if len(text.strip()) == 0 or text.strip().lower() in item.text().lower():
|
||||
@@ -148,18 +156,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
"""
|
||||
|
||||
symbol_path = self.getSymbol()
|
||||
|
||||
pixmap = QtGui.QPixmap(symbol_path)
|
||||
if not pixmap.isNull():
|
||||
for item in self._items:
|
||||
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
|
||||
|
||||
for item in self._items:
|
||||
item.setSymbol(symbol_path)
|
||||
return True
|
||||
|
||||
def getSymbol(self):
|
||||
@@ -167,27 +165,27 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
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
|
||||
return current.data(QtCore.Qt.UserRole).id()
|
||||
else:
|
||||
return self.uiSymbolLineEdit.text()
|
||||
return os.path.basename(self.uiSymbolLineEdit.text())
|
||||
return None
|
||||
|
||||
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)
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Image", SymbolSelectionDialog._symbols_dir, file_formats)
|
||||
if not path:
|
||||
return
|
||||
SymbolSelectionDialog._symbols_dir = os.path.dirname(path)
|
||||
|
||||
self._symbols_dir = os.path.dirname(path)
|
||||
symbol_id = os.path.basename(path)
|
||||
Controller.instance().post("/symbols/" + symbol_id + "/raw", qpartial(self._finishSymbolUpload, path), body=pathlib.Path(path), progressText="Uploading {}".format(symbol_id), timeout=None)
|
||||
|
||||
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while uploading symbol: {}".format(path))
|
||||
return
|
||||
self.uiSymbolLineEdit.clear()
|
||||
self.uiSymbolLineEdit.setText(path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(path))
|
||||
@@ -199,10 +197,9 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
|
||||
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
|
||||
if result and self._items and not self._applyPreferencesSlot():
|
||||
result = 0
|
||||
super().done(result)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
Text editor to edit Note items.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from ..qt import QtCore, QtWidgets, qslot
|
||||
from ..ui.text_editor_dialog_ui import Ui_TextEditorDialog
|
||||
|
||||
|
||||
@@ -70,16 +70,19 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
|
||||
color.blue(),
|
||||
color.alpha()))
|
||||
|
||||
def _setFontSlot(self):
|
||||
@qslot
|
||||
def _setFontSlot(self, *args):
|
||||
"""
|
||||
Slot to select the font.
|
||||
"""
|
||||
|
||||
selected_font, ok = QtWidgets.QFontDialog.getFont(self.uiPlainTextEdit.font(), self)
|
||||
selected_font, ok = QtWidgets.QFontDialog.getFont(self.uiPlainTextEdit.font(), self,
|
||||
options=QtWidgets.QFontDialog.DontUseNativeDialog)
|
||||
if ok:
|
||||
self.uiPlainTextEdit.setFont(selected_font)
|
||||
|
||||
def _setColorSlot(self):
|
||||
@qslot
|
||||
def _setColorSlot(self, *args):
|
||||
"""
|
||||
Slot to select the color.
|
||||
"""
|
||||
@@ -88,7 +91,8 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
|
||||
if color.isValid():
|
||||
self._setColor(color)
|
||||
|
||||
def _applyPreferencesSlot(self):
|
||||
@qslot
|
||||
def _applyPreferencesSlot(self, *args):
|
||||
"""
|
||||
Applies the new text settings.
|
||||
"""
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
from .vm_wizard import VMWizard
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.servers import Servers
|
||||
from gns3.controller import Controller
|
||||
|
||||
|
||||
class VMWithImagesWizard(VMWizard):
|
||||
@@ -26,18 +26,17 @@ 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)
|
||||
def __init__(self, devices, 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)
|
||||
super().__init__(devices, parent)
|
||||
|
||||
def refreshImageStepsButtons(self):
|
||||
"""
|
||||
@@ -89,9 +88,7 @@ class VMWithImagesWizard(VMWizard):
|
||||
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)
|
||||
create_dialog = create_image_wizard(self, self.getSettings()["server"], self.uiNameLineEdit.text() + image_suffix)
|
||||
if QtWidgets.QDialog.Accepted == create_dialog.exec_():
|
||||
line_edit.setText(create_dialog.uiLocationLineEdit.text())
|
||||
|
||||
@@ -100,8 +97,7 @@ class VMWithImagesWizard(VMWizard):
|
||||
Slot to open a file browser and select an image.
|
||||
"""
|
||||
|
||||
server = Servers.instance().getServerFromString(self.getSettings()["server"])
|
||||
path = image_selector(self, server)
|
||||
path = image_selector(self, self._compute_id)
|
||||
if not path:
|
||||
return
|
||||
line_edit.clear()
|
||||
@@ -146,7 +142,7 @@ class VMWithImagesWizard(VMWizard):
|
||||
:param endpoint: server endpoint with the list of Images
|
||||
"""
|
||||
|
||||
self._server.get(endpoint, self._getImagesFromServerCallback)
|
||||
Controller.instance().getCompute(endpoint, self._compute_id, self._getImagesFromServerCallback)
|
||||
|
||||
def _getImagesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -185,10 +181,8 @@ class VMWithImagesWizard(VMWizard):
|
||||
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
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
import sys
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.servers import Servers
|
||||
from gns3.gns3_vm import GNS3VM
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.controller import Controller
|
||||
|
||||
|
||||
class VMWizard(QtWidgets.QWizard):
|
||||
@@ -27,18 +27,17 @@ 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):
|
||||
def __init__(self, devices, parent):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.setModal(True)
|
||||
|
||||
self._devices = devices
|
||||
self._use_local_server = use_local_server
|
||||
self._local_server_disable = False
|
||||
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
@@ -50,16 +49,16 @@ class VMWizard(QtWidgets.QWizard):
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
if hasattr(self, "uiLoadBalanceCheckBox"):
|
||||
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
|
||||
if Controller.instance().isRemote():
|
||||
self.uiLocalRadioButton.setText("Run device on the main server")
|
||||
|
||||
# By default we use the local server
|
||||
self._server = Servers.instance().localServer()
|
||||
self._compute_id = "local"
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
self._localToggledSlot(True)
|
||||
|
||||
if Servers.instance().isNonLocalServerConfigured() is False:
|
||||
# skip the server page if we use the local server
|
||||
if len(ComputeManager.instance().computes()) == 1:
|
||||
# skip the server page if we use the first server
|
||||
self.setStartId(1)
|
||||
|
||||
def _vmToggledSlot(self, checked):
|
||||
@@ -81,6 +80,7 @@ class VMWizard(QtWidgets.QWizard):
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(True)
|
||||
self.uiRemoteServersComboBox.setEnabled(True)
|
||||
self.uiRemoteServersGroupBox.show()
|
||||
|
||||
def _localToggledSlot(self, checked):
|
||||
@@ -93,74 +93,68 @@ class VMWizard(QtWidgets.QWizard):
|
||||
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.uiRemoteRadioButton.setEnabled(False)
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
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.setEnabled(False)
|
||||
for compute in ComputeManager.instance().computes():
|
||||
if compute.id() == "local":
|
||||
self.uiLocalRadioButton.setEnabled(True)
|
||||
elif compute.id() == "vm":
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.setEnabled(True)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setEnabled(True)
|
||||
self.uiRemoteServersComboBox.addItem(compute.name(), compute.id())
|
||||
|
||||
if self.uiLocalRadioButton.isEnabled() and not self._local_server_disable:
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isEnabled():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
else:
|
||||
if self.uiRemoteRadioButton.isEnabled():
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
def _disableLocalServer(self):
|
||||
"""
|
||||
Validates the server.
|
||||
Turn off the local server
|
||||
"""
|
||||
self._local_server_disable = True
|
||||
self.uiLocalRadioButton.hide()
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
self.setStartId(0)
|
||||
|
||||
if hasattr(self, "uiNamePlatformWizardPage") and self.currentPage() == self.uiNamePlatformWizardPage:
|
||||
def validateCurrentPage(self):
|
||||
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 the local button is not visible it's because it's not supported
|
||||
if self.uiLocalRadioButton.isChecked() and self.uiLocalRadioButton.isHidden():
|
||||
QtWidgets.QMessageBox.critical(self, "New device", "Please configure before the GNS3 VM in order to use this device.")
|
||||
return False
|
||||
|
||||
if self.uiRemoteRadioButton.isChecked():
|
||||
if not Servers.instance().remoteServers():
|
||||
if self.uiRemoteServersComboBox.count() == 0:
|
||||
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())
|
||||
self._compute_id = 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
|
||||
self._compute_id = "vm"
|
||||
else:
|
||||
self._server = Servers.instance().localServer()
|
||||
if self.uiLocalRadioButton.isEnabled():
|
||||
self._compute_id = "local"
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Server", "No available server support this type of node. You probably need to setup the GNS3 VM")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _loadBalanceToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the load balance checkbox is toggled.
|
||||
|
||||
:param checked: either the box is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersComboBox.setEnabled(False)
|
||||
else:
|
||||
self.uiRemoteServersComboBox.setEnabled(True)
|
||||
|
||||
312
gns3/gns3_vm.py
312
gns3/gns3_vm.py
@@ -1,312 +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/>.
|
||||
|
||||
"""
|
||||
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 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
File diff suppressed because it is too large
Load Diff
@@ -16,11 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import copy
|
||||
import pathlib
|
||||
import glob
|
||||
|
||||
from gns3.servers import Servers
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
from gns3.settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.controller import Controller
|
||||
from gns3.utils.file_copy_worker import FileCopyWorker
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
|
||||
@@ -31,7 +33,7 @@ class ImageManager:
|
||||
# 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):
|
||||
def askCopyUploadImage(self, parent, path, server, node_type):
|
||||
"""
|
||||
Ask user for copying the image to the default directory or upload
|
||||
it to remote server.
|
||||
@@ -39,14 +41,14 @@ class ImageManager:
|
||||
: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
|
||||
:param node_type: Remote upload endpoint
|
||||
:returns path: Final path
|
||||
"""
|
||||
|
||||
if server and not server.isLocal():
|
||||
return self._uploadImageToRemoteServer(path, server, vm_type)
|
||||
if (server and server != "local") or Controller.instance().isRemote():
|
||||
return self._uploadImageToRemoteServer(path, server, node_type)
|
||||
else:
|
||||
destination_directory = self.getDirectoryForType(vm_type)
|
||||
destination_directory = self.getDirectoryForType(node_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,
|
||||
@@ -73,56 +75,30 @@ class ImageManager:
|
||||
path = destination_path
|
||||
return path
|
||||
|
||||
def _uploadImageToRemoteServer(self, path, server, vm_type):
|
||||
def _uploadImageToRemoteServer(self, path, server, node_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
|
||||
:param node_type: Image node_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'
|
||||
if node_type == 'QEMU':
|
||||
upload_endpoint = '/qemu/images'
|
||||
elif node_type == 'IOU':
|
||||
upload_endpoint = '/iou/images'
|
||||
elif node_type == 'DYNAMIPS':
|
||||
upload_endpoint = '/dynamips/images'
|
||||
else:
|
||||
raise Exception('Invalid image vm_type')
|
||||
raise Exception('Invalid node 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)
|
||||
filename = self._getRelativeImagePath(path, node_type).replace("\\", "/")
|
||||
|
||||
Controller.instance().postCompute('{}/{}'.format(upload_endpoint, filename), server, 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()
|
||||
@@ -135,20 +111,20 @@ class ImageManager:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _getRelativeImagePath(self, path, vm_type):
|
||||
def _getRelativeImagePath(self, path, node_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
|
||||
:param node_type: Type of vm
|
||||
:return: file path
|
||||
"""
|
||||
|
||||
if not path:
|
||||
return ""
|
||||
img_directory = self.getDirectoryForType(vm_type)
|
||||
img_directory = self.getDirectoryForType(node_type)
|
||||
path = os.path.abspath(path)
|
||||
if os.path.commonprefix([img_directory, path]) == img_directory:
|
||||
return os.path.relpath(path, img_directory)
|
||||
@@ -161,19 +137,19 @@ class ImageManager:
|
||||
:returns: path to the default images directory
|
||||
"""
|
||||
|
||||
return Servers.instance().localServerSettings()['images_path']
|
||||
return copy.copy(LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)['images_path'])
|
||||
|
||||
def getDirectoryForType(self, vm_type):
|
||||
def getDirectoryForType(self, node_type):
|
||||
"""
|
||||
Return the path of local directory of the images
|
||||
of a specific vm_type
|
||||
of a specific node_type
|
||||
|
||||
:param vm_type: Type of vm
|
||||
:param node_type: Type of vm
|
||||
"""
|
||||
if vm_type == 'DYNAMIPS':
|
||||
if node_type == 'DYNAMIPS':
|
||||
return os.path.join(self.getDirectory(), 'IOS')
|
||||
else:
|
||||
return os.path.join(self.getDirectory(), vm_type)
|
||||
return os.path.join(self.getDirectory(), node_type)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
#!/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()
|
||||
217
gns3/items/drawing_item.py
Normal file
217
gns3/items/drawing_item.py
Normal file
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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 ..qt import QtCore, QtWidgets, qslot
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
import binascii
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DrawingItem:
|
||||
|
||||
show_layer = False
|
||||
|
||||
"""
|
||||
Base class for non emulation item
|
||||
"""
|
||||
|
||||
def __init__(self, project=None, pos=None, drawing_id=None, svg=None, z=0, rotation=0, **kws):
|
||||
self._id = drawing_id
|
||||
if self._id is None:
|
||||
self._id = str(uuid.uuid4())
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._graphics_view = MainWindow.instance().uiGraphicsView
|
||||
self._main_window = MainWindow.instance()
|
||||
|
||||
self._project = project
|
||||
|
||||
# Store a hash of the SVG to avoid him
|
||||
# to be send if he doesn't change
|
||||
self._hash_svg = None
|
||||
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
if z:
|
||||
self.setZValue(z)
|
||||
if rotation:
|
||||
self.setRotation(rotation)
|
||||
|
||||
def drawing_id(self):
|
||||
return self._id
|
||||
|
||||
def create(self):
|
||||
if self._project:
|
||||
self._project.post("/drawings", self._createDrawingCallback, body=self.__json__())
|
||||
|
||||
def _createDrawingCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up drawing: {}".format(result["message"]))
|
||||
return False
|
||||
self._id = result["drawing_id"]
|
||||
self.updateDrawingCallback(result)
|
||||
|
||||
def updateDrawing(self):
|
||||
if self._id:
|
||||
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__())
|
||||
|
||||
@qslot
|
||||
def updateDrawingCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up drawing: {}".format(result["message"]))
|
||||
return False
|
||||
self.setPos(QtCore.QPoint(result["x"], result["y"]))
|
||||
self.setZValue(result["z"])
|
||||
self.setRotation(result["rotation"])
|
||||
if "svg" in result:
|
||||
self.fromSvg(result["svg"])
|
||||
|
||||
def handleKeyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
:return: Boolean True the event has been captured
|
||||
"""
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
if key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() == 0:
|
||||
self.setRotation(359)
|
||||
else:
|
||||
self.setRotation(self.rotation() - 1)
|
||||
return True
|
||||
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
return True
|
||||
return False
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
if not self.handleKeyPressEvent(event):
|
||||
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
|
||||
|
||||
def __json__(self):
|
||||
data = {
|
||||
"drawing_id": self._id,
|
||||
"x": int(self.pos().x()),
|
||||
"y": int(self.pos().y()),
|
||||
"z": int(self.zValue()),
|
||||
"rotation": int(self.rotation())
|
||||
}
|
||||
svg = self.toSvg()
|
||||
hash_svg = binascii.crc32(svg.encode())
|
||||
if hash_svg != self._hash_svg:
|
||||
data["svg"] = svg
|
||||
self._hash_svg = hash_svg
|
||||
return data
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtWidgets.QGraphicsItem.setZValue(self, value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def delete(self, skip_controller=False):
|
||||
"""
|
||||
Deletes this drawing.
|
||||
|
||||
:param skip_controller: Do not replicate change on the controller (usefull when it's already deleted on controller)
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeDrawing(self)
|
||||
if self._id and not skip_controller:
|
||||
self._project.delete("/drawings/" + self._id, None, body=self.__json__())
|
||||
|
||||
def itemChange(self, change, value):
|
||||
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)
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if not value:
|
||||
self.updateDrawing()
|
||||
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
|
||||
|
||||
def updateNode(self):
|
||||
self.updateDrawing()
|
||||
|
||||
def drawLayerInfo(self, painter):
|
||||
"""
|
||||
Draws the layer position.
|
||||
|
||||
:param painter: QPainter instance
|
||||
"""
|
||||
|
||||
if self.show_layer is False:
|
||||
return
|
||||
|
||||
brect = self.boundingRect()
|
||||
# don't draw anything if the object is too small
|
||||
if brect.width() < 20 or brect.height() < 20:
|
||||
return
|
||||
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
@@ -19,6 +19,9 @@
|
||||
Graphical representation of an ellipse on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import math
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .shape_item import ShapeItem
|
||||
|
||||
@@ -29,25 +32,9 @@ class EllipseItem(QtWidgets.QGraphicsEllipseItem, ShapeItem):
|
||||
Class to draw an ellipse on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, pos=None, width=200, height=200):
|
||||
def __init__(self, width=200, height=200, **kws):
|
||||
super().__init__(width=width, height=height, **kws)
|
||||
|
||||
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
|
||||
self.setBrush(brush)
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this ellipse.
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeEllipse(self)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -61,16 +48,21 @@ class EllipseItem(QtWidgets.QGraphicsEllipseItem, ShapeItem):
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def duplicate(self):
|
||||
def toSvg(self):
|
||||
"""
|
||||
Duplicates this ellipse item.
|
||||
|
||||
:return: EllipseItem instance
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
svg = ET.Element("svg")
|
||||
svg.set("width", str(self.rect().width()))
|
||||
svg.set("height", str(self.rect().height()))
|
||||
|
||||
ellipse = ET.SubElement(svg, "ellipse")
|
||||
ellipse.set("cx", str(math.floor(self.rect().width() / 2)))
|
||||
ellipse.set("rx", str(math.ceil(self.rect().width() / 2)))
|
||||
ellipse.set("cy", str(math.floor(self.rect().height() / 2)))
|
||||
ellipse.set("ry", str(math.ceil(self.rect().height() / 2)))
|
||||
|
||||
ellipse = self._styleSvg(ellipse)
|
||||
|
||||
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
|
||||
ellipse_item = EllipseItem(QtCore.QPointF(self.x() + 20, self.y() + 20), self.rect().width(), self.rect().height())
|
||||
ellipse_item.setPen(self.pen())
|
||||
ellipse_item.setBrush(self.brush())
|
||||
ellipse_item.setZValue(self.zValue())
|
||||
ellipse_item.setRotation(self.rotation())
|
||||
return ellipse_item
|
||||
|
||||
@@ -36,12 +36,11 @@ class EthernetLinkItem(LinkItem):
|
||||
:param destination_port: destination Port instance
|
||||
:param link: Link instance (contains back-end stuff for this link)
|
||||
:param adding_flag: indicates if this link is being added (no destination yet)
|
||||
:param multilink: used to draw multiple link between the same source and destination
|
||||
"""
|
||||
|
||||
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
|
||||
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False):
|
||||
|
||||
super().__init__(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)
|
||||
self._source_collision_offset = 0.0
|
||||
self._destination_collision_offset = 0.0
|
||||
|
||||
@@ -116,13 +115,16 @@ class EthernetLinkItem(LinkItem):
|
||||
if self._source_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._source_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
point1 = QtCore.QPointF(self.source + self.edge_offset) + QtCore.QPointF((self.dx * self._source_collision_offset) / self.length, (self.dy * self._source_collision_offset) / self.length)
|
||||
|
||||
# avoid any collision of the status point with the source node
|
||||
@@ -137,22 +139,16 @@ class EthernetLinkItem(LinkItem):
|
||||
self._source_collision_offset -= 10
|
||||
|
||||
source_port_label = self._source_port.label()
|
||||
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
source_port_label.setPlainText(self._source_port.shortName())
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, point1))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
if not self._source_port.isStub():
|
||||
source_port_name = self._source_port.name().replace(self._source_port.longNameType(),
|
||||
self._source_port.shortNameType())
|
||||
else:
|
||||
source_port_name = self._source_port.name()
|
||||
source_port_label.setPlainText(source_port_name)
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, point1))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
elif source_port_label and not source_port_label.isVisible():
|
||||
source_port_label.show()
|
||||
|
||||
elif source_port_label:
|
||||
source_port_label.show()
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
painter.drawPoint(point1)
|
||||
@@ -160,13 +156,16 @@ class EthernetLinkItem(LinkItem):
|
||||
if self._destination_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
point2 = QtCore.QPointF(self.destination - self.edge_offset) - QtCore.QPointF((self.dx * self._destination_collision_offset) / self.length, (self.dy * self._destination_collision_offset) / self.length)
|
||||
|
||||
# avoid any collision of the status point with the destination node
|
||||
@@ -181,24 +180,18 @@ class EthernetLinkItem(LinkItem):
|
||||
self._destination_collision_offset -= 10
|
||||
|
||||
destination_port_label = self._destination_port.label()
|
||||
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
destination_port_label.setPlainText(self._destination_port.shortName())
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, point2))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
if not self._destination_port.isStub():
|
||||
destination_port_name = self._destination_port.name().replace(self._destination_port.longNameType(),
|
||||
self._destination_port.shortNameType())
|
||||
else:
|
||||
destination_port_name = self._destination_port.name()
|
||||
destination_port_label.setPlainText(destination_port_name)
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, point2))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
elif destination_port_label and not destination_port_label.isVisible():
|
||||
destination_port_label.show()
|
||||
|
||||
elif destination_port_label:
|
||||
destination_port_label.show()
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(point2)
|
||||
|
||||
self._drawCaptureSymbol()
|
||||
self._drawCaptureSymbol()
|
||||
|
||||
@@ -19,41 +19,38 @@
|
||||
Graphical representation of an image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtWidgets, QtCore
|
||||
from ..qt import QtSvg
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from .drawing_item import DrawingItem
|
||||
|
||||
|
||||
class ImageItem():
|
||||
class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
|
||||
|
||||
"""
|
||||
Class to insert an image on the scene.
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
def __init__(self, image_path=None, pos=None, svg=None, **kws):
|
||||
|
||||
def __init__(self, image_path, pos=None):
|
||||
|
||||
self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)
|
||||
self._image_path = image_path
|
||||
# Because we call the Qt C++ code we need to handle the case of pos is None otherwise we will get a conversion error
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
super().__init__(pos=pos, **kws)
|
||||
else:
|
||||
super().__init__(**kws)
|
||||
|
||||
def filePath(self):
|
||||
"""
|
||||
Return image file
|
||||
"""
|
||||
return self._image_path
|
||||
if self._image_path:
|
||||
renderer = QImageSvgRenderer(image_path)
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this image item.
|
||||
"""
|
||||
# By default center the image
|
||||
if pos is None:
|
||||
x = self.pos().x() - (self.boundingRect().width() / 2)
|
||||
y = self.pos().y() - (self.boundingRect().height() / 2)
|
||||
self.setPos(x, y)
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
try:
|
||||
Topology.instance().removeImage(self)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Image", "Cannot delete the image: {}".format(str(e)))
|
||||
if svg:
|
||||
svg = self.fromSvg(svg)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -65,68 +62,14 @@ class ImageItem():
|
||||
"""
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
if self.show_layer is False:
|
||||
return
|
||||
def fromSvg(self, svg):
|
||||
renderer = QImageSvgRenderer(svg)
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
brect = self.boundingRect()
|
||||
# don't draw anything if the object is too small
|
||||
if brect.width() < 20 or brect.height() < 20:
|
||||
return
|
||||
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
def setZValue(self, value):
|
||||
def toSvg(self):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
|
||||
super().setZValue(value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this image item.
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
image_info = {"path": self._image_path,
|
||||
"x": self.x(),
|
||||
"y": self.y()}
|
||||
|
||||
if self.zValue() != 0:
|
||||
image_info["z"] = self.zValue()
|
||||
|
||||
return image_info
|
||||
|
||||
def load(self, image_info):
|
||||
"""
|
||||
Loads an image representation
|
||||
(from a topology file).
|
||||
|
||||
:param image_info: representation of the image item (dictionary)
|
||||
"""
|
||||
|
||||
# load mandatory properties
|
||||
x = image_info["x"]
|
||||
y = image_info["y"]
|
||||
self.setPos(x, y)
|
||||
|
||||
# load optional properties
|
||||
z = image_info.get("z")
|
||||
if z is not None:
|
||||
self.setZValue(z)
|
||||
return self.renderer().svg()
|
||||
|
||||
@@ -21,11 +21,9 @@ Link items are graphical representation of a link on the QGraphicsScene
|
||||
"""
|
||||
|
||||
import math
|
||||
import struct
|
||||
import sys
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
|
||||
|
||||
from ..node import Node
|
||||
from ..packet_capture import PacketCapture
|
||||
|
||||
|
||||
class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
|
||||
@@ -51,16 +49,16 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
:param destination_port: destination Port instance
|
||||
:param link: Link instance (contains back-end stuff for this link)
|
||||
:param adding_flag: indicates if this link is being added (no destination yet)
|
||||
:param multilink: used to draw multiple link between the same source and destination
|
||||
"""
|
||||
|
||||
_draw_port_labels = False
|
||||
delete_link_item_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
|
||||
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False):
|
||||
|
||||
super().__init__()
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setZValue(-1)
|
||||
self.setZValue(-0.5)
|
||||
self._link = None
|
||||
|
||||
from ..main_window import MainWindow
|
||||
@@ -77,10 +75,6 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
# default pen size
|
||||
self._pen_width = 2.0
|
||||
|
||||
# indicates the link position when there are multiple links
|
||||
# between the same source and destination
|
||||
self._multilink = multilink
|
||||
|
||||
# source & destination items and ports
|
||||
self._source_item = source_item
|
||||
self._destination_item = destination_item
|
||||
@@ -96,6 +90,8 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
if not self._adding_flag:
|
||||
# there is a destination
|
||||
self._link = link
|
||||
self._link.updated_link_signal.connect(self._drawCaptureSymbol)
|
||||
self._link.delete_link_signal.connect(self._linkDeletedSlot)
|
||||
self.setFlag(self.ItemIsFocusable)
|
||||
source_item.addLink(self)
|
||||
destination_item.addLink(self)
|
||||
@@ -107,19 +103,10 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
self.adjust()
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete this link
|
||||
"""
|
||||
|
||||
if not self._source_port.isHotPluggable() and self._source_item.node().status() == Node.started:
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Connection", "A link cannot be removed because {} is running".format(self._source_item.node().name()))
|
||||
return False
|
||||
elif not self._destination_port.isHotPluggable() and self._destination_item.node().status() == Node.started:
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Connection", "A link cannot be removed because {} is running".format(self._destination_item.node().name()))
|
||||
return False
|
||||
|
||||
@qslot
|
||||
def _linkDeletedSlot(self, link_id, *args):
|
||||
# first delete the port labels if any
|
||||
|
||||
if self._source_port.label():
|
||||
self._source_port.label().setParentItem(None)
|
||||
self.scene().removeItem(self._source_port.label())
|
||||
@@ -127,11 +114,15 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
self._destination_port.label().setParentItem(None)
|
||||
self.scene().removeItem(self._destination_port.label())
|
||||
|
||||
self._source_item.removeLink(self)
|
||||
self._destination_item.removeLink(self)
|
||||
if self.scene():
|
||||
if self in self.scene().items():
|
||||
self.scene().removeItem(self)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete this link
|
||||
"""
|
||||
self._link.deleteLink()
|
||||
if self in self.scene().items():
|
||||
self.scene().removeItem(self)
|
||||
|
||||
def link(self):
|
||||
"""
|
||||
@@ -211,14 +202,14 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
:param menu: QMenu instance
|
||||
"""
|
||||
|
||||
if not self._source_port.capturing() or not self._destination_port.capturing():
|
||||
if not self._link.capturing():
|
||||
# start capture
|
||||
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():
|
||||
if self._link.capturing():
|
||||
# stop capture
|
||||
stop_capture_action = QtWidgets.QAction("Stop capture", menu)
|
||||
stop_capture_action.setIcon(QtGui.QIcon(':/icons/capture-stop.svg'))
|
||||
@@ -231,8 +222,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
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).
|
||||
if PacketCapture.instance().packetAnalyzerAvailable():
|
||||
analyze_action = QtWidgets.QAction("Analyze capture", menu)
|
||||
analyze_action.setIcon(QtGui.QIcon(':/icons/rtv.png'))
|
||||
analyze_action.triggered.connect(self._analyzeCaptureActionSlot)
|
||||
@@ -294,26 +284,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
ports = {}
|
||||
if self._source_port.packetCaptureSupported() and not self._source_port.capturing():
|
||||
for dlt_name, dlt in self._source_port.dataLinkTypes().items():
|
||||
port = "{} port {} ({} encapsulation: {})".format(self._source_item.node().name(), self._source_port.name(), dlt_name, dlt)
|
||||
ports[port] = [self._source_item.node(), self._source_port, dlt]
|
||||
|
||||
if self._destination_port.packetCaptureSupported() and not self._destination_port.capturing():
|
||||
for dlt_name, dlt in self._destination_port.dataLinkTypes().items():
|
||||
port = "{} port {} ({} encapsulation: {})".format(self._destination_item.node().name(), self._destination_port.name(), dlt_name, dlt)
|
||||
ports[port] = [self._destination_item.node(), self._destination_port, dlt]
|
||||
|
||||
if not ports:
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Packet capture", "Packet capture is not supported on this link")
|
||||
return
|
||||
|
||||
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]
|
||||
node.startPacketCapture(port, port.captureFileName(node.name()), dlt)
|
||||
PacketCapture.instance().startCapture(self._link)
|
||||
|
||||
def _stopCaptureActionSlot(self):
|
||||
"""
|
||||
@@ -321,21 +292,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
if self._source_port.capturing() and self._destination_port.capturing():
|
||||
ports = {}
|
||||
source_port = "{} port {}".format(self._source_item.node().name(), self._source_port.name())
|
||||
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 = 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]
|
||||
node.stopPacketCapture(port)
|
||||
elif self._source_port.capturing():
|
||||
self._source_item.node().stopPacketCapture(self._source_port)
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_item.node().stopPacketCapture(self._destination_port)
|
||||
PacketCapture.instance().stopCapture(self._link)
|
||||
|
||||
def _startWiresharkActionSlot(self):
|
||||
"""
|
||||
@@ -343,22 +300,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
try:
|
||||
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 = 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_item.node().name())
|
||||
else:
|
||||
self._destination_port.startPacketCaptureReader(self._destination_item.node().name())
|
||||
elif self._source_port.capturing():
|
||||
self._source_port.startPacketCaptureReader(self._source_item.node().name())
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_port.startPacketCaptureReader(self._destination_item.node().name())
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Packet capture", "Cannot start Wireshark: {}".format(e))
|
||||
PacketCapture.instance().startPacketCaptureReader(self._link)
|
||||
|
||||
def _analyzeCaptureActionSlot(self):
|
||||
"""
|
||||
@@ -367,19 +309,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
"""
|
||||
|
||||
try:
|
||||
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 = 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()
|
||||
else:
|
||||
self._destination_port.startPacketCaptureAnalyzer()
|
||||
elif self._source_port.capturing():
|
||||
self._source_port.startPacketCaptureAnalyzer()
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_port.startPacketCaptureAnalyzer()
|
||||
PacketCapture.instance().startPacketCaptureAnalyzer(self._link)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Capture analyzer", "Cannot start the packet capture analyzer program: {}".format(e))
|
||||
|
||||
@@ -414,6 +344,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
self.setHovered(False)
|
||||
|
||||
@qslot
|
||||
def adjust(self):
|
||||
"""
|
||||
Computes the source point and destination point.
|
||||
@@ -423,7 +354,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
# links must always be below node items on the scene
|
||||
if not self._adding_flag:
|
||||
min_zvalue = min([self._source_item.zValue(), self._destination_item.zValue()])
|
||||
self.setZValue(min_zvalue - 1)
|
||||
self.setZValue(min_zvalue - 0.5)
|
||||
|
||||
self.prepareGeometryChange()
|
||||
source_rect = self._source_item.boundingRect()
|
||||
@@ -441,15 +372,54 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
# compute the length of the line
|
||||
self.length = math.sqrt(self.dx * self.dx + self.dy * self.dy)
|
||||
|
||||
multilink = self._computeMultiLink()
|
||||
|
||||
# multi-link management
|
||||
if not self._adding_flag and self._multilink and self.length:
|
||||
if not self._adding_flag and multilink and self.length:
|
||||
angle = math.radians(90)
|
||||
self.dxrot = math.cos(angle) * self.dx - math.sin(angle) * self.dy
|
||||
self.dyrot = math.sin(angle) * self.dx + math.cos(angle) * self.dy
|
||||
offset = QtCore.QPointF((self.dxrot * (self._multilink * 5)) / self.length, (self.dyrot * (self._multilink * 5)) / self.length)
|
||||
offset = QtCore.QPointF((self.dxrot * (multilink * 5)) / self.length, (self.dyrot * (multilink * 5)) / self.length)
|
||||
self.source = QtCore.QPointF(self.source + offset)
|
||||
self.destination = QtCore.QPointF(self.destination + offset)
|
||||
|
||||
def _computeMultiLink(self):
|
||||
# Multi-link management
|
||||
#
|
||||
# multi is the offset of the link
|
||||
# +------+ multi = -1 Link 2 +-------+
|
||||
# | +-----------------------------+ |
|
||||
# | R1 | | R2 |
|
||||
# | | multi = 0 Link 1 | |
|
||||
# | +-----------------------------+ |
|
||||
# | | multi = 1 Link 3 | |
|
||||
# +------+-----------------------------+-------+
|
||||
|
||||
if self._source_item == self._destination_item:
|
||||
multi = 0
|
||||
elif not hasattr(self._destination_item, "node"): # Could be temporary a qpointf during link creation
|
||||
multi = 0
|
||||
else:
|
||||
multi = 0
|
||||
link_items = self._source_item.links()
|
||||
for link_item in link_items:
|
||||
if link_item == self:
|
||||
break
|
||||
if link_item.destinationItem().node().id() == self._destination_item.node().id():
|
||||
multi += 1
|
||||
if link_item.sourceItem().node().id() == self._destination_item.node().id():
|
||||
multi += 1
|
||||
|
||||
# MAX 7 links on the scene between 2 nodes
|
||||
if multi > 7:
|
||||
multi = 0
|
||||
# Pair item represent the bottom links
|
||||
elif multi % 2 == 0:
|
||||
multi = multi // 2
|
||||
else:
|
||||
multi = -multi // 2
|
||||
return multi
|
||||
|
||||
def setMousePoint(self, scene_point):
|
||||
"""
|
||||
Sets new mouse point coordinates.
|
||||
@@ -462,13 +432,14 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
self.adjust()
|
||||
self.update()
|
||||
|
||||
def _drawCaptureSymbol(self):
|
||||
@qslot
|
||||
def _drawCaptureSymbol(self, *args):
|
||||
"""
|
||||
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:
|
||||
if self._link.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)
|
||||
|
||||
@@ -19,14 +19,20 @@
|
||||
Graphical representation of a node on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
import sip
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from .note_item import NoteItem
|
||||
from ..symbol import Symbol
|
||||
from ..controller import Controller
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NodeItem():
|
||||
class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
"""
|
||||
Node for the scene.
|
||||
@@ -35,25 +41,35 @@ class NodeItem():
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
GRID_SIZE = 75
|
||||
|
||||
def __init__(self, node):
|
||||
super().__init__()
|
||||
|
||||
# attached node
|
||||
self._node = node
|
||||
# link items connected to this node item.
|
||||
self._links = []
|
||||
self._symbol = None
|
||||
|
||||
# says if the attached node has been initialized
|
||||
# by the server.
|
||||
self._initialized = False
|
||||
|
||||
# node label
|
||||
self._node_label = None
|
||||
|
||||
# link items connected to this node item.
|
||||
self._links = []
|
||||
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
|
||||
self.setZValue(self._node.z())
|
||||
|
||||
# Temporary symbol during loading
|
||||
renderer = QImageSvgRenderer(":/icons/reload.svg")
|
||||
renderer.setObjectName("symbol_loading")
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
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)
|
||||
|
||||
@@ -63,7 +79,6 @@ class NodeItem():
|
||||
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.
|
||||
@@ -73,33 +88,71 @@ class NodeItem():
|
||||
node.suspended_signal.connect(self.suspendedSlot)
|
||||
node.updated_signal.connect(self.updatedSlot)
|
||||
node.deleted_signal.connect(self.deletedSlot)
|
||||
node.delete_links_signal.connect(self.deleteLinksSlot)
|
||||
node.error_signal.connect(self.errorSlot)
|
||||
node.server_error_signal.connect(self.serverErrorSlot)
|
||||
|
||||
# used when a port has been selected from the contextual menu
|
||||
self._selected_port = None
|
||||
|
||||
# says if the attached node has been initialized
|
||||
# by the server.
|
||||
self._initialized = False
|
||||
|
||||
# contains the last error message received
|
||||
# from the server.
|
||||
self._last_error = None
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._main_window = MainWindow.instance()
|
||||
if self._main_window.uiSnapToGridAction.isChecked():
|
||||
self._snapToGrid()
|
||||
self._settings = self._main_window.uiGraphicsView.settings()
|
||||
|
||||
def setUnsavedState(self):
|
||||
"""
|
||||
Indicates the project is in a unsaved state.
|
||||
"""
|
||||
if node.initialized():
|
||||
self.createdSlot(node.id())
|
||||
|
||||
from ..main_window import MainWindow
|
||||
main_window = MainWindow.instance()
|
||||
main_window.setUnsavedState()
|
||||
def _snapToGrid(self):
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
x = (self.GRID_SIZE * round((self.x() + mid_x) / self.GRID_SIZE)) - mid_x
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
y = (self.GRID_SIZE * round((self.y() + mid_y) / self.GRID_SIZE)) - mid_y
|
||||
self.setPos(x, y)
|
||||
|
||||
def updateNode(self):
|
||||
"""
|
||||
Sync change to the node
|
||||
"""
|
||||
if self._initialized:
|
||||
self._node.setGraphics(self)
|
||||
|
||||
@qslot
|
||||
def setSymbol(self, symbol):
|
||||
"""
|
||||
:param symbol: Change the symbol path
|
||||
"""
|
||||
# create renderer using symbols path/resource
|
||||
if symbol is None:
|
||||
symbol = self._node.defaultSymbol()
|
||||
if self._symbol != symbol:
|
||||
self._symbol = symbol
|
||||
|
||||
# Temporary symbol during loading
|
||||
renderer = QImageSvgRenderer(":/icons/reload.svg")
|
||||
renderer.setObjectName("symbol_loading")
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
Controller.instance().getStatic(Symbol(symbol_id=symbol).url(), self._symbolLoadedCallback)
|
||||
|
||||
def symbol(self):
|
||||
return self._symbol
|
||||
|
||||
@qslot
|
||||
def _symbolLoadedCallback(self, path, *args):
|
||||
renderer = QImageSvgRenderer(path, fallback=":/icons/cancel.svg")
|
||||
renderer.setObjectName(path)
|
||||
self.setSharedRenderer(renderer)
|
||||
if self._node.settings().get("symbol") != self._symbol:
|
||||
self.updateNode()
|
||||
if not self._initialized:
|
||||
self._showLabel()
|
||||
self._initialized = True
|
||||
self.updateNode()
|
||||
|
||||
def node(self):
|
||||
"""
|
||||
@@ -110,27 +163,44 @@ class NodeItem():
|
||||
|
||||
return self._node
|
||||
|
||||
def addLink(self, link):
|
||||
def setPos(self, *args):
|
||||
super().setPos(*args)
|
||||
self._node.setSettingValue("x", int(self.x()))
|
||||
self._node.setSettingValue("y", int(self.y()))
|
||||
|
||||
@qslot
|
||||
def addLink(self, link_item, *args):
|
||||
"""
|
||||
Adds a link items to this node item.
|
||||
|
||||
:param link: LinkItem instance
|
||||
"""
|
||||
|
||||
self._links.append(link)
|
||||
self._node.updated_signal.emit()
|
||||
self.setUnsavedState()
|
||||
if not sip.isdeleted(link_item):
|
||||
self._links.append(link_item)
|
||||
link_item.link().delete_link_signal.connect(self._removeLink)
|
||||
link_item.link().updated_link_signal.connect(self._linkUpdatedSlot)
|
||||
self._node.updated_signal.emit()
|
||||
|
||||
def removeLink(self, link):
|
||||
@qslot
|
||||
def _linkUpdatedSlot(self, *args):
|
||||
"""
|
||||
When a link change we also notify the listener of the node
|
||||
"""
|
||||
self._node.updated_signal.emit()
|
||||
|
||||
@qslot
|
||||
def _removeLink(self, link_id, *args):
|
||||
"""
|
||||
Removes a link items from this node item.
|
||||
|
||||
:param link: LinkItem instance
|
||||
"""
|
||||
|
||||
if link in self._links:
|
||||
self._links.remove(link)
|
||||
self.setUnsavedState()
|
||||
for link_item in self._links:
|
||||
if link_item.link().id() == link_id:
|
||||
self._links.remove(link_item)
|
||||
return
|
||||
|
||||
def links(self):
|
||||
"""
|
||||
@@ -141,19 +211,21 @@ class NodeItem():
|
||||
|
||||
return self._links
|
||||
|
||||
def createdSlot(self, node_id):
|
||||
@qslot
|
||||
def createdSlot(self, base_node_id, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a the node has been created/initialized.
|
||||
|
||||
:param node_id: node identifier (integer)
|
||||
:param base_node_id: base node identifier (integer)
|
||||
"""
|
||||
|
||||
self._initialized = True
|
||||
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
|
||||
self.setSymbol(self._node.symbol())
|
||||
self.update()
|
||||
self._showLabel()
|
||||
|
||||
def startedSlot(self):
|
||||
@qslot
|
||||
def startedSlot(self, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a the node has started.
|
||||
@@ -162,7 +234,8 @@ class NodeItem():
|
||||
for link in self._links:
|
||||
link.update()
|
||||
|
||||
def stoppedSlot(self):
|
||||
@qslot
|
||||
def stoppedSlot(self, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a the node has stopped.
|
||||
@@ -171,7 +244,8 @@ class NodeItem():
|
||||
for link in self._links:
|
||||
link.update()
|
||||
|
||||
def suspendedSlot(self):
|
||||
@qslot
|
||||
def suspendedSlot(self, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a the node has suspended.
|
||||
@@ -180,70 +254,59 @@ class NodeItem():
|
||||
for link in self._links:
|
||||
link.update()
|
||||
|
||||
def updatedSlot(self):
|
||||
@qslot
|
||||
def updatedSlot(self, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a the node has been updated.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
if self._node_label:
|
||||
if self._node_label.toPlainText() != self._node.name():
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
self._centerLabel()
|
||||
self.setUnsavedState()
|
||||
self.setSymbol(self._node.settings().get("symbol"))
|
||||
self.setPos(self._node.settings().get("x", 0), self._node.settings().get("y", 0))
|
||||
self.setZValue(self._node.settings().get("z", 0))
|
||||
|
||||
self._updateLabel()
|
||||
|
||||
# update the link tooltips in case the
|
||||
# node name has changed
|
||||
for link in self._links:
|
||||
link.setCustomToolTip()
|
||||
|
||||
def deleteLinksSlot(self):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a all the links must be deleted.
|
||||
"""
|
||||
|
||||
for link in self._links.copy():
|
||||
link.delete()
|
||||
|
||||
def deletedSlot(self):
|
||||
@qslot
|
||||
def deletedSlot(self, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when the node has been deleted.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
if not self.scene():
|
||||
return
|
||||
self._node.removeAllocatedName()
|
||||
if self in self.scene().items():
|
||||
self.scene().removeItem(self)
|
||||
self.setUnsavedState()
|
||||
|
||||
def serverErrorSlot(self, node_id, message):
|
||||
@qslot
|
||||
def serverErrorSlot(self, base_node_id, message, *args):
|
||||
"""
|
||||
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 base_node_id: base node identifier
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
if self:
|
||||
self._last_error = "{message}".format(message=message)
|
||||
self._last_error = "{message}".format(message=message)
|
||||
|
||||
def errorSlot(self, node_id, message):
|
||||
@qslot
|
||||
def errorSlot(self, base_node_id, message, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when the node wants to report an error.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: base node identifier
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
if self:
|
||||
self._last_error = "{message}".format(message=message)
|
||||
self._last_error = "{message}".format(message=message)
|
||||
|
||||
def setCustomToolTip(self):
|
||||
"""
|
||||
@@ -268,14 +331,11 @@ class NodeItem():
|
||||
|
||||
return self._node_label
|
||||
|
||||
def setLabel(self, label):
|
||||
def _labelUnselectedSlot(self):
|
||||
"""
|
||||
Sets the node label.
|
||||
|
||||
:param label: NoteItem instance.
|
||||
Called when user unselect the label
|
||||
"""
|
||||
|
||||
self._node_label = label
|
||||
self.updateNode()
|
||||
|
||||
def _centerLabel(self):
|
||||
"""
|
||||
@@ -289,6 +349,7 @@ class NodeItem():
|
||||
label_x_pos = node_middle.x() - text_middle.x()
|
||||
label_y_pos = -25
|
||||
self._node_label.setPos(label_x_pos, label_y_pos)
|
||||
return
|
||||
|
||||
def _showLabel(self):
|
||||
"""
|
||||
@@ -297,9 +358,29 @@ class NodeItem():
|
||||
|
||||
if not self._node_label:
|
||||
self._node_label = NoteItem(self)
|
||||
self._node_label.item_unselected_signal.connect(self._labelUnselectedSlot)
|
||||
self._node_label.setEditable(False)
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
self._updateLabel()
|
||||
self._node.setSettingValue("label", self._node_label.dump())
|
||||
|
||||
def _updateLabel(self):
|
||||
"""
|
||||
Update the label using the informations stored in the node
|
||||
"""
|
||||
if not self._node_label:
|
||||
return
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
label_data = self._node.settings().get("label")
|
||||
|
||||
if self._node_label.toPlainText() != label_data["text"]:
|
||||
self._node_label.setPlainText(label_data["text"])
|
||||
self._node_label.setStyle(label_data["style"])
|
||||
self._node_label.setRotation(label_data["rotation"])
|
||||
if label_data["x"] is None:
|
||||
self._centerLabel()
|
||||
self.updateNode()
|
||||
else:
|
||||
self._node_label.setPos(label_data["x"], label_data["y"])
|
||||
|
||||
def connectToPort(self, unavailable_ports=[]):
|
||||
"""
|
||||
@@ -330,7 +411,6 @@ class NodeItem():
|
||||
ports_dict[port.portNumber()] = port
|
||||
else:
|
||||
ports_dict[port.name()] = port
|
||||
|
||||
try:
|
||||
ports = sorted(ports_dict.keys(), key=int)
|
||||
except ValueError:
|
||||
@@ -377,20 +457,26 @@ class NodeItem():
|
||||
:param value: value of the change
|
||||
"""
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
value.setX((self.GRID_SIZE * round((value.x() + mid_x) / self.GRID_SIZE)) - mid_x)
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
value.setY((self.GRID_SIZE * round((value.y() + mid_y) / self.GRID_SIZE)) - mid_y)
|
||||
|
||||
# dynamically change the renderer when this node item is selected/unselected.
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if value:
|
||||
self.graphicsEffect().setEnabled(True)
|
||||
else:
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
self.updateNode()
|
||||
|
||||
# adjust link item positions when this node is moving or has changed.
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange or change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
|
||||
self.setUnsavedState()
|
||||
for link in self._links:
|
||||
link.adjust()
|
||||
|
||||
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -464,3 +550,11 @@ class NodeItem():
|
||||
|
||||
if not self.isSelected():
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
def mouseRelease(self):
|
||||
"""
|
||||
Handle all mouse release for this item.
|
||||
It the item is select but mouse is not on it the event
|
||||
is send also
|
||||
"""
|
||||
self.updateNode()
|
||||
|
||||
@@ -20,6 +20,7 @@ Graphical representation of a note on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from .utils import colorFromSvg
|
||||
|
||||
|
||||
class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
@@ -28,6 +29,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
|
||||
:param parent: optional parent
|
||||
"""
|
||||
item_unselected_signal = QtCore.Signal()
|
||||
|
||||
show_layer = False
|
||||
|
||||
@@ -170,7 +172,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
painter.drawText(QtCore.QPointF(center.x(), center.y()), zval)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
@@ -187,6 +189,54 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def setStyle(self, styles):
|
||||
"""
|
||||
Set text style using a SVG style
|
||||
"""
|
||||
font = QtGui.QFont()
|
||||
for style in styles.split(";"):
|
||||
if ":" in style:
|
||||
key, val = style.split(":")
|
||||
key = key.strip()
|
||||
val = val.strip()
|
||||
|
||||
if key == "font-size":
|
||||
font.setPointSize(int(val))
|
||||
elif key == "font-family":
|
||||
font.setFamily(val)
|
||||
elif key == "font-style" and val == "italic":
|
||||
font.setItalic(True)
|
||||
elif key == "font-weight" and val == "bold":
|
||||
font.setBold(True)
|
||||
elif key == "text-decoration" and val == "underline":
|
||||
font.setUnderline(True)
|
||||
elif key == "text-decoration" and val == "line-through":
|
||||
font.setStrikeOut(True)
|
||||
elif key == "fill":
|
||||
new_color = colorFromSvg(val)
|
||||
color = self.defaultTextColor()
|
||||
color.setBlue(new_color.blue())
|
||||
color.setRed(new_color.red())
|
||||
color.setGreen(new_color.green())
|
||||
self.setDefaultTextColor(color)
|
||||
elif key == "fill-opacity":
|
||||
color = self.defaultTextColor()
|
||||
color.setAlphaF(float(val))
|
||||
self.setDefaultTextColor(color)
|
||||
self.setFont(font)
|
||||
|
||||
def itemChange(self, change, value):
|
||||
"""
|
||||
Notifies this node item that some part of the item's state changes.
|
||||
|
||||
:param change: GraphicsItemChange type
|
||||
:param value: value of the change
|
||||
"""
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if value == 0:
|
||||
self.item_unselected_signal.emit()
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this note.
|
||||
@@ -195,63 +245,29 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
"""
|
||||
|
||||
note_info = {"text": self.toPlainText(),
|
||||
"x": self.x(),
|
||||
"y": self.y()}
|
||||
"x": int(self.x()),
|
||||
"y": int(self.y()),
|
||||
"rotation": int(self.rotation())}
|
||||
|
||||
note_info["font"] = self.font().toString()
|
||||
note_info["color"] = self.defaultTextColor().name(QtGui.QColor.HexArgb)
|
||||
if self.rotation() != 0:
|
||||
note_info["rotation"] = self.rotation()
|
||||
if self.zValue() != 2:
|
||||
note_info["z"] = self.zValue()
|
||||
style = ""
|
||||
|
||||
style += "font-family: {};".format(self.font().family())
|
||||
style += "font-size: {};".format(self.font().pointSize())
|
||||
|
||||
if self.font().italic():
|
||||
style += "font-style: italic;"
|
||||
|
||||
if self.font().bold():
|
||||
style += "font-weight: bold;"
|
||||
|
||||
if self.font().strikeOut():
|
||||
style += "text-decoration: line-through;"
|
||||
elif self.font().underline():
|
||||
style += "text-decoration: underline;"
|
||||
|
||||
style += "fill: {};".format("#" + hex(self.defaultTextColor().rgba())[4:])
|
||||
style += "fill-opacity: {};".format(self.defaultTextColor().alphaF())
|
||||
|
||||
note_info["style"] = style
|
||||
|
||||
return note_info
|
||||
|
||||
def load(self, note_info):
|
||||
"""
|
||||
Loads a note representation
|
||||
(from a topology file).
|
||||
|
||||
:param note_info: representation of the note (dictionary)
|
||||
"""
|
||||
|
||||
# load mandatory properties
|
||||
text = note_info["text"]
|
||||
x = note_info["x"]
|
||||
y = note_info["y"]
|
||||
|
||||
self.setPlainText(text)
|
||||
self.setPos(x, y)
|
||||
|
||||
# load optional properties
|
||||
font = note_info.get("font")
|
||||
color = note_info.get("color")
|
||||
rotation = note_info.get("rotation")
|
||||
z = note_info.get("z")
|
||||
|
||||
if font:
|
||||
qt_font = QtGui.QFont()
|
||||
if qt_font.fromString(font):
|
||||
self.setFont(qt_font)
|
||||
if color:
|
||||
self.setDefaultTextColor(QtGui.QColor(color))
|
||||
if rotation is not None:
|
||||
self.setRotation(float(rotation))
|
||||
if z is not None:
|
||||
self.setZValue(z)
|
||||
|
||||
def duplicate(self):
|
||||
"""
|
||||
Duplicates this node item.
|
||||
|
||||
:return: NoteItem instance
|
||||
"""
|
||||
|
||||
note_item = NoteItem(self.parent())
|
||||
note_item.setPlainText(self.toPlainText())
|
||||
note_item.setPos(self.x() + 20, self.y() + 20)
|
||||
note_item.setZValue(self.zValue())
|
||||
note_item.setFont(self.font())
|
||||
note_item.setDefaultTextColor(self.defaultTextColor())
|
||||
note_item.setRotation(self.rotation())
|
||||
return note_item
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# -*- 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,6 +19,8 @@
|
||||
Graphical representation of a rectangle on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .shape_item import ShapeItem
|
||||
|
||||
@@ -29,25 +31,8 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
Class to draw a rectangle on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, pos=None, width=200, height=100):
|
||||
|
||||
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
|
||||
self.setBrush(brush)
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this rectangle.
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeRectangle(self)
|
||||
def __init__(self, width=200, height=100, **kws):
|
||||
super().__init__(width=width, height=height, **kws)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -61,16 +46,19 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def duplicate(self):
|
||||
def toSvg(self):
|
||||
"""
|
||||
Duplicates this rectangle item.
|
||||
|
||||
:return: RectangleItem instance
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
svg = ET.Element("svg")
|
||||
svg.set("width", str(int(self.rect().width())))
|
||||
svg.set("height", str(int(self.rect().height())))
|
||||
|
||||
rect = ET.SubElement(svg, "rect")
|
||||
rect.set("width", str(int(self.rect().width())))
|
||||
rect.set("height", str(int(self.rect().height())))
|
||||
|
||||
rect = self._styleSvg(rect)
|
||||
|
||||
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
|
||||
rectangle_item = RectangleItem(QtCore.QPointF(self.x() + 20, self.y() + 20), self.rect().width(), self.rect().height())
|
||||
rectangle_item.setPen(self.pen())
|
||||
rectangle_item.setBrush(self.brush())
|
||||
rectangle_item.setZValue(self.zValue())
|
||||
rectangle_item.setRotation(self.rotation())
|
||||
return rectangle_item
|
||||
|
||||
@@ -37,12 +37,11 @@ class SerialLinkItem(LinkItem):
|
||||
:param destination_port: destination Port instance
|
||||
:param link: Link instance (contains back-end stuff for this link)
|
||||
:param adding_flag: indicates if this link is being added (no destination yet)
|
||||
:param multilink: used to draw multiple link between the same source and destination
|
||||
"""
|
||||
|
||||
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False, multilink=0):
|
||||
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False):
|
||||
|
||||
super().__init__(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)
|
||||
|
||||
def adjust(self):
|
||||
"""
|
||||
@@ -117,32 +116,28 @@ class SerialLinkItem(LinkItem):
|
||||
# source point color
|
||||
if self._source_port.status() == Port.started:
|
||||
# port is active
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.green
|
||||
elif self._source_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.yellow
|
||||
else:
|
||||
shape = QtCore.Qt.SquareCap
|
||||
color = QtCore.Qt.red
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
|
||||
source_port_label = self._source_port.label()
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
source_port_label.setPlainText(self._source_port.shortName())
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, self.source))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
if not self._source_port.isStub():
|
||||
source_port_name = self._source_port.name().replace(self._source_port.longNameType(),
|
||||
self._source_port.shortNameType())
|
||||
else:
|
||||
source_port_name = self._source_port.name()
|
||||
source_port_label.setPlainText(source_port_name)
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, self.source))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
elif source_port_label and not source_port_label.isVisible():
|
||||
source_port_label.show()
|
||||
|
||||
elif source_port_label:
|
||||
source_port_label.show()
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.source_point)
|
||||
@@ -151,31 +146,28 @@ class SerialLinkItem(LinkItem):
|
||||
if self._destination_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
|
||||
destination_port_label = self._destination_port.label()
|
||||
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
destination_port_label.setPlainText(self._destination_port.shortName())
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, self.destination))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
if not self._destination_port.isStub():
|
||||
destination_port_name = self._destination_port.name().replace(self._destination_port.longNameType(),
|
||||
self._destination_port.shortNameType())
|
||||
else:
|
||||
destination_port_name = self._destination_port.name()
|
||||
destination_port_label.setPlainText(destination_port_name)
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, self.destination))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
elif destination_port_label and not destination_port_label.isVisible():
|
||||
destination_port_label.show()
|
||||
|
||||
elif destination_port_label:
|
||||
destination_port_label.show()
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.destination_point)
|
||||
|
||||
@@ -19,46 +19,48 @@
|
||||
Base class for shape items (Rectangle, ellipse etc.).
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .drawing_item import DrawingItem
|
||||
from .utils import colorFromSvg
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShapeItem:
|
||||
class ShapeItem(DrawingItem):
|
||||
|
||||
# Map QT stroke to SVG style
|
||||
QT_DASH_TO_SVG = {
|
||||
QtCore.Qt.SolidLine: "",
|
||||
QtCore.Qt.NoPen: None,
|
||||
QtCore.Qt.DashLine: "25, 25",
|
||||
QtCore.Qt.DotLine: "5, 25",
|
||||
QtCore.Qt.DashDotLine: "5, 25, 25",
|
||||
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
|
||||
}
|
||||
|
||||
"""
|
||||
Base class to draw shapes on the scene.
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
def __init__(self, width=200, height=200, svg=None, **kws):
|
||||
|
||||
def __init__(self, **kws):
|
||||
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable)
|
||||
super().__init__(svg=svg, **kws)
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._border = 5
|
||||
self._edge = None
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._graphics_view = MainWindow.instance().uiGraphicsView
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
if key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() > -360.0:
|
||||
self.setRotation(self.rotation() - 1)
|
||||
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
if svg is None:
|
||||
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
|
||||
self.setBrush(brush)
|
||||
else:
|
||||
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
|
||||
self.fromSvg(svg)
|
||||
if self._id is None:
|
||||
self.create()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""
|
||||
@@ -178,128 +180,67 @@ class ShapeItem:
|
||||
if self.zValue() >= 0:
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def drawLayerInfo(self, painter):
|
||||
def _styleSvg(self, element):
|
||||
"""
|
||||
Draws the layer position.
|
||||
|
||||
:param painter: QPainter instance
|
||||
Add style from the shape item to the SVG element that we will
|
||||
export
|
||||
"""
|
||||
|
||||
if self.show_layer is False:
|
||||
return
|
||||
|
||||
brect = self.boundingRect()
|
||||
# don't draw anything if the object is too small
|
||||
if brect.width() < 20 or brect.height() < 20:
|
||||
return
|
||||
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtWidgets.QGraphicsItem.setZValue(self, value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this shape item.
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
shape_info = {"width": self.rect().width(),
|
||||
"height": self.rect().height(),
|
||||
"x": self.x(),
|
||||
"y": self.y()}
|
||||
|
||||
brush = self.brush()
|
||||
if brush.color() != QtCore.Qt.white:
|
||||
shape_info["color"] = brush.color().name()
|
||||
if brush.color().alpha() != 255:
|
||||
shape_info["transparency"] = brush.color().alpha()
|
||||
|
||||
style = ""
|
||||
pen = self.pen()
|
||||
if pen.color() != QtCore.Qt.black:
|
||||
shape_info["border_color"] = pen.color().name()
|
||||
if pen.color().alpha() != 255:
|
||||
shape_info["border_transparency"] = pen.color().alpha()
|
||||
if pen.width() != 2:
|
||||
shape_info["border_width"] = pen.width()
|
||||
if pen.style() != QtCore.Qt.SolidLine:
|
||||
shape_info["border_style"] = pen.style()
|
||||
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
|
||||
element.set("fill-opacity", str(self.brush().color().alphaF()))
|
||||
|
||||
if self.rotation() != 0:
|
||||
shape_info["rotation"] = self.rotation()
|
||||
if self.zValue() != 0:
|
||||
shape_info["z"] = self.zValue()
|
||||
dasharray = self.QT_DASH_TO_SVG[pen.style()]
|
||||
if dasharray is None: # No border to the element
|
||||
return element
|
||||
elif dasharray == "":
|
||||
pass # Solid line
|
||||
else:
|
||||
element.set("stroke-dasharray", dasharray)
|
||||
element.set("stroke-width", str(pen.width()))
|
||||
element.set("stroke", "#" + hex(pen.color().rgba())[4:])
|
||||
return element
|
||||
|
||||
return shape_info
|
||||
|
||||
def load(self, shape_info):
|
||||
def fromSvg(self, svg):
|
||||
"""
|
||||
Loads a representation of this shape item.
|
||||
(from a topology file).
|
||||
|
||||
:param shape_info: representation of the shape item (dictionary)
|
||||
Import element informations from an SVG
|
||||
"""
|
||||
|
||||
# load mandatory properties
|
||||
width = shape_info["width"]
|
||||
height = shape_info["height"]
|
||||
x = shape_info["x"]
|
||||
y = shape_info["y"]
|
||||
svg = ET.fromstring(svg)
|
||||
width = float(svg.get("width", self.rect().width()))
|
||||
height = float(svg.get("height", self.rect().height()))
|
||||
self.setRect(0, 0, width, height)
|
||||
self.setPos(x, y)
|
||||
|
||||
# load optional properties
|
||||
z = shape_info.get("z")
|
||||
color = shape_info.get("color")
|
||||
transparency = shape_info.get("transparency")
|
||||
border_color = shape_info.get("border_color")
|
||||
border_transparency = shape_info.get("border_transparency")
|
||||
border_width = shape_info.get("border_width")
|
||||
border_style = shape_info.get("border_style")
|
||||
rotation = shape_info.get("rotation")
|
||||
pen = QtGui.QPen()
|
||||
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
|
||||
|
||||
if color:
|
||||
color = QtGui.QColor(color)
|
||||
else:
|
||||
color = QtGui.QColor(255, 255, 255)
|
||||
if transparency is not None:
|
||||
color.setAlpha(transparency)
|
||||
self.setBrush(QtGui.QBrush(color))
|
||||
if len(svg):
|
||||
if svg[0].get("stroke-width"):
|
||||
pen.setWidth(int(svg[0].get("stroke-width")))
|
||||
if svg[0].get("stroke"):
|
||||
pen.setColor(colorFromSvg(svg[0].get("stroke")))
|
||||
if svg[0].get("fill"):
|
||||
new_color = colorFromSvg(svg[0].get("fill"))
|
||||
color = brush.color()
|
||||
color.setBlue(new_color.blue())
|
||||
color.setRed(new_color.red())
|
||||
color.setGreen(new_color.green())
|
||||
brush.setColor(color)
|
||||
if svg[0].get("fill-opacity"):
|
||||
color = brush.color()
|
||||
color.setAlphaF(float(svg[0].get("fill-opacity")))
|
||||
brush.setColor(color)
|
||||
|
||||
# Map SVG stroke style (border of the element to the Qt version)
|
||||
if not svg[0].get("stroke"):
|
||||
pen.setStyle(QtCore.Qt.NoPen)
|
||||
else:
|
||||
pen.setStyle(QtCore.Qt.SolidLine)
|
||||
stroke = svg[0].get("stroke-dasharray")
|
||||
if stroke:
|
||||
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
|
||||
if svg_stroke == stroke:
|
||||
pen.setStyle(qt_stroke)
|
||||
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
if border_color:
|
||||
border_color = QtGui.QColor(border_color)
|
||||
else:
|
||||
border_color = pen.color()
|
||||
if border_transparency:
|
||||
border_color.setAlpha(border_transparency)
|
||||
pen.setColor(border_color)
|
||||
if border_width is not None:
|
||||
pen.setWidth(int(border_width))
|
||||
if border_style is not None:
|
||||
pen.setStyle(QtCore.Qt.PenStyle(border_style))
|
||||
self.setPen(pen)
|
||||
|
||||
if rotation is not None:
|
||||
self.setRotation(rotation)
|
||||
if z is not None:
|
||||
self.setZValue(z)
|
||||
self.setBrush(brush)
|
||||
self.update()
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# -*- 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
|
||||
@@ -1,51 +0,0 @@
|
||||
# -*- 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)
|
||||
191
gns3/items/text_item.py
Normal file
191
gns3/items/text_item.py
Normal file
@@ -0,0 +1,191 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a note on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from .drawing_item import DrawingItem
|
||||
from .utils import colorFromSvg
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
"""
|
||||
Text item for the QGraphicsView.
|
||||
"""
|
||||
|
||||
def __init__(self, svg=None, **kws):
|
||||
|
||||
super().__init__(**kws)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
|
||||
main_window = MainWindow.instance()
|
||||
view_settings = main_window.uiGraphicsView.settings()
|
||||
qt_font = QtGui.QFont()
|
||||
qt_font.fromString(view_settings["default_label_font"])
|
||||
self.setDefaultTextColor(QtGui.QColor(view_settings["default_label_color"]))
|
||||
self.setFont(qt_font)
|
||||
|
||||
if svg:
|
||||
try:
|
||||
svg = self.fromSvg(svg)
|
||||
except ET.ParseError as e:
|
||||
log.warning(str(e))
|
||||
|
||||
if self._id is None:
|
||||
self.create()
|
||||
|
||||
def editText(self):
|
||||
"""
|
||||
Edit mode for this note.
|
||||
"""
|
||||
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
|
||||
self.setSelected(True)
|
||||
self.setFocus()
|
||||
cursor = self.textCursor()
|
||||
cursor.select(QtGui.QTextCursor.Document)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
"""
|
||||
Handles all mouse double click events.
|
||||
|
||||
:param event: QMouseEvent instance
|
||||
"""
|
||||
|
||||
self.editText()
|
||||
|
||||
def focusOutEvent(self, event):
|
||||
"""
|
||||
Handles all focus out events.
|
||||
|
||||
:param event: QFocusEvent instance
|
||||
"""
|
||||
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False)
|
||||
cursor = self.textCursor()
|
||||
if cursor.hasSelection():
|
||||
cursor.clearSelection()
|
||||
self.setTextCursor(cursor)
|
||||
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
|
||||
if not self.toPlainText():
|
||||
# delete the note if empty
|
||||
self.delete()
|
||||
return
|
||||
else:
|
||||
self.updateDrawing()
|
||||
return super().focusOutEvent(event)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
Paints the contents of an item in local coordinates.
|
||||
|
||||
:param painter: QPainter instance
|
||||
:param option: QStyleOptionGraphicsItem instance
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def toSvg(self):
|
||||
"""
|
||||
Return an SVG version of the text
|
||||
"""
|
||||
svg = ET.Element("svg")
|
||||
svg.set("width", str(int(self.boundingRect().width())))
|
||||
svg.set("height", str(int(self.boundingRect().height())))
|
||||
|
||||
text = ET.SubElement(svg, "text")
|
||||
text.set("font-family", self.font().family())
|
||||
text.set("font-size", str(self.font().pointSize()))
|
||||
if self.font().italic():
|
||||
text.set("font-style", "italic")
|
||||
if self.font().bold():
|
||||
text.set("font-weight", "bold")
|
||||
if self.font().strikeOut():
|
||||
text.set("text-decoration", "line-through")
|
||||
elif self.font().underline():
|
||||
text.set("text-decoration", "underline")
|
||||
text.set("fill", "#" + hex(self.defaultTextColor().rgba())[4:])
|
||||
text.set("fill-opacity", str(self.defaultTextColor().alphaF()))
|
||||
text.text = self.toPlainText()
|
||||
|
||||
svg = ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
return svg
|
||||
|
||||
def fromSvg(self, svg):
|
||||
svg = ET.fromstring(svg)
|
||||
text = svg[0]
|
||||
|
||||
font = QtGui.QFont()
|
||||
color = text.get("fill")
|
||||
if color:
|
||||
new_color = colorFromSvg(color)
|
||||
color = self.defaultTextColor()
|
||||
color.setBlue(new_color.blue())
|
||||
color.setRed(new_color.red())
|
||||
color.setGreen(new_color.green())
|
||||
self.setDefaultTextColor(color)
|
||||
|
||||
opacity = text.get("fill-opacity")
|
||||
if opacity:
|
||||
color = self.defaultTextColor()
|
||||
color.setAlphaF(float(opacity))
|
||||
self.setDefaultTextColor(color)
|
||||
|
||||
font.setPointSize(int(text.get("font-size", self.font().pointSize())))
|
||||
font.setFamily(text.get("font-family", self.font().family()))
|
||||
if text.get("font-style") == "italic":
|
||||
font.setItalic(True)
|
||||
if text.get("font-weight") == "bold":
|
||||
font.setBold(True)
|
||||
if text.get("text-decoration") == "underline":
|
||||
font.setUnderline(True)
|
||||
if text.get("text-decoration") == "line-through":
|
||||
font.setStrikeOut(True)
|
||||
|
||||
self.setFont(font)
|
||||
self.setPlainText(text.text)
|
||||
|
||||
def editable(self):
|
||||
"""
|
||||
Returns either the note is editable or not.
|
||||
|
||||
:return: boolean
|
||||
"""
|
||||
|
||||
return True
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
if not self.handleKeyPressEvent(event):
|
||||
super().keyPressEvent(event)
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
# 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
|
||||
@@ -15,13 +15,17 @@
|
||||
# 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 class for NIOs (Network Input/Output).
|
||||
"""
|
||||
from ..qt import QtGui
|
||||
|
||||
|
||||
class NIO:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
pass
|
||||
def colorFromSvg(value):
|
||||
"""
|
||||
Transform a color coming from a SVG file to a Qcolor
|
||||
"""
|
||||
value = value.strip('#')
|
||||
if value == "":
|
||||
value = "000000"
|
||||
if len(value) == 6: # If alpha channel is missing
|
||||
value = "ff" + value
|
||||
value = int(value, base=16)
|
||||
return QtGui.QColor.fromRgba(value)
|
||||
526
gns3/link.py
526
gns3/link.py
@@ -19,10 +19,14 @@
|
||||
Manages and stores everything needed for a connection between 2 devices.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sip
|
||||
import uuid
|
||||
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .controller import Controller
|
||||
|
||||
from .qt import QtCore
|
||||
from .nios.nio_udp import NIOUDP
|
||||
from .nios.nio_vmnet import NIOVMNET
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -37,17 +41,21 @@ class Link(QtCore.QObject):
|
||||
:param source_port: source Port instance
|
||||
:param destination_node: destination Node instance
|
||||
:param destination_port: destination Port instance
|
||||
:param stub: indicates if the link is connected to a stub device like a Cloud
|
||||
"""
|
||||
|
||||
# signals used to let the GUI view know about link
|
||||
# additions and deletions.
|
||||
add_link_signal = QtCore.Signal(int)
|
||||
delete_link_signal = QtCore.Signal(int)
|
||||
updated_link_signal = QtCore.Signal(int)
|
||||
error_link_signal = QtCore.Signal(int)
|
||||
|
||||
_instance_count = 1
|
||||
|
||||
def __init__(self, source_node, source_port, destination_node, destination_port):
|
||||
def __init__(self, source_node, source_port, destination_node, destination_port, link_id=None, **link_data):
|
||||
"""
|
||||
:param link_data: Link information from the API
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
@@ -64,56 +72,166 @@ class Link(QtCore.QObject):
|
||||
self._source_port = source_port
|
||||
self._destination_node = destination_node
|
||||
self._destination_port = destination_port
|
||||
self._source_nio = None
|
||||
self._destination_nio = None
|
||||
self._source_nio_active = False
|
||||
self._destination_nio_active = False
|
||||
self._source_label = None
|
||||
self._destination_label = None
|
||||
self._link_id = link_id
|
||||
self._capturing = False
|
||||
self._capture_file_path = None
|
||||
self._capture_file = None
|
||||
self._initialized = False
|
||||
|
||||
if source_port.isStub() or destination_port.isStub():
|
||||
self._stub = True
|
||||
# Boolean if True we are creatin the first instance of this node
|
||||
# if false the node already exist in the topology
|
||||
# use to avoid erasing informations when reloading
|
||||
self._creator = False
|
||||
|
||||
self._nodes = []
|
||||
|
||||
self._source_node.addLink(self)
|
||||
self._destination_node.addLink(self)
|
||||
|
||||
body = self._prepareParams()
|
||||
if self._link_id:
|
||||
link_data["link_id"] = self._link_id
|
||||
self._linkCreatedCallback(link_data)
|
||||
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)
|
||||
self._link_id = str(uuid.uuid4())
|
||||
self._creator = True
|
||||
Controller.instance().post("/projects/{project_id}/links".format(project_id=source_node.project().id()), self._linkCreatedCallback, body=body)
|
||||
|
||||
# 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
|
||||
def _parseResponse(self, result):
|
||||
self._capturing = result.get("capturing", False)
|
||||
|
||||
# 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()
|
||||
# If the controller is remote the capture path should be rewrite to something local
|
||||
if Controller.instance().isRemote():
|
||||
if self._capture_file_path is None and result.get("capture_file_path", None) is not None:
|
||||
self._capture_file = QtCore.QTemporaryFile()
|
||||
self._capture_file.open(QtCore.QFile.WriteOnly)
|
||||
self._capture_file.setAutoRemove(True)
|
||||
self._capture_file_path = self._capture_file.fileName()
|
||||
Controller.instance().get(
|
||||
"/projects/{project_id}/links/{link_id}/pcap".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id),
|
||||
None,
|
||||
showProgress=False,
|
||||
downloadProgressCallback=self._downloadPcapProgress,
|
||||
ignoreErrors=True, # If something is wrong avoid disconnect us from server
|
||||
timeout=None)
|
||||
else:
|
||||
# handle stub connections (to a cloud for instance).
|
||||
if not source_port.isStub() and destination_port.isStub():
|
||||
source_node.nio_signal.connect(self.newNIOSlot)
|
||||
self._source_nio = self._destination_port.defaultNio()
|
||||
self._source_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._source_node.addNIO(self._source_port, self._source_nio)
|
||||
elif not destination_port.isStub() and source_port.isStub():
|
||||
destination_node.nio_signal.connect(self.newNIOSlot)
|
||||
self._destination_nio = self._source_port.defaultNio()
|
||||
self._destination_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._destination_node.addNIO(self._destination_port, self._destination_nio)
|
||||
self._capture_file_path = result["capture_file_path"]
|
||||
|
||||
if "nodes" in result:
|
||||
self._nodes = result["nodes"]
|
||||
self._updateLabels()
|
||||
self.updated_link_signal.emit(self._id)
|
||||
|
||||
def creator(self):
|
||||
return self._creator
|
||||
|
||||
def initialized(self):
|
||||
return self._initialized
|
||||
|
||||
def addPortLabel(self, port, label):
|
||||
if port.adapterNumber() == self._source_port.adapterNumber() and port.portNumber() == self._source_port.portNumber() and port.destinationNode() == self._destination_node:
|
||||
self._source_label = label
|
||||
else:
|
||||
self._destination_label = label
|
||||
label.item_unselected_signal.connect(self.update)
|
||||
if self.creator():
|
||||
self.update()
|
||||
else:
|
||||
self._updateLabels()
|
||||
|
||||
def update(self):
|
||||
if not self._link_id:
|
||||
return
|
||||
body = self._prepareParams()
|
||||
Controller.instance().put("/projects/{project_id}/links/{link_id}".format(project_id=self._source_node.project().id(), link_id=self._link_id), self.updateLinkCallback, body=body)
|
||||
|
||||
def updateLinkCallback(self, result, error=False, *args, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Update link", "Error while updating link: {}".format(result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
def _updateLabels(self):
|
||||
for node in self._nodes:
|
||||
if node["node_id"] == self._source_node.node_id() and node["adapter_number"] == self._source_port.adapterNumber() and node["port_number"] == self._source_port.portNumber():
|
||||
self._updateLabel(self._source_label, node["label"])
|
||||
elif node["node_id"] == self._destination_node.node_id() and node["adapter_number"] == self._destination_port.adapterNumber() and node["port_number"] == self._destination_port.portNumber():
|
||||
self._updateLabel(self._destination_label, node["label"])
|
||||
else:
|
||||
log.error("both ports are stub!")
|
||||
raise NotImplementedError
|
||||
|
||||
def _updateLabel(self, label, label_data):
|
||||
if not label or sip.isdeleted(label):
|
||||
return
|
||||
label.setPlainText(label_data["text"])
|
||||
label.setPos(label_data["x"], label_data["y"])
|
||||
label.setStyle(label_data["style"])
|
||||
label.setRotation(label_data["rotation"])
|
||||
|
||||
def _prepareParams(self):
|
||||
body = {
|
||||
"nodes": [
|
||||
{
|
||||
"node_id": self._source_node.node_id(),
|
||||
"adapter_number": self._source_port.adapterNumber(),
|
||||
"port_number": self._source_port.portNumber(),
|
||||
},
|
||||
{
|
||||
"node_id": self._destination_node.node_id(),
|
||||
"adapter_number": self._destination_port.adapterNumber(),
|
||||
"port_number": self._destination_port.portNumber()
|
||||
}
|
||||
]
|
||||
}
|
||||
if self._source_port.label():
|
||||
body["nodes"][0]["label"] = self._source_port.label().dump()
|
||||
if self._destination_port.label():
|
||||
body["nodes"][1]["label"] = self._destination_port.label().dump()
|
||||
return body
|
||||
|
||||
def _linkCreatedCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Create link", "Error while creating link: {}".format(result["message"]))
|
||||
self.deleteLink(skip_controller=True)
|
||||
return
|
||||
|
||||
self._initialized = True
|
||||
|
||||
# let the GUI know about this link has been created
|
||||
self.add_link_signal.emit(self._id)
|
||||
self._source_port.setLinkId(self._id)
|
||||
self._source_port.setLink(self)
|
||||
self._source_port.setDestinationNode(self._destination_node)
|
||||
self._source_port.setDestinationPort(self._destination_port)
|
||||
self._destination_port.setLinkId(self._id)
|
||||
self._destination_port.setLink(self)
|
||||
self._destination_port.setDestinationNode(self._source_node)
|
||||
self._destination_port.setDestinationPort(self._source_port)
|
||||
|
||||
self._link_id = result["link_id"]
|
||||
self._parseResponse(result)
|
||||
|
||||
def link_id(self):
|
||||
return self._link_id
|
||||
|
||||
def capturing(self):
|
||||
"""
|
||||
Is a capture running on the link?
|
||||
"""
|
||||
return self._capturing
|
||||
|
||||
def capture_file_path(self):
|
||||
"""
|
||||
Path of the capture file
|
||||
"""
|
||||
return self._capture_file_path
|
||||
|
||||
def project(self):
|
||||
return self._source_node.project()
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
@@ -130,7 +248,18 @@ class Link(QtCore.QObject):
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
|
||||
def deleteLink(self):
|
||||
def capture_file_name(self):
|
||||
"""
|
||||
:returns: File name for a capture on this link
|
||||
"""
|
||||
capture_file_name = "{}_{}_to_{}_{}".format(
|
||||
self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
return re.sub("[^0-9A-Za-z_-]", "", capture_file_name)
|
||||
|
||||
def deleteLink(self, skip_controller=False):
|
||||
"""
|
||||
Deletes this link.
|
||||
"""
|
||||
@@ -140,19 +269,92 @@ class Link(QtCore.QObject):
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name()))
|
||||
|
||||
# delete the NIOs on both source and destination nodes
|
||||
if self._source_port.nio():
|
||||
self._source_node.deleteNIO(self._source_port)
|
||||
if skip_controller:
|
||||
self._linkDeletedCallback({})
|
||||
else:
|
||||
Controller.instance().delete("/projects/{project_id}/links/{link_id}".format(project_id=self.project().id(),
|
||||
link_id=self._link_id), self._linkDeletedCallback)
|
||||
|
||||
def _linkDeletedCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Called after the link is remove from the topology
|
||||
"""
|
||||
if error:
|
||||
log.error("Error while deleting link: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
self._source_port.setFree()
|
||||
self._source_node.deleteLink(self)
|
||||
self._source_node.updated_signal.emit()
|
||||
if self._destination_port.nio():
|
||||
self._destination_node.deleteNIO(self._destination_port)
|
||||
self._destination_port.setFree()
|
||||
self._destination_node.deleteLink(self)
|
||||
self._destination_node.updated_signal.emit()
|
||||
|
||||
# let the GUI know about this link has been deleted
|
||||
self.delete_link_signal.emit(self._id)
|
||||
|
||||
def startCapture(self, data_link_type, capture_file_name):
|
||||
data = {
|
||||
"capture_file_name": capture_file_name,
|
||||
"data_link_type": data_link_type
|
||||
}
|
||||
Controller.instance().post(
|
||||
"/projects/{project_id}/links/{link_id}/start_capture".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id),
|
||||
self._startCaptureCallback,
|
||||
body=data)
|
||||
|
||||
def _startCaptureCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while starting capture on link: {}".format(result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
def _downloadPcapProgress(self, content, server=None, context={}, **kwargs):
|
||||
"""
|
||||
Called for each part of the file of the PCAP
|
||||
"""
|
||||
if not self._capture_file_path:
|
||||
return
|
||||
self._capture_file.write(content)
|
||||
self._capture_file.flush()
|
||||
|
||||
def stopCapture(self):
|
||||
if Controller.instance().isRemote():
|
||||
if self._capture_file:
|
||||
self._capture_file.close()
|
||||
self._capture_file = None
|
||||
if self._capture_file_path:
|
||||
try:
|
||||
os.remove(self._capture_file_path)
|
||||
except OSError as e:
|
||||
log.error("Can't remove file {}".format(self._capture_file_path))
|
||||
self._capture_file_path = None
|
||||
Controller.instance().post(
|
||||
"/projects/{project_id}/links/{link_id}/stop_capture".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id),
|
||||
self._stopCaptureCallback)
|
||||
|
||||
def _stopCaptureCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while stopping capture on link: {}".format(result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
def get(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP Get from a link
|
||||
"""
|
||||
Controller.instance().get(
|
||||
"/projects/{project_id}/links/{link_id}{path}".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id,
|
||||
path=path),
|
||||
callback,
|
||||
**kwargs)
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
Returns this link identifier.
|
||||
@@ -198,214 +400,12 @@ class Link(QtCore.QObject):
|
||||
|
||||
return self._destination_port
|
||||
|
||||
def UDPPortAllocatedSlot(self, node_id, port_id, lport):
|
||||
def getNodePort(self, node):
|
||||
"""
|
||||
Slot to receive events from Node instances
|
||||
when a UDP port has been allocated in order to create a NIO UDP.
|
||||
Search the port in the link corresponding to this node
|
||||
|
||||
:param node_id: node identifier
|
||||
:param port_id: port identifier
|
||||
:param lport: local UDP port
|
||||
:returns: Node instance
|
||||
"""
|
||||
# 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()
|
||||
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)
|
||||
|
||||
log.debug("{} has allocated UDP port {} for host {}".format(self._source_node.name(),
|
||||
lport,
|
||||
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()
|
||||
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)
|
||||
|
||||
log.debug("{} has allocated UDP port {} for host {}".format(self._destination_node.name(),
|
||||
lport,
|
||||
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
|
||||
rport, raddr = self._destination_udp
|
||||
|
||||
self._source_nio = NIOUDP(lport, raddr, rport)
|
||||
self._destination_nio = NIOUDP(rport, laddr, lport)
|
||||
|
||||
self._source_udp = None
|
||||
self._destination_udp = None
|
||||
|
||||
log.debug("creating UDP tunnel from {}:{} to {}:{} ".format(laddr, lport, raddr, rport))
|
||||
|
||||
# add the UDP 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 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
|
||||
when a NIO has been created on the server
|
||||
and are active.
|
||||
|
||||
:param node_id: node identifier
|
||||
: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
|
||||
# disconnect the signal has we don't expect new source NIO for this link.
|
||||
self._source_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
|
||||
# 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():
|
||||
self._destination_nio_active = True
|
||||
# disconnect the signal has we don't expect new destination NIO for this link.
|
||||
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
|
||||
if not self._stub and self._source_nio_active and self._destination_nio_active:
|
||||
# both NIOs are active now.
|
||||
self._addToSourcePort(self._source_nio)
|
||||
self._addToDestinationPort(self._destination_nio)
|
||||
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self._source_nio_active = False
|
||||
self._destination_nio_active = False
|
||||
|
||||
# let the GUI know about this link has been created
|
||||
self.add_link_signal.emit(self._id)
|
||||
elif self._stub and self._source_nio_active:
|
||||
self._addToSourcePort(self._source_nio)
|
||||
# add the NIO to destination to show the port is not free.
|
||||
self._addToDestinationPort(self._source_nio)
|
||||
self._source_nio_active = False
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self.add_link_signal.emit(self._id)
|
||||
elif self._stub and self._destination_nio_active:
|
||||
# add the NIO to source to show the port is not free.
|
||||
self._addToSourcePort(self._destination_nio)
|
||||
self._addToDestinationPort(self._destination_nio)
|
||||
self._destination_nio_active = False
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self.add_link_signal.emit(self._id)
|
||||
|
||||
def _addToSourcePort(self, nio):
|
||||
"""
|
||||
Adds a NIO, a link id and a description to the source port.
|
||||
|
||||
:param nio: NIO instance
|
||||
"""
|
||||
|
||||
self._source_port.setNio(nio)
|
||||
self._source_port.setLinkId(self._id)
|
||||
self._source_port.setDestinationNode(self._destination_node)
|
||||
self._source_port.setDestinationPort(self._destination_port)
|
||||
|
||||
log.debug("{} attached to {} on port {}".format(nio,
|
||||
self._source_node.name(),
|
||||
self._source_port.name()))
|
||||
|
||||
def _addToDestinationPort(self, nio):
|
||||
"""
|
||||
Adds a NIO, a link id and a description to the destination port.
|
||||
|
||||
:param nio: NIO instance
|
||||
"""
|
||||
|
||||
self._destination_port.setNio(nio)
|
||||
self._destination_port.setLinkId(self._id)
|
||||
self._destination_port.setDestinationNode(self._source_node)
|
||||
self._destination_port.setDestinationPort(self._source_port)
|
||||
|
||||
log.debug("{} attached to {} on port {}".format(nio,
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name()))
|
||||
|
||||
def cancelNIOSlot(self, node_id):
|
||||
"""
|
||||
Slot to receive events from Node instances
|
||||
when a NIO has been canceled because of an
|
||||
error returned by the server.
|
||||
|
||||
:param node_id: node identifier
|
||||
"""
|
||||
|
||||
if not self._stub:
|
||||
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 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)
|
||||
else:
|
||||
if self._source_node.id() == node_id:
|
||||
self._source_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
else:
|
||||
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
|
||||
self._source_nio_active = False
|
||||
self._destination_nio_active = False
|
||||
self.deleteLink()
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this link.
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
return {"id": self.id(),
|
||||
"description": str(self),
|
||||
"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()}
|
||||
if self._destination_node == node:
|
||||
return self._destination_port
|
||||
return self._source_port
|
||||
|
||||
@@ -23,9 +23,10 @@ import copy
|
||||
|
||||
import psutil
|
||||
|
||||
from .qt import QtCore
|
||||
from .qt import QtCore, QtWidgets, qslot
|
||||
from .version import __version__
|
||||
from .utils import parse_version
|
||||
from .controller import Controller
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -38,24 +39,54 @@ class LocalConfig(QtCore.QObject):
|
||||
"""
|
||||
|
||||
config_changed_signal = QtCore.Signal()
|
||||
# When this signal is emit the config is saved on controller
|
||||
save_on_controller_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, config_file=None):
|
||||
"""
|
||||
:param config_file: Path to the config file (override all other config, usefull for tests)
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self._profile = None
|
||||
self._config_file = config_file
|
||||
# Security to avoid pushing to the controller settings before
|
||||
# we get the original settings from controller
|
||||
self._settings_retrieved_from_controller = False
|
||||
self._migrateOldConfigPath()
|
||||
self._resetLoadConfig()
|
||||
self._monitoring_changes = False
|
||||
Controller.instance().connected_signal.connect(self.refreshConfigFromController)
|
||||
self.save_on_controller_signal.connect(self._saveOnController)
|
||||
|
||||
def _monitorChanges(self):
|
||||
"""
|
||||
Poll the remote server waiting for settings update
|
||||
"""
|
||||
if self._monitoring_changes:
|
||||
return
|
||||
self._monitoring_changes = True
|
||||
self._timer = QtCore.QTimer()
|
||||
self._timer.setInterval(5000)
|
||||
self._refreshingSettings = False
|
||||
self._timer.timeout.connect(self.refreshConfigFromController)
|
||||
self._timer.start()
|
||||
|
||||
def _resetLoadConfig(self):
|
||||
"""
|
||||
Reload the config from scratch everything is clean
|
||||
|
||||
"""
|
||||
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)
|
||||
@@ -63,10 +94,8 @@ class LocalConfig(QtCore.QObject):
|
||||
# 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)
|
||||
if not self._config_file:
|
||||
self._config_file = os.path.join(self.configDirectory(), filename)
|
||||
|
||||
# First load system wide settings
|
||||
if os.path.exists(system_wide_config_file):
|
||||
@@ -89,10 +118,56 @@ class LocalConfig(QtCore.QObject):
|
||||
# overwrite system wide settings with user specific ones
|
||||
self._settings.update(user_settings)
|
||||
self._migrateOldConfig()
|
||||
self._writeConfig()
|
||||
self.writeConfig()
|
||||
|
||||
@staticmethod
|
||||
def configDirectory():
|
||||
def profile(self):
|
||||
"""
|
||||
:returns: Current settings profile
|
||||
"""
|
||||
return self._profile
|
||||
|
||||
def setProfile(self, profile):
|
||||
previous_profile = self._profile
|
||||
if profile == "default":
|
||||
self._profile = None
|
||||
else:
|
||||
self._profile = profile
|
||||
if previous_profile != self._profile:
|
||||
self._config_file = None
|
||||
self._resetLoadConfig()
|
||||
|
||||
@qslot
|
||||
def refreshConfigFromController(self):
|
||||
"""
|
||||
Refresh the configuration from the controller
|
||||
"""
|
||||
controller = Controller.instance()
|
||||
if controller.connected():
|
||||
self._refreshingSettings = True
|
||||
controller.get("/settings", self._getSettingsCallback, showProgress=False)
|
||||
self._monitorChanges()
|
||||
|
||||
def _getSettingsCallback(self, result, error=False, **kwargs):
|
||||
self._refreshingSettings = False
|
||||
if error:
|
||||
log.error("Can't get settings from controller")
|
||||
return
|
||||
if result == {} and self._settings != {}:
|
||||
self._settings_retrieved_from_controller = True
|
||||
self.save_on_controller_signal.emit()
|
||||
return
|
||||
|
||||
# The server return an uuid to keep track of settings version
|
||||
if self._settings.get("modification_uuid") != result.get("modification_uuid"):
|
||||
self._settings.update(result)
|
||||
# Update already loaded section
|
||||
for section in self._settings.keys():
|
||||
if isinstance(self._settings[section], dict):
|
||||
self.loadSectionSettings(section, self._settings[section])
|
||||
self.config_changed_signal.emit()
|
||||
self._settings_retrieved_from_controller = True
|
||||
|
||||
def configDirectory(self):
|
||||
"""
|
||||
Get the configuration directory
|
||||
"""
|
||||
@@ -102,6 +177,10 @@ class LocalConfig(QtCore.QObject):
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
path = os.path.join(home, ".config", "GNS3")
|
||||
|
||||
if self._profile is not None:
|
||||
path = os.path.join(path, "profiles", self._profile)
|
||||
|
||||
return os.path.normpath(path)
|
||||
|
||||
def _migrateOldConfigPath(self):
|
||||
@@ -109,7 +188,7 @@ class LocalConfig(QtCore.QObject):
|
||||
Migrate pre 1.4 config path
|
||||
"""
|
||||
|
||||
# In < 1.4 on Mac the config was in a gns3.net directory
|
||||
# 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")
|
||||
@@ -118,13 +197,23 @@ class LocalConfig(QtCore.QObject):
|
||||
try:
|
||||
shutil.copytree(old_path, new_path)
|
||||
except OSError as e:
|
||||
print("Can't copy the old config: %s", str(e))
|
||||
log.error("Can't copy the old config: %s", str(e))
|
||||
|
||||
def _migrateOldConfig(self):
|
||||
"""
|
||||
Migrate pre 1.4 config
|
||||
"""
|
||||
|
||||
# Display an error if settings come from a more recent version of GNS3
|
||||
# patch level version are compatible (ex 1.5.3 and 1.5.2). But if you open
|
||||
# settings from 1.6.1 with 1.5.1 you will have an error
|
||||
if "version" in self._settings:
|
||||
if parse_version(self._settings["version"])[:2] > parse_version(__version__)[:2]:
|
||||
QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
|
||||
QtWidgets.QMessageBox.critical(None, "Version error", "Your settings are for version {} of GNS3. You cannot use a previous version of GNS3 without risking losing data. If you want to reset delete the settings in {}".format(self._settings["version"], self.configDirectory()))
|
||||
# Exit immediately not clean but we want to avoid any side effect that could corrupt the file
|
||||
sys.exit(1)
|
||||
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.0alpha1"):
|
||||
|
||||
servers = self._settings.get("Servers", {})
|
||||
@@ -134,7 +223,7 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
# 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"
|
||||
servers["local_server"]["path"] = "gns3server"
|
||||
|
||||
if "RemoteServers" in self._settings:
|
||||
servers["remote_servers"] = copy.copy(self._settings["RemoteServers"])
|
||||
@@ -151,9 +240,29 @@ class LocalConfig(QtCore.QObject):
|
||||
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():
|
||||
if self._settings["MainWindow"].get("telnet_console_command") not in PRECONFIGURED_TELNET_CONSOLE_COMMANDS.values():
|
||||
self._settings["MainWindow"]["telnet_console_command"] = DEFAULT_TELNET_CONSOLE_COMMAND
|
||||
|
||||
# Migrate 1.X to 2.0
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
|
||||
if "Qemu" in self._settings:
|
||||
# The internet VM is replaced by the nat Node
|
||||
# we remove it from the list of available VM
|
||||
vms = []
|
||||
for vm in self._settings["Qemu"].get("vms", []):
|
||||
if vm.get("hda_disk_image") != "core-linux-6.4-internet-0.1.img":
|
||||
vms.append(vm)
|
||||
self._settings["Qemu"]["vms"] = vms
|
||||
|
||||
# Starting with 2.0.0dev5 IOU licence is stored in the settings
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
|
||||
if "IOU" in self._settings and "iourc_path" in self._settings["IOU"] and "iourc_content" not in self._settings["IOU"]:
|
||||
try:
|
||||
with open(self._settings["IOU"]["iourc_path"], "r") as f:
|
||||
self._settings["IOU"]["iourc_content"] = f.read().replace("\r\n", "\n")
|
||||
del self._settings["IOU"]["iourc_path"]
|
||||
except OSError as e:
|
||||
log.warn("Can't import IOU licence {}: {}".format(self._settings["IOU"]["iourc_path"], str(e)))
|
||||
|
||||
def _readConfig(self, config_path):
|
||||
"""
|
||||
@@ -176,7 +285,7 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
return dict()
|
||||
|
||||
def _writeConfig(self):
|
||||
def writeConfig(self):
|
||||
"""
|
||||
Write the configuration file.
|
||||
"""
|
||||
@@ -191,6 +300,25 @@ class LocalConfig(QtCore.QObject):
|
||||
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))
|
||||
self.save_on_controller_signal.emit()
|
||||
|
||||
@qslot
|
||||
def _saveOnController(self, *args):
|
||||
"""
|
||||
Save some settings on controller for the transition from
|
||||
GUI to a central controller. Will be removed later
|
||||
"""
|
||||
if Controller.instance().connected() and self._settings_retrieved_from_controller:
|
||||
# We save only non user specific sections
|
||||
section_to_save_on_controller = ["Builtin", "Docker", "IOU", "Qemu", "VMware", "VPCS", "VirtualBox", "GraphicsView", "Dynamips"]
|
||||
controller_settings = {}
|
||||
for key, val in self._settings.items():
|
||||
if key in section_to_save_on_controller:
|
||||
controller_settings[key] = val
|
||||
# We want only the VM settings on the server
|
||||
elif key == "Server":
|
||||
controller_settings["Server"]["vm"] = self._settings["Server"]["vm"]
|
||||
Controller.instance().post("/settings", None, body=controller_settings)
|
||||
|
||||
def checkConfigChanged(self):
|
||||
|
||||
@@ -219,7 +347,7 @@ class LocalConfig(QtCore.QObject):
|
||||
"""
|
||||
|
||||
self._config_file = config_file
|
||||
self._readConfig(self._config_file)
|
||||
self._resetLoadConfig()
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
@@ -239,7 +367,8 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
if self._settings != settings:
|
||||
self._settings.update(settings)
|
||||
self._writeConfig()
|
||||
self.writeConfig()
|
||||
self.config_changed_signal.emit()
|
||||
|
||||
def loadSectionSettings(self, section, default_settings):
|
||||
"""
|
||||
@@ -274,7 +403,7 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
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()
|
||||
self.writeConfig()
|
||||
|
||||
return copy.deepcopy(settings)
|
||||
|
||||
@@ -292,7 +421,7 @@ class LocalConfig(QtCore.QObject):
|
||||
if self._settings[section] != settings:
|
||||
self._settings[section].update(copy.deepcopy(settings))
|
||||
log.info("Section %s has changed. Saving configuration", section)
|
||||
self._writeConfig()
|
||||
self.writeConfig()
|
||||
else:
|
||||
log.debug("Section %s has not changed. Skip saving configuration", section)
|
||||
|
||||
@@ -304,8 +433,30 @@ class LocalConfig(QtCore.QObject):
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["experimental_features"]
|
||||
|
||||
def hdpi(self):
|
||||
"""
|
||||
:returns: Boolean. True if hdpi is allowed
|
||||
"""
|
||||
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["hdpi"]
|
||||
|
||||
def multiProfiles(self):
|
||||
"""
|
||||
:returns: Boolean. True if multi_profiles is enabled
|
||||
"""
|
||||
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["multi_profiles"]
|
||||
|
||||
def setMultiProfiles(self, value):
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
settings = self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)
|
||||
settings["multi_profiles"] = value
|
||||
self.saveSectionSettings("MainWindow", settings)
|
||||
|
||||
@staticmethod
|
||||
def instance(config_file=None):
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of LocalConfig.
|
||||
|
||||
@@ -313,7 +464,7 @@ class LocalConfig(QtCore.QObject):
|
||||
"""
|
||||
|
||||
if not hasattr(LocalConfig, "_instance") or LocalConfig._instance is None:
|
||||
LocalConfig._instance = LocalConfig(config_file=config_file)
|
||||
LocalConfig._instance = LocalConfig()
|
||||
return LocalConfig._instance
|
||||
|
||||
@staticmethod
|
||||
@@ -323,7 +474,7 @@ class LocalConfig(QtCore.QObject):
|
||||
"""
|
||||
|
||||
my_pid = os.getpid()
|
||||
pid_path = os.path.join(LocalConfig.configDirectory(), "gns3_gui.pid")
|
||||
pid_path = os.path.join(LocalConfig.instance().configDirectory(), "gns3_gui.pid")
|
||||
|
||||
if os.path.exists(pid_path):
|
||||
try:
|
||||
|
||||
599
gns3/local_server.py
Normal file
599
gns3/local_server.py
Normal file
@@ -0,0 +1,599 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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
|
||||
import sys
|
||||
import copy
|
||||
import stat
|
||||
import shlex
|
||||
import socket
|
||||
import shutil
|
||||
import random
|
||||
import string
|
||||
import struct
|
||||
import psutil
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore
|
||||
from gns3.settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.utils.http import getSynchronous
|
||||
from gns3.utils.sudo import sudo
|
||||
from gns3.http_client import HTTPClient
|
||||
from gns3.controller import Controller
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StopLocalServerWorker(QtCore.QObject):
|
||||
"""
|
||||
Worker for displaying a progress dialog when closing
|
||||
the server
|
||||
"""
|
||||
# signals to update the progress dialog.
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
finished = QtCore.pyqtSignal()
|
||||
updated = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, local_server_process):
|
||||
super().__init__()
|
||||
self._local_server_process = local_server_process
|
||||
|
||||
def run(self):
|
||||
precision = 1
|
||||
remaining_trial = 4 / precision # 4 Seconds
|
||||
while remaining_trial > 0:
|
||||
if self._local_server_process.returncode is None:
|
||||
remaining_trial -= 1
|
||||
self.thread().sleep(precision)
|
||||
else:
|
||||
break
|
||||
self.finished.emit()
|
||||
|
||||
def cancel(self):
|
||||
return
|
||||
|
||||
|
||||
class LocalServer(QtCore.QObject):
|
||||
"""
|
||||
Manage the local server process
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
# Remember if the server was started by us or not
|
||||
self._server_started_by_me = False
|
||||
self._local_server_path = ""
|
||||
self._local_server_process = None
|
||||
|
||||
super().__init__()
|
||||
self._parent = parent
|
||||
self._config_directory = LocalConfig.instance().configDirectory()
|
||||
self._settings = {}
|
||||
self.localServerSettings()
|
||||
self._port = self._settings.get("port", 3080)
|
||||
if not self._settings.get("auto_start", True):
|
||||
if self._settings.get("host") is None:
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
else:
|
||||
self._http_client = None
|
||||
|
||||
self._stopping = False
|
||||
self._timer = QtCore.QTimer()
|
||||
self._timer.setInterval(5000)
|
||||
self._timer.timeout.connect(self._checkLocalServerRunningSlot)
|
||||
self._timer.start()
|
||||
|
||||
def _pid_path(self):
|
||||
"""
|
||||
:returns: Path of the PID file
|
||||
"""
|
||||
return os.path.join(self._config_directory, "gns3_server.pid")
|
||||
|
||||
def parent(self):
|
||||
"""
|
||||
Parent window
|
||||
"""
|
||||
if self._parent is None:
|
||||
from gns3.main_window import MainWindow
|
||||
return MainWindow.instance()
|
||||
return self._parent
|
||||
|
||||
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:
|
||||
log.error("Could not check if the {} service is running: {}".format(service_name, e.strerror))
|
||||
return True
|
||||
|
||||
def _checkUbridgePermissions(self):
|
||||
"""
|
||||
Checks that uBridge can interact with network interfaces.
|
||||
"""
|
||||
|
||||
path = os.path.abspath(self._settings["ubridge_path"])
|
||||
|
||||
if not path or len(path) == 0 or not os.path.exists(path):
|
||||
return False
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
# do not check anything on Windows
|
||||
return True
|
||||
|
||||
if os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return True
|
||||
|
||||
request_setuid = False
|
||||
if sys.platform.startswith("linux"):
|
||||
# test if the executable has the CAP_NET_RAW capability (Linux only)
|
||||
try:
|
||||
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:
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge? All users on the system will be able to read packet from the network interfaces.",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
sudo(["setcap", "cap_net_admin,cap_net_raw=ep"])
|
||||
else:
|
||||
# capabilities not supported
|
||||
request_setuid = True
|
||||
except AttributeError:
|
||||
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
|
||||
log.warning("Could not determine if CAP_NET_RAW capability is set for uBridge (Python bug)")
|
||||
return True
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set CAP_NET_RAW capability to uBridge {}: {}".format(path, str(e)))
|
||||
return False
|
||||
|
||||
if sys.platform.startswith("darwin") or request_setuid:
|
||||
try:
|
||||
if os.stat(path).st_uid != 0 or not os.stat(path).st_mode & stat.S_ISUID:
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge? All admin users on the system will be able to read packet from the network interfaces.",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set root permissions to uBridge {}: {}".format(path, str(e)))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _passwordGenerate(self):
|
||||
"""
|
||||
Generate a random password
|
||||
"""
|
||||
return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||
|
||||
def localServerSettings(self):
|
||||
"""
|
||||
Returns the local server settings.
|
||||
|
||||
:returns: local server settings (dict)
|
||||
"""
|
||||
|
||||
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
|
||||
self._settings = copy.copy(settings)
|
||||
|
||||
# user & password
|
||||
if settings["auth"] is True and not settings["user"].strip():
|
||||
settings["user"] = "admin"
|
||||
settings["password"] = self._passwordGenerate()
|
||||
|
||||
# local GNS3 server path
|
||||
local_server_path = shutil.which(settings["path"].strip())
|
||||
if local_server_path is None:
|
||||
default_server_path = shutil.which("gns3server")
|
||||
if default_server_path is not None:
|
||||
settings["path"] = os.path.abspath(default_server_path)
|
||||
else:
|
||||
settings["path"] = os.path.abspath(local_server_path)
|
||||
|
||||
# uBridge path
|
||||
ubridge_path = shutil.which(settings["ubridge_path"].strip())
|
||||
if ubridge_path is None:
|
||||
default_ubridge_path = shutil.which("ubridge")
|
||||
if default_ubridge_path is not None:
|
||||
settings["ubridge_path"] = os.path.abspath(default_ubridge_path)
|
||||
else:
|
||||
settings["ubridge_path"] = os.path.abspath(ubridge_path)
|
||||
|
||||
if self._settings != settings:
|
||||
self.updateLocalServerSettings(settings)
|
||||
return settings
|
||||
|
||||
def updateLocalServerSettings(self, new_settings):
|
||||
"""
|
||||
Update the local server settings. Keep the key not in new_settings
|
||||
"""
|
||||
old_settings = copy.copy(self._settings)
|
||||
if not self._settings:
|
||||
self._settings = new_settings
|
||||
else:
|
||||
self._settings.update(new_settings)
|
||||
self._port = self._settings["port"]
|
||||
LocalServerConfig.instance().saveSettings("Server", self._settings)
|
||||
|
||||
# Settings have changed we need to restart the server
|
||||
if old_settings != self._settings:
|
||||
if self._settings["auto_start"]:
|
||||
# We restart the local server only if we really need. Auth can be hot change
|
||||
settings_require_restart = ('host', 'port', 'path')
|
||||
need_restart = False
|
||||
for s in settings_require_restart:
|
||||
if old_settings.get(s) != self._settings.get(s):
|
||||
need_restart = True
|
||||
|
||||
if need_restart:
|
||||
self.stopLocalServer(wait=True)
|
||||
|
||||
self.localServerAutoStartIfRequire()
|
||||
# If the controller is remote:
|
||||
else:
|
||||
self.stopLocalServer(wait=True)
|
||||
|
||||
if self._settings.get("host") is None:
|
||||
self._http_client = None
|
||||
else:
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
|
||||
def shouldLocalServerAutoStart(self):
|
||||
"""
|
||||
Returns either the local server
|
||||
is automatically started on startup.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._settings["auto_start"] and self._settings["host"] is not None
|
||||
|
||||
def localServerPath(self):
|
||||
"""
|
||||
Returns the local server path.
|
||||
|
||||
:returns: path to local server program.
|
||||
"""
|
||||
|
||||
return self._settings["path"]
|
||||
|
||||
def _killAlreadyRunningServer(self):
|
||||
"""
|
||||
Kill a running zombie server (started by a gui that no longer exists)
|
||||
This will not kill server started by hand.
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(self._pid_path()):
|
||||
with open(self._pid_path()) as f:
|
||||
pid = int(f.read())
|
||||
process = psutil.Process(pid=pid)
|
||||
log.info("Kill already running server with PID %d", pid)
|
||||
process.kill()
|
||||
except (OSError, ValueError, psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
# Permission issue, or process no longer exists, or file is empty
|
||||
return
|
||||
|
||||
def localServerAutoStartIfRequire(self):
|
||||
"""
|
||||
Try to start the embed gns3 server.
|
||||
"""
|
||||
|
||||
if not self.shouldLocalServerAutoStart():
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
return
|
||||
|
||||
if self.isLocalServerRunning() and self._server_started_by_me:
|
||||
return True
|
||||
|
||||
# We check if two gui are not launched at the same time
|
||||
# to avoid killing the server of the other GUI
|
||||
if not LocalConfig.isMainGui():
|
||||
log.info("Not the main GUI, will not auto start the server")
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
return True
|
||||
|
||||
if self.isLocalServerRunning():
|
||||
log.info("A local server already running on this host")
|
||||
# Try to kill the server. The server can be still running after
|
||||
# if the server was started by hand
|
||||
self._killAlreadyRunningServer()
|
||||
|
||||
if not self.isLocalServerRunning():
|
||||
if not self.initLocalServer():
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not start the local server process: {}".format(self._settings["path"]))
|
||||
return False
|
||||
if not self.startLocalServer():
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not start the local server process: {}".format(self._settings["path"]))
|
||||
return False
|
||||
|
||||
if self.parent():
|
||||
worker = WaitForConnectionWorker(self._settings["host"], self._port)
|
||||
progress_dialog = ProgressDialog(worker,
|
||||
"Local server",
|
||||
"Connecting to server {} on port {}...".format(self._settings["host"], self._port),
|
||||
"Cancel", busy=True, parent=self.parent())
|
||||
progress_dialog.show()
|
||||
if not progress_dialog.exec_():
|
||||
return False
|
||||
self._server_started_by_me = True
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
|
||||
return True
|
||||
|
||||
def initLocalServer(self):
|
||||
"""
|
||||
Initialize the local server.
|
||||
"""
|
||||
|
||||
self._checkUbridgePermissions()
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Error", "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
|
||||
return False
|
||||
|
||||
self._port = self._settings["port"]
|
||||
|
||||
# check the local server path
|
||||
local_server_path = self.localServerPath()
|
||||
if not local_server_path:
|
||||
log.warn("No local server is configured")
|
||||
return False
|
||||
if not os.path.isfile(local_server_path):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not find local server {}".format(local_server_path))
|
||||
return False
|
||||
elif not os.access(local_server_path, os.X_OK):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "{} is not an executable".format(local_server_path))
|
||||
return False
|
||||
|
||||
try:
|
||||
# check if the local address still exists
|
||||
for res in socket.getaddrinfo(self._settings["host"], 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
|
||||
af, socktype, proto, _, sa = res
|
||||
with socket.socket(af, socktype, proto) as sock:
|
||||
sock.bind(sa)
|
||||
break
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not bind with {}: {} (please check your host binding setting in the preferences)".format(self._settings["host"], e))
|
||||
return False
|
||||
|
||||
try:
|
||||
# check if the port is already taken
|
||||
find_unused_port = False
|
||||
for res in socket.getaddrinfo(self._settings["host"], self._port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
|
||||
af, socktype, proto, _, sa = res
|
||||
with socket.socket(af, socktype, proto) as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(sa)
|
||||
break
|
||||
except OSError as e:
|
||||
log.warning("Could not use socket {}:{} {}".format(self._settings["host"], self._port, e))
|
||||
find_unused_port = True
|
||||
|
||||
if find_unused_port:
|
||||
# find an alternate port for the local server
|
||||
old_port = self._port
|
||||
try:
|
||||
self._port = self._findUnusedLocalPort(self._settings["host"])
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not find an unused port for the local server: {}".format(e))
|
||||
return False
|
||||
log.warning("The server port {} is already in use, fallback to port {}".format(old_port, self._port))
|
||||
return True
|
||||
|
||||
def _findUnusedLocalPort(self, host):
|
||||
"""
|
||||
Find an unused port.
|
||||
|
||||
:param host: server hosts
|
||||
|
||||
:returns: port number
|
||||
"""
|
||||
|
||||
with socket.socket() as s:
|
||||
s.bind((host, 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
def startLocalServer(self):
|
||||
"""
|
||||
Starts the local server process.
|
||||
"""
|
||||
|
||||
self._stopping = False
|
||||
path = self.localServerPath()
|
||||
command = '"{executable}" --local'.format(executable=path)
|
||||
|
||||
if LocalConfig.instance().profile():
|
||||
command += " --profile {}".format(LocalConfig.instance().profile())
|
||||
|
||||
if self._settings["allow_console_from_anywhere"]:
|
||||
# allow connections to console from remote addresses
|
||||
command += " --allow"
|
||||
|
||||
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
||||
command += " --debug"
|
||||
|
||||
settings_dir = self._config_directory
|
||||
if os.path.isdir(settings_dir):
|
||||
# save server logging info to a file in the settings directory
|
||||
logpath = os.path.join(settings_dir, "gns3_server.log")
|
||||
if os.path.isfile(logpath):
|
||||
# delete the previous log file
|
||||
try:
|
||||
os.remove(logpath)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as e:
|
||||
log.warn("could not delete server log file {}: {}".format(logpath, e))
|
||||
command += ' --log="{}" --pid="{}"'.format(logpath, self._pid_path())
|
||||
|
||||
log.info("Starting local server process with {}".format(command))
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
# use the string on Windows
|
||||
self._local_server_process = subprocess.Popen(command, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, stderr=subprocess.PIPE)
|
||||
else:
|
||||
# use arguments on other platforms
|
||||
args = shlex.split(command)
|
||||
self._local_server_process = subprocess.Popen(args, stderr=subprocess.PIPE)
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.warning('Could not start local server "{}": {}'.format(command, e))
|
||||
return False
|
||||
|
||||
log.info("Local server process has started (PID={})".format(self._local_server_process.pid))
|
||||
return True
|
||||
|
||||
def _checkLocalServerRunningSlot(self):
|
||||
if self._local_server_process and not self._stopping:
|
||||
if not self.localServerProcessIsRunning():
|
||||
log.error("Local server process has stopped")
|
||||
try:
|
||||
log.error(self._local_server_process.stderr.read().decode())
|
||||
except (OSError, UnicodeDecodeError):
|
||||
pass
|
||||
self._local_server_process = None
|
||||
|
||||
def localServerProcessIsRunning(self):
|
||||
"""
|
||||
Returns either the local server is running.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
try:
|
||||
if self._local_server_process and self._local_server_process.poll() is None:
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def isLocalServerRunning(self):
|
||||
"""
|
||||
Synchronous check if a server is already running on this host.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
status, json_data = getSynchronous(self._settings["protocol"], self._settings["host"], self._port, "version",
|
||||
timeout=2, user=self._settings["user"], password=self._settings["password"])
|
||||
|
||||
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 stopLocalServer(self, wait=False):
|
||||
"""
|
||||
Stops the local server.
|
||||
|
||||
:param wait: wait for the server to stop
|
||||
"""
|
||||
|
||||
if self.localServerProcessIsRunning():
|
||||
self._stopping = True
|
||||
log.info("Stopping local server (PID={})".format(self._local_server_process.pid))
|
||||
# local server is running, let's stop it
|
||||
if self._http_client:
|
||||
self._http_client.shutdown()
|
||||
if wait:
|
||||
worker = StopLocalServerWorker(self._local_server_process)
|
||||
progress_dialog = ProgressDialog(worker, "Local server", "Waiting for the local server to stop...", None, busy=True, parent=self.parent())
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
if self._local_server_process.returncode is None:
|
||||
self._killLocalServer()
|
||||
self._server_started_by_me = False
|
||||
|
||||
def _killLocalServer(self):
|
||||
# the local server couldn't be stopped with the normal procedure
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
self._local_server_process.send_signal(signal.CTRL_BREAK_EVENT)
|
||||
else:
|
||||
self._local_server_process.send_signal(signal.SIGINT)
|
||||
# If the process is already dead we received a permission error
|
||||
# it's a race condition between the timeout and send signal
|
||||
except (PermissionError, SystemError):
|
||||
pass
|
||||
try:
|
||||
# wait for the server to stop for maximum x seconds
|
||||
self._local_server_process.wait(timeout=60)
|
||||
except subprocess.TimeoutExpired:
|
||||
proceed = QtWidgets.QMessageBox.question(self.parent(),
|
||||
"Local server",
|
||||
"The Local server cannot be stopped, would you like to kill it?",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
self._local_server_process.kill()
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of LocalServer.
|
||||
:returns: instance of LocalServer
|
||||
"""
|
||||
|
||||
if not hasattr(LocalServer, '_instance') or LocalServer._instance is None:
|
||||
LocalServer._instance = LocalServer()
|
||||
return LocalServer._instance
|
||||
|
||||
|
||||
def main():
|
||||
import pprint
|
||||
|
||||
pp = pprint.PrettyPrinter(indent=4)
|
||||
print("Local server config")
|
||||
local_server = LocalServer(False)
|
||||
pp.pprint(local_server.localServerSettings())
|
||||
local_server.localServerAutoStart()
|
||||
local_server.stopLocalServer()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -30,22 +30,25 @@ class LocalServerConfig:
|
||||
Local server configuration.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, config_file=None):
|
||||
|
||||
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)
|
||||
if config_file:
|
||||
self._config_file = config_file
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
self._config_file = os.path.join(home, ".config", appname, filename)
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_server.ini"
|
||||
else:
|
||||
filename = "gns3_server.conf"
|
||||
|
||||
from .local_config import LocalConfig
|
||||
if sys.platform.startswith("win"):
|
||||
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
|
||||
else:
|
||||
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
|
||||
|
||||
try:
|
||||
# create the config file if it doesn't exist
|
||||
@@ -54,6 +57,14 @@ class LocalServerConfig:
|
||||
log.error("Could not create the local server configuration {}: {}".format(self._config_file, e))
|
||||
self.readConfig()
|
||||
|
||||
def setConfigFile(self, path):
|
||||
"""
|
||||
Change the location of the server config (use for test)
|
||||
"""
|
||||
self._config = configparser.RawConfigParser()
|
||||
self._config_file = path
|
||||
self.readConfig()
|
||||
|
||||
def readConfig(self):
|
||||
"""
|
||||
Read the configuration file.
|
||||
@@ -76,13 +87,12 @@ class LocalServerConfig:
|
||||
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):
|
||||
def loadSettings(self, section, default_settings):
|
||||
"""
|
||||
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)
|
||||
"""
|
||||
@@ -92,14 +102,16 @@ class LocalServerConfig:
|
||||
|
||||
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:
|
||||
if isinstance(default, bool):
|
||||
settings[name] = self._config[section].getboolean(name, default)
|
||||
elif types[name] is float:
|
||||
elif isinstance(default, int):
|
||||
settings[name] = self._config[section].getint(name, default)
|
||||
elif isinstance(default, float):
|
||||
settings[name] = self._config[section].getfloat(name, default)
|
||||
else:
|
||||
settings[name] = self._config[section].get(name, default)
|
||||
if settings[name] == "None":
|
||||
settings[name] = None
|
||||
|
||||
# sync with the config file
|
||||
self.saveSettings(section, settings)
|
||||
|
||||
@@ -75,7 +75,7 @@ class ColouredStreamHandler(logging.StreamHandler):
|
||||
stream.write(msg)
|
||||
stream.write(self.terminator)
|
||||
self.flush()
|
||||
# On OSX when frozen flush raise a BrokenPipeError
|
||||
# On OSX when frozen flush raise a BrokenPipeError
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
except Exception:
|
||||
@@ -85,14 +85,33 @@ class ColouredStreamHandler(logging.StreamHandler):
|
||||
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", "{")
|
||||
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {name}:{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", "{")
|
||||
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {name}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{")
|
||||
logging.basicConfig(level=level, handlers=[stream_handler])
|
||||
log = logging.getLogger()
|
||||
log.addHandler(stream_handler)
|
||||
|
||||
log_factory = logging.getLogRecordFactory()
|
||||
|
||||
def factory(name, level, fn, lno, msg, args, exc_info, func=None, sinfo=None, **kwargs):
|
||||
"""
|
||||
Reformat the log message to get something more clean
|
||||
"""
|
||||
# When qt message box is display the correct line number is a part of
|
||||
# the name
|
||||
if ":" in name:
|
||||
name, lno = name.split(":")
|
||||
lno = int(lno)
|
||||
name = name.replace("gns3.", "")
|
||||
try:
|
||||
return log_factory(name, level, fn, lno, msg, args, exc_info, func=func, sinfo=sinfo, **kwargs)
|
||||
except Exception as e: # To avoid recursion we just print the message if something is wrong when logging
|
||||
print(msg)
|
||||
return
|
||||
logging.setLogRecordFactory(factory)
|
||||
|
||||
try:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(logfile))
|
||||
|
||||
102
gns3/main.py
102
gns3/main.py
@@ -45,10 +45,10 @@ import time
|
||||
import locale
|
||||
import argparse
|
||||
import signal
|
||||
import re
|
||||
import psutil
|
||||
|
||||
try:
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, DEFAULT_BINDING
|
||||
from gns3.qt import QtCore, 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
|
||||
@@ -57,7 +57,8 @@ 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
|
||||
from gns3.dialogs.profile_select import ProfileSelectDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -118,17 +119,17 @@ def main():
|
||||
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("-q", "--quiet", action="store_true", help="do not show logs on stdout")
|
||||
parser.add_argument("--config", help="Configuration file")
|
||||
parser.add_argument("--profile", help="Settings profile (blank will use default settings files)")
|
||||
options = parser.parse_args()
|
||||
exception_file_path = "exceptions.log"
|
||||
|
||||
if options.config:
|
||||
LocalConfig.instance(config_file=options.config)
|
||||
else:
|
||||
LocalConfig.instance()
|
||||
if options.project:
|
||||
options.project = os.path.abspath(options.project)
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
# We add to the path where the OS search executable our binary location starting by GNS3
|
||||
# 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"):
|
||||
@@ -146,7 +147,6 @@ def main():
|
||||
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):
|
||||
@@ -180,30 +180,14 @@ def main():
|
||||
# catch exceptions to write them in a file
|
||||
sys.excepthook = exceptionHook
|
||||
|
||||
current_year = datetime.date.today().year
|
||||
print("GNS3 GUI version {}".format(__version__))
|
||||
print("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
|
||||
|
||||
# we only support Python 3 version >= 3.4
|
||||
if sys.version_info < (3, 4):
|
||||
raise SystemExit("Python 3.4 or higher is required")
|
||||
|
||||
def version(version_string):
|
||||
result = []
|
||||
for i in re.split(r'[^0-9]', version_string):
|
||||
if len(i):
|
||||
result.append(int(i))
|
||||
return result
|
||||
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))
|
||||
|
||||
# 4.8.3 because of QSettings (http://pyqt.sourceforge.net/Docs/PyQt4/pyqt_qsettings.html)
|
||||
if DEFAULT_BINDING == "PyQt4" and version(QtCore.BINDING_VERSION_STR) < version("4.8.3"):
|
||||
raise SystemExit("Requirement is PyQt version 4.8.3 or higher, got version {}".format(QtCore.BINDING_VERSION_STR))
|
||||
|
||||
if DEFAULT_BINDING == "PyQt5" and version(QtCore.BINDING_VERSION_STR) < version("5.0.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.0.0 or higher, got version {}".format(QtCore.BINDING_VERSION_STR))
|
||||
|
||||
import psutil
|
||||
if version(psutil.__version__) < version("2.2.1"):
|
||||
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
|
||||
@@ -236,46 +220,82 @@ def main():
|
||||
except win32console.error as e:
|
||||
print("warning: could not allocate console: {}".format(e))
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
|
||||
global app
|
||||
app = Application(sys.argv)
|
||||
app = Application(sys.argv, hdpi=local_config.hdpi())
|
||||
|
||||
if local_config.multiProfiles() and not options.profile:
|
||||
profile_select = ProfileSelectDialog()
|
||||
profile_select.show()
|
||||
profile_select.exec_()
|
||||
options.profile = profile_select.profile()
|
||||
|
||||
# Init the config
|
||||
if options.config:
|
||||
local_config.setConfigFilePath(options.config)
|
||||
elif options.profile:
|
||||
local_config.setProfile(options.profile)
|
||||
|
||||
# save client logging info to a file
|
||||
logfile = os.path.join(LocalConfig.configDirectory(), "gns3_gui.log")
|
||||
logfile = os.path.join(LocalConfig.instance().configDirectory(), "gns3_gui.log")
|
||||
|
||||
# on debug enable logging to stdout
|
||||
if options.debug:
|
||||
root_logger = init_logger(logging.DEBUG, logfile)
|
||||
init_logger(logging.DEBUG, logfile)
|
||||
elif options.quiet:
|
||||
init_logger(logging.ERROR, logfile)
|
||||
else:
|
||||
root_logger = init_logger(logging.INFO, logfile)
|
||||
init_logger(logging.INFO, logfile)
|
||||
|
||||
current_year = datetime.date.today().year
|
||||
log.info("GNS3 GUI version {}".format(__version__))
|
||||
log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
|
||||
|
||||
log.info("Application started with {}".format("".join(sys.argv)))
|
||||
|
||||
# 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)
|
||||
exception_file_path = os.path.join(LocalConfig.instance().configDirectory(), exception_file_path)
|
||||
|
||||
# We disallow to run GNS3 from outside the /Applications folder to avoid
|
||||
# issue when people run GNS3 from the .dmg
|
||||
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
|
||||
if not os.path.realpath(sys.executable).startswith("/Applications"):
|
||||
QtWidgets.QMessageBox.critical(None, "Error", "You need to copy GNS3 in your /Applications folder before using it.")
|
||||
sys.exit(1)
|
||||
|
||||
global mainwindow
|
||||
mainwindow = MainWindow()
|
||||
startup_file = app.open_file_at_startup
|
||||
if not startup_file:
|
||||
startup_file = options.project
|
||||
|
||||
mainwindow = MainWindow(open_file=startup_file)
|
||||
|
||||
# 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))
|
||||
# loadPath is smart and will load only if a path is present
|
||||
app.file_open_signal.connect(lambda path: mainwindow.loadPath(path))
|
||||
|
||||
# Manage Ctrl + C or kill command
|
||||
def sigint_handler(*args):
|
||||
log.info("Signal received exiting the application")
|
||||
mainwindow.setSoftExit(False)
|
||||
app.closeAllWindows()
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
signal.signal(signal.SIGTERM, sigint_handler)
|
||||
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
|
||||
# conditions
|
||||
import gc
|
||||
gc.collect()
|
||||
|
||||
|
||||
1427
gns3/main_window.py
1427
gns3/main_window.py
File diff suppressed because it is too large
Load Diff
@@ -22,5 +22,6 @@ 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 = [VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Builtin]
|
||||
MODULES = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker]
|
||||
|
||||
@@ -20,10 +20,23 @@ Built-in module implementation.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
|
||||
from ..module import Module
|
||||
from .cloud import Cloud
|
||||
from .host import Host
|
||||
from .nat import Nat
|
||||
from .ethernet_hub import EthernetHub
|
||||
from .ethernet_switch import EthernetSwitch
|
||||
from .frame_relay_switch import FrameRelaySwitch
|
||||
from .atm_switch import ATMSwitch
|
||||
|
||||
from .settings import (
|
||||
BUILTIN_SETTINGS,
|
||||
CLOUD_SETTINGS,
|
||||
NAT_SETTINGS,
|
||||
ETHERNET_HUB_SETTINGS,
|
||||
ETHERNET_SWITCH_SETTINGS
|
||||
)
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -38,11 +51,144 @@ class Builtin(Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._settings = {}
|
||||
self._nodes = []
|
||||
self._cloud_nodes = {}
|
||||
self._nat_nodes = {}
|
||||
self._ethernet_hubs = {}
|
||||
self._ethernet_switches = {}
|
||||
|
||||
# load the settings
|
||||
self._loadSettings()
|
||||
|
||||
def configChangedSlot(self):
|
||||
|
||||
pass
|
||||
|
||||
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 _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__, BUILTIN_SETTINGS)
|
||||
self._loadNodes()
|
||||
|
||||
def _loadBuilinNodesPerType(self, node_dict, node_type, default_settings):
|
||||
|
||||
settings = LocalConfig.instance().settings()
|
||||
if node_type in settings.get(self.__class__.__name__, {}):
|
||||
for device in settings[self.__class__.__name__][node_type]:
|
||||
name = device.get("name")
|
||||
server = device.get("server")
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
if key in node_dict or not name or not server:
|
||||
continue
|
||||
node_settings = default_settings.copy()
|
||||
node_settings.update(device)
|
||||
node_dict[key] = node_settings
|
||||
|
||||
def _loadNodes(self):
|
||||
"""
|
||||
Load the built-in nodes from the persistent settings file.
|
||||
"""
|
||||
|
||||
self._loadBuilinNodesPerType(self._cloud_nodes, "cloud_nodes", CLOUD_SETTINGS)
|
||||
self._loadBuilinNodesPerType(self._ethernet_hubs, "ethernet_hubs", ETHERNET_HUB_SETTINGS)
|
||||
self._loadBuilinNodesPerType(self._ethernet_switches, "ethernet_switches", ETHERNET_SWITCH_SETTINGS)
|
||||
|
||||
def _saveNodes(self):
|
||||
"""
|
||||
Saves the built-in nodes to the persistent settings file.
|
||||
"""
|
||||
|
||||
self._settings["cloud_nodes"] = list(self._cloud_nodes.values())
|
||||
self._settings["ethernet_hubs"] = list(self._ethernet_hubs.values())
|
||||
self._settings["ethernet_switches"] = list(self._ethernet_switches.values())
|
||||
self._saveSettings()
|
||||
|
||||
def cloudNodes(self):
|
||||
"""
|
||||
Returns cloud nodes settings.
|
||||
|
||||
:returns: Cloud nodes settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._cloud_nodes
|
||||
|
||||
def setCloudNodes(self, new_cloud_nodes):
|
||||
"""
|
||||
Sets cloud nodes settings.
|
||||
|
||||
:param new_cloud_nodes: cloud nodes settings (dictionary)
|
||||
"""
|
||||
|
||||
self._cloud_nodes = new_cloud_nodes.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def ethernetHubs(self):
|
||||
"""
|
||||
Returns Ethernet hubs settings.
|
||||
|
||||
:returns: Ethernet hubs settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._ethernet_hubs
|
||||
|
||||
def setEthernetHubs(self, new_ethernet_hubs):
|
||||
"""
|
||||
Sets Ethernet hubs settings.
|
||||
|
||||
:param new_ethernet_hubs: Ethernet hubs settings (dictionary)
|
||||
"""
|
||||
|
||||
self._ethernet_hubs = new_ethernet_hubs.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def ethernetSwitches(self):
|
||||
"""
|
||||
Returns Ethernet switches settings.
|
||||
|
||||
:returns: Ethernet switches settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._ethernet_switches
|
||||
|
||||
def setEthernetSwitches(self, new_ethernet_switches):
|
||||
"""
|
||||
Sets Ethernet switches settings.
|
||||
|
||||
:param new_ethernet_switches: Ethernet switches settings (dictionary)
|
||||
"""
|
||||
|
||||
self._ethernet_switches = new_ethernet_switches.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def addNode(self, node):
|
||||
"""
|
||||
Adds a node to this module.
|
||||
@@ -67,33 +213,55 @@ class Builtin(Module):
|
||||
Resets the module.
|
||||
"""
|
||||
|
||||
log.info("Built-in module reset")
|
||||
self._nodes.clear()
|
||||
|
||||
def createNode(self, node_class, server, project):
|
||||
def instantiateNode(self, node_class, server, project):
|
||||
"""
|
||||
Creates a new node.
|
||||
Instantiate a new node.
|
||||
|
||||
:param node_class: Node object
|
||||
:param server: HTTPClient instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("creating node {}".format(node_class))
|
||||
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def setupNode(self, node, node_name):
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Setups a node.
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("configuring node {}".format(node))
|
||||
node.setup()
|
||||
log.info("creating node {}".format(node))
|
||||
if isinstance(node, Cloud):
|
||||
for key, info in self._cloud_nodes.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
|
||||
return
|
||||
elif isinstance(node, Nat):
|
||||
for key, info in self._nat_nodes.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(default_name_format=default_name_format)
|
||||
return
|
||||
elif isinstance(node, EthernetHub):
|
||||
for key, info in self._ethernet_hubs.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
|
||||
return
|
||||
elif isinstance(node, EthernetSwitch):
|
||||
for key, info in self._ethernet_switches.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
|
||||
return
|
||||
node.create()
|
||||
|
||||
@staticmethod
|
||||
def findAlternativeInterface(node, missing_interface):
|
||||
@@ -129,6 +297,22 @@ class Builtin(Module):
|
||||
return globals()[name]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def getNodeType(name, platform=None):
|
||||
if name == "cloud":
|
||||
return Cloud
|
||||
elif name == "nat":
|
||||
return Nat
|
||||
elif name == "ethernet_hub":
|
||||
return EthernetHub
|
||||
elif name == "ethernet_switch":
|
||||
return EthernetSwitch
|
||||
elif name == "frame_relay_switch":
|
||||
return FrameRelaySwitch
|
||||
elif name == "atm_switch":
|
||||
return ATMSwitch
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def classes():
|
||||
"""
|
||||
@@ -137,7 +321,7 @@ class Builtin(Module):
|
||||
:returns: list of classes
|
||||
"""
|
||||
|
||||
return [Cloud, Host]
|
||||
return [Nat, Cloud, EthernetHub, EthernetSwitch, FrameRelaySwitch, ATMSwitch]
|
||||
|
||||
def nodes(self):
|
||||
"""
|
||||
@@ -152,8 +336,44 @@ class Builtin(Module):
|
||||
"name": node_class.symbolName(),
|
||||
"categories": node_class.categories(),
|
||||
"symbol": node_class.defaultSymbol(),
|
||||
"builtin": True}
|
||||
"builtin": True,
|
||||
"node_type": node_class.URL_PREFIX
|
||||
}
|
||||
)
|
||||
|
||||
# add custom cloud node templates
|
||||
for cloud_node in self._cloud_nodes.values():
|
||||
nodes.append(
|
||||
{"class": Cloud.__name__,
|
||||
"name": cloud_node["name"],
|
||||
"server": cloud_node["server"],
|
||||
"symbol": cloud_node["symbol"],
|
||||
"categories": [cloud_node["category"]]
|
||||
}
|
||||
)
|
||||
|
||||
# add custom Ethernet hub templates
|
||||
for hub in self._ethernet_hubs.values():
|
||||
nodes.append(
|
||||
{"class": EthernetHub.__name__,
|
||||
"name": hub["name"],
|
||||
"server": hub["server"],
|
||||
"symbol": hub["symbol"],
|
||||
"categories": [hub["category"]]
|
||||
}
|
||||
)
|
||||
|
||||
# add custom Ethernet switch templates
|
||||
for switch in self._ethernet_switches.values():
|
||||
nodes.append(
|
||||
{"class": EthernetSwitch.__name__,
|
||||
"name": switch["name"],
|
||||
"server": switch["server"],
|
||||
"symbol": switch["symbol"],
|
||||
"categories": [switch["category"]]
|
||||
}
|
||||
)
|
||||
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
@@ -162,7 +382,12 @@ class Builtin(Module):
|
||||
:returns: QWidget object list
|
||||
"""
|
||||
|
||||
return []
|
||||
from .pages.builtin_preferences_page import BuiltinPreferencesPage
|
||||
from .pages.cloud_preferences_page import CloudPreferencesPage
|
||||
from .pages.ethernet_hub_preferences_page import EthernetHubPreferencesPage
|
||||
from .pages.ethernet_switch_preferences_page import EthernetSwitchPreferencesPage
|
||||
|
||||
return [BuiltinPreferencesPage, EthernetHubPreferencesPage, EthernetSwitchPreferencesPage, CloudPreferencesPage]
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
|
||||
229
gns3/modules/builtin/atm_switch.py
Normal file
229
gns3/modules/builtin/atm_switch.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# -*- 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
|
||||
import uuid
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ATMSwitch(Node):
|
||||
|
||||
"""
|
||||
ATM switch.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
URL_PREFIX = "atm_switch"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"mappings": {}})
|
||||
|
||||
def create(self, name=None, node_id=None, mappings=None, default_name_format="ATM{0}"):
|
||||
"""
|
||||
Creates this ATM switch.
|
||||
|
||||
:param name: optional name for this switch.
|
||||
:param node_id: Node identifier on the server
|
||||
:param mappings: mappings to be automatically added when creating this ATM switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if mappings:
|
||||
params["mappings"] = mappings
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this ATM switch.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this ATM switch.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """ATM switch {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's Node ID is {node_id}
|
||||
Hardware is Dynamips emulated simple ATM switch
|
||||
Switch's server runs on {host}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self._compute.name())
|
||||
|
||||
port_info = ""
|
||||
mapping = re.compile(r"""^([0-9]*):([0-9]*):([0-9]*)$""")
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " Port {} is empty\n".format(port.name())
|
||||
else:
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
for source, destination in self._settings["mappings"].items():
|
||||
match_source_mapping = mapping.search(source)
|
||||
match_destination_mapping = mapping.search(destination)
|
||||
if match_source_mapping and match_destination_mapping:
|
||||
source_port, source_vpi, source_vci = match_source_mapping.group(1, 2, 3)
|
||||
destination_port, destination_vpi, destination_vci = match_destination_mapping.group(1, 2, 3)
|
||||
else:
|
||||
source_port, source_vpi = source.split(":")
|
||||
destination_port, destination_vpi = destination.split(":")
|
||||
source_vci = destination_vci = 0
|
||||
|
||||
if port.name() == source_port or port.name() == destination_port:
|
||||
if port.name() == source_port:
|
||||
vpi1 = source_vpi
|
||||
vci1 = source_vci
|
||||
port = destination_port
|
||||
vci2 = destination_vci
|
||||
vpi2 = destination_vpi
|
||||
else:
|
||||
vpi1 = destination_vpi
|
||||
vci1 = destination_vci
|
||||
port = source_port
|
||||
vci2 = source_vci
|
||||
vpi2 = source_vpi
|
||||
|
||||
if vci1 and vci2:
|
||||
port_info += " incoming VPI {vpi1} and VCI {vci1} is switched to port {port} outgoing VPI {vpi2} and VCI {vci2}\n".format(vpi1=vpi1,
|
||||
vci1=vci1,
|
||||
port=port,
|
||||
vpi2=vpi2,
|
||||
vci2=vci2)
|
||||
else:
|
||||
port_info += " incoming VPI {vpi1} is switched to port {port} outgoing VPI {vpi2}\n".format(vpi1=vpi1,
|
||||
port=port,
|
||||
vpi2=vpi2)
|
||||
|
||||
break
|
||||
|
||||
return info + port_info
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this ATM switch
|
||||
(to be saved in a topology file).
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
atmsw = super().dump()
|
||||
if self._settings["mappings"]:
|
||||
atmsw["properties"]["mappings"] = self._settings["mappings"]
|
||||
return atmsw
|
||||
|
||||
def load(self, node_info):
|
||||
"""
|
||||
Loads an ATM switch representation
|
||||
(from a topology file).
|
||||
|
||||
:param node_info: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
super().load(node_info)
|
||||
properties = node_info["properties"]
|
||||
name = properties.pop("name")
|
||||
|
||||
# ATM switches do not have an UUID before version 2.0
|
||||
node_id = properties.get("node_id", str(uuid.uuid4()))
|
||||
|
||||
mappings = {}
|
||||
if "mappings" in properties:
|
||||
mappings = properties["mappings"]
|
||||
|
||||
log.info("ATM switch {} is loading".format(name))
|
||||
self.create(name, node_id, mappings)
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
|
||||
from .pages.atm_switch_configuration_page import ATMSwitchConfigurationPage
|
||||
return ATMSwitchConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/atm_switch.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "ATM switch"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.switches]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "ATM switch"
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
# 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
|
||||
@@ -15,21 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
NIO implementation on the client side (in the form of a pseudo node represented as a cloud).
|
||||
"""
|
||||
|
||||
import re
|
||||
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
|
||||
from gns3.nios.nio_vde import NIOVDE
|
||||
from gns3.nios.nio_null import NIONull
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -38,238 +24,53 @@ log = logging.getLogger(__name__)
|
||||
class Cloud(Node):
|
||||
|
||||
"""
|
||||
Dynamips cloud.
|
||||
Cloud node
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
_name_instance_count = 1
|
||||
URL_PREFIX = "cloud"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self._interfaces = {}
|
||||
self._cloud_settings = {"ports_mapping": []}
|
||||
self.settings().update(self._cloud_settings)
|
||||
|
||||
log.info("cloud is being created")
|
||||
# create an unique id and name
|
||||
self._name_id = Cloud._name_instance_count
|
||||
Cloud._name_instance_count += 1
|
||||
def interfaces(self):
|
||||
|
||||
name = "Cloud {}".format(self._name_id)
|
||||
self.setStatus(Node.started) # this is an always-on node
|
||||
self._initial_settings = None
|
||||
self._settings = {"name": name,
|
||||
"interfaces": {},
|
||||
"nios": []}
|
||||
return self._interfaces
|
||||
|
||||
def delete(self):
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="Cloud{0}"):
|
||||
"""
|
||||
Deletes this cloud.
|
||||
"""
|
||||
|
||||
# first delete all the links attached to this node
|
||||
self.delete_links_signal.emit()
|
||||
self.deleted_signal.emit()
|
||||
|
||||
def setup(self, name=None, additional_settings={}):
|
||||
"""
|
||||
Setups this cloud.
|
||||
Creates this cloud.
|
||||
|
||||
:param name: optional name for this cloud
|
||||
:param node_id: Node identifier on the server
|
||||
:param ports: ports to be automatically added when creating this cloud
|
||||
"""
|
||||
|
||||
if name:
|
||||
self._settings["name"] = name
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
if additional_settings and "nios" in additional_settings:
|
||||
self._settings["nios"] = additional_settings["nios"]
|
||||
|
||||
self._server.get("/interfaces", self._setupCallback)
|
||||
|
||||
def _setupCallback(self, result, error=False, **kwargs):
|
||||
def _createCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for setup.
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
if "ports_mapping" in result:
|
||||
self._settings["ports_mapping"] = result["ports_mapping"].copy()
|
||||
|
||||
if error:
|
||||
log.error("error while setting up {}: {}".format(self.name(), result["message"]))
|
||||
# a warning message instead of a error is more appropriate here
|
||||
self.warning_signal.emit(self.id(), result["message"])
|
||||
else:
|
||||
self._settings["interfaces"] = result.copy()
|
||||
|
||||
if self._settings["nios"]:
|
||||
self._addPorts(self._settings["nios"])
|
||||
|
||||
if self._loading:
|
||||
self.loaded_signal.emit()
|
||||
else:
|
||||
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):
|
||||
"""
|
||||
Creates a NIO UDP.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_udp:(\d+):(.+):(\d+)$""", nio)
|
||||
if match:
|
||||
lport = int(match.group(1))
|
||||
rhost = match.group(2)
|
||||
rport = int(match.group(3))
|
||||
return NIOUDP(lport, rhost, rport)
|
||||
return None
|
||||
|
||||
def _createNIOGenericEthernet(self, nio):
|
||||
"""
|
||||
Creates a NIO Generic Ethernet.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_gen_eth:(.+)$""", nio)
|
||||
if match:
|
||||
ethernet_device = match.group(1)
|
||||
return NIOGenericEthernet(ethernet_device)
|
||||
return None
|
||||
|
||||
def _createNIOLinuxEthernet(self, nio):
|
||||
"""
|
||||
Creates a NIO Linux Ethernet.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_gen_linux:(.+)$""", nio)
|
||||
if match:
|
||||
linux_device = match.group(1)
|
||||
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.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_tap:(.+)$""", nio)
|
||||
if match:
|
||||
tap_device = match.group(1)
|
||||
return NIOTAP(tap_device)
|
||||
return None
|
||||
|
||||
def _createNIOUNIX(self, nio):
|
||||
"""
|
||||
Creates a NIO UNIX.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_unix:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
local_file = match.group(1)
|
||||
remote_file = match.group(2)
|
||||
return NIOUNIX(local_file, remote_file)
|
||||
return None
|
||||
|
||||
def _createNIOVDE(self, nio):
|
||||
"""
|
||||
Creates a NIO VDE.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_vde:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
control_file = match.group(1)
|
||||
local_file = match.group(2)
|
||||
return NIOVDE(control_file, local_file)
|
||||
return None
|
||||
|
||||
def _createNIONull(self, nio):
|
||||
"""
|
||||
Creates a NIO Null.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_null:(.+)$""", nio)
|
||||
if match:
|
||||
identifier = match.group(1)
|
||||
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))
|
||||
if "interfaces" in result:
|
||||
self._interfaces = result["interfaces"].copy()
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
@@ -278,45 +79,35 @@ class Cloud(Node):
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
updated = False
|
||||
if "nios" in new_settings:
|
||||
nios = new_settings["nios"]
|
||||
self._addPorts(nios, ignore_existing_nio=True)
|
||||
updated = True
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
# 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
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
self._settings["nios"] = new_settings["nios"].copy()
|
||||
:param result: server response
|
||||
"""
|
||||
|
||||
if "name" in new_settings and new_settings["name"] != self.name():
|
||||
self._settings["name"] = new_settings["name"]
|
||||
updated = True
|
||||
if "ports_mapping" in result:
|
||||
self._settings["ports_mapping"] = result["ports_mapping"].copy()
|
||||
|
||||
if updated:
|
||||
log.info("cloud {} has been updated".format(self.name()))
|
||||
self.updated_signal.emit()
|
||||
|
||||
def deleteNIO(self, port):
|
||||
|
||||
pass
|
||||
if "interfaces" in result:
|
||||
self._interfaces = result["interfaces"].copy()
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this cloud.
|
||||
|
||||
:returns: formated string
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Cloud device {name} is always-on
|
||||
This is a pseudo-device for external connections
|
||||
This is a node for external connections
|
||||
""".format(name=self.name())
|
||||
|
||||
port_info = ""
|
||||
@@ -327,124 +118,8 @@ This is a pseudo-device for external connections
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
# add the Windows interface name
|
||||
match = re.search(r"""^nio_gen_eth:(\\device\\npf_.+)$""", port.name())
|
||||
if match:
|
||||
for interface in self._settings["interfaces"]:
|
||||
if interface["name"].lower() == match.group(1):
|
||||
port_info += " Windows name: {}\n".format(interface["description"])
|
||||
break
|
||||
|
||||
return info + port_info
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this cloud
|
||||
(to be saved in a topology file).
|
||||
|
||||
:returns: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
cloud = {"id": self.id(),
|
||||
"type": self.__class__.__name__,
|
||||
"description": str(self),
|
||||
"properties": {"name": self.name(),
|
||||
"nios": self._settings["nios"]},
|
||||
"server_id": self._server.id()}
|
||||
|
||||
# add the ports
|
||||
if self._ports:
|
||||
ports = cloud["ports"] = []
|
||||
for port in self._ports:
|
||||
ports.append(port.dump())
|
||||
|
||||
return cloud
|
||||
|
||||
def load(self, node_info):
|
||||
"""
|
||||
Loads a cloud representation
|
||||
(from a topology file).
|
||||
|
||||
:param node_info: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
settings = node_info["properties"]
|
||||
name = settings.pop("name")
|
||||
log.info("cloud {} is loading".format(name))
|
||||
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.loaded_signal.disconnect(self._updatePortSettings)
|
||||
|
||||
# update the port with the correct IDs
|
||||
if "ports" in self._node_info:
|
||||
ports = self._node_info["ports"]
|
||||
for topology_port in ports:
|
||||
for port in self._ports:
|
||||
if topology_port["name"] == port.name():
|
||||
port.setId(topology_port["id"])
|
||||
if topology_port["name"].startswith("nio_gen_eth") or topology_port["name"].startswith("nio_linux_eth"):
|
||||
# lookup if the interface exists
|
||||
available_interface = False
|
||||
topology_port_name = topology_port["name"].split(':', 1)[1]
|
||||
for interface in self._settings["interfaces"]:
|
||||
if interface["name"] == topology_port_name:
|
||||
available_interface = True
|
||||
break
|
||||
if not available_interface:
|
||||
alternative_interface = self._module.findAlternativeInterface(self, topology_port_name)
|
||||
if alternative_interface:
|
||||
if topology_port["name"] in self._settings["nios"]:
|
||||
self._settings["nios"].remove(topology_port["name"])
|
||||
topology_port["name"] = topology_port["name"].replace(topology_port_name, alternative_interface)
|
||||
nio = self._allocateNIO(topology_port["name"])
|
||||
port.setDefaultNio(nio)
|
||||
port.setName(topology_port["name"])
|
||||
self._settings["nios"].append(topology_port["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):
|
||||
"""
|
||||
Returns the name of this cloud.
|
||||
|
||||
:returns: name (string)
|
||||
"""
|
||||
|
||||
return self._settings["name"]
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns all this cloud settings.
|
||||
|
||||
:returns: settings dictionary
|
||||
"""
|
||||
|
||||
return self._settings
|
||||
|
||||
def ports(self):
|
||||
"""
|
||||
Returns all the ports for this cloud.
|
||||
|
||||
:returns: list of Port instances
|
||||
"""
|
||||
|
||||
return self._ports
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
@@ -475,7 +150,7 @@ This is a pseudo-device for external connections
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node category (integer)
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.end_devices]
|
||||
|
||||
57
gns3/modules/builtin/dialogs/cloud_wizard.py
Normal file
57
gns3/modules/builtin/dialogs/cloud_wizard.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Wizard for cloud nodes.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.node import Node
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.cloud_wizard_ui import Ui_CloudNodeWizard
|
||||
from .. import Builtin
|
||||
|
||||
|
||||
class CloudWizard(VMWizard, Ui_CloudNodeWizard):
|
||||
|
||||
"""
|
||||
Wizard to create a cloud node template.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, cloud_nodes, parent):
|
||||
|
||||
super().__init__(cloud_nodes, parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/cloud.svg"))
|
||||
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
|
||||
def getSettings(self):
|
||||
"""
|
||||
Returns the settings set in this Wizard.
|
||||
|
||||
:return: settings dict
|
||||
"""
|
||||
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"category": Node.end_devices,
|
||||
"server": self._compute_id}
|
||||
|
||||
return settings
|
||||
63
gns3/modules/builtin/dialogs/ethernet_hub_wizard.py
Normal file
63
gns3/modules/builtin/dialogs/ethernet_hub_wizard.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/>.
|
||||
|
||||
"""
|
||||
Wizard for Ethernet hubs.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.node import Node
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.ethernet_hub_wizard_ui import Ui_EthernetHubWizard
|
||||
from .. import Builtin
|
||||
|
||||
|
||||
class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
|
||||
|
||||
"""
|
||||
Wizard to create an Ethernet hub template.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, ethernet_hubs, parent):
|
||||
|
||||
super().__init__(ethernet_hubs, parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/hub.svg"))
|
||||
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
|
||||
def getSettings(self):
|
||||
"""
|
||||
Returns the settings set in this Wizard.
|
||||
|
||||
:return: settings dict
|
||||
"""
|
||||
|
||||
ports = []
|
||||
for port_number in range(0, self.uiPortsSpinBox.value()):
|
||||
ports.append({"port_number": int(port_number),
|
||||
"name": "Ethernet{}".format(port_number)})
|
||||
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"symbol": ":/symbols/hub.svg",
|
||||
"category": Node.switches,
|
||||
"server": self._compute_id,
|
||||
"ports_mapping": ports}
|
||||
|
||||
return settings
|
||||
66
gns3/modules/builtin/dialogs/ethernet_switch_wizard.py
Normal file
66
gns3/modules/builtin/dialogs/ethernet_switch_wizard.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Wizard for Ethernet switches.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.node import Node
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.ethernet_switch_wizard_ui import Ui_EthernetSwitchWizard
|
||||
from .. import Builtin
|
||||
|
||||
|
||||
class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
|
||||
|
||||
"""
|
||||
Wizard to create an Ethernet switch template.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, ethernet_switches, parent):
|
||||
|
||||
super().__init__(ethernet_switches, parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/ethernet_switch.svg"))
|
||||
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
|
||||
def getSettings(self):
|
||||
"""
|
||||
Returns the settings set in this Wizard.
|
||||
|
||||
:return: settings dict
|
||||
"""
|
||||
|
||||
ports = []
|
||||
for port_number in range(0, self.uiPortsSpinBox.value()):
|
||||
ports.append({"port_number": int(port_number),
|
||||
"name": "Ethernet{}".format(port_number),
|
||||
"type": "access",
|
||||
"vlan": 1,
|
||||
"ethertype": ""})
|
||||
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"symbol": ":/symbols/ethernet_switch.svg",
|
||||
"category": Node.switches,
|
||||
"server": self._compute_id,
|
||||
"ports_mapping": ports}
|
||||
|
||||
return settings
|
||||
150
gns3/modules/builtin/ethernet_hub.py
Normal file
150
gns3/modules/builtin/ethernet_hub.py
Normal file
@@ -0,0 +1,150 @@
|
||||
# -*- 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.node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EthernetHub(Node):
|
||||
"""
|
||||
Ethernet hub.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
URL_PREFIX = "ethernet_hub"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"ports_mapping": []})
|
||||
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="Hub{0}"):
|
||||
"""
|
||||
Creates this hub.
|
||||
|
||||
:param name: optional name for this hub
|
||||
:param node_id: node identifier on the server
|
||||
:param ports: ports to automatically be added when creating this hub
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this Ethernet hub.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if "name" in new_settings:
|
||||
params["name"] = new_settings["name"]
|
||||
if "ports_mapping" in new_settings:
|
||||
params["ports_mapping"] = new_settings["ports_mapping"]
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this Ethernet hub.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Ethernet hub {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's node ID is {node_id}
|
||||
Hub's server runs on {host}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self.compute().name())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " Port {} is empty\n".format(port.name())
|
||||
else:
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
return info + port_info
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
|
||||
from .pages.ethernet_hub_configuration_page import EthernetHubConfigurationPage
|
||||
return EthernetHubConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/hub.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Ethernet hub"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.switches]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Ethernet hub"
|
||||
170
gns3/modules/builtin/ethernet_switch.py
Normal file
170
gns3/modules/builtin/ethernet_switch.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EthernetSwitch(Node):
|
||||
|
||||
"""
|
||||
Ethernet switch.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
URL_PREFIX = "ethernet_switch"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"ports_mapping": []})
|
||||
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="SW{0}"):
|
||||
"""
|
||||
Creates this Ethernet switch.
|
||||
|
||||
:param name: optional name for this switch
|
||||
:param node_id: node identifier on the server
|
||||
:param ports: ports to be automatically added when creating this switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this Ethernet switch.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this Ethernet switch.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Ethernet switch {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's Node ID is {node_id}
|
||||
Switch's server runs on {host}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self.compute().name())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " Port {} is empty\n".format(port.name())
|
||||
else:
|
||||
for port_settings in self._settings["ports_mapping"]:
|
||||
if port_settings["port_number"] == port.portNumber():
|
||||
|
||||
port_type = port_settings["type"]
|
||||
port_ethertype = port_settings.get("ethertype", "")
|
||||
port_vlan = port_settings["vlan"]
|
||||
port_ethertype_info = ""
|
||||
|
||||
if port_type == "access":
|
||||
port_vlan_info = "VLAN ID {}".format(port_vlan)
|
||||
elif port_type == "dot1q":
|
||||
port_vlan_info = "native VLAN {}".format(port_vlan)
|
||||
elif port_type == "qinq":
|
||||
port_vlan_info = "outer VLAN {}".format(port_vlan)
|
||||
port_ethertype_info = "({})".format(port_ethertype)
|
||||
|
||||
port_info += " Port {name} is in {port_type} {port_ethertype_info} mode, with {port_vlan_info},\n".format(name=port.name(),
|
||||
port_type=port_type,
|
||||
port_ethertype_info=port_ethertype_info,
|
||||
port_vlan_info=port_vlan_info)
|
||||
port_info += " {port_description}\n".format(port_description=port.description())
|
||||
break
|
||||
|
||||
return info + port_info
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
|
||||
from .pages.ethernet_switch_configuration_page import EthernetSwitchConfigurationPage
|
||||
return EthernetSwitchConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/ethernet_switch.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Ethernet switch"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.switches]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Ethernet switch"
|
||||
171
gns3/modules/builtin/frame_relay_switch.py
Normal file
171
gns3/modules/builtin/frame_relay_switch.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FrameRelaySwitch(Node):
|
||||
|
||||
"""
|
||||
Frame-Relay switch.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
URL_PREFIX = "frame_relay_switch"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"mappings": {}})
|
||||
|
||||
def create(self, name=None, node_id=None, mappings={}, default_name_format="FR{0}"):
|
||||
"""
|
||||
Creates this Frame Relay switch.
|
||||
|
||||
:param name: name for this switch.
|
||||
:param node_id: node identifier on the server
|
||||
:param mappings: mappings to be automatically added when creating this Frame relay switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if mappings:
|
||||
params["mappings"] = mappings
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this Frame Relay switch.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this Frame Relay switch.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Frame relay switch {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's Node ID is {node_id}
|
||||
Hardware is Dynamips emulated simple Frame relay switch
|
||||
Switch's server runs on {host}:{port}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self._compute.host(),
|
||||
port=self._compute.port())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " Port {} is empty\n".format(port.name())
|
||||
else:
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
for source, destination in self._settings["mappings"].items():
|
||||
source_port, source_dlci = source.split(":")
|
||||
destination_port, destination_dlci = destination.split(":")
|
||||
|
||||
if port.name() == source_port or port.name() == destination_port:
|
||||
if port.name() == source_port:
|
||||
dlci1 = source_dlci
|
||||
port = destination_port
|
||||
dlci2 = destination_dlci
|
||||
else:
|
||||
dlci1 = destination_dlci
|
||||
port = source_port
|
||||
dlci2 = source_dlci
|
||||
port_info += " incoming DLCI {dlci1} is switched to port {port} outgoing DLCI {dlci2}\n".format(dlci1=dlci1,
|
||||
port=port,
|
||||
dlci2=dlci2)
|
||||
break
|
||||
|
||||
return info + port_info
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
|
||||
from .pages.frame_relay_switch_configuration_page import FrameRelaySwitchConfigurationPage
|
||||
return FrameRelaySwitchConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/frame_relay_switch.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Frame Relay switch"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.switches]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Frame Relay switch"
|
||||
@@ -1,108 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from gns3.node import Node
|
||||
from .cloud import Cloud
|
||||
|
||||
import logging
|
||||
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, 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)
|
||||
self._settings["name"] = name
|
||||
|
||||
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):
|
||||
"""
|
||||
Auto adds all Ethernet and TAP interfaces.
|
||||
|
||||
:param node_id: ignored
|
||||
"""
|
||||
|
||||
new_settings = {"nios": []}
|
||||
for interface in self._settings["interfaces"]:
|
||||
if interface["name"].startswith("tap"):
|
||||
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
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this host.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/computer.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Host"
|
||||
|
||||
@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 "Host"
|
||||
143
gns3/modules/builtin/nat.py
Normal file
143
gns3/modules/builtin/nat.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# -*- 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.node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Nat(Node):
|
||||
|
||||
"""
|
||||
Nat node
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
URL_PREFIX = "nat"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self._nat_settings = {}
|
||||
self.settings().update(self._nat_settings)
|
||||
|
||||
def interfaces(self):
|
||||
|
||||
return self._interfaces
|
||||
|
||||
def create(self, name=None, node_id=None, default_name_format="Nat{0}"):
|
||||
"""
|
||||
Creates this nat.
|
||||
|
||||
:param name: optional name for this nat
|
||||
:param node_id: Node identifier on the server
|
||||
"""
|
||||
|
||||
params = {}
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while creating nat: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this nat.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
if error:
|
||||
log.error("Error while creating nat: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this nat.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Nat device {name} is always-on
|
||||
This is a node for external connections
|
||||
""".format(name=self.name())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " Port {} is empty\n".format(port.name())
|
||||
else:
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
return info + port_info
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this nat.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/cloud.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Nat"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.end_devices]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Nat"
|
||||
@@ -195,7 +195,5 @@ class ATMSwitchConfigurationPage(QtWidgets.QWidget, Ui_atmSwitchConfigPageWidget
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "ATM switch name cannot be empty!")
|
||||
else:
|
||||
settings["name"] = name
|
||||
else:
|
||||
del settings["name"]
|
||||
|
||||
settings["mappings"] = self._mapping.copy()
|
||||
return settings
|
||||
60
gns3/modules/builtin/pages/builtin_preferences_page.py
Normal file
60
gns3/modules/builtin/pages/builtin_preferences_page.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Configuration page for Built-in preferences.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from .. import Builtin
|
||||
from ..ui.builtin_preferences_page_ui import Ui_BuiltinPreferencesPageWidget
|
||||
from ..settings import BUILTIN_SETTINGS
|
||||
|
||||
|
||||
class BuiltinPreferencesPage(QtWidgets.QWidget, Ui_BuiltinPreferencesPageWidget):
|
||||
"""QWidget preference page for Built-in."""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
# connect signals
|
||||
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
|
||||
|
||||
def _restoreDefaultsSlot(self):
|
||||
"""Slot to populate the page widgets with the default settings."""
|
||||
|
||||
self._populateWidgets(BUILTIN_SETTINGS)
|
||||
|
||||
def _populateWidgets(self, settings):
|
||||
"""Populates the widgets with the settings.
|
||||
|
||||
:param settings: Built-in settings
|
||||
"""
|
||||
|
||||
def loadPreferences(self):
|
||||
"""Loads Built-in preferences."""
|
||||
|
||||
builtin_settings = Builtin.instance().settings()
|
||||
self._populateWidgets(builtin_settings)
|
||||
|
||||
def savePreferences(self):
|
||||
"""Saves Built-in preferences."""
|
||||
|
||||
new_settings = {}
|
||||
Builtin.instance().setSettings(new_settings)
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
# 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
|
||||
@@ -19,8 +19,11 @@
|
||||
Configuration page for clouds.
|
||||
"""
|
||||
|
||||
import re
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.controller import Controller
|
||||
from gns3.node import Node
|
||||
|
||||
from ..ui.cloud_configuration_page_ui import Ui_cloudConfigPageWidget
|
||||
|
||||
|
||||
@@ -34,492 +37,339 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
self._nios = set()
|
||||
self._node = None
|
||||
self._ports = []
|
||||
self._interfaces = []
|
||||
|
||||
# connect NIO generic Ethernet slots
|
||||
self.uiGenericEthernetComboBox.currentIndexChanged.connect(self._genericEthernetSelectedSlot)
|
||||
self.uiGenericEthernetListWidget.itemSelectionChanged.connect(self._genericEthernetChangedSlot)
|
||||
self.uiAddGenericEthernetPushButton.clicked.connect(self._genericEthernetAddSlot)
|
||||
self.uiDeleteGenericEthernetPushButton.clicked.connect(self._genericEthernetDeleteSlot)
|
||||
# add the categories
|
||||
for name, category in Node.defaultCategories().items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
|
||||
# connect NIO Linux Ethernet slots
|
||||
self.uiLinuxEthernetComboBox.currentIndexChanged.connect(self._linuxEthernetSelectedSlot)
|
||||
self.uiLinuxEthernetListWidget.itemSelectionChanged.connect(self._linuxEthernetChangedSlot)
|
||||
self.uiAddLinuxEthernetPushButton.clicked.connect(self._linuxEthernetAddSlot)
|
||||
self.uiDeleteLinuxEthernetPushButton.clicked.connect(self._linuxEthernetDeleteSlot)
|
||||
# connect Ethernet slots
|
||||
self.uiEthernetListWidget.itemSelectionChanged.connect(self._EthernetChangedSlot)
|
||||
self.uiEthernetWarningPushButton.clicked.connect(self._EthernetWarningSlot)
|
||||
self.uiAddEthernetPushButton.clicked.connect(self._EthernetAddSlot)
|
||||
self.uiAddAllEthernetPushButton.clicked.connect(self._EthernetAddAllSlot)
|
||||
self.uiDeleteEthernetPushButton.clicked.connect(self._EthernetDeleteSlot)
|
||||
|
||||
# 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 TAP slots
|
||||
self.uiTAPComboBox.currentIndexChanged.connect(self._TAPSelectedSlot)
|
||||
self.uiTAPListWidget.itemSelectionChanged.connect(self._TAPChangedSlot)
|
||||
self.uiAddTAPPushButton.clicked.connect(self._TAPAddSlot)
|
||||
self.uiAddAllTAPPushButton.clicked.connect(self._TAPAddAllSlot)
|
||||
self.uiDeleteTAPPushButton.clicked.connect(self._TAPDeleteSlot)
|
||||
|
||||
# connect NIO UDP slots
|
||||
self.uiNIOUDPListWidget.currentRowChanged.connect(self._NIOUDPSelectedSlot)
|
||||
self.uiNIOUDPListWidget.itemSelectionChanged.connect(self._NIOUDPChangedSlot)
|
||||
self.uiAddNIOUDPPushButton.clicked.connect(self._NIOUDPAddSlot)
|
||||
self.uiDeleteNIOUDPPushButton.clicked.connect(self._NIOUDPDeleteSlot)
|
||||
# connect UDP slots
|
||||
self.uiUDPTreeWidget.itemActivated.connect(self._UDPSelectedSlot)
|
||||
self.uiUDPTreeWidget.itemSelectionChanged.connect(self._UDPChangedSlot)
|
||||
self.uiAddUDPPushButton.clicked.connect(self._UDPAddSlot)
|
||||
self.uiDeleteUDPPushButton.clicked.connect(self._UDPDeleteSlot)
|
||||
|
||||
# connect NIO TAP slots
|
||||
self.uiNIOTAPListWidget.currentRowChanged.connect(self._NIOTAPSelectedSlot)
|
||||
self.uiNIOTAPListWidget.itemSelectionChanged.connect(self._NIOTAPChangedSlot)
|
||||
self.uiAddNIOTAPPushButton.clicked.connect(self._NIOTAPAddSlot)
|
||||
self.uiDeleteNIOTAPPushButton.clicked.connect(self._NIOTAPDeleteSlot)
|
||||
self.uiShowSpecialInterfacesCheckBox.stateChanged.connect(self._showSpecialInterfacesSlot)
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
|
||||
# connect NIO UNIX slots
|
||||
self.uiNIOUNIXListWidget.currentRowChanged.connect(self._NIOUNIXSelectedSlot)
|
||||
self.uiNIOUNIXListWidget.itemSelectionChanged.connect(self._NIOUNIXChangedSlot)
|
||||
self.uiAddNIOUNIXPushButton.clicked.connect(self._NIOUNIXAddSlot)
|
||||
self.uiDeleteNIOUNIXPushButton.clicked.connect(self._NIOUNIXDeleteSlot)
|
||||
|
||||
# connect NIO VDE slots
|
||||
self.uiNIOVDEListWidget.currentRowChanged.connect(self._NIOVDESelectedSlot)
|
||||
self.uiNIOVDEListWidget.itemSelectionChanged.connect(self._NIOVDEChangedSlot)
|
||||
self.uiAddNIOVDEPushButton.clicked.connect(self._NIOVDEAddSlot)
|
||||
self.uiDeleteNIOVDEPushButton.clicked.connect(self._NIOVDEDeleteSlot)
|
||||
|
||||
# connect NIO NULL slots
|
||||
self.uiNIONullListWidget.currentRowChanged.connect(self._NIONullSelectedSlot)
|
||||
self.uiNIONullListWidget.itemSelectionChanged.connect(self._NIONullChangedSlot)
|
||||
self.uiAddNIONullPushButton.clicked.connect(self._NIONullAddSlot)
|
||||
self.uiDeleteNIONullPushButton.clicked.connect(self._NIONullDeleteSlot)
|
||||
|
||||
def _genericEthernetSelectedSlot(self, index):
|
||||
"""
|
||||
Loads the selected generic Ethernet interface in lineEdit.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
self.uiGenericEthernetLineEdit.setText(self.uiGenericEthernetComboBox.currentText())
|
||||
|
||||
def _genericEthernetChangedSlot(self):
|
||||
def _EthernetChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiGenericEthernetListWidget.currentItem()
|
||||
item = self.uiEthernetListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteGenericEthernetPushButton.setEnabled(True)
|
||||
self.uiDeleteEthernetPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteGenericEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteEthernetPushButton.setEnabled(False)
|
||||
|
||||
def _genericEthernetAddSlot(self):
|
||||
def _EthernetWarningSlot(self):
|
||||
"""
|
||||
Adds a new generic Ethernet NIO.
|
||||
Shows a warning about Wifi Ethernet interfaces.
|
||||
"""
|
||||
|
||||
interface = self.uiGenericEthernetLineEdit.text()
|
||||
QtWidgets.QMessageBox.warning(self, "Ethernet interfaces", "Wifi interfaces may not work properly. It is recommended to use wired Ethernet or Loopback interfaces only.")
|
||||
|
||||
def _EthernetAddSlot(self, interface=None):
|
||||
"""
|
||||
Adds a new Ethernet interface.
|
||||
"""
|
||||
|
||||
if not interface:
|
||||
interface = self.uiEthernetComboBox.currentText()
|
||||
if interface:
|
||||
nio = "nio_gen_eth:{interface}".format(interface=interface)
|
||||
if nio not in self._nios:
|
||||
self.uiGenericEthernetListWidget.addItem(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _genericEthernetDeleteSlot(self):
|
||||
"""
|
||||
Deletes the selected generic Ethernet NIO.
|
||||
"""
|
||||
|
||||
item = self.uiGenericEthernetListWidget.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))
|
||||
for port in self._ports:
|
||||
if port["name"] == interface and port["type"] == "ethernet":
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiGenericEthernetListWidget.takeItem(self.uiGenericEthernetListWidget.currentRow())
|
||||
self.uiEthernetListWidget.addItem(interface)
|
||||
self._ports.append({"name": interface,
|
||||
"port_number": len(self._ports),
|
||||
"type": "ethernet",
|
||||
"interface": interface})
|
||||
index = self.uiEthernetComboBox.findText(interface)
|
||||
if index != -1:
|
||||
self.uiEthernetComboBox.removeItem(index)
|
||||
|
||||
def _linuxEthernetSelectedSlot(self, index):
|
||||
def _EthernetAddAllSlot(self):
|
||||
"""
|
||||
Loads the selected Linux interface in lineEdit.
|
||||
Adds all Ethernet interfaces.
|
||||
"""
|
||||
|
||||
for index in range(0, self.uiEthernetComboBox.count()):
|
||||
interface = self.uiEthernetComboBox.itemText(index)
|
||||
self._EthernetAddSlot(interface)
|
||||
|
||||
def _EthernetDeleteSlot(self):
|
||||
"""
|
||||
Deletes the selected Ethernet interface.
|
||||
"""
|
||||
|
||||
if self._node:
|
||||
for item in self.uiEthernetListWidget.selectedItems():
|
||||
interface = item.text()
|
||||
# check we can delete that interface
|
||||
for node_port in self._node.ports():
|
||||
if node_port.name() == interface and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to {}, please remove it first".format(interface))
|
||||
return
|
||||
|
||||
for item in self.uiEthernetListWidget.selectedItems():
|
||||
interface = item.text()
|
||||
for port in self._ports.copy():
|
||||
if port["name"] == interface:
|
||||
self._ports.remove(port)
|
||||
self.uiEthernetListWidget.takeItem(self.uiEthernetListWidget.row(item))
|
||||
for interface in self._interfaces:
|
||||
if not self.uiShowSpecialInterfacesCheckBox.isChecked() and interface["special"]:
|
||||
continue
|
||||
if interface["name"] == port["name"] and interface["type"] == "ethernet":
|
||||
self.uiEthernetComboBox.addItem(interface["name"])
|
||||
break
|
||||
break
|
||||
|
||||
def _TAPSelectedSlot(self, index):
|
||||
"""
|
||||
Loads the selected TAP interface.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
self.uiLinuxEthernetLineEdit.setText(self.uiLinuxEthernetComboBox.currentText())
|
||||
self.uiTAPLineEdit.setText(self.uiTAPComboBox.currentText())
|
||||
|
||||
def _linuxEthernetChangedSlot(self):
|
||||
def _TAPChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiLinuxEthernetListWidget.currentItem()
|
||||
item = self.uiTAPListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteLinuxEthernetPushButton.setEnabled(True)
|
||||
self.uiDeleteTAPPushButton.setEnabled(True)
|
||||
self.uiTAPLineEdit.setText(item.text())
|
||||
else:
|
||||
self.uiDeleteLinuxEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteTAPPushButton.setEnabled(False)
|
||||
|
||||
def _linuxEthernetAddSlot(self):
|
||||
def _TAPAddSlot(self, interface=None):
|
||||
"""
|
||||
Adds a new Linux Ethernet NIO.
|
||||
Adds a new TAP interface.
|
||||
"""
|
||||
|
||||
interface = self.uiLinuxEthernetLineEdit.text()
|
||||
if not interface:
|
||||
interface = self.uiTAPLineEdit.text()
|
||||
if interface:
|
||||
nio = "nio_gen_linux:{interface}".format(interface=interface)
|
||||
if nio not in self._nios:
|
||||
self.uiLinuxEthernetListWidget.addItem(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _linuxEthernetDeleteSlot(self):
|
||||
"""
|
||||
Deletes the selected Linux Ethernet NIO.
|
||||
"""
|
||||
|
||||
item = self.uiLinuxEthernetListWidget.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))
|
||||
for port in self._ports:
|
||||
if port["name"] == interface and port["type"] == "tap":
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiLinuxEthernetListWidget.takeItem(self.uiLinuxEthernetListWidget.currentRow())
|
||||
self.uiTAPListWidget.addItem(interface)
|
||||
self._ports.append({"name": interface,
|
||||
"port_number": len(self._ports),
|
||||
"type": "tap",
|
||||
"interface": interface})
|
||||
index = self.uiTAPComboBox.findText(interface)
|
||||
if index != -1:
|
||||
self.uiTAPComboBox.removeItem(index)
|
||||
|
||||
def _NIONATSelectedSlot(self, index):
|
||||
def _TAPAddAllSlot(self):
|
||||
"""
|
||||
Loads a selected NAT NIO.
|
||||
|
||||
:param index: ignored
|
||||
Adds all TAP interfaces
|
||||
"""
|
||||
|
||||
item = self.uiNIONATListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_nat:(.+)$""", nio)
|
||||
if match:
|
||||
self.uiNIONATIdentiferLineEdit.setText(match.group(1))
|
||||
for index in range(0, self.uiTAPComboBox.count()):
|
||||
interface = self.uiTAPComboBox.itemText(index)
|
||||
self._TAPAddSlot(interface)
|
||||
|
||||
def _NIONATChangedSlot(self):
|
||||
def _TAPDeleteSlot(self):
|
||||
"""
|
||||
Deletes a TAP interface.
|
||||
"""
|
||||
|
||||
if self._node:
|
||||
for item in self.uiTAPListWidget.selectedItems():
|
||||
interface = item.text()
|
||||
# check we can delete that interface
|
||||
for node_port in self._node.ports():
|
||||
if node_port.name() == interface and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to {}, please remove it first".format(interface))
|
||||
return
|
||||
|
||||
for item in self.uiTAPListWidget.selectedItems():
|
||||
interface = item.text()
|
||||
for port in self._ports.copy():
|
||||
if port["name"] == interface:
|
||||
self._ports.remove(port)
|
||||
self.uiTAPListWidget.takeItem(self.uiTAPListWidget.row(item))
|
||||
for interface in self._interfaces:
|
||||
if interface["name"] == port["name"] and interface["type"] == "tap":
|
||||
self.uiTAPComboBox.addItem(interface["name"])
|
||||
break
|
||||
|
||||
def _UDPSelectedSlot(self, item, column):
|
||||
"""
|
||||
Loads a selected UDP tunnel.
|
||||
|
||||
:param item: selected TreeWidgetItem instance
|
||||
:param column: ignored
|
||||
"""
|
||||
|
||||
name = item.text(0)
|
||||
local_port = int(item.text(1))
|
||||
remote_host = item.text(2)
|
||||
remote_port = int(item.text(3))
|
||||
self.uiUDPNameLineEdit.setText(name)
|
||||
self.uiLocalPortSpinBox.setValue(local_port)
|
||||
self.uiRemoteHostLineEdit.setText(remote_host)
|
||||
self.uiRemotePortSpinBox.setValue(remote_port)
|
||||
|
||||
def _UDPChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiNIONATListWidget.currentItem()
|
||||
item = self.uiUDPTreeWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIONATPushButton.setEnabled(True)
|
||||
self.uiDeleteUDPPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteNIONATPushButton.setEnabled(False)
|
||||
self.uiDeleteUDPPushButton.setEnabled(False)
|
||||
|
||||
def _NIONATAddSlot(self):
|
||||
def _UDPAddSlot(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.uiNIONATListWidget.takeItem(self.uiNIONATListWidget.currentRow())
|
||||
|
||||
def _NIOUDPSelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected UDP.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIOUDPListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_udp:(\d+):(.+):(\d+)$""", nio)
|
||||
if match:
|
||||
self.uiLocalPortSpinBox.setValue(int(match.group(1)))
|
||||
self.uiRemoteHostLineEdit.setText(match.group(2))
|
||||
self.uiRemotePortSpinBox.setValue(int(match.group(3)))
|
||||
|
||||
def _NIOUDPChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiNIOUDPListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIOUDPPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteNIOUDPPushButton.setEnabled(False)
|
||||
|
||||
def _NIOUDPAddSlot(self):
|
||||
"""
|
||||
Adds a new UDP NIO.
|
||||
Adds a new UDP tunnel
|
||||
"""
|
||||
|
||||
name = self.uiUDPNameLineEdit.text()
|
||||
local_port = self.uiLocalPortSpinBox.value()
|
||||
remote_host = self.uiRemoteHostLineEdit.text()
|
||||
remote_port = self.uiRemotePortSpinBox.value()
|
||||
if remote_host:
|
||||
nio = "nio_udp:{lport}:{rhost}:{rport}".format(lport=local_port,
|
||||
rhost=remote_host,
|
||||
rport=remote_port)
|
||||
if nio not in self._nios:
|
||||
self.uiNIOUDPListWidget.addItem(nio)
|
||||
self._nios.add(nio)
|
||||
self.uiLocalPortSpinBox.setValue(local_port + 1)
|
||||
self.uiRemotePortSpinBox.setValue(remote_port + 1)
|
||||
|
||||
def _NIOUDPDeleteSlot(self):
|
||||
"""
|
||||
Deletes an UDP NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIOUDPListWidget.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))
|
||||
if name and remote_host:
|
||||
for port in self._ports:
|
||||
if port["name"] == name:
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIOUDPListWidget.takeItem(self.uiNIOUDPListWidget.currentRow())
|
||||
|
||||
def _NIOTAPSelectedSlot(self, index):
|
||||
# add a new entry in the tree widget
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiUDPTreeWidget)
|
||||
item.setText(0, name)
|
||||
item.setText(1, str(local_port))
|
||||
item.setText(2, remote_host)
|
||||
item.setText(3, str(remote_port))
|
||||
self.uiUDPTreeWidget.addTopLevelItem(item)
|
||||
self._ports.append({"name": name,
|
||||
"port_number": len(self._ports),
|
||||
"type": "udp",
|
||||
"lport": local_port,
|
||||
"rhost": remote_host,
|
||||
"rport": remote_port})
|
||||
self.uiLocalPortSpinBox.setValue(local_port + 1)
|
||||
self.uiRemotePortSpinBox.setValue(remote_port + 1)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(0)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(1)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(2)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(3)
|
||||
nb_tunnels = 0
|
||||
for port in self._ports:
|
||||
if port["type"] == "udp":
|
||||
nb_tunnels += 1
|
||||
self.uiUDPNameLineEdit.setText("UDP tunnel {}".format(nb_tunnels + 1))
|
||||
|
||||
def _UDPDeleteSlot(self):
|
||||
"""
|
||||
Loads the selected NIO TAP in lineEdit.
|
||||
|
||||
:param index: ignored
|
||||
Deletes an UDP tunnel.
|
||||
"""
|
||||
|
||||
item = self.uiNIOTAPListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_tap:(.+)$""", nio)
|
||||
if match:
|
||||
self.uiNIOTAPLineEdit.setText(match.group(1))
|
||||
if self._node:
|
||||
for item in self.uiUDPTreeWidget.selectedItems():
|
||||
name = item.text(0)
|
||||
# check we can delete that UDP tunnel
|
||||
for node_port in self._node.ports():
|
||||
if node_port.name() == name and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to {}, please remove it first".format(name))
|
||||
return
|
||||
|
||||
def _NIOTAPChangedSlot(self):
|
||||
for item in self.uiUDPTreeWidget.selectedItems():
|
||||
name = item.text(0)
|
||||
for port in self._ports.copy():
|
||||
if port["name"] == name:
|
||||
self._ports.remove(port)
|
||||
self.uiUDPTreeWidget.takeTopLevelItem(self.uiUDPTreeWidget.indexOfTopLevelItem(item))
|
||||
nb_tunnels = 0
|
||||
for port in self._ports:
|
||||
if port["type"] == "udp":
|
||||
nb_tunnels += 1
|
||||
self.uiUDPNameLineEdit.setText("UDP tunnel {}".format(nb_tunnels + 1))
|
||||
|
||||
def _showSpecialInterfacesSlot(self, state):
|
||||
|
||||
self.uiEthernetComboBox.clear()
|
||||
index = 0
|
||||
for interface in self._interfaces:
|
||||
if interface["type"] == "ethernet":
|
||||
if not state and interface["special"]:
|
||||
continue
|
||||
if self.uiEthernetListWidget.findItems(interface["name"], QtCore.Qt.MatchFixedString):
|
||||
continue
|
||||
self.uiEthernetComboBox.addItem(interface["name"])
|
||||
index += 1
|
||||
|
||||
def _symbolBrowserSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
Slot to open the symbol browser and select a new symbol.
|
||||
"""
|
||||
|
||||
item = self.uiNIOTAPListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIOTAPPushButton.setEnabled(True)
|
||||
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 _loadNetworkInterfaces(self, interfaces):
|
||||
|
||||
self.uiEthernetComboBox.clear()
|
||||
index = 0
|
||||
for interface in interfaces:
|
||||
if interface["type"] == "ethernet" and not interface["special"]:
|
||||
self.uiEthernetComboBox.addItem(interface["name"])
|
||||
index += 1
|
||||
|
||||
# load all TAP interfaces
|
||||
self.uiTAPComboBox.clear()
|
||||
index = 0
|
||||
for interface in interfaces:
|
||||
if interface["type"] == "tap":
|
||||
self.uiTAPComboBox.addItem(interface["name"])
|
||||
index += 1
|
||||
|
||||
def _getInterfacesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for retrieving the network interfaces
|
||||
|
||||
:param progress_dialog: QProgressDialog instance
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Network interfaces", "{}".format(result["message"]))
|
||||
else:
|
||||
self.uiDeleteNIOTAPPushButton.setEnabled(False)
|
||||
self._interfaces = result
|
||||
self._loadNetworkInterfaces(result)
|
||||
|
||||
def _NIOTAPAddSlot(self):
|
||||
"""
|
||||
Adds a new UDP NIO.
|
||||
"""
|
||||
|
||||
tap_interface = self.uiNIOTAPLineEdit.text()
|
||||
if tap_interface:
|
||||
nio = "nio_tap:{}".format(tap_interface.lower())
|
||||
if nio not in self._nios:
|
||||
self.uiNIOTAPListWidget.addItem(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _NIOTAPDeleteSlot(self):
|
||||
"""
|
||||
Deletes a TAP NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIOTAPListWidget.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.uiNIOTAPListWidget.takeItem(self.uiNIOTAPListWidget.currentRow())
|
||||
|
||||
def _NIOUNIXSelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected UNIX NIO.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIOUNIXListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_unix:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
self.uiLocalFileLineEdit.setText(match.group(1))
|
||||
self.uiRemoteFileLineEdit.setText(match.group(2))
|
||||
|
||||
def _NIOUNIXChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiNIOUNIXListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIOUNIXPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteNIOUNIXPushButton.setEnabled(False)
|
||||
|
||||
def _NIOUNIXAddSlot(self):
|
||||
"""
|
||||
Adds a new UNIX NIO.
|
||||
"""
|
||||
|
||||
local_file = self.uiLocalFileLineEdit.text()
|
||||
remote_file = self.uiRemoteFileLineEdit.text()
|
||||
if local_file and remote_file:
|
||||
nio = "nio_unix:{local}:{remote}".format(local=local_file,
|
||||
remote=remote_file)
|
||||
if nio not in self._nios:
|
||||
self.uiNIOUNIXListWidget.addItem(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _NIOUNIXDeleteSlot(self):
|
||||
"""
|
||||
Deletes an UNIX NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIOUNIXListWidget.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.uiNIOUNIXListWidget.takeItem(self.uiNIOUNIXListWidget.currentRow())
|
||||
|
||||
def _NIOVDESelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected VDE NIO.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIOVDEListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_vde:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
self.uiVDEControlFileLineEdit.setText(match.group(1))
|
||||
self.uiVDELocalFileLineEdit.setText(match.group(2))
|
||||
|
||||
def _NIOVDEChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiNIOVDEListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIOVDEPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteNIOVDEPushButton.setEnabled(False)
|
||||
|
||||
def _NIOVDEAddSlot(self):
|
||||
"""
|
||||
Adds a new VDE NIO.
|
||||
"""
|
||||
|
||||
control_file = self.uiVDEControlFileLineEdit.text()
|
||||
local_file = self.uiVDELocalFileLineEdit.text()
|
||||
if local_file and control_file:
|
||||
nio = "nio_vde:{control}:{local}".format(control=control_file, local=local_file)
|
||||
if nio not in self._nios:
|
||||
self.uiNIOVDEListWidget.addItem(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _NIOVDEDeleteSlot(self):
|
||||
"""
|
||||
Deletes a VDE NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIOVDEListWidget.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.uiNIOVDEListWidget.takeItem(self.uiNIOVDEListWidget.currentRow())
|
||||
|
||||
def _NIONullSelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected NULL NIO.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIONullListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_null:(.+)$""", nio)
|
||||
if match:
|
||||
self.uiNIONullIdentiferLineEdit.setText(match.group(1))
|
||||
|
||||
def _NIONullChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiNIONullListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIONullPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteNIONullPushButton.setEnabled(False)
|
||||
|
||||
def _NIONullAddSlot(self):
|
||||
"""
|
||||
Adds a new NULL NIO.
|
||||
"""
|
||||
|
||||
identifier = self.uiNIONullIdentiferLineEdit.text()
|
||||
if identifier:
|
||||
nio = "nio_null:{}".format(identifier)
|
||||
if nio not in self._nios:
|
||||
self.uiNIONullListWidget.addItem(nio)
|
||||
self._nios.add(nio)
|
||||
|
||||
def _NIONullDeleteSlot(self):
|
||||
"""
|
||||
Deletes a NULL NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIONullListWidget.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.uiNIONullListWidget.takeItem(self.uiNIONullListWidget.currentRow())
|
||||
|
||||
def loadSettings(self, settings, node, group=False):
|
||||
def loadSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Loads the cloud settings.
|
||||
|
||||
@@ -533,58 +383,72 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
else:
|
||||
self.uiNameLineEdit.setEnabled(False)
|
||||
|
||||
self._node = node
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# load all network interfaces
|
||||
self.uiGenericEthernetComboBox.clear()
|
||||
index = 0
|
||||
for interface in settings["interfaces"]:
|
||||
if interface["name"].startswith("tap"):
|
||||
# do not add TAP interfaces
|
||||
continue
|
||||
self.uiGenericEthernetComboBox.addItem(interface["name"])
|
||||
self.uiGenericEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
|
||||
index += 1
|
||||
self.uiGenericEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
# rename the label from "Name" to "Template name"
|
||||
self.uiNameLabel.setText("Template name:")
|
||||
|
||||
# load all network interfaces
|
||||
self.uiLinuxEthernetComboBox.clear()
|
||||
index = 0
|
||||
for interface in settings["interfaces"]:
|
||||
if not interface["name"].startswith(r"\Device\NPF_") and not interface["name"].startswith("tap"):
|
||||
self.uiLinuxEthernetComboBox.addItem(interface["name"])
|
||||
self.uiLinuxEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
|
||||
index += 1
|
||||
self.uiLinuxEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
# load the default name format
|
||||
self.uiDefaultNameFormatLineEdit.setText(settings["default_name_format"])
|
||||
|
||||
# populate the NIO lists
|
||||
self.nios = set()
|
||||
self.uiGenericEthernetListWidget.clear()
|
||||
self.uiLinuxEthernetListWidget.clear()
|
||||
self.uiNIOUDPListWidget.clear()
|
||||
self.uiNIOTAPListWidget.clear()
|
||||
self.uiNIOUNIXListWidget.clear()
|
||||
self.uiNIOVDEListWidget.clear()
|
||||
self.uiNIONullListWidget.clear()
|
||||
# load the symbol
|
||||
self.uiSymbolLineEdit.setText(settings["symbol"])
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(settings["symbol"]))
|
||||
|
||||
for nio in settings["nios"]:
|
||||
self._nios.add(nio)
|
||||
if nio.lower().startswith("nio_gen_eth"):
|
||||
self.uiGenericEthernetListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_gen_linux"):
|
||||
self.uiLinuxEthernetListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_udp"):
|
||||
self.uiNIOUDPListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_tap"):
|
||||
self.uiNIOTAPListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_unix"):
|
||||
self.uiNIOUNIXListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_vde"):
|
||||
self.uiNIOVDEListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_null"):
|
||||
self.uiNIONullListWidget.addItem(nio)
|
||||
# load the category
|
||||
index = self.uiCategoryComboBox.findData(settings["category"])
|
||||
if index != -1:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
|
||||
def saveSettings(self, settings, node, group=False):
|
||||
Controller.instance().getCompute("/network/interfaces", settings["server"],
|
||||
self._getInterfacesFromServerCallback,
|
||||
progressText="Retrieving network interfaces...")
|
||||
|
||||
else:
|
||||
self.uiDefaultNameFormatLabel.hide()
|
||||
self.uiDefaultNameFormatLineEdit.hide()
|
||||
self.uiSymbolLabel.hide()
|
||||
self.uiSymbolLineEdit.hide()
|
||||
self.uiSymbolToolButton.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
self.uiCategoryLabel.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
self._node = node
|
||||
self._interfaces = self._node.interfaces()
|
||||
self._loadNetworkInterfaces(self._interfaces)
|
||||
|
||||
# load the current ports
|
||||
self._ports = []
|
||||
self.uiEthernetListWidget.clear()
|
||||
self.uiTAPListWidget.clear()
|
||||
self.uiUDPTreeWidget.clear()
|
||||
|
||||
for port in settings["ports_mapping"]:
|
||||
self._ports.append(port)
|
||||
if port["type"] == "ethernet":
|
||||
self.uiEthernetListWidget.addItem(port["name"])
|
||||
index = self.uiEthernetComboBox.findText(port["name"])
|
||||
if index != -1:
|
||||
self.uiEthernetComboBox.removeItem(index)
|
||||
elif port["type"] == "tap":
|
||||
self.uiTAPListWidget.addItem(port["name"])
|
||||
index = self.uiTAPComboBox.findText(port["name"])
|
||||
if index != -1:
|
||||
self.uiTAPComboBox.removeItem(index)
|
||||
elif port["type"] == "udp":
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiUDPTreeWidget)
|
||||
item.setText(0, port["name"])
|
||||
item.setText(1, str(port["lport"]))
|
||||
item.setText(2, port["rhost"])
|
||||
item.setText(3, str(port["rport"]))
|
||||
self.uiUDPTreeWidget.addTopLevelItem(item)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(0)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(1)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(2)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(3)
|
||||
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Saves the cloud settings.
|
||||
|
||||
@@ -595,7 +459,22 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
|
||||
if not group:
|
||||
settings["name"] = self.uiNameLineEdit.text()
|
||||
else:
|
||||
del settings["name"]
|
||||
|
||||
settings["nios"] = list(self._nios)
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# 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()
|
||||
settings["symbol"] = symbol_path
|
||||
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
settings["ports_mapping"] = self._ports
|
||||
else:
|
||||
settings["ports_mapping"] = self._ports
|
||||
return settings
|
||||
|
||||
188
gns3/modules/builtin/pages/cloud_preferences_page.py
Normal file
188
gns3/modules/builtin/pages/cloud_preferences_page.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Configuration page for cloud node preferences.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.controller import Controller
|
||||
|
||||
from .. import Builtin
|
||||
from ..settings import CLOUD_SETTINGS
|
||||
from ..ui.cloud_preferences_page_ui import Ui_CloudPreferencesPageWidget
|
||||
from ..pages.cloud_configuration_page import CloudConfigurationPage
|
||||
from ..dialogs.cloud_wizard import CloudWizard
|
||||
|
||||
|
||||
class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
"""
|
||||
QWidget preference page for cloud node preferences.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = MainWindow.instance()
|
||||
self._cloud_nodes = {}
|
||||
self._items = []
|
||||
|
||||
self.uiNewCloudNodePushButton.clicked.connect(self._newCloudNodeSlot)
|
||||
self.uiEditCloudNodePushButton.clicked.connect(self._editCloudNodeSlot)
|
||||
self.uiDeleteCloudNodePushButton.clicked.connect(self._deleteCloudNodeSlot)
|
||||
self.uiCloudNodesTreeWidget.itemSelectionChanged.connect(self._cloudNodeChangedSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
|
||||
section_item = QtWidgets.QTreeWidgetItem(self.uiCloudNodeInfoTreeWidget)
|
||||
section_item.setText(0, name)
|
||||
font = section_item.font(0)
|
||||
font.setBold(True)
|
||||
section_item.setFont(0, font)
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, cloud_node):
|
||||
|
||||
self.uiCloudNodeInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", cloud_node["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", cloud_node["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["server"]).name()])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.uiCloudNodeInfoTreeWidget.expandAll()
|
||||
self.uiCloudNodeInfoTreeWidget.resizeColumnToContents(0)
|
||||
self.uiCloudNodeInfoTreeWidget.resizeColumnToContents(1)
|
||||
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _cloudNodeChangedSlot(self):
|
||||
"""
|
||||
Loads a selected cloud node template from the tree widget.
|
||||
"""
|
||||
|
||||
selection = self.uiCloudNodesTreeWidget.selectedItems()
|
||||
self.uiDeleteCloudNodePushButton.setEnabled(len(selection) != 0)
|
||||
single_selected = len(selection) == 1
|
||||
self.uiEditCloudNodePushButton.setEnabled(single_selected)
|
||||
|
||||
if single_selected:
|
||||
key = selection[0].data(0, QtCore.Qt.UserRole)
|
||||
cloud_node = self._cloud_nodes[key]
|
||||
self._refreshInfo(cloud_node)
|
||||
else:
|
||||
self.uiCloudNodeInfoTreeWidget.clear()
|
||||
|
||||
def _newCloudNodeSlot(self):
|
||||
"""
|
||||
Creates a new cloud node template.
|
||||
"""
|
||||
|
||||
wizard = CloudWizard(self._cloud_nodes, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_cloud_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_cloud_settings["server"], name=new_cloud_settings["name"])
|
||||
self._cloud_nodes[key] = CLOUD_SETTINGS.copy()
|
||||
self._cloud_nodes[key].update(new_cloud_settings)
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiCloudNodesTreeWidget)
|
||||
item.setText(0, self._cloud_nodes[key]["name"])
|
||||
Controller.instance().getSymbolIcon(self._cloud_nodes[key]["symbol"], qpartial(self._setItemIcon, item))
|
||||
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
self.uiCloudNodesTreeWidget.setCurrentItem(item)
|
||||
|
||||
def _editCloudNodeSlot(self):
|
||||
"""
|
||||
Edits a cloud node template.
|
||||
"""
|
||||
|
||||
item = self.uiCloudNodesTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
cloud_node = self._cloud_nodes[key]
|
||||
dialog = ConfigurationDialog(cloud_node["name"], cloud_node, CloudConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(cloud_node["symbol"], qpartial(self._setItemIcon, item))
|
||||
if cloud_node["name"] != item.text(0):
|
||||
new_key = "{server}:{name}".format(server=cloud_node["server"], name=cloud_node["name"])
|
||||
if new_key in self._cloud_nodes:
|
||||
QtWidgets.QMessageBox.critical(self, "Cloud node", "Cloud node name {} already exists for server {}".format(cloud_node["name"],
|
||||
cloud_node["server"]))
|
||||
cloud_node["name"] = item.text(0)
|
||||
return
|
||||
self._cloud_nodes[new_key] = self._cloud_nodes[key]
|
||||
del self._cloud_nodes[key]
|
||||
item.setText(0, cloud_node["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(cloud_node)
|
||||
|
||||
def _deleteCloudNodeSlot(self):
|
||||
"""
|
||||
Deletes a cloud node template.
|
||||
"""
|
||||
|
||||
for item in self.uiCloudNodesTreeWidget.selectedItems():
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
del self._cloud_nodes[key]
|
||||
self.uiCloudNodesTreeWidget.takeTopLevelItem(self.uiCloudNodesTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
def loadPreferences(self):
|
||||
"""
|
||||
Loads the cloud node preferences.
|
||||
"""
|
||||
|
||||
builtin_module = Builtin.instance()
|
||||
self._cloud_nodes = copy.deepcopy(builtin_module.cloudNodes())
|
||||
self._items.clear()
|
||||
|
||||
for key, cloud_node in self._cloud_nodes.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiCloudNodesTreeWidget)
|
||||
item.setText(0, cloud_node["name"])
|
||||
Controller.instance().getSymbolIcon(cloud_node["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
|
||||
if self._items:
|
||||
self.uiCloudNodesTreeWidget.setCurrentItem(self._items[0])
|
||||
self.uiCloudNodesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
item.setIcon(0, icon)
|
||||
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def savePreferences(self):
|
||||
"""
|
||||
Saves the cloud node preferences.
|
||||
"""
|
||||
|
||||
Builtin.instance().setCloudNodes(self._cloud_nodes)
|
||||
152
gns3/modules/builtin/pages/ethernet_hub_configuration_page.py
Normal file
152
gns3/modules/builtin/pages/ethernet_hub_configuration_page.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# -*- 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 Ethernet hubs.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.dialogs.node_properties_dialog import ConfigurationError
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.node import Node
|
||||
|
||||
from ..ui.ethernet_hub_configuration_page_ui import Ui_ethernetHubConfigPageWidget
|
||||
|
||||
|
||||
class EthernetHubConfigurationPage(QtWidgets.QWidget, Ui_ethernetHubConfigPageWidget):
|
||||
|
||||
"""
|
||||
QWidget configuration page for Ethernet hubs.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
# add the categories
|
||||
for name, category in Node.defaultCategories().items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
|
||||
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 Ethernet hub settings.
|
||||
|
||||
:param settings: the settings (dictionary)
|
||||
:param node: Node instance
|
||||
:param group: indicates the settings apply to a group
|
||||
"""
|
||||
|
||||
if not group:
|
||||
self.uiNameLineEdit.setText(settings["name"])
|
||||
else:
|
||||
self.uiNameLineEdit.hide()
|
||||
self.uiNameLabel.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"]))
|
||||
|
||||
# load the category
|
||||
index = self.uiCategoryComboBox.findData(settings["category"])
|
||||
if index != -1:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
self.uiDefaultNameFormatLabel.hide()
|
||||
self.uiDefaultNameFormatLineEdit.hide()
|
||||
self.uiSymbolLabel.hide()
|
||||
self.uiSymbolLineEdit.hide()
|
||||
self.uiSymbolToolButton.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
self.uiCategoryLabel.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
|
||||
nb_ports = len(settings["ports_mapping"])
|
||||
self.uiPortsSpinBox.setValue(nb_ports)
|
||||
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Saves the Ethernet hub settings.
|
||||
|
||||
:param settings: the settings (dictionary)
|
||||
:param node: Node instance
|
||||
:param group: indicates the settings apply to a group
|
||||
"""
|
||||
|
||||
if not group:
|
||||
# set the device name
|
||||
name = self.uiNameLineEdit.text()
|
||||
if not name:
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "Ethernet hub name cannot be empty!")
|
||||
else:
|
||||
settings["name"] = name
|
||||
|
||||
nb_ports = self.uiPortsSpinBox.value()
|
||||
|
||||
if node:
|
||||
# check that a link isn't connected to a port before we delete it
|
||||
ports = node.ports()
|
||||
for port in ports:
|
||||
if not port.isFree() and port.portNumber() > nb_ports:
|
||||
self.loadSettings(settings, node)
|
||||
QtWidgets.QMessageBox.critical(self, node.name(), "A link is connected to port {}, please remove it first".format(port.name()))
|
||||
raise ConfigurationError()
|
||||
|
||||
else:
|
||||
# these are template settings
|
||||
|
||||
# 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()
|
||||
settings["symbol"] = symbol_path
|
||||
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
|
||||
settings["ports_mapping"] = []
|
||||
for port_number in range(0, nb_ports):
|
||||
settings["ports_mapping"].append({"port_number": int(port_number),
|
||||
"name": "Ethernet{}".format(port_number)})
|
||||
return settings
|
||||
189
gns3/modules/builtin/pages/ethernet_hub_preferences_page.py
Normal file
189
gns3/modules/builtin/pages/ethernet_hub_preferences_page.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Configuration page for Ethernet hub preferences.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.controller import Controller
|
||||
|
||||
from .. import Builtin
|
||||
from ..settings import ETHERNET_HUB_SETTINGS
|
||||
from ..ui.ethernet_hub_preferences_page_ui import Ui_EthernetHubPreferencesPageWidget
|
||||
from ..pages.ethernet_hub_configuration_page import EthernetHubConfigurationPage
|
||||
from ..dialogs.ethernet_hub_wizard import EthernetHubWizard
|
||||
|
||||
|
||||
class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPageWidget):
|
||||
"""
|
||||
QWidget preference page for Ethernet hub preferences.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = MainWindow.instance()
|
||||
self._ethernet_hubs = {}
|
||||
self._items = []
|
||||
|
||||
self.uiNewEthernetHubPushButton.clicked.connect(self._newEthernetHubSlot)
|
||||
self.uiEditEthernetHubPushButton.clicked.connect(self._editEthernetHubSlot)
|
||||
self.uiDeleteEthernetHubPushButton.clicked.connect(self._deleteEthernetHubSlot)
|
||||
self.uiEthernetHubsTreeWidget.itemSelectionChanged.connect(self._ethernetHubChangedSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
|
||||
section_item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubInfoTreeWidget)
|
||||
section_item.setText(0, name)
|
||||
font = section_item.font(0)
|
||||
font.setBold(True)
|
||||
section_item.setFont(0, font)
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, ethernet_hub):
|
||||
|
||||
self.uiEthernetHubInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_hub["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_hub["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["server"]).name()])
|
||||
except KeyError:
|
||||
pass
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Number of ports:", str(len(ethernet_hub["ports_mapping"]))])
|
||||
|
||||
self.uiEthernetHubInfoTreeWidget.expandAll()
|
||||
self.uiEthernetHubInfoTreeWidget.resizeColumnToContents(0)
|
||||
self.uiEthernetHubInfoTreeWidget.resizeColumnToContents(1)
|
||||
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _ethernetHubChangedSlot(self):
|
||||
"""
|
||||
Loads a selected Ethernet hub template from the tree widget.
|
||||
"""
|
||||
|
||||
selection = self.uiEthernetHubsTreeWidget.selectedItems()
|
||||
self.uiDeleteEthernetHubPushButton.setEnabled(len(selection) != 0)
|
||||
single_selected = len(selection) == 1
|
||||
self.uiEditEthernetHubPushButton.setEnabled(single_selected)
|
||||
|
||||
if single_selected:
|
||||
key = selection[0].data(0, QtCore.Qt.UserRole)
|
||||
ethernet_hub = self._ethernet_hubs[key]
|
||||
self._refreshInfo(ethernet_hub)
|
||||
else:
|
||||
self.uiEthernetHubInfoTreeWidget.clear()
|
||||
|
||||
def _newEthernetHubSlot(self):
|
||||
"""
|
||||
Creates a new Ethernet hub template.
|
||||
"""
|
||||
|
||||
wizard = EthernetHubWizard(self._ethernet_hubs, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_ethernet_hub_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_ethernet_hub_settings["server"], name=new_ethernet_hub_settings["name"])
|
||||
self._ethernet_hubs[key] = ETHERNET_HUB_SETTINGS.copy()
|
||||
self._ethernet_hubs[key].update(new_ethernet_hub_settings)
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubsTreeWidget)
|
||||
item.setText(0, self._ethernet_hubs[key]["name"])
|
||||
Controller.instance().getSymbolIcon(self._ethernet_hubs[key]["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
self.uiEthernetHubsTreeWidget.setCurrentItem(item)
|
||||
|
||||
def _editEthernetHubSlot(self):
|
||||
"""
|
||||
Edits an Ethernet hub template.
|
||||
"""
|
||||
|
||||
item = self.uiEthernetHubsTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
ethernet_hub = self._ethernet_hubs[key]
|
||||
dialog = ConfigurationDialog(ethernet_hub["name"], ethernet_hub, EthernetHubConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(ethernet_hub["symbol"], qpartial(self._setItemIcon, item))
|
||||
if ethernet_hub["name"] != item.text(0):
|
||||
new_key = "{server}:{name}".format(server=ethernet_hub["server"], name=ethernet_hub["name"])
|
||||
if new_key in self._ethernet_hubs:
|
||||
QtWidgets.QMessageBox.critical(self, "Ethernet hub", "Ethernet hub name {} already exists for server {}".format(ethernet_hub["name"],
|
||||
ethernet_hub["server"]))
|
||||
ethernet_hub["name"] = item.text(0)
|
||||
return
|
||||
self._ethernet_hubs[new_key] = self._ethernet_hubs[key]
|
||||
del self._ethernet_hubs[key]
|
||||
item.setText(0, ethernet_hub["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(ethernet_hub)
|
||||
|
||||
def _deleteEthernetHubSlot(self):
|
||||
"""
|
||||
Deletes an Ethernet hub template.
|
||||
"""
|
||||
|
||||
for item in self.uiEthernetHubsTreeWidget.selectedItems():
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
del self._ethernet_hubs[key]
|
||||
self.uiEthernetHubsTreeWidget.takeTopLevelItem(self.uiEthernetHubsTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
def loadPreferences(self):
|
||||
"""
|
||||
Loads the ethernet hub preferences.
|
||||
"""
|
||||
|
||||
builtin_module = Builtin.instance()
|
||||
self._ethernet_hubs = copy.deepcopy(builtin_module.ethernetHubs())
|
||||
self._items.clear()
|
||||
|
||||
for key, ethernet_hub in self._ethernet_hubs.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubsTreeWidget)
|
||||
item.setText(0, ethernet_hub["name"])
|
||||
Controller.instance().getSymbolIcon(ethernet_hub["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
|
||||
if self._items:
|
||||
self.uiEthernetHubsTreeWidget.setCurrentItem(self._items[0])
|
||||
self.uiEthernetHubsTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
item.setIcon(0, icon)
|
||||
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def savePreferences(self):
|
||||
"""
|
||||
Saves the Ethernet hub preferences.
|
||||
"""
|
||||
|
||||
Builtin.instance().setEthernetHubs(self._ethernet_hubs)
|
||||
@@ -16,10 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for Dynamips Ethernet switches.
|
||||
Configuration page for Ethernet switches.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.node import Node
|
||||
|
||||
from ..utils.tree_widget_item import TreeWidgetItem
|
||||
from ..ui.ethernet_switch_configuration_page_ui import Ui_ethernetSwitchConfigPageWidget
|
||||
|
||||
@@ -36,6 +39,10 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
self.setupUi(self)
|
||||
self._ports = {}
|
||||
|
||||
# add the categories
|
||||
for name, category in Node.defaultCategories().items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
|
||||
# connect slots
|
||||
self.uiAddPushButton.clicked.connect(self._addPortSlot)
|
||||
self.uiDeletePushButton.clicked.connect(self._deletePortSlot)
|
||||
@@ -47,6 +54,21 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
self.uiPortsTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiPortsTreeWidget.setSortingEnabled(True)
|
||||
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
|
||||
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 _portSelectedSlot(self, item, column):
|
||||
"""
|
||||
Loads a selected port from the tree widget.
|
||||
@@ -123,7 +145,9 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
item.setText(3, port_ethertype)
|
||||
self.uiPortsTreeWidget.addTopLevelItem(item)
|
||||
|
||||
self._ports[port] = {"type": port_type,
|
||||
self._ports[port] = {"name": "Ethernet{}".format(port),
|
||||
"port_number": port,
|
||||
"type": port_type,
|
||||
"vlan": vlan,
|
||||
"ethertype": port_ethertype}
|
||||
|
||||
@@ -138,11 +162,11 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
item = self.uiPortsTreeWidget.currentItem()
|
||||
if item:
|
||||
port = int(item.text(0))
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.portNumber() == port and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to port {}, please remove it first".format(node_port.name()))
|
||||
return
|
||||
if self._node:
|
||||
for node_port in self._node.ports():
|
||||
if node_port.portNumber() == port and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to port {}, please remove it first".format(node_port.name()))
|
||||
return
|
||||
del self._ports[port]
|
||||
self.uiPortsTreeWidget.takeTopLevelItem(self.uiPortsTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
@@ -151,7 +175,7 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
else:
|
||||
self.uiPortSpinBox.setValue(1)
|
||||
|
||||
def loadSettings(self, settings, node, group=False):
|
||||
def loadSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Loads the Ethernet switch settings.
|
||||
|
||||
@@ -169,21 +193,48 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
self._ports = {}
|
||||
self._node = node
|
||||
|
||||
for port, info in settings["ports"].items():
|
||||
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"]))
|
||||
|
||||
# load the category
|
||||
index = self.uiCategoryComboBox.findData(settings["category"])
|
||||
if index != -1:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
self.uiDefaultNameFormatLabel.hide()
|
||||
self.uiDefaultNameFormatLineEdit.hide()
|
||||
self.uiSymbolLabel.hide()
|
||||
self.uiSymbolLineEdit.hide()
|
||||
self.uiSymbolToolButton.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
self.uiCategoryLabel.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
|
||||
for port_info in settings["ports_mapping"]:
|
||||
item = TreeWidgetItem(self.uiPortsTreeWidget)
|
||||
item.setText(0, str(port))
|
||||
item.setText(1, str(info["vlan"]))
|
||||
item.setText(2, info["type"])
|
||||
item.setText(3, info["ethertype"])
|
||||
item.setText(0, str(port_info["port_number"]))
|
||||
item.setText(1, str(port_info["vlan"]))
|
||||
item.setText(2, port_info["type"])
|
||||
item.setText(3, port_info.get("ethertype", ""))
|
||||
self.uiPortsTreeWidget.addTopLevelItem(item)
|
||||
self._ports[port] = info
|
||||
self._ports[port_info["port_number"]] = port_info
|
||||
|
||||
self.uiPortsTreeWidget.resizeColumnToContents(0)
|
||||
self.uiPortsTreeWidget.resizeColumnToContents(1)
|
||||
if len(self._ports) > 0:
|
||||
self.uiPortSpinBox.setValue(max(self._ports) + 1)
|
||||
|
||||
def saveSettings(self, settings, node, group=False):
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Saves the Ethernet switch settings.
|
||||
|
||||
@@ -199,7 +250,21 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "Ethernet switch name cannot be empty!")
|
||||
else:
|
||||
settings["name"] = name
|
||||
else:
|
||||
del settings["name"]
|
||||
|
||||
settings["ports"] = self._ports.copy()
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# 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()
|
||||
settings["symbol"] = symbol_path
|
||||
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
|
||||
settings["ports_mapping"] = list(self._ports.values())
|
||||
return settings
|
||||
194
gns3/modules/builtin/pages/ethernet_switch_preferences_page.py
Normal file
194
gns3/modules/builtin/pages/ethernet_switch_preferences_page.py
Normal file
@@ -0,0 +1,194 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Configuration page for Ethernet switch preferences.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from gns3.controller import Controller
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.compute_manager import ComputeManager
|
||||
|
||||
from .. import Builtin
|
||||
from ..settings import ETHERNET_SWITCH_SETTINGS
|
||||
from ..ui.ethernet_switch_preferences_page_ui import Ui_EthernetSwitchPreferencesPageWidget
|
||||
from ..pages.ethernet_switch_configuration_page import EthernetSwitchConfigurationPage
|
||||
from ..dialogs.ethernet_switch_wizard import EthernetSwitchWizard
|
||||
|
||||
|
||||
class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferencesPageWidget):
|
||||
"""
|
||||
QWidget preference page for Ethernet switch preferences.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = MainWindow.instance()
|
||||
self._ethernet_switches = {}
|
||||
self._items = []
|
||||
|
||||
self.uiNewEthernetSwitchPushButton.clicked.connect(self._newEthernetSwitchSlot)
|
||||
self.uiEditEthernetSwitchPushButton.clicked.connect(self._editEthernetSwitchSlot)
|
||||
self.uiDeleteEthernetSwitchPushButton.clicked.connect(self._deleteEthernetSwitchSlot)
|
||||
self.uiEthernetSwitchesTreeWidget.itemSelectionChanged.connect(self._ethernetSwitchChangedSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
|
||||
section_item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchInfoTreeWidget)
|
||||
section_item.setText(0, name)
|
||||
font = section_item.font(0)
|
||||
font.setBold(True)
|
||||
section_item.setFont(0, font)
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, ethernet_switch):
|
||||
|
||||
self.uiEthernetSwitchInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_switch["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_switch["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["server"]).name()])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for port in ethernet_switch["ports_mapping"]:
|
||||
section_item = self._createSectionItem("Port{}".format(port["port_number"]))
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Name:", port["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Type:", port["type"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["VLAN:", str(port["vlan"])])
|
||||
|
||||
self.uiEthernetSwitchInfoTreeWidget.expandAll()
|
||||
self.uiEthernetSwitchInfoTreeWidget.resizeColumnToContents(0)
|
||||
self.uiEthernetSwitchInfoTreeWidget.resizeColumnToContents(1)
|
||||
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _ethernetSwitchChangedSlot(self):
|
||||
"""
|
||||
Loads a selected Ethernet switch template from the tree widget.
|
||||
"""
|
||||
|
||||
selection = self.uiEthernetSwitchesTreeWidget.selectedItems()
|
||||
self.uiDeleteEthernetSwitchPushButton.setEnabled(len(selection) != 0)
|
||||
single_selected = len(selection) == 1
|
||||
self.uiEditEthernetSwitchPushButton.setEnabled(single_selected)
|
||||
|
||||
if single_selected:
|
||||
key = selection[0].data(0, QtCore.Qt.UserRole)
|
||||
ethernet_switch = self._ethernet_switches[key]
|
||||
self._refreshInfo(ethernet_switch)
|
||||
else:
|
||||
self.uiEthernetSwitchInfoTreeWidget.clear()
|
||||
|
||||
def _newEthernetSwitchSlot(self):
|
||||
"""
|
||||
Creates a new Ethernet switch template.
|
||||
"""
|
||||
|
||||
wizard = EthernetSwitchWizard(self._ethernet_switches, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_ethernet_switch_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_ethernet_switch_settings["server"], name=new_ethernet_switch_settings["name"])
|
||||
self._ethernet_switches[key] = ETHERNET_SWITCH_SETTINGS.copy()
|
||||
self._ethernet_switches[key].update(new_ethernet_switch_settings)
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchesTreeWidget)
|
||||
item.setText(0, self._ethernet_switches[key]["name"])
|
||||
Controller.instance().getSymbolIcon(self._ethernet_switches[key]["symbol"], qpartial(self._setItemIcon, item))
|
||||
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
self.uiEthernetSwitchesTreeWidget.setCurrentItem(item)
|
||||
|
||||
def _editEthernetSwitchSlot(self):
|
||||
"""
|
||||
Edits an Ethernet switch template.
|
||||
"""
|
||||
|
||||
item = self.uiEthernetSwitchesTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
ethernet_switch = self._ethernet_switches[key]
|
||||
dialog = ConfigurationDialog(ethernet_switch["name"], ethernet_switch, EthernetSwitchConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(ethernet_switch["symbol"], qpartial(self._setItemIcon, item))
|
||||
if ethernet_switch["name"] != item.text(0):
|
||||
new_key = "{server}:{name}".format(server=ethernet_switch["server"], name=ethernet_switch["name"])
|
||||
if new_key in self._ethernet_switches:
|
||||
QtWidgets.QMessageBox.critical(self, "Ethernet switch", "Ethernet switch name {} already exists for server {}".format(ethernet_switch["name"],
|
||||
ethernet_switch["server"]))
|
||||
ethernet_switch["name"] = item.text(0)
|
||||
return
|
||||
self._ethernet_switches[new_key] = self._ethernet_switches[key]
|
||||
del self._ethernet_switches[key]
|
||||
item.setText(0, ethernet_switch["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(ethernet_switch)
|
||||
|
||||
def _deleteEthernetSwitchSlot(self):
|
||||
"""
|
||||
Deletes an Ethernet switch template.
|
||||
"""
|
||||
for item in self.uiEthernetSwitchesTreeWidget.selectedItems():
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
del self._ethernet_switches[key]
|
||||
self.uiEthernetSwitchesTreeWidget.takeTopLevelItem(self.uiEthernetSwitchesTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
def loadPreferences(self):
|
||||
"""
|
||||
Loads the ethernet switch preferences.
|
||||
"""
|
||||
|
||||
builtin_module = Builtin.instance()
|
||||
self._ethernet_switches = copy.deepcopy(builtin_module.ethernetSwitches())
|
||||
self._items.clear()
|
||||
|
||||
for key, ethernet_switch in self._ethernet_switches.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchesTreeWidget)
|
||||
item.setText(0, ethernet_switch["name"])
|
||||
Controller.instance().getSymbolIcon(ethernet_switch["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
|
||||
if self._items:
|
||||
self.uiEthernetSwitchesTreeWidget.setCurrentItem(self._items[0])
|
||||
self.uiEthernetSwitchesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def savePreferences(self):
|
||||
"""
|
||||
Saves the Ethernet switch preferences.
|
||||
"""
|
||||
|
||||
Builtin.instance().setEthernetSwitches(self._ethernet_switches)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
item.setIcon(0, icon)
|
||||
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
@@ -170,7 +170,6 @@ class FrameRelaySwitchConfigurationPage(QtWidgets.QWidget, Ui_frameRelaySwitchCo
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "Frame relay switch name cannot be empty!")
|
||||
else:
|
||||
settings["name"] = name
|
||||
else:
|
||||
del settings["name"]
|
||||
|
||||
settings["mappings"] = self._mapping.copy()
|
||||
return settings
|
||||
58
gns3/modules/builtin/settings.py
Normal file
58
gns3/modules/builtin/settings.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Default Built-in settings.
|
||||
"""
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
BUILTIN_SETTINGS = {
|
||||
}
|
||||
|
||||
|
||||
NAT_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Nat{0}",
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"category": Node.end_devices,
|
||||
"ports_mapping": [],
|
||||
}
|
||||
|
||||
CLOUD_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Cloud{0}",
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"category": Node.end_devices,
|
||||
"ports_mapping": [],
|
||||
}
|
||||
|
||||
ETHERNET_HUB_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Hub{0}",
|
||||
"symbol": ":/symbols/hub.svg",
|
||||
"category": Node.switches,
|
||||
"ports_mapping": [],
|
||||
}
|
||||
|
||||
ETHERNET_SWITCH_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Switch{0}",
|
||||
"symbol": ":/symbols/ethernet_switch.svg",
|
||||
"category": Node.switches,
|
||||
"ports_mapping": [],
|
||||
}
|
||||
@@ -6,13 +6,16 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>459</width>
|
||||
<height>419</height>
|
||||
<width>540</width>
|
||||
<height>553</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>ATM Switch</string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p>This is a simple ATM switch. Only IOS c7200 routers with at least a configured PA-A1 adapter can connect to it.</p></body></html></string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0" colspan="3">
|
||||
<widget class="QGroupBox" name="uiGeneralGroupBox">
|
||||
@@ -1,19 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/dynamips/ui/atm_switch_configuration_page.ui'
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/atm_switch_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.4.2
|
||||
# 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_atmSwitchConfigPageWidget(object):
|
||||
|
||||
def setupUi(self, atmSwitchConfigPageWidget):
|
||||
atmSwitchConfigPageWidget.setObjectName("atmSwitchConfigPageWidget")
|
||||
atmSwitchConfigPageWidget.resize(459, 419)
|
||||
atmSwitchConfigPageWidget.resize(459, 430)
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout(atmSwitchConfigPageWidget)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.uiGeneralGroupBox = QtWidgets.QGroupBox(atmSwitchConfigPageWidget)
|
||||
@@ -170,6 +168,7 @@ class Ui_atmSwitchConfigPageWidget(object):
|
||||
def retranslateUi(self, atmSwitchConfigPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
atmSwitchConfigPageWidget.setWindowTitle(_translate("atmSwitchConfigPageWidget", "ATM Switch"))
|
||||
atmSwitchConfigPageWidget.setWhatsThis(_translate("atmSwitchConfigPageWidget", "<html><head/><body><p>This is a simple ATM switch. Only IOS c7200 routers with at least a configured PA-A1 adapter can connect to it.</p></body></html>"))
|
||||
self.uiGeneralGroupBox.setTitle(_translate("atmSwitchConfigPageWidget", "General"))
|
||||
self.uiNameLabel.setText(_translate("atmSwitchConfigPageWidget", "Name:"))
|
||||
self.uiVPICheckBox.setText(_translate("atmSwitchConfigPageWidget", "Use VPI only (VP tunnel)"))
|
||||
@@ -186,3 +185,4 @@ class Ui_atmSwitchConfigPageWidget(object):
|
||||
self.uiDestinationPortLabel.setText(_translate("atmSwitchConfigPageWidget", "Port:"))
|
||||
self.uiDestinationVPILabel.setText(_translate("atmSwitchConfigPageWidget", "VPI:"))
|
||||
self.uiDestinationVCILabel.setText(_translate("atmSwitchConfigPageWidget", "VCI:"))
|
||||
|
||||
92
gns3/modules/builtin/ui/builtin_preferences_page.ui
Normal file
92
gns3/modules/builtin/ui/builtin_preferences_page.ui
Normal file
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>BuiltinPreferencesPageWidget</class>
|
||||
<widget class="QWidget" name="BuiltinPreferencesPageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>330</width>
|
||||
<height>200</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Built-in</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>Local settings</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<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>
|
||||
47
gns3/modules/builtin/ui/builtin_preferences_page_ui.py
Normal file
47
gns3/modules/builtin/ui/builtin_preferences_page_ui.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/builtin_preferences_page.ui'
|
||||
#
|
||||
# Created: Wed Dec 7 21:40: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_BuiltinPreferencesPageWidget(object):
|
||||
def setupUi(self, BuiltinPreferencesPageWidget):
|
||||
BuiltinPreferencesPageWidget.setObjectName("BuiltinPreferencesPageWidget")
|
||||
BuiltinPreferencesPageWidget.resize(330, 200)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(BuiltinPreferencesPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(BuiltinPreferencesPageWidget)
|
||||
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")
|
||||
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(BuiltinPreferencesPageWidget)
|
||||
self.uiRestoreDefaultsPushButton.setObjectName("uiRestoreDefaultsPushButton")
|
||||
self.horizontalLayout_2.addWidget(self.uiRestoreDefaultsPushButton)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout_2)
|
||||
|
||||
self.retranslateUi(BuiltinPreferencesPageWidget)
|
||||
self.uiTabWidget.setCurrentIndex(0)
|
||||
QtCore.QMetaObject.connectSlotsByName(BuiltinPreferencesPageWidget)
|
||||
|
||||
def retranslateUi(self, BuiltinPreferencesPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
BuiltinPreferencesPageWidget.setWindowTitle(_translate("BuiltinPreferencesPageWidget", "Built-in"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("BuiltinPreferencesPageWidget", "Local settings"))
|
||||
self.uiRestoreDefaultsPushButton.setText(_translate("BuiltinPreferencesPageWidget", "Restore defaults"))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user