mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-06-07 19:16:26 +03:00
Compare commits
838 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ebe3435c4 | ||
|
|
a1cd34d7c4 | ||
|
|
1e4a44135c | ||
|
|
a407f1ec90 | ||
|
|
faab113384 | ||
|
|
c158b7fc46 | ||
|
|
16de9e830f | ||
|
|
25c625c0bb | ||
|
|
bf42d1a355 | ||
|
|
1c0f3493ee | ||
|
|
c3c1f87c5e | ||
|
|
6b80914385 | ||
|
|
a114d9ace7 | ||
|
|
4dca4d057a | ||
|
|
17af21e29a | ||
|
|
7fbce0266d | ||
|
|
d5cdbdbf90 | ||
|
|
e5a790f4b2 | ||
|
|
f3769df0d6 | ||
|
|
a21db74941 | ||
|
|
d1e1f6dfb6 | ||
|
|
cc45c9631a | ||
|
|
d16a52e389 | ||
|
|
ee2bea7cdd | ||
|
|
7cbc25cbbf | ||
|
|
7237cf1b88 | ||
|
|
965923900b | ||
|
|
a5a3a4e8cc | ||
|
|
d898b30d84 | ||
|
|
e86ced750e | ||
|
|
e15b717cb0 | ||
|
|
d8bd33f0e7 | ||
|
|
bc2fbe33ef | ||
|
|
b99b26f463 | ||
|
|
5b7606793f | ||
|
|
b8b5e8739e | ||
|
|
0126c30887 | ||
|
|
a89086ff60 | ||
|
|
9ca35c56de | ||
|
|
3ddccf40a8 | ||
|
|
1398ef323a | ||
|
|
d52c4d839d | ||
|
|
6989ee2c8b | ||
|
|
5c182e95ca | ||
|
|
bcb7a8e57b | ||
|
|
dba75e844e | ||
|
|
6733739fa5 | ||
|
|
f5de62aa05 | ||
|
|
4087d35f6a | ||
|
|
f6a1af46a0 | ||
|
|
6cef2fed5a | ||
|
|
e16c8db311 | ||
|
|
61c95a93ca | ||
|
|
0df36dab30 | ||
|
|
9a5da633e0 | ||
|
|
02fed964f2 | ||
|
|
84ba56ae74 | ||
|
|
e8c4758cb7 | ||
|
|
d8dc31965f | ||
|
|
9af48ba9a3 | ||
|
|
67d8e317e0 | ||
|
|
64392780c5 | ||
|
|
81bb159d45 | ||
|
|
85352af9bd | ||
|
|
65cfdf6b33 | ||
|
|
5d45dbebf6 | ||
|
|
0a7b6d81d8 | ||
|
|
588dcadd3a | ||
|
|
f24c93a55f | ||
|
|
26e5c80406 | ||
|
|
eb502232a2 | ||
|
|
25e17d718c | ||
|
|
89108070df | ||
|
|
f4df3ff9c0 | ||
|
|
90f80b9804 | ||
|
|
3e86044132 | ||
|
|
78d805cebc | ||
|
|
289f754108 | ||
|
|
1a0c1f826b | ||
|
|
9837d661a5 | ||
|
|
ff60776769 | ||
|
|
7ac442631a | ||
|
|
8da2ff3a97 | ||
|
|
a04d9784f2 | ||
|
|
623aa4a2de | ||
|
|
ef3c2afab9 | ||
|
|
73e59d92ca | ||
|
|
8f3d5bf038 | ||
|
|
d638c6e0d7 | ||
|
|
c5688cacf9 | ||
|
|
b34bcd6369 | ||
|
|
5e0fc3675f | ||
|
|
e75a21e2ed | ||
|
|
aeee44e597 | ||
|
|
3cebee64ad | ||
|
|
fd1619cfd3 | ||
|
|
4573d2aed8 | ||
|
|
7f29c497cc | ||
|
|
6da42b5013 | ||
|
|
430366947f | ||
|
|
3870f8ecdc | ||
|
|
f68626e4cc | ||
|
|
6b4126b688 | ||
|
|
2ef9890dc1 | ||
|
|
100e3dbf27 | ||
|
|
b85db6e24f | ||
|
|
05d2077b16 | ||
|
|
357d039434 | ||
|
|
0288384c85 | ||
|
|
84347848e9 | ||
|
|
24e5ef885c | ||
|
|
0d8255ecaf | ||
|
|
679548e4ad | ||
|
|
e5384af45d | ||
|
|
ae68d4d84b | ||
|
|
2ab81816ef | ||
|
|
4c4241183a | ||
|
|
46406d1e7b | ||
|
|
a92573394f | ||
|
|
6baf628997 | ||
|
|
8f9190e094 | ||
|
|
7e14e734b2 | ||
|
|
76fc2f07ce | ||
|
|
eee066d5f3 | ||
|
|
9dead47a37 | ||
|
|
a22bd8e9be | ||
|
|
a4b897d458 | ||
|
|
e784f21c0f | ||
|
|
3a000cdc60 | ||
|
|
405f3b3382 | ||
|
|
7397f76566 | ||
|
|
178cb35d6a | ||
|
|
012e5d331d | ||
|
|
bd81d36635 | ||
|
|
234eab57c8 | ||
|
|
e4b19714f4 | ||
|
|
7bfba1015b | ||
|
|
498ba2d2b1 | ||
|
|
f3756b8401 | ||
|
|
68f6d37aab | ||
|
|
6ce35fa5b5 | ||
|
|
e376753859 | ||
|
|
7b03c3eae7 | ||
|
|
902ba42be1 | ||
|
|
73fe898eda | ||
|
|
1ff488d39a | ||
|
|
1622a79383 | ||
|
|
1564c63a42 | ||
|
|
29f651aaea | ||
|
|
9ee0222339 | ||
|
|
6e1384c985 | ||
|
|
20190c5816 | ||
|
|
cab3412ddc | ||
|
|
6d74ce4070 | ||
|
|
159d21af9a | ||
|
|
713feff11f | ||
|
|
64c5ca712e | ||
|
|
1572a6f67f | ||
|
|
fcee5c6916 | ||
|
|
3d21f9a997 | ||
|
|
d93ad5e9d5 | ||
|
|
13739281da | ||
|
|
1f281a807b | ||
|
|
2ca250d2c2 | ||
|
|
b82b031168 | ||
|
|
c48048f013 | ||
|
|
9aaca9955a | ||
|
|
a0e6a82ea2 | ||
|
|
9a3e320e95 | ||
|
|
c3fce51493 | ||
|
|
116cf55758 | ||
|
|
269c6bd0cd | ||
|
|
31aa612a62 | ||
|
|
55f396694f | ||
|
|
b51fd9c92f | ||
|
|
5857d3709b | ||
|
|
0c00e1309c | ||
|
|
06dbf9f7d8 | ||
|
|
ef651d9e9a | ||
|
|
65dd3a23c6 | ||
|
|
85f697d47b | ||
|
|
0988fdca09 | ||
|
|
eba3d5751e | ||
|
|
93e140ae05 | ||
|
|
b81a531a7b | ||
|
|
089b4108cc | ||
|
|
b89f70370a | ||
|
|
81b4ded30a | ||
|
|
b658eea427 | ||
|
|
da225ffdf9 | ||
|
|
b7fb6e6b13 | ||
|
|
078cef064b | ||
|
|
bec1c41f75 | ||
|
|
64f3516153 | ||
|
|
558e8ad8ce | ||
|
|
5f7408809e | ||
|
|
8359da3c76 | ||
|
|
c613e20971 | ||
|
|
34ab6c2e1b | ||
|
|
5382a8a397 | ||
|
|
507f104ae5 | ||
|
|
ada2f647a0 | ||
|
|
347b76d39e | ||
|
|
3749819016 | ||
|
|
4d4871d165 | ||
|
|
59f6a22e81 | ||
|
|
0982338e2c | ||
|
|
20efde749c | ||
|
|
23c3576256 | ||
|
|
1dbf30c6cb | ||
|
|
2081689c12 | ||
|
|
983c55928e | ||
|
|
de625d6cfc | ||
|
|
523d791cac | ||
|
|
270518f294 | ||
|
|
7ab8d679f7 | ||
|
|
f518464eb2 | ||
|
|
321685acb8 | ||
|
|
747ca36a5a | ||
|
|
485844f8de | ||
|
|
b7c0a8c368 | ||
|
|
678c42f941 | ||
|
|
1eaf6c97e0 | ||
|
|
f92282f823 | ||
|
|
9ef90210d8 | ||
|
|
f280ea4c68 | ||
|
|
0067634990 | ||
|
|
4799fc7c93 | ||
|
|
829154fb1c | ||
|
|
7e0caba4b0 | ||
|
|
3c3890ff21 | ||
|
|
65411d1742 | ||
|
|
146a6a5af2 | ||
|
|
fc72140402 | ||
|
|
1b13d83e38 | ||
|
|
e3f073d74b | ||
|
|
8f6e84f8a9 | ||
|
|
c594c3d8a7 | ||
|
|
a550527e6d | ||
|
|
0ce377f321 | ||
|
|
ab37a6237c | ||
|
|
ecc57133c6 | ||
|
|
fc6c2c0304 | ||
|
|
92c731a9c9 | ||
|
|
bc433e5281 | ||
|
|
21eb0b0f03 | ||
|
|
3f70d0238f | ||
|
|
8fd3f67378 | ||
|
|
4e9cb90468 | ||
|
|
d38e62fa38 | ||
|
|
4b7df545aa | ||
|
|
6af5f5f3fb | ||
|
|
b9318dfe6a | ||
|
|
6ffc2c807b | ||
|
|
d177ea44bd | ||
|
|
39f3b22817 | ||
|
|
d157295550 | ||
|
|
d7b9465850 | ||
|
|
0766dac62b | ||
|
|
4f81dde2fd | ||
|
|
4a716000ff | ||
|
|
d8b0e9234e | ||
|
|
bf0af2a929 | ||
|
|
a05e47a4d2 | ||
|
|
996c5c927c | ||
|
|
dab7569575 | ||
|
|
2a0e8a3b4f | ||
|
|
872e7199e4 | ||
|
|
67b57a8d78 | ||
|
|
ba3c1e6969 | ||
|
|
1879172505 | ||
|
|
22fe51fe5a | ||
|
|
b5ac40896f | ||
|
|
2d6b53245b | ||
|
|
eee377c4fc | ||
|
|
c34b82c255 | ||
|
|
c750ce8d80 | ||
|
|
2d2e682540 | ||
|
|
e06cf5b9a1 | ||
|
|
d25258c47f | ||
|
|
1e40a36a48 | ||
|
|
d5059d22fc | ||
|
|
bae61bdcaa | ||
|
|
eb44226ee4 | ||
|
|
8ee251cbb2 | ||
|
|
a84e081a75 | ||
|
|
d92db4e99d | ||
|
|
873e04ed9d | ||
|
|
c0c41b99eb | ||
|
|
12b694047a | ||
|
|
59651a3fe5 | ||
|
|
02ad5d2f3a | ||
|
|
a31b98f781 | ||
|
|
e9a674c4e9 | ||
|
|
4b383e2b06 | ||
|
|
6d2ca353a3 | ||
|
|
d8b5caf679 | ||
|
|
d61088e3a7 | ||
|
|
a3f0569663 | ||
|
|
31e82bb410 | ||
|
|
cab3baf2c6 | ||
|
|
55b80cc9cb | ||
|
|
aec6c37016 | ||
|
|
574da9c80a | ||
|
|
117f6ec3b1 | ||
|
|
574d6b3792 | ||
|
|
8321883199 | ||
|
|
9608614aa9 | ||
|
|
98e4aefa65 | ||
|
|
67d816baa3 | ||
|
|
13a8bd4500 | ||
|
|
802b80b764 | ||
|
|
fe5f80382a | ||
|
|
a4ed59200d | ||
|
|
59292ff6cb | ||
|
|
7810d19f4d | ||
|
|
0788ce569f | ||
|
|
4b0379892d | ||
|
|
3ca05c7427 | ||
|
|
6a16bcedc0 | ||
|
|
8f8994e7df | ||
|
|
4e172fc7e3 | ||
|
|
56ebfc7fd0 | ||
|
|
8ed8a2c115 | ||
|
|
073665a75d | ||
|
|
4ccc67aa46 | ||
|
|
6e2632e91f | ||
|
|
38ddcde902 | ||
|
|
436563afcb | ||
|
|
7eaab3e38b | ||
|
|
0927a2a8c9 | ||
|
|
87e6159ff6 | ||
|
|
effdcf5e24 | ||
|
|
021cdd2e65 | ||
|
|
9b559d43be | ||
|
|
ad7d06ef21 | ||
|
|
b88bf71be9 | ||
|
|
3b019edc82 | ||
|
|
f3504809ed | ||
|
|
23735f35ad | ||
|
|
3adc46fbe2 | ||
|
|
363c4a9966 | ||
|
|
7082c75511 | ||
|
|
8a303e4563 | ||
|
|
842ad8ae26 | ||
|
|
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 | ||
|
|
466c349642 | ||
|
|
1356fd9c69 | ||
|
|
2d1c9444c5 | ||
|
|
22d7815d8e | ||
|
|
53487d5937 | ||
|
|
a2059d3e7c | ||
|
|
ab729d8f67 | ||
|
|
eb5c10de3d | ||
|
|
c4cc819d50 | ||
|
|
0796d9aae5 | ||
|
|
c8148c1877 | ||
|
|
1b6d534b8e | ||
|
|
d6021afa0f | ||
|
|
9018bdac07 | ||
|
|
a48b898d92 | ||
|
|
949c1bbe37 | ||
|
|
0089edecc0 | ||
|
|
91da1d492b | ||
|
|
0242f53c17 | ||
|
|
bab7a1016f | ||
|
|
2ec91e1ef7 | ||
|
|
38af30ec15 | ||
|
|
3f8a0cb527 | ||
|
|
fd1a86df03 | ||
|
|
c954a16ead | ||
|
|
9418b997eb | ||
|
|
9682cad4f4 | ||
|
|
60ee9e1374 | ||
|
|
dbd5b9366a | ||
|
|
f63314d4d0 | ||
|
|
a9b5b9eda2 | ||
|
|
96eaec0100 | ||
|
|
5d554976e2 | ||
|
|
d1d8390b73 | ||
|
|
5f3f6462c2 | ||
|
|
821809bf6f | ||
|
|
a712fab7a4 | ||
|
|
4ab21ea9f9 | ||
|
|
ce12eb86e8 | ||
|
|
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 | ||
|
|
d4ffbd9f97 | ||
|
|
4c01a465ac | ||
|
|
5948e5c4cb | ||
|
|
791aa27158 | ||
|
|
0b8ab56ffc | ||
|
|
12dcfda756 | ||
|
|
ef44beac41 | ||
|
|
97a71904f7 | ||
|
|
012bc1e406 | ||
|
|
94f4059d67 | ||
|
|
14ed2546bc | ||
|
|
908258c163 | ||
|
|
05ba772715 | ||
|
|
9ea57f511b | ||
|
|
5aaa2d7280 | ||
|
|
704191ba25 | ||
|
|
be7fc9abe2 | ||
|
|
e0daf1dbb1 | ||
|
|
a8877d4f8a | ||
|
|
497eb19369 | ||
|
|
70049aa877 | ||
|
|
ece7930cb1 | ||
|
|
2db850a3f3 | ||
|
|
c7df589857 | ||
|
|
8bcc92f319 | ||
|
|
dedde63b60 | ||
|
|
ec7cdedb86 | ||
|
|
606fa15a55 | ||
|
|
3eca9b0e54 | ||
|
|
3abaf74580 | ||
|
|
9713748633 | ||
|
|
405e86aff4 | ||
|
|
4da824dc2b | ||
|
|
0ec177c644 | ||
|
|
9ef4f86050 | ||
|
|
96b830817e | ||
|
|
0db8b00fb9 | ||
|
|
005dde6c2b | ||
|
|
260f9b352e | ||
|
|
852b0bc498 | ||
|
|
3a503a5fc0 | ||
|
|
88b408695a | ||
|
|
776b45363b | ||
|
|
adb270b64c | ||
|
|
5329b8fd72 | ||
|
|
d6379e4bb9 | ||
|
|
6ca18d5b29 | ||
|
|
50c008ddec | ||
|
|
4f56f100fa | ||
|
|
a81d1443f9 | ||
|
|
e69089f4cf | ||
|
|
0c052542b3 | ||
|
|
00e402f28c | ||
|
|
0742b282a3 | ||
|
|
2b588aa0bf | ||
|
|
92fb8418ab | ||
|
|
9c7dbc864e | ||
|
|
25aebaa46c | ||
|
|
0e30b3cf5f | ||
|
|
755667c4d5 | ||
|
|
16dbdf70d9 | ||
|
|
806c7479ee | ||
|
|
cc8b84725a | ||
|
|
e01701614e | ||
|
|
efaffac801 | ||
|
|
0c16a5b0d1 | ||
|
|
8f33ad3c70 | ||
|
|
7dca1b404c | ||
|
|
99ff98ff47 | ||
|
|
083d6e1298 | ||
|
|
71716c451e | ||
|
|
3c4a244b75 | ||
|
|
85892a3bd6 | ||
|
|
1d3d721cf3 | ||
|
|
37a72af75f | ||
|
|
c93713b9e7 | ||
|
|
77b3118cbc | ||
|
|
bc0cbfd040 | ||
|
|
0cfc66b4d4 | ||
|
|
1b9b1cbe3c | ||
|
|
407187c826 | ||
|
|
a6eb1e65ac | ||
|
|
efbb19a862 | ||
|
|
2a94816b58 | ||
|
|
aab307a519 | ||
|
|
1279e16484 | ||
|
|
c2e20f9bd6 | ||
|
|
c58366e9cb | ||
|
|
068ebcdea0 | ||
|
|
51f2b4bfa8 | ||
|
|
168e4ab86e | ||
|
|
4acdaf6b5a | ||
|
|
25313cbcde | ||
|
|
7ae18ff82a | ||
|
|
c694173f9d | ||
|
|
0de0eb12eb | ||
|
|
b58b92c9f0 | ||
|
|
7d0fe52600 | ||
|
|
ba1d3b2423 | ||
|
|
cae6afe85d | ||
|
|
5865ac267b | ||
|
|
162993839c | ||
|
|
4359b490cc | ||
|
|
e4a8e67229 | ||
|
|
3ddb2e70d4 | ||
|
|
05966a9119 | ||
|
|
8ea24e9920 | ||
|
|
6605270e64 | ||
|
|
09e2bfeed0 | ||
|
|
47f34fd5af | ||
|
|
b24733466d | ||
|
|
c31be48f20 | ||
|
|
a9ed27f42c | ||
|
|
89321a6cad | ||
|
|
1440caa532 | ||
|
|
513eb21940 | ||
|
|
de348d39da | ||
|
|
0a3697962b | ||
|
|
e0edcf3d23 | ||
|
|
6690ba7108 | ||
|
|
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 | ||
|
|
b81a02fbb4 | ||
|
|
c36695c59a |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -60,3 +60,6 @@ keys
|
||||
updates
|
||||
.cache
|
||||
__pycache__
|
||||
|
||||
# Virtualenv
|
||||
env
|
||||
|
||||
26
.travis.yml
26
.travis.yml
@@ -1,19 +1,19 @@
|
||||
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
|
||||
before_deploy:
|
||||
- sudo pip install twine
|
||||
- sudo pip install urllib3[secure]
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: noplay
|
||||
password:
|
||||
secure: FofcqlJjgqf2jaDaXpLHeigVoexbrOz3WwnDuiJpwJxeFUlPY8s2cQs/Bm+dzxzZaOaGiVE0A83v/Xa10yD5tflThHt4sqYJK3iQCinA7wgeAlDimB4xrWUNplfNJZ/Eod5Ssa++E02W+3i29PxpXY//mjCY7qDxaoxul1gnFJY=
|
||||
on:
|
||||
tags: true
|
||||
repo: GNS3/gns3-gui
|
||||
|
||||
555
CHANGELOG
555
CHANGELOG
@@ -1,5 +1,541 @@
|
||||
# Change Log
|
||||
|
||||
## 2.1.3 19/01/2018
|
||||
|
||||
* Change messages when there are different client and server versions. Fixes #2391.
|
||||
* Fix "Transport selection via DSN is deprecated" message. Sync is configured with HTTPTransport.
|
||||
* Refresh CPU/RAM info every 1 second. Ref #2262.
|
||||
* Only check for AVG on Windows
|
||||
* Improve the search for VBoxManage.
|
||||
* Allow telnet console to node with name containing double quotes. Fixes #2371.
|
||||
|
||||
## 2.1.2 08/01/2018
|
||||
|
||||
* Update VMware promotion in setup wizard.
|
||||
* Confirm exit. Fixes #2359.
|
||||
* Fix with .exe build
|
||||
|
||||
## 2.1.1 22/12/2017
|
||||
|
||||
* Fix dragging appliance into topology from nodes window, fixes: #2363
|
||||
* Fix Appliances in Docked mode, fixes: #2362
|
||||
* Create local variable in order to debug issue in the next occurrence, #2366
|
||||
* Fix ParseError: not well-formed (invalid token), #2364
|
||||
* Fix local variable 'vm' referenced before assignment #2365
|
||||
* Fix: 'NodesDockWidget' object has no attribute 'uiNodesView', #2362
|
||||
* Tentative fix for packet capture not working correctly when remote main server is configured. Ref #2111.
|
||||
* Log Qt messages with log.debug() instead of log.info().
|
||||
* Fix auto idle-pc from preferences. Fixes #2344.
|
||||
* Snapshoting project without timeout but with button. Ref. #2314
|
||||
* Improve validation for idle-pc.
|
||||
* Activate faulthandler.
|
||||
* Add PATH to OS X console commands
|
||||
* Use raw triple quotes in large console settings This eliminates one level of quoting
|
||||
* Fix issue in node summary when console is not supported by a node.
|
||||
* Remove unused symbols. Fixes #2320.
|
||||
* Show console information in Topology Summary Dock. Fixes #2258.
|
||||
* New option: require KVM. If false, Qemu VMs will not be prevented to run without KVM.
|
||||
* Implement variable replacement for Qemu VM options.
|
||||
* Show on what server a node is installed in the servers summary pane. Fixes #2279.
|
||||
* Add more info when cannot remove capture file after stopping packet capture in a remote project. Ref #1223.
|
||||
* Do not overwrites the disk images when copied to default directory. Fixes #2326.
|
||||
* Only replace quoted telnet for macOS Telnet commands. Ref #2328.
|
||||
* Support Telnet path containing spaces. Ref #2328.
|
||||
* Fix problem when embedded telnet client path contains a space on macOS. Ref #2328.
|
||||
* Do not launch console for builtin nodes when using the "Console to all nodes" button. Fixes #2309.
|
||||
* Update frame_relay_switch_configuration_page_ui.py
|
||||
* Turn off timeout for node creation
|
||||
|
||||
## 2.1.0 09/11/2017
|
||||
|
||||
* Update dynamips binary on OSX
|
||||
|
||||
## 2.1.0rc4 07/11/2017
|
||||
|
||||
* Accurate upload progress dialogs for large files
|
||||
* Disable direct file upload on default
|
||||
* Add registry version 5
|
||||
* Direct file upload enabled on default
|
||||
* Progress Dialog: don't count finished queries done in background
|
||||
* Add debug messages to file upload
|
||||
* Image Upload Manager for uploading
|
||||
* Fix race condition on NodesDockWidget, fixes: #2304
|
||||
* Do not write an error message when importing non existing config from a directory. Fixes #2296.
|
||||
* Fix bug when replacing Telnet path on OSX. Ref #2274.
|
||||
* Back to development on 2.1.0rc3
|
||||
|
||||
## 2.1.0rc3 19/10/2017
|
||||
|
||||
* Add debug when using Telnet path on OSX. Ref #2274.
|
||||
* Force to use the telnet client embedded in DMG. Ref #2274.
|
||||
* Upload directly to compute - experimental feature
|
||||
* Filter additional QXcbConnection log messages
|
||||
* Do not add missing file extension for screenshot file names on Mac. Fixes #2287.
|
||||
* Log Qt messages as info instead of error. Ref #2281.
|
||||
|
||||
## 2.1.0rc2 04/10/2017
|
||||
|
||||
* Only show "can't get settings from controller" message in debug mode.
|
||||
* Remove explicit Telnet path on OS X. Ref #2274
|
||||
* Disable WebSocket notification for lower PyQT version than 5.6. Fixes #2272
|
||||
* Increase timeout to 5 minutes when creating and restoring a snapshot.
|
||||
* Add more information when a request timeouts. Ref #2277.
|
||||
* Do not show the progress dialog when moving a node. Ref #2275.
|
||||
* Increase timer before showing a progress dialog from 250ms to 500ms. Ref #2275.
|
||||
* Use embedded Telnet client on OS X. Ref #2274.
|
||||
* Fix small bug when adding an appliance template and the name already exists.
|
||||
* Use RAW sockets by default on Linux for VMware VM connections.
|
||||
* Increase timeout to get compute servers from controller. Ref #2269.
|
||||
* Fix "Node doesn't exist" after deletion, but still on the canvas. Fixes #2266.
|
||||
* Make sure the warning button icon appears in cloud properties dialog on Windows. Fixes #2245.
|
||||
* Fix bug when cancelling the importation of a configuration file. Fixes #2260.
|
||||
|
||||
## 2.1.0rc1 13/09/2017
|
||||
|
||||
* Fix missing spice console option in appliance template schema. Fixes #2255.
|
||||
|
||||
## 2.1.0b2 05/09/2017
|
||||
|
||||
* Fix resources dependencies for cloud configuration page (Fixes: #2251)
|
||||
* Disabled possibility of moving items under zero layer (Fixes #2220)
|
||||
* dialog-warning.svg fallback for themed icon (Ref. #2245)
|
||||
* Change width of packet filters dialog (Fixes #2244)
|
||||
* Fix high CPU usage when using packet filters. Fixes #2240.
|
||||
* Toggle Node menu item (Fixes #2227)
|
||||
* Fixes multiselection styles change crash on LineItem (#2216)
|
||||
* Fixes loading symbols for QEMU at Edit Page (#2214)
|
||||
* Fixes exception when right click on Dynamips router in the device dock (#2211)
|
||||
* Update frame_relay_switch_configuration_page.ui
|
||||
|
||||
## 2.1.0b1 04/08/2017
|
||||
|
||||
* Info added to the Nat node
|
||||
* Add missing popup information in cloud and docker node
|
||||
* Handle invalid json in websockets
|
||||
* Avoid invalid bad request error when receiving partial answer
|
||||
* Catch parse error for broken SVG
|
||||
* Filter QXcbConnection log messages
|
||||
* Catch class 'PyQt5.QtNetwork.QNetworkReply'> returned a result with an error set
|
||||
* Fix KeyError: 'overlay_notifications'
|
||||
|
||||
## 2.1.0a2 31/07/2017
|
||||
|
||||
* Fix permission error when importing a project on a remote server
|
||||
* Fix RecursionError
|
||||
* Fix 'NodesDockWidget' object has no attribute 'loadPath'
|
||||
* Fix 'MainWindow' object has no attribute '_settings
|
||||
* Fix object has no attribute 'warning_signal'
|
||||
* Fix timeout issues when using an appliance
|
||||
* Make sure ubridge path is not a directory
|
||||
|
||||
## 2.1.0a1 24/07/2017
|
||||
|
||||
* Packet filtering
|
||||
* Suspend a link
|
||||
* Duplicate a node
|
||||
* Move config to central server
|
||||
* Appliance templates on server
|
||||
|
||||
## 2.0.3 13/06/2017
|
||||
|
||||
* Display error when we can't export files
|
||||
* Fix auth header not sent is some conditions
|
||||
* If we have auth issue at server startup continue to get better error
|
||||
* Do not override IOU configuration file when you change the image
|
||||
* Fix some PNG loading issues on Windows
|
||||
* Handle label with missing elements
|
||||
* Support floating value for font size
|
||||
* Handle partial json in a response
|
||||
* Add Dominik as a new team member
|
||||
|
||||
## 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
|
||||
@@ -33,6 +569,25 @@
|
||||
* 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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Run tests inside a container
|
||||
FROM ubuntu:vivid
|
||||
FROM ubuntu:17.10
|
||||
|
||||
MAINTAINER GNS3 Team
|
||||
|
||||
#ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --force-yes python3.4 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3.4-dev xvfb
|
||||
RUN apt-get install -y --force-yes python3.6 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3.6-dev xvfb
|
||||
RUN apt-get clean
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@ ADD . /src
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
CMD xvfb-run python3.4 -m pytest -vv
|
||||
CMD xvfb-run python3.6 -m pytest -vv
|
||||
|
||||
@@ -40,3 +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
|
||||
|
||||
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
|
||||
|
||||
119
gns3/appliance_manager.py
Normal file
119
gns3/appliance_manager.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/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 .controller import Controller
|
||||
from .utils.server_select import server_select
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApplianceManager(QtCore.QObject):
|
||||
|
||||
appliances_changed_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._appliance_templates = []
|
||||
self._appliances = []
|
||||
self._controller = Controller.instance()
|
||||
self._controller.connected_signal.connect(self.refresh)
|
||||
self._controller.disconnected_signal.connect(self._controllerDisconnectedSlot)
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
if self._controller.connected():
|
||||
self._controller.get("/appliances/templates", self._listApplianceTemplateCallback)
|
||||
self._controller.get("/appliances", self._listAppliancesCallback)
|
||||
|
||||
def _controllerDisconnectedSlot(self):
|
||||
self._appliance_templates = []
|
||||
self._appliances = []
|
||||
self.appliances_changed_signal.emit()
|
||||
|
||||
def appliance_templates(self):
|
||||
return self._appliance_templates
|
||||
|
||||
def appliances(self):
|
||||
return self._appliances
|
||||
|
||||
def getAppliance(self, appliance_id):
|
||||
"""
|
||||
Look for an appliance by appliance ID
|
||||
"""
|
||||
for appliance in self._appliances:
|
||||
if appliance["appliance_id"] == appliance_id:
|
||||
return appliance
|
||||
return None
|
||||
|
||||
def _listAppliancesCallback(self, result, error=False, **kwargs):
|
||||
if error is True:
|
||||
log.error("Error while getting appliances list: {}".format(result["message"]))
|
||||
return
|
||||
self._appliances = result
|
||||
self.appliances_changed_signal.emit()
|
||||
|
||||
def _listApplianceTemplateCallback(self, result, error=False, **kwargs):
|
||||
if error is True:
|
||||
log.error("Error while getting appliance templates list: {}".format(result["message"]))
|
||||
return
|
||||
self._appliance_templates = result
|
||||
self.appliances_changed_signal.emit()
|
||||
|
||||
def createNodeFromApplianceId(self, project, appliance_id, x, y):
|
||||
for appliance in self._appliances:
|
||||
if appliance["appliance_id"] == appliance_id:
|
||||
break
|
||||
|
||||
project_id = project.id()
|
||||
|
||||
if appliance.get("compute_id") is None:
|
||||
from .main_window import MainWindow
|
||||
server = server_select(MainWindow.instance(), node_type=appliance["node_type"])
|
||||
if server is None:
|
||||
return False
|
||||
self._controller.post("/projects/" + project_id + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
|
||||
"compute_id": server.id(),
|
||||
"x": int(x),
|
||||
"y": int(y)
|
||||
},
|
||||
timeout=None)
|
||||
else:
|
||||
self._controller.post("/projects/" + project_id + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
|
||||
"x": int(x),
|
||||
"y": int(y)
|
||||
},
|
||||
timeout=None)
|
||||
return True
|
||||
|
||||
def _createNodeFromApplianceCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while creating node: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of ApplianceManager.
|
||||
:returns: instance of ApplianceManager
|
||||
"""
|
||||
|
||||
if not hasattr(ApplianceManager, '_instance') or ApplianceManager._instance is None:
|
||||
ApplianceManager._instance = ApplianceManager()
|
||||
return ApplianceManager._instance
|
||||
@@ -29,14 +29,20 @@ 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
|
||||
self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
|
||||
self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
|
||||
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)
|
||||
|
||||
|
||||
@@ -19,8 +19,12 @@
|
||||
Base class for node classes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from .qt import QtCore
|
||||
from .ports.port import Port
|
||||
from .utils.normalize_filename import normalize_filename
|
||||
|
||||
|
||||
import logging
|
||||
@@ -46,7 +50,6 @@ class BaseNode(QtCore.QObject):
|
||||
loaded_signal = QtCore.Signal()
|
||||
deleted_signal = QtCore.Signal()
|
||||
error_signal = QtCore.Signal(int, str)
|
||||
warning_signal = QtCore.Signal(int, str)
|
||||
server_error_signal = QtCore.Signal(int, str)
|
||||
|
||||
_instance_count = 1
|
||||
@@ -90,7 +93,10 @@ class BaseNode(QtCore.QObject):
|
||||
self._links.add(link)
|
||||
|
||||
def deleteLink(self, link):
|
||||
self._links.remove(link)
|
||||
try:
|
||||
self._links.remove(link)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
@@ -174,19 +180,16 @@ class BaseNode(QtCore.QObject):
|
||||
# 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):
|
||||
"""
|
||||
@@ -314,7 +317,6 @@ class BaseNode(QtCore.QObject):
|
||||
|
||||
self._project.get(path, callback, context=context, **kwargs)
|
||||
|
||||
|
||||
def controllerHttpDelete(self, path, callback, context={}, **kwargs):
|
||||
"""
|
||||
Delete on current server / project
|
||||
@@ -325,3 +327,73 @@ class BaseNode(QtCore.QObject):
|
||||
"""
|
||||
|
||||
self._project.delete(path, callback, context=context, **kwargs)
|
||||
|
||||
def exportConfigToDirectory(self, directory):
|
||||
"""
|
||||
Exports the initial-config to a directory.
|
||||
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
if not hasattr(self, "configFiles"):
|
||||
return
|
||||
for file in self.configFiles():
|
||||
self.controllerHttpGet("/nodes/{node_id}/files/{file}".format(node_id=self._node_id, file=file),
|
||||
self._exportConfigToDirectoryCallback,
|
||||
context={"directory": directory, "file": file},
|
||||
raw=True)
|
||||
|
||||
def _exportConfigToDirectoryCallback(self, result, error=False, raw_body=None, context={}, **kwargs):
|
||||
"""
|
||||
Callback for exportConfigToDirectory.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
# The file could be missing if you have not private config for
|
||||
# exemple
|
||||
return
|
||||
export_directory = context["directory"]
|
||||
|
||||
filename = normalize_filename(self.name()) + "_{}".format(context["file"].replace("/", "_")) # We can have / in the case of Docker
|
||||
config_path = os.path.join(export_directory, filename)
|
||||
try:
|
||||
with open(config_path, "wb") as f:
|
||||
log.debug("saving {} config to {}".format(self.name(), config_path))
|
||||
f.write(raw_body)
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not export config to {}: {}".format(config_path, e))
|
||||
|
||||
def importConfigFromDirectory(self, directory):
|
||||
"""
|
||||
Imports an initial-config from a directory.
|
||||
|
||||
:param directory: source directory path
|
||||
"""
|
||||
|
||||
if not hasattr(self, "configFiles"):
|
||||
return
|
||||
|
||||
try:
|
||||
contents = os.listdir(directory)
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "Can't list file in {}: {}".format(directory, str(e)))
|
||||
return
|
||||
|
||||
for file in self.configFiles():
|
||||
filename = normalize_filename(self.name()) + "_{}".format(file.replace("/", "_")) # We can have / in the case of Docker
|
||||
if filename in contents:
|
||||
self.controllerHttpPost("/nodes/{node_id}/files/{file}".format(
|
||||
node_id=self._node_id,
|
||||
file=file), self._importConfigCallback,
|
||||
pathlib.Path(os.path.join(directory, filename)))
|
||||
else:
|
||||
log.warning("{}: config file '{}' not found".format(self.name(), filename))
|
||||
|
||||
def _importConfigCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while import config: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
@@ -22,15 +22,16 @@ 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 = None
|
||||
self._protocol = "http"
|
||||
self._host = None
|
||||
self._port = None
|
||||
self._port = 3080
|
||||
self._user = None
|
||||
self._password = None
|
||||
self._cpu_usage_percent = None
|
||||
|
||||
@@ -20,9 +20,13 @@ from .qt import QtCore
|
||||
from .compute import Compute
|
||||
from .controller import Controller
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import logging
|
||||
import urllib
|
||||
import datetime
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -36,6 +40,7 @@ class ComputeManager(QtCore.QObject):
|
||||
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
|
||||
@@ -43,19 +48,30 @@ class ComputeManager(QtCore.QObject):
|
||||
|
||||
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._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 5:
|
||||
if self._refreshingComputes:
|
||||
return
|
||||
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 1:
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
self._controller.get("/computes", self._listComputesCallback, showProgress=True)
|
||||
self._refreshingComputes = True
|
||||
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
|
||||
|
||||
def _controllerConnectedSlot(self):
|
||||
if self._controller.connected():
|
||||
self._controller.get("/computes", self._listComputesCallback)
|
||||
self._refreshingComputes = True
|
||||
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
|
||||
|
||||
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
|
||||
@@ -68,6 +84,7 @@ class ComputeManager(QtCore.QObject):
|
||||
Called when we received data from a compute
|
||||
node.
|
||||
"""
|
||||
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
|
||||
new_node = False
|
||||
@@ -91,11 +108,25 @@ class ComputeManager(QtCore.QObject):
|
||||
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
|
||||
"""
|
||||
return list(self._computes.values())
|
||||
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):
|
||||
"""
|
||||
@@ -115,6 +146,17 @@ class ComputeManager(QtCore.QObject):
|
||||
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
|
||||
@@ -122,6 +164,12 @@ class ComputeManager(QtCore.QObject):
|
||||
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)
|
||||
@@ -153,12 +201,14 @@ class ComputeManager(QtCore.QObject):
|
||||
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():
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
Compute summary view that list all the compute, their status.
|
||||
"""
|
||||
|
||||
import sip
|
||||
|
||||
from .qt import QtGui, QtCore, QtWidgets
|
||||
from .compute_manager import ComputeManager
|
||||
from .topology import Topology
|
||||
from .node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -62,7 +62,7 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
|
||||
text = "{} CPU {}%, RAM {}%".format(text, self._compute.cpuUsagePercent(), self._compute.memoryUsagePercent())
|
||||
|
||||
self.setText(0, text)
|
||||
self.setToolTip(0, text)
|
||||
self.setToolTip(0, text + " on " + self._compute.capabilities().get("platform", ""))
|
||||
|
||||
if self._compute.connected():
|
||||
self._status = "connected"
|
||||
@@ -76,7 +76,23 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
|
||||
else:
|
||||
self._status = "stopped"
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
|
||||
self._parent.sortItems(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
# add nodes belonging to this compute
|
||||
self.takeChildren()
|
||||
nodes = Topology.instance().nodes()
|
||||
for node in nodes:
|
||||
if node.compute().id() == self._compute.id():
|
||||
item = QtWidgets.QTreeWidgetItem()
|
||||
item.setText(0, node.name())
|
||||
if node.status() == Node.started:
|
||||
item.setIcon(0, QtGui.QIcon(':/icons/led_green.svg'))
|
||||
elif node.status() == Node.suspended:
|
||||
item.setIcon(0, QtGui.QIcon(':/icons/led_yellow.svg'))
|
||||
else:
|
||||
item.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
|
||||
self.addChild(item)
|
||||
self.sortChildren(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
class ComputeSummaryView(QtWidgets.QTreeWidget):
|
||||
|
||||
@@ -106,17 +122,26 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
|
||||
"""
|
||||
|
||||
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 removed to the list of computes
|
||||
Called when a compute is updated
|
||||
|
||||
:params url: URL of the compute
|
||||
"""
|
||||
|
||||
if compute_id in self._computes:
|
||||
self._computes[compute_id]._refreshStatusSlot()
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
!
|
||||
service timestamps debug datetime msec
|
||||
service timestamps log datetime msec
|
||||
no service password-encryption
|
||||
!
|
||||
hostname %h
|
||||
!
|
||||
ip cef
|
||||
no ip domain-lookup
|
||||
no ip icmp rate-limit unreachable
|
||||
ip tcp synwait 5
|
||||
no cdp log mismatch duplex
|
||||
!
|
||||
line con 0
|
||||
exec-timeout 0 0
|
||||
logging synchronous
|
||||
privilege level 15
|
||||
no login
|
||||
line aux 0
|
||||
exec-timeout 0 0
|
||||
logging synchronous
|
||||
privilege level 15
|
||||
no login
|
||||
!
|
||||
!
|
||||
end
|
||||
@@ -1,181 +0,0 @@
|
||||
!
|
||||
service timestamps debug datetime msec
|
||||
service timestamps log datetime msec
|
||||
no service password-encryption
|
||||
no service dhcp
|
||||
!
|
||||
hostname %h
|
||||
!
|
||||
ip cef
|
||||
no ip routing
|
||||
no ip domain-lookup
|
||||
no ip icmp rate-limit unreachable
|
||||
ip tcp synwait 5
|
||||
no cdp log mismatch duplex
|
||||
vtp file nvram:vlan.dat
|
||||
!
|
||||
!
|
||||
interface FastEthernet0/0
|
||||
description *** Unused for Layer2 EtherSwitch ***
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface FastEthernet0/1
|
||||
description *** Unused for Layer2 EtherSwitch ***
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface FastEthernet1/0
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/1
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/2
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/3
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/4
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/5
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/6
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/7
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/8
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/9
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/10
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/11
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/12
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/13
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/14
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/15
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface Vlan1
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
!
|
||||
line con 0
|
||||
exec-timeout 0 0
|
||||
logging synchronous
|
||||
privilege level 15
|
||||
no login
|
||||
line aux 0
|
||||
exec-timeout 0 0
|
||||
logging synchronous
|
||||
privilege level 15
|
||||
no login
|
||||
!
|
||||
!
|
||||
banner exec $
|
||||
|
||||
***************************************************************
|
||||
This is a normal Router with a SW module inside (NM-16ESW)
|
||||
It has been preconfigured with hard coded speed and duplex
|
||||
|
||||
To create vlans use the command "vlan database" from exec mode
|
||||
After creating all desired vlans use "exit" to apply the config
|
||||
|
||||
To view existing vlans use the command "show vlan-switch brief"
|
||||
|
||||
Warning: You are using an old IOS image for this router.
|
||||
Please update the IOS to enable the "macro" command!
|
||||
***************************************************************
|
||||
|
||||
$
|
||||
!
|
||||
!Warning: If the IOS is old and doesn't support macro, it will stop the configuration loading from this point!
|
||||
!
|
||||
macro name add_vlan
|
||||
end
|
||||
vlan database
|
||||
vlan $v
|
||||
exit
|
||||
@
|
||||
macro name del_vlan
|
||||
end
|
||||
vlan database
|
||||
no vlan $v
|
||||
exit
|
||||
@
|
||||
!
|
||||
!
|
||||
banner exec $
|
||||
|
||||
***************************************************************
|
||||
This is a normal Router with a Switch module inside (NM-16ESW)
|
||||
It has been pre-configured with hard-coded speed and duplex
|
||||
|
||||
To create vlans use the command "vlan database" in exec mode
|
||||
After creating all desired vlans use "exit" to apply the config
|
||||
|
||||
To view existing vlans use the command "show vlan-switch brief"
|
||||
|
||||
Alias(exec) : vl - "show vlan-switch brief" command
|
||||
Alias(configure): va X - macro to add vlan X
|
||||
Alias(configure): vd X - macro to delete vlan X
|
||||
***************************************************************
|
||||
|
||||
$
|
||||
!
|
||||
alias configure va macro global trace add_vlan $v
|
||||
alias configure vd macro global trace del_vlan $v
|
||||
alias exec vl show vlan-switch brief
|
||||
!
|
||||
!
|
||||
end
|
||||
@@ -1,132 +0,0 @@
|
||||
!
|
||||
service timestamps debug datetime msec
|
||||
service timestamps log datetime msec
|
||||
no service password-encryption
|
||||
!
|
||||
hostname %h
|
||||
!
|
||||
!
|
||||
!
|
||||
logging discriminator EXCESS severity drops 6 msg-body drops EXCESSCOLL
|
||||
logging buffered 50000
|
||||
logging console discriminator EXCESS
|
||||
!
|
||||
no ip icmp rate-limit unreachable
|
||||
!
|
||||
ip cef
|
||||
no ip domain-lookup
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
ip tcp synwait-time 5
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
interface Ethernet0/0
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/1
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/2
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/3
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/0
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/1
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/2
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/3
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet2/0
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet2/1
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet2/2
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet2/3
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet3/0
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet3/1
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet3/2
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet3/3
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Vlan1
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
line con 0
|
||||
exec-timeout 0 0
|
||||
privilege level 15
|
||||
logging synchronous
|
||||
line aux 0
|
||||
exec-timeout 0 0
|
||||
privilege level 15
|
||||
logging synchronous
|
||||
!
|
||||
end
|
||||
@@ -1,108 +0,0 @@
|
||||
!
|
||||
service timestamps debug datetime msec
|
||||
service timestamps log datetime msec
|
||||
no service password-encryption
|
||||
!
|
||||
hostname %h
|
||||
!
|
||||
!
|
||||
!
|
||||
no ip icmp rate-limit unreachable
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
ip cef
|
||||
no ip domain-lookup
|
||||
!
|
||||
!
|
||||
ip tcp synwait-time 5
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
interface Ethernet0/0
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet0/1
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet0/2
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet0/3
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet1/0
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet1/1
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet1/2
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet1/3
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Serial2/0
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial2/1
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial2/2
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial2/3
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial3/0
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial3/1
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial3/2
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial3/3
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
!
|
||||
no cdp log mismatch duplex
|
||||
!
|
||||
line con 0
|
||||
exec-timeout 0 0
|
||||
privilege level 15
|
||||
logging synchronous
|
||||
line aux 0
|
||||
exec-timeout 0 0
|
||||
privilege level 15
|
||||
logging synchronous
|
||||
!
|
||||
end
|
||||
@@ -1 +0,0 @@
|
||||
set pcname %h
|
||||
@@ -21,7 +21,6 @@ Handles commands typed in the GNS3 console.
|
||||
|
||||
import sys
|
||||
import cmd
|
||||
import logging
|
||||
import struct
|
||||
import sip
|
||||
import json
|
||||
@@ -30,6 +29,9 @@ from .node import Node
|
||||
from .qt import QtCore
|
||||
from .version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
@@ -177,6 +179,24 @@ class ConsoleCmd(cmd.Cmd):
|
||||
print("Cannot console to {}".format(device))
|
||||
break
|
||||
|
||||
def do_log(self, args):
|
||||
"""
|
||||
Log a message
|
||||
|
||||
log level message
|
||||
"""
|
||||
|
||||
args = args.split()
|
||||
if len(args) == 0:
|
||||
return
|
||||
level = args.pop(0)
|
||||
if level == "info":
|
||||
log.info(" ".join(args))
|
||||
elif level == "warning":
|
||||
log.warning(" ".join(args))
|
||||
else:
|
||||
log.error(" ".join(args))
|
||||
|
||||
def _start_console(self, node):
|
||||
"""
|
||||
Starts a console application for a specific node.
|
||||
@@ -249,44 +269,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
|
||||
base_node_id = None
|
||||
|
||||
# get the node ID
|
||||
for node in self._topology.nodes():
|
||||
if node.name() == node_name:
|
||||
base_node_id = node.id()
|
||||
break
|
||||
|
||||
if base_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"] == base_node_id:
|
||||
print(json.dumps(node, sort_keys=True, indent=4))
|
||||
break
|
||||
|
||||
def do_show(self, args):
|
||||
"""
|
||||
Show detail information about every device in current lab:
|
||||
@@ -294,15 +276,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() == "":
|
||||
@@ -312,10 +285,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,11 +15,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import platform
|
||||
import sys
|
||||
import sip
|
||||
import struct
|
||||
import inspect
|
||||
import datetime
|
||||
import platform
|
||||
|
||||
from .qt import QtCore, Qt
|
||||
from .topology import Topology
|
||||
@@ -37,24 +38,31 @@ 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("{}\n".format(message), error=True)
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "error")
|
||||
elif level_no >= logging.WARNING:
|
||||
self._console_view.write("{}\n".format(message), warning=True)
|
||||
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("{}\n".format(message))
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "debug")
|
||||
elif level_no >= logging.DEBUG:
|
||||
self._console_view.write("{}\n".format(message))
|
||||
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
|
||||
@@ -89,16 +97,25 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
self._handleLogs()
|
||||
|
||||
if LocalConfig.instance().experimental():
|
||||
log.warning("WARNING: Experimental features enable. You can use some unfinished features and lost data.")
|
||||
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):
|
||||
"""
|
||||
@@ -174,11 +191,9 @@ 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, base_node_id, message):
|
||||
"""
|
||||
@@ -195,8 +210,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
|
||||
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, base_node_id, message):
|
||||
"""
|
||||
@@ -213,8 +227,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
|
||||
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, base_node_id, message):
|
||||
"""
|
||||
@@ -235,8 +248,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
text = "Server error {server}:{name} {message}".format(server=server,
|
||||
name=name,
|
||||
message=message)
|
||||
self.write(text.strip(), error=True)
|
||||
self.write("\n")
|
||||
self.write_message_signal.emit(text.strip(), "error")
|
||||
|
||||
def _run(self):
|
||||
"""
|
||||
|
||||
@@ -19,7 +19,7 @@ import os
|
||||
import hashlib
|
||||
import tempfile
|
||||
|
||||
from .qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from .qt import QtCore, QtGui, QtWidgets, qpartial, qslot
|
||||
from .symbol import Symbol
|
||||
from .local_server_config import LocalServerConfig
|
||||
from .settings import LOCAL_SERVER_SETTINGS
|
||||
@@ -33,14 +33,27 @@ 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._cache_directory = tempfile.TemporaryDirectory()
|
||||
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):
|
||||
"""
|
||||
@@ -49,6 +62,12 @@ class Controller(QtCore.QObject):
|
||||
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
|
||||
@@ -67,34 +86,115 @@ class Controller(QtCore.QObject):
|
||||
"""
|
||||
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 getHttpClient(self):
|
||||
"""
|
||||
:return: Instance of HTTP client to communicate with the server
|
||||
"""
|
||||
return self._http_client
|
||||
|
||||
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.get('/version', self._versionGetSlot)
|
||||
self.disconnected_signal.emit()
|
||||
self._connectingToServer()
|
||||
|
||||
def _versionGetSlot(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Called after the inital version get
|
||||
"""
|
||||
if error:
|
||||
if "message" in result and self._first_error:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Connection", result["message"])
|
||||
# Try to connect again in 1 seconds
|
||||
QtCore.QTimer.singleShot(1000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_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 getEndpoint(self, path, compute_id, *args, **kwargs):
|
||||
"""
|
||||
API post on a specific compute
|
||||
"""
|
||||
compute_id = self.__fix_compute_id(compute_id)
|
||||
path = "/computes/endpoint/{}{}".format(compute_id, path)
|
||||
return self.get(path, *args, **kwargs)
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("PUT", *args, **kwargs)
|
||||
|
||||
@@ -111,6 +211,9 @@ class Controller(QtCore.QObject):
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
return self._http_client.getSynchronous(endpoint, timeout)
|
||||
|
||||
def connectWebSocket(self, path, *args):
|
||||
return self._http_client.connectWebSocket(path)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
@@ -122,12 +225,13 @@ class Controller(QtCore.QObject):
|
||||
Controller._instance = Controller()
|
||||
return Controller._instance
|
||||
|
||||
def getStatic(self, url, callback):
|
||||
def getStatic(self, url, callback, fallback=None):
|
||||
"""
|
||||
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
|
||||
:param fallback: Fallback url in case of error
|
||||
"""
|
||||
|
||||
if not self._http_client:
|
||||
@@ -139,31 +243,83 @@ class Controller(QtCore.QObject):
|
||||
extension = ".svg"
|
||||
else:
|
||||
extension = ".png"
|
||||
path = os.path.join(self._cache_directory.name, m.hexdigest() + extension)
|
||||
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, fallback, ))
|
||||
else:
|
||||
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, callback, url, path))
|
||||
self._static_asset_download_queue[path] = [(callback, fallback, )]
|
||||
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, url, path))
|
||||
|
||||
def _getStaticCallback(self, callback, url, path, result, error=False, raw_body=None, **kwargs):
|
||||
if error:
|
||||
log.error("Error while downloading file: {}".format(url))
|
||||
def _getStaticCallback(self, url, path, result, error=False, raw_body=None, **kwargs):
|
||||
if path not in self._static_asset_download_queue:
|
||||
return
|
||||
with open(path, "wb+") as f:
|
||||
f.write(raw_body)
|
||||
log.debug("File stored {} for {}".format(path, url))
|
||||
callback(path)
|
||||
|
||||
def getSymbolIcon(self, symbol_id, callback):
|
||||
if error:
|
||||
fallback_used = False
|
||||
for callback, fallback in self._static_asset_download_queue[path]:
|
||||
if fallback:
|
||||
self.getStatic(fallback, callback)
|
||||
fallback_used = True
|
||||
if fallback_used:
|
||||
log.error("Error while downloading file: {}".format(url))
|
||||
log.error("Error while downloading file: {}".format(url))
|
||||
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, fallback in self._static_asset_download_queue[path]:
|
||||
callback(path)
|
||||
del self._static_asset_download_queue[path]
|
||||
|
||||
def getSymbolIcon(self, symbol_id, callback, fallback=None):
|
||||
"""
|
||||
Get a QIcon for a symbol from the controller
|
||||
|
||||
:param url: URL without the protocol and host part
|
||||
:param symbol_id: Symbol id
|
||||
:param callback: Callback to call when file is ready
|
||||
:param fallback: Fallback symbol if not found
|
||||
"""
|
||||
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback))
|
||||
if symbol_id is None:
|
||||
self.getStatic(Symbol(fallback).url(), qpartial(self._getIconCallback, callback))
|
||||
else:
|
||||
if fallback:
|
||||
fallback = Symbol(fallback).url()
|
||||
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback), fallback=fallback)
|
||||
|
||||
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
|
||||
|
||||
@@ -41,7 +41,7 @@ if __version_info__[3] != 0:
|
||||
import faulthandler
|
||||
# Display a traceback in case of segfault crash. Usefull when frozen
|
||||
# Not enabled by default for security reason
|
||||
log.info("Enable catching segfault")
|
||||
log.debug("Enable catching segfault")
|
||||
faulthandler.enable()
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "https://468f899afc3c46d99bad2eb474516d2c:415662f2e4c240829d5eaf8f09ca99b4@app.getsentry.com/38506"
|
||||
DSN = "sync+https://9a23499382c64b82bfc707a3716bd0b1:6ad6c81972af4b12837ca4d1895eac8b@sentry.io/38506"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = get_resource("cacert.pem")
|
||||
if cacert is not None and os.path.isfile(cacert):
|
||||
@@ -70,11 +70,18 @@ class CrashReport:
|
||||
|
||||
def captureException(self, exception, value, tb):
|
||||
from .local_server import LocalServer
|
||||
from .local_config import LocalConfig
|
||||
|
||||
local_server = LocalServer.instance().localServerSettings()
|
||||
if local_server["report_errors"]:
|
||||
if not RAVEN_AVAILABLE:
|
||||
return
|
||||
|
||||
if os.path.exists(LocalConfig.instance().runAsRootPath()):
|
||||
log.warning("User has run application as root. Crash reports are disabled.")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
if os.path.exists(".git"):
|
||||
log.warning("A .git directory exist crash report is turn off for developers. Instant exit")
|
||||
sys.exit(1)
|
||||
@@ -104,7 +111,7 @@ class CrashReport:
|
||||
except Exception as e:
|
||||
log.error("Can't send crash report to Sentry: {}".format(e))
|
||||
return
|
||||
log.info("Crash report sent with event ID: {}".format(client.get_ident(report)))
|
||||
log.debug("Crash report sent with event ID: {}".format(client.get_ident(report)))
|
||||
|
||||
def _add_qt_information(self, context):
|
||||
try:
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sip
|
||||
import shutil
|
||||
|
||||
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
|
||||
@@ -30,27 +30,39 @@ from ..utils import human_filesize
|
||||
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..compute_manager import ComputeManager
|
||||
from ..controller import Controller
|
||||
from ..local_config import LocalConfig
|
||||
from ..image_upload_manager import ImageUploadManager
|
||||
|
||||
|
||||
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)
|
||||
@@ -60,6 +72,18 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
if Controller.instance().isRemote():
|
||||
self.uiLocalRadioButton.setText("Install the appliance on the main server")
|
||||
else:
|
||||
if not path.endswith('.builtin.gns3a'):
|
||||
destination = Config().appliances_dir
|
||||
try:
|
||||
os.makedirs(destination, exist_ok=True)
|
||||
destination = os.path.join(destination, os.path.basename(path))
|
||||
shutil.copy(path, destination)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Can't copy {} to {}".format(path, destination), str(e))
|
||||
|
||||
self.uiServerWizardPage.isComplete = self._uiServerWizardPage_isComplete
|
||||
|
||||
def initializePage(self, page_id):
|
||||
@@ -115,14 +139,23 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiInfoTreeWidget.addTopLevelItem(item)
|
||||
|
||||
elif self.page(page_id) == self.uiServerWizardPage:
|
||||
is_mac = ComputeManager.instance().localPlatform().startswith("darwin")
|
||||
is_win = ComputeManager.instance().localPlatform().startswith("win")
|
||||
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
for compute in ComputeManager.instance().remoteComputes():
|
||||
self.uiRemoteServersComboBox.addItem(compute.name(), compute)
|
||||
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 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 is_mac or is_win:
|
||||
if type == "qemu":
|
||||
# Qemu has issues on OSX and Windows we disallow usage of the local server
|
||||
if not LocalConfig.instance().experimental():
|
||||
@@ -132,18 +165,26 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
if ComputeManager.instance().vmCompute():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
elif ComputeManager.instance().localCompute():
|
||||
elif ComputeManager.instance().localCompute() and self.uiLocalRadioButton.isEnabled():
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
elif len(ComputeManager.instance().remoteComputes()) > 0:
|
||||
elif self.uiRemoteRadioButton.isEnabled():
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setChecked(False)
|
||||
|
||||
if is_mac or is_win:
|
||||
if not self.uiRemoteRadioButton.isEnabled() \
|
||||
and not self.uiVMRadioButton.isEnabled() \
|
||||
and not self.uiLocalRadioButton.isEnabled():
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self, "No GNS3 VM available.",
|
||||
"GNS3 VM is not available, please configure GNS3 VM before adding new Appliance.")
|
||||
|
||||
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()
|
||||
@@ -166,12 +207,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):
|
||||
"""
|
||||
@@ -192,91 +233,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 not "versions" in self._appliance:
|
||||
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
|
||||
@@ -285,7 +346,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)
|
||||
@@ -294,14 +355,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)
|
||||
@@ -314,40 +379,52 @@ 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_manger = ImageUploadManager(
|
||||
image, Controller.instance(), self._compute_id,
|
||||
self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
|
||||
image_upload_manger.upload()
|
||||
|
||||
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -368,6 +445,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):
|
||||
"""
|
||||
@@ -385,17 +466,23 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
if version is None:
|
||||
appliance_configuration = self._appliance.copy()
|
||||
else:
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
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"]))
|
||||
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add appliance", "New name:", QtWidgets.QLineEdit.Normal, appliance_configuration["name"])
|
||||
if not ok:
|
||||
return False
|
||||
appliance_configuration["name"] = appliance_configuration["name"].strip()
|
||||
|
||||
if "qemu" in appliance_configuration:
|
||||
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
|
||||
|
||||
worker = WaitForLambdaWorker(lambda: config.add_appliance(appliance_configuration, self._server), 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_():
|
||||
@@ -408,6 +495,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_manger = ImageUploadManager(
|
||||
image, Controller.instance(), self._compute_id,
|
||||
self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
|
||||
image_upload_manger.upload()
|
||||
|
||||
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 "docker" in self._appliance:
|
||||
@@ -425,8 +532,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"]))
|
||||
@@ -435,8 +548,13 @@ 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()
|
||||
if current:
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
@@ -449,17 +567,18 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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()).id()
|
||||
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
|
||||
self._server = "vm"
|
||||
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 = "local"
|
||||
self._compute_id = "local"
|
||||
|
||||
elif self.currentPage() == self.uiQemuWizardPage:
|
||||
if self.uiQemuListComboBox.currentIndex() == -1:
|
||||
@@ -471,6 +590,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 +601,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 +613,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.
|
||||
|
||||
@@ -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,8 +22,8 @@ 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, \
|
||||
PRECONFIGURED_SPICE_CONSOLE_COMMANDS, \
|
||||
CUSTOM_CONSOLE_COMMANDS_SETTINGS
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
|
||||
|
||||
def __init__(self, parent, console_type="telnet", current=None):
|
||||
"""
|
||||
:params console_type: telnet, serial or vnc
|
||||
:params console_type: telnet, serial, vnc or spice
|
||||
:params current: Current console command
|
||||
"""
|
||||
super().__init__(parent)
|
||||
@@ -63,8 +63,8 @@ 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)
|
||||
elif self._console_type == "spice":
|
||||
self._consoles = copy.copy(PRECONFIGURED_SPICE_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
|
||||
self.uiCommandComboBox.clear()
|
||||
|
||||
@@ -95,13 +95,14 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
def checkAVGInstalled(self):
|
||||
"""Checking if AVG software is not installed"""
|
||||
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
psinfo = proc.as_dict(["exe"])
|
||||
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
|
||||
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
if sys.platform.startswith("win32"):
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
psinfo = proc.as_dict(["exe"])
|
||||
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
|
||||
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
return (0, None)
|
||||
|
||||
def checkFreeRam(self):
|
||||
@@ -136,20 +137,17 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
if not os.path.exists(path):
|
||||
return (2, "Ubridge path {path} doesn't exists".format(path=path))
|
||||
|
||||
request_setuid = False
|
||||
if sys.platform.startswith("linux"):
|
||||
if "security.capability" in os.listxattr(path):
|
||||
caps = os.getxattr(path, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
return(2, "Ubridge require CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
|
||||
else:
|
||||
# capabilities not supported
|
||||
request_setuid = True
|
||||
try:
|
||||
if "security.capability" not in os.listxattr(path) or not struct.unpack("<IIIII", os.getxattr(path, "security.capability"))[1] & 1 << 13:
|
||||
return (2, "Ubridge requires CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
|
||||
except (OSError, AttributeError) as e:
|
||||
# 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: {}".format(e))
|
||||
|
||||
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 sys.platform.startswith("darwin"):
|
||||
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):
|
||||
@@ -164,11 +162,15 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
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):
|
||||
@@ -220,5 +222,5 @@ if __name__ == '__main__':
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = DoctorDialog(main, console=True)
|
||||
#dialog.show()
|
||||
# dialog.show()
|
||||
#exit_code = app.exec_()
|
||||
|
||||
@@ -41,9 +41,6 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
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())
|
||||
@@ -81,7 +78,7 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
|
||||
host = self.uiServerHostLineEdit.text().strip()
|
||||
name = self.uiServerNameLineEdit.text().strip()
|
||||
protocol = self.uiServerProtocolComboBox.currentText().lower()
|
||||
protocol = "http"
|
||||
port = self.uiServerPortSpinBox.value()
|
||||
user = self.uiServerUserLineEdit.text().strip()
|
||||
password = self.uiServerPasswordLineEdit.text().strip()
|
||||
@@ -89,6 +86,9 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
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
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from ..qt import QtWidgets
|
||||
from ..topology import Topology
|
||||
from ..ui.edit_project_dialog_ui import Ui_EditProjectDialog
|
||||
|
||||
@@ -25,6 +25,7 @@ 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__)
|
||||
@@ -44,16 +45,27 @@ 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.debug("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.instance().configDirectory()
|
||||
for filename in os.listdir(dir):
|
||||
@@ -61,12 +73,20 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
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()
|
||||
|
||||
@@ -67,6 +67,6 @@ class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
|
||||
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
|
||||
if not error:
|
||||
self.uiFileTextEdit.setText(raw_body.decode("utf-8"))
|
||||
elif result["status"] == 404:
|
||||
elif result.get("status") == 404:
|
||||
if self._default:
|
||||
self.uiFileTextEdit.setText(self._default)
|
||||
|
||||
174
gns3/dialogs/filter_dialog.py
Normal file
174
gns3/dialogs/filter_dialog.py
Normal file
@@ -0,0 +1,174 @@
|
||||
# -*- 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 QtGui, QtWidgets, qslot
|
||||
from ..ui.filter_dialog_ui import Ui_FilterDialog
|
||||
|
||||
|
||||
class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
|
||||
|
||||
"""
|
||||
Filter dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, link):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self._link = link
|
||||
self._link.updated_link_signal.connect(self._updateUiSlot)
|
||||
self._link.listAvailableFilters(self._listAvailableFiltersCallback)
|
||||
self._initialized = False
|
||||
self._filter_items = {}
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).clicked.connect(self._resetSlot)
|
||||
|
||||
def _listAvailableFiltersCallback(self, result, error=False, *args, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Link", "Error while listing information about the link: {}".format(result["message"]))
|
||||
return
|
||||
self._filters = result
|
||||
self._initialized = True
|
||||
self._updateUiSlot()
|
||||
|
||||
@qslot
|
||||
def _updateUiSlot(self, *args):
|
||||
|
||||
# Empty the main layout
|
||||
while True:
|
||||
item = self.uiVerticalLayout.takeAt(0)
|
||||
if item is None:
|
||||
break
|
||||
elif item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
if len(self._filters) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Link", "No filter available for this link. Try with a different node type.")
|
||||
self.reject()
|
||||
|
||||
self._tabWidget = QtWidgets.QTabWidget(self)
|
||||
for i, filter in enumerate(self._filters):
|
||||
tab = QtWidgets.QWidget()
|
||||
self._tabWidget.addTab(tab, filter['name'])
|
||||
self._tabWidget.setTabToolTip(i, filter['description'])
|
||||
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_red.svg'))
|
||||
vlayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
gridLayout = QtWidgets.QGridLayout()
|
||||
line = 0
|
||||
filter["spinBoxes"] = []
|
||||
filter["textEdits"] = []
|
||||
|
||||
nb_spin = 0
|
||||
|
||||
for param in filter["parameters"]:
|
||||
label = QtWidgets.QLabel()
|
||||
label.setText(param["name"] + ":")
|
||||
gridLayout.addWidget(label, line, 0, 1, 1)
|
||||
|
||||
if param["type"] == "int":
|
||||
spinBox = QtWidgets.QSpinBox()
|
||||
filter["spinBoxes"].append(spinBox)
|
||||
spinBox.setMinimum(param["minimum"])
|
||||
spinBox.setMaximum(param["maximum"])
|
||||
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(spinBox.sizePolicy().hasHeightForWidth())
|
||||
spinBox.setSizePolicy(sizePolicy)
|
||||
try:
|
||||
value = self._link.filters()[filter["type"]][nb_spin]
|
||||
spinBox.setValue(value)
|
||||
if value != 0:
|
||||
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_green.svg'))
|
||||
except(KeyError, IndexError):
|
||||
pass
|
||||
nb_spin += 1
|
||||
gridLayout.addWidget(spinBox, line, 1, 1, 1)
|
||||
unit = QtWidgets.QLabel()
|
||||
unit.setText(param["unit"])
|
||||
gridLayout.addWidget(unit, line, 2, 1, 1)
|
||||
elif param["type"] == "text":
|
||||
textEdit = QtWidgets.QTextEdit()
|
||||
textEdit.setAcceptRichText(False)
|
||||
filter["textEdits"].append(textEdit)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
textEdit.setMinimumWidth(300)
|
||||
textEdit.setSizePolicy(sizePolicy)
|
||||
try:
|
||||
text = self._link.filters()[filter["type"]][0]
|
||||
textEdit.setPlainText(text)
|
||||
if text:
|
||||
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_green.svg'))
|
||||
except(KeyError, IndexError):
|
||||
pass
|
||||
gridLayout.addWidget(textEdit, line, 1, 1, 1)
|
||||
|
||||
line += 1
|
||||
|
||||
spacerItem = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
gridLayout.addItem(spacerItem, line, 0, 1, 1)
|
||||
vlayout.addLayout(gridLayout)
|
||||
tab.setLayout(vlayout)
|
||||
|
||||
self.uiVerticalLayout.addWidget(self._tabWidget)
|
||||
|
||||
@qslot
|
||||
def _applyPreferencesSlot(self, *args):
|
||||
new_filters = {}
|
||||
for filter in self._filters:
|
||||
new_filters[filter["type"]] = []
|
||||
for spinBox in filter["spinBoxes"]:
|
||||
new_filters[filter["type"]].append(spinBox.value())
|
||||
for spinBox in filter["textEdits"]:
|
||||
new_filters[filter["type"]].append(spinBox.toPlainText())
|
||||
self._link.setFilters(new_filters)
|
||||
self._link.update()
|
||||
|
||||
@qslot
|
||||
def _helpSlot(self, *args):
|
||||
help_text = "Filters are applied to packets in both direction.\n\n"
|
||||
|
||||
filter_nb = 0
|
||||
for filter in self._filters:
|
||||
help_text += "{}: {}".format(filter["name"], filter["description"])
|
||||
filter_nb += 1
|
||||
if len(self._filters) != filter_nb:
|
||||
help_text += "\n\n"
|
||||
|
||||
QtWidgets.QMessageBox.information(self, "Help for filters", help_text)
|
||||
|
||||
@qslot
|
||||
def _resetSlot(self, *args):
|
||||
|
||||
filters = {}
|
||||
self._link.setFilters(filters)
|
||||
self._link.update()
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
Called when the dialog is closed.
|
||||
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
if result and self._initialized:
|
||||
self._applyPreferencesSlot()
|
||||
super().done(result)
|
||||
@@ -40,7 +40,8 @@ class IdlePCDialog(QtWidgets.QDialog, Ui_IdlePCDialog):
|
||||
self._idlepcs = idlepcs
|
||||
|
||||
for value in self._idlepcs:
|
||||
match = re.search(r"^(0x[0-9a-f]+)\s+\[(\d+)\]$", value)
|
||||
# validate idle-pc format, e.g. 0x60c09aa0
|
||||
match = re.search(r"^(0x[0-9a-f]{8})\s+\[(\d+)\]$", value)
|
||||
if match:
|
||||
idlepc = match.group(1)
|
||||
count = int(match.group(2))
|
||||
|
||||
@@ -137,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):
|
||||
"""
|
||||
@@ -153,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:
|
||||
@@ -215,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):
|
||||
|
||||
|
||||
190
gns3/dialogs/notif_dialog.py
Normal file
190
gns3/dialogs/notif_dialog.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Display error to the user in an overlay popup
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore, qslot
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
MAX_ELEMENTS = 3
|
||||
DISPLAY_DURATION = {
|
||||
"CRITICAL": 120,
|
||||
"ERROR": 120,
|
||||
"WARNING": 20,
|
||||
"INFO": 5
|
||||
}
|
||||
|
||||
|
||||
class NotifDialogHandler(logging.StreamHandler):
|
||||
|
||||
def __init__(self, dialog):
|
||||
super().__init__()
|
||||
self._dialog = dialog
|
||||
self.setLevel(logging.INFO)
|
||||
self._dialog.show()
|
||||
|
||||
def emit(self, record):
|
||||
self._dialog.addNotif(record.levelname, record.getMessage())
|
||||
|
||||
|
||||
class NotifDialog(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self._notifs = []
|
||||
|
||||
self.setWindowFlags(QtCore.Qt.FramelessWindowHint |
|
||||
QtCore.Qt.WindowDoesNotAcceptFocus |
|
||||
QtCore.Qt.SubWindow)
|
||||
# QtCore.Qt.Tool)
|
||||
# QtCore.Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) # | QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
self._layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
self._timer = QtCore.QTimer()
|
||||
self._timer.setInterval(1000)
|
||||
self._timer.timeout.connect(self._refreshSlot)
|
||||
self._timer.start()
|
||||
|
||||
for i in range(0, MAX_ELEMENTS):
|
||||
l = QtWidgets.QLabel()
|
||||
l.setAlignment(QtCore.Qt.AlignTop)
|
||||
l.setWordWrap(True)
|
||||
l.hide()
|
||||
self._layout.addWidget(l)
|
||||
self.setLayout(self._layout)
|
||||
|
||||
@qslot
|
||||
def addNotif(self, level, message):
|
||||
if not self.parent().settings().get("overlay_notifications", True):
|
||||
return
|
||||
|
||||
# This unicode char prevent the wordwrap at /
|
||||
message = message.replace("/", "\u2060/\u2060")
|
||||
if len(self._notifs) == MAX_ELEMENTS:
|
||||
self._notifs.pop(0)
|
||||
self._notifs.append((level, message, time.time()))
|
||||
self.update()
|
||||
|
||||
@qslot
|
||||
def _refreshSlot(self):
|
||||
"""
|
||||
Hide the notifs after some delay
|
||||
"""
|
||||
notifs = []
|
||||
for (i, (level, message, when)) in enumerate(self._notifs):
|
||||
if when + DISPLAY_DURATION[level] > time.time():
|
||||
notifs.append((level, message, when))
|
||||
if notifs != self._notifs:
|
||||
self._notifs = notifs
|
||||
self.update()
|
||||
elif len(notifs) > 0:
|
||||
self.resize()
|
||||
|
||||
def update(self):
|
||||
if len(self._notifs) == 0:
|
||||
self.hide()
|
||||
else:
|
||||
for (i, (level, message, when)) in enumerate(self._notifs):
|
||||
w = self._layout.itemAt(i).widget()
|
||||
w.setText(message)
|
||||
if level == "ERROR" or level == "CRITICAL":
|
||||
w.setStyleSheet("""
|
||||
color: black;
|
||||
padding-left: 12px;
|
||||
background-color: rgb(247, 205, 198);
|
||||
border-left: 10px solid red;
|
||||
""")
|
||||
elif level == "WARNING":
|
||||
w.setStyleSheet("""
|
||||
color: black;
|
||||
padding-left: 12px;
|
||||
background-color: #f4f2b5;
|
||||
border-left: 10px solid orange;
|
||||
""")
|
||||
elif level == "INFO":
|
||||
w.setStyleSheet("""
|
||||
color: black;
|
||||
padding-left: 12px;
|
||||
background-color: #cfffc9;
|
||||
border-left: 10px solid green;
|
||||
""")
|
||||
|
||||
w.show()
|
||||
for i in range(i + 1, MAX_ELEMENTS):
|
||||
w = self._layout.itemAt(i).widget()
|
||||
w.hide()
|
||||
|
||||
self.resize()
|
||||
self.show()
|
||||
|
||||
def resize(self):
|
||||
x = self.parent().width() - self.width() - 10
|
||||
y = 10
|
||||
self.setGeometry(x, y, self.sizeHint().width(), self.sizeHint().height())
|
||||
|
||||
@qslot
|
||||
def mousePressEvent(self, event):
|
||||
self._notifs.clear()
|
||||
self.update()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
"""
|
||||
A demo main for testing the features
|
||||
"""
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
class MainWindow(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
l1 = QtWidgets.QLabel()
|
||||
l1.setText("Hello World")
|
||||
|
||||
vbox = QtWidgets.QVBoxLayout()
|
||||
vbox.addWidget(l1)
|
||||
self.setLayout(vbox)
|
||||
self.setStyleSheet("background-color:blue;")
|
||||
self._dialog = NotifDialog(self)
|
||||
log.addHandler(NotifDialogHandler(self._dialog))
|
||||
log.info("test")
|
||||
|
||||
def moveEvent(self, event):
|
||||
log.error("An error")
|
||||
log.info("An info with an url http://test")
|
||||
log.warning("A warning with a long long long longlong longlong longlong longlong longlong longlong longlong long message")
|
||||
self._dialog.update()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._dialog.update()
|
||||
|
||||
main = MainWindow()
|
||||
main.setMinimumWidth(600)
|
||||
main.setMinimumHeight(600)
|
||||
main.show()
|
||||
exit_code = app.exec_()
|
||||
@@ -27,6 +27,9 @@ 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):
|
||||
|
||||
@@ -41,6 +44,7 @@ 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
|
||||
@@ -102,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)
|
||||
@@ -124,6 +129,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
# Class name, changed signal
|
||||
widget_to_watch = {
|
||||
QtWidgets.QLineEdit: "textChanged",
|
||||
QtWidgets.QPlainTextEdit: "textChanged",
|
||||
# QtWidgets.QTreeWidget: "itemChanged",
|
||||
QtWidgets.QComboBox: "currentIndexChanged",
|
||||
QtWidgets.QSpinBox: "valueChanged",
|
||||
@@ -140,17 +146,23 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
"""
|
||||
|
||||
# Found the page with the change
|
||||
widget = self.sender()
|
||||
widget = sender = self.sender()
|
||||
while widget.parent() != self.uiStackedWidget:
|
||||
widget = widget.parent()
|
||||
|
||||
self.addModifiedPage(widget)
|
||||
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):
|
||||
"""
|
||||
@@ -182,7 +194,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
"""
|
||||
|
||||
success = True
|
||||
for preferences_page in self._modified_pages:
|
||||
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:
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
@@ -41,6 +42,7 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
self.setupUi(self)
|
||||
|
||||
self.uiNewPushButton.clicked.connect(self._newPushButtonSlot)
|
||||
self.uiDeletePushButton.clicked.connect(self._deletePushButtonSlot)
|
||||
|
||||
# Center on screen
|
||||
screen = QtWidgets.QApplication.desktop().screenGeometry()
|
||||
@@ -52,15 +54,20 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
path = os.path.join(home, ".config", "GNS3")
|
||||
profiles_path = os.path.join(path, "profiles")
|
||||
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(profiles_path):
|
||||
for profil in sorted(os.listdir(os.path.join(path, "profiles"))):
|
||||
self.uiProfileSelectComboBox.addItem(profil)
|
||||
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
|
||||
|
||||
@@ -78,8 +85,19 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
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__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
dialog = ProfileSelectDialog()
|
||||
dialog.show()
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qslot
|
||||
from ..ui.project_dialog_ui import Ui_ProjectDialog
|
||||
from ..controller import Controller
|
||||
from ..topology import Topology
|
||||
@@ -43,7 +43,6 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = parent
|
||||
self._projects = []
|
||||
self._project_settings = {}
|
||||
self.uiNameLineEdit.setText(default_project_name)
|
||||
self.uiLocationLineEdit.setText(os.path.join(Topology.instance().projectsDirPath(), default_project_name))
|
||||
@@ -65,15 +64,14 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
self.uiLocationLineEdit.setVisible(False)
|
||||
self.uiLocationBrowserToolButton.setVisible(False)
|
||||
self.uiOpenProjectPushButton.setVisible(False)
|
||||
Controller.instance().connected_signal.connect(self._refreshProjects)
|
||||
|
||||
self.uiProjectsTreeWidget.itemDoubleClicked.connect(self._projectsTreeWidgetDoubleClickedSlot)
|
||||
self.uiDeleteProjectButton.clicked.connect(self._deleteProjectSlot)
|
||||
self.uiRefreshProjectsPushButton.clicked.connect(self._refreshProjects)
|
||||
self._refreshProjects()
|
||||
|
||||
def _refreshProjects(self):
|
||||
Controller.instance().get("/projects", self._projectListCallback)
|
||||
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):
|
||||
"""
|
||||
@@ -85,9 +83,9 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
def _projectsTreeWidgetDoubleClickedSlot(self, item, column):
|
||||
self.done(True)
|
||||
|
||||
def _deleteProjectSlot(self):
|
||||
current = self.uiProjectsTreeWidget.currentItem()
|
||||
if current is None:
|
||||
@qslot
|
||||
def _deleteProjectSlot(self, *args):
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Delete project", "No project selected")
|
||||
return
|
||||
|
||||
@@ -105,39 +103,75 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
projects_to_delete.add(project_id)
|
||||
|
||||
for project_id in projects_to_delete:
|
||||
Controller.instance().delete("/projects/{}".format(project_id), self._deleteProjectCallback)
|
||||
Controller.instance().deleteProject(project_id)
|
||||
|
||||
def _deleteProjectCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while deleting project: {}".format(result["message"]))
|
||||
def _duplicateProjectSlot(self):
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Duplicate project", "No project selected")
|
||||
return
|
||||
Controller.instance().get("/projects", self._projectListCallback)
|
||||
|
||||
def _projectListCallback(self, result, error=False, **kwargs):
|
||||
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 duplicating project: {}".format(result["message"]))
|
||||
return
|
||||
Controller.instance().refreshProjectList()
|
||||
|
||||
@qslot
|
||||
def _updateProjectListSlot(self, *args):
|
||||
self.uiProjectsTreeWidget.clear()
|
||||
self.uiDeleteProjectButton.setEnabled(False)
|
||||
if not error:
|
||||
self._projects = result
|
||||
self.uiProjectsTreeWidget.setUpdatesEnabled(False)
|
||||
items = []
|
||||
for project in result:
|
||||
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)
|
||||
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(result):
|
||||
self.uiDeleteProjectButton.setEnabled(True)
|
||||
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)
|
||||
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):
|
||||
"""
|
||||
@@ -194,51 +228,67 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.triggered.connect(self._menuTriggeredSlot)
|
||||
for action in self._main_window._recent_project_actions:
|
||||
menu.addAction(action)
|
||||
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:
|
||||
if "message" in result:
|
||||
log.error("Error while overwrite project: {}".format(result["message"]))
|
||||
return
|
||||
self._projects = []
|
||||
self._refreshProjects()
|
||||
# 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):
|
||||
project_name = self.uiNameLineEdit.text()
|
||||
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
|
||||
|
||||
if not project_name:
|
||||
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 self._projects:
|
||||
if project_name == existing_project["name"]:
|
||||
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(project_name),
|
||||
"Project {} already exists, overwrite it?".format(existing_project["name"]),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
Controller.instance().delete("/projects/{}".format(existing_project["project_id"]), self._overwriteProjectCallback)
|
||||
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
|
||||
|
||||
self._project_settings["project_name"] = project_name
|
||||
|
||||
if not Controller.instance().isRemote():
|
||||
project_location = self.uiLocationLineEdit.text()
|
||||
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, project_name + ".gns3")
|
||||
self._project_settings["project_files_dir"] = project_location
|
||||
return True
|
||||
|
||||
def done(self, result):
|
||||
@@ -255,5 +305,4 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
|
||||
self._project_settings["project_id"] = current.data(0, QtCore.Qt.UserRole)
|
||||
self._project_settings["project_name"] = current.data(1, QtCore.Qt.UserRole)
|
||||
self._project_settings["project_path"] = current.data(2, QtCore.Qt.UserRole)
|
||||
super().done(result)
|
||||
|
||||
@@ -19,17 +19,21 @@ import sys
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets, QtGui, QtNetwork
|
||||
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 gns3.dialogs.new_appliance_dialog import NewApplianceDialog
|
||||
|
||||
from ..settings import DEFAULT_LOCAL_SERVER_HOST
|
||||
from ..ui.setup_wizard_ui import Ui_SetupWizard
|
||||
from ..version import __version__
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
"""
|
||||
@@ -56,6 +60,8 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
|
||||
|
||||
self.uiLocalServerToolButton.clicked.connect(self._localServerBrowserSlot)
|
||||
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('')
|
||||
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
|
||||
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
|
||||
@@ -76,15 +82,13 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
# load all available addresses
|
||||
for address in QtNetwork.QNetworkInterface.allAddresses():
|
||||
address_string = address.toString()
|
||||
# if address.protocol() == QtNetwork.QAbstractSocket.IPv6Protocol:
|
||||
# we do not want the scope id when using an IPv6 address...
|
||||
# address.setScopeId("")
|
||||
self.uiLocalServerHostComboBox.addItem(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"))
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.png"))
|
||||
else:
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.jpg"))
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.png"))
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
self.uiVMRadioButton.setText("Run the topologies in an isolated and standard VM")
|
||||
@@ -92,6 +96,9 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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.
|
||||
@@ -109,9 +116,9 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
def _VMwareBannerButtonClickedSlot(self):
|
||||
if sys.platform.startswith("darwin"):
|
||||
url = "http://send.onenetworkdirect.net/z/616461/CD225091/"
|
||||
url = "http://send.onenetworkdirect.net/z/621394/CD225091/"
|
||||
else:
|
||||
url = "http://send.onenetworkdirect.net/z/616460/CD225091/"
|
||||
url = "http://send.onenetworkdirect.net/z/616207/CD225091/"
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
|
||||
|
||||
def _listVMwareVMsSlot(self):
|
||||
@@ -125,7 +132,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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()
|
||||
|
||||
@@ -141,7 +148,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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()
|
||||
|
||||
@@ -176,6 +183,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
super().initializePage(page_id)
|
||||
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":
|
||||
@@ -195,15 +203,36 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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"])
|
||||
elif self.page(page_id) == self.uiLocalServerStatusWizardPage:
|
||||
self._refreshLocalServerStatusSlot()
|
||||
|
||||
elif self.page(page_id) == self.uiSummaryWizardPage:
|
||||
use_local_server = self.uiLocalRadioButton.isChecked()
|
||||
self.uiSummaryTreeWidget.clear()
|
||||
if use_local_server:
|
||||
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())
|
||||
@@ -211,6 +240,20 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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
|
||||
|
||||
@@ -236,6 +279,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
Validates the settings.
|
||||
"""
|
||||
|
||||
Controller.instance().setDisplayError(True)
|
||||
if self.currentPage() == self.uiVMWizardPage:
|
||||
vmname = self.uiVMListComboBox.currentText()
|
||||
if vmname:
|
||||
@@ -279,26 +323,29 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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"] = "http"
|
||||
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:
|
||||
use_local_server = self.uiLocalRadioButton.isChecked()
|
||||
if use_local_server:
|
||||
if self.uiLocalRadioButton.isChecked():
|
||||
# deactivate the GNS3 VM if using the local server
|
||||
vm_settings = self._GNS3VMSettings()
|
||||
vm_settings["enable"] = False
|
||||
self._setGNS3VMSettings(vm_settings)
|
||||
|
||||
# update the modules so they use the local server
|
||||
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
|
||||
|
||||
return True
|
||||
|
||||
@@ -345,12 +392,19 @@ 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)
|
||||
dialog = NewApplianceDialog(self.parentWidget())
|
||||
dialog.show()
|
||||
|
||||
def nextId(self):
|
||||
"""
|
||||
@@ -358,9 +412,21 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
"""
|
||||
|
||||
current_id = self.currentId()
|
||||
if self.page(current_id) == self.uiServerWizardPage and self.uiVMRadioButton.isChecked():
|
||||
# skip the local server page if using the GNS3 VM
|
||||
return self.uiLocalServerWizardPage.nextId()
|
||||
if self.page(current_id) == self.uiLocalServerWizardPage:
|
||||
return self.uiVMWizardPage.nextId()
|
||||
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,11 @@
|
||||
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 ..node import Node
|
||||
from ..controller import Controller
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..utils.create_snapshot_worker import CreateSnapshotWorker
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@@ -64,7 +58,8 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
"""
|
||||
|
||||
self.uiSnapshotsList.clear()
|
||||
Controller.instance().get("/projects/{}/snapshots".format(self._project.id()), self._listSnapshotsCallback)
|
||||
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:
|
||||
@@ -91,16 +86,22 @@ 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:
|
||||
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()), self._createSnapshotsCallback, {"name": snapshot_name})
|
||||
if ok and snapshot_name and self._project:
|
||||
snapshot_worker = CreateSnapshotWorker(self._project, snapshot_name)
|
||||
snapshot_worker.finished.connect(self._createSnapshotsCallback)
|
||||
|
||||
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
return
|
||||
progress_dialog = ProgressDialog(snapshot_worker, "Snapshot progress", "Creation of snapshot in progress...",
|
||||
"Cancel", busy=True, parent=self, create_thread=False, cancelable=True)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
|
||||
def _createSnapshotsCallback(self):
|
||||
self._listSnapshots()
|
||||
|
||||
def _createSnapshotsErrorCallback(self, message, error):
|
||||
log.error(message)
|
||||
|
||||
def _deleteSnapshotSlot(self):
|
||||
"""
|
||||
Slot to delete a snapshot.
|
||||
@@ -138,7 +139,7 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
if reply == QtWidgets.QMessageBox.Cancel:
|
||||
return
|
||||
|
||||
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id), self._restoreSnapshotsCallback)
|
||||
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id), self._restoreSnapshotsCallback, timeout=300)
|
||||
|
||||
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
|
||||
@@ -21,6 +21,7 @@ Style editor to edit Shape items.
|
||||
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
|
||||
from ..items.shape_item import ShapeItem
|
||||
|
||||
|
||||
class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
@@ -52,12 +53,18 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
# use the first item in the list as the model
|
||||
first_item = items[0]
|
||||
pen = first_item.pen()
|
||||
brush = first_item.brush()
|
||||
self._color = brush.color()
|
||||
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
|
||||
self._color.green(),
|
||||
self._color.blue(),
|
||||
self._color.alpha()))
|
||||
if hasattr(first_item, "brush"): # Line don't have brush
|
||||
brush = first_item.brush()
|
||||
self._color = brush.color()
|
||||
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
|
||||
self._color.green(),
|
||||
self._color.blue(),
|
||||
self._color.alpha()))
|
||||
else:
|
||||
self.uiColorLabel.hide()
|
||||
self.uiColorPushButton.hide()
|
||||
self._color = None
|
||||
|
||||
self._border_color = pen.color()
|
||||
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
|
||||
self._border_color.green(),
|
||||
@@ -102,11 +109,17 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
border_style = QtCore.Qt.PenStyle(self.uiBorderStyleComboBox.itemData(self.uiBorderStyleComboBox.currentIndex()))
|
||||
pen = QtGui.QPen(self._border_color, self.uiBorderWidthSpinBox.value(), border_style, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
brush = QtGui.QBrush(self._color)
|
||||
if self._color:
|
||||
brush = QtGui.QBrush(self._color)
|
||||
else:
|
||||
brush = None
|
||||
|
||||
for item in self._items:
|
||||
item.setPen(pen)
|
||||
item.setBrush(brush)
|
||||
# on multiselection it's possible to select many type of items
|
||||
# but brush can be applied only on ShapeItem,
|
||||
if brush and isinstance(item, ShapeItem):
|
||||
item.setBrush(brush)
|
||||
item.setRotation(self.uiRotationSpinBox.value())
|
||||
|
||||
def done(self, 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.
|
||||
"""
|
||||
|
||||
@@ -21,24 +21,22 @@ from gns3.qt import QtWidgets
|
||||
from gns3.controller import Controller
|
||||
|
||||
|
||||
|
||||
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):
|
||||
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):
|
||||
"""
|
||||
@@ -144,7 +142,7 @@ class VMWithImagesWizard(VMWizard):
|
||||
:param endpoint: server endpoint with the list of Images
|
||||
"""
|
||||
|
||||
Controller.instance().get("/computes/{}{}".format(self._compute_id, endpoint), self._getImagesFromServerCallback)
|
||||
Controller.instance().getCompute(endpoint, self._compute_id, self._getImagesFromServerCallback)
|
||||
|
||||
def _getImagesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -183,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
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import sys
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.controller import Controller
|
||||
|
||||
|
||||
class VMWizard(QtWidgets.QWizard):
|
||||
@@ -26,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"):
|
||||
@@ -49,9 +49,11 @@ class VMWizard(QtWidgets.QWizard):
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
if Controller.instance().isRemote():
|
||||
self.uiLocalRadioButton.setText("Run device on the main server")
|
||||
|
||||
# By default we use the local server
|
||||
self._compute_id = ComputeManager.instance().computes()[0].id()
|
||||
self._compute_id = "local"
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
self._localToggledSlot(True)
|
||||
|
||||
@@ -103,15 +105,16 @@ class VMWizard(QtWidgets.QWizard):
|
||||
for compute in ComputeManager.instance().computes():
|
||||
if compute.id() == "local":
|
||||
self.uiLocalRadioButton.setEnabled(True)
|
||||
elif compute.id() == "vm" and hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.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._use_local_server and self.uiLocalRadioButton.isEnabled() and self.uiLocalRadioButton.isVisible():
|
||||
if self.uiLocalRadioButton.isEnabled() and not self._local_server_disable:
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
elif self.uiVMRadioButton.isEnabled():
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isEnabled():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
else:
|
||||
if self.uiRemoteRadioButton.isEnabled():
|
||||
@@ -123,6 +126,7 @@ class VMWizard(QtWidgets.QWizard):
|
||||
"""
|
||||
Turn off the local server
|
||||
"""
|
||||
self._local_server_disable = True
|
||||
self.uiLocalRadioButton.hide()
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
self.setStartId(0)
|
||||
|
||||
@@ -22,19 +22,19 @@ Graphical view on the scene where items are drawn.
|
||||
import logging
|
||||
import os
|
||||
import sip
|
||||
import pickle
|
||||
import sys
|
||||
|
||||
from .qt import QtCore, QtGui, QtSvg, QtNetwork, QtWidgets, qpartial
|
||||
from .qt import QtCore, QtGui, QtNetwork, QtWidgets, qpartial, qslot
|
||||
from .items.node_item import NodeItem
|
||||
from .dialogs.node_properties_dialog import NodePropertiesDialog
|
||||
from .link import Link
|
||||
from .node import Node
|
||||
from .modules import MODULES
|
||||
from .modules.builtin.cloud import Cloud
|
||||
from .modules.module_error import ModuleError
|
||||
from .modules.builtin import Builtin
|
||||
from .settings import GRAPHICS_VIEW_SETTINGS
|
||||
from .topology import Topology
|
||||
from .ports.port import Port
|
||||
from .appliance_manager import ApplianceManager
|
||||
from .dialogs.style_editor_dialog import StyleEditorDialog
|
||||
from .dialogs.text_editor_dialog import TextEditorDialog
|
||||
from .dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
@@ -44,7 +44,6 @@ from .dialogs.file_editor_dialog import FileEditorDialog
|
||||
from .local_config import LocalConfig
|
||||
from .progress import Progress
|
||||
from .utils.server_select import server_select
|
||||
from .utils.normalize_filename import normalize_filename
|
||||
from .compute_manager import ComputeManager
|
||||
|
||||
# link items
|
||||
@@ -58,6 +57,7 @@ from .items.text_item import TextItem
|
||||
from .items.shape_item import ShapeItem
|
||||
from .items.drawing_item import DrawingItem
|
||||
from .items.rectangle_item import RectangleItem
|
||||
from .items.line_item import LineItem
|
||||
from .items.ellipse_item import EllipseItem
|
||||
from .items.image_item import ImageItem
|
||||
|
||||
@@ -85,6 +85,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_note = False
|
||||
self._adding_rectangle = False
|
||||
self._adding_ellipse = False
|
||||
self._adding_line = False
|
||||
self._newlink = None
|
||||
self._dragging = False
|
||||
self._last_mouse_position = None
|
||||
@@ -116,6 +117,16 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
def setSceneSize(self, width, height):
|
||||
self.scene().setSceneRect(-(width / 2), -(height / 2), width, height)
|
||||
|
||||
def setZoom(self, zoom):
|
||||
"""
|
||||
Sets zoom of the Graphics View
|
||||
:param zoom:
|
||||
:return:
|
||||
"""
|
||||
if zoom:
|
||||
factor = zoom / 100.
|
||||
self.scale(factor, factor)
|
||||
|
||||
def setEnabled(self, enabled):
|
||||
|
||||
if enabled is False:
|
||||
@@ -125,6 +136,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self.scene().addItem(item)
|
||||
super().setEnabled(enabled)
|
||||
|
||||
self.toggleUiDeviceMenu()
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Remove all the items from the scene and
|
||||
@@ -236,6 +249,20 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_ellipse = False
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addLine(self, state):
|
||||
"""
|
||||
Adds a line.
|
||||
|
||||
:param state: boolean
|
||||
"""
|
||||
|
||||
if state:
|
||||
self._adding_line = True
|
||||
self.setCursor(QtCore.Qt.PointingHandCursor)
|
||||
else:
|
||||
self._adding_line = False
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addImage(self, image_path):
|
||||
"""
|
||||
Adds an image.
|
||||
@@ -280,6 +307,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
link = self._topology.getLink(link_id)
|
||||
if not link:
|
||||
return
|
||||
source_item = None
|
||||
destination_item = None
|
||||
source_port = link.sourcePort()
|
||||
@@ -300,41 +329,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self.deleteLinkSlot(link_id)
|
||||
return
|
||||
|
||||
# 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 source_item == destination_item:
|
||||
multi = 0
|
||||
else:
|
||||
multi = 0
|
||||
link_items = source_item.links()
|
||||
for link_item in link_items:
|
||||
if link_item.destinationItem().node().id() == destination_item.node().id():
|
||||
multi += 1
|
||||
if link_item.sourceItem().node().id() == 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
|
||||
|
||||
if link.sourcePort().linkType() == "Serial":
|
||||
link_item = SerialLinkItem(source_item, source_port, destination_item, destination_port, link, multilink=multi)
|
||||
link_item = SerialLinkItem(source_item, source_port, destination_item, destination_port, link)
|
||||
else:
|
||||
link_item = EthernetLinkItem(source_item, source_port, destination_item, destination_port, link, multilink=multi)
|
||||
link_item = EthernetLinkItem(source_item, source_port, destination_item, destination_port, link)
|
||||
self.scene().addItem(link_item)
|
||||
|
||||
def deleteLinkSlot(self, link_id):
|
||||
@@ -362,12 +360,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
source_port = source_item.connectToPort()
|
||||
if not source_port:
|
||||
return
|
||||
if not source_item.node().initialized():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "This node hasn't been initialized correctly")
|
||||
return
|
||||
if not source_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Port {} isn't free".format(source_port.name()))
|
||||
|
||||
if source_port.link() is not None:
|
||||
QtWidgets.QMessageBox.warning(self, "Create link", "Can't create the link the port is not free")
|
||||
return
|
||||
|
||||
if source_port.linkType() == "Serial":
|
||||
self._newlink = SerialLinkItem(source_item, source_port, self.mapToScene(event.pos()), None, adding_flag=True)
|
||||
else:
|
||||
@@ -377,24 +374,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
source_item = self._newlink.sourceItem()
|
||||
source_port = self._newlink.sourcePort()
|
||||
destination_item = item
|
||||
if source_item == destination_item:
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Cannot connect to itself!")
|
||||
return
|
||||
destination_port = destination_item.connectToPort()
|
||||
if not destination_port:
|
||||
return
|
||||
if not destination_item.node().initialized():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "This node hasn't been initialized correctly")
|
||||
return
|
||||
if not destination_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Port {} isn't free".format(destination_port.name()))
|
||||
return
|
||||
elif source_port.linkType() != destination_port.linkType():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Cannot connect this port!")
|
||||
return
|
||||
|
||||
if isinstance(source_item.node(), Cloud) and isinstance(destination_item.node(), Cloud):
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Sorry, you cannot connect a cloud to another cloud!")
|
||||
if destination_port.link() is not None:
|
||||
QtWidgets.QMessageBox.warning(self, "Create link", "Can't create the link the destination port is not free")
|
||||
return
|
||||
|
||||
if self._newlink in self.scene().items():
|
||||
@@ -436,7 +421,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
item.setSelected(True)
|
||||
elif is_not_link and event.button() == QtCore.Qt.RightButton and not self._adding_link:
|
||||
if item:
|
||||
if item and not sip.isdeleted(item):
|
||||
# Prevent right clicking on a selected item from de-selecting all other items
|
||||
if not item.isSelected():
|
||||
if not event.modifiers() & QtCore.Qt.ControlModifier:
|
||||
@@ -446,7 +431,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
item.setFlag(item.ItemIsSelectable, True)
|
||||
item.setSelected(True)
|
||||
self._showDeviceContextualMenu(QtGui.QCursor.pos())
|
||||
if item.zValue() < 0:
|
||||
if not sip.isdeleted(item) and item.zValue() < 0:
|
||||
item.setFlag(item.ItemIsSelectable, False)
|
||||
|
||||
else:
|
||||
@@ -462,7 +447,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._userNodeLinking(event, item)
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_note:
|
||||
pos = self.mapToScene(event.pos())
|
||||
note = self.createDrawingItem("text", pos.x(), pos.y(), 0)
|
||||
note = self.createDrawingItem("text", pos.x(), pos.y(), 1)
|
||||
pos_x = note.pos().x()
|
||||
pos_y = note.pos().y() - (note.boundingRect().height() / 2)
|
||||
note.setPos(pos_x, pos_y)
|
||||
@@ -482,9 +467,17 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._main_window.uiDrawEllipseAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_ellipse = False
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_line:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("line", pos.x(), pos.y(), 0)
|
||||
self._main_window.uiDrawLineAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_line = False
|
||||
else:
|
||||
super().mousePressEvent(event)
|
||||
|
||||
self.toggleUiDeviceMenu()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""
|
||||
Handles all mouse release events.
|
||||
@@ -507,6 +500,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
item.setSelected(True)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
self.toggleUiDeviceMenu()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
"""
|
||||
Handles zoom in or out using the mouse wheel.
|
||||
@@ -519,6 +514,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if delta is not None and delta.x() == 0:
|
||||
# CTRL is pressed then use the mouse wheel to zoom in or out.
|
||||
self.scaleView(pow(2.0, delta.y() / 240.0))
|
||||
self._topology.project().setZoom(round(self.transform().m11() * 100))
|
||||
self._topology.project().update()
|
||||
else:
|
||||
super().wheelEvent(event)
|
||||
|
||||
@@ -531,6 +528,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if factor < 0.10 or factor > 10:
|
||||
return
|
||||
self.scale(scale_factor, scale_factor)
|
||||
self._main_window.uiStatusBar.showMessage("Zoom: {}%".format(round(self.transform().m11() * 100)), 2000)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
@@ -595,10 +593,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if not self._adding_link:
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
item.setSelected(True)
|
||||
if item.node().status() == Node.stopped:
|
||||
if item.node().status() == Node.stopped or item.node().isAlwaysOn():
|
||||
self.configureSlot()
|
||||
return
|
||||
else:
|
||||
if sys.platform.startswith("win") and item.node().bringToFront():
|
||||
return
|
||||
self.consoleFromItems(self.scene().selectedItems())
|
||||
return
|
||||
elif isinstance(item, NoteItem) and isinstance(item.parentItem(), NodeItem):
|
||||
@@ -632,7 +632,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
# check if what is dragged is handled by this view
|
||||
if event.mimeData().hasFormat("application/x-gns3-node") or event.mimeData().hasFormat("text/uri-list"):
|
||||
if event.mimeData().hasFormat("text/uri-list") \
|
||||
or event.mimeData().hasFormat("application/x-gns3-appliance"):
|
||||
event.acceptProposedAction()
|
||||
event.accept()
|
||||
else:
|
||||
@@ -646,10 +647,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
# check if what has been dropped is handled by this view
|
||||
if event.mimeData().hasFormat("application/x-gns3-node"):
|
||||
data = event.mimeData().data("application/x-gns3-node")
|
||||
# load the pickled node data
|
||||
node_data = pickle.loads(data)
|
||||
if event.mimeData().hasFormat("application/x-gns3-appliance"):
|
||||
appliance_id = event.mimeData().data("application/x-gns3-appliance").data().decode()
|
||||
event.setDropAction(QtCore.Qt.CopyAction)
|
||||
event.accept()
|
||||
if event.keyboardModifiers() == QtCore.Qt.ShiftModifier:
|
||||
@@ -660,12 +659,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
for node_number in range(integer):
|
||||
x = event.pos().x() - (150 / 2) + (node_number % max_nodes_per_line) * offset
|
||||
y = event.pos().y() - (70 / 2) + (node_number // max_nodes_per_line) * offset
|
||||
node_item = self.createNode(node_data, QtCore.QPoint(x, y))
|
||||
if node_item is None:
|
||||
# stop if there is any error
|
||||
if self.createNodeFromApplianceId(appliance_id, QtCore.QPoint(x, y)) is False:
|
||||
event.ignore()
|
||||
break
|
||||
else:
|
||||
self.createNode(node_data, event.pos())
|
||||
if self.createNodeFromApplianceId(appliance_id, event.pos()) is False:
|
||||
event.ignore()
|
||||
elif event.mimeData().hasFormat("text/uri-list") and event.mimeData().hasUrls():
|
||||
# This should not arrive but we received bug report with it...
|
||||
if len(event.mimeData().urls()) == 0:
|
||||
@@ -674,7 +673,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
QtWidgets.QMessageBox.critical(self, "Project files", "Please drop only one file")
|
||||
return
|
||||
path = event.mimeData().urls()[0].toLocalFile()
|
||||
if os.path.isfile(path) and self._main_window.checkForUnsavedChanges():
|
||||
if os.path.isfile(path):
|
||||
self._main_window.loadPath(path)
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
@@ -723,6 +722,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
change_symbol_action.triggered.connect(self.changeSymbolActionSlot)
|
||||
menu.addAction(change_symbol_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, DrawingItem) or isinstance(item, NodeItem), items)):
|
||||
duplicate_action = QtWidgets.QAction("Duplicate", menu)
|
||||
duplicate_action.setIcon(QtGui.QIcon(':/icons/new.svg'))
|
||||
duplicate_action.triggered.connect(self.duplicateActionSlot)
|
||||
menu.addAction(duplicate_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "nodeDir"), items)):
|
||||
# Action: Show in file manager
|
||||
show_in_file_manager_action = QtWidgets.QAction("Show in file manager", menu)
|
||||
@@ -748,6 +753,13 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
|
||||
menu.addAction(aux_console_action)
|
||||
|
||||
if sys.platform.startswith("win") and True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"), items)):
|
||||
# Action: bring console or window to front (Windows only)
|
||||
bring_to_front_action = QtWidgets.QAction("Bring to front", menu)
|
||||
bring_to_front_action.setIcon(QtGui.QIcon(':/icons/front.svg'))
|
||||
bring_to_front_action.triggered.connect(self.bringToFrontSlot)
|
||||
menu.addAction(bring_to_front_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
|
||||
import_config_action = QtWidgets.QAction("Import config", menu)
|
||||
import_config_action.setIcon(QtGui.QIcon(':/icons/import_config.svg'))
|
||||
@@ -802,12 +814,6 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
reload_action.triggered.connect(self.reloadActionSlot)
|
||||
menu.addAction(reload_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, DrawingItem), items)):
|
||||
duplicate_action = QtWidgets.QAction("Duplicate", menu)
|
||||
duplicate_action.setIcon(QtGui.QIcon(':/icons/new.svg'))
|
||||
duplicate_action.triggered.connect(self.duplicateActionSlot)
|
||||
menu.addAction(duplicate_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NoteItem), items)):
|
||||
text_edit_action = QtWidgets.QAction("Text edit", menu)
|
||||
text_edit_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
|
||||
@@ -820,7 +826,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
text_edit_action.triggered.connect(self.textEditActionSlot)
|
||||
menu.addAction(text_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, ShapeItem), items)):
|
||||
if True in list(map(lambda item: isinstance(item, ShapeItem) or isinstance(item, LineItem), items)):
|
||||
style_action = QtWidgets.QAction("Style", menu)
|
||||
style_action.setIcon(QtGui.QIcon(':/icons/drawing.svg'))
|
||||
style_action.triggered.connect(self.styleActionSlot)
|
||||
@@ -917,7 +923,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
if isinstance(item, NodeItem) and item.node().initialized() and hasattr(item.node(), "configPage"):
|
||||
items.append(item)
|
||||
|
||||
if items:
|
||||
@@ -969,12 +975,14 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
break
|
||||
|
||||
if os.path.exists(node_dir):
|
||||
log.debug("Open %s in file manage")
|
||||
log.debug("Open %s in file manager")
|
||||
if QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(node_dir)) is False:
|
||||
QtWidgets.QMessageBox.critical(self, "Show in file manager", "Failed to open {}".format(node_dir))
|
||||
break
|
||||
else:
|
||||
QtWidgets.QMessageBox.information(self, "Show in file manager", "The device directory is located in {} on {}".format(node_dir, node.compute().name()))
|
||||
reply = QtWidgets.QMessageBox.information(self, "Show in file manager", "The device directory is located in {} on {}\n\nCopy path to clipboard?".format(node_dir, node.compute().name()), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
QtWidgets.QApplication.clipboard().setText(node_dir)
|
||||
break
|
||||
|
||||
def consoleToNode(self, node, aux=False):
|
||||
@@ -995,6 +1003,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# returns True to ignore this node.
|
||||
return True
|
||||
|
||||
# TightVNC has lack support of IPv6 host at this moment
|
||||
if "vncviewer" in node.consoleCommand() and ":" in node.consoleHost():
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self, "TightVNC", "TightVNC (vncviewer) may not start because of lack of IPv6 support.")
|
||||
|
||||
try:
|
||||
node.openConsole(aux=aux)
|
||||
except (OSError, ValueError) as e:
|
||||
@@ -1010,12 +1023,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
nodes = {}
|
||||
node_initialized = False
|
||||
for item in items:
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
|
||||
node = item.node()
|
||||
nodes[node.name()] = node
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized():
|
||||
node_initialized = True
|
||||
if item.node().status() == Node.started:
|
||||
node = item.node()
|
||||
nodes[node.name()] = node
|
||||
|
||||
if not nodes:
|
||||
if not nodes and node_initialized:
|
||||
if len(items) > 1:
|
||||
QtWidgets.QMessageBox.warning(self, "Console", "At least one node must be started before a console can be opened")
|
||||
else:
|
||||
@@ -1029,6 +1045,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._main_window.run_later(counter, callback)
|
||||
counter += delay
|
||||
|
||||
def consoleFromAllItems(self):
|
||||
"""
|
||||
Console from all scene items, except builtin devices.
|
||||
"""
|
||||
|
||||
items = [item for item in self.scene().items()
|
||||
if not (isinstance(item, NodeItem) and isinstance(item.node().module(), Builtin))]
|
||||
self.consoleFromItems(items)
|
||||
|
||||
def consoleActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the console action in the
|
||||
@@ -1046,7 +1071,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
console_type = "telnet"
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
|
||||
if item.node().consoleType() not in ("telnet", "serial", "vnc"):
|
||||
if item.node().consoleType() not in ("telnet", "serial", "vnc", "spice"):
|
||||
continue
|
||||
current_cmd = item.node().consoleCommand()
|
||||
console_type = item.node().consoleType()
|
||||
@@ -1056,7 +1081,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
|
||||
node = item.node()
|
||||
if node.consoleType() not in ("telnet", "serial", "vnc"):
|
||||
if node.consoleType() not in ("telnet", "serial", "vnc", "spice"):
|
||||
continue
|
||||
try:
|
||||
node.openConsole(command=cmd)
|
||||
@@ -1071,12 +1096,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
nodes = {}
|
||||
node_initialized = False
|
||||
for item in items:
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole") and item.node().initialized() and item.node().status() == Node.started:
|
||||
node = item.node()
|
||||
nodes[node.name()] = node
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole") and item.node().initialized():
|
||||
node_initialized = True
|
||||
if item.node().status() == Node.started:
|
||||
node = item.node()
|
||||
nodes[node.name()] = node
|
||||
|
||||
if not nodes:
|
||||
if not nodes and node_initialized:
|
||||
if len(items) > 1:
|
||||
QtWidgets.QMessageBox.warning(self, "Console", "At least one node must be started before a console can be opened")
|
||||
else:
|
||||
@@ -1128,6 +1156,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._import_config_dir,
|
||||
"All files (*.*);;Config files (*.cfg)",
|
||||
"Config files (*.cfg)")
|
||||
if not path:
|
||||
continue
|
||||
self._import_config_dir = os.path.dirname(path)
|
||||
item.node().importFile(config_file, path)
|
||||
|
||||
@@ -1175,12 +1205,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
for item in items:
|
||||
for config_file in item.node().configFiles():
|
||||
path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_configs_to_dir, item.node().name() + "_" + os.path.basename(config_file)), "All files (*.*);;Config files (*.cfg)")
|
||||
|
||||
if not path:
|
||||
continue
|
||||
|
||||
self._export_configs_to_dir = os.path.dirname(path)
|
||||
|
||||
item.node().exportFile(config_file, path)
|
||||
|
||||
def getCommandLineSlot(self):
|
||||
@@ -1208,6 +1235,16 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
def bringToFrontSlot(self):
|
||||
"""
|
||||
Slot to receive events from the bring to front action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"):
|
||||
item.node().bringToFront()
|
||||
|
||||
def idlepcActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the idlepc action in the
|
||||
@@ -1232,7 +1269,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC", "Error: {}".format(result["message"]))
|
||||
else:
|
||||
router = context["router"]
|
||||
log.info("{} has received Idle-PC proposals".format(router.name()))
|
||||
log.debug("{} has received Idle-PC proposals".format(router.name()))
|
||||
idlepcs = result
|
||||
if idlepcs and idlepcs[0] != "0x0":
|
||||
dialog = IdlePCDialog(router, idlepcs, parent=self)
|
||||
@@ -1266,7 +1303,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
router = context["router"]
|
||||
idlepc = result["idlepc"]
|
||||
log.info("{} has received the auto idle-pc value: {}".format(router.name(), idlepc))
|
||||
log.debug("{} has received the auto idle-pc value: {}".format(router.name(), idlepc))
|
||||
router.setIdlepc(idlepc)
|
||||
# apply Idle-PC to all routers with the same IOS image
|
||||
ios_image = os.path.basename(router.settings()["image"])
|
||||
@@ -1296,6 +1333,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
type = "image"
|
||||
self.createDrawingItem(type, item.pos().x() + 20, item.pos().y() + 20, item.zValue(), rotation=item.rotation(), svg=item.toSvg())
|
||||
elif isinstance(item, NodeItem):
|
||||
item.node().duplicate(item.pos().x() + 20, item.pos().y() + 20, item.zValue())
|
||||
|
||||
def styleActionSlot(self):
|
||||
"""
|
||||
@@ -1305,7 +1344,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, ShapeItem):
|
||||
if isinstance(item, ShapeItem) or isinstance(item, LineItem):
|
||||
items.append(item)
|
||||
if items:
|
||||
style_dialog = StyleEditorDialog(self._main_window, items)
|
||||
@@ -1356,7 +1395,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if item.parentItem() is None:
|
||||
if horizontal_pos is None:
|
||||
horizontal_pos = item.y() + item.boundingRect().height() / 2
|
||||
item.setPos(item.x(), horizontal_pos - item.boundingRect().height() / 2)
|
||||
item.setX(item.x())
|
||||
item.setY(horizontal_pos - item.boundingRect().height() / 2)
|
||||
item.updateNode()
|
||||
|
||||
def verticalAlignmentSlot(self):
|
||||
"""
|
||||
@@ -1369,7 +1410,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if item.parentItem() is None:
|
||||
if vertical_position is None:
|
||||
vertical_position = item.x() + item.boundingRect().width() / 2
|
||||
item.setPos(vertical_position - item.boundingRect().width() / 2, item.y())
|
||||
item.setX(vertical_position - item.boundingRect().width() / 2)
|
||||
item.setY(item.y())
|
||||
item.updateNode()
|
||||
|
||||
def raiseLayerActionSlot(self):
|
||||
"""
|
||||
@@ -1381,6 +1424,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if item.parentItem() is None:
|
||||
current_zvalue = item.zValue()
|
||||
item.setZValue(current_zvalue + 1)
|
||||
item.updateNode()
|
||||
item.update()
|
||||
|
||||
def lowerLayerActionSlot(self):
|
||||
@@ -1393,6 +1437,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if item.parentItem() is None:
|
||||
current_zvalue = item.zValue()
|
||||
item.setZValue(current_zvalue - 1)
|
||||
item.updateNode()
|
||||
item.update()
|
||||
|
||||
if item.zValue() == -1:
|
||||
@@ -1424,6 +1469,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
elif item.parentItem() is None:
|
||||
item.delete()
|
||||
|
||||
self.scene().clearSelection()
|
||||
self.toggleUiDeviceMenu()
|
||||
|
||||
def allocateCompute(self, node_data, module_instance):
|
||||
"""
|
||||
Allocates a server.
|
||||
@@ -1434,75 +1482,47 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
mainwindow = MainWindow.instance()
|
||||
|
||||
if "server" in node_data:
|
||||
return ComputeManager.instance().getCompute(node_data["server"])
|
||||
try:
|
||||
return ComputeManager.instance().getCompute(node_data["server"])
|
||||
except KeyError:
|
||||
raise ModuleError("Compute {} doesn't exists".format(node_data["server"]))
|
||||
|
||||
if "builtin" in node_data:
|
||||
allow_local_server = True
|
||||
else:
|
||||
allow_local_server = module_instance.settings()["use_local_server"]
|
||||
server = server_select(mainwindow, node_data.get("node_type"), allow_local_server=allow_local_server)
|
||||
server = server_select(mainwindow, node_data.get("node_type"))
|
||||
if server is None:
|
||||
raise ModuleError("Please select a server")
|
||||
return server
|
||||
|
||||
def createNode(self, node_data, pos):
|
||||
def createNodeFromApplianceId(self, appliance_id, pos):
|
||||
"""
|
||||
Creates a new node on the scene.
|
||||
|
||||
:param node_data: node data to create a new node
|
||||
:param pos: position of the drop event
|
||||
|
||||
:returns: NodeItem instance
|
||||
Ask the server to create a node using this appliance
|
||||
"""
|
||||
try:
|
||||
node_module = None
|
||||
for module in MODULES:
|
||||
instance = module.instance()
|
||||
node_class = module.getNodeClass(node_data["class"])
|
||||
if node_class in instance.classes():
|
||||
node_module = instance
|
||||
break
|
||||
|
||||
if not node_module:
|
||||
raise ModuleError("Could not find any module for {}".format(node_class))
|
||||
|
||||
node = node_module.instantiateNode(node_class, self.allocateCompute(node_data, instance), self._topology.project())
|
||||
# If no server is available a ValueError is raised
|
||||
except (ModuleError, ValueError) as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Node creation", "{}".format(e))
|
||||
return
|
||||
|
||||
pos = self.mapToScene(pos)
|
||||
node_item = self.createNodeItem(node, node_data["symbol"], pos.x(), pos.y())
|
||||
node.setGraphics(node_item)
|
||||
node_module.createNode(node, node_data["name"])
|
||||
return node_item
|
||||
return ApplianceManager().instance().createNodeFromApplianceId(self._topology.project(), appliance_id, pos.x(), pos.y())
|
||||
|
||||
def createNodeItem(self, node, symbol, x, y):
|
||||
node.setSymbol(symbol)
|
||||
node.setPos(x, y)
|
||||
node_item = NodeItem(node)
|
||||
|
||||
self.scene().addItem(node_item)
|
||||
self._topology.addNode(node)
|
||||
|
||||
node.error_signal.connect(self._main_window.uiConsoleTextEdit.writeError)
|
||||
node.error_signal.connect(self._displayNodeErrorSlot)
|
||||
node.warning_signal.connect(self._main_window.uiConsoleTextEdit.writeWarning)
|
||||
node.server_error_signal.connect(self._main_window.uiConsoleTextEdit.writeServerError)
|
||||
node.server_error_signal.connect(self._displayNodeErrorSlot)
|
||||
|
||||
return node_item
|
||||
|
||||
def _displayNodeErrorSlot(self, node_id, message):
|
||||
@qslot
|
||||
def _displayNodeErrorSlot(self, node_id, message, *args):
|
||||
"""
|
||||
Show error send by a node to the user
|
||||
"""
|
||||
node = Topology.instance().getNode(node_id)
|
||||
name = "Node"
|
||||
if node:
|
||||
if node.name():
|
||||
name = node.name()
|
||||
QtWidgets.QMessageBox.critical(self._main_window, name, message.strip())
|
||||
if node and node.name():
|
||||
name = node.name()
|
||||
if self._main_window and not sip.isdeleted(self._main_window):
|
||||
QtWidgets.QMessageBox.critical(self._main_window, name, message.strip())
|
||||
|
||||
def createDrawingItem(self, type, x, y, z, rotation=0, svg=None, drawing_id=None):
|
||||
|
||||
@@ -1510,6 +1530,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
item = EllipseItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "rect":
|
||||
item = RectangleItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "line":
|
||||
item = LineItem(pos=QtCore.QPoint(x, y), dst=QtCore.QPoint(200, 0), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "image":
|
||||
item = ImageItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "text":
|
||||
@@ -1541,3 +1563,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
painter.drawLine(rect.left(), y, rect.right(), y)
|
||||
y += gridSize
|
||||
painter.restore()
|
||||
|
||||
def toggleUiDeviceMenu(self):
|
||||
""" Hook which enables/disables uiDeviceMenu based on the current items selection"""
|
||||
items = self.scene().selectedItems()
|
||||
if len(items) > 0:
|
||||
self._main_window.uiDeviceMenu.setEnabled(True)
|
||||
else:
|
||||
self._main_window.uiDeviceMenu.setEnabled(False)
|
||||
|
||||
@@ -15,18 +15,21 @@
|
||||
# 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 sip
|
||||
import json
|
||||
import copy
|
||||
import ipaddress
|
||||
import http
|
||||
import uuid
|
||||
import pathlib
|
||||
import urllib.request
|
||||
import base64
|
||||
import datetime
|
||||
import ipaddress
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
|
||||
from .version import __version__, __version_info__
|
||||
from .qt import QtCore, QtNetwork, qpartial
|
||||
from .qt import QtCore, QtNetwork, qpartial, sip_is_deleted, QtWebSockets
|
||||
from .utils import parse_version
|
||||
|
||||
import logging
|
||||
@@ -48,42 +51,53 @@ class HTTPClient(QtCore.QObject):
|
||||
:param network_manager: A QT network manager
|
||||
"""
|
||||
|
||||
# How many times we need to retry a connection
|
||||
MAX_RETRY_CONNECTION = 5
|
||||
|
||||
# Callback class used for displaying progress
|
||||
_progress_callback = None
|
||||
|
||||
connection_connected_signal = QtCore.Signal()
|
||||
connection_disconnected_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, settings, network_manager=None):
|
||||
|
||||
def __init__(self, settings, network_manager=None, max_retry_connection=5):
|
||||
super().__init__()
|
||||
|
||||
self._protocol = settings.get("protocol", "http")
|
||||
self._host = settings["host"]
|
||||
if self._host == "0.0.0.0":
|
||||
self._host = "127.0.0.1"
|
||||
try:
|
||||
if self._host is None or self._host == "0.0.0.0":
|
||||
self._host = "127.0.0.1"
|
||||
elif ":" in self._host and ipaddress.IPv6Address(self._host) and str(ipaddress.IPv6Address(self._host)) == "::":
|
||||
self._host = "::1"
|
||||
except ipaddress.AddressValueError:
|
||||
log.error("Invalid host name %s", self._host)
|
||||
self._port = int(settings["port"])
|
||||
self._user = settings.get("user", None)
|
||||
self._password = settings.get("password", None)
|
||||
# How many time we have retry connection
|
||||
# How many time we have already retried connection
|
||||
self._retry = 0
|
||||
self._max_retry_connection = max_retry_connection
|
||||
self._connected = False
|
||||
self._shutdown = False # Shutdown in progress
|
||||
self._accept_insecure_certificate = settings.get("accept_insecure_certificate", None)
|
||||
|
||||
# In order to detect computer hibernation we detect the date of the last
|
||||
# query and disconnect if time is too long between two query
|
||||
self._last_query_timestamp = None
|
||||
self._max_time_difference_between_queries = None
|
||||
if network_manager:
|
||||
self._network_manager = network_manager
|
||||
else:
|
||||
self._network_manager = QtNetwork.QNetworkAccessManager()
|
||||
|
||||
# A buffer used by progress download
|
||||
self._buffer = {}
|
||||
|
||||
# List of query waiting for the connection
|
||||
self._query_waiting_connections = []
|
||||
|
||||
self._websocket = QtWebSockets.QWebSocket()
|
||||
|
||||
def setMaxTimeDifferenceBetweenQueries(self, value):
|
||||
self._max_time_difference_between_queries = value
|
||||
|
||||
def host(self):
|
||||
"""
|
||||
Host display to user
|
||||
@@ -125,15 +139,20 @@ class HTTPClient(QtCore.QObject):
|
||||
def url(self):
|
||||
"""Returns current server url"""
|
||||
|
||||
if ":" in self.host():
|
||||
return "{}://[{}]:{}".format(self.protocol(), self.host(), self.port())
|
||||
return "{}://{}:{}".format(self.protocol(), self.host(), self.port())
|
||||
|
||||
def fullUrl(self):
|
||||
"""Returns current server url including user and password"""
|
||||
host = self.host()
|
||||
if ":" in self.host():
|
||||
host = "[{}]".format(host)
|
||||
|
||||
if self._user:
|
||||
return "{}://{}:{}@{}:{}".format(self.protocol(), self._user, self._password, self.host(), self.port())
|
||||
return "{}://{}:{}@{}:{}".format(self.protocol(), self._user, self._password, host, self.port())
|
||||
else:
|
||||
return "{}://{}:{}".format(self.protocol(), self.host(), self.port())
|
||||
return "{}://{}:{}".format(self.protocol(), host, self.port())
|
||||
|
||||
def password(self):
|
||||
return self._password
|
||||
@@ -148,11 +167,30 @@ class HTTPClient(QtCore.QObject):
|
||||
self.createHTTPQuery("POST", "/shutdown", None, showProgress=False)
|
||||
self._shutdown = True
|
||||
|
||||
def getNetworkManager(self):
|
||||
"""
|
||||
:return: instance of NetworkManager
|
||||
"""
|
||||
return self._network_manager
|
||||
|
||||
def setMaxRetryConnection(self, retries):
|
||||
"""
|
||||
Sets how many times we need to retry a connection
|
||||
:param retries: integer
|
||||
"""
|
||||
self._max_retry_connection = retries
|
||||
|
||||
def getMaxRetryConnection(self):
|
||||
"""
|
||||
Returns how many times we need to retry a connection
|
||||
"""
|
||||
return self._max_retry_connection
|
||||
|
||||
def _notify_progress_start_query(self, query_id, progress_text, response):
|
||||
"""
|
||||
Called when a query start
|
||||
"""
|
||||
if HTTPClient._progress_callback:
|
||||
if not sip_is_deleted(HTTPClient._progress_callback):
|
||||
if progress_text:
|
||||
HTTPClient._progress_callback.add_query_signal.emit(query_id, progress_text, response)
|
||||
else:
|
||||
@@ -163,22 +201,24 @@ class HTTPClient(QtCore.QObject):
|
||||
Called when a query is over
|
||||
"""
|
||||
|
||||
if HTTPClient._progress_callback:
|
||||
if not sip_is_deleted(HTTPClient._progress_callback):
|
||||
HTTPClient._progress_callback.remove_query_signal.emit(query_id)
|
||||
|
||||
def _notify_progress_upload(self, query_id, sent, total):
|
||||
"""
|
||||
Called when a query upload progress
|
||||
"""
|
||||
if HTTPClient._progress_callback:
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
|
||||
if not sip_is_deleted(HTTPClient._progress_callback):
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(total))
|
||||
|
||||
def _notify_progress_download(self, query_id, sent, total):
|
||||
"""
|
||||
Called when a query download progress
|
||||
"""
|
||||
if HTTPClient._progress_callback:
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
|
||||
if not sip_is_deleted(HTTPClient._progress_callback):
|
||||
# abs() for maxium because sometimes the system send negative
|
||||
# values
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(abs(total)))
|
||||
|
||||
@classmethod
|
||||
def setProgressCallback(cls, progress_callback):
|
||||
@@ -201,6 +241,7 @@ class HTTPClient(QtCore.QObject):
|
||||
Closes the connection with the server.
|
||||
"""
|
||||
self._connected = False
|
||||
self._progress_callback.reset()
|
||||
|
||||
def _request(self, url):
|
||||
"""
|
||||
@@ -221,7 +262,18 @@ class HTTPClient(QtCore.QObject):
|
||||
:param query: The Server to connect
|
||||
"""
|
||||
|
||||
def createHTTPQuery(self, method, path, callback, body={}, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, timeout=120, server=None, prefix="/v2", params={}, **kwargs):
|
||||
def createHTTPQuery(self, method, path, callback, body={}, context={},
|
||||
downloadProgressCallback=None,
|
||||
showProgress=True,
|
||||
ignoreErrors=False,
|
||||
progressText=None,
|
||||
timeout=120,
|
||||
server=None,
|
||||
prefix="/v2",
|
||||
params={},
|
||||
networkManager=None,
|
||||
eventsHandler=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Call the remote server, if not connected, check connection before
|
||||
|
||||
@@ -237,24 +289,51 @@ class HTTPClient(QtCore.QObject):
|
||||
:param server: The server where the query will run
|
||||
:param timeout: Delay in seconds before raising a timeout
|
||||
:param prefix: Prefix to the path
|
||||
:param networkManager: QNetworkAccessManager None use the default
|
||||
:param eventsHandler: Handler receiving and triggering events like `updated`, `cancelled`.
|
||||
If not specified and showProgress is `True` then `ProgressDialog` receives them.
|
||||
:param params: Query arguments parameters
|
||||
:returns: QNetworkReply
|
||||
"""
|
||||
|
||||
if "dev" in __version__:
|
||||
assert QtCore.QThread.currentThread() == self.thread(), "HTTP request not started from the main thread"
|
||||
|
||||
# Shutdown in progress do not execute the query
|
||||
if self._shutdown:
|
||||
return
|
||||
|
||||
request = qpartial(self._executeHTTPQuery, method, path, qpartial(callback), body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText, server=server, timeout=timeout, prefix=prefix, params=params)
|
||||
# We try to detect computer hibernation
|
||||
# if time between two query is too long we trigger a disconnect
|
||||
if self._max_time_difference_between_queries:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
if self._last_query_timestamp is not None and now > self._last_query_timestamp + self._max_time_difference_between_queries:
|
||||
log.warning("Synchronisation lost with the server.")
|
||||
self.disconnect()
|
||||
self._last_query_timestamp = None
|
||||
return
|
||||
self._last_query_timestamp = now
|
||||
|
||||
request = qpartial(self._executeHTTPQuery, method, path, qpartial(callback), body, context,
|
||||
downloadProgressCallback=downloadProgressCallback,
|
||||
showProgress=showProgress,
|
||||
ignoreErrors=ignoreErrors,
|
||||
progressText=progressText,
|
||||
networkManager=networkManager,
|
||||
server=server,
|
||||
timeout=timeout,
|
||||
prefix=prefix,
|
||||
eventsHandler=eventsHandler,
|
||||
params=params)
|
||||
|
||||
if self._connected:
|
||||
return request()
|
||||
else:
|
||||
self._query_waiting_connections.append((request, callback))
|
||||
# If we are not connected and we enqueue the first query we open the conection
|
||||
# enqueue the first query and open the connection if we are not connected
|
||||
if len(self._query_waiting_connections) == 1:
|
||||
log.info("Connection to {}".format(self.url()))
|
||||
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=5)
|
||||
log.debug("Connection to {}".format(self.url()))
|
||||
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=10, showProgress=False)
|
||||
|
||||
def _connectionError(self, callback, msg="", server=None):
|
||||
"""
|
||||
@@ -268,10 +347,10 @@ class HTTPClient(QtCore.QObject):
|
||||
if len(msg) > 0:
|
||||
msg = "Cannot connect to server {}: {}".format(self.url(), msg)
|
||||
else:
|
||||
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall.".format(self.url())
|
||||
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall. And that server version is {}.".format(self.url(), __version__)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
callback({"message": msg}, error=True, server=server, connection_error=True)
|
||||
self._query_waiting_connections = []
|
||||
|
||||
def _retryConnection(self, server=None):
|
||||
@@ -291,7 +370,7 @@ class HTTPClient(QtCore.QObject):
|
||||
"""
|
||||
|
||||
if error is not False:
|
||||
if self._retry < self.MAX_RETRY_CONNECTION:
|
||||
if self._retry < self.getMaxRetryConnection():
|
||||
self._retryConnection(server=server)
|
||||
return
|
||||
for request, callback in self._query_waiting_connections:
|
||||
@@ -300,7 +379,7 @@ class HTTPClient(QtCore.QObject):
|
||||
return
|
||||
|
||||
if "version" not in params or "local" not in params:
|
||||
if self._retry < self.MAX_RETRY_CONNECTION:
|
||||
if self._retry < self.getMaxRetryConnection():
|
||||
self._retryConnection(server=server)
|
||||
return
|
||||
msg = "The remote server {} is not a GNS3 server".format(self.url())
|
||||
@@ -311,22 +390,23 @@ class HTTPClient(QtCore.QObject):
|
||||
self._query_waiting_connections = []
|
||||
return
|
||||
|
||||
if params["version"] != __version__:
|
||||
msg = "Client version {} differs with server version {}".format(__version__, params["version"])
|
||||
log.error(msg)
|
||||
if params["version"].split("-")[0] != __version__.split("-")[0]:
|
||||
msg = "Client version {} is not the same as server version {}".format(__version__, params["version"])
|
||||
# Stable release
|
||||
if __version_info__[3] == 0:
|
||||
log.error(msg)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
return
|
||||
# We don't allow different major version to interact even with dev build
|
||||
elif parse_version(__version__)[:2] != parse_version(params["version"])[:2]:
|
||||
log.error(msg)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
return
|
||||
log.warning("Use a different client and server version can create bugs. Use it at your own risk.")
|
||||
log.warning("{}\nUsing different versions may result in unexpected problems. Please use at your own risk.".format(msg))
|
||||
|
||||
self._connected = True
|
||||
self._retry = 0
|
||||
@@ -385,7 +465,47 @@ class HTTPClient(QtCore.QObject):
|
||||
request.setRawHeader(b"Authorization", auth_string.encode())
|
||||
return request
|
||||
|
||||
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, **kwargs):
|
||||
def connectWebSocket(self, path, prefix="/v2"):
|
||||
"""
|
||||
Path of the websocket endpoint
|
||||
"""
|
||||
host = self._getHostForQuery()
|
||||
request = self._websocket.request()
|
||||
request.setUrl(QtCore.QUrl("ws://{host}:{port}{prefix}{path}".format(host=host, port=self._port, path=path, prefix=prefix)))
|
||||
self._addAuth(request)
|
||||
self._websocket.open(request)
|
||||
return self._websocket
|
||||
|
||||
def _getHostForQuery(self):
|
||||
"""
|
||||
Get hostname that could be use by Qt
|
||||
"""
|
||||
try:
|
||||
ip = self._host.rsplit('%', 1)[0]
|
||||
ipaddress.IPv6Address(ip) # remove any scope ID
|
||||
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
|
||||
host = "[{}]".format(ip)
|
||||
except ipaddress.AddressValueError:
|
||||
host = self._host
|
||||
return host
|
||||
|
||||
def _paramsToQueryString(self, params):
|
||||
"""
|
||||
:param params: Dictionnary of query string parameters
|
||||
:returns: String of the query string
|
||||
"""
|
||||
if params == {}:
|
||||
query_string = ""
|
||||
else:
|
||||
query_string = "?"
|
||||
params = params.copy()
|
||||
for key, value in params.copy().items():
|
||||
if value is None:
|
||||
del params[key]
|
||||
query_string += urllib.parse.urlencode(params)
|
||||
return query_string
|
||||
|
||||
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, networkManager=None, eventsHandler=None, **kwargs):
|
||||
"""
|
||||
Call the remote server
|
||||
|
||||
@@ -396,30 +516,19 @@ class HTTPClient(QtCore.QObject):
|
||||
:param context: Pass a context to the response callback
|
||||
:param downloadProgressCallback: Callback called when received something, it can be an incomplete response
|
||||
:param showProgress: Display progress to the user
|
||||
:param networkManager: The network manager to use. If None use default
|
||||
:param progressText: Text display to user in progress dialog. None for auto generated
|
||||
:param ignoreErrors: Ignore connection error (usefull to not closing a connection when notification feed is broken)
|
||||
:param server: The server where the query is executed
|
||||
:param timeout: Delay in seconds before raising a timeout
|
||||
:param eventsHandler: Handler receiving and triggering events like `updated`, `cancelled`.
|
||||
If not specified and showProgress is `True` then `ProgressDialog` receives them.
|
||||
:param params: Query arguments parameters
|
||||
:returns: QNetworkReply
|
||||
"""
|
||||
|
||||
# TODO: remove it when all call are migrated
|
||||
if "compute/" in path:
|
||||
log.warning("Legacy compute direct call %s", path)
|
||||
|
||||
try:
|
||||
ip = self._host.rsplit('%', 1)[0]
|
||||
ipaddress.IPv6Address(ip) # remove any scope ID
|
||||
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
|
||||
host = "[{}]".format(ip)
|
||||
except ipaddress.AddressValueError:
|
||||
host = self._host
|
||||
|
||||
if params == {}:
|
||||
query_string = ""
|
||||
else:
|
||||
query_string = "?" + urllib.parse.urlencode(params)
|
||||
host = self._getHostForQuery()
|
||||
query_string = self._paramsToQueryString(params)
|
||||
|
||||
log.debug("{method} {protocol}://{host}:{port}{prefix}{path} {body}{query_string}".format(method=method, protocol=self._protocol, host=host, port=self._port, path=path, body=body, prefix=prefix, query_string=query_string))
|
||||
if self._user:
|
||||
@@ -435,18 +544,29 @@ class HTTPClient(QtCore.QObject):
|
||||
# By default QT doesn't support GET with body even if it's in the RFC that's why we need to use sendCustomRequest
|
||||
body = self._addBodyToRequest(body, request)
|
||||
|
||||
response = self._network_manager.sendCustomRequest(request, method.encode(), body)
|
||||
if not networkManager:
|
||||
networkManager = self._network_manager
|
||||
|
||||
try:
|
||||
response = networkManager.sendCustomRequest(request, method.encode(), body)
|
||||
except SystemError as e:
|
||||
log.error("Can't send query: {}".format(str(e)))
|
||||
return
|
||||
|
||||
context = copy.copy(context)
|
||||
context["query_id"] = str(uuid.uuid4())
|
||||
|
||||
response.finished.connect(qpartial(self._processResponse, response, server, callback, context, body, ignoreErrors))
|
||||
response.error.connect(qpartial(self._processError, response, server, callback, context, body, ignoreErrors))
|
||||
|
||||
if downloadProgressCallback is not None:
|
||||
response.readyRead.connect(qpartial(self._readyReadySlot, response, downloadProgressCallback, context, server))
|
||||
|
||||
if HTTPClient._progress_callback and HTTPClient._progress_callback.progress_dialog():
|
||||
request_canceled = qpartial(self._requestCanceled, response, context)
|
||||
request_canceled = qpartial(self._requestCanceled, response, context)
|
||||
|
||||
if eventsHandler is not None:
|
||||
eventsHandler.canceled.connect(request_canceled)
|
||||
elif not sip_is_deleted(HTTPClient._progress_callback) and HTTPClient._progress_callback.progress_dialog():
|
||||
HTTPClient._progress_callback.progress_dialog().canceled.connect(request_canceled)
|
||||
|
||||
if showProgress:
|
||||
@@ -457,7 +577,7 @@ class HTTPClient(QtCore.QObject):
|
||||
self._notify_progress_start_query(context["query_id"], progressText, response)
|
||||
|
||||
if timeout is not None:
|
||||
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response))
|
||||
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response, timeout))
|
||||
|
||||
return response
|
||||
|
||||
@@ -492,45 +612,48 @@ class HTTPClient(QtCore.QObject):
|
||||
else:
|
||||
callback(content, server=server, context=context)
|
||||
|
||||
def _timeoutSlot(self, response):
|
||||
def _timeoutSlot(self, response, timeout):
|
||||
"""
|
||||
Beware it's call for all request you need to check the status of the response
|
||||
"""
|
||||
# We check if we received HTTP headers
|
||||
if not len(response.rawHeaderList()) > 0:
|
||||
response.abort()
|
||||
if not sip.isdeleted(response) and response.isRunning() and not len(response.rawHeaderList()) > 0:
|
||||
if not response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
log.warning("Timeout after {} seconds for request {}".format(timeout, response.url().toString()))
|
||||
response.abort()
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
Disconnect from the remote server
|
||||
"""
|
||||
self.connection_disconnected_signal.emit()
|
||||
self.close()
|
||||
|
||||
def _requestCanceled(self, response, context):
|
||||
|
||||
if response.isRunning():
|
||||
log.warn("Aborting request for {}".format(response.url()))
|
||||
if response.isRunning() and not response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
log.warn("Aborting request for {}".format(response.url().toString()))
|
||||
response.abort()
|
||||
if "query_id" in context:
|
||||
self._notify_progress_end_query(context["query_id"])
|
||||
|
||||
def _processResponse(self, response, server, callback, context, request_body, ignore_errors):
|
||||
|
||||
if request_body is not None:
|
||||
request_body.close()
|
||||
|
||||
status = None
|
||||
body = None
|
||||
|
||||
if "query_id" in context:
|
||||
self._notify_progress_end_query(context["query_id"])
|
||||
|
||||
if response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
error_code = response.error()
|
||||
def _processError(self, response, server, callback, context, request_body, ignore_errors, error_code):
|
||||
if error_code != QtNetwork.QNetworkReply.NoError:
|
||||
error_message = response.errorString()
|
||||
|
||||
if not ignore_errors:
|
||||
log.debug("Response error: %s (error: %d)", error_message, error_code)
|
||||
log.debug("Response error: %s for %s (error: %d)", error_message, response.url().toString(), error_code)
|
||||
|
||||
if error_code < 200:
|
||||
if not ignore_errors:
|
||||
self.close()
|
||||
if callback is not None:
|
||||
callback({"message": error_message}, error=True, server=server, context=context)
|
||||
if "query_id" in context:
|
||||
self._notify_progress_end_query(context["query_id"])
|
||||
|
||||
if error_code < 200 or error_code == 403:
|
||||
if error_code == QtNetwork.QNetworkReply.OperationCanceledError: # It's legit to cancel do not disconnect
|
||||
error_message = "Operation timeout" # It's more clear than cancel, because cancel is trigger by us when we timeout
|
||||
elif not ignore_errors:
|
||||
self.disconnect()
|
||||
if callback is not None:
|
||||
callback({"message": error_message}, error=True, server=server, context=context)
|
||||
return
|
||||
else:
|
||||
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
@@ -547,7 +670,7 @@ class HTTPClient(QtCore.QObject):
|
||||
if not body or content_type != "application/json":
|
||||
callback({"message": error_message}, error=True, server=server, context=context)
|
||||
else:
|
||||
log.debug(body)
|
||||
# log.debug(body)
|
||||
try:
|
||||
callback(json.loads(body), error=True, server=server, context=context)
|
||||
except ValueError:
|
||||
@@ -559,7 +682,15 @@ class HTTPClient(QtCore.QObject):
|
||||
log.error(json.loads(body)["message"])
|
||||
except (ValueError, KeyError):
|
||||
log.error(error_message)
|
||||
else:
|
||||
|
||||
def _processResponse(self, response, server, callback, context, request_body, ignore_errors):
|
||||
if request_body is not None:
|
||||
request_body.close()
|
||||
|
||||
if "query_id" in context:
|
||||
self._notify_progress_end_query(context["query_id"])
|
||||
|
||||
if response.error() == QtNetwork.QNetworkReply.NoError:
|
||||
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
log.debug("Decoding response from {} response {}".format(response.url().toString(), status))
|
||||
try:
|
||||
@@ -569,9 +700,12 @@ class HTTPClient(QtCore.QObject):
|
||||
except UnicodeDecodeError:
|
||||
body = None
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
log.debug(body)
|
||||
if body and len(body.strip(" \n\t")) > 0 and content_type == "application/json":
|
||||
params = json.loads(body)
|
||||
try:
|
||||
params = json.loads(body)
|
||||
except ValueError: # Partial JSON
|
||||
params = {}
|
||||
status = 504
|
||||
else:
|
||||
params = {}
|
||||
if callback is not None:
|
||||
@@ -579,16 +713,15 @@ class HTTPClient(QtCore.QObject):
|
||||
callback(params, error=True, server=server, context=context)
|
||||
else:
|
||||
callback(params, server=server, context=context, raw_body=raw_body)
|
||||
# response.deleteLater()
|
||||
if status == 400:
|
||||
try:
|
||||
params = json.loads(body)
|
||||
e = HttpBadRequest(body)
|
||||
e.fingerprint = params["path"]
|
||||
# If something goes wrong for a any reason just raise the bad request
|
||||
except Exception:
|
||||
e = HttpBadRequest(body)
|
||||
raise e
|
||||
if status == 400:
|
||||
try:
|
||||
params = json.loads(body)
|
||||
e = HttpBadRequest(body)
|
||||
e.fingerprint = params["path"]
|
||||
# If something goes wrong for a any reason just raise the bad request
|
||||
except Exception:
|
||||
e = HttpBadRequest(body)
|
||||
raise e
|
||||
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
"""
|
||||
@@ -610,7 +743,6 @@ class HTTPClient(QtCore.QObject):
|
||||
urllib.request.install_opener(opener)
|
||||
else:
|
||||
log.debug("Synchronous get {} (no authentication)".format(url))
|
||||
|
||||
response = urllib.request.urlopen(url, timeout=timeout)
|
||||
content_type = response.getheader("CONTENT-TYPE")
|
||||
if response.status == 200:
|
||||
@@ -632,3 +764,23 @@ class HTTPClient(QtCore.QObject):
|
||||
except (OSError, http.client.BadStatusLine, ValueError) as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return 0, None
|
||||
|
||||
@classmethod
|
||||
def fromUrl(cls, url, network_manager=None, base_settings=None):
|
||||
"""
|
||||
Returns HttpClient instance based on the url
|
||||
:param url: Url to parse
|
||||
:param network_manager: Optional network_manager
|
||||
:param base_settings: Source of the settings, if necessary
|
||||
:return: HttpClient
|
||||
"""
|
||||
settings = {}
|
||||
if base_settings is not None:
|
||||
settings.update(**base_settings)
|
||||
parse_results = urllib.parse.urlparse(url)
|
||||
settings['protocol'] = parse_results.scheme
|
||||
settings['host'] = parse_results.hostname
|
||||
settings['port'] = parse_results.port
|
||||
settings['user'] = parse_results.username
|
||||
settings['password'] = parse_results.password
|
||||
return cls(settings, network_manager)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
import os
|
||||
import copy
|
||||
import pathlib
|
||||
import glob
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
@@ -26,6 +25,7 @@ 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
|
||||
from gns3.registry.image import Image
|
||||
|
||||
|
||||
class ImageManager:
|
||||
@@ -34,7 +34,29 @@ 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, node_type):
|
||||
def _getUniqueDestinationPath(self, source_image, node_type, path):
|
||||
"""
|
||||
Get a unique destination path (with counter).
|
||||
"""
|
||||
|
||||
if not os.path.exists(path):
|
||||
return path
|
||||
path, extension = os.path.splitext(path)
|
||||
counter = 1
|
||||
new_path = "{}-{}{}".format(path, counter, extension)
|
||||
while os.path.exists(new_path):
|
||||
destination_image = Image(node_type, new_path, filename=os.path.basename(new_path))
|
||||
try:
|
||||
if source_image.md5sum == destination_image.md5sum:
|
||||
# the source and destination images are identical
|
||||
return new_path
|
||||
except OSError:
|
||||
continue
|
||||
counter += 1
|
||||
new_path = "{}-{}{}".format(path, counter, extension)
|
||||
return new_path
|
||||
|
||||
def askCopyUploadImage(self, parent, source_path, server, node_type):
|
||||
"""
|
||||
Ask user for copying the image to the default directory or upload
|
||||
it to remote server.
|
||||
@@ -46,35 +68,52 @@ class ImageManager:
|
||||
:returns path: Final path
|
||||
"""
|
||||
|
||||
if server and server != "local":
|
||||
return self._uploadImageToRemoteServer(path, server, node_type)
|
||||
if (server and server != "local") or Controller.instance().isRemote():
|
||||
return self._uploadImageToRemoteServer(source_path, server, node_type)
|
||||
else:
|
||||
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
|
||||
destination_path = os.path.join(destination_directory, os.path.basename(source_path))
|
||||
source_filename = os.path.basename(source_path)
|
||||
destination_filename = os.path.basename(destination_path)
|
||||
if os.path.normpath(os.path.dirname(source_path)) != destination_directory:
|
||||
# the image is not in the default images directory
|
||||
if source_filename == destination_filename:
|
||||
# the filename already exists in the default images directory
|
||||
source_image = Image(node_type, source_path, filename=source_filename)
|
||||
destination_image = Image(node_type, destination_path, filename=destination_filename)
|
||||
try:
|
||||
if source_image.md5sum == destination_image.md5sum:
|
||||
# the source and destination images are identical
|
||||
return source_path
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(parent, 'Image', 'Cannot compare image file {} with {}: {}.'.format(source_path, destination_path, str(e)))
|
||||
return source_path
|
||||
# find a new unique path to avoid overwriting existing destination file
|
||||
destination_path = self._getUniqueDestinationPath(source_image, node_type, destination_path)
|
||||
|
||||
reply = QtWidgets.QMessageBox.question(parent,
|
||||
'Image',
|
||||
'Would you like to copy {} to the default images directory'.format(os.path.basename(path)),
|
||||
'Would you like to copy {} to the default images directory'.format(source_filename),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
destination_path = os.path.join(destination_directory, os.path.basename(path))
|
||||
try:
|
||||
os.makedirs(destination_directory, exist_ok=True)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(parent, 'Image', 'Could not create destination directory {}: {}'.format(destination_directory, str(e)))
|
||||
return path
|
||||
worker = FileCopyWorker(path, destination_path)
|
||||
progress_dialog = ProgressDialog(worker, 'Image', 'Copying {}'.format(os.path.basename(path)), 'Cancel', busy=True, parent=parent)
|
||||
return source_path
|
||||
|
||||
worker = FileCopyWorker(source_path, destination_path)
|
||||
progress_dialog = ProgressDialog(worker, 'Image', 'Copying {}'.format(source_filename), 'Cancel', busy=True, parent=parent)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
errors = progress_dialog.errors()
|
||||
if errors:
|
||||
QtWidgets.QMessageBox.critical(parent, 'Image', '{}'.format(''.join(errors)))
|
||||
return path
|
||||
return source_path
|
||||
else:
|
||||
path = destination_path
|
||||
return path
|
||||
source_path = destination_path
|
||||
return source_path
|
||||
|
||||
def _uploadImageToRemoteServer(self, path, server, node_type):
|
||||
"""
|
||||
@@ -96,22 +135,9 @@ class ImageManager:
|
||||
raise Exception('Invalid node type')
|
||||
|
||||
filename = self._getRelativeImagePath(path, node_type).replace("\\", "/")
|
||||
|
||||
Controller.instance().post(r'/computes/{}{}/{}'.format(server, upload_endpoint, filename), None, body=pathlib.Path(path), progressText="Uploading {}".format(filename), timeout=None)
|
||||
Controller.instance().postCompute('{}/{}'.format(upload_endpoint, filename), server, None, body=pathlib.Path(path), progressText="Uploading {}".format(filename), timeout=None)
|
||||
return filename
|
||||
|
||||
def _askForUploadMissingImage(self, filename, server):
|
||||
from gns3.main_window import MainWindow
|
||||
parent = MainWindow.instance()
|
||||
reply = QtWidgets.QMessageBox.warning(parent,
|
||||
'Image',
|
||||
'{} is missing on server {} but exist on your computer. Do you want to upload it?'.format(filename, server.url()),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _getRelativeImagePath(self, path, node_type):
|
||||
"""
|
||||
Get a path relative to images directory path
|
||||
|
||||
91
gns3/image_upload_manager.py
Normal file
91
gns3/image_upload_manager.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2017 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 pathlib
|
||||
import urllib.parse
|
||||
|
||||
from gns3.http_client import HTTPClient
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageUploadManager(object):
|
||||
"""
|
||||
Manager over the image upload. Encapsulates file uploads to computes or via controller.
|
||||
"""
|
||||
|
||||
def __init__(self, image, controller, compute_id, callback=None, directFileUpload=False):
|
||||
self._image = image
|
||||
self._compute_id = compute_id
|
||||
self._callback = callback
|
||||
self._directFileUpload = directFileUpload
|
||||
self._controller = controller
|
||||
|
||||
def upload(self):
|
||||
if self._directFileUpload:
|
||||
# first obtain endpoint and know when target request
|
||||
self._controller.getEndpoint(
|
||||
self._getComputePath(), self._compute_id, self._onLoadEndpointCallback, showProgress=False)
|
||||
else:
|
||||
self._fileUploadToController()
|
||||
|
||||
def _getComputePath(self):
|
||||
return '/{emulator}/images/{filename}'.format(
|
||||
emulator=self._image.emulator, filename=self._image.filename)
|
||||
|
||||
def _onLoadEndpointCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while getting endpoint: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
# we know where is the endpoint and we trying to post there a file
|
||||
endpoint = result['endpoint']
|
||||
self._fileUploadToCompute(endpoint)
|
||||
|
||||
def _checkIfSuccessfulCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
connection_error = kwargs.get('connection_error', False)
|
||||
if connection_error:
|
||||
log.debug("During direct file upload compute is not visible. Fallback to upload via controller.")
|
||||
# there was an issue with connection, probably we don't have a direct access to compute
|
||||
# we need to fallback to uploading files via controller
|
||||
self._fileUploadToController()
|
||||
else:
|
||||
if "message" in result:
|
||||
log.error("Error while direct file upload: {}".format(result["message"]))
|
||||
return
|
||||
self._callback(result, error, **kwargs)
|
||||
|
||||
def _fileUploadToCompute(self, endpoint):
|
||||
log.info("Uploading file to compute: {}".format(endpoint))
|
||||
|
||||
parse_results = urllib.parse.urlparse(endpoint)
|
||||
network_manager = self._controller.getHttpClient().getNetworkManager()
|
||||
client = HTTPClient.fromUrl(endpoint, network_manager=network_manager)
|
||||
# We don't retry connection as in case of fail we try direct file upload
|
||||
client.setMaxRetryConnection(0)
|
||||
client.createHTTPQuery(
|
||||
'POST', parse_results.path, self._checkIfSuccessfulCallback, body=pathlib.Path(self._image.path),
|
||||
progressText="Uploading {}".format(self._image.filename), timeout=None, prefix="")
|
||||
|
||||
def _fileUploadToController(self):
|
||||
log.info("Uploading file to controller: {}".format(self._getComputePath()))
|
||||
self._controller.postCompute(
|
||||
self._getComputePath(), self._compute_id, self._callback, body=pathlib.Path(self._image.path),
|
||||
progressText="Uploading {}".format(self._image.filename), timeout=None)
|
||||
@@ -15,7 +15,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from ..qt import QtCore, QtWidgets, qslot, QtGui
|
||||
from .utils import colorFromSvg
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
@@ -24,6 +25,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class 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"
|
||||
}
|
||||
|
||||
show_layer = False
|
||||
|
||||
@@ -58,7 +68,8 @@ class DrawingItem:
|
||||
return self._id
|
||||
|
||||
def create(self):
|
||||
self._project.post("/drawings", self._createDrawingCallback, body=self.__json__())
|
||||
if self._project:
|
||||
self._project.post("/drawings", self._createDrawingCallback, body=self.__json__())
|
||||
|
||||
def _createDrawingCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -77,8 +88,9 @@ class DrawingItem:
|
||||
|
||||
def updateDrawing(self):
|
||||
if self._id:
|
||||
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__())
|
||||
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__(), showProgress=False)
|
||||
|
||||
@qslot
|
||||
def updateDrawingCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for update.
|
||||
@@ -153,6 +165,7 @@ class DrawingItem:
|
||||
"""
|
||||
|
||||
QtWidgets.QGraphicsItem.setZValue(self, value)
|
||||
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
@@ -188,6 +201,9 @@ class DrawingItem:
|
||||
self.updateDrawing()
|
||||
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
|
||||
|
||||
def updateNode(self):
|
||||
self.updateDrawing()
|
||||
|
||||
def drawLayerInfo(self, painter):
|
||||
"""
|
||||
Draws the layer position.
|
||||
@@ -210,3 +226,48 @@ class DrawingItem:
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
def _styleSvg(self, element):
|
||||
"""
|
||||
Add style from the shape item to the SVG element that we will
|
||||
export
|
||||
"""
|
||||
style = ""
|
||||
pen = self.pen()
|
||||
if hasattr(self, "brush"): # Line don't have a brush
|
||||
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
|
||||
element.set("fill-opacity", str(self.brush().color().alphaF()))
|
||||
|
||||
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
|
||||
|
||||
def _penFromSVGElement(self, svg):
|
||||
"""
|
||||
Get a pen from a SVG element
|
||||
|
||||
:param svg:
|
||||
"""
|
||||
pen = QtGui.QPen()
|
||||
if svg.get("stroke-width"):
|
||||
pen.setWidth(int(svg.get("stroke-width")))
|
||||
if svg.get("stroke"):
|
||||
pen.setColor(colorFromSvg(svg.get("stroke")))
|
||||
# Map SVG stroke style (border of the element to the Qt version)
|
||||
if not svg.get("stroke"):
|
||||
pen.setStyle(QtCore.Qt.NoPen)
|
||||
else:
|
||||
pen.setStyle(QtCore.Qt.SolidLine)
|
||||
stroke = svg.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)
|
||||
return pen
|
||||
|
||||
@@ -48,6 +48,13 @@ class EllipseItem(QtWidgets.QGraphicsEllipseItem, ShapeItem):
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets Z value of the item
|
||||
:param value: z layer
|
||||
"""
|
||||
return ShapeItem.setZValue(self, value)
|
||||
|
||||
def toSvg(self):
|
||||
"""
|
||||
Return an SVG version of the shape
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -113,14 +112,14 @@ class EthernetLinkItem(LinkItem):
|
||||
if self.length < 100:
|
||||
return
|
||||
|
||||
if self._source_port.status() == Port.started:
|
||||
if self._link.suspended() or self._source_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif 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
|
||||
@@ -143,8 +142,7 @@ class EthernetLinkItem(LinkItem):
|
||||
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
source_port_name = self._source_port.name()
|
||||
source_port_label.setPlainText(source_port_name)
|
||||
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)
|
||||
|
||||
@@ -155,14 +153,14 @@ class EthernetLinkItem(LinkItem):
|
||||
|
||||
painter.drawPoint(point1)
|
||||
|
||||
if self._destination_port.status() == Port.started:
|
||||
if self._link.suspended() or self._destination_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif 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
|
||||
@@ -185,8 +183,7 @@ class EthernetLinkItem(LinkItem):
|
||||
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
destination_port_name = self._destination_port.name()
|
||||
destination_port_label.setPlainText(destination_port_name)
|
||||
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)
|
||||
|
||||
@@ -197,4 +194,4 @@ class EthernetLinkItem(LinkItem):
|
||||
|
||||
painter.drawPoint(point2)
|
||||
|
||||
self._drawCaptureSymbol()
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
Graphical representation of an image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtWidgets, QtCore, QtSvg
|
||||
from ..qt import QtSvg
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from .drawing_item import DrawingItem
|
||||
|
||||
@@ -32,8 +30,7 @@ class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
|
||||
Class to insert an image on the scene.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, image_path=None, pos=None, svg=None, **kws):
|
||||
def __init__(self, image_path=None, pos=None, svg=None, **kws):
|
||||
|
||||
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
|
||||
@@ -42,7 +39,6 @@ class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
|
||||
else:
|
||||
super().__init__(**kws)
|
||||
|
||||
|
||||
if self._image_path:
|
||||
renderer = QImageSvgRenderer(image_path)
|
||||
self.setSharedRenderer(renderer)
|
||||
@@ -68,6 +64,13 @@ class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets Z value of the item
|
||||
:param value: z layer
|
||||
"""
|
||||
return DrawingItem.setZValue(self, value)
|
||||
|
||||
def fromSvg(self, svg):
|
||||
renderer = QImageSvgRenderer(svg)
|
||||
self.setSharedRenderer(renderer)
|
||||
@@ -77,4 +80,3 @@ class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
return self.renderer().svg()
|
||||
|
||||
|
||||
223
gns3/items/line_item.py
Normal file
223
gns3/items/line_item.py
Normal file
@@ -0,0 +1,223 @@
|
||||
# -*- 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 rectangle on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .drawing_item import DrawingItem
|
||||
|
||||
|
||||
class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
|
||||
|
||||
"""
|
||||
Class to draw a rectangle on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, dst=None, svg=None, **kws):
|
||||
super().__init__(svg=svg, **kws)
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
self._edge = None
|
||||
self._border = 20
|
||||
|
||||
if svg is None:
|
||||
if dst is not None:
|
||||
self.setLine(0,
|
||||
0,
|
||||
dst.x(),
|
||||
dst.y())
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.setPen(pen)
|
||||
else:
|
||||
self.fromSvg(svg)
|
||||
if self._id is None:
|
||||
self.create()
|
||||
|
||||
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 setZValue(self, value):
|
||||
"""
|
||||
Sets Z value of the item
|
||||
:param value: z layer
|
||||
"""
|
||||
return DrawingItem.setZValue(self, value)
|
||||
|
||||
def toSvg(self):
|
||||
"""
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
svg = ET.Element("svg")
|
||||
width = abs(self.line().x1() - self.line().x2())
|
||||
height = abs(self.line().y1() - self.line().y2())
|
||||
svg.set("width", str(int(width)))
|
||||
svg.set("height", str(int(height)))
|
||||
|
||||
line = ET.SubElement(svg, "line")
|
||||
line.set("x1", str(int(self.line().x1())))
|
||||
line.set("x2", str(int(self.line().x2())))
|
||||
line.set("y1", str(int(self.line().y1())))
|
||||
line.set("y2", str(int(self.line().y2())))
|
||||
line = self._styleSvg(line)
|
||||
|
||||
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
|
||||
def fromSvg(self, svg):
|
||||
"""
|
||||
Import element informations from an SVG
|
||||
"""
|
||||
svg = ET.fromstring(svg)
|
||||
width = float(svg.get("width", 0))
|
||||
height = float(svg.get("height", 0))
|
||||
|
||||
# Backup the pos and restore it
|
||||
pos = self.pos()
|
||||
y1 = self.line().y1()
|
||||
self.setLine(0, 0, width, height)
|
||||
|
||||
pen = QtGui.QPen()
|
||||
|
||||
if len(svg):
|
||||
pen = self._penFromSVGElement(svg[0])
|
||||
self.setLine(
|
||||
float(svg[0].get("x1")),
|
||||
float(svg[0].get("y1")),
|
||||
float(svg[0].get("x2")),
|
||||
float(svg[0].get("y2"))
|
||||
)
|
||||
self.setPos(pos)
|
||||
self.setPen(pen)
|
||||
self.update()
|
||||
|
||||
def _isHorizontalLine(self):
|
||||
return abs(self.line().x1() - self.line().x2()) > abs(self.line().y1() - self.line().y2())
|
||||
|
||||
def hoverMoveEvent(self, event):
|
||||
"""
|
||||
Handles all hover move events.
|
||||
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
# objects on the background layer don't need cursors
|
||||
if self.zValue() >= 0:
|
||||
|
||||
if self._isHorizontalLine():
|
||||
if event.pos().x() > (self.line().x2() - self._border):
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
|
||||
elif event.pos().x() < self._border:
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
|
||||
else:
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
|
||||
# Vertical line
|
||||
else:
|
||||
if event.pos().y() > (self.line().y2() - self._border):
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
|
||||
elif event.pos().y() < self._border:
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
|
||||
else:
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""
|
||||
Handles all mouse move events.
|
||||
|
||||
:param event: QMouseEvent instance
|
||||
"""
|
||||
|
||||
self.update()
|
||||
if self._edge:
|
||||
scenePos = event.scenePos()
|
||||
if self._edge == "left" or self._edge == "bottom":
|
||||
diff_x = self.x() - scenePos.x()
|
||||
diff_y = self.y() - scenePos.y()
|
||||
self.setPos(scenePos.x(), scenePos.y())
|
||||
self.setLine(
|
||||
0,
|
||||
0,
|
||||
self.line().x2() + diff_x,
|
||||
self.line().y2() + diff_y)
|
||||
elif self._edge == "right" or self._edge == "top":
|
||||
pos = self.mapFromScene(scenePos)
|
||||
self.setLine(
|
||||
0,
|
||||
0,
|
||||
pos.x(),
|
||||
pos.y())
|
||||
self.setPos(self.x(), self.y())
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""
|
||||
Handles all mouse press events.
|
||||
|
||||
:param event: QMouseEvent instance
|
||||
"""
|
||||
|
||||
self.update()
|
||||
if self._isHorizontalLine():
|
||||
if event.pos().x() > (self.line().x2() - self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "right"
|
||||
elif event.pos().x() < (self.line().x1() + self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "left"
|
||||
else:
|
||||
if event.pos().y() > (self.line().y2() - self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "top"
|
||||
elif event.pos().y() < (self.line().y1() + self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "bottom"
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""
|
||||
Handles all mouse release events.
|
||||
|
||||
:param: QMouseEvent instance
|
||||
"""
|
||||
|
||||
self.update()
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
|
||||
|
||||
self._edge = None
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def hoverLeaveEvent(self, event):
|
||||
"""
|
||||
Handles all hover leave events.
|
||||
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
# objects on the background layer don't need cursors
|
||||
if self.zValue() >= 0:
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
@@ -21,13 +21,13 @@ Link items are graphical representation of a link on the QGraphicsScene
|
||||
"""
|
||||
|
||||
import math
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
|
||||
|
||||
from ..node import Node
|
||||
from ..packet_capture import PacketCapture
|
||||
from ..dialogs.filter_dialog import FilterDialog
|
||||
|
||||
|
||||
class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
|
||||
class SvgIconItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
def __init__(self, symbol, parent):
|
||||
|
||||
@@ -50,12 +50,12 @@ 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)
|
||||
@@ -76,10 +76,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
|
||||
@@ -91,11 +87,17 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
# QGraphicsSvgItem to indicate a capture
|
||||
self._capturing_item = None
|
||||
# QGraphicsSvgItem to indicate a filter is applied
|
||||
self._filter_item = None
|
||||
# QGraphicsSvgItem to indicate we suspend a link
|
||||
self._suspend_item = None
|
||||
# QGraphicsSvgItem to indicate a filter is applied and a capture is active
|
||||
self._filter_capturing_item = None
|
||||
|
||||
if not self._adding_flag:
|
||||
# there is a destination
|
||||
self._link = link
|
||||
self._link.updated_link_signal.connect(self._drawCaptureSymbol)
|
||||
self._link.updated_link_signal.connect(self._drawSymbol)
|
||||
self._link.delete_link_signal.connect(self._linkDeletedSlot)
|
||||
self.setFlag(self.ItemIsFocusable)
|
||||
source_item.addLink(self)
|
||||
@@ -108,8 +110,10 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
self.adjust()
|
||||
|
||||
def _linkDeletedSlot(self, link_id):
|
||||
@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())
|
||||
@@ -117,11 +121,19 @@ 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)
|
||||
|
||||
if self in self.scene().items():
|
||||
self.scene().removeItem(self)
|
||||
@qslot
|
||||
def _filterActionSlot(self, *args):
|
||||
dialog = FilterDialog(self._main_window, self._link)
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
@qslot
|
||||
def _suspendActionSlot(self, *args):
|
||||
self._link.toggleSuspend()
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
@@ -233,12 +245,32 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
analyze_action.triggered.connect(self._analyzeCaptureActionSlot)
|
||||
menu.addAction(analyze_action)
|
||||
|
||||
if self._link.suspended() is False:
|
||||
# Edit filters
|
||||
filter_action = QtWidgets.QAction("Packet filters", menu)
|
||||
filter_action.setIcon(QtGui.QIcon(':/icons/filter.svg'))
|
||||
filter_action.triggered.connect(self._filterActionSlot)
|
||||
menu.addAction(filter_action)
|
||||
|
||||
# Suspend link
|
||||
suspend_action = QtWidgets.QAction("Suspend", menu)
|
||||
suspend_action.setIcon(QtGui.QIcon(':/icons/pause.svg'))
|
||||
suspend_action.triggered.connect(self._suspendActionSlot)
|
||||
menu.addAction(suspend_action)
|
||||
else:
|
||||
# Resume link
|
||||
resume_action = QtWidgets.QAction("Resume", menu)
|
||||
resume_action.setIcon(QtGui.QIcon(':/icons/start.svg'))
|
||||
resume_action.triggered.connect(self._suspendActionSlot)
|
||||
menu.addAction(resume_action)
|
||||
|
||||
# delete
|
||||
delete_action = QtWidgets.QAction("Delete", menu)
|
||||
delete_action.setIcon(QtGui.QIcon(':/icons/delete.svg'))
|
||||
delete_action.triggered.connect(self._deleteActionSlot)
|
||||
menu.addAction(delete_action)
|
||||
|
||||
@qslot
|
||||
def mousePressEvent(self, event):
|
||||
"""
|
||||
Called when the link is clicked and shows a contextual menu.
|
||||
@@ -349,6 +381,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
self.setHovered(False)
|
||||
|
||||
@qslot
|
||||
def adjust(self):
|
||||
"""
|
||||
Computes the source point and destination point.
|
||||
@@ -376,15 +409,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.
|
||||
@@ -397,19 +469,91 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
self.adjust()
|
||||
self.update()
|
||||
|
||||
def _drawCaptureSymbol(self):
|
||||
@qslot
|
||||
def _drawSymbol(self, *args):
|
||||
"""
|
||||
Draws a capture symbol in the middle of the link to indicate a capture is active.
|
||||
Draws a symbol in the middle of the link to indicate a capture, a suspend or a filter is active.
|
||||
"""
|
||||
|
||||
#FIXME: refactor ugly symbol management
|
||||
if not self._adding_flag:
|
||||
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)
|
||||
self._capturing_item.setScale(0.6)
|
||||
self._capturing_item.setPos(link_center)
|
||||
if not self._capturing_item.isVisible():
|
||||
self._capturing_item.show()
|
||||
elif self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
|
||||
if self._link.suspended():
|
||||
if 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._suspend_item is None:
|
||||
self._suspend_item = SvgIconItem(':/icons/pause.svg', self)
|
||||
self._suspend_item.setScale(0.6)
|
||||
if not self._suspend_item.isVisible():
|
||||
self._suspend_item.show()
|
||||
self._suspend_item.setPos(link_center)
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
elif self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
|
||||
elif self._link.capturing() and len(self._link.filters()) > 0:
|
||||
if 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._filter_capturing_item is None:
|
||||
self._filter_capturing_item = SvgIconItem(':/icons/filter-capture.svg', self)
|
||||
self._filter_capturing_item.setScale(0.6)
|
||||
if not self._filter_capturing_item.isVisible():
|
||||
self._filter_capturing_item.show()
|
||||
self._filter_capturing_item.setPos(link_center)
|
||||
elif self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
|
||||
elif self._link.capturing():
|
||||
if 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 = SvgIconItem(':/icons/inspect.svg', self)
|
||||
self._capturing_item.setScale(0.6)
|
||||
self._capturing_item.setPos(link_center)
|
||||
if not self._capturing_item.isVisible():
|
||||
self._capturing_item.show()
|
||||
elif self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
|
||||
elif len(self._link.filters()) > 0:
|
||||
if 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._filter_item is None:
|
||||
self._filter_item = SvgIconItem(':/icons/filter.svg', self)
|
||||
self._filter_item.setScale(0.6)
|
||||
if not self._filter_item.isVisible():
|
||||
self._filter_item.show()
|
||||
self._filter_item.setPos(link_center)
|
||||
elif self._filter_item:
|
||||
self._filter_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
|
||||
else:
|
||||
if self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
Graphical representation of a node on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
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
|
||||
@@ -39,6 +41,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
GRID_SIZE = 75
|
||||
|
||||
def __init__(self, node):
|
||||
super().__init__()
|
||||
@@ -56,9 +59,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
# node label
|
||||
self._node_label = None
|
||||
|
||||
# Temporary symbol during loading
|
||||
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)
|
||||
@@ -96,18 +100,28 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
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()
|
||||
|
||||
if node.initialized():
|
||||
self.createdSlot(node.id())
|
||||
|
||||
def _updateNode(self):
|
||||
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
|
||||
@@ -128,16 +142,17 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
def symbol(self):
|
||||
return self._symbol
|
||||
|
||||
def _symbolLoadedCallback(self, path):
|
||||
renderer = QImageSvgRenderer(path)
|
||||
@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()
|
||||
self.updateNode()
|
||||
if not self._initialized:
|
||||
self._showLabel()
|
||||
self._initialized = True
|
||||
self._updateNode()
|
||||
self.updateNode()
|
||||
|
||||
def node(self):
|
||||
"""
|
||||
@@ -153,25 +168,39 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
self._node.setSettingValue("x", int(self.x()))
|
||||
self._node.setSettingValue("y", int(self.y()))
|
||||
|
||||
def addLink(self, link):
|
||||
@qslot
|
||||
def addLink(self, link_item, *args):
|
||||
"""
|
||||
Adds a link items to this node item.
|
||||
|
||||
:param link: LinkItem instance
|
||||
"""
|
||||
|
||||
self._links.append(link)
|
||||
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()
|
||||
|
||||
@qslot
|
||||
def _linkUpdatedSlot(self, *args):
|
||||
"""
|
||||
When a link change we also notify the listener of the node
|
||||
"""
|
||||
self._node.updated_signal.emit()
|
||||
|
||||
def removeLink(self, link):
|
||||
@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)
|
||||
for link_item in self._links:
|
||||
if link_item.link().id() == link_id:
|
||||
self._links.remove(link_item)
|
||||
return
|
||||
|
||||
def links(self):
|
||||
"""
|
||||
@@ -182,7 +211,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
return self._links
|
||||
|
||||
def createdSlot(self, base_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.
|
||||
@@ -190,54 +220,47 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param base_node_id: base node identifier (integer)
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
|
||||
self.setSymbol(self._node.symbol())
|
||||
self.update()
|
||||
|
||||
def startedSlot(self):
|
||||
@qslot
|
||||
def startedSlot(self, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a the node has started.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
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.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
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.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
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
|
||||
|
||||
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))
|
||||
@@ -249,18 +272,20 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
for link in self._links:
|
||||
link.setCustomToolTip()
|
||||
|
||||
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 or not self.scene():
|
||||
if not self.scene():
|
||||
return
|
||||
if self in self.scene().items():
|
||||
self.scene().removeItem(self)
|
||||
|
||||
def serverErrorSlot(self, base_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.
|
||||
@@ -269,10 +294,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
if self:
|
||||
self._last_error = "{message}".format(message=message)
|
||||
self._last_error = "{message}".format(message=message)
|
||||
|
||||
def errorSlot(self, base_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.
|
||||
@@ -281,8 +306,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
if self:
|
||||
self._last_error = "{message}".format(message=message)
|
||||
self._last_error = "{message}".format(message=message)
|
||||
|
||||
def setCustomToolTip(self):
|
||||
"""
|
||||
@@ -311,7 +335,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
Called when user unselect the label
|
||||
"""
|
||||
self._updateNode()
|
||||
self.updateNode()
|
||||
|
||||
def _centerLabel(self):
|
||||
"""
|
||||
@@ -350,11 +374,11 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
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"])
|
||||
self._node_label.setStyle(label_data.get("style", ""))
|
||||
self._node_label.setRotation(label_data.get("rotation", 0))
|
||||
if label_data["x"] is None:
|
||||
self._centerLabel()
|
||||
self._updateNode()
|
||||
self.updateNode()
|
||||
else:
|
||||
self._node_label.setPos(label_data["x"], label_data["y"])
|
||||
|
||||
@@ -433,14 +457,11 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param value: value of the change
|
||||
"""
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
GRID_SIZE = 75
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
tmp_x = (GRID_SIZE * round((self.x() + mid_x) / GRID_SIZE)) - mid_x
|
||||
value.setX((self.GRID_SIZE * round((value.x() + mid_x) / self.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)
|
||||
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:
|
||||
@@ -448,7 +469,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
self.graphicsEffect().setEnabled(True)
|
||||
else:
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
self._updateNode()
|
||||
self.updateNode()
|
||||
|
||||
# adjust link item positions when this node is moving or has changed.
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange or change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
|
||||
@@ -508,7 +529,6 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
self._node_label.setFlag(self.ItemIsMovable, True)
|
||||
for link in self._links:
|
||||
link.adjust()
|
||||
self._node.setSettingValue("z", int(value))
|
||||
|
||||
def hoverEnterEvent(self, event):
|
||||
"""
|
||||
@@ -537,4 +557,4 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
It the item is select but mouse is not on it the event
|
||||
is send also
|
||||
"""
|
||||
self._updateNode()
|
||||
self.updateNode()
|
||||
|
||||
@@ -201,13 +201,17 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
val = val.strip()
|
||||
|
||||
if key == "font-size":
|
||||
font.setPointSize(int(val))
|
||||
font.setPointSizeF(float(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()
|
||||
@@ -248,7 +252,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
style = ""
|
||||
|
||||
style += "font-family: {};".format(self.font().family())
|
||||
style += "font-size: {};".format(self.font().pointSize())
|
||||
style += "font-size: {};".format(self.font().pointSizeF())
|
||||
|
||||
if self.font().italic():
|
||||
style += "font-style: italic;"
|
||||
@@ -256,6 +260,11 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
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())
|
||||
|
||||
|
||||
@@ -46,6 +46,13 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets Z value of the item
|
||||
:param value: z layer
|
||||
"""
|
||||
return ShapeItem.setZValue(self, value)
|
||||
|
||||
def toSvg(self):
|
||||
"""
|
||||
Return an SVG version of the shape
|
||||
@@ -61,4 +68,3 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
rect = self._styleSvg(rect)
|
||||
|
||||
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
@@ -115,25 +114,24 @@ class SerialLinkItem(LinkItem):
|
||||
return
|
||||
|
||||
# source point color
|
||||
if self._source_port.status() == Port.started:
|
||||
if self._link.suspended() or self._source_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.yellow
|
||||
elif 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))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, 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_name = self._source_port.name()
|
||||
source_port_label.setPlainText(source_port_name)
|
||||
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)
|
||||
|
||||
@@ -145,14 +143,14 @@ class SerialLinkItem(LinkItem):
|
||||
painter.drawPoint(self.source_point)
|
||||
|
||||
# destination point color
|
||||
if self._destination_port.status() == Port.started:
|
||||
if self._link.suspended() or self._destination_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif 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
|
||||
@@ -163,8 +161,7 @@ class SerialLinkItem(LinkItem):
|
||||
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
destination_port_name = self._destination_port.name()
|
||||
destination_port_label.setPlainText(destination_port_name)
|
||||
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)
|
||||
|
||||
@@ -175,4 +172,4 @@ class SerialLinkItem(LinkItem):
|
||||
|
||||
painter.drawPoint(self.destination_point)
|
||||
|
||||
self._drawCaptureSymbol()
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -20,7 +20,7 @@ Base class for shape items (Rectangle, ellipse etc.).
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .drawing_item import DrawingItem
|
||||
from .utils import colorFromSvg
|
||||
|
||||
@@ -30,17 +30,6 @@ log = logging.getLogger(__name__)
|
||||
|
||||
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.
|
||||
"""
|
||||
@@ -181,27 +170,6 @@ class ShapeItem(DrawingItem):
|
||||
if self.zValue() >= 0:
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def _styleSvg(self, element):
|
||||
"""
|
||||
Add style from the shape item to the SVG element that we will
|
||||
export
|
||||
"""
|
||||
style = ""
|
||||
pen = self.pen()
|
||||
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
|
||||
element.set("fill-opacity", str(self.brush().color().alphaF()))
|
||||
|
||||
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
|
||||
|
||||
def fromSvg(self, svg):
|
||||
"""
|
||||
Import element informations from an SVG
|
||||
@@ -215,10 +183,7 @@ class ShapeItem(DrawingItem):
|
||||
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
|
||||
|
||||
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")))
|
||||
pen = self._penFromSVGElement(svg[0])
|
||||
if svg[0].get("fill"):
|
||||
new_color = colorFromSvg(svg[0].get("fill"))
|
||||
color = brush.color()
|
||||
@@ -231,17 +196,6 @@ class ShapeItem(DrawingItem):
|
||||
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)
|
||||
|
||||
self.setPen(pen)
|
||||
self.setBrush(brush)
|
||||
self.update()
|
||||
|
||||
@@ -26,10 +26,15 @@ 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)
|
||||
@@ -44,7 +49,10 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
self.setFont(qt_font)
|
||||
|
||||
if svg:
|
||||
svg = self.fromSvg(svg)
|
||||
try:
|
||||
svg = self.fromSvg(svg)
|
||||
except ET.ParseError as e:
|
||||
log.warning(str(e))
|
||||
|
||||
if self._id is None:
|
||||
self.create()
|
||||
@@ -103,6 +111,13 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets Z value of the item
|
||||
:param value: z layer
|
||||
"""
|
||||
return DrawingItem.setZValue(self, value)
|
||||
|
||||
def toSvg(self):
|
||||
"""
|
||||
Return an SVG version of the text
|
||||
@@ -113,11 +128,15 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
|
||||
text = ET.SubElement(svg, "text")
|
||||
text.set("font-family", self.font().family())
|
||||
text.set("font-size", str(self.font().pointSize()))
|
||||
text.set("font-size", str(self.font().pointSizeF()))
|
||||
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()
|
||||
@@ -126,7 +145,19 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
return svg
|
||||
|
||||
def fromSvg(self, svg):
|
||||
svg = ET.fromstring(svg)
|
||||
|
||||
# sometimes we receive \0 at the end of string inside <svg> element
|
||||
try:
|
||||
svg = svg.replace("\u0000", "")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
svg = ET.fromstring(svg)
|
||||
except ET.ParseError:
|
||||
self.setPlainText("Unable to parse `text_item`")
|
||||
return
|
||||
|
||||
text = svg[0]
|
||||
|
||||
font = QtGui.QFont()
|
||||
@@ -145,12 +176,16 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
color.setAlphaF(float(opacity))
|
||||
self.setDefaultTextColor(color)
|
||||
|
||||
font.setPointSize(int(text.get("font-size", self.font().pointSize())))
|
||||
font.setPointSizeF(float(text.get("font-size", self.font().pointSizeF())))
|
||||
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)
|
||||
|
||||
@@ -23,7 +23,9 @@ def colorFromSvg(value):
|
||||
Transform a color coming from a SVG file to a Qcolor
|
||||
"""
|
||||
value = value.strip('#')
|
||||
if len(value) == 6: # If alpha channel is missing
|
||||
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)
|
||||
|
||||
135
gns3/link.py
135
gns3/link.py
@@ -21,8 +21,8 @@ Manages and stores everything needed for a connection between 2 devices.
|
||||
|
||||
import os
|
||||
import re
|
||||
import sip
|
||||
import uuid
|
||||
import tempfile
|
||||
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .controller import Controller
|
||||
@@ -59,10 +59,10 @@ class Link(QtCore.QObject):
|
||||
|
||||
super().__init__()
|
||||
|
||||
log.info("adding link from {} {} to {} {}".format(source_node.name(),
|
||||
source_port.name(),
|
||||
destination_node.name(),
|
||||
destination_port.name()))
|
||||
log.debug("adding link from {} {} to {} {}".format(source_node.name(),
|
||||
source_port.name(),
|
||||
destination_node.name(),
|
||||
destination_port.name()))
|
||||
|
||||
# create an unique ID
|
||||
self._id = Link._instance_count
|
||||
@@ -77,11 +77,14 @@ class Link(QtCore.QObject):
|
||||
self._link_id = link_id
|
||||
self._capturing = False
|
||||
self._capture_file_path = None
|
||||
self._capture_file = None
|
||||
self._initialized = False
|
||||
self._filters = {}
|
||||
self._suspend = False
|
||||
|
||||
# Boolean if True we are creatin the first instance of this node
|
||||
# Boolean if True we are creating the first instance of this node
|
||||
# if false the node already exist in the topology
|
||||
# use to avoid erasing informations when reloading
|
||||
# use to avoid erasing information when reloading
|
||||
self._creator = False
|
||||
|
||||
self._nodes = []
|
||||
@@ -102,28 +105,44 @@ class Link(QtCore.QObject):
|
||||
self._capturing = result.get("capturing", False)
|
||||
|
||||
# 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:
|
||||
(handle, self._capture_file_path) = tempfile.mkstemp()
|
||||
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,
|
||||
timeout=None)
|
||||
else:
|
||||
self._capture_file_path = result["capture_file_path"]
|
||||
if self._capturing:
|
||||
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:
|
||||
self._capture_file_path = result["capture_file_path"]
|
||||
|
||||
if "nodes" in result:
|
||||
self._nodes = result["nodes"]
|
||||
self._updateLabels()
|
||||
if "filters" in result:
|
||||
self._filters = result["filters"]
|
||||
if "suspend" in result:
|
||||
self._suspend = result["suspend"]
|
||||
self.updated_link_signal.emit(self._id)
|
||||
|
||||
def creator(self):
|
||||
return self._creator
|
||||
|
||||
def suspended(self):
|
||||
return self._suspend
|
||||
|
||||
def toggleSuspend(self):
|
||||
self._suspend = not self._suspend
|
||||
self.update()
|
||||
|
||||
def initialized(self):
|
||||
return self._initialized
|
||||
|
||||
@@ -144,6 +163,12 @@ class Link(QtCore.QObject):
|
||||
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 listAvailableFilters(self, callback):
|
||||
"""
|
||||
Get the list of available filters
|
||||
"""
|
||||
Controller.instance().get("/projects/{project_id}/links/{link_id}/available_filters".format(project_id=self._source_node.project().id(), link_id=self._link_id), callback)
|
||||
|
||||
def updateLinkCallback(self, result, error=False, *args, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Update link", "Error while updating link: {}".format(result["message"]))
|
||||
@@ -160,12 +185,16 @@ class Link(QtCore.QObject):
|
||||
raise NotImplementedError
|
||||
|
||||
def _updateLabel(self, label, label_data):
|
||||
if not label:
|
||||
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"])
|
||||
if "text" in label_data:
|
||||
label.setPlainText(label_data["text"])
|
||||
if "x" in label_data and "y" in label_data:
|
||||
label.setPos(label_data["x"], label_data["y"])
|
||||
if "style" in label_data:
|
||||
label.setStyle(label_data["style"])
|
||||
if "rotation" in label_data:
|
||||
label.setRotation(label_data["rotation"])
|
||||
|
||||
def _prepareParams(self):
|
||||
body = {
|
||||
@@ -180,7 +209,9 @@ class Link(QtCore.QObject):
|
||||
"adapter_number": self._destination_port.adapterNumber(),
|
||||
"port_number": self._destination_port.portNumber()
|
||||
}
|
||||
]
|
||||
],
|
||||
"filters": self._filters,
|
||||
"suspend": self._suspend
|
||||
}
|
||||
if self._source_port.label():
|
||||
body["nodes"][0]["label"] = self._source_port.label().dump()
|
||||
@@ -191,6 +222,7 @@ class Link(QtCore.QObject):
|
||||
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
|
||||
@@ -237,10 +269,18 @@ class Link(QtCore.QObject):
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Link from {} port {} to {} port {}".format(self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
description = "Link from {} port {} to {} port {}".format(self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
|
||||
if self.capturing():
|
||||
description += "\nPacket capture is active"
|
||||
|
||||
for filter_type in self._filters.keys():
|
||||
description += "\nPacket filter '{}' is active".format(filter_type)
|
||||
|
||||
return description
|
||||
|
||||
def capture_file_name(self):
|
||||
"""
|
||||
@@ -258,10 +298,10 @@ class Link(QtCore.QObject):
|
||||
Deletes this link.
|
||||
"""
|
||||
|
||||
log.info("deleting link from {} {} to {} {}".format(self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name()))
|
||||
log.debug("deleting link from {} {} to {} {}".format(self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name()))
|
||||
|
||||
if skip_controller:
|
||||
self._linkDeletedCallback({})
|
||||
@@ -311,20 +351,19 @@ class Link(QtCore.QObject):
|
||||
"""
|
||||
if not self._capture_file_path:
|
||||
return
|
||||
try:
|
||||
with open(self._capture_file_path, 'ab') as f:
|
||||
f.write(content)
|
||||
except OSError as e:
|
||||
log.error("Can't write file {}: {}".format(self._capture_file_path, e), True)
|
||||
return
|
||||
self._capture_file.write(content)
|
||||
self._capture_file.flush()
|
||||
|
||||
def stopCapture(self):
|
||||
if Controller.instance().isRemote():
|
||||
if self._capture_file_path:
|
||||
if self._capture_file:
|
||||
self._capture_file.close()
|
||||
self._capture_file = None
|
||||
if self._capture_file_path and os.path.exists(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))
|
||||
log.error("Can't remove file {}: {}".format(self._capture_file_path, e))
|
||||
self._capture_file_path = None
|
||||
Controller.instance().post(
|
||||
"/projects/{project_id}/links/{link_id}/stop_capture".format(
|
||||
@@ -404,3 +443,15 @@ class Link(QtCore.QObject):
|
||||
if self._destination_node == node:
|
||||
return self._destination_port
|
||||
return self._source_port
|
||||
|
||||
def filters(self):
|
||||
"""
|
||||
:returns: List the filters active on the node
|
||||
"""
|
||||
return self._filters
|
||||
|
||||
def setFilters(self, filters):
|
||||
"""
|
||||
:params filters: List of filters
|
||||
"""
|
||||
self._filters = filters
|
||||
|
||||
@@ -23,7 +23,7 @@ import copy
|
||||
|
||||
import psutil
|
||||
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .qt import QtCore, QtWidgets, qslot
|
||||
from .version import __version__
|
||||
from .utils import parse_version
|
||||
from .controller import Controller
|
||||
@@ -39,6 +39,8 @@ 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):
|
||||
"""
|
||||
@@ -48,8 +50,27 @@ class LocalConfig(QtCore.QObject):
|
||||
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):
|
||||
"""
|
||||
@@ -97,8 +118,7 @@ class LocalConfig(QtCore.QObject):
|
||||
# overwrite system wide settings with user specific ones
|
||||
self._settings.update(user_settings)
|
||||
self._migrateOldConfig()
|
||||
self._writeConfig()
|
||||
Controller.instance().connected_signal.connect(self.refreshConfigFromController)
|
||||
self.writeConfig()
|
||||
|
||||
def profile(self):
|
||||
"""
|
||||
@@ -116,28 +136,36 @@ class LocalConfig(QtCore.QObject):
|
||||
self._config_file = None
|
||||
self._resetLoadConfig()
|
||||
|
||||
@qslot
|
||||
def refreshConfigFromController(self):
|
||||
"""
|
||||
Refresh the configuration from the controller
|
||||
"""
|
||||
controller = Controller.instance()
|
||||
if controller.connected():
|
||||
controller.get("/settings", self._getSettingsCallback)
|
||||
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")
|
||||
log.debug("Can't get settings from controller")
|
||||
return
|
||||
if result == {} and self._settings != {}:
|
||||
self._saveOnController()
|
||||
self._settings_retrieved_from_controller = True
|
||||
self.save_on_controller_signal.emit()
|
||||
return
|
||||
|
||||
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()
|
||||
# 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):
|
||||
"""
|
||||
@@ -155,6 +183,13 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
return os.path.normpath(path)
|
||||
|
||||
def runAsRootPath(self):
|
||||
"""
|
||||
Gets run as root filename
|
||||
:return: string
|
||||
"""
|
||||
return os.path.join(self.configDirectory(), "run_as_root")
|
||||
|
||||
def _migrateOldConfigPath(self):
|
||||
"""
|
||||
Migrate pre 1.4 config path
|
||||
@@ -181,8 +216,8 @@ class LocalConfig(QtCore.QObject):
|
||||
# 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]:
|
||||
app = 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.".format(self._settings["version"]))
|
||||
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)
|
||||
|
||||
@@ -212,7 +247,7 @@ 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
|
||||
@@ -226,12 +261,22 @@ class LocalConfig(QtCore.QObject):
|
||||
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):
|
||||
"""
|
||||
Read the configuration file.
|
||||
"""
|
||||
|
||||
log.info("Load config from %s", config_path)
|
||||
log.debug("Load config from %s", config_path)
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
self._last_config_changed = os.stat(config_path).st_mtime
|
||||
@@ -247,7 +292,7 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
return dict()
|
||||
|
||||
def _writeConfig(self):
|
||||
def writeConfig(self):
|
||||
"""
|
||||
Write the configuration file.
|
||||
"""
|
||||
@@ -258,20 +303,21 @@ class LocalConfig(QtCore.QObject):
|
||||
with open(temporary, "w", encoding="utf-8") as f:
|
||||
json.dump(self._settings, f, sort_keys=True, indent=4)
|
||||
shutil.move(temporary, self._config_file)
|
||||
log.info("Configuration save to %s", self._config_file)
|
||||
log.debug("Configuration save to %s", self._config_file)
|
||||
self._last_config_changed = os.stat(self._config_file).st_mtime
|
||||
except (ValueError, OSError) as e:
|
||||
log.error("Could not write the config file {}: {}".format(self._config_file, e))
|
||||
self._saveOnController()
|
||||
self.save_on_controller_signal.emit()
|
||||
|
||||
def _saveOnController(self):
|
||||
@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():
|
||||
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"]
|
||||
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:
|
||||
@@ -285,7 +331,7 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
try:
|
||||
if self._last_config_changed and self._last_config_changed < os.stat(self._config_file).st_mtime:
|
||||
log.info("Client config has changed, reloading it...")
|
||||
log.debug("Client config has changed, reloading it...")
|
||||
self._readConfig(self._config_file)
|
||||
self.config_changed_signal.emit()
|
||||
except OSError as e:
|
||||
@@ -328,7 +374,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):
|
||||
"""
|
||||
@@ -362,9 +409,8 @@ class LocalConfig(QtCore.QObject):
|
||||
self._settings[section] = settings
|
||||
|
||||
if changed:
|
||||
log.info("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
|
||||
self._writeConfig()
|
||||
|
||||
log.debug("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
|
||||
self.writeConfig()
|
||||
return copy.deepcopy(settings)
|
||||
|
||||
def saveSectionSettings(self, section, settings):
|
||||
@@ -380,8 +426,8 @@ 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()
|
||||
log.debug("Section %s has changed. Saving configuration", section)
|
||||
self.writeConfig()
|
||||
else:
|
||||
log.debug("Section %s has not changed. Skip saving configuration", section)
|
||||
|
||||
@@ -393,6 +439,14 @@ 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
|
||||
@@ -407,6 +461,20 @@ class LocalConfig(QtCore.QObject):
|
||||
settings["multi_profiles"] = value
|
||||
self.saveSectionSettings("MainWindow", settings)
|
||||
|
||||
def directFileUpload(self):
|
||||
"""
|
||||
:returns: Boolean. True if direct_file_upload is enabled
|
||||
"""
|
||||
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["direct_file_upload"]
|
||||
|
||||
def setDirectFileUpload(self, value):
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
settings = self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)
|
||||
settings["direct_file_upload"] = value
|
||||
self.saveSectionSettings("MainWindow", settings)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
|
||||
@@ -30,7 +30,7 @@ import signal
|
||||
import subprocess
|
||||
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore
|
||||
from gns3.qt import QtWidgets, QtCore, qslot
|
||||
from gns3.settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
@@ -59,17 +59,20 @@ class StopLocalServerWorker(QtCore.QObject):
|
||||
def __init__(self, local_server_process):
|
||||
super().__init__()
|
||||
self._local_server_process = local_server_process
|
||||
self._precision = 100 # In MS
|
||||
self._remaining_trial = int(10 * (1000 / self._precision))
|
||||
|
||||
@qslot
|
||||
def _callbackSlot(self, *params):
|
||||
self._local_server_process.poll()
|
||||
if self._local_server_process.returncode is None and self._remaining_trial > 0:
|
||||
self._remaining_trial -= 1
|
||||
QtCore.QTimer.singleShot(self._precision, self._callbackSlot)
|
||||
else:
|
||||
self.finished.emit()
|
||||
|
||||
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()
|
||||
QtCore.QTimer.singleShot(1000, self._callbackSlot)
|
||||
|
||||
def cancel(self):
|
||||
return
|
||||
@@ -81,22 +84,30 @@ class LocalServer(QtCore.QObject):
|
||||
"""
|
||||
|
||||
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._local_server_path = ""
|
||||
self._local_server_process = None
|
||||
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):
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
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
|
||||
@@ -121,7 +132,7 @@ class LocalServer(QtCore.QObject):
|
||||
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
|
||||
return False
|
||||
except pywintypes.error as e:
|
||||
if e.winerror == 1060:
|
||||
if e.winerror == 1060: # service is not installed
|
||||
return False
|
||||
else:
|
||||
log.error("Could not check if the {} service is running: {}".format(service_name, e.strerror))
|
||||
@@ -134,7 +145,7 @@ class LocalServer(QtCore.QObject):
|
||||
|
||||
path = os.path.abspath(self._settings["ubridge_path"])
|
||||
|
||||
if not path or len(path) == 0 or not os.path.exists(path):
|
||||
if not path or len(path) == 0 or not os.path.exists(path) or not os.path.isfile(path):
|
||||
return False
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
@@ -149,24 +160,23 @@ class LocalServer(QtCore.QObject):
|
||||
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?",
|
||||
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
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if "security.capability" not in os.listxattr(path) or not struct.unpack("<IIIII", os.getxattr(path, "security.capability"))[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", path])
|
||||
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
|
||||
request_setuid = True
|
||||
|
||||
if sys.platform.startswith("darwin") or request_setuid:
|
||||
try:
|
||||
@@ -174,12 +184,11 @@ class LocalServer(QtCore.QObject):
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires root permissions to interact with network interfaces. Set root permissions to 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(["chmod", "4755", path])
|
||||
sudo(["chown", "root", path])
|
||||
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
|
||||
@@ -243,13 +252,25 @@ class LocalServer(QtCore.QObject):
|
||||
# Settings have changed we need to restart the server
|
||||
if old_settings != self._settings:
|
||||
if self._settings["auto_start"]:
|
||||
self.stopLocalServer(wait=True)
|
||||
# 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)
|
||||
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
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):
|
||||
@@ -260,7 +281,7 @@ class LocalServer(QtCore.QObject):
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._settings["auto_start"]
|
||||
return self._settings["auto_start"] and self._settings["host"] is not None
|
||||
|
||||
def localServerPath(self):
|
||||
"""
|
||||
@@ -293,8 +314,13 @@ class LocalServer(QtCore.QObject):
|
||||
"""
|
||||
|
||||
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():
|
||||
@@ -304,7 +330,7 @@ class LocalServer(QtCore.QObject):
|
||||
return True
|
||||
|
||||
if self.isLocalServerRunning():
|
||||
log.info("A local server already running on this host")
|
||||
log.debug("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()
|
||||
@@ -326,7 +352,7 @@ class LocalServer(QtCore.QObject):
|
||||
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)
|
||||
|
||||
@@ -342,7 +368,7 @@ class LocalServer(QtCore.QObject):
|
||||
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
|
||||
return False
|
||||
|
||||
self._port = self._settings["port"]
|
||||
|
||||
@@ -350,13 +376,13 @@ class LocalServer(QtCore.QObject):
|
||||
local_server_path = self.localServerPath()
|
||||
if not local_server_path:
|
||||
log.warn("No local server is configured")
|
||||
return
|
||||
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
|
||||
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
|
||||
return False
|
||||
|
||||
try:
|
||||
# check if the local address still exists
|
||||
@@ -411,6 +437,7 @@ class LocalServer(QtCore.QObject):
|
||||
Starts the local server process.
|
||||
"""
|
||||
|
||||
self._stopping = False
|
||||
path = self.localServerPath()
|
||||
command = '"{executable}" --local'.format(executable=path)
|
||||
|
||||
@@ -438,22 +465,32 @@ class LocalServer(QtCore.QObject):
|
||||
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))
|
||||
log.debug("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)
|
||||
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)
|
||||
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))
|
||||
log.debug("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.
|
||||
@@ -475,10 +512,14 @@ class LocalServer(QtCore.QObject):
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
status, json_data = getSynchronous(self._settings["host"], self._port, "version",
|
||||
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:
|
||||
if status == 401: # Auth issue that need to be solved later
|
||||
return True
|
||||
elif json_data is None:
|
||||
return False
|
||||
elif status != 200:
|
||||
return False
|
||||
else:
|
||||
version = json_data.get("version", None)
|
||||
@@ -495,7 +536,8 @@ class LocalServer(QtCore.QObject):
|
||||
"""
|
||||
|
||||
if self.localServerProcessIsRunning():
|
||||
log.info("Stopping local server (PID={})".format(self._local_server_process.pid))
|
||||
self._stopping = True
|
||||
log.debug("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()
|
||||
@@ -506,6 +548,7 @@ class LocalServer(QtCore.QObject):
|
||||
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
|
||||
@@ -516,11 +559,11 @@ class LocalServer(QtCore.QObject):
|
||||
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:
|
||||
except (PermissionError, SystemError):
|
||||
pass
|
||||
try:
|
||||
# wait for the server to stop for maximum 2 seconds
|
||||
self._local_server_process.wait(timeout=2)
|
||||
# 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",
|
||||
@@ -553,5 +596,6 @@ def main():
|
||||
local_server.localServerAutoStart()
|
||||
local_server.stopLocalServer()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -30,21 +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"
|
||||
|
||||
from .local_config import LocalConfig
|
||||
if sys.platform.startswith("win"):
|
||||
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
|
||||
if config_file:
|
||||
self._config_file = config_file
|
||||
else:
|
||||
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), 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
|
||||
@@ -106,6 +110,8 @@ class LocalServerConfig:
|
||||
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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
48
gns3/main.py
48
gns3/main.py
@@ -18,6 +18,7 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import faulthandler
|
||||
|
||||
# Try to install updates & restart application if an update is installed
|
||||
try:
|
||||
@@ -48,7 +49,7 @@ import signal
|
||||
import psutil
|
||||
|
||||
try:
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets
|
||||
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
|
||||
@@ -60,7 +61,6 @@ from gns3.application import Application
|
||||
from gns3.utils import parse_version
|
||||
from gns3.dialogs.profile_select import ProfileSelectDialog
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -111,6 +111,9 @@ def main():
|
||||
Entry point for GNS3 GUI.
|
||||
"""
|
||||
|
||||
# Get Python tracebacks explicitly, on a fault like segfault
|
||||
faulthandler.enable()
|
||||
|
||||
# Sometimes (for example at first launch) the OSX app service launcher add
|
||||
# an extra argument starting with -psn_. We filter it
|
||||
if sys.platform.startswith("darwin"):
|
||||
@@ -120,11 +123,15 @@ 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.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
|
||||
# packaged binary
|
||||
@@ -144,7 +151,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):
|
||||
@@ -178,16 +184,12 @@ 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")
|
||||
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.0.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.0.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.5.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.5.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
|
||||
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__))
|
||||
@@ -222,31 +224,41 @@ def main():
|
||||
except win32console.error as e:
|
||||
print("warning: could not allocate console: {}".format(e))
|
||||
|
||||
global app
|
||||
app = Application(sys.argv)
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
if local_config.multiProfiles():
|
||||
|
||||
global app
|
||||
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()
|
||||
if profile_select.exec_():
|
||||
options.profile = profile_select.profile()
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
# Init the config
|
||||
if options.config:
|
||||
local_config.setConfigFilePath(options.config)
|
||||
elif options.profile:
|
||||
local_config.setProfile(options.profile)
|
||||
profile = options.profile
|
||||
|
||||
# save client logging info to a file
|
||||
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.instance().configDirectory(), exception_file_path)
|
||||
|
||||
@@ -27,7 +27,7 @@ import logging
|
||||
from .local_config import LocalConfig
|
||||
from .local_server import LocalServer
|
||||
from .modules import MODULES
|
||||
from .qt import QtGui, QtCore, QtWidgets
|
||||
from .qt import QtGui, QtCore, QtWidgets, qslot
|
||||
from .controller import Controller
|
||||
from .node import Node
|
||||
from .ui.main_window_ui import Ui_MainWindow
|
||||
@@ -41,11 +41,9 @@ from .dialogs.doctor_dialog import DoctorDialog
|
||||
from .dialogs.edit_project_dialog import EditProjectDialog
|
||||
from .dialogs.setup_wizard import SetupWizard
|
||||
from .settings import GENERAL_SETTINGS
|
||||
from .utils.progress_dialog import ProgressDialog
|
||||
from .items.node_item import NodeItem
|
||||
from .items.link_item import LinkItem
|
||||
from .items.shape_item import ShapeItem
|
||||
from .items.image_item import ImageItem
|
||||
from .topology import Topology
|
||||
from .http_client import HTTPClient
|
||||
from .progress import Progress
|
||||
@@ -53,6 +51,8 @@ from .update_manager import UpdateManager
|
||||
from .utils.analytics import AnalyticsClient
|
||||
from .dialogs.appliance_wizard import ApplianceWizard
|
||||
from .dialogs.new_appliance_dialog import NewApplianceDialog
|
||||
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
|
||||
from .status_bar import StatusBarHandler
|
||||
from .registry.appliance import ApplianceError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -75,8 +75,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
super().__init__(parent)
|
||||
self._settings = {}
|
||||
|
||||
self.setupUi(self)
|
||||
|
||||
self._notif_dialog = NotifDialog(self)
|
||||
# Setup logger
|
||||
logging.getLogger().addHandler(NotifDialogHandler(self._notif_dialog))
|
||||
logging.getLogger().addHandler(StatusBarHandler(self.uiStatusBar))
|
||||
|
||||
self._open_file_at_startup = open_file
|
||||
|
||||
MainWindow._instance = self
|
||||
@@ -84,18 +91,18 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
topology.setMainWindow(self)
|
||||
topology.project_changed_signal.connect(self._projectChangedSlot)
|
||||
Controller.instance().setParent(self)
|
||||
LocalServer.instance().setParent(self)
|
||||
|
||||
self._settings = {}
|
||||
HTTPClient.setProgressCallback(Progress.instance(self))
|
||||
|
||||
self._first_file_load = True
|
||||
self._open_project_path = None
|
||||
self._loadSettings()
|
||||
self._connections()
|
||||
self._max_recent_files = 5
|
||||
self._maxrecent_files = 5
|
||||
self._project_dialog = None
|
||||
self._recent_file_actions = []
|
||||
self._recent_project_actions = []
|
||||
self.recent_file_actions = []
|
||||
self.recent_project_actions = []
|
||||
self._start_time = time.time()
|
||||
local_config = LocalConfig.instance()
|
||||
local_config.config_changed_signal.connect(self._localConfigChangedSlot)
|
||||
@@ -112,8 +119,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiDocksMenu.addAction(self.uiTopologySummaryDockWidget.toggleViewAction())
|
||||
self.uiDocksMenu.addAction(self.uiComputeSummaryDockWidget.toggleViewAction())
|
||||
self.uiDocksMenu.addAction(self.uiConsoleDockWidget.toggleViewAction())
|
||||
self.uiDocksMenu.addAction(self.uiNodesDockWidget.toggleViewAction())
|
||||
action = self.uiNodesDockWidget.toggleViewAction()
|
||||
action.setIconText("All devices")
|
||||
self.uiDocksMenu.addAction(action)
|
||||
|
||||
# Sometimes the parent seem invalid https://github.com/GNS3/gns3-gui/issues/2182
|
||||
self.uiNodesDockWidget.setParent(self)
|
||||
# make sure the dock widget is not open
|
||||
self.uiNodesDockWidget.setVisible(False)
|
||||
|
||||
@@ -124,28 +135,25 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._pictures_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
|
||||
|
||||
# add recent file actions to the File menu
|
||||
for i in range(0, self._max_recent_files):
|
||||
for i in range(0, self._maxrecent_files):
|
||||
action = QtWidgets.QAction(self.uiFileMenu)
|
||||
action.setVisible(False)
|
||||
action.triggered.connect(self.openRecentFileSlot)
|
||||
self._recent_file_actions.append(action)
|
||||
self.uiFileMenu.insertActions(self.uiQuitAction, self._recent_file_actions)
|
||||
self._recent_file_actions_separator = self.uiFileMenu.insertSeparator(self.uiQuitAction)
|
||||
self._recent_file_actions_separator.setVisible(False)
|
||||
self.recent_file_actions.append(action)
|
||||
self.uiFileMenu.insertActions(self.uiQuitAction, self.recent_file_actions)
|
||||
self.recent_file_actions_separator = self.uiFileMenu.insertSeparator(self.uiQuitAction)
|
||||
self.recent_file_actions_separator.setVisible(False)
|
||||
self.updateRecentFileActions()
|
||||
|
||||
# add recent file actions to the File menu
|
||||
for i in range(0, self._max_recent_files):
|
||||
action = QtWidgets.QAction(self.uiProjectMenu)
|
||||
# add recent projects to the File menu
|
||||
for i in range(0, self._maxrecent_files):
|
||||
action = QtWidgets.QAction(self.uiFileMenu)
|
||||
action.setVisible(False)
|
||||
action.triggered.connect(self.openRecentProjectSlot)
|
||||
self._recent_project_actions.append(action)
|
||||
self._recent_project_actions_separator = self.uiProjectMenu.addSeparator()
|
||||
self._recent_project_actions_separator.setVisible(False)
|
||||
|
||||
self.uiProjectMenu.addActions(self._recent_project_actions)
|
||||
|
||||
self.updateRecentProjectActions()
|
||||
self.recent_project_actions.append(action)
|
||||
self.recent_project_actions_separator = self.uiFileMenu.addSeparator()
|
||||
self.recent_project_actions_separator.setVisible(False)
|
||||
self.uiFileMenu.addActions(self.recent_project_actions)
|
||||
|
||||
# set the window icon
|
||||
self.setWindowIcon(QtGui.QIcon(":/images/gns3.ico"))
|
||||
@@ -215,6 +223,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiResetPortLabelsAction.triggered.connect(self._resetPortLabelsActionSlot)
|
||||
self.uiShowPortNamesAction.triggered.connect(self._showPortNamesActionSlot)
|
||||
self.uiShowGridAction.triggered.connect(self._showGridActionSlot)
|
||||
self.uiSnapToGridAction.triggered.connect(self._snapToGridActionSlot)
|
||||
|
||||
# tool menu connections
|
||||
self.uiWebInterfaceAction.triggered.connect(self._openWebInterfaceActionSlot)
|
||||
|
||||
# control menu connections
|
||||
self.uiStartAllAction.triggered.connect(self._startAllActionSlot)
|
||||
@@ -232,6 +244,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiInsertImageAction.triggered.connect(self._insertImageActionSlot)
|
||||
self.uiDrawRectangleAction.triggered.connect(self._drawRectangleActionSlot)
|
||||
self.uiDrawEllipseAction.triggered.connect(self._drawEllipseActionSlot)
|
||||
self.uiDrawLineAction.triggered.connect(self._drawLineActionSlot)
|
||||
self.uiEditReadmeAction.triggered.connect(self._editReadmeActionSlot)
|
||||
|
||||
# help menu connections
|
||||
@@ -291,12 +304,34 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
# save the settings
|
||||
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
|
||||
|
||||
def _openWebInterfaceActionSlot(self):
|
||||
if Controller.instance().connected():
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(Controller.instance().httpClient().fullUrl()))
|
||||
|
||||
def _showGridActionSlot(self):
|
||||
"""
|
||||
Called when we ask to display the grid
|
||||
"""
|
||||
self.showGrid(self.uiShowGridAction.isChecked())
|
||||
|
||||
self.uiGraphicsView.viewport().update()
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setShowGrid(self.uiShowGridAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def _snapToGridActionSlot(self):
|
||||
"""
|
||||
Called when user click on the snap to grid menu item
|
||||
:return: None
|
||||
"""
|
||||
self.snapToGrid(self.uiSnapToGridAction.isChecked())
|
||||
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setSnapToGrid(self.uiSnapToGridAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def analyticsClient(self):
|
||||
"""
|
||||
@@ -309,6 +344,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Slot called to create a new project.
|
||||
"""
|
||||
|
||||
# prevents race condition
|
||||
if self._project_dialog is not None:
|
||||
return
|
||||
|
||||
self._project_dialog = ProjectDialog(self)
|
||||
self._project_dialog.show()
|
||||
create_new_project = self._project_dialog.exec_()
|
||||
@@ -317,7 +357,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiNodesDockWidget.setWindowTitle("")
|
||||
|
||||
if create_new_project:
|
||||
Topology.instance().createLoadProject(self._project_dialog.getProjectSettings())
|
||||
Topology.instance().createLoadProject(
|
||||
self._project_dialog.getProjectSettings())
|
||||
|
||||
self._project_dialog = None
|
||||
|
||||
@@ -329,7 +370,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
def openApplianceActionSlot(self):
|
||||
# No projects
|
||||
if Topology.instance().project() is None:
|
||||
if self._open_file_at_startup:
|
||||
self.loadPath(self._open_file_at_startup)
|
||||
self._open_file_at_startup = None
|
||||
else:
|
||||
self._newProjectActionSlot()
|
||||
|
||||
@qslot
|
||||
def openApplianceActionSlot(self, *args):
|
||||
"""
|
||||
Slot called to open an appliance.
|
||||
"""
|
||||
@@ -377,9 +427,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
action = self.sender()
|
||||
if action:
|
||||
project_id = action.data()
|
||||
Topology.instance().createLoadProject({"project_id": project_id})
|
||||
if action and action.data():
|
||||
if len(action.data()) == 2:
|
||||
project_id, project_path = action.data()
|
||||
Topology.instance().createLoadProject({
|
||||
"project_path": project_path,
|
||||
"project_id": project_id})
|
||||
else:
|
||||
(project_id, ) = action.data()
|
||||
Topology.instance().createLoadProject({"project_id": project_id})
|
||||
|
||||
def loadPath(self, path):
|
||||
"""Open a file and close the previous project"""
|
||||
@@ -398,6 +454,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
if path.endswith(".gns3project") or path.endswith(".gns3p"):
|
||||
# Portable GNS3 project
|
||||
Topology.instance().importProject(path)
|
||||
elif path.endswith(".net"):
|
||||
QtWidgets.QMessageBox.critical(self, "Open project", "Importing legacy project is not supported in 2.0.\nYou must open it using GNS3 1.x in order to convert it or manually run the gns3 converter.")
|
||||
return
|
||||
|
||||
elif path.endswith(".gns3appliance") or path.endswith(".gns3a"):
|
||||
# GNS3 appliance
|
||||
@@ -408,16 +467,28 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
self._appliance_wizard.show()
|
||||
self._appliance_wizard.exec_()
|
||||
else:
|
||||
elif path.endswith(".gns3"):
|
||||
if Controller.instance().isRemote():
|
||||
QtWidgets.QMessageBox.critical(self, "Project", "You can't remote open a .gns3 please use import / export in order to provide to the remote server the full project")
|
||||
QtWidgets.QMessageBox.critical(self, "Open project", "Cannot open a .gns3 file on a remote server, please use a portable project (.gns3p) instead")
|
||||
return
|
||||
Topology.instance().loadProject(path)
|
||||
else:
|
||||
Topology.instance().loadProject(path)
|
||||
else:
|
||||
try:
|
||||
extension = path.split('.')[1]
|
||||
QtWidgets.QMessageBox.critical(self, "File open", "Unsupported file extension {} for {}".format(extension, path))
|
||||
except IndexError:
|
||||
QtWidgets.QMessageBox.critical(self, "File open", "Missing file extension for {}".format(path))
|
||||
|
||||
def _projectChangedSlot(self):
|
||||
@qslot
|
||||
def _projectChangedSlot(self, *args):
|
||||
"""
|
||||
Called when a project finish to load
|
||||
"""
|
||||
project = Topology.instance().project()
|
||||
if project is not None and self._project_dialog:
|
||||
self._project_dialog.reject()
|
||||
self._project_dialog = None
|
||||
self._refreshVisibleWidgets()
|
||||
|
||||
def _refreshVisibleWidgets(self):
|
||||
@@ -503,6 +574,60 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
# TODO: quality option
|
||||
return image.save(path)
|
||||
|
||||
def showLayers(self, show_layers):
|
||||
"""
|
||||
Shows layers in GUI
|
||||
:param show_layers: boolean
|
||||
:return: None
|
||||
"""
|
||||
NodeItem.show_layer = show_layers
|
||||
ShapeItem.show_layer = show_layers
|
||||
for item in self.uiGraphicsView.items():
|
||||
item.update()
|
||||
|
||||
def showGrid(self, show_grid):
|
||||
"""
|
||||
Shows grid in GUI
|
||||
:param show_grid: boolean
|
||||
:return: None
|
||||
"""
|
||||
self.uiGraphicsView.viewport().update()
|
||||
|
||||
def snapToGrid(self, snap_to_grid):
|
||||
"""
|
||||
Snap to grid in GUI
|
||||
:param snap_to_grid: boolean
|
||||
:return: None
|
||||
"""
|
||||
self.uiGraphicsView.viewport().update()
|
||||
|
||||
def showInterfaceLabels(self, show_interface_labels):
|
||||
"""
|
||||
Show interface labels in GUI
|
||||
:param show_interface_labels: boolean
|
||||
:return: None
|
||||
"""
|
||||
LinkItem.showPortLabels(show_interface_labels)
|
||||
for item in self.uiGraphicsView.scene().items():
|
||||
if isinstance(item, LinkItem):
|
||||
item.adjust()
|
||||
|
||||
def _updateZoomSettings(self, zoom=None):
|
||||
"""
|
||||
Updates zoom settings
|
||||
:param zoom integer optional, when not provided then calculated from current view
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if zoom is None:
|
||||
zoom = round(self.uiGraphicsView.transform().m11() * 100)
|
||||
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setZoom(zoom)
|
||||
project.update()
|
||||
|
||||
def _screenshotActionSlot(self):
|
||||
"""
|
||||
Slot called to take a screenshot of the scene.
|
||||
@@ -515,10 +640,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
self._screenshots_dir = os.path.dirname(path)
|
||||
|
||||
# add the extension if missing
|
||||
file_format = "." + selected_filter[:4].lower().strip()
|
||||
if not path.endswith(file_format):
|
||||
path += file_format
|
||||
# add the extension if missing (Mac OS automatically adds an extension already)
|
||||
if not sys.platform.startswith("darwin"):
|
||||
file_format = "." + selected_filter[:4].lower().strip()
|
||||
if not path.endswith(file_format):
|
||||
path += file_format
|
||||
|
||||
if not self.createScreenshot(path):
|
||||
QtWidgets.QMessageBox.critical(self, "Screenshot", "Could not create screenshot file {}".format(path))
|
||||
@@ -571,6 +697,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
factor_in = pow(2.0, 120 / 240.0)
|
||||
self.uiGraphicsView.scaleView(factor_in)
|
||||
self._updateZoomSettings()
|
||||
|
||||
def _zoomOutActionSlot(self):
|
||||
"""
|
||||
@@ -579,6 +706,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
factor_out = pow(2.0, -120 / 240.0)
|
||||
self.uiGraphicsView.scaleView(factor_out)
|
||||
self._updateZoomSettings()
|
||||
|
||||
def _zoomResetActionSlot(self):
|
||||
"""
|
||||
@@ -586,6 +714,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
self.uiGraphicsView.resetTransform()
|
||||
self._updateZoomSettings()
|
||||
|
||||
def _fitInViewActionSlot(self):
|
||||
"""
|
||||
@@ -601,11 +730,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Slot called to show the layer positions on the scene.
|
||||
"""
|
||||
self.showLayers(self.uiShowLayersAction.isChecked())
|
||||
|
||||
NodeItem.show_layer = self.uiShowLayersAction.isChecked()
|
||||
ShapeItem.show_layer = self.uiShowLayersAction.isChecked()
|
||||
for item in self.uiGraphicsView.items():
|
||||
item.update()
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setShowLayers(self.uiShowLayersAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def _resetPortLabelsActionSlot(self):
|
||||
"""
|
||||
@@ -622,16 +753,18 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Slot called to show the port names on the scene.
|
||||
"""
|
||||
|
||||
LinkItem.showPortLabels(self.uiShowPortNamesAction.isChecked())
|
||||
for item in self.uiGraphicsView.scene().items():
|
||||
if isinstance(item, LinkItem):
|
||||
item.adjust()
|
||||
self.showInterfaceLabels(self.uiShowPortNamesAction.isChecked())
|
||||
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setShowInterfaceLabels(self.uiShowPortNamesAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def _startAllActionSlot(self):
|
||||
"""
|
||||
Slot called when starting all the nodes.
|
||||
"""
|
||||
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.start_all_nodes()
|
||||
@@ -683,7 +816,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Slot called when connecting to all the nodes using the console.
|
||||
"""
|
||||
|
||||
self.uiGraphicsView.consoleFromItems(self.uiGraphicsView.scene().items())
|
||||
self.uiGraphicsView.consoleFromAllItems()
|
||||
|
||||
def _addNoteActionSlot(self):
|
||||
"""
|
||||
@@ -704,7 +837,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
self._pictures_dir = os.path.dirname(path)
|
||||
|
||||
image = QtGui.QPixmap(path)
|
||||
QtGui.QPixmap(path)
|
||||
self.uiGraphicsView.addImage(path)
|
||||
|
||||
def _drawRectangleActionSlot(self):
|
||||
@@ -721,6 +854,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
self.uiGraphicsView.addEllipse(self.uiDrawEllipseAction.isChecked())
|
||||
|
||||
def _drawLineActionSlot(self):
|
||||
"""
|
||||
Slot called when adding a line on the scene.
|
||||
"""
|
||||
|
||||
self.uiGraphicsView.addLine(self.uiDrawLineAction.isChecked())
|
||||
|
||||
def _onlineHelpActionSlot(self):
|
||||
"""
|
||||
Slot to launch a browser pointing to the documentation page.
|
||||
@@ -746,7 +886,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
with Progress.instance().context(min_duration=0):
|
||||
setup_wizard = SetupWizard(self)
|
||||
setup_wizard.show()
|
||||
setup_wizard.exec_()
|
||||
res = setup_wizard.exec_()
|
||||
# start and connect to the local server if needed
|
||||
LocalServer.instance().localServerAutoStartIfRequire()
|
||||
if res:
|
||||
self._newApplianceActionSlot()
|
||||
|
||||
def _aboutQtActionSlot(self):
|
||||
"""
|
||||
@@ -805,8 +949,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
else:
|
||||
self.uiNodesDockWidget.setWindowTitle(title)
|
||||
self.uiNodesDockWidget.setVisible(True)
|
||||
self.uiNodesView.clear()
|
||||
self.uiNodesView.populateNodesView(category)
|
||||
self.uiNodesDockWidget.populateNodesView(category)
|
||||
|
||||
def _localConfigChangedSlot(self):
|
||||
"""
|
||||
@@ -881,6 +1024,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Topology.instance().editReadme()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._notif_dialog.resize()
|
||||
super().resizeEvent(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events for the main window.
|
||||
@@ -903,6 +1050,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
:param event: QCloseEvent
|
||||
"""
|
||||
|
||||
if Topology.instance().project():
|
||||
reply = QtWidgets.QMessageBox.question(self, "Confirm Exit", "Are you sure you want to exit GNS3?",
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
event.ignore()
|
||||
return
|
||||
|
||||
progress = Progress.instance()
|
||||
progress.setAllowCancelQuery(True)
|
||||
progress.setCancelButtonText("Force quit")
|
||||
@@ -910,21 +1064,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
log.debug("Close the Main Window")
|
||||
self._analytics_client.sendScreenView("Main Window", session_start=False)
|
||||
|
||||
project = Topology.instance().project()
|
||||
if not project:
|
||||
self._finish_application_closing(close_windows=False)
|
||||
event.accept()
|
||||
self.uiConsoleTextEdit.closeIO()
|
||||
elif project.closed() or not project.autoClose():
|
||||
log.debug("Project is closed killing server and closing main windows")
|
||||
self._finish_application_closing(close_windows=False)
|
||||
event.accept()
|
||||
self.uiConsoleTextEdit.closeIO()
|
||||
else:
|
||||
log.debug("Project is not closed asking for project closing")
|
||||
project.project_closed_signal.connect(self._finish_application_closing)
|
||||
project.close(local_server_shutdown=True)
|
||||
event.ignore()
|
||||
self._finish_application_closing(close_windows=False)
|
||||
event.accept()
|
||||
self.uiConsoleTextEdit.closeIO()
|
||||
|
||||
def _finish_application_closing(self, close_windows=True):
|
||||
"""
|
||||
@@ -972,12 +1114,31 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
reply = QtWidgets.QMessageBox.warning(self, "GNS3", "Another GNS3 GUI is already running. Continue?",
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
self.close()
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
run_as_root_path = LocalConfig.instance().runAsRootPath()
|
||||
|
||||
if not sys.platform.startswith("win") and os.geteuid() == 0:
|
||||
# touches file to know that user has run GNS3 as root and to prevent
|
||||
# from running as user
|
||||
if not os.path.exists(run_as_root_path):
|
||||
try:
|
||||
open(run_as_root_path, 'a').close()
|
||||
except OSError as e:
|
||||
log.warning("Cannot write `run_as_root` file due to: {}".format(str(e)))
|
||||
|
||||
QtWidgets.QMessageBox.warning(self, "Root", "Running GNS3 as root is not recommended and could be dangerous")
|
||||
|
||||
if not sys.platform.startswith("win") and os.geteuid() != 0 and os.path.exists(run_as_root_path):
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self, "Run as user",
|
||||
"GNS3 has been previously run as root. It is not possible "
|
||||
"to change to another user and GNS3 will be shutdown. Please delete the '{}' file "
|
||||
"and start the program again.".format(run_as_root_path))
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
# restore debug level
|
||||
if self._settings["debug_level"]:
|
||||
root = logging.getLogger()
|
||||
@@ -987,24 +1148,22 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._setStyle(self._settings.get("style"))
|
||||
|
||||
Controller.instance().connected_signal.connect(self._controllerConnectedSlot)
|
||||
Controller.instance().project_list_updated_signal.connect(self.updateRecentProjectActions)
|
||||
|
||||
# start and connect to the local server if needed
|
||||
LocalServer.instance().localServerAutoStartIfRequire()
|
||||
self._analytics_client.sendScreenView("Main Window")
|
||||
self.uiGraphicsView.setEnabled(False)
|
||||
|
||||
# show the setup wizard
|
||||
if not self._settings["hide_setup_wizard"]:
|
||||
with Progress.instance().context(min_duration=0):
|
||||
setup_wizard = SetupWizard(self)
|
||||
setup_wizard.show()
|
||||
setup_wizard.exec_()
|
||||
|
||||
self._analytics_client.sendScreenView("Main Window")
|
||||
|
||||
self.uiGraphicsView.setEnabled(False)
|
||||
if self._open_file_at_startup:
|
||||
self.loadPath(self._open_file_at_startup)
|
||||
self._setupWizardActionSlot()
|
||||
else:
|
||||
self._newProjectActionSlot()
|
||||
# start and connect to the local server if needed
|
||||
LocalServer.instance().localServerAutoStartIfRequire()
|
||||
if self._open_file_at_startup:
|
||||
self.loadPath(self._open_file_at_startup)
|
||||
self._open_file_at_startup = None
|
||||
elif Topology.instance().project() is None:
|
||||
self._newProjectActionSlot()
|
||||
|
||||
if self._settings["check_for_update"]:
|
||||
# automatic check for update every week (604800 seconds)
|
||||
@@ -1015,28 +1174,36 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._settings["last_check_for_update"] = current_epoch
|
||||
self.setSettings(self._settings)
|
||||
|
||||
def updateRecentProjectsSettings(self, project_id, project_name):
|
||||
def updateRecentProjectsSettings(self, project_id, project_name, project_path):
|
||||
"""
|
||||
Updates the recent project settings.
|
||||
|
||||
:param project_id: The ID of the project
|
||||
:param project_name: The name of the project
|
||||
:param project_path: The project path
|
||||
"""
|
||||
|
||||
# Projects are stored as a list of project_id:project_name
|
||||
key = "{}:{}".format(project_id, project_name)
|
||||
key = "{}:{}:{}".format(project_id, project_name, project_path)
|
||||
|
||||
recent_projects = []
|
||||
for project in self._settings["recent_projects"]:
|
||||
recent_projects.append(project)
|
||||
|
||||
# Because the name can change we compare only the project id
|
||||
# Because the name can change we compare only the project id and path
|
||||
for project_key in list(recent_projects):
|
||||
if project_key.split(":")[0] == project_id:
|
||||
recent_projects.remove(project_key)
|
||||
for project_key in list(recent_projects):
|
||||
try:
|
||||
if project_key.split(":")[2] == project_path:
|
||||
recent_projects.remove(project_key)
|
||||
# 2.0.0 alpha1 compatible
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
recent_projects.insert(0, key)
|
||||
if len(recent_projects) > self._max_recent_files:
|
||||
if len(recent_projects) > self._maxrecent_files:
|
||||
recent_projects.pop()
|
||||
|
||||
# write the recent file list
|
||||
@@ -1052,18 +1219,37 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
size = len(self._settings["recent_projects"])
|
||||
for project in self._settings["recent_projects"]:
|
||||
# Projects are stored as a list of project_id:project_name
|
||||
project_id, project_name = project.split(":", maxsplit=1)
|
||||
action = self._recent_project_actions[index]
|
||||
action.setText(" {}. {}".format(index + 1, project_name))
|
||||
action.setData(project_id)
|
||||
action.setVisible(True)
|
||||
try:
|
||||
project_id, project_name, project_path = project.split(":", maxsplit=2)
|
||||
except ValueError: # Compatible with 2.0.0a1
|
||||
project_path = None
|
||||
project_id, project_name = project.split(":", maxsplit=1)
|
||||
|
||||
if project_id not in [p["project_id"] for p in Controller.instance().projects()]:
|
||||
size -= 1
|
||||
continue
|
||||
|
||||
action = self.recent_project_actions[index]
|
||||
if project_path and os.path.exists(project_path):
|
||||
action.setText(" {}. {} [{}]".format(index + 1, project_name, project_path))
|
||||
action.setData((project_id, project_path, ))
|
||||
else:
|
||||
action.setText(" {}. {}".format(index + 1, project_name))
|
||||
action.setData((project_id, ))
|
||||
index += 1
|
||||
|
||||
for index in range(size + 1, self._max_recent_files):
|
||||
self._recent_project_actions[index].setVisible(False)
|
||||
if Controller.instance().isRemote():
|
||||
for index in range(0, size):
|
||||
self.recent_project_actions[index].setVisible(True)
|
||||
for index in range(size + 1, self._maxrecent_files):
|
||||
self.recent_project_actions[index].setVisible(False)
|
||||
|
||||
if size:
|
||||
self._recent_project_actions_separator.setVisible(True)
|
||||
if size:
|
||||
self.recent_project_actions_separator.setVisible(True)
|
||||
else:
|
||||
for action in self.recent_project_actions:
|
||||
action.setVisible(False)
|
||||
self.recent_project_actions_separator.setVisible(False)
|
||||
|
||||
def updateRecentFileSettings(self, path):
|
||||
"""
|
||||
@@ -1083,7 +1269,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
if path in recent_files:
|
||||
recent_files.remove(path)
|
||||
recent_files.insert(0, path)
|
||||
if len(recent_files) > self._max_recent_files:
|
||||
if len(recent_files) > self._maxrecent_files:
|
||||
recent_files.pop()
|
||||
|
||||
# write the recent file list
|
||||
@@ -1100,7 +1286,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
for file_path in self._settings["recent_files"]:
|
||||
try:
|
||||
if file_path and os.path.exists(file_path):
|
||||
action = self._recent_file_actions[index]
|
||||
action = self.recent_file_actions[index]
|
||||
action.setText(" {}. {}".format(index + 1, os.path.basename(file_path)))
|
||||
action.setData(file_path)
|
||||
action.setVisible(True)
|
||||
@@ -1111,15 +1297,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
pass
|
||||
|
||||
if not Controller.instance().isRemote():
|
||||
for index in range(size + 1, self._max_recent_files):
|
||||
self._recent_file_actions[index].setVisible(False)
|
||||
for index in range(size + 1, self._maxrecent_files):
|
||||
self.recent_file_actions[index].setVisible(False)
|
||||
|
||||
if size:
|
||||
self._recent_file_actions_separator.setVisible(True)
|
||||
self.recent_file_actions_separator.setVisible(True)
|
||||
else:
|
||||
for index in range(0, self._max_recent_files):
|
||||
self._recent_file_actions[index].setVisible(False)
|
||||
self._recent_file_actions_separator.setVisible(False)
|
||||
for index in range(0, self._maxrecent_files):
|
||||
self.recent_file_actions[index].setVisible(False)
|
||||
self.recent_file_actions_separator.setVisible(False)
|
||||
|
||||
def _controllerConnectedSlot(self):
|
||||
self.updateRecentFileActions()
|
||||
@@ -1156,11 +1342,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Topology.instance().importProject(path)
|
||||
|
||||
def _editProjectActionSlot(self):
|
||||
if Topology.instance().project() is None:
|
||||
return
|
||||
dialog = EditProjectDialog(self)
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
def _deleteProjectActionSlot(self):
|
||||
if Topology.instance().project() is None:
|
||||
return
|
||||
reply = QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
"GNS3",
|
||||
|
||||
@@ -33,7 +33,6 @@ from .atm_switch import ATMSwitch
|
||||
from .settings import (
|
||||
BUILTIN_SETTINGS,
|
||||
CLOUD_SETTINGS,
|
||||
NAT_SETTINGS,
|
||||
ETHERNET_HUB_SETTINGS,
|
||||
ETHERNET_SWITCH_SETTINGS
|
||||
)
|
||||
@@ -224,41 +223,9 @@ class Builtin(Module):
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("creating node {}".format(node))
|
||||
if isinstance(node, Cloud):
|
||||
for key, info in self._cloud_nodes.items():
|
||||
if node_name == info["name"]:
|
||||
node.create(ports=info["ports_mapping"], default_name_format=info["default_name_format"])
|
||||
return
|
||||
elif isinstance(node, Nat):
|
||||
for key, info in self._nat_nodes.items():
|
||||
if node_name == info["name"]:
|
||||
node.create(default_name_format=info["default_name_format"])
|
||||
return
|
||||
elif isinstance(node, EthernetHub):
|
||||
for key, info in self._ethernet_hubs.items():
|
||||
if node_name == info["name"]:
|
||||
node.create(ports=info["ports_mapping"], default_name_format=info["default_name_format"])
|
||||
return
|
||||
elif isinstance(node, EthernetSwitch):
|
||||
for key, info in self._ethernet_switches.items():
|
||||
if node_name == info["name"]:
|
||||
node.create(ports=info["ports_mapping"], default_name_format=info["default_name_format"])
|
||||
return
|
||||
node.create()
|
||||
|
||||
@staticmethod
|
||||
def findAlternativeInterface(node, missing_interface):
|
||||
|
||||
@@ -326,17 +293,6 @@ class Builtin(Module):
|
||||
"""
|
||||
|
||||
nodes = []
|
||||
for node_class in Builtin.classes():
|
||||
nodes.append(
|
||||
{"class": node_class.__name__,
|
||||
"name": node_class.symbolName(),
|
||||
"categories": node_class.categories(),
|
||||
"symbol": node_class.defaultSymbol(),
|
||||
"builtin": True,
|
||||
"node_type": node_class.URL_PREFIX
|
||||
}
|
||||
)
|
||||
|
||||
# add custom cloud node templates
|
||||
for cloud_node in self._cloud_nodes.values():
|
||||
nodes.append(
|
||||
@@ -367,9 +323,8 @@ class Builtin(Module):
|
||||
"server": switch["server"],
|
||||
"symbol": switch["symbol"],
|
||||
"categories": [switch["category"]]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -44,20 +44,6 @@ class ATMSwitch(Node):
|
||||
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.
|
||||
@@ -167,28 +153,6 @@ class ATMSwitch(Node):
|
||||
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.
|
||||
|
||||
@@ -46,20 +46,6 @@ class Cloud(Node):
|
||||
|
||||
return self._interfaces
|
||||
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="Cloud{0}"):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
@@ -108,7 +94,9 @@ class Cloud(Node):
|
||||
|
||||
info = """Cloud device {name} is always-on
|
||||
This is a node for external connections
|
||||
""".format(name=self.name())
|
||||
Device run on {host}
|
||||
""".format(name=self.name(),
|
||||
host=self.compute().name())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
|
||||
@@ -37,7 +37,7 @@ class CloudWizard(VMWizard, Ui_CloudNodeWizard):
|
||||
|
||||
def __init__(self, cloud_nodes, parent):
|
||||
|
||||
super().__init__(cloud_nodes, Builtin.instance().settings()["use_local_server"], parent)
|
||||
super().__init__(cloud_nodes, parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/cloud.svg"))
|
||||
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
|
||||
@@ -37,7 +37,7 @@ class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
|
||||
|
||||
def __init__(self, ethernet_hubs, parent):
|
||||
|
||||
super().__init__(ethernet_hubs, Builtin.instance().settings()["use_local_server"], parent)
|
||||
super().__init__(ethernet_hubs, parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/hub.svg"))
|
||||
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
@@ -50,7 +50,7 @@ class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
|
||||
"""
|
||||
|
||||
ports = []
|
||||
for port_number in range(1, self.uiPortsSpinBox.value() + 1):
|
||||
for port_number in range(0, self.uiPortsSpinBox.value()):
|
||||
ports.append({"port_number": int(port_number),
|
||||
"name": "Ethernet{}".format(port_number)})
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
|
||||
|
||||
def __init__(self, ethernet_switches, parent):
|
||||
|
||||
super().__init__(ethernet_switches, Builtin.instance().settings()["use_local_server"], parent)
|
||||
super().__init__(ethernet_switches, parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/ethernet_switch.svg"))
|
||||
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
@@ -50,7 +50,7 @@ class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
|
||||
"""
|
||||
|
||||
ports = []
|
||||
for port_number in range(1, self.uiPortsSpinBox.value() + 1):
|
||||
for port_number in range(0, self.uiPortsSpinBox.value()):
|
||||
ports.append({"port_number": int(port_number),
|
||||
"name": "Ethernet{}".format(port_number),
|
||||
"type": "access",
|
||||
|
||||
@@ -39,20 +39,6 @@ class EthernetHub(Node):
|
||||
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.
|
||||
@@ -98,7 +84,7 @@ class EthernetHub(Node):
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self.compute().id())
|
||||
host=self.compute().name())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
|
||||
@@ -38,21 +38,7 @@ class EthernetSwitch(Node):
|
||||
# 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)
|
||||
self.settings().update({"ports_mapping": [], "console": None, "console_type": "telnet"})
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
@@ -61,6 +47,10 @@ class EthernetSwitch(Node):
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
self.settings()["console"] = result["console"]
|
||||
|
||||
def console(self):
|
||||
return self.settings()["console"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
@@ -98,7 +88,7 @@ class EthernetSwitch(Node):
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self.compute().id())
|
||||
host=self.compute().name())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
@@ -126,7 +116,7 @@ class EthernetSwitch(Node):
|
||||
port_ethertype_info=port_ethertype_info,
|
||||
port_vlan_info=port_vlan_info)
|
||||
port_info += " {port_description}\n".format(port_description=port.description())
|
||||
break
|
||||
break
|
||||
|
||||
return info + port_info
|
||||
|
||||
|
||||
@@ -41,20 +41,6 @@ class FrameRelaySwitch(Node):
|
||||
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.
|
||||
@@ -96,12 +82,11 @@ class FrameRelaySwitch(Node):
|
||||
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}
|
||||
Switch's server runs on {host}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self._compute.host(),
|
||||
port=self._compute.port())
|
||||
host=self._compute.name())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
|
||||
@@ -46,17 +46,6 @@ class Nat(Node):
|
||||
|
||||
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.
|
||||
@@ -101,7 +90,9 @@ class Nat(Node):
|
||||
|
||||
info = """Nat device {name} is always-on
|
||||
This is a node for external connections
|
||||
""".format(name=self.name())
|
||||
Device run on {host}
|
||||
""".format(name=self.name(),
|
||||
host=self.compute().name())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
|
||||
@@ -47,8 +47,6 @@ class BuiltinPreferencesPage(QtWidgets.QWidget, Ui_BuiltinPreferencesPageWidget)
|
||||
:param settings: Built-in settings
|
||||
"""
|
||||
|
||||
self.uiUseLocalServercheckBox.setChecked(settings["use_local_server"])
|
||||
|
||||
def loadPreferences(self):
|
||||
"""Loads Built-in preferences."""
|
||||
|
||||
@@ -59,5 +57,4 @@ class BuiltinPreferencesPage(QtWidgets.QWidget, Ui_BuiltinPreferencesPageWidget)
|
||||
"""Saves Built-in preferences."""
|
||||
|
||||
new_settings = {}
|
||||
new_settings["use_local_server"] = self.uiUseLocalServercheckBox.isChecked()
|
||||
Builtin.instance().setSettings(new_settings)
|
||||
|
||||
@@ -19,13 +19,12 @@
|
||||
Configuration page for clouds.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui, QtCore, QtWidgets
|
||||
from gns3.qt import QtCore, QtGui, 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
|
||||
from ..cloud import Cloud
|
||||
|
||||
|
||||
class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
@@ -48,6 +47,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
|
||||
# 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)
|
||||
@@ -68,6 +68,12 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiShowSpecialInterfacesCheckBox.stateChanged.connect(self._showSpecialInterfacesSlot)
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
|
||||
# add an icon to warning button
|
||||
icon = QtGui.QIcon.fromTheme("dialog-warning")
|
||||
if icon.isNull():
|
||||
icon = QtGui.QIcon(':/icons/dialog-warning.svg')
|
||||
self.uiEthernetWarningPushButton.setIcon(icon)
|
||||
|
||||
def _EthernetChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
@@ -79,6 +85,13 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
else:
|
||||
self.uiDeleteEthernetPushButton.setEnabled(False)
|
||||
|
||||
def _EthernetWarningSlot(self):
|
||||
"""
|
||||
Shows a warning about Wifi Ethernet interfaces.
|
||||
"""
|
||||
|
||||
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.
|
||||
@@ -394,9 +407,9 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
if index != -1:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
|
||||
Controller.instance().get("/computes/{}/network/interfaces".format(settings["server"]),
|
||||
self._getInterfacesFromServerCallback,
|
||||
progressText="Retrieving network interfaces...")
|
||||
Controller.instance().getCompute("/network/interfaces", settings["server"],
|
||||
self._getInterfacesFromServerCallback,
|
||||
progressText="Retrieving network interfaces...")
|
||||
|
||||
else:
|
||||
self.uiDefaultNameFormatLabel.hide()
|
||||
|
||||
@@ -69,7 +69,10 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
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"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["server"]).name()])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["server"]).name()])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.uiCloudNodeInfoTreeWidget.expandAll()
|
||||
self.uiCloudNodeInfoTreeWidget.resizeColumnToContents(0)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
Configuration page for Ethernet hubs.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
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
|
||||
@@ -146,7 +146,7 @@ class EthernetHubConfigurationPage(QtWidgets.QWidget, Ui_ethernetHubConfigPageWi
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
|
||||
settings["ports_mapping"] = []
|
||||
for port_number in range(1, nb_ports + 1):
|
||||
for port_number in range(0, nb_ports):
|
||||
settings["ports_mapping"].append({"port_number": int(port_number),
|
||||
"name": "Ethernet{}".format(port_number)})
|
||||
"name": "Ethernet{}".format(port_number)})
|
||||
return settings
|
||||
|
||||
@@ -70,7 +70,10 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
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"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["server"]).name()])
|
||||
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()
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
Configuration page for Ethernet switches.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui, QtCore, QtWidgets
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.node import Node
|
||||
|
||||
|
||||
@@ -70,7 +70,10 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
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"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["server"]).name()])
|
||||
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"]))
|
||||
@@ -134,7 +137,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(ethernet_switches["symbol"], qpartial(self._setItemIcon, item))
|
||||
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:
|
||||
|
||||
@@ -22,18 +22,9 @@ Default Built-in settings.
|
||||
from gns3.node import Node
|
||||
|
||||
BUILTIN_SETTINGS = {
|
||||
"use_local_server": True,
|
||||
}
|
||||
|
||||
|
||||
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}",
|
||||
|
||||
@@ -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:"))
|
||||
|
||||
|
||||
@@ -24,19 +24,9 @@
|
||||
</property>
|
||||
<widget class="QWidget" name="uiServerSettingsTabWidget">
|
||||
<attribute name="title">
|
||||
<string>General settings</string>
|
||||
<string>Local settings</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="uiUseLocalServercheckBox">
|
||||
<property name="text">
|
||||
<string>Use the local server</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/builtin_preferences_page.ui'
|
||||
#
|
||||
# Created: Thu Jun 9 21:08:46 2016
|
||||
# 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!
|
||||
@@ -22,10 +22,6 @@ class Ui_BuiltinPreferencesPageWidget(object):
|
||||
self.uiServerSettingsTabWidget.setObjectName("uiServerSettingsTabWidget")
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.uiServerSettingsTabWidget)
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.uiUseLocalServercheckBox = QtWidgets.QCheckBox(self.uiServerSettingsTabWidget)
|
||||
self.uiUseLocalServercheckBox.setChecked(True)
|
||||
self.uiUseLocalServercheckBox.setObjectName("uiUseLocalServercheckBox")
|
||||
self.verticalLayout_2.addWidget(self.uiUseLocalServercheckBox)
|
||||
spacerItem = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout_2.addItem(spacerItem)
|
||||
self.uiTabWidget.addTab(self.uiServerSettingsTabWidget, "")
|
||||
@@ -46,7 +42,6 @@ class Ui_BuiltinPreferencesPageWidget(object):
|
||||
def retranslateUi(self, BuiltinPreferencesPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
BuiltinPreferencesPageWidget.setWindowTitle(_translate("BuiltinPreferencesPageWidget", "Built-in"))
|
||||
self.uiUseLocalServercheckBox.setText(_translate("BuiltinPreferencesPageWidget", "Use the local server"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("BuiltinPreferencesPageWidget", "General settings"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("BuiltinPreferencesPageWidget", "Local settings"))
|
||||
self.uiRestoreDefaultsPushButton.setText(_translate("BuiltinPreferencesPageWidget", "Restore defaults"))
|
||||
|
||||
|
||||
@@ -6,13 +6,16 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>758</width>
|
||||
<height>299</height>
|
||||
<width>821</width>
|
||||
<height>363</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Cloud configuration</string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p>A cloud node allows you to connect your project to the &quot;real world&quot; (a network or host) using either an Ethernet interface, a TAP interface (Linux only) or even an UDP tunnel. <span style=" font-weight:600;">Please be aware that Wifi interfaces may not work properly.</span></p></body></html></string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="uiTabWidget">
|
||||
@@ -40,21 +43,21 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<item row="0" column="2">
|
||||
<widget class="QPushButton" name="uiAddEthernetPushButton">
|
||||
<property name="text">
|
||||
<string>&Add</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<item row="0" column="3">
|
||||
<widget class="QPushButton" name="uiAddAllEthernetPushButton">
|
||||
<property name="text">
|
||||
<string>&Add all</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<item row="0" column="4">
|
||||
<widget class="QPushButton" name="uiDeleteEthernetPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
@@ -64,7 +67,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="4">
|
||||
<item row="1" column="0" colspan="5">
|
||||
<widget class="QListWidget" name="uiEthernetListWidget">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
@@ -74,7 +77,14 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="uiEthernetWarningPushButton">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiShowSpecialInterfacesCheckBox">
|
||||
<property name="text">
|
||||
<string>&Show special Ethernet interfaces</string>
|
||||
@@ -88,6 +98,7 @@
|
||||
<zorder>uiDeleteEthernetPushButton</zorder>
|
||||
<zorder>uiAddAllEthernetPushButton</zorder>
|
||||
<zorder>uiShowSpecialInterfacesCheckBox</zorder>
|
||||
<zorder>uiEthernetWarningPushButton</zorder>
|
||||
</widget>
|
||||
<widget class="QWidget" name="TAPTab">
|
||||
<attribute name="title">
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
|
||||
#
|
||||
# Created: Fri Jun 10 16:26:54 2016
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
# Created by: PyQt5 UI code generator 5.9
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_cloudConfigPageWidget(object):
|
||||
def setupUi(self, cloudConfigPageWidget):
|
||||
cloudConfigPageWidget.setObjectName("cloudConfigPageWidget")
|
||||
cloudConfigPageWidget.resize(758, 299)
|
||||
cloudConfigPageWidget.resize(821, 363)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
|
||||
@@ -33,21 +32,32 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.gridLayout_3.addWidget(self.uiEthernetComboBox, 0, 0, 1, 1)
|
||||
self.uiAddEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiAddEthernetPushButton.setObjectName("uiAddEthernetPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiAddEthernetPushButton, 0, 1, 1, 1)
|
||||
self.gridLayout_3.addWidget(self.uiAddEthernetPushButton, 0, 2, 1, 1)
|
||||
self.uiAddAllEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiAddAllEthernetPushButton.setObjectName("uiAddAllEthernetPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiAddAllEthernetPushButton, 0, 2, 1, 1)
|
||||
self.gridLayout_3.addWidget(self.uiAddAllEthernetPushButton, 0, 3, 1, 1)
|
||||
self.uiDeleteEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiDeleteEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteEthernetPushButton.setObjectName("uiDeleteEthernetPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiDeleteEthernetPushButton, 0, 3, 1, 1)
|
||||
self.gridLayout_3.addWidget(self.uiDeleteEthernetPushButton, 0, 4, 1, 1)
|
||||
self.uiEthernetListWidget = QtWidgets.QListWidget(self.EthernetTab)
|
||||
self.uiEthernetListWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.uiEthernetListWidget.setObjectName("uiEthernetListWidget")
|
||||
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 4)
|
||||
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 5)
|
||||
self.uiEthernetWarningPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiEthernetWarningPushButton.setText("")
|
||||
self.uiEthernetWarningPushButton.setObjectName("uiEthernetWarningPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiEthernetWarningPushButton, 0, 1, 1, 1)
|
||||
self.uiShowSpecialInterfacesCheckBox = QtWidgets.QCheckBox(self.EthernetTab)
|
||||
self.uiShowSpecialInterfacesCheckBox.setObjectName("uiShowSpecialInterfacesCheckBox")
|
||||
self.gridLayout_3.addWidget(self.uiShowSpecialInterfacesCheckBox, 2, 0, 1, 1)
|
||||
self.gridLayout_3.addWidget(self.uiShowSpecialInterfacesCheckBox, 2, 0, 1, 2)
|
||||
self.uiEthernetListWidget.raise_()
|
||||
self.uiEthernetComboBox.raise_()
|
||||
self.uiAddEthernetPushButton.raise_()
|
||||
self.uiDeleteEthernetPushButton.raise_()
|
||||
self.uiAddAllEthernetPushButton.raise_()
|
||||
self.uiShowSpecialInterfacesCheckBox.raise_()
|
||||
self.uiEthernetWarningPushButton.raise_()
|
||||
self.uiTabWidget.addTab(self.EthernetTab, "")
|
||||
self.TAPTab = QtWidgets.QWidget()
|
||||
self.TAPTab.setObjectName("TAPTab")
|
||||
@@ -225,6 +235,7 @@ class Ui_cloudConfigPageWidget(object):
|
||||
def retranslateUi(self, cloudConfigPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
cloudConfigPageWidget.setWindowTitle(_translate("cloudConfigPageWidget", "Cloud configuration"))
|
||||
cloudConfigPageWidget.setWhatsThis(_translate("cloudConfigPageWidget", "<html><head/><body><p>A cloud node allows you to connect your project to the "real world" (a network or host) using either an Ethernet interface, a TAP interface (Linux only) or even an UDP tunnel. <span style=\" font-weight:600;\">Please be aware that Wifi interfaces may not work properly.</span></p></body></html>"))
|
||||
self.uiAddEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiAddAllEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
|
||||
self.uiDeleteEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
|
||||
@@ -105,13 +105,13 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>65535</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>1</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/ethernet_switch_configuration_page.ui'
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/builtin/ui/ethernet_switch_configuration_page.ui'
|
||||
#
|
||||
# Created: Fri Jun 10 20:45:43 2016
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
# Created by: PyQt5 UI code generator 5.6
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_ethernetSwitchConfigPageWidget(object):
|
||||
|
||||
def setupUi(self, ethernetSwitchConfigPageWidget):
|
||||
ethernetSwitchConfigPageWidget.setObjectName("ethernetSwitchConfigPageWidget")
|
||||
ethernetSwitchConfigPageWidget.resize(545, 435)
|
||||
@@ -69,9 +70,9 @@ class Ui_ethernetSwitchConfigPageWidget(object):
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiPortSpinBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiPortSpinBox.setSizePolicy(sizePolicy)
|
||||
self.uiPortSpinBox.setMinimum(1)
|
||||
self.uiPortSpinBox.setMinimum(0)
|
||||
self.uiPortSpinBox.setMaximum(65535)
|
||||
self.uiPortSpinBox.setProperty("value", 1)
|
||||
self.uiPortSpinBox.setProperty("value", 0)
|
||||
self.uiPortSpinBox.setObjectName("uiPortSpinBox")
|
||||
self.gridlayout.addWidget(self.uiPortSpinBox, 0, 1, 1, 1)
|
||||
self.label_3 = QtWidgets.QLabel(self.uiEthernetSwitchSettingsGroupBox)
|
||||
@@ -174,4 +175,3 @@ class Ui_ethernetSwitchConfigPageWidget(object):
|
||||
self.uiPortsTreeWidget.headerItem().setText(3, _translate("ethernetSwitchConfigPageWidget", "EtherType"))
|
||||
self.uiAddPushButton.setText(_translate("ethernetSwitchConfigPageWidget", "&Add"))
|
||||
self.uiDeletePushButton.setText(_translate("ethernetSwitchConfigPageWidget", "&Delete"))
|
||||
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>499</width>
|
||||
<height>405</height>
|
||||
<height>414</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Frame Relay Switch</string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p>This is a simple Frame Relay switch. Only serial links can be connected to it. <span style=" font-weight:600;">Note that only the Frame-Relay LMI ANSI type is supported.</span></p></body></html></string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<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/frame_relay_switch_configuration_page.ui'
|
||||
# Form implementation generated from reading ui file '/home/dominik/projects/gns3-gui/gns3/modules/builtin/ui/frame_relay_switch_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.4.2
|
||||
# Created by: PyQt5 UI code generator 5.8.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_frameRelaySwitchConfigPageWidget(object):
|
||||
|
||||
def setupUi(self, frameRelaySwitchConfigPageWidget):
|
||||
frameRelaySwitchConfigPageWidget.setObjectName("frameRelaySwitchConfigPageWidget")
|
||||
frameRelaySwitchConfigPageWidget.resize(499, 405)
|
||||
frameRelaySwitchConfigPageWidget.resize(499, 414)
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout(frameRelaySwitchConfigPageWidget)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.uiGeneralGroupBox = QtWidgets.QGroupBox(frameRelaySwitchConfigPageWidget)
|
||||
@@ -136,6 +134,7 @@ class Ui_frameRelaySwitchConfigPageWidget(object):
|
||||
def retranslateUi(self, frameRelaySwitchConfigPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
frameRelaySwitchConfigPageWidget.setWindowTitle(_translate("frameRelaySwitchConfigPageWidget", "Frame Relay Switch"))
|
||||
frameRelaySwitchConfigPageWidget.setWhatsThis(_translate("frameRelaySwitchConfigPageWidget", "<html><head/><body><p>This is a simple Frame Relay switch. Only serial links can be connected to it. <span style=\" font-weight:600;\">Note that only the Frame-Relay LMI ANSI type is supported.</span></p></body></html>"))
|
||||
self.uiGeneralGroupBox.setTitle(_translate("frameRelaySwitchConfigPageWidget", "General"))
|
||||
self.uiNameLabel.setText(_translate("frameRelaySwitchConfigPageWidget", "Name:"))
|
||||
self.uiFrameRelayMappingGroupBox.setTitle(_translate("frameRelaySwitchConfigPageWidget", "Mapping"))
|
||||
@@ -149,3 +148,4 @@ class Ui_frameRelaySwitchConfigPageWidget(object):
|
||||
self.uiDestinationDLCILabel.setText(_translate("frameRelaySwitchConfigPageWidget", "DLCI:"))
|
||||
self.uiAddPushButton.setText(_translate("frameRelaySwitchConfigPageWidget", "&Add"))
|
||||
self.uiDeletePushButton.setText(_translate("frameRelaySwitchConfigPageWidget", "&Delete"))
|
||||
|
||||
|
||||
@@ -137,61 +137,9 @@ class Docker(Module):
|
||||
:param node_class: Node object
|
||||
:param server: HTTPClient instance
|
||||
"""
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
log.info("creating node {} with id {}".format(node, node.id()))
|
||||
|
||||
image = None
|
||||
if node_name:
|
||||
for image_key, info in self._docker_containers.items():
|
||||
if node_name == info["name"]:
|
||||
image = image_key
|
||||
if not image:
|
||||
selected_images = []
|
||||
for image, info in self._docker_containers.items():
|
||||
if info["server"] == node.server().host() or (
|
||||
node.server().isLocal() and info["server"] == "local"):
|
||||
selected_images.append(image)
|
||||
|
||||
if not selected_images:
|
||||
raise ModuleError("No Docker VM on server {}".format(
|
||||
node.server().url()))
|
||||
elif len(selected_images) > 1:
|
||||
from gns3.main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
|
||||
(selection, ok) = QtWidgets.QInputDialog.getItem(
|
||||
mainwindow, "Docker Image", "Please choose an image",
|
||||
selected_images, 0, False)
|
||||
if ok:
|
||||
image = selection
|
||||
else:
|
||||
raise ModuleError("Please select a Docker Image")
|
||||
else:
|
||||
image = selected_images[0]
|
||||
|
||||
image_settings = {}
|
||||
for setting_name, value in self._docker_containers[image].items():
|
||||
if setting_name in node.settings() and value != "" and value is not None:
|
||||
if setting_name not in ['name', 'image']:
|
||||
image_settings[setting_name] = value
|
||||
|
||||
default_name_format = DOCKER_CONTAINER_SETTINGS["default_name_format"]
|
||||
if self._docker_containers[image]["default_name_format"]:
|
||||
default_name_format = self._docker_containers[image]["default_name_format"]
|
||||
|
||||
image = self._docker_containers[image]["image"]
|
||||
node.create(image, base_name=node_name, additional_settings=image_settings, default_name_format=default_name_format)
|
||||
|
||||
def reset(self):
|
||||
"""Resets the servers."""
|
||||
self._nodes.clear()
|
||||
@@ -202,7 +150,7 @@ class Docker(Module):
|
||||
:param server: server to send the request to
|
||||
:param callback: callback for the reply from the server
|
||||
"""
|
||||
Controller.instance().get("/computes/{}/docker/images".format(compute_id), callback)
|
||||
Controller.instance().getCompute("/docker/images", compute_id, callback)
|
||||
|
||||
@staticmethod
|
||||
def getNodeClass(name):
|
||||
|
||||
@@ -21,6 +21,7 @@ import sys
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
from gns3.compute_manager import ComputeManager
|
||||
|
||||
from ..ui.docker_vm_wizard_ui import Ui_DockerVMWizard
|
||||
from .. import Docker
|
||||
@@ -35,7 +36,7 @@ class DockerVMWizard(VMWizard, Ui_DockerVMWizard):
|
||||
|
||||
def __init__(self, docker_containers, parent):
|
||||
|
||||
super().__init__(docker_containers, Docker.instance().settings()["use_local_server"], parent)
|
||||
super().__init__(docker_containers, parent)
|
||||
self._docker_containers = docker_containers
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/icons/docker.png"))
|
||||
|
||||
@@ -43,7 +44,7 @@ class DockerVMWizard(VMWizard, Ui_DockerVMWizard):
|
||||
self._existingImageRadioButtonToggledSlot(False)
|
||||
self.uiExistingImageRadioButton.toggled.connect(self._existingImageRadioButtonToggledSlot)
|
||||
|
||||
if sys.platform.startswith("win") or sys.platform.startswith("darwin"):
|
||||
if ComputeManager.instance().localPlatform().startswith("win") or ComputeManager.instance().localPlatform().startswith("darwin"):
|
||||
# Cannot use Docker locally on Windows and Mac
|
||||
self._disableLocalServer()
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ class DockerVM(Node):
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
log.info("Docker VM is being created")
|
||||
|
||||
docker_vm_settings = {"image": "",
|
||||
"adapters": DOCKER_CONTAINER_SETTINGS["adapters"],
|
||||
@@ -54,23 +53,6 @@ class DockerVM(Node):
|
||||
|
||||
self.settings().update(docker_vm_settings)
|
||||
|
||||
def create(self, image, name=None, base_name=None, node_id=None, additional_settings={}, default_name_format="{name}-{0}"):
|
||||
"""Creates this Docker container.
|
||||
|
||||
:param image: image name
|
||||
:param name: optional name
|
||||
:param additional_settings: additional settings for this VM
|
||||
"""
|
||||
|
||||
params = {
|
||||
"image": image,
|
||||
"adapters": self._settings["adapters"]
|
||||
}
|
||||
params.update(additional_settings)
|
||||
if base_name:
|
||||
default_name_format = default_name_format.replace('{name}', base_name)
|
||||
self._create(name=name, node_id=node_id, params=params, default_name_format=default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for Docker container creating.
|
||||
@@ -106,10 +88,15 @@ class DockerVM(Node):
|
||||
|
||||
info = """Docker container {name} is {state}
|
||||
Node ID is {id}, server's Docker container ID is {node_id}
|
||||
Docker VM's server run on {host}
|
||||
Console is on port {console} and type is {console_type}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
state=state)
|
||||
state=state,
|
||||
host=self.compute().name(),
|
||||
console=self._settings["console"],
|
||||
console_type=self._settings["console_type"])
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
|
||||
@@ -40,9 +40,8 @@ class DockerPreferencesPage(QtWidgets.QWidget, Ui_DockerPreferencesPageWidget):
|
||||
# connect signals
|
||||
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
|
||||
|
||||
if not sys.platform.startswith("linux"):
|
||||
# if not sys.platform.startswith("linux"):
|
||||
# Docker is only supported on Linux
|
||||
self.uiUseLocalServercheckBox.setEnabled(False)
|
||||
|
||||
def _restoreDefaultsSlot(self):
|
||||
"""Slot to populate the page widgets with the default settings."""
|
||||
@@ -53,7 +52,6 @@ class DockerPreferencesPage(QtWidgets.QWidget, Ui_DockerPreferencesPageWidget):
|
||||
|
||||
:param settings: Docker settings
|
||||
"""
|
||||
self.uiUseLocalServercheckBox.setChecked(settings["use_local_server"])
|
||||
|
||||
def loadPreferences(self):
|
||||
"""Loads Docker preferences."""
|
||||
@@ -63,5 +61,4 @@ class DockerPreferencesPage(QtWidgets.QWidget, Ui_DockerPreferencesPageWidget):
|
||||
def savePreferences(self):
|
||||
"""Saves Docker preferences."""
|
||||
new_settings = {}
|
||||
new_settings["use_local_server"] = self.uiUseLocalServercheckBox.isChecked()
|
||||
Docker.instance().setSettings(new_settings)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user