mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-06-08 11:33:44 +03:00
Compare commits
2780 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47d335f4c9 | ||
|
|
20d4f73f56 | ||
|
|
5204184029 | ||
|
|
9915beeb8e | ||
|
|
1ea383fce2 | ||
|
|
2744e669b4 | ||
|
|
6ab2d63bdc | ||
|
|
0de6bfe7e1 | ||
|
|
f144103bca | ||
|
|
c0b26aff48 | ||
|
|
9601e4e6f2 | ||
|
|
88708c2a8d | ||
|
|
8eff12194d | ||
|
|
b0520b2bd4 | ||
|
|
17d2c023bf | ||
|
|
ce9fdea0a0 | ||
|
|
24d7dacb4e | ||
|
|
bb36765407 | ||
|
|
250db92ce0 | ||
|
|
d59ec39505 | ||
|
|
5e9ae04dc1 | ||
|
|
ddb0fccda3 | ||
|
|
9b22a52f14 | ||
|
|
948878bfdd | ||
|
|
7340abbaa9 | ||
|
|
4ea0528bf2 | ||
|
|
49005e6add | ||
|
|
5484c039b5 | ||
|
|
daaf71b6d2 | ||
|
|
450f0e006b | ||
|
|
a6a967fbde | ||
|
|
1a6293709e | ||
|
|
2ed53225e0 | ||
|
|
b8798fbda5 | ||
|
|
368de32faa | ||
|
|
98d01cbfa0 | ||
|
|
ad62bb7832 | ||
|
|
637061663a | ||
|
|
c137198985 | ||
|
|
946efb61de | ||
|
|
4c610acfa4 | ||
|
|
37f74824f1 | ||
|
|
5ccf8c414d | ||
|
|
913f0d5e4a | ||
|
|
061bac0cc6 | ||
|
|
ec59cd87bd | ||
|
|
05d9ee8499 | ||
|
|
a72ece5c18 | ||
|
|
63baa2eff0 | ||
|
|
b91fd4a0c2 | ||
|
|
718217e332 | ||
|
|
c202c5e4be | ||
|
|
71830dd69f | ||
|
|
37a7fdfa68 | ||
|
|
0efe006cad | ||
|
|
4a663a5910 | ||
|
|
a559bd4ae4 | ||
|
|
5ebb3011d3 | ||
|
|
81300fd40e | ||
|
|
d4dda2a285 | ||
|
|
5a4342d4b8 | ||
|
|
94fc5e6c4f | ||
|
|
a3e81fbf2e | ||
|
|
514eb97eac | ||
|
|
7637039cb2 | ||
|
|
ac989b191b | ||
|
|
c971cef31b | ||
|
|
c1af2df780 | ||
|
|
eaaa141be9 | ||
|
|
226169cdc6 | ||
|
|
42a4c89f20 | ||
|
|
1482b0e804 | ||
|
|
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 | ||
|
|
716f65786d | ||
|
|
0c397ba0a8 | ||
|
|
bff8a09147 | ||
|
|
f65ea13c6f | ||
|
|
3975b09898 | ||
|
|
6525d3130c | ||
|
|
5d8ad83cbe | ||
|
|
13a819ee87 | ||
|
|
cd1beb5191 | ||
|
|
483259ba2c | ||
|
|
3d828c42de | ||
|
|
d70dbe82d7 | ||
|
|
243c5a0f82 | ||
|
|
f4470d8190 | ||
|
|
b435163a3c | ||
|
|
26f6315b69 | ||
|
|
b81a02fbb4 | ||
|
|
b19988784f | ||
|
|
5caf576f83 | ||
|
|
e61b132c93 | ||
|
|
41b826e9ac | ||
|
|
4079f19e25 | ||
|
|
04f108cbdf | ||
|
|
e222e3e7c2 | ||
|
|
cb687205a4 | ||
|
|
4ef19056bb | ||
|
|
8a4fb55cdf | ||
|
|
805b573370 | ||
|
|
3f87719d02 | ||
|
|
2a27bb560c | ||
|
|
73dbe68301 | ||
|
|
c2bd4c8984 | ||
|
|
3021cfb164 | ||
|
|
794f317f83 | ||
|
|
e357581587 | ||
|
|
e46b699fcb | ||
|
|
c426ea6e03 | ||
|
|
b0a1fdb65a | ||
|
|
4515620259 | ||
|
|
c36695c59a | ||
|
|
090caa967b | ||
|
|
e9ac774464 | ||
|
|
de2de45196 | ||
|
|
64dd0e0be3 | ||
|
|
6cd5438c89 | ||
|
|
df5a09b46d | ||
|
|
cba03a7539 | ||
|
|
069d02d908 | ||
|
|
a207ba61de | ||
|
|
496db6427d | ||
|
|
1bcf6699f0 | ||
|
|
fa12721c3c | ||
|
|
65ad43431c | ||
|
|
081544e778 | ||
|
|
a30daf03d4 | ||
|
|
f80af230af | ||
|
|
71125a4b58 | ||
|
|
c1dfaf13fb | ||
|
|
03a3081361 | ||
|
|
b5715e46d2 | ||
|
|
3aa9ed61c6 | ||
|
|
0b6cc588b6 | ||
|
|
ed4af4a8e7 | ||
|
|
c1f6c3ddeb | ||
|
|
d57652815e | ||
|
|
ea3191739b | ||
|
|
378b4f973b | ||
|
|
6ecbe59011 | ||
|
|
2fcaa1f6cc | ||
|
|
f0a7582fd2 | ||
|
|
2da076a501 | ||
|
|
f31cc4806f | ||
|
|
ae228988a5 | ||
|
|
185f0463d4 | ||
|
|
cf668e774b | ||
|
|
b933cdd950 | ||
|
|
f38d3bdb8e | ||
|
|
2c5ed9c884 | ||
|
|
f2e457de6c | ||
|
|
6b146fc7a7 | ||
|
|
25b44a2070 | ||
|
|
ac8ef05b06 | ||
|
|
663185d93e | ||
|
|
61ac6ed6e0 | ||
|
|
0fa51bcd36 | ||
|
|
f000425350 | ||
|
|
8df07808a9 | ||
|
|
7f169261d4 | ||
|
|
a86c728a99 | ||
|
|
11d56d1ea7 | ||
|
|
9c52cf0b0b | ||
|
|
cd96492dff | ||
|
|
43ab1deb24 | ||
|
|
646bf10017 | ||
|
|
6a23874054 | ||
|
|
b6fe18b975 | ||
|
|
84125fe463 | ||
|
|
12732715bd | ||
|
|
ece0b94ae8 | ||
|
|
84d0532039 | ||
|
|
ab68c1f1ab | ||
|
|
b360d8d931 | ||
|
|
f9681f2766 | ||
|
|
e8a09eef72 | ||
|
|
3290639e54 | ||
|
|
02d3275475 | ||
|
|
fd92049cda | ||
|
|
f48a5655ed | ||
|
|
22267b4123 | ||
|
|
b06230ef3d | ||
|
|
87b37e2839 | ||
|
|
0f3dd2e05d | ||
|
|
0495429df8 | ||
|
|
2e97f1e037 | ||
|
|
8133ba61a5 | ||
|
|
4cce5cd4ff | ||
|
|
1f1f95e3da | ||
|
|
7c9c66470d | ||
|
|
da5bead39c | ||
|
|
68a0c74a1f | ||
|
|
949d1a900a | ||
|
|
88c7a472b1 | ||
|
|
4b49501630 | ||
|
|
9eb71b870a | ||
|
|
5d46560427 | ||
|
|
d7856af6db | ||
|
|
c0a093d044 | ||
|
|
03c7df9dad | ||
|
|
d847316914 | ||
|
|
190e17b445 | ||
|
|
ce5d1b56f0 | ||
|
|
cb7cbc15b3 | ||
|
|
dc6756da04 | ||
|
|
e4eeb437eb | ||
|
|
81b94070ac | ||
|
|
de36a04d88 | ||
|
|
1287b77dfe | ||
|
|
146bb004c0 | ||
|
|
d26abecea7 | ||
|
|
d2da14a951 | ||
|
|
4ae17a1f66 | ||
|
|
473f60167d | ||
|
|
5377590520 | ||
|
|
58fe1f6e2b | ||
|
|
c0564c89c8 | ||
|
|
34d27dc120 | ||
|
|
d907af46bc | ||
|
|
a8055471cd | ||
|
|
317c28eaf2 | ||
|
|
d2b6aeddbd | ||
|
|
4285d01a19 | ||
|
|
802c59a1d1 | ||
|
|
f1879c8d4b | ||
|
|
97ad294623 | ||
|
|
6147dcd304 | ||
|
|
d900842363 | ||
|
|
e6ac92abb4 | ||
|
|
944b5fc6c9 | ||
|
|
bf6c645281 | ||
|
|
14f04e5f88 | ||
|
|
ecbf7bd661 | ||
|
|
de15edcd0a | ||
|
|
a5d619d6a8 | ||
|
|
271811d376 | ||
|
|
6ed9652a2a | ||
|
|
0d62811a17 | ||
|
|
615f1f2b5d | ||
|
|
721bff01b0 | ||
|
|
69f671106c | ||
|
|
0c59970974 | ||
|
|
d161a75ab2 | ||
|
|
b87f2b2952 | ||
|
|
a0b4c38a44 | ||
|
|
c9cc98ae39 | ||
|
|
ad492e2b90 | ||
|
|
a977042017 | ||
|
|
9e4b5ad02b | ||
|
|
90b9b2d29c | ||
|
|
54b6efce59 | ||
|
|
1f77a825b3 | ||
|
|
283d787c8d | ||
|
|
47be4d39c2 | ||
|
|
8a4fab9528 | ||
|
|
a31586fac9 | ||
|
|
3b88c72778 | ||
|
|
0e4a5da71a | ||
|
|
2a6327b2f2 | ||
|
|
d4d8adf4ac | ||
|
|
8492a31dd7 | ||
|
|
bb91402d2c | ||
|
|
35d8b4f848 | ||
|
|
073dc1afe7 | ||
|
|
b61e6dabd4 | ||
|
|
316fa688c3 | ||
|
|
0724387257 | ||
|
|
8e87c8cdbe | ||
|
|
9553d3ba25 | ||
|
|
4bccd6de25 | ||
|
|
dd5bafca0a | ||
|
|
6d3e28226a | ||
|
|
0fc040773a | ||
|
|
1f2294f9bf | ||
|
|
bed3f1d8fa | ||
|
|
b1da0b8279 | ||
|
|
32f3137f4d | ||
|
|
df10bca2c0 | ||
|
|
21fba1c4f7 | ||
|
|
f56e7e8dd8 | ||
|
|
41f6119118 | ||
|
|
7732f2a27e | ||
|
|
0be4f31162 | ||
|
|
6bc8428dd0 | ||
|
|
fd92e92a4f | ||
|
|
9dc7a4447b | ||
|
|
c2a597ffcf | ||
|
|
fc4850afab | ||
|
|
6c31de36ac | ||
|
|
4082aa8d77 | ||
|
|
71a835ff5f | ||
|
|
b156df6fc2 | ||
|
|
d077621ee9 | ||
|
|
3624502a23 | ||
|
|
c9f12fece7 | ||
|
|
0f43fd4560 | ||
|
|
1c85f980d3 | ||
|
|
49428d3ead | ||
|
|
2f48752ff2 | ||
|
|
48ac89abc9 | ||
|
|
bd8bad5e4c | ||
|
|
13c189fb00 | ||
|
|
eaab3c3f5e | ||
|
|
61fb8246f0 | ||
|
|
b09249b384 | ||
|
|
7d6b98766c | ||
|
|
96bcf55942 | ||
|
|
7bb6078b13 | ||
|
|
c8d6a4640a | ||
|
|
27be2b7a1d | ||
|
|
b05d682aa3 | ||
|
|
636b26b0e8 | ||
|
|
ae2a111536 | ||
|
|
33796a8bd3 | ||
|
|
695e5d3daa | ||
|
|
9d805d5d42 | ||
|
|
ec3fd63138 | ||
|
|
8e5e2d4a0c | ||
|
|
ee6e2b41f7 | ||
|
|
42c54ef02f | ||
|
|
227cbfc79a | ||
|
|
0fd5a1a91d | ||
|
|
4406c940b5 | ||
|
|
8eab44349f | ||
|
|
f50f7153dc | ||
|
|
a994f65d79 | ||
|
|
1a3a17e480 | ||
|
|
840e4aec54 | ||
|
|
74b660af61 | ||
|
|
cc0c56087a | ||
|
|
990e6c0eed | ||
|
|
3bc6cd8b4d | ||
|
|
515119e1fa | ||
|
|
570303273c | ||
|
|
1739cc58d4 | ||
|
|
b3a7d42f9d | ||
|
|
17ed1f9806 | ||
|
|
1e3883674e | ||
|
|
bead888c67 | ||
|
|
07fcd66d8d | ||
|
|
0f4cac1b76 | ||
|
|
89fbc537bf | ||
|
|
f0ebdf295f | ||
|
|
d396cb911a | ||
|
|
9064487a3e | ||
|
|
8b03f32f95 | ||
|
|
3295cc514e | ||
|
|
c6df492852 | ||
|
|
565c71cb80 | ||
|
|
30bd710650 | ||
|
|
c0dbf95b94 | ||
|
|
7f58837111 | ||
|
|
53f609c4d7 | ||
|
|
26790fd80d | ||
|
|
cfcb24a732 | ||
|
|
e31746b676 | ||
|
|
154435d5a5 | ||
|
|
d24a0312d8 | ||
|
|
aa5d8b9377 | ||
|
|
e9703e03cd | ||
|
|
13a8d27349 | ||
|
|
939f8f52c1 | ||
|
|
cb1e062f9b | ||
|
|
a1d1bc5aea | ||
|
|
1d81c0521f | ||
|
|
d3ef916b23 | ||
|
|
c9b7259cd7 | ||
|
|
fa8c135b22 | ||
|
|
9d53d806fd | ||
|
|
6f499e6c56 | ||
|
|
e66bdc936a | ||
|
|
be34e062e7 | ||
|
|
5664b32cc5 | ||
|
|
6bf0ea63d4 | ||
|
|
628970e588 | ||
|
|
fb68ccad15 | ||
|
|
d6b394500f | ||
|
|
32508d60b1 | ||
|
|
0ae23c30c4 | ||
|
|
e5f18c5e22 | ||
|
|
df0f25b234 | ||
|
|
dc1d9e59b0 | ||
|
|
266eb77eb5 | ||
|
|
c1cac82081 | ||
|
|
323c787d91 | ||
|
|
41070495ba | ||
|
|
037e531b22 | ||
|
|
54713b5d68 | ||
|
|
d94f9a91db | ||
|
|
516b8e848f | ||
|
|
6d1d1705b2 | ||
|
|
666a527aa3 | ||
|
|
f296c7fdad | ||
|
|
58e62da913 | ||
|
|
ca364d4d56 | ||
|
|
2232680ded | ||
|
|
b662c54a07 | ||
|
|
cd2f897ff2 | ||
|
|
c877d4b1d7 | ||
|
|
dc6032aa43 | ||
|
|
fec9431ae5 | ||
|
|
bf1b7e640b | ||
|
|
780ab5b14f | ||
|
|
7371aebb76 | ||
|
|
c227f39a03 | ||
|
|
ff794f1578 | ||
|
|
f26c342e82 | ||
|
|
edacb88ff5 | ||
|
|
4854eac2da | ||
|
|
60cd105b82 | ||
|
|
184db222c5 | ||
|
|
abcfb9ee12 | ||
|
|
5f3ba669eb | ||
|
|
942d4756c7 | ||
|
|
d9b6dfd8d0 | ||
|
|
ff79e7ad36 | ||
|
|
ebfdac96ae | ||
|
|
e2a85885be | ||
|
|
c6b0fb4d65 | ||
|
|
fb5c4df4db | ||
|
|
9cb4eb775b | ||
|
|
8c349e4669 | ||
|
|
b5d879139a | ||
|
|
0e05918631 | ||
|
|
c3fee8d323 | ||
|
|
b5743d9902 | ||
|
|
44974c04ad | ||
|
|
336f8d525b | ||
|
|
f76b6afe6a | ||
|
|
602b58d1df | ||
|
|
030edccc90 | ||
|
|
dd3317f4f6 | ||
|
|
f29d0e45b7 | ||
|
|
c038ed3db4 | ||
|
|
afcf2a9400 | ||
|
|
6bcc4c86e6 | ||
|
|
ded32730bf | ||
|
|
2e1b6aef9f | ||
|
|
26d918e218 | ||
|
|
23e1097f89 | ||
|
|
6d1e2d9fab | ||
|
|
7c37284901 | ||
|
|
e9384676e1 | ||
|
|
88bf51c066 | ||
|
|
fbbe8aff54 | ||
|
|
e3d441d19f | ||
|
|
4f3d20a7c4 | ||
|
|
222ea18bcd | ||
|
|
df7c91f17f | ||
|
|
99331fcc54 | ||
|
|
b59d31855e | ||
|
|
a2211cfa46 | ||
|
|
e9ec42be02 | ||
|
|
0801d9bf65 | ||
|
|
379b7a56ef | ||
|
|
8e9062c812 | ||
|
|
21e03e8318 | ||
|
|
fa1b53682c | ||
|
|
ec68deb7e4 | ||
|
|
1d51f3eed5 | ||
|
|
e8e189d5f3 | ||
|
|
443e338cc3 | ||
|
|
9cab049696 | ||
|
|
e30e869025 | ||
|
|
b106be2ed5 | ||
|
|
842519d7d0 | ||
|
|
d2ff73b579 | ||
|
|
c31d9dfbb2 | ||
|
|
d7ed734ffb | ||
|
|
b5a04bfe63 | ||
|
|
077e6a110e | ||
|
|
66d87e8b12 | ||
|
|
6bf5e7abcc | ||
|
|
39979a411d | ||
|
|
fdd5c71711 | ||
|
|
d6e20fe166 | ||
|
|
7988b13281 | ||
|
|
8395865b75 | ||
|
|
c3f33acdb3 | ||
|
|
6d9167c30f | ||
|
|
f8d698aea9 | ||
|
|
0cbde5046e | ||
|
|
7a137a68ae | ||
|
|
09f7e6ce99 | ||
|
|
305cc72485 | ||
|
|
9b04901754 | ||
|
|
d262f429c4 | ||
|
|
6bb1223614 | ||
|
|
1d97b217cd | ||
|
|
dfe48466e0 | ||
|
|
6fed45e7a8 | ||
|
|
d97b75a3e1 | ||
|
|
87f2e08b3a | ||
|
|
f2c517a4a4 | ||
|
|
b8b810cdb1 | ||
|
|
c21900100e | ||
|
|
50222f5083 | ||
|
|
594b596cf9 | ||
|
|
3494a4875c | ||
|
|
a5d880e411 | ||
|
|
e9f445380b | ||
|
|
ea51f15253 | ||
|
|
58501c205a | ||
|
|
47f23884b4 | ||
|
|
35ff7fd83e | ||
|
|
1375d7922c | ||
|
|
acc0a2ec67 | ||
|
|
eecf1f4a54 | ||
|
|
088d022d5e | ||
|
|
22fcb14f9a | ||
|
|
6f2294f9b9 | ||
|
|
d7190b0602 | ||
|
|
4cf769e7b6 | ||
|
|
3889c8c1fa | ||
|
|
d122e10703 | ||
|
|
05f1fa0ecb | ||
|
|
3974629e34 | ||
|
|
dfd8147873 | ||
|
|
bb503d9cc7 | ||
|
|
913cb1a3cd | ||
|
|
7125fb285e | ||
|
|
0454868958 | ||
|
|
8523e3d1a4 | ||
|
|
e2415b68d3 | ||
|
|
0850a3428e | ||
|
|
6f44a8b6ee | ||
|
|
0cc16c232b | ||
|
|
57bd21d346 | ||
|
|
f18e7295bd | ||
|
|
d6a6343aa8 | ||
|
|
7397d2da50 | ||
|
|
f70c457e88 | ||
|
|
7750720f4d | ||
|
|
950281caa6 | ||
|
|
b5202b5591 | ||
|
|
4aa01acce4 | ||
|
|
c58e788eba | ||
|
|
e7b60a1f27 | ||
|
|
1e4bbc4ecf | ||
|
|
e599da7033 | ||
|
|
341b5cd947 | ||
|
|
fa35f3f9e4 | ||
|
|
1375578b52 | ||
|
|
1e2326913b | ||
|
|
82c41e09b5 | ||
|
|
16f3b71af4 | ||
|
|
03373f3cda | ||
|
|
2c9c01b991 | ||
|
|
8a44b6fdb7 | ||
|
|
3ceb886ca9 | ||
|
|
fb3df39263 | ||
|
|
829e8ed745 | ||
|
|
ed5c52a807 | ||
|
|
8afc5afadf | ||
|
|
b26401203f | ||
|
|
c127548dd1 | ||
|
|
f8c1a48350 | ||
|
|
55f634bec3 | ||
|
|
8c14e42a09 | ||
|
|
2e30a96389 | ||
|
|
3fc4898904 | ||
|
|
3561c55174 | ||
|
|
5195c647f6 | ||
|
|
f3a0d1daac | ||
|
|
dcad6e2d23 | ||
|
|
6a402fe544 | ||
|
|
310ae5905f | ||
|
|
23aa820cdf | ||
|
|
74f702cea6 | ||
|
|
52335bddbc | ||
|
|
05acf724a8 | ||
|
|
71319a0a7c | ||
|
|
c341c55258 | ||
|
|
cf40e641a6 | ||
|
|
7cb6af85a8 | ||
|
|
358ef34918 | ||
|
|
a66d194e12 | ||
|
|
57b3ce4666 | ||
|
|
aaa2b6f817 | ||
|
|
97b56e5620 | ||
|
|
79850176c3 | ||
|
|
8e1896ef5b | ||
|
|
8cf911bb15 | ||
|
|
ad0af16fa3 | ||
|
|
2aada61af3 | ||
|
|
66b9b4c68c | ||
|
|
60d6151ce9 | ||
|
|
bfb4b0b9da | ||
|
|
0f00e206bf | ||
|
|
71536ef9d3 | ||
|
|
354e73b4e7 | ||
|
|
30121e3617 | ||
|
|
4d422e716b | ||
|
|
8aa0f8d070 | ||
|
|
7c03c0cbcf | ||
|
|
c13a4835b2 | ||
|
|
412d9b7645 | ||
|
|
27cdaf1ed5 | ||
|
|
32e8a45e4e | ||
|
|
34f35aff27 | ||
|
|
9b0101321a | ||
|
|
a4c9487192 | ||
|
|
6449973ddc | ||
|
|
bd9a168667 | ||
|
|
5c0b03f133 | ||
|
|
5708f039c0 | ||
|
|
ba0809159c | ||
|
|
4aeb4238b2 | ||
|
|
119bc8207f | ||
|
|
5e7dc27e1f | ||
|
|
6d3b4db760 | ||
|
|
ccba9aa4d5 | ||
|
|
47cbc91b02 | ||
|
|
7a10fa157d | ||
|
|
a27ed4051c | ||
|
|
dbbde4b098 | ||
|
|
2f71480849 | ||
|
|
7e2284e094 | ||
|
|
0ce5c198aa | ||
|
|
c8519188a1 | ||
|
|
bf9f782970 | ||
|
|
72f580efb8 | ||
|
|
a443e3dcde | ||
|
|
5496c6c8af | ||
|
|
b96d5e765e | ||
|
|
cee5fb915a | ||
|
|
54888ff278 | ||
|
|
ab3f3d72ab | ||
|
|
b0eb0d74fb | ||
|
|
8451b4b14e | ||
|
|
ca85d5e8c0 | ||
|
|
9f7cf16335 | ||
|
|
e09353b0fe | ||
|
|
56ace4dd31 | ||
|
|
3cfd1a0957 | ||
|
|
3bd91dc9cb | ||
|
|
aa805a611a | ||
|
|
b46109a086 | ||
|
|
141b102129 | ||
|
|
2a03953f6c | ||
|
|
0ff3bb1a34 | ||
|
|
45d4c26972 | ||
|
|
c05aeffbbb | ||
|
|
b37b07bb06 | ||
|
|
83bb38b857 | ||
|
|
6ac398f11d | ||
|
|
774c210097 | ||
|
|
173aa53cbe | ||
|
|
be128bc12a | ||
|
|
305975bb3b | ||
|
|
e6726eb69d | ||
|
|
2988bae855 | ||
|
|
d65e1087f9 | ||
|
|
03744a7606 | ||
|
|
65e2a1c8aa | ||
|
|
1e8ef4b208 | ||
|
|
2a636481e8 | ||
|
|
9efc424462 | ||
|
|
ad9db64e8b | ||
|
|
0cf04e34c7 | ||
|
|
f932f96097 | ||
|
|
4c5dac5e13 | ||
|
|
abd838de00 | ||
|
|
cd92f69804 | ||
|
|
9d4cddb4a0 | ||
|
|
4f105ced0e | ||
|
|
983a69ed5d | ||
|
|
e17b6aa5c0 | ||
|
|
c73c302d77 | ||
|
|
bdd40ec59d | ||
|
|
d78064daa6 | ||
|
|
7683f7820f | ||
|
|
c6b88d1fcd | ||
|
|
dfaae1df1a | ||
|
|
58efa8411b | ||
|
|
95f000252b | ||
|
|
2cf5880940 | ||
|
|
88c948f117 | ||
|
|
89a369165e | ||
|
|
9fc53329b5 | ||
|
|
8765b7b3bd | ||
|
|
c4710b4bd2 | ||
|
|
43bd08a58f | ||
|
|
8a78cc2f5e | ||
|
|
186429890e | ||
|
|
85d9988d79 | ||
|
|
25ed2b794d | ||
|
|
1e3d216961 | ||
|
|
b43a94b3c7 | ||
|
|
fc5cb3f0ad | ||
|
|
fc83a9e905 | ||
|
|
6635f2f9c1 | ||
|
|
dc054d7e6b | ||
|
|
555d464f8f | ||
|
|
c8a8336dc7 | ||
|
|
228c39719d | ||
|
|
5207fedd61 | ||
|
|
ae9c082cb7 | ||
|
|
b302f16f65 | ||
|
|
bf0bb0519a | ||
|
|
c9db57fb7f | ||
|
|
ca0ace0832 | ||
|
|
32217db357 | ||
|
|
a2ddfc5674 | ||
|
|
1ae7be4f6a | ||
|
|
a418af0aad | ||
|
|
50a92e9ea0 | ||
|
|
1e63fc14cb | ||
|
|
6c00ef65af | ||
|
|
5c29d42d8c | ||
|
|
19055ba004 | ||
|
|
0825ae8cb5 | ||
|
|
c032c9f458 | ||
|
|
fa5a9621e0 | ||
|
|
b2db2cc719 | ||
|
|
0f76819936 | ||
|
|
66d1597312 | ||
|
|
74f4ae03f3 | ||
|
|
5894cec3e4 | ||
|
|
e66c411989 | ||
|
|
f74920fd1b | ||
|
|
c1c98cc7b6 | ||
|
|
d217d9a291 | ||
|
|
29a73b183c | ||
|
|
79c64f0e38 | ||
|
|
b8a3deeb02 | ||
|
|
108c774c0f | ||
|
|
830c7556b8 | ||
|
|
5470add29a | ||
|
|
f6c9ab0068 | ||
|
|
e44b34062c | ||
|
|
320ae611a1 | ||
|
|
e54a87c436 | ||
|
|
608cc363a2 | ||
|
|
f9609c5871 | ||
|
|
cc422a6b1d | ||
|
|
638d75c388 | ||
|
|
b02495dd3d | ||
|
|
90ee8033b0 | ||
|
|
3f5d8fe2a1 | ||
|
|
32176d3e2f | ||
|
|
c65f55b22a | ||
|
|
c9d221404b | ||
|
|
ee73961832 | ||
|
|
ef39c174ed | ||
|
|
962d8f77dd | ||
|
|
bbc7abc50d | ||
|
|
00f1258032 | ||
|
|
beb297967f | ||
|
|
00c913fd19 | ||
|
|
a38a8c4ba4 | ||
|
|
56fafba8e9 | ||
|
|
d0396b3da9 | ||
|
|
180eaa2ce5 | ||
|
|
d8de60afb9 | ||
|
|
d5248e8472 | ||
|
|
a7c199b195 | ||
|
|
97a5351a52 | ||
|
|
e0b4452007 | ||
|
|
2e4a532b3c | ||
|
|
e54266d3a5 | ||
|
|
422ed0a5e2 | ||
|
|
59e17738cc | ||
|
|
50644cf3c4 | ||
|
|
4a3ceb710d | ||
|
|
5ab640c380 | ||
|
|
ba1afca4dd | ||
|
|
4d4ffdb86c | ||
|
|
0c6002a861 | ||
|
|
3ebdd8da14 | ||
|
|
31eb689635 | ||
|
|
c3d66f243a | ||
|
|
5c108635d0 | ||
|
|
365808eff2 | ||
|
|
5c654e99e4 | ||
|
|
709c47d40d | ||
|
|
8ff8fb9c92 | ||
|
|
e3cdc5d3ff | ||
|
|
69851d1596 | ||
|
|
c5330246b1 | ||
|
|
0f2b46b56a | ||
|
|
6580ea5891 | ||
|
|
1cfd5ae4f0 | ||
|
|
339beeabaf | ||
|
|
3a4b9e2e31 | ||
|
|
3055eeaa4f | ||
|
|
e517fa6000 | ||
|
|
3946ebcb92 | ||
|
|
a34fa04e4f | ||
|
|
2108f3209d | ||
|
|
8d2ae5e254 | ||
|
|
5092bc571d | ||
|
|
b013e8af50 | ||
|
|
3af8c4d28f | ||
|
|
1f1860e53c | ||
|
|
21015bccb5 | ||
|
|
b27fabea12 | ||
|
|
932c708538 | ||
|
|
adf241c146 | ||
|
|
b27a62c625 | ||
|
|
132596a17e | ||
|
|
f0359dcde9 | ||
|
|
08a005b271 | ||
|
|
1b873acd72 | ||
|
|
a76ac9b5e3 | ||
|
|
f7fa47026c | ||
|
|
36c3fe6a27 | ||
|
|
7c67f08362 | ||
|
|
5d71a828c4 | ||
|
|
3e9392b4b7 | ||
|
|
02d9d7c22c | ||
|
|
8d60c65e5b | ||
|
|
0418cd0a95 | ||
|
|
a464295e5b | ||
|
|
95ec16fa92 | ||
|
|
2bd43cdc62 | ||
|
|
37e7222371 | ||
|
|
254c766883 | ||
|
|
e82a8ad63e | ||
|
|
39b4b233c9 | ||
|
|
3e8208a117 | ||
|
|
fbc7fd1de3 | ||
|
|
d533733d4b | ||
|
|
082716e21a | ||
|
|
5ffdecab9e | ||
|
|
62c87b4f87 | ||
|
|
ab3a50f22f | ||
|
|
71d3e8dd04 | ||
|
|
aa8bbc32c5 | ||
|
|
05646c03cc | ||
|
|
1453b30e41 | ||
|
|
4f575fda73 | ||
|
|
14b6c70f47 | ||
|
|
07144659b1 | ||
|
|
69b7eb43f6 | ||
|
|
2a8b59b79a | ||
|
|
8f4e9ac48f | ||
|
|
30069e719b | ||
|
|
71fa0dff4b | ||
|
|
40f3a78795 | ||
|
|
0d11c71bb7 | ||
|
|
b58abf2a5c | ||
|
|
f7911701b1 | ||
|
|
f9d4b58588 | ||
|
|
1269aa273b | ||
|
|
758480dd5f | ||
|
|
2ca84501ba | ||
|
|
f9756e0977 | ||
|
|
8894c26748 | ||
|
|
9bb7e3a541 | ||
|
|
235cba5ba5 | ||
|
|
6c04b3936a | ||
|
|
31ba460553 | ||
|
|
57f519db65 | ||
|
|
edf6c65e38 | ||
|
|
349cf1981a | ||
|
|
a15635d953 | ||
|
|
04d9f3808b | ||
|
|
494724c795 | ||
|
|
71cadad05a | ||
|
|
30d204dddc | ||
|
|
cc19748fd2 | ||
|
|
48f197b7ea | ||
|
|
99b0ab5f50 | ||
|
|
db02b4443b | ||
|
|
8e35500269 | ||
|
|
74628642ad | ||
|
|
f41edf284c | ||
|
|
f740fde834 | ||
|
|
db35c28607 | ||
|
|
b1aae4a85a | ||
|
|
f4435c255c | ||
|
|
5aaec02af0 | ||
|
|
7f4b3edd84 | ||
|
|
14cc7fcfeb | ||
|
|
95c0afd5dd | ||
|
|
306ea31f0b | ||
|
|
18b7989e03 | ||
|
|
559eef594e | ||
|
|
7e6d2c6586 | ||
|
|
9fd22a92ac | ||
|
|
e9470f4c94 | ||
|
|
cceb4bb324 | ||
|
|
f14b4f4429 | ||
|
|
23db719c36 | ||
|
|
8b88a17836 | ||
|
|
f1599d6e69 | ||
|
|
df0c72ab0a | ||
|
|
c68395549f | ||
|
|
d69addc3af | ||
|
|
d96e67e850 | ||
|
|
321d9f376e | ||
|
|
e3450352c5 | ||
|
|
4ba8be67fe | ||
|
|
abff997179 | ||
|
|
55216e1de7 | ||
|
|
194f3352a1 | ||
|
|
9e0ae5dc96 | ||
|
|
ba88fd5306 | ||
|
|
179529214b | ||
|
|
b27a7a1c31 | ||
|
|
1a5b5327dc | ||
|
|
d30ac79a77 | ||
|
|
a1461d9ea6 | ||
|
|
783248d58b | ||
|
|
d13d77e39a | ||
|
|
bf333e1964 | ||
|
|
1833e8683b | ||
|
|
e4a336cd67 | ||
|
|
1810956185 | ||
|
|
af7f0f49d1 | ||
|
|
d6ebf8ea04 | ||
|
|
13721c9811 | ||
|
|
eafde17259 | ||
|
|
02810e84a9 | ||
|
|
ac3214fedc | ||
|
|
c376689ad4 | ||
|
|
a12a686a68 | ||
|
|
16ec69bb8c | ||
|
|
82cb95aff7 | ||
|
|
1eb3693f0a | ||
|
|
50925536d1 | ||
|
|
bfd9c654aa | ||
|
|
1104ce3176 | ||
|
|
19ef5b7e1d | ||
|
|
b1477f5fb5 | ||
|
|
8613a89264 | ||
|
|
6a90bac196 | ||
|
|
cda6398010 | ||
|
|
e502f1dcc4 | ||
|
|
13b699a183 | ||
|
|
0cd1f314f6 | ||
|
|
0c894ee48a | ||
|
|
f6f1d4a97c | ||
|
|
c85820e685 | ||
|
|
445f721ceb | ||
|
|
5fa24247c6 | ||
|
|
7ee76fdd2b | ||
|
|
2acfe7f1bf | ||
|
|
b33f660f90 | ||
|
|
91509888af | ||
|
|
fa3657f736 | ||
|
|
09638633c1 | ||
|
|
11a6f1124f | ||
|
|
98ba58f39b | ||
|
|
b6fa4f3242 | ||
|
|
baa7436e43 | ||
|
|
4a9b649e9f | ||
|
|
42143f77c5 | ||
|
|
e2bdd216bb | ||
|
|
856fde79ec | ||
|
|
91987b2e06 | ||
|
|
36c6eeabc9 | ||
|
|
549e364da3 | ||
|
|
1cc2d6c6b7 | ||
|
|
d0321ce0fa | ||
|
|
0d4cca3aa7 | ||
|
|
d77177a98f | ||
|
|
13bcac7e22 | ||
|
|
0046d8ba90 | ||
|
|
4475a93fc5 | ||
|
|
041b609f99 | ||
|
|
bb6653cecb | ||
|
|
d84ce07038 | ||
|
|
c9fec9ad51 | ||
|
|
e953ea7212 | ||
|
|
d1d7cd8186 | ||
|
|
d3e4f17dab | ||
|
|
344fde0d6f | ||
|
|
e27c8955c5 | ||
|
|
27fa234888 | ||
|
|
59ae047359 | ||
|
|
a847a2ff91 | ||
|
|
8445637dd1 | ||
|
|
7f339ede4e | ||
|
|
e54faa3079 | ||
|
|
4d2a4f3433 | ||
|
|
ddd6de24ce | ||
|
|
19f1b969ea | ||
|
|
48beb69103 | ||
|
|
a0be08d62a | ||
|
|
a750c7df2a | ||
|
|
950986c69e | ||
|
|
cc7bddefa1 | ||
|
|
1c4b735880 | ||
|
|
0862c135d0 | ||
|
|
01e3cf1b1c | ||
|
|
519bd389f6 | ||
|
|
ee6aac2614 | ||
|
|
fa319b6529 | ||
|
|
905be4130e | ||
|
|
814e973f9a | ||
|
|
d84596860c | ||
|
|
5d9414e728 | ||
|
|
016e279d43 | ||
|
|
d432047ac1 | ||
|
|
9938e60e05 | ||
|
|
6e751bbfaf | ||
|
|
8bdc07185b | ||
|
|
8872aceb0b | ||
|
|
f7b14c7da7 | ||
|
|
86cd732453 | ||
|
|
ae6571b18b | ||
|
|
0faf773f71 | ||
|
|
3178fb1a46 | ||
|
|
a917e80774 | ||
|
|
9bde5fcad6 | ||
|
|
e33f423733 | ||
|
|
cc35408607 | ||
|
|
10d91b50ec | ||
|
|
7dd06b5659 | ||
|
|
86087bf505 | ||
|
|
cb4923815a | ||
|
|
43258d64d2 | ||
|
|
6d3109be67 | ||
|
|
a25e0cc23f | ||
|
|
026a78b1b6 | ||
|
|
380a4a0395 | ||
|
|
f6c2a6387f | ||
|
|
27f65a92e9 | ||
|
|
858f33f782 | ||
|
|
fa072bf387 | ||
|
|
154ea7354d | ||
|
|
53dbac1d5c | ||
|
|
a780866cd8 | ||
|
|
bf6f1af217 | ||
|
|
29c85f3c65 | ||
|
|
ba51d3ebf5 | ||
|
|
e4f3ea1da9 | ||
|
|
f037452188 | ||
|
|
3d35601d47 | ||
|
|
146e642097 | ||
|
|
e2c53443e3 | ||
|
|
965a98d5a7 | ||
|
|
41a68e7b0e | ||
|
|
20e62b824c | ||
|
|
36e9fcbddf | ||
|
|
bd3d33d67b | ||
|
|
caf77a24f1 | ||
|
|
729f9e103d | ||
|
|
7261debb79 | ||
|
|
b4d0deddfc | ||
|
|
d62a32c7d7 | ||
|
|
cb94fc4d1e | ||
|
|
554888b21d | ||
|
|
4ae01282d7 | ||
|
|
020e62ddf5 | ||
|
|
c1dce98595 | ||
|
|
676187061c | ||
|
|
14f4f26791 | ||
|
|
57cf10c251 | ||
|
|
70b4fae62c | ||
|
|
5c3b9e7fae | ||
|
|
9ea39a78db | ||
|
|
8f77697482 | ||
|
|
1b5e53ddc6 | ||
|
|
86ee9595f4 | ||
|
|
dd7937dba4 | ||
|
|
a4c646152e | ||
|
|
3d7db9a9c7 | ||
|
|
06ed266278 | ||
|
|
5df16db823 | ||
|
|
4f23706b19 | ||
|
|
e9ceadfc34 | ||
|
|
267cdc365d | ||
|
|
6dfbaccee1 | ||
|
|
f97f48d428 | ||
|
|
70b3bc680e | ||
|
|
c125579a38 | ||
|
|
9bc2a7e7bc | ||
|
|
a5358dec14 | ||
|
|
c84f554f36 | ||
|
|
0dd9803f74 | ||
|
|
2baaa26a42 | ||
|
|
3fd62f906e | ||
|
|
41e8b44dde | ||
|
|
09cbd9b606 | ||
|
|
3ab8d79ff7 | ||
|
|
97eaed6d2c | ||
|
|
805aa23948 | ||
|
|
a25c45a502 | ||
|
|
6ebda209ac | ||
|
|
d226b31fab | ||
|
|
27a3009ce1 | ||
|
|
654ad61105 | ||
|
|
31a461195f | ||
|
|
331eef3164 | ||
|
|
cb0d576ade | ||
|
|
32978381b2 | ||
|
|
a7f7a688d3 | ||
|
|
5d87726397 | ||
|
|
b9601cb54a | ||
|
|
bb66555896 | ||
|
|
37726630ce | ||
|
|
3d12f85f66 | ||
|
|
e26762fce3 | ||
|
|
32e59fcce4 | ||
|
|
9b43330e95 | ||
|
|
b3b94243b4 | ||
|
|
fba1032388 | ||
|
|
ccc0803c5b | ||
|
|
f0340bcc98 | ||
|
|
8d19b8fcbf | ||
|
|
dbf66d5a9a | ||
|
|
0fa8d61b19 | ||
|
|
02d326419b | ||
|
|
79b8baac9f | ||
|
|
8a6df8abc7 | ||
|
|
8b8d763fb7 | ||
|
|
608e80a80b | ||
|
|
d90f11eb86 | ||
|
|
dc39187091 | ||
|
|
e4cd418533 | ||
|
|
8559469c73 | ||
|
|
ac8d2beb80 | ||
|
|
5e6384074e | ||
|
|
e6ba7bdd98 | ||
|
|
784055689f | ||
|
|
c937811f45 | ||
|
|
79c8021faa | ||
|
|
a428730f59 | ||
|
|
2522bd44d6 | ||
|
|
3cad8ea046 | ||
|
|
76131f1cc7 | ||
|
|
54fb5dc765 | ||
|
|
4110af56e7 | ||
|
|
ab1775e44b | ||
|
|
9c3facb07a | ||
|
|
e5ae7b2d25 | ||
|
|
60462ff986 | ||
|
|
39443cd676 | ||
|
|
25ab8249ae | ||
|
|
6e86c606cc | ||
|
|
8878b96e74 | ||
|
|
c3ef2edbab | ||
|
|
f7e398edc3 | ||
|
|
601c0217b9 | ||
|
|
b0971b4ba3 | ||
|
|
5a4549c36c | ||
|
|
fe2cc362f8 | ||
|
|
dd0220fd59 | ||
|
|
26fdd9ef6f | ||
|
|
733ee259e5 | ||
|
|
762fecbcff | ||
|
|
5e85dfe5fd | ||
|
|
60916e8b80 | ||
|
|
4fe55634ae | ||
|
|
b1b861c99d | ||
|
|
755fb5c8f3 | ||
|
|
fdb382874d | ||
|
|
f64a226336 | ||
|
|
dec257bb6b | ||
|
|
aeb42b3ffe | ||
|
|
1694a57ed9 | ||
|
|
26001463a0 | ||
|
|
36c1197f1f | ||
|
|
1fa16936fe | ||
|
|
fca65784ee | ||
|
|
c6e06f3941 | ||
|
|
b7e32a60ce | ||
|
|
da100e494b | ||
|
|
9d7b5bccb8 | ||
|
|
112b05c3dd | ||
|
|
987e85cce8 | ||
|
|
8142d0baa7 | ||
|
|
cc32b2661c | ||
|
|
781857f598 | ||
|
|
69179dcb63 | ||
|
|
8017838d60 | ||
|
|
81946493ec | ||
|
|
a7f40c3d50 | ||
|
|
c28723287f | ||
|
|
c7a8588647 | ||
|
|
4a64261c5d | ||
|
|
5f4365542c | ||
|
|
2e2c951ffc | ||
|
|
2ba7dde326 | ||
|
|
a1579ca86b | ||
|
|
81c4ddb85f | ||
|
|
285a8d413a | ||
|
|
672c86b38d | ||
|
|
905611d2a8 | ||
|
|
339fb22217 | ||
|
|
4b0050d26c | ||
|
|
19bf40dc89 | ||
|
|
23cda61d17 | ||
|
|
c74ffde65a | ||
|
|
0a9c10e748 | ||
|
|
6973eaaa02 | ||
|
|
9ce483398b | ||
|
|
55744ab129 | ||
|
|
0e1cb47aa1 | ||
|
|
3bf12753df | ||
|
|
8907659220 | ||
|
|
a4ccb0b620 | ||
|
|
edfe2c7f47 | ||
|
|
ca69673439 | ||
|
|
8677707a9a | ||
|
|
f3abbe5d58 | ||
|
|
c0f36590be | ||
|
|
bdb097a173 | ||
|
|
510491e26d | ||
|
|
6f0015a678 | ||
|
|
690e1d5ea0 | ||
|
|
19734e0bd3 | ||
|
|
5172c0b19e | ||
|
|
d74898c8a3 | ||
|
|
eb10f10988 | ||
|
|
eb2e504acc | ||
|
|
6c502e1213 | ||
|
|
462c00b358 | ||
|
|
b83eb61a42 | ||
|
|
96c35c49ff | ||
|
|
36b48f24dc | ||
|
|
efb21665f0 | ||
|
|
9ab77cfe20 | ||
|
|
6031a349be | ||
|
|
7163daf480 | ||
|
|
f662c39df0 | ||
|
|
d7caba76c4 | ||
|
|
36ff527cc0 | ||
|
|
d96ab77a67 | ||
|
|
b4ade1982e | ||
|
|
4c9ef92c2d | ||
|
|
078fc0f5ed | ||
|
|
40cc73c4f1 | ||
|
|
07c1443a8d | ||
|
|
dc592e0d4f | ||
|
|
c873b6c603 | ||
|
|
c7d0a7a504 | ||
|
|
e7eddf0a7e | ||
|
|
579e382971 | ||
|
|
95ecb05434 | ||
|
|
a2353f32e4 | ||
|
|
793181d208 | ||
|
|
932ff53190 | ||
|
|
e92e23a8be | ||
|
|
a16adb2b46 | ||
|
|
f579ebf5a0 | ||
|
|
67593c97ab | ||
|
|
fe185283a0 | ||
|
|
d2f8214a52 | ||
|
|
a2293b21ce | ||
|
|
57872c38b3 | ||
|
|
51f998c177 | ||
|
|
7abbc630c3 | ||
|
|
a0dee6cb42 | ||
|
|
d38143ae1c | ||
|
|
1db36dfb3e | ||
|
|
a347de96ed | ||
|
|
31c76bf56d | ||
|
|
8abf321d9a | ||
|
|
3f7b6e7be4 | ||
|
|
7b1337cd83 | ||
|
|
7f63a221af | ||
|
|
cfeb5c9495 | ||
|
|
17655cd855 | ||
|
|
ab071cd989 | ||
|
|
c73dd10783 | ||
|
|
6a5584ae41 | ||
|
|
16e82592a6 | ||
|
|
842e771eef | ||
|
|
d31f9c49f5 | ||
|
|
8b61c7ddc3 | ||
|
|
af247e2dd6 | ||
|
|
35360ae196 | ||
|
|
f37117ed09 | ||
|
|
9d2d80db25 | ||
|
|
364d8db5b2 | ||
|
|
0ac5215d4b | ||
|
|
c5ab0c8d02 | ||
|
|
03323e5df8 | ||
|
|
a8c429b77e | ||
|
|
52095c8ed9 | ||
|
|
0be67907ba | ||
|
|
ba564869a0 | ||
|
|
a6ddddea99 | ||
|
|
a686a18d56 | ||
|
|
a98bd132e1 | ||
|
|
a100ca33b9 | ||
|
|
d2f75262d8 | ||
|
|
76a8735133 | ||
|
|
da3c1334da | ||
|
|
92500e96d4 | ||
|
|
c2ba6f5cb3 | ||
|
|
07610e7556 | ||
|
|
741749dffb | ||
|
|
593c777ff0 | ||
|
|
8a616fa0c5 | ||
|
|
f842812745 | ||
|
|
1d9839ed23 | ||
|
|
3c4fc4c1bd | ||
|
|
1013cf527d | ||
|
|
1684ee3074 | ||
|
|
7170e58551 | ||
|
|
02968f1ece | ||
|
|
67c6e03f3b | ||
|
|
faa3b519b6 | ||
|
|
3b4dca7545 | ||
|
|
66495cbf83 | ||
|
|
95fea87b29 | ||
|
|
a4d7aaf0a1 | ||
|
|
d7fd04de03 | ||
|
|
f447cd9be6 | ||
|
|
bd034237c0 | ||
|
|
66aff0d9a2 | ||
|
|
04a4142128 | ||
|
|
ec71da4062 | ||
|
|
0ae97aa933 | ||
|
|
580e19de0c | ||
|
|
1adeee9ba1 | ||
|
|
b598339e55 | ||
|
|
c65e5d8795 | ||
|
|
b8597bc196 | ||
|
|
be75a8e2b8 | ||
|
|
2352840c3b | ||
|
|
98f9f59af3 | ||
|
|
2f746ff791 | ||
|
|
8b881dfd20 | ||
|
|
cfece32185 | ||
|
|
d09ce859aa | ||
|
|
0f3efaa3fe | ||
|
|
55b5f49c79 | ||
|
|
3601acf66e | ||
|
|
383306c0ab | ||
|
|
5f87be16d7 | ||
|
|
63ab01d364 | ||
|
|
1095ed1663 | ||
|
|
c5557e45c4 | ||
|
|
f5159a93f3 | ||
|
|
b00d39e531 | ||
|
|
0c077b5647 | ||
|
|
98aed2a8b6 | ||
|
|
0523873e5e | ||
|
|
ed5ce93df6 | ||
|
|
ab4f85b862 | ||
|
|
67912a918f | ||
|
|
0af939bd37 | ||
|
|
b712b54786 | ||
|
|
19bf52661e | ||
|
|
c0c0261c68 | ||
|
|
1eea941b55 | ||
|
|
547a8fb6c4 | ||
|
|
a0271926e6 | ||
|
|
7e796fc3f1 | ||
|
|
ba17b9d789 | ||
|
|
a4c17562ad | ||
|
|
ca173fb491 | ||
|
|
9330689641 | ||
|
|
ca67553e66 | ||
|
|
dabcb6d4f4 | ||
|
|
753eb79a15 | ||
|
|
034c9e8ad5 | ||
|
|
ba04783e8f | ||
|
|
a8ba51cdee | ||
|
|
24997c39f5 | ||
|
|
919311e2ab | ||
|
|
61c3b0bfff | ||
|
|
8024738400 | ||
|
|
ac58bc677f | ||
|
|
3b887f7f1b | ||
|
|
8ea24a156e | ||
|
|
264fe8f0c8 | ||
|
|
3f67cd4a99 | ||
|
|
5d9e9dfc4a | ||
|
|
c1c6c812c8 | ||
|
|
0a8d947375 | ||
|
|
c9a699fc30 | ||
|
|
8bf350a7dd | ||
|
|
bbc468db0f | ||
|
|
2353f074ab | ||
|
|
79b7174a83 | ||
|
|
82e1c088c7 | ||
|
|
1d13a3de3c | ||
|
|
0fb0bafc14 | ||
|
|
6cc5547509 | ||
|
|
a72df53b69 | ||
|
|
f0802038db | ||
|
|
631b4b7a61 | ||
|
|
d9436520af | ||
|
|
d805cfbd6a | ||
|
|
078d5023c4 | ||
|
|
bc053da538 | ||
|
|
5d79bbce39 | ||
|
|
98c8d56dc5 | ||
|
|
1acaa18c3c | ||
|
|
8325fe0cee | ||
|
|
eedd12207c | ||
|
|
64ed8c3de0 | ||
|
|
ab4f2f3922 | ||
|
|
fc5bf2dc4b | ||
|
|
bd0fabd1f6 | ||
|
|
a4a2963bc3 | ||
|
|
310f47b52a | ||
|
|
b4a187dd02 | ||
|
|
f4afdca576 | ||
|
|
28bebff7b0 | ||
|
|
c3a4daef87 | ||
|
|
18a5a28283 | ||
|
|
36d08d2dca | ||
|
|
91bf6e667d | ||
|
|
a88a47e223 | ||
|
|
8ede0f2089 | ||
|
|
576f6c81a1 | ||
|
|
1666fc4aa0 | ||
|
|
1216882e89 | ||
|
|
3f825a163b | ||
|
|
2fcfa10573 | ||
|
|
e0f03ec582 | ||
|
|
54139845ac | ||
|
|
b007aea2f5 | ||
|
|
3900645d1f | ||
|
|
5d644467fc | ||
|
|
29fcd07b1c | ||
|
|
69f99742a8 | ||
|
|
e7dc901d5e | ||
|
|
4dba7fa7fc | ||
|
|
55d4201aaf | ||
|
|
74a4e464c8 | ||
|
|
329c196047 | ||
|
|
2946e931aa | ||
|
|
30871f0cd1 | ||
|
|
70aa165444 | ||
|
|
beb35c6108 | ||
|
|
90d1cb9a7f | ||
|
|
5754d66560 | ||
|
|
77ba02f0ae | ||
|
|
effdc862a2 | ||
|
|
59c4736f41 | ||
|
|
dff831eafe | ||
|
|
4765640dc7 | ||
|
|
b8799a91b4 | ||
|
|
17aaa90fb1 | ||
|
|
40b8969d44 | ||
|
|
73454d97e1 | ||
|
|
7cf39aac5a | ||
|
|
2880168566 | ||
|
|
615d9240ad | ||
|
|
76f8aa4f60 | ||
|
|
0172dd0b53 | ||
|
|
69f13621ca | ||
|
|
92b9efea12 | ||
|
|
d1af92942d | ||
|
|
b47b5e2dc0 | ||
|
|
3265a94d26 | ||
|
|
e9d9dc2748 | ||
|
|
8b38d4967c | ||
|
|
3729070e33 | ||
|
|
d415c20446 | ||
|
|
efb5931be3 | ||
|
|
4867a5510a | ||
|
|
932dd79ac1 | ||
|
|
dc5957dd0a | ||
|
|
bb131b4ff5 | ||
|
|
76e3d3523e | ||
|
|
2bc9dd2802 | ||
|
|
0950c3d80d | ||
|
|
808ea00787 | ||
|
|
c4400f8a64 | ||
|
|
4a78cfe00a | ||
|
|
4e073b4681 | ||
|
|
e9548b2e03 | ||
|
|
05820e973b | ||
|
|
f779b6fe7d | ||
|
|
210c46f49d | ||
|
|
e746ddc525 | ||
|
|
d186d4ce1e | ||
|
|
cee80cc579 | ||
|
|
1c0232cf96 | ||
|
|
77ff7a8aba | ||
|
|
fed48de0d5 | ||
|
|
95c1df229f | ||
|
|
78b2cb8a32 | ||
|
|
e1ba7a0ad6 | ||
|
|
e64597eaf0 | ||
|
|
f1b7ea13ef | ||
|
|
25d6a0a160 | ||
|
|
5be13a645e | ||
|
|
f18a9fe7f5 | ||
|
|
a5d0fe5b8b | ||
|
|
3ef70a1213 | ||
|
|
3bd2136450 | ||
|
|
c165146d7c | ||
|
|
adfdae3a8e | ||
|
|
81aaeaf25f | ||
|
|
3cc62563df | ||
|
|
aa87cb0064 | ||
|
|
1e6e2d32e3 | ||
|
|
a95805bc04 | ||
|
|
a5ccce30d6 | ||
|
|
3f9c1af8e8 | ||
|
|
be9a88029b | ||
|
|
de53581826 | ||
|
|
d8db54d8eb | ||
|
|
64aa8e9190 | ||
|
|
7719fb5fed | ||
|
|
e5d8fac004 | ||
|
|
c8ce6e0dc1 | ||
|
|
7523ff50ff | ||
|
|
74cd7f07a0 | ||
|
|
1edfcc2c42 | ||
|
|
631f283cba | ||
|
|
a33609b144 | ||
|
|
572e028917 | ||
|
|
31262f9f7f | ||
|
|
e5af0e778f | ||
|
|
97a6d23d36 | ||
|
|
a43236dfc3 | ||
|
|
12e65994ae | ||
|
|
d7b43ef5cf | ||
|
|
97a7496854 | ||
|
|
e6e24ef953 | ||
|
|
1903fc2a6f | ||
|
|
94613f0458 | ||
|
|
4313aed9f0 | ||
|
|
1cbc3a0063 | ||
|
|
102e5cba6b | ||
|
|
e932780f02 | ||
|
|
1b0415f81d | ||
|
|
aa82383d2c | ||
|
|
606ddb7b38 | ||
|
|
f3f7829bbe | ||
|
|
ae9a009f1f | ||
|
|
6c6bf7870d | ||
|
|
6cff9d6fb5 | ||
|
|
0a71be1205 | ||
|
|
3736c3380c | ||
|
|
1beb25a820 | ||
|
|
202eb95eb3 | ||
|
|
eaa5a326fe | ||
|
|
e05ee6bba0 | ||
|
|
111ed742ec | ||
|
|
8fd5743d75 | ||
|
|
63b79ccf3b | ||
|
|
45f4265c03 | ||
|
|
b7b13ea2cb | ||
|
|
1f660b180e | ||
|
|
3b2ccf75ec | ||
|
|
734fc65c29 | ||
|
|
5252ed16ca | ||
|
|
a812796bdc | ||
|
|
42b86c6b18 | ||
|
|
8aec2275fd | ||
|
|
61f03d734b | ||
|
|
907e20d0ec | ||
|
|
f84c759e8d | ||
|
|
f6fb4695c1 | ||
|
|
3a1ccb5ba0 | ||
|
|
7b8ab4ac2c | ||
|
|
53504f1c3d | ||
|
|
d5408165f9 | ||
|
|
9bf8c115c1 | ||
|
|
a06ac4cbb6 | ||
|
|
6406b7412d | ||
|
|
41aca47f92 | ||
|
|
a170e1cfb5 | ||
|
|
b2429a6a1b | ||
|
|
23e1baf92f | ||
|
|
278c94b7df | ||
|
|
256fc5a222 | ||
|
|
a0fa28b3fd | ||
|
|
09d8212225 | ||
|
|
e658786e88 | ||
|
|
4a1e6eba8c | ||
|
|
ce5e209681 | ||
|
|
40178b5277 | ||
|
|
631c487233 | ||
|
|
5e47afe3a4 | ||
|
|
942bf9094d | ||
|
|
985200b6d9 | ||
|
|
35b61bc891 | ||
|
|
b149abbb23 | ||
|
|
536387ad8c | ||
|
|
1628edbb8b | ||
|
|
39adbbdb27 | ||
|
|
be49fa9b54 | ||
|
|
aaed8435d1 | ||
|
|
c5bd406351 | ||
|
|
4004caadc7 | ||
|
|
1ac2a782c4 | ||
|
|
6887928bba | ||
|
|
1401537796 | ||
|
|
e53f5ca175 | ||
|
|
463f49586c | ||
|
|
5a87098f95 | ||
|
|
da44ff05aa | ||
|
|
3839171d42 | ||
|
|
8d9a009f89 | ||
|
|
2c0a4cf7a7 | ||
|
|
4b2269b668 | ||
|
|
3bfff093f8 | ||
|
|
6d835a2068 | ||
|
|
36398c54e0 | ||
|
|
2a2777b22d | ||
|
|
11dc69334d | ||
|
|
c4c17aa115 | ||
|
|
4b41c06dc4 | ||
|
|
a9e27cd63f | ||
|
|
0f6b4f2b32 | ||
|
|
08877155e2 | ||
|
|
7007f3ea44 | ||
|
|
9419cce747 | ||
|
|
033c884059 | ||
|
|
99b0b65e89 | ||
|
|
67042470f3 | ||
|
|
7c5388ee71 | ||
|
|
96634ece3f | ||
|
|
ee9ea92a11 | ||
|
|
246e9f7e3f | ||
|
|
fc43f89d9e | ||
|
|
be75dc95a3 | ||
|
|
6bb0f7b902 | ||
|
|
6178c56606 | ||
|
|
0a2ca923ee | ||
|
|
5adc4cb437 | ||
|
|
28d4371f4d | ||
|
|
d343bbe1ac | ||
|
|
c6a6163aec | ||
|
|
be60a37a29 | ||
|
|
b3d42866b7 | ||
|
|
9633fb659a | ||
|
|
fab637f5ae | ||
|
|
edcd991659 | ||
|
|
84f41b9c2f | ||
|
|
e7a61f07a2 | ||
|
|
57616ffd83 | ||
|
|
ea002e6634 | ||
|
|
24574360a0 | ||
|
|
e4dff4916b | ||
|
|
bd1ff4c954 | ||
|
|
563c762756 | ||
|
|
6c6bd65969 | ||
|
|
e2dbd5216d | ||
|
|
959a05643f | ||
|
|
3e0f33859a | ||
|
|
b70615c19c | ||
|
|
a1d6d17685 | ||
|
|
fe768a650d | ||
|
|
d9ba282024 | ||
|
|
1b48cc99e5 | ||
|
|
8740e20c90 | ||
|
|
93a2cdf4bf | ||
|
|
0faa4e62f0 | ||
|
|
78e97e9731 | ||
|
|
d59be759bd | ||
|
|
2df78eb436 | ||
|
|
3f132a759f | ||
|
|
b87244bf9a | ||
|
|
6d826001cc | ||
|
|
2de4d36c9f | ||
|
|
ef01212ce8 | ||
|
|
9fa8c36b5f | ||
|
|
a4973616b4 | ||
|
|
a8dd2ed2da | ||
|
|
2766667d16 | ||
|
|
a7e7b9a3ca | ||
|
|
7df272fd4a | ||
|
|
cd694366ed | ||
|
|
4a9fb62663 | ||
|
|
8c7144205b | ||
|
|
f4057d4c2c | ||
|
|
ad5de8c84c | ||
|
|
15f414cae3 | ||
|
|
54d01d2ffd | ||
|
|
f8e87c5aa1 | ||
|
|
090a85bdd5 | ||
|
|
403611443f | ||
|
|
82b14a14e0 | ||
|
|
a0789b45e4 | ||
|
|
12975b1ecf | ||
|
|
59de1212cb | ||
|
|
cc8246b474 | ||
|
|
f13a91e83e | ||
|
|
598aae8ef1 | ||
|
|
fe5414bdf4 | ||
|
|
fd40289887 | ||
|
|
225b8aa63a | ||
|
|
e63bbe734c | ||
|
|
8c76fb6f7c | ||
|
|
617ed0a3cd | ||
|
|
e32e7dd828 | ||
|
|
d6ba027ae7 | ||
|
|
1ea4a7c113 | ||
|
|
8d7e161662 | ||
|
|
332b04d640 | ||
|
|
e7af8305c2 | ||
|
|
edffba3496 | ||
|
|
f403ff7776 | ||
|
|
e873150542 | ||
|
|
73440be270 | ||
|
|
e8caa8853e | ||
|
|
4d63643fbf | ||
|
|
c632303fd6 | ||
|
|
06596d8626 | ||
|
|
e0a47b050a | ||
|
|
55bd9bbc7e | ||
|
|
a4c47b920c | ||
|
|
25355596bb | ||
|
|
a40d128704 | ||
|
|
2f0ab09250 | ||
|
|
6be425de4d | ||
|
|
35725b2324 | ||
|
|
2f51f985d8 | ||
|
|
bacdca038e | ||
|
|
f80e190e22 | ||
|
|
024aec1891 | ||
|
|
b80f6cc507 | ||
|
|
be11046cfc | ||
|
|
0a0522d92d | ||
|
|
f436a34474 | ||
|
|
7ab3884cb9 | ||
|
|
b616be8c0e | ||
|
|
65431c462f | ||
|
|
bc6ae0e773 | ||
|
|
5698b2eab9 | ||
|
|
4b1ff7deb5 | ||
|
|
327c0d7a2e | ||
|
|
90522914c0 | ||
|
|
cf44a36153 | ||
|
|
ba23cfdaca | ||
|
|
a85888bcbd | ||
|
|
3d8c25159d | ||
|
|
543f73bf7a | ||
|
|
4130082a8d | ||
|
|
692815713b | ||
|
|
14bef07d25 | ||
|
|
e9c69a118c | ||
|
|
275faea616 | ||
|
|
f4268bb447 | ||
|
|
35eeae7c58 | ||
|
|
ed04df26f8 | ||
|
|
79efaad817 | ||
|
|
1075745439 | ||
|
|
2f7255301d | ||
|
|
fc60d50560 | ||
|
|
488d32974f | ||
|
|
bdd12b262e | ||
|
|
e2fa8b3199 | ||
|
|
2128f46165 | ||
|
|
14bd2c6a3b | ||
|
|
109ee591c1 | ||
|
|
c67cf56dc5 | ||
|
|
727dcc149d | ||
|
|
b450178444 | ||
|
|
23e281689a | ||
|
|
f96eb630d1 | ||
|
|
7cd16c7063 | ||
|
|
87a04193f9 | ||
|
|
5c196d7e47 | ||
|
|
8db1b230d4 | ||
|
|
f92e98d587 | ||
|
|
9b3ed76fb0 | ||
|
|
91780f0b9c | ||
|
|
0f7f7946cd | ||
|
|
c4a0037956 | ||
|
|
586492fc92 | ||
|
|
cfe34628fa | ||
|
|
1e2b7c7e02 | ||
|
|
c12a91ee5f | ||
|
|
c93a2dcb1c | ||
|
|
9baa529200 | ||
|
|
29bf1e5dc4 | ||
|
|
0f1b78e1a5 | ||
|
|
d03820bf91 | ||
|
|
adbf7aeb42 | ||
|
|
b57bf29247 | ||
|
|
4a96468e42 | ||
|
|
272c7850d7 | ||
|
|
7466bda816 | ||
|
|
c202399eb6 | ||
|
|
3c046020ef | ||
|
|
af3ab140bd | ||
|
|
9d9d43a249 | ||
|
|
5045447bc6 | ||
|
|
a9772dd313 | ||
|
|
59999abb61 | ||
|
|
23a42b7c48 | ||
|
|
8bda1fe719 | ||
|
|
207e55e869 | ||
|
|
a09b8f1762 | ||
|
|
28f23ae595 | ||
|
|
da0dc31ed0 | ||
|
|
6a9873dbaa | ||
|
|
c68d311f92 | ||
|
|
c148fa9000 | ||
|
|
26fc48ce14 | ||
|
|
0eb7174183 | ||
|
|
248e8750e1 | ||
|
|
6c240fc5d3 | ||
|
|
def3d617b0 | ||
|
|
c49314e755 | ||
|
|
18a80d4fdd | ||
|
|
195e136798 | ||
|
|
606e702e6f | ||
|
|
aed7a6fbf3 | ||
|
|
bbb79bba0f | ||
|
|
731a838c16 | ||
|
|
1a55c472e0 | ||
|
|
222b476d84 | ||
|
|
0e42f31b88 | ||
|
|
d5b3f605f3 | ||
|
|
17471db248 | ||
|
|
e2cdff3604 | ||
|
|
118b1a039b | ||
|
|
ea8119f3ad | ||
|
|
9b0f548336 | ||
|
|
e8a7c15fee | ||
|
|
6055127118 | ||
|
|
81e4a402f2 | ||
|
|
ca975e4f94 | ||
|
|
2e06972161 | ||
|
|
d6f26f78a5 | ||
|
|
ae53634f48 | ||
|
|
09d8e1ce6b | ||
|
|
535069587e | ||
|
|
5318fbaca1 | ||
|
|
5f251c296e | ||
|
|
291f87e197 | ||
|
|
cca86141fd | ||
|
|
84c4fb825c | ||
|
|
79272be631 | ||
|
|
4b9d03fb59 | ||
|
|
3c95e88f08 | ||
|
|
73ed4fa6f3 | ||
|
|
6451f580cb | ||
|
|
dcf8a4948b | ||
|
|
3458cec41e | ||
|
|
809008561f | ||
|
|
72d60e1227 | ||
|
|
8fc335718a | ||
|
|
e6129ad78b | ||
|
|
10802d6a2c | ||
|
|
7b05d26d7e | ||
|
|
fa8d67ebe1 | ||
|
|
0d320c26cd | ||
|
|
96c0276a0e | ||
|
|
3cc5a8ae5c | ||
|
|
5b0ca03640 | ||
|
|
0af08cf578 | ||
|
|
dfe21c5b8c | ||
|
|
f8c5da52f3 | ||
|
|
20752bf48e | ||
|
|
7778790bae | ||
|
|
fccbc90307 | ||
|
|
c5895a7d21 | ||
|
|
9262c8527b | ||
|
|
32484570bb | ||
|
|
f26f6c33d0 | ||
|
|
ec2db0594e | ||
|
|
a6f3f425c3 | ||
|
|
4566fec58a | ||
|
|
a362206e55 | ||
|
|
a9543e50f2 | ||
|
|
06fc3e726a | ||
|
|
e19a2ac67a | ||
|
|
5e28fb5246 | ||
|
|
2ad9fcf701 | ||
|
|
427b38912f | ||
|
|
5d4619a70a | ||
|
|
5e56f27e45 | ||
|
|
92fca450f1 | ||
|
|
e52f8bbbde | ||
|
|
94c6ccb549 | ||
|
|
a8edd8ffb9 | ||
|
|
298768f0cb | ||
|
|
b566098a56 | ||
|
|
f8f34e46e3 | ||
|
|
9227831922 | ||
|
|
625d4c951e | ||
|
|
5cdc479029 | ||
|
|
d385585291 | ||
|
|
248107bfb8 | ||
|
|
539e336fa1 | ||
|
|
8ab07563e0 | ||
|
|
487332df40 | ||
|
|
68d156c0e8 | ||
|
|
a9a5622525 | ||
|
|
9e02950844 | ||
|
|
a1730c9524 | ||
|
|
ed3257399b | ||
|
|
763f65cbbe | ||
|
|
26675eac64 | ||
|
|
4789be70fc | ||
|
|
2b0168296f | ||
|
|
2a8868e029 | ||
|
|
7410abf895 | ||
|
|
86d0c3bfcb | ||
|
|
fb14747a8b | ||
|
|
d11fbc1069 | ||
|
|
b48d8d4372 | ||
|
|
e257a64772 | ||
|
|
a6b1961d77 | ||
|
|
d363212191 | ||
|
|
266bf202bb | ||
|
|
f280445138 | ||
|
|
fcd93c8db8 | ||
|
|
d02f0bf5b4 | ||
|
|
986734e29b | ||
|
|
147d9dbe83 | ||
|
|
c974853fe8 | ||
|
|
d124f9c8e0 | ||
|
|
cb9222271d | ||
|
|
72505b550d | ||
|
|
7500b602dc | ||
|
|
89410cc55d | ||
|
|
9d3b17d86b | ||
|
|
553e3b02f5 | ||
|
|
756f1dd218 | ||
|
|
accf332668 | ||
|
|
4b0b5cb85c | ||
|
|
23ad776a74 | ||
|
|
02cc9ec935 | ||
|
|
1d0c21eff1 | ||
|
|
422c97f681 | ||
|
|
f0d00420c3 | ||
|
|
64c1eac2d0 | ||
|
|
ae7c98ce78 | ||
|
|
e04fc7d950 | ||
|
|
97f75fb971 | ||
|
|
0e2c411198 | ||
|
|
ca7917b407 | ||
|
|
69013d2765 | ||
|
|
8a022a2365 | ||
|
|
0d07af4862 | ||
|
|
d5279fb50c | ||
|
|
7d887bdbef | ||
|
|
d12770f97c | ||
|
|
a3d9efbda2 | ||
|
|
3f6479e578 | ||
|
|
5d8d8a5ffe | ||
|
|
9d511e0370 | ||
|
|
73d26b45fc | ||
|
|
03d1cc4f78 | ||
|
|
49c89810d8 | ||
|
|
4dc3647370 | ||
|
|
6ab00e46b2 | ||
|
|
ec22d72f3f | ||
|
|
97324ce4d4 | ||
|
|
a11d84e812 | ||
|
|
1d7774bcd6 | ||
|
|
806a8efe94 | ||
|
|
b0894b1e75 | ||
|
|
92faccdd90 | ||
|
|
7025accd88 |
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
gns3/version.py merge=ours
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -37,6 +37,7 @@ nosetests.xml
|
||||
|
||||
# PyCharm
|
||||
.idea
|
||||
/.eggs
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
@@ -50,3 +51,15 @@ nosetests.xml
|
||||
|
||||
# Qt creator
|
||||
*.autosave
|
||||
|
||||
# Licence keys
|
||||
keys
|
||||
|
||||
# Custom config
|
||||
/gns3_server.ini
|
||||
updates
|
||||
.cache
|
||||
__pycache__
|
||||
|
||||
# Virtualenv
|
||||
env
|
||||
|
||||
50
.travis.yml
50
.travis.yml
@@ -1,33 +1,21 @@
|
||||
language: python
|
||||
|
||||
#New container architecture
|
||||
#http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
#sudo: false
|
||||
|
||||
python:
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
|
||||
cache:
|
||||
apt: true
|
||||
directories:
|
||||
- build
|
||||
|
||||
before_install:
|
||||
- sh scripts/prepare_travis.sh
|
||||
|
||||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
notifications:
|
||||
email:
|
||||
- julien@gns3.net
|
||||
#irc:
|
||||
# channels:
|
||||
# - "chat.freenode.net#gns3"
|
||||
# on_success: change
|
||||
# on_failure: always
|
||||
|
||||
install:
|
||||
- "pip install --upgrade -r dev-requirements.txt"
|
||||
|
||||
email: false
|
||||
script:
|
||||
- "xvfb-run py.test -vv" # Run tests in a fake X server
|
||||
# - "pep8 --exclude=build,.git,ui"
|
||||
- docker build -t gns3-gui-test .
|
||||
- docker run gns3-gui-test
|
||||
before_deploy:
|
||||
- sudo pip install twine
|
||||
- sudo pip install urllib3[secure]
|
||||
deploy:
|
||||
provider: pypi
|
||||
edge:
|
||||
branch: v1.8.45
|
||||
user: noplay
|
||||
password:
|
||||
secure: FofcqlJjgqf2jaDaXpLHeigVoexbrOz3WwnDuiJpwJxeFUlPY8s2cQs/Bm+dzxzZaOaGiVE0A83v/Xa10yD5tflThHt4sqYJK3iQCinA7wgeAlDimB4xrWUNplfNJZ/Eod5Ssa++E02W+3i29PxpXY//mjCY7qDxaoxul1gnFJY=
|
||||
on:
|
||||
tags: true
|
||||
repo: GNS3/gns3-gui
|
||||
|
||||
@@ -18,13 +18,17 @@ it on https://github.com/GNS3/gns3-gui we will take care of the triage.
|
||||
|
||||
For bugs specific to the GNS3 VM, please report on https://github.com/GNS3/gns3-vm
|
||||
|
||||
## Asking for new features
|
||||
## Security issues
|
||||
|
||||
For security issues please keep it private and send an email to developers@gns3.net
|
||||
|
||||
## Asking for new features
|
||||
|
||||
The best is to start a discussion on the community website in order to get feedback
|
||||
from the whole community.
|
||||
|
||||
|
||||
## Contributing code
|
||||
## Contributing code
|
||||
|
||||
We welcome code contribution from everyone including beginners.
|
||||
Don't be afraid to submit a half finished or mediocre contribution and we will help you.
|
||||
@@ -45,6 +49,6 @@ The reason we do this is to ensure, to the extent possible, that we don’t “t
|
||||
|
||||
More information there: https://github.com/GNS3/cla
|
||||
|
||||
### Pull requests
|
||||
### Pull requests
|
||||
|
||||
Creating a pull request is the easiest way to contribute code. Do not hesitate to create one early when contributing for new feature in order to get our feedback.
|
||||
|
||||
70
COPYING
70
COPYING
@@ -272,50 +272,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License notice for Apache Libcloud
|
||||
----------------------------------
|
||||
https://github.com/apache/libcloud/blob/trunk/LICENSE
|
||||
|
||||
Copyright (c) 2010-2015 The Apache Software Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
License notice for requests
|
||||
---------------------------
|
||||
https://github.com/kennethreitz/requests/blob/master/LICENSE
|
||||
|
||||
Copyright (c) 2015 Kenneth Reitz
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
License notice for gns3-converter
|
||||
---------------------------------
|
||||
https://github.com/dlintott/gns3-converter/blob/master/COPYING
|
||||
|
||||
License notice for paramiko
|
||||
---------------------------
|
||||
https://github.com/paramiko/paramiko/blob/master/LICENSE
|
||||
|
||||
License notice for pywin32
|
||||
--------------------------
|
||||
https://github.com/SublimeText/Pywin32/blob/master/License.txt
|
||||
@@ -520,3 +476,29 @@ THE POSSIBILITY OF SUCH DAMAGE.
|
||||
License notice for Python
|
||||
-------------------------
|
||||
https://www.python.org/download/releases/3.4.2/license/
|
||||
|
||||
License notice for BusyBox
|
||||
---------------------------
|
||||
BusyBox is distributed under version 2 of the General Public License
|
||||
https://busybox.net/license.html
|
||||
|
||||
Source code is available here:
|
||||
https://github.com/GNS3/busybox
|
||||
|
||||
|
||||
Licence notice for zipstream
|
||||
-----------------------------
|
||||
zipstream is distributed under version 3 of the General Public License
|
||||
https://github.com/allanlei/python-zipstream/blob/master/LICENSE
|
||||
|
||||
Source code is available here:
|
||||
https://pypi.python.org/pypi/zipstream
|
||||
|
||||
|
||||
Licence notice for aiohttp_cors
|
||||
-------------------------------
|
||||
Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>.
|
||||
|
||||
Licensed under the Apache License, Version 2.0, see LICENSE file for details.
|
||||
|
||||
https://github.com/aio-libs/aiohttp_cors
|
||||
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# Run tests inside a container
|
||||
FROM ubuntu:17.10
|
||||
|
||||
MAINTAINER GNS3 Team
|
||||
|
||||
#ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update
|
||||
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
|
||||
|
||||
|
||||
ADD dev-requirements.txt /dev-requirements.txt
|
||||
ADD requirements.txt /requirements.txt
|
||||
RUN pip3 install -r /dev-requirements.txt
|
||||
|
||||
|
||||
ADD . /src
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
CMD xvfb-run python3.6 -m pytest -vv
|
||||
@@ -1,8 +1,8 @@
|
||||
include README.rst
|
||||
include AUTHORS
|
||||
include INSTALL
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
include tox.ini
|
||||
recursive-include tests *
|
||||
recursive-include gns3 *
|
||||
|
||||
103
README.rst
103
README.rst
@@ -10,86 +10,10 @@ GNS3-gui
|
||||
|
||||
GNS3 GUI repository.
|
||||
|
||||
Linux (Debian based)
|
||||
--------------------
|
||||
Installation
|
||||
------------
|
||||
|
||||
The following instructions have been tested with Ubuntu and Mint.
|
||||
You must be connected to the Internet in order to install the dependencies.
|
||||
|
||||
Dependencies:
|
||||
|
||||
- Python 3.3 or above
|
||||
- Setuptools
|
||||
- PyQt libraries
|
||||
- Apache Libcloud library
|
||||
- Requests library
|
||||
- Paramiko library
|
||||
|
||||
The following commands will install some of these dependencies:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
sudo apt-get install python3-setuptools
|
||||
sudo apt-get install python3-pyqt4
|
||||
|
||||
Finally these commands will install the GUI as well as the rest of the dependencies:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd gns3-gui-master
|
||||
sudo python3 setup.py install
|
||||
gns3
|
||||
|
||||
Windows
|
||||
-------
|
||||
|
||||
Please use our `all-in-one installer <https://gns3.com/software/download>`_ to install the stable build.
|
||||
|
||||
If you install via source you need to first install:
|
||||
|
||||
- Python (3.3 or above) - https://www.python.org/downloads/windows/
|
||||
- Pywin32 - https://sourceforge.net/projects/pywin32/
|
||||
- Qt4 - http://www.qt.io/download-open-source/
|
||||
- PyQt4 - http://www.riverbankcomputing.com/software/pyqt/download
|
||||
- PyCrypto (which if you compile from source, requires Visual Studio 2010 with GMP or MPIR libraries)
|
||||
|
||||
And finally, call
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python setup.py install
|
||||
|
||||
to install the remaining dependencies.
|
||||
|
||||
Mac OS X
|
||||
--------
|
||||
|
||||
Please use our DMG package or you can manually install using the following steps (experimental):
|
||||
|
||||
`First install homebrew <http://brew.sh/>`_.
|
||||
|
||||
Then install the GNS3 dependencies.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
brew install python3
|
||||
brew install qt
|
||||
brew install sip --without-python --with-python3
|
||||
brew install pyqt --without-python --with-python3
|
||||
|
||||
Finally, install both the GUI & server from the source.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd gns3-gui-master
|
||||
python3 setup.py install
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd gns3-server-master
|
||||
python3 setup.py install
|
||||
|
||||
Or follow this `HOWTO that uses MacPorts <http://binarynature.blogspot.ca/2014/05/install-gns3-early-release-on-mac-os-x.html>`_.
|
||||
https://gns3.com/support/docs
|
||||
|
||||
Development
|
||||
-------------
|
||||
@@ -100,3 +24,24 @@ If you want to update the interface, modify the .ui files using QT tools. And:
|
||||
|
||||
cd scripts
|
||||
python build_pyqt.py
|
||||
|
||||
Debug
|
||||
"""""
|
||||
|
||||
If you want to see the full logs in the internal shell you can type:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
debug 2
|
||||
|
||||
|
||||
Or start the app with --debug flag.
|
||||
|
||||
Due to the fact PyQT intercept you can use a web debugger for inspecting stuff:
|
||||
https://github.com/Kozea/wdb
|
||||
|
||||
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
|
||||
|
||||
40
fake_frozen_gns3.py
Executable file
40
fake_frozen_gns3.py
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
This script fake GNS3 run as a frozen app.
|
||||
|
||||
Use it for testing stuff like self update.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
|
||||
# Fake GNS3 run from a binary
|
||||
sys.executable = os.path.realpath(__file__)
|
||||
|
||||
# Add site-package directory before cx_freeze directory
|
||||
sys.path.insert(0, os.path.dirname(sys.executable))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(sys.executable), 'site-packages'))
|
||||
|
||||
sys.frozen = True
|
||||
sys.executable = "/Applications/GNS3.app/Contents/MacOS/gns3"
|
||||
os.environ["_"] = "/Applications/GNS3.app/Contents/MacOS/gns3"
|
||||
|
||||
module = importlib.import_module("gns3.main")
|
||||
module.main()
|
||||
29
gns3-gui.appdata.xml
Normal file
29
gns3-gui.appdata.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright 2016 Athmane Madjoudj <athmane@fedoraproject.org> -->
|
||||
<component type="desktop">
|
||||
<id>gns3-gui.desktop</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<name>GNS3</name>
|
||||
<summary>Graphical Network Simulator 3</summary>
|
||||
<description>
|
||||
<p>
|
||||
GNS3 is a graphical network simulator that allows you to design complex network
|
||||
topologies. You may run simulations or configure devices ranging from simple
|
||||
workstations to powerful routers.
|
||||
</p>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://a.fsdn.com/con/app/proj/gns-3/screenshots/127765.jpg</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://a.fsdn.com/con/app/proj/gns-3/screenshots/127755.jpg</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://a.fsdn.com/con/app/proj/gns-3/screenshots/127755.jpg</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<url type="homepage">http://gns3.com/</url>
|
||||
<update_contact>athmane_at_fedoraproject.org</update_contact>
|
||||
</component>
|
||||
9
gns3-gui.desktop
Normal file
9
gns3-gui.desktop
Normal file
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Name=GNS3
|
||||
GenericName=Graphical Network Simulator 3
|
||||
Comment=Graphical Network Simulator 3
|
||||
Exec=gns3
|
||||
Icon=gns3
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Application;Network;Qt;
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -15,13 +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/>.
|
||||
|
||||
"""
|
||||
Base class for NIOs (Network Input/Output).
|
||||
"""
|
||||
from .main import main
|
||||
|
||||
|
||||
class NIO(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
pass
|
||||
main()
|
||||
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
|
||||
66
gns3/application.py
Normal file
66
gns3/application.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
from .qt import QtWidgets, QtGui, QtCore
|
||||
from gns3.utils import parse_version
|
||||
from .version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Application(QtWidgets.QApplication):
|
||||
file_open_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, argv, hdpi=True):
|
||||
|
||||
self.setStyle(QtWidgets.QStyleFactory.create("Fusion"))
|
||||
# both Qt and PyQt must be version >= 5.6 in order to enable high DPI scaling
|
||||
if parse_version(QtCore.QT_VERSION_STR) >= parse_version("5.6") and parse_version(QtCore.PYQT_VERSION_STR) >= parse_version("5.6"):
|
||||
# only available starting Qt version 5.6
|
||||
if hdpi:
|
||||
if sys.platform.startswith("linux"):
|
||||
log.warning("HDPI mode is enabled. HDPI support on Linux is not fully stable and GNS3 may crash depending of your version of Linux. To disabled HDPI mode please edit ~/.config/GNS3/gns3_gui.conf and set 'hdpi' to 'false'")
|
||||
self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
|
||||
self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
|
||||
else:
|
||||
log.info("HDPI mode is disabled")
|
||||
self.setAttribute(QtCore.Qt.AA_DisableHighDpiScaling)
|
||||
|
||||
super().__init__(argv)
|
||||
|
||||
# this info is necessary for QSettings
|
||||
self.setOrganizationName("GNS3")
|
||||
self.setOrganizationDomain("gns3.net")
|
||||
self.setApplicationName("GNS3")
|
||||
self.setApplicationVersion(__version__)
|
||||
|
||||
# File path if we have received the path to
|
||||
# a file on system via an OSX event
|
||||
self.open_file_at_startup = None
|
||||
|
||||
def event(self, event):
|
||||
# When you double click file you receive an event
|
||||
# and not the file as command line parameter
|
||||
if sys.platform.startswith("darwin"):
|
||||
if isinstance(event, QtGui.QFileOpenEvent):
|
||||
self.open_file_at_startup = str(event.file())
|
||||
self.file_open_signal.emit(str(event.file()))
|
||||
return super().event(event)
|
||||
399
gns3/base_node.py
Normal file
399
gns3/base_node.py
Normal file
@@ -0,0 +1,399 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Base class for node classes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from .qt import QtCore
|
||||
from .ports.port import Port
|
||||
from .utils.normalize_filename import normalize_filename
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseNode(QtCore.QObject):
|
||||
|
||||
"""
|
||||
BaseNode implementation.
|
||||
|
||||
:param module: Module instance
|
||||
:param server: client connection to a server
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
# signals used to let the GUI know about some events.
|
||||
created_signal = QtCore.Signal(int)
|
||||
started_signal = QtCore.Signal()
|
||||
stopped_signal = QtCore.Signal()
|
||||
suspended_signal = QtCore.Signal()
|
||||
updated_signal = QtCore.Signal()
|
||||
loaded_signal = QtCore.Signal()
|
||||
deleted_signal = QtCore.Signal()
|
||||
error_signal = QtCore.Signal(int, str)
|
||||
server_error_signal = QtCore.Signal(int, str)
|
||||
|
||||
_instance_count = 1
|
||||
_allocated_names = set()
|
||||
|
||||
# node statuses
|
||||
stopped = 0
|
||||
started = 1
|
||||
suspended = 2
|
||||
|
||||
# node categories
|
||||
routers = 0
|
||||
switches = 1
|
||||
end_devices = 2
|
||||
security_devices = 3
|
||||
|
||||
def __init__(self, module, compute, project):
|
||||
|
||||
super().__init__()
|
||||
# create an unique ID
|
||||
self._id = BaseNode._instance_count
|
||||
BaseNode._instance_count += 1
|
||||
|
||||
self._module = module
|
||||
self._compute = compute
|
||||
assert project is not None
|
||||
self._project = project
|
||||
self._initialized = False
|
||||
self._loading = False
|
||||
self._status = BaseNode.stopped
|
||||
self._ports = []
|
||||
self._links = set()
|
||||
|
||||
def links(self):
|
||||
"""
|
||||
Links connected to the node
|
||||
"""
|
||||
return self._links
|
||||
|
||||
def addLink(self, link):
|
||||
self._links.add(link)
|
||||
|
||||
def deleteLink(self, link):
|
||||
try:
|
||||
self._links.remove(link)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
"""
|
||||
Reset the instance count.
|
||||
"""
|
||||
|
||||
cls._instance_count = 1
|
||||
|
||||
def module(self):
|
||||
"""
|
||||
Returns this node module.
|
||||
|
||||
:returns: Module instance
|
||||
"""
|
||||
|
||||
return self._module
|
||||
|
||||
def compute(self):
|
||||
"""
|
||||
Returns this node compute.
|
||||
|
||||
:returns: Compute instance
|
||||
"""
|
||||
return self._compute
|
||||
|
||||
def project(self):
|
||||
"""
|
||||
Returns this node project.
|
||||
|
||||
:returns: Project instance
|
||||
"""
|
||||
|
||||
return self._project
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
Returns this node identifier.
|
||||
|
||||
:returns: node identifier (integer)
|
||||
"""
|
||||
|
||||
return self._id
|
||||
|
||||
def setId(self, new_id):
|
||||
"""
|
||||
Sets an identifier for this node.
|
||||
|
||||
:param new_id: node identifier (integer)
|
||||
"""
|
||||
|
||||
self._id = new_id
|
||||
|
||||
# update the instance count to avoid conflicts
|
||||
if new_id >= BaseNode._instance_count:
|
||||
BaseNode._instance_count = new_id + 1
|
||||
|
||||
def status(self):
|
||||
"""
|
||||
Returns the status of this node.
|
||||
0 = stopped, 1 = started, 2 = suspended.
|
||||
|
||||
:returns: node status (integer)
|
||||
"""
|
||||
|
||||
return self._status
|
||||
|
||||
def setStatus(self, status):
|
||||
"""
|
||||
Sets a status for this node.
|
||||
0 = stopped, 1 = started, 2 = suspended.
|
||||
|
||||
:param status: node status (integer)
|
||||
"""
|
||||
|
||||
if status == self._status:
|
||||
return
|
||||
self._status = status
|
||||
if status == self.started:
|
||||
for port in self._ports:
|
||||
# set ports as started
|
||||
port.setStatus(Port.started)
|
||||
self.started_signal.emit()
|
||||
elif status == self.stopped:
|
||||
for port in self._ports:
|
||||
# set ports as stopped
|
||||
port.setStatus(Port.stopped)
|
||||
self.stopped_signal.emit()
|
||||
elif status == self.suspended:
|
||||
for port in self._ports:
|
||||
# set ports as suspended
|
||||
port.setStatus(Port.suspended)
|
||||
self.suspended_signal.emit()
|
||||
|
||||
def initialized(self):
|
||||
"""
|
||||
Returns if the node has been initialized
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._initialized
|
||||
|
||||
def setInitialized(self, initialized):
|
||||
"""
|
||||
Sets if the node has been initialized
|
||||
|
||||
:param initialized: boolean
|
||||
"""
|
||||
|
||||
self._initialized = initialized
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this node.
|
||||
Must be overloaded.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def ports(self):
|
||||
"""
|
||||
Returns all the ports for this node.
|
||||
|
||||
:returns: list of Port instances
|
||||
"""
|
||||
|
||||
return self._ports
|
||||
|
||||
@staticmethod
|
||||
def defaultCategories():
|
||||
"""
|
||||
Returns the default categories.
|
||||
|
||||
:returns: dict
|
||||
"""
|
||||
|
||||
categories = {"Routers": BaseNode.routers,
|
||||
"Switches": BaseNode.switches,
|
||||
"End devices": BaseNode.end_devices,
|
||||
"Security devices": BaseNode.security_devices}
|
||||
|
||||
return categories
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
Must be overloaded.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
"""
|
||||
Returns the symbol name (for the nodes view).
|
||||
|
||||
:returns: name (string)
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def categories(self):
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node category (integer)
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Must be overloaded.
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def controllerHttpPost(self, path, callback, body={}, context={}, **kwargs):
|
||||
"""
|
||||
POST on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param body: params to send (dictionary)
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.post(path, callback, body=body, context=context, **kwargs)
|
||||
|
||||
def controllerHttpPut(self, path, callback, body={}, context={}, **kwargs):
|
||||
"""
|
||||
PUT on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param body: params to send (dictionary)
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.put(path, callback, body=body, context=context, **kwargs)
|
||||
|
||||
def controllerHttpGet(self, path, callback, context={}, **kwargs):
|
||||
"""
|
||||
Get on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param body: params to send (dictionary)
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.get(path, callback, context=context, **kwargs)
|
||||
|
||||
def controllerHttpDelete(self, path, callback, context={}, **kwargs):
|
||||
"""
|
||||
Delete on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.delete(path, callback, context=context, **kwargs)
|
||||
|
||||
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
|
||||
@@ -1,337 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Base cloud controller class.
|
||||
|
||||
Base class for interacting with Cloud APIs to create and manage cloud
|
||||
instances.
|
||||
|
||||
"""
|
||||
from collections import namedtuple
|
||||
import hashlib
|
||||
import os
|
||||
import logging
|
||||
from io import StringIO, BytesIO
|
||||
|
||||
from libcloud.compute.base import NodeAuthSSHKey
|
||||
from libcloud.storage.types import ContainerAlreadyExistsError, ContainerDoesNotExistError, ObjectDoesNotExistError
|
||||
|
||||
from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed
|
||||
from .exceptions import OverLimit, BadRequest, ServiceUnavailable
|
||||
from .exceptions import Unauthorized, ApiError
|
||||
|
||||
|
||||
KeyPair = namedtuple("KeyPair", ['name'], verbose=False)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_exception(exception):
|
||||
"""
|
||||
Parse the exception to separate the HTTP status code from the text.
|
||||
|
||||
Libcloud raises many exceptions of the form:
|
||||
Exception("<http status code> <http error> <reponse body>")
|
||||
|
||||
in lieu of raising specific incident-based exceptions.
|
||||
|
||||
"""
|
||||
|
||||
e_str = str(exception)
|
||||
|
||||
try:
|
||||
status = int(e_str[0:3])
|
||||
error_text = e_str[3:]
|
||||
|
||||
except ValueError:
|
||||
status = None
|
||||
error_text = e_str
|
||||
|
||||
return status, error_text
|
||||
|
||||
|
||||
class BaseCloudCtrl(object):
|
||||
|
||||
""" Base class for interacting with a cloud provider API. """
|
||||
|
||||
http_status_to_exception = {
|
||||
400: BadRequest,
|
||||
401: Unauthorized,
|
||||
404: ItemNotFound,
|
||||
405: MethodNotAllowed,
|
||||
413: OverLimit,
|
||||
500: ApiError,
|
||||
503: ServiceUnavailable
|
||||
}
|
||||
|
||||
GNS3_CONTAINER_NAME = 'GNS3'
|
||||
|
||||
def __init__(self, username, api_key):
|
||||
self.username = username
|
||||
self.api_key = api_key
|
||||
|
||||
def _handle_exception(self, status, error_text, response_overrides=None):
|
||||
""" Raise an exception based on the HTTP status. """
|
||||
|
||||
if response_overrides:
|
||||
if status in response_overrides:
|
||||
raise response_overrides[status](error_text)
|
||||
|
||||
raise self.http_status_to_exception[status](error_text)
|
||||
|
||||
def authenticate(self):
|
||||
""" Validate cloud account credentials. Return boolean. """
|
||||
raise NotImplementedError
|
||||
|
||||
def list_sizes(self):
|
||||
""" Return a list of NodeSize objects. """
|
||||
|
||||
return self.driver.list_sizes()
|
||||
|
||||
def list_flavors(self):
|
||||
""" Return an iterable of flavors """
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def create_instance(self, name, size_id, image_id, keypair):
|
||||
"""
|
||||
Create a new instance with the supplied attributes.
|
||||
|
||||
Return a Node object.
|
||||
|
||||
"""
|
||||
try:
|
||||
image = self.get_image(image_id)
|
||||
if image is None:
|
||||
raise ItemNotFound("Image not found")
|
||||
|
||||
size = self.driver.ex_get_size(size_id)
|
||||
|
||||
args = {
|
||||
"name": name,
|
||||
"size": size,
|
||||
"image": image,
|
||||
}
|
||||
|
||||
if keypair is not None:
|
||||
auth_key = NodeAuthSSHKey(keypair.public_key)
|
||||
args["auth"] = auth_key
|
||||
args["ex_keyname"] = name
|
||||
|
||||
return self.driver.create_node(**args)
|
||||
|
||||
except Exception as e:
|
||||
status, error_text = parse_exception(e)
|
||||
|
||||
if status:
|
||||
self._handle_exception(status, error_text)
|
||||
else:
|
||||
log.error("create_instance method raised an exception: {}".format(e))
|
||||
log.error('image id {}'.format(image_id))
|
||||
|
||||
def delete_instance(self, instance):
|
||||
""" Delete the specified instance. Returns True or False. """
|
||||
|
||||
try:
|
||||
return self.driver.destroy_node(instance)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
status, error_text = parse_exception(e)
|
||||
|
||||
if status:
|
||||
self._handle_exception(status, error_text)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def get_instance(self, instance):
|
||||
""" Return a Node object representing the requested instance. """
|
||||
|
||||
for i in self.driver.list_nodes():
|
||||
if i.id == instance.id:
|
||||
return i
|
||||
|
||||
raise ItemNotFound("Instance not found")
|
||||
|
||||
def list_instances(self):
|
||||
""" Return a list of instances in the current region. """
|
||||
|
||||
return self.driver.list_nodes()
|
||||
|
||||
def create_key_pair(self, name):
|
||||
""" Create and return a new Key Pair. """
|
||||
|
||||
response_overrides = {
|
||||
409: KeyPairExists
|
||||
}
|
||||
try:
|
||||
return self.driver.create_key_pair(name)
|
||||
|
||||
except Exception as e:
|
||||
status, error_text = parse_exception(e)
|
||||
if status:
|
||||
self._handle_exception(status, error_text, response_overrides)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def delete_key_pair(self, keypair):
|
||||
""" Delete the keypair. Returns True or False. """
|
||||
|
||||
try:
|
||||
return self.driver.delete_key_pair(keypair)
|
||||
|
||||
except Exception as e:
|
||||
status, error_text = parse_exception(e)
|
||||
if status:
|
||||
self._handle_exception(status, error_text)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def delete_key_pair_by_name(self, keypair_name):
|
||||
""" Utility method to incapsulate boilerplate code """
|
||||
|
||||
kp = KeyPair(name=keypair_name)
|
||||
return self.delete_key_pair(kp)
|
||||
|
||||
def list_key_pairs(self):
|
||||
""" Return a list of Key Pairs. """
|
||||
|
||||
return self.driver.list_key_pairs()
|
||||
|
||||
def upload_file(self, file_path, cloud_object_name):
|
||||
"""
|
||||
Uploads file to cloud storage (if it is not identical to a file already in cloud storage).
|
||||
:param file_path: path to file to upload
|
||||
:param cloud_object_name: name of file saved in cloud storage
|
||||
:return: True if file was uploaded, False if it was skipped because it already existed and was identical
|
||||
"""
|
||||
try:
|
||||
gns3_container = self.storage_driver.create_container(self.GNS3_CONTAINER_NAME)
|
||||
except ContainerAlreadyExistsError:
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
|
||||
with open(file_path, 'rb') as file:
|
||||
local_file_hash = hashlib.md5(file.read()).hexdigest()
|
||||
|
||||
cloud_hash_name = cloud_object_name + '.md5'
|
||||
cloud_objects = [obj.name for obj in gns3_container.list_objects()]
|
||||
|
||||
# if the file and its hash are in object storage, and the local and storage file hashes match
|
||||
# do not upload the file, otherwise upload it
|
||||
if cloud_object_name in cloud_objects and cloud_hash_name in cloud_objects:
|
||||
hash_object = gns3_container.get_object(cloud_hash_name)
|
||||
cloud_object_hash = ''
|
||||
for chunk in hash_object.as_stream():
|
||||
cloud_object_hash += chunk.decode('utf8')
|
||||
|
||||
if cloud_object_hash == local_file_hash:
|
||||
return False
|
||||
|
||||
file.seek(0)
|
||||
self.storage_driver.upload_object_via_stream(file, gns3_container, cloud_object_name)
|
||||
self.storage_driver.upload_object_via_stream(StringIO(local_file_hash), gns3_container, cloud_hash_name)
|
||||
return True
|
||||
|
||||
def list_projects(self):
|
||||
"""
|
||||
Lists projects in cloud storage
|
||||
:return: Dictionary where project names are keys and values are names of objects in storage
|
||||
"""
|
||||
|
||||
try:
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
projects = {
|
||||
obj.name.replace('projects/', '').replace('.zip', ''): obj.name
|
||||
for obj in gns3_container.list_objects()
|
||||
if obj.name.startswith('projects/') and obj.name[-4:] == '.zip'
|
||||
}
|
||||
return projects
|
||||
except ContainerDoesNotExistError:
|
||||
return []
|
||||
|
||||
def download_file(self, file_name, destination=None):
|
||||
"""
|
||||
Downloads file from cloud storage. If a file exists at destination, and it is identical to the file in cloud
|
||||
storage, it is not downloaded.
|
||||
:param file_name: name of file in cloud storage to download
|
||||
:param destination: local path to save file to (if None, returns file contents as a file-like object)
|
||||
:return: A file-like object if file contents are returned, or None if file is saved to filesystem
|
||||
"""
|
||||
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
storage_object = gns3_container.get_object(file_name)
|
||||
|
||||
if destination is not None:
|
||||
if os.path.isfile(destination):
|
||||
# if a file exists at destination and its hash matches that of the
|
||||
# file in cloud storage, don't download it
|
||||
with open(destination, 'rb') as f:
|
||||
local_file_hash = hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
hash_object = gns3_container.get_object(file_name + '.md5')
|
||||
cloud_object_hash = ''
|
||||
for chunk in hash_object.as_stream():
|
||||
cloud_object_hash += chunk.decode('utf8')
|
||||
|
||||
if local_file_hash == cloud_object_hash:
|
||||
return
|
||||
|
||||
storage_object.download(destination)
|
||||
else:
|
||||
contents = b''
|
||||
|
||||
for chunk in storage_object.as_stream():
|
||||
contents += chunk
|
||||
|
||||
return BytesIO(contents)
|
||||
|
||||
def find_storage_image_names(self, images_to_find):
|
||||
"""
|
||||
Maps names of image files to their full name in cloud storage
|
||||
:param images_to_find: list of image names to find
|
||||
:return: A dictionary where keys are image names, and values are the corresponding names of
|
||||
the files in cloud storage
|
||||
"""
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
images_in_storage = [obj.name for obj in gns3_container.list_objects() if obj.name.startswith('images/')]
|
||||
|
||||
images = {}
|
||||
for image_name in images_to_find:
|
||||
images_with_same_name =\
|
||||
list(filter(lambda storage_image_name: storage_image_name.endswith(image_name), images_in_storage))
|
||||
|
||||
if len(images_with_same_name) == 1:
|
||||
images[image_name] = images_with_same_name[0]
|
||||
else:
|
||||
raise Exception('Image does not exist in cloud storage or is duplicated')
|
||||
|
||||
return images
|
||||
|
||||
def delete_file(self, file_name):
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
|
||||
try:
|
||||
object_to_delete = gns3_container.get_object(file_name)
|
||||
object_to_delete.delete()
|
||||
except ObjectDoesNotExistError:
|
||||
pass
|
||||
|
||||
try:
|
||||
hash_object = gns3_container.get_object(file_name + '.md5')
|
||||
hash_object.delete()
|
||||
except ObjectDoesNotExistError:
|
||||
pass
|
||||
@@ -1,67 +0,0 @@
|
||||
""" Exception classes for CloudCtrl classes. """
|
||||
|
||||
|
||||
class ApiError(Exception):
|
||||
|
||||
""" Raised when the server returns 500 Compute Error. """
|
||||
pass
|
||||
|
||||
|
||||
class BadRequest(Exception):
|
||||
|
||||
""" Raised when the server returns 400 Bad Request. """
|
||||
pass
|
||||
|
||||
|
||||
class ComputeFault(Exception):
|
||||
|
||||
""" Raised when the server returns 400|500 Compute Fault. """
|
||||
pass
|
||||
|
||||
|
||||
class Forbidden(Exception):
|
||||
|
||||
""" Raised when the server returns 403 Forbidden. """
|
||||
pass
|
||||
|
||||
|
||||
class ItemNotFound(Exception):
|
||||
|
||||
""" Raised when the server returns 404 Not Found. """
|
||||
pass
|
||||
|
||||
|
||||
class KeyPairExists(Exception):
|
||||
|
||||
""" Raised when the server returns 409 Conflict Key pair exists. """
|
||||
pass
|
||||
|
||||
|
||||
class MethodNotAllowed(Exception):
|
||||
|
||||
""" Raised when the server returns 405 Method Not Allowed. """
|
||||
pass
|
||||
|
||||
|
||||
class OverLimit(Exception):
|
||||
|
||||
""" Raised when the server returns 413 Over Limit. """
|
||||
pass
|
||||
|
||||
|
||||
class ServerCapacityUnavailable(Exception):
|
||||
|
||||
""" Raised when the server returns 503 Server Capacity Uavailable. """
|
||||
pass
|
||||
|
||||
|
||||
class ServiceUnavailable(Exception):
|
||||
|
||||
""" Raised when the server returns 503 Service Unavailable. """
|
||||
pass
|
||||
|
||||
|
||||
class Unauthorized(Exception):
|
||||
|
||||
""" Raised when the server returns 401 Unauthorized. """
|
||||
pass
|
||||
@@ -1,259 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
""" Interacts with Rackspace API to create and manage cloud instances. """
|
||||
|
||||
from .base_cloud_ctrl import BaseCloudCtrl
|
||||
import json
|
||||
import requests
|
||||
from libcloud.compute.drivers.rackspace import ENDPOINT_ARGS_MAP
|
||||
from libcloud.compute.providers import get_driver
|
||||
from libcloud.compute.types import Provider
|
||||
from libcloud.storage.providers import get_driver as get_storage_driver
|
||||
from libcloud.storage.types import Provider as StorageProvider
|
||||
|
||||
from .exceptions import ItemNotFound, ApiError
|
||||
from ..version import __version__
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
RACKSPACE_REGIONS = [{ENDPOINT_ARGS_MAP[k]['region']: k} for k in
|
||||
ENDPOINT_ARGS_MAP]
|
||||
|
||||
|
||||
class RackspaceCtrl(BaseCloudCtrl):
|
||||
|
||||
""" Controller class for interacting with Rackspace API. """
|
||||
|
||||
def __init__(self, username, api_key, *args, **kwargs):
|
||||
super(RackspaceCtrl, self).__init__(username, api_key)
|
||||
|
||||
# set this up so it can be swapped out with a mock for testing
|
||||
self.post_fn = requests.post
|
||||
self.driver_cls = get_driver(Provider.RACKSPACE)
|
||||
self.storage_driver_cls = get_storage_driver(StorageProvider.CLOUDFILES)
|
||||
|
||||
self.driver = None
|
||||
self.storage_driver = None
|
||||
self.region = None
|
||||
self.instances = {}
|
||||
|
||||
self.authenticated = False
|
||||
self.identity_ep = \
|
||||
"https://identity.api.rackspacecloud.com/v2.0/tokens"
|
||||
|
||||
self.regions = []
|
||||
self.token = None
|
||||
self.tenant_id = None
|
||||
self.flavor_ep = "https://dfw.servers.api.rackspacecloud.com/v2/{username}/flavors"
|
||||
self._flavors = OrderedDict([
|
||||
('2', '512MB, 1 VCPU'),
|
||||
('3', '1GB, 1 VCPU'),
|
||||
('4', '2GB, 2 VCPUs'),
|
||||
('5', '4GB, 2 VCPUs'),
|
||||
('6', '8GB, 4 VCPUs'),
|
||||
('7', '15GB, 6 VCPUs'),
|
||||
('8', '30GB, 8 VCPUs'),
|
||||
('performance1-1', '1GB Performance, 1 VCPU'),
|
||||
('performance1-2', '2GB Performance, 2 VCPUs'),
|
||||
('performance1-4', '4GB Performance, 4 VCPUs'),
|
||||
('performance1-8', '8GB Performance, 8 VCPUs'),
|
||||
('performance2-15', '15GB Performance, 4 VCPUs'),
|
||||
('performance2-30', '30GB Performance, 8 VCPUs'),
|
||||
('performance2-60', '60GB Performance, 16 VCPUs'),
|
||||
('performance2-90', '90GB Performance, 24 VCPUs'),
|
||||
('performance2-120', '120GB Performance, 32 VCPUs',)
|
||||
])
|
||||
|
||||
def authenticate(self):
|
||||
"""
|
||||
Submit username and api key to API service.
|
||||
|
||||
If authentication is successful, set self.regions and self.token.
|
||||
Return boolean.
|
||||
|
||||
"""
|
||||
|
||||
self.authenticated = False
|
||||
|
||||
if len(self.username) < 1:
|
||||
return False
|
||||
|
||||
if len(self.api_key) < 1:
|
||||
return False
|
||||
|
||||
data = json.dumps({
|
||||
"auth": {
|
||||
"RAX-KSKEY:apiKeyCredentials": {
|
||||
"username": self.username,
|
||||
"apiKey": self.api_key
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
headers = {
|
||||
'Content-type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
response = self.post_fn(self.identity_ep, data=data, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
|
||||
api_data = response.json()
|
||||
self.token = self._parse_token(api_data)
|
||||
|
||||
if self.token:
|
||||
self.authenticated = True
|
||||
user_regions = self._parse_endpoints(api_data)
|
||||
self.regions = self._make_region_list(user_regions)
|
||||
self.tenant_id = self._parse_tenant_id(api_data)
|
||||
|
||||
else:
|
||||
self.regions = []
|
||||
self.token = None
|
||||
|
||||
response.connection.close()
|
||||
|
||||
return self.authenticated
|
||||
|
||||
def list_regions(self):
|
||||
""" Return a list the regions available to the user. """
|
||||
|
||||
return self.regions
|
||||
|
||||
def list_flavors(self):
|
||||
""" Return the dictionary containing flavors id and names """
|
||||
|
||||
return self._flavors
|
||||
|
||||
def _parse_endpoints(self, api_data):
|
||||
"""
|
||||
Parse the JSON-encoded data returned by the Identity Service API.
|
||||
|
||||
Return a list of regions available for Compute v2.
|
||||
|
||||
"""
|
||||
|
||||
region_codes = []
|
||||
|
||||
for ep_type in api_data['access']['serviceCatalog']:
|
||||
if ep_type['name'] == "cloudServersOpenStack" \
|
||||
and ep_type['type'] == "compute":
|
||||
|
||||
for ep in ep_type['endpoints']:
|
||||
if ep['versionId'] == "2":
|
||||
region_codes.append(ep['region'])
|
||||
|
||||
return region_codes
|
||||
|
||||
def _parse_token(self, api_data):
|
||||
""" Parse the token from the JSON-encoded data returned by the API. """
|
||||
|
||||
try:
|
||||
token = api_data['access']['token']['id']
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
return token
|
||||
|
||||
def _parse_tenant_id(self, api_data):
|
||||
""" """
|
||||
try:
|
||||
roles = api_data['access']['user']['roles']
|
||||
for role in roles:
|
||||
if 'tenantId' in role and role['name'] == 'compute:default':
|
||||
return role['tenantId']
|
||||
return None
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def _make_region_list(self, region_codes):
|
||||
"""
|
||||
Make a list of regions for use in the GUI.
|
||||
|
||||
Returns a list of key-value pairs in the form:
|
||||
<API's Region Name>: <libcloud's Region Name>
|
||||
eg,
|
||||
[
|
||||
{'DFW': 'dfw'}
|
||||
{'ORD': 'ord'},
|
||||
...
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
region_list = []
|
||||
|
||||
for ep in ENDPOINT_ARGS_MAP:
|
||||
if ENDPOINT_ARGS_MAP[ep]['region'] in region_codes:
|
||||
region_list.append({ENDPOINT_ARGS_MAP[ep]['region']: ep})
|
||||
|
||||
return region_list
|
||||
|
||||
def set_region(self, region):
|
||||
""" Set self.region and self.driver. Returns True or False. """
|
||||
|
||||
try:
|
||||
self.driver = self.driver_cls(self.username, self.api_key,
|
||||
region=region)
|
||||
self.storage_driver = self.storage_driver_cls(self.username, self.api_key,
|
||||
region=region)
|
||||
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
self.region = region
|
||||
return True
|
||||
|
||||
def get_image(self, image_id):
|
||||
return self.driver.get_image(image_id)
|
||||
|
||||
|
||||
def get_provider(cloud_settings):
|
||||
"""
|
||||
Utility function to retrieve a cloud provider instance already authenticated and with the
|
||||
region set
|
||||
|
||||
:param cloud_settings: cloud settings dictionary
|
||||
:return: a provider instance or None on errors
|
||||
"""
|
||||
try:
|
||||
username = cloud_settings['cloud_user_name']
|
||||
apikey = cloud_settings['cloud_api_key']
|
||||
region = cloud_settings['cloud_region']
|
||||
except KeyError as e:
|
||||
log.error("Unable to create cloud provider: {}".format(e))
|
||||
return
|
||||
|
||||
provider = RackspaceCtrl(username, apikey)
|
||||
|
||||
if not provider.authenticate():
|
||||
log.error("Authentication failed for cloud provider")
|
||||
return
|
||||
|
||||
if not region:
|
||||
region = provider.list_regions().values()[0]
|
||||
|
||||
if not provider.set_region(region):
|
||||
log.error("Unable to set cloud provider region")
|
||||
return
|
||||
|
||||
return provider
|
||||
@@ -1,538 +0,0 @@
|
||||
from contextlib import contextmanager
|
||||
import io
|
||||
import json
|
||||
from socket import error as socket_error
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
from ..qt import QtCore
|
||||
|
||||
from .exceptions import KeyPairExists
|
||||
from .rackspace_ctrl import get_provider
|
||||
from ..topology import Topology
|
||||
from ..servers import Servers
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def ssh_client(host, key_string):
|
||||
"""
|
||||
Context manager wrapping a SSHClient instance: the client connects on
|
||||
enter and close the connection on exit
|
||||
"""
|
||||
|
||||
import paramiko
|
||||
|
||||
class AllowAndForgetPolicy(paramiko.MissingHostKeyPolicy):
|
||||
|
||||
"""
|
||||
Custom policy for server host keys: we simply accept the key
|
||||
the server sent to us without storing it.
|
||||
"""
|
||||
|
||||
def missing_host_key(self, *args, **kwargs):
|
||||
"""
|
||||
According to MissingHostKeyPolicy protocol, to accept
|
||||
the key, simply return.
|
||||
"""
|
||||
return
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
try:
|
||||
f_key = io.StringIO(key_string)
|
||||
key = paramiko.RSAKey.from_private_key(f_key)
|
||||
client.set_missing_host_key_policy(AllowAndForgetPolicy())
|
||||
client.connect(hostname=host, username="root", pkey=key)
|
||||
yield client
|
||||
except socket_error as e:
|
||||
log.debug("SSH connection socket error to {}: {}".format(host, e))
|
||||
yield None
|
||||
except Exception as e:
|
||||
log.debug("SSH connection error to {}: {}".format(host, e))
|
||||
yield None
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
class ListInstancesThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Helper class to retrieve data from the provider in a separate thread,
|
||||
avoid freezing the gui
|
||||
"""
|
||||
instancesReady = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent, provider):
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
instances = self._provider.list_instances()
|
||||
log.debug('Instance list: {}'.format([(i.name, i.state) for i in instances]))
|
||||
self.instancesReady.emit(instances)
|
||||
except Exception as e:
|
||||
log.info('list_instances error: {}'.format(e))
|
||||
|
||||
|
||||
class CreateInstanceThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Helper class to create instances in a separate thread
|
||||
"""
|
||||
instanceCreated = QtCore.pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, parent, provider, name, flavor_id, image_id):
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
self._name = name
|
||||
self._flavor_id = flavor_id
|
||||
self._image_id = image_id
|
||||
|
||||
def run(self):
|
||||
log.debug("Creating cloud keypair with name {}".format(self._name))
|
||||
try:
|
||||
k = self._provider.create_key_pair(self._name)
|
||||
except KeyPairExists:
|
||||
log.debug("Cloud keypair with name {} exists. Recreating.".format(self._name))
|
||||
# delete keypairs if they already exist
|
||||
self._provider.delete_key_pair_by_name(self._name)
|
||||
k = self._provider.create_key_pair(self._name)
|
||||
|
||||
log.debug("Creating cloud server with name {}".format(self._name))
|
||||
i = self._provider.create_instance(self._name, self._flavor_id, self._image_id, k)
|
||||
log.debug("Cloud server {} created".format(self._name))
|
||||
|
||||
self.instanceCreated.emit(i, k)
|
||||
|
||||
|
||||
class DeleteInstanceThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Helper class to remove an instance in a separate thread
|
||||
"""
|
||||
instanceDeleted = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent, provider, instance):
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
self._instance = instance
|
||||
|
||||
def run(self):
|
||||
if self._provider.delete_instance(self._instance):
|
||||
self.instanceDeleted.emit(self._instance)
|
||||
|
||||
|
||||
class StartGNS3ServerThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Perform an SSH connection to the instances in a separate thread,
|
||||
outside the GUI event loop, and start GNS3 server
|
||||
"""
|
||||
gns3server_started = QtCore.pyqtSignal(str, str, str)
|
||||
|
||||
# This is for testing without pushing to github
|
||||
# commands = '''
|
||||
# DEBIAN_FRONTEND=noninteractive dpkg --configure -a
|
||||
# DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -y update
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
|
||||
# ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
|
||||
# mkdir -p /opt/gns3
|
||||
# tar xzf /tmp/gns3-server.tgz -C /opt/gns3
|
||||
# cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
|
||||
# cd /opt/gns3/gns3-server; python3 ./setup.py install
|
||||
# ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
|
||||
# wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap.tar.gz'
|
||||
# python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
|
||||
# hostname gns3-iouvm
|
||||
# tar xzf iouyap.tar.gz -C /usr/local/bin
|
||||
# killall python3 gns3server gns3dms
|
||||
# '''
|
||||
|
||||
commands = '''
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --configure -a
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
|
||||
ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
|
||||
mkdir -p /opt/gns3
|
||||
cd /opt/gns3; git clone https://github.com/planctechnologies/gns3-server.git
|
||||
cd /opt/gns3/gns3-server; git checkout dev; git pull
|
||||
cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
|
||||
cd /opt/gns3/gns3-server; python3 ./setup.py install
|
||||
ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
|
||||
wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap-64-bit.tar.gz'
|
||||
tar xzf iouyap-64-bit.tar.gz -C /usr/local/bin
|
||||
python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
|
||||
hostname gns3-iouvm # set hostname for iou
|
||||
wget -O vpcs http://sourceforge.net/projects/vpcs/files/0.6/vpcs_0.6_Linux64/download
|
||||
cp vpcs /usr/local/bin/vpcs
|
||||
chmod a+x /usr/local/bin/vpcs
|
||||
killall python3 gns3server gns3dms
|
||||
'''
|
||||
|
||||
def __init__(self, parent, host, private_key_string, server_id, username, api_key, region, dead_time):
|
||||
super().__init__(parent)
|
||||
self._host = host
|
||||
self._private_key_string = private_key_string
|
||||
self._server_id = server_id
|
||||
self._username = username
|
||||
self._api_key = api_key
|
||||
self._region = region
|
||||
self._dead_time = dead_time
|
||||
|
||||
def exec_command(self, client, cmd, wait_time=-1):
|
||||
|
||||
cmd += '; exit $?'
|
||||
|
||||
stdout_data = b''
|
||||
stderr_data = b''
|
||||
|
||||
log.debug('cmd: {}'.format(cmd))
|
||||
# Send the command (non-blocking)
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
|
||||
# Wait for the command to terminate
|
||||
wait = int(wait_time)
|
||||
while not stdout.channel.exit_status_ready() and wait != 0:
|
||||
time.sleep(1)
|
||||
wait -= 1
|
||||
|
||||
stdout_data = stdout.read()
|
||||
stderr_data = stderr.read()
|
||||
log.debug('exit status: {}'.format(stdout.channel.exit_status))
|
||||
log.debug('stdout: {}'.format(stdout_data.decode('utf-8')))
|
||||
log.debug('stderr: {}'.format(stderr_data.decode('utf-8')))
|
||||
return stdout_data, stderr_data
|
||||
|
||||
def run(self):
|
||||
# We might be attempting a connection before the instance is fully booted, so retry
|
||||
# when the ssh connection fails.
|
||||
ssh_connected = False
|
||||
while not ssh_connected:
|
||||
with ssh_client(self._host, self._private_key_string) as client:
|
||||
if client is None:
|
||||
time.sleep(1)
|
||||
continue
|
||||
ssh_connected = True
|
||||
|
||||
# This is for testing without pushing to github
|
||||
# os.system('rm -rf /tmp/gns3-server')
|
||||
# os.system('cp -a /Users/jseutter/projects/gns3-server /tmp/gns3-server')
|
||||
# os.system('cd /tmp; tar czf /tmp/gns3-server.tgz gns3-server')
|
||||
# sftp = client.open_sftp()
|
||||
# sftp.put('/tmp/gns3-server.tgz', '/tmp/gns3-server.tgz')
|
||||
# sftp.close()
|
||||
|
||||
for cmd in [l for l in self.commands.splitlines() if l.strip()]:
|
||||
self.exec_command(client, cmd)
|
||||
|
||||
data = {
|
||||
'instance_id': self._server_id,
|
||||
'cloud_user_name': self._username,
|
||||
'cloud_api_key': self._api_key,
|
||||
'cloud_region': self._region,
|
||||
'dead_time': self._dead_time,
|
||||
}
|
||||
# TODO: Properly escape the data portion of the command line
|
||||
start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --ip={} --data="{}" 2>/tmp/gns3-stderr.log'.format(self._host, data)
|
||||
stdout, stderr = self.exec_command(client, start_cmd, wait_time=15)
|
||||
response = stdout.decode('utf-8')
|
||||
self.gns3server_started.emit(str(self._server_id), str(self._host), str(response))
|
||||
|
||||
|
||||
class WSConnectThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Establish a websocket connection with the remote gns3server
|
||||
instance. Run outside the GUI event loop.
|
||||
"""
|
||||
established = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent, provider, server_id, host, port, ca_file,
|
||||
auth_user, auth_password, ssh_pkey, instance_id):
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
self._server_id = server_id
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._ca_file = ca_file
|
||||
self._auth_user = auth_user
|
||||
self._auth_password = auth_password
|
||||
self._ssh_pkey = ssh_pkey
|
||||
self._instance_id = instance_id
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Establish a websocket connection to gns3server on the cloud instance.
|
||||
"""
|
||||
|
||||
log.debug('WSConnectThread.run() begin')
|
||||
servers = Servers.instance()
|
||||
server = servers.getCloudServer(self._host, self._port, self._ca_file,
|
||||
self._auth_user, self._auth_password, self._ssh_pkey,
|
||||
self._instance_id)
|
||||
log.debug('after getCloudServer call. {}'.format(server))
|
||||
self.established.emit(str(self._server_id))
|
||||
|
||||
log.debug('WSConnectThread.run() end')
|
||||
# emit signal on success
|
||||
self.established.emit(self._server_id)
|
||||
|
||||
|
||||
class UploadProjectThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Zip and Upload project to the cloud
|
||||
"""
|
||||
|
||||
# signals to update the progress dialog.
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent, cloud_settings, project_path, images_path):
|
||||
super().__init__(parent)
|
||||
self.cloud_settings = cloud_settings
|
||||
self.project_path = project_path
|
||||
self.images_path = images_path
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
log.info("Exporting project to cloud")
|
||||
self.update.emit(0)
|
||||
|
||||
zipped_project_file = self.zip_project_dir()
|
||||
|
||||
self.update.emit(10) # update progress to 10%
|
||||
|
||||
provider = get_provider(self.cloud_settings)
|
||||
provider.upload_file(zipped_project_file, 'projects/' + os.path.basename(zipped_project_file))
|
||||
|
||||
self.update.emit(20) # update progress to 20%
|
||||
|
||||
topology = Topology.instance()
|
||||
images = set([node.settings()["image"] for node in topology.nodes() if 'image' in node.settings()])
|
||||
|
||||
for i, image in enumerate(images):
|
||||
provider.upload_file(image, 'images/' + os.path.relpath(image, self.images_path))
|
||||
self.update.emit(20 + (float(i) / len(images) * 80))
|
||||
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error exporting project to cloud")
|
||||
self.error.emit("Error exporting project: {}".format(e), True)
|
||||
|
||||
def zip_project_dir(self):
|
||||
"""
|
||||
Zips project files
|
||||
:return: path to zipped project file
|
||||
"""
|
||||
project_name = os.path.basename(self.project_path)
|
||||
output_filename = os.path.join(tempfile.gettempdir(), project_name + ".zip")
|
||||
project_dir = os.path.dirname(self.project_path)
|
||||
relroot = os.path.abspath(os.path.join(project_dir, os.pardir))
|
||||
with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for root, dirs, files in os.walk(project_dir):
|
||||
# add directory (needed for empty dirs)
|
||||
zip_file.write(root, os.path.relpath(root, relroot))
|
||||
for file in files:
|
||||
filename = os.path.join(root, file)
|
||||
if os.path.isfile(filename) and not self._should_exclude(filename): # regular files only
|
||||
arcname = os.path.join(os.path.relpath(root, relroot), file)
|
||||
zip_file.write(filename, arcname)
|
||||
|
||||
return output_filename
|
||||
|
||||
def _should_exclude(self, filename):
|
||||
"""
|
||||
Returns True if file should be excluded from zip of project files
|
||||
:param filename:
|
||||
:return: True if file should be excluded from zip, False otherwise
|
||||
"""
|
||||
return filename.endswith('.ghost')
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class UploadFilesThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Uploads files to cloud files
|
||||
|
||||
:param cloud_settings:
|
||||
:param files_to_upload: list of tuples of (file path, file name to save in cloud)
|
||||
"""
|
||||
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent, cloud_settings, files_to_upload):
|
||||
super().__init__(parent)
|
||||
self._cloud_settings = cloud_settings
|
||||
self._files_to_upload = files_to_upload
|
||||
|
||||
def run(self):
|
||||
self.update.emit(0)
|
||||
|
||||
try:
|
||||
for i, file_to_upload in enumerate(self._files_to_upload):
|
||||
provider = get_provider(self._cloud_settings)
|
||||
|
||||
log.debug('Uploading image {} to cloud as {}'.format(file_to_upload[0], file_to_upload[1]))
|
||||
provider.upload_file(file_to_upload[0], file_to_upload[1])
|
||||
|
||||
self.update.emit((i + 1) * 100 / len(self._files_to_upload))
|
||||
log.debug('Uploading image completed')
|
||||
except Exception as e:
|
||||
log.exception("Error uploading images to cloud")
|
||||
self.error.emit("Error uploading images: {}".format(e), True)
|
||||
|
||||
self.completed.emit()
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class DownloadProjectThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Downloads project from cloud storage
|
||||
"""
|
||||
|
||||
# signals to update the progress dialog.
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent, cloud_project_file_name, project_dest_path, images_dest_path, cloud_settings):
|
||||
super().__init__(parent)
|
||||
self.project_name = cloud_project_file_name
|
||||
self.project_dest_path = project_dest_path
|
||||
self.images_dest_path = images_dest_path
|
||||
self.cloud_settings = cloud_settings
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.update.emit(0)
|
||||
provider = get_provider(self.cloud_settings)
|
||||
zip_file = provider.download_file(self.project_name)
|
||||
zip_file = zipfile.ZipFile(zip_file, mode='r')
|
||||
zip_file.extractall(self.project_dest_path)
|
||||
zip_file.close()
|
||||
project_name = zip_file.namelist()[0].strip('/')
|
||||
|
||||
self.update.emit(20)
|
||||
|
||||
with open(os.path.join(self.project_dest_path, project_name, project_name + '.gns3'), 'r') as f:
|
||||
project_settings = json.loads(f.read())
|
||||
|
||||
images = set()
|
||||
for node in project_settings["topology"].get("nodes", []):
|
||||
if "properties" in node and "image" in node["properties"]:
|
||||
images.add(node["properties"]["image"])
|
||||
|
||||
image_names_in_cloud = provider.find_storage_image_names(images)
|
||||
|
||||
for i, image in enumerate(images):
|
||||
dest_path = os.path.join(self.images_dest_path, *image_names_in_cloud[image].split('/')[1:])
|
||||
|
||||
if not os.path.exists(os.path.dirname(dest_path)):
|
||||
os.makedirs(os.path.dirname(dest_path))
|
||||
|
||||
provider.download_file(image_names_in_cloud[image], dest_path)
|
||||
self.update.emit(20 + (float(i) / len(images) * 80))
|
||||
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error importing project from cloud")
|
||||
self.error.emit("Error importing project: {}".format(e), True)
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class DownloadImagesThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Downloads multiple files from cloud files
|
||||
"""
|
||||
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, cloud_settings, images_dest_path, image_names):
|
||||
super().__init__()
|
||||
self._cloud_settings = cloud_settings
|
||||
self._images_dest_path = images_dest_path
|
||||
self._image_names = image_names
|
||||
|
||||
def run(self):
|
||||
self.update.emit(0)
|
||||
try:
|
||||
provider = get_provider(self._cloud_settings)
|
||||
image_names_in_cloud = provider.find_storage_image_names(self._image_names)
|
||||
|
||||
for i, image in enumerate(self._image_names):
|
||||
dest_path = os.path.join(self._images_dest_path, *image_names_in_cloud[image].split('/')[1:])
|
||||
|
||||
if not os.path.exists(os.path.dirname(dest_path)):
|
||||
os.makedirs(os.path.dirname(dest_path))
|
||||
|
||||
provider.download_file(image_names_in_cloud[image], dest_path)
|
||||
|
||||
self.update.emit(i * 100 / len(self._image_names))
|
||||
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error importing project from cloud")
|
||||
self.error.emit("Error importing project: {}".format(e), True)
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class DeleteProjectThread(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Deletes project from cloud storage
|
||||
"""
|
||||
|
||||
# signals to update the progress dialog.
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent, project_file_name, cloud_settings):
|
||||
super().__init__(parent)
|
||||
self.project_file_name = project_file_name
|
||||
self.cloud_settings = cloud_settings
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
provider = get_provider(self.cloud_settings)
|
||||
provider.delete_file(self.project_file_name)
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error deleting project")
|
||||
self.error.emit("Error deleting project: {}".format(e), True)
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_cloud_projects(cloud_settings):
|
||||
provider = get_provider(cloud_settings)
|
||||
return provider.list_projects()
|
||||
@@ -1,252 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
from PyQt4.QtCore import QThread
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from .cloud.utils import ssh_client
|
||||
from .cloud.exceptions import KeyPairExists
|
||||
|
||||
from .servers import Servers
|
||||
from .topology import Topology
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudBuilder(QThread):
|
||||
|
||||
"""
|
||||
"""
|
||||
# Notify with progress amount and instance_id
|
||||
progressUpdate = pyqtSignal(object, str)
|
||||
|
||||
# Notify with current state and instance_id
|
||||
stateChange = pyqtSignal(object, str)
|
||||
|
||||
# Notify when instance is ready with instance_id
|
||||
buildComplete = pyqtSignal(str)
|
||||
|
||||
# Notify when the instance has been created with instance and keypair
|
||||
instanceCreated = pyqtSignal(object, object)
|
||||
|
||||
# Notify when the public ip is available with ip and instance_id
|
||||
instanceHasIP = pyqtSignal(str, str)
|
||||
|
||||
# Notify when instance id exists with builder and instance_id
|
||||
instanceIdExists = pyqtSignal(object, str)
|
||||
|
||||
def __init__(self, parent, cloud_provider, ca_dir):
|
||||
super(QThread, self).__init__(parent)
|
||||
# Store our parent so it can be passed to threads we spawn.
|
||||
self._parent = parent
|
||||
self._provider = cloud_provider
|
||||
self._ca_dir = ca_dir
|
||||
self._start_at_create = False
|
||||
self._start_at_setup = False
|
||||
self._instance = None
|
||||
|
||||
def startAtCreate(self, instance_name, flavor_id, image_id):
|
||||
self._start_at_create = True
|
||||
self._instance_name = instance_name
|
||||
self._flavor_id = flavor_id
|
||||
self._image_id = image_id
|
||||
|
||||
def startAtSetup(self, instance, keypair):
|
||||
self._start_at_setup = True
|
||||
self._instance = instance
|
||||
self._key_pair = keypair
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
log.debug('CloudBuilder.run')
|
||||
if self._start_at_create:
|
||||
log.debug('CloudBuilder._start_at_create')
|
||||
self._createInstance(self._provider, self._instance_name, self._flavor_id,
|
||||
self._image_id)
|
||||
log.debug('got here 3')
|
||||
if self._start_at_setup:
|
||||
log.debug('CloudBuilder start at setup')
|
||||
self._instanceCreated(self._instance, self._key_pair)
|
||||
except Exception:
|
||||
log.exception("CloudBuilder trapped an exception:")
|
||||
log.error('CloudBuilder stopped in error state.')
|
||||
|
||||
def _createInstance(self, provider, name, flavor_id, image_id):
|
||||
log.debug("Creating cloud keypair with name {}".format(name))
|
||||
key_pair = None
|
||||
while key_pair is None:
|
||||
try:
|
||||
key_pair = provider.create_key_pair(name)
|
||||
except KeyPairExists:
|
||||
log.debug("Deleting old key pair with name {}.".format(name))
|
||||
self._provider.delete_key_pair_by_name(name)
|
||||
except Exception as e:
|
||||
log.debug("create_key_pair exception {}".format(e))
|
||||
|
||||
log.debug("Creating cloud server with name {}".format(name))
|
||||
instance = None
|
||||
while instance is None:
|
||||
try:
|
||||
instance = self._provider.create_instance(name, flavor_id, image_id, key_pair)
|
||||
except Exception as e:
|
||||
log.debug("create_instance exception {}".format(e))
|
||||
log.debug("Cloud server {} created".format(name))
|
||||
self._instanceCreated(instance, key_pair)
|
||||
|
||||
def _instanceCreated(self, instance, key_pair):
|
||||
log.debug('CloudBuilder._instanceCreated {}'.format(instance.id))
|
||||
self._instance = instance
|
||||
self._instance_id = instance.id
|
||||
self._key_pair = key_pair
|
||||
self.instanceIdExists.emit(self, instance.id)
|
||||
self.instanceCreated.emit(instance, key_pair)
|
||||
self._waitForPublicIP()
|
||||
|
||||
def _waitForPublicIP(self):
|
||||
public_ip = None
|
||||
while public_ip is None:
|
||||
time.sleep(10)
|
||||
try:
|
||||
instance = self._provider.get_instance(self._instance)
|
||||
# Look for public ip address
|
||||
for ip in instance.public_ips:
|
||||
# Don't use the ipv6 address
|
||||
if ':' not in ip:
|
||||
public_ip = ip
|
||||
break
|
||||
except Exception as e:
|
||||
log.debug('list_instances error: {}'.format(e))
|
||||
|
||||
# updated info, keep it.
|
||||
self._instance = instance
|
||||
self._public_ip = public_ip
|
||||
self.instanceHasIP.emit(self._public_ip, self._instance.id)
|
||||
time.sleep(60)
|
||||
self._startGNS3Server(1800)
|
||||
|
||||
def _startGNS3Server(self, dead_time):
|
||||
commands = '''
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --configure -a
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
|
||||
ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
|
||||
mkdir -p /opt/gns3
|
||||
cd /opt/gns3; git clone https://github.com/planctechnologies/gns3-server.git
|
||||
cd /opt/gns3/gns3-server; git checkout dev; git pull
|
||||
cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
|
||||
cd /opt/gns3/gns3-server; python3 ./setup.py install
|
||||
ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
|
||||
wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap.tar.gz'
|
||||
tar xzf iouyap.tar.gz -C /usr/local/bin
|
||||
python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
|
||||
hostname gns3-iouvm # set hostname for iou
|
||||
wget 'http://downloads.sourceforge.net/project/vpcs/0.6/vpcs_0.6_Linux64'
|
||||
cp vpcs_0.6_Linux64 /usr/local/bin/vpcs
|
||||
chmod a+x /usr/local/bin/vpcs
|
||||
killall python3 gns3server gns3dms
|
||||
'''
|
||||
|
||||
def exec_command(client, cmd, wait_time=-1):
|
||||
|
||||
cmd += '; exit $?'
|
||||
|
||||
stdout_data = b''
|
||||
stderr_data = b''
|
||||
|
||||
log.debug('cmd: {}'.format(cmd))
|
||||
# Send the command (non-blocking)
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
|
||||
# Wait for the command to terminate
|
||||
wait = int(wait_time)
|
||||
while not stdout.channel.exit_status_ready() and wait != 0:
|
||||
time.sleep(1)
|
||||
wait -= 1
|
||||
|
||||
stdout_data = stdout.read()
|
||||
stderr_data = stderr.read()
|
||||
log.debug('exit status: {}'.format(stdout.channel.exit_status))
|
||||
log.debug('stdout: {}'.format(stdout_data.decode('utf-8')))
|
||||
log.debug('stderr: {}'.format(stderr_data.decode('utf-8')))
|
||||
return stdout_data, stderr_data
|
||||
|
||||
# We might be attempting a connection before the instance is fully booted, so retry
|
||||
# when the ssh connection fails.
|
||||
ssh_connected = False
|
||||
response = None
|
||||
while not ssh_connected:
|
||||
with ssh_client(self._public_ip, self._key_pair.private_key) as client:
|
||||
if client is None:
|
||||
time.sleep(1)
|
||||
continue
|
||||
ssh_connected = True
|
||||
|
||||
for cmd in [l for l in commands.splitlines() if l.strip()]:
|
||||
exec_command(client, cmd)
|
||||
|
||||
data = {
|
||||
'instance_id': self._instance_id,
|
||||
'cloud_user_name': self._provider.username,
|
||||
'cloud_api_key': self._provider.api_key,
|
||||
'cloud_region': self._provider.region,
|
||||
'dead_time': dead_time,
|
||||
}
|
||||
# TODO: Properly escape the data portion of the command line
|
||||
start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --ip={} --data="{}" 2>/tmp/gns3-stderr.log'.format(self._public_ip, data)
|
||||
stdout, stderr = exec_command(client, start_cmd, wait_time=15)
|
||||
response = stdout.decode('utf-8')
|
||||
|
||||
log.debug(response)
|
||||
data = ast.literal_eval(response)
|
||||
# TODO: have the server return the port it is running on
|
||||
port = 8000
|
||||
|
||||
username = data['WEB_USERNAME']
|
||||
password = data['WEB_PASSWORD']
|
||||
|
||||
ssl_cert = ''.join(data['SSL_CRT'])
|
||||
ca_filename = 'cloud_server_{}.crt'.format(self._public_ip)
|
||||
ca_dir = self._ca_dir
|
||||
ca_file = os.path.join(ca_dir, ca_filename)
|
||||
try:
|
||||
os.makedirs(ca_dir)
|
||||
except FileExistsError:
|
||||
pass
|
||||
with open(ca_file, 'wb') as ca_fh:
|
||||
ca_fh.write(ssl_cert.encode('utf-8'))
|
||||
|
||||
topology = Topology.instance()
|
||||
top_instance = topology.getInstance(self._instance_id)
|
||||
top_instance.set_later_attributes(self._public_ip, port, ssl_cert, ca_file)
|
||||
|
||||
servers = Servers.instance()
|
||||
server = servers.getCloudServer(self._public_ip, port, ca_file, username, password,
|
||||
self._key_pair.private_key, self._instance_id)
|
||||
servers.save()
|
||||
log.debug('Cloud server gns3server started.')
|
||||
self.buildComplete.emit(self._instance_id)
|
||||
@@ -1,427 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
|
||||
from libcloud.compute.types import NodeState
|
||||
|
||||
from .qt import QtCore, QtGui
|
||||
from .cloud.utils import (ListInstancesThread, DeleteInstanceThread)
|
||||
from .topology import Topology
|
||||
from .servers import Servers
|
||||
|
||||
|
||||
# this widget was promoted on Creator, must use absolute imports
|
||||
from gns3.ui.cloud_inspector_view_ui import Ui_CloudInspectorView
|
||||
from gns3.cloud_builder import CloudBuilder
|
||||
from gns3.cloud_instances import CloudInstances
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
POLLING_TIMER = 10000 # in milliseconds
|
||||
|
||||
|
||||
class RunningInstanceState(NodeState):
|
||||
|
||||
"""
|
||||
GNS3 states for running instances
|
||||
"""
|
||||
GNS3SERVER_STARTING = -1
|
||||
GNS3SERVER_STARTED = -2
|
||||
WS_CONNECTED = -3
|
||||
|
||||
|
||||
class InstanceTableModel(QtCore.QAbstractTableModel):
|
||||
|
||||
"""
|
||||
A custom table model storing data of cloud instances
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InstanceTableModel, self).__init__(*args, **kwargs)
|
||||
self._header_data = ['Instance', '', 'Size', 'Devices'] # status has an empty header label
|
||||
self._width = len(self._header_data)
|
||||
self._instances = {}
|
||||
self._ids = []
|
||||
self.flavors = {}
|
||||
|
||||
@property
|
||||
def instanceIds(self):
|
||||
return self._ids
|
||||
|
||||
def clear(self):
|
||||
self._instances = {}
|
||||
self._ids = []
|
||||
self.reset()
|
||||
|
||||
def _get_status_icon_path(self, instance):
|
||||
"""
|
||||
Return a string pointing to the graphic resource
|
||||
"""
|
||||
if instance.state == RunningInstanceState.WS_CONNECTED:
|
||||
return ':/icons/led_green.svg'
|
||||
elif instance.state in (RunningInstanceState.STOPPED,
|
||||
RunningInstanceState.TERMINATED,
|
||||
RunningInstanceState.UNKNOWN):
|
||||
return ':/icons/led_red.svg'
|
||||
else:
|
||||
return ':/icons/led_yellow.svg'
|
||||
|
||||
def rowCount(self, QModelIndex_parent=None, *args, **kwargs):
|
||||
return len(self._instances)
|
||||
|
||||
def columnCount(self, QModelIndex_parent=None, *args, **kwargs):
|
||||
return self._width if len(self._instances) else 0
|
||||
|
||||
def data(self, index, role=None):
|
||||
instance = self._instances.get(self._ids[index.row()])
|
||||
col = index.column()
|
||||
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
if col == 1:
|
||||
# status
|
||||
return QtGui.QIcon(self._get_status_icon_path(instance))
|
||||
|
||||
elif role == QtCore.Qt.DisplayRole:
|
||||
if col == 0:
|
||||
# name
|
||||
return instance.name
|
||||
elif col == 2:
|
||||
# size
|
||||
try:
|
||||
# for Rackspace instances, update flavor id with a verbose description
|
||||
return self.flavors.get(instance.extra['flavorId'])
|
||||
except KeyError:
|
||||
# fallback to libcloud size property
|
||||
if instance.size:
|
||||
return instance.size.ram
|
||||
# giveup on showing size
|
||||
return 'Unknown'
|
||||
elif col == 3:
|
||||
# devices
|
||||
count = 0
|
||||
topology = Topology.instance()
|
||||
for node in topology.nodes():
|
||||
id = node._server.instance_id or 0
|
||||
if instance.id == id:
|
||||
count += 1
|
||||
return count
|
||||
return None
|
||||
|
||||
def headerData(self, section, orientation, role=None):
|
||||
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
|
||||
try:
|
||||
return self._header_data[section]
|
||||
except IndexError:
|
||||
return None
|
||||
return super(InstanceTableModel, self).headerData(section, orientation, role)
|
||||
|
||||
def addInstance(self, instance):
|
||||
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
|
||||
if not len(self._instances):
|
||||
self.beginInsertColumns(QtCore.QModelIndex(), 0, self._width - 1)
|
||||
self.endInsertColumns()
|
||||
self._ids.append(instance.id)
|
||||
self._instances[instance.id] = instance
|
||||
self.endInsertRows()
|
||||
|
||||
def getInstance(self, index):
|
||||
"""
|
||||
Retrieve the i-th instance if index is in range
|
||||
"""
|
||||
try:
|
||||
return self._instances.get(self._ids[index])
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def removeInstance(self, instance):
|
||||
self.removeInstanceById(instance.id)
|
||||
|
||||
def removeInstanceById(self, instance_id):
|
||||
try:
|
||||
index = self._ids.index(instance_id)
|
||||
self.beginRemoveRows(QtCore.QModelIndex(), index, index)
|
||||
del self._instances[instance_id]
|
||||
del self._ids[index]
|
||||
self.endRemoveRows()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def updateInstanceFields(self, instance, field_names):
|
||||
"""
|
||||
Update model data and notify connected views
|
||||
"""
|
||||
if instance.id in self._ids:
|
||||
index = self._ids.index(instance.id)
|
||||
current = self._instances[instance.id]
|
||||
for field in field_names:
|
||||
setattr(current, field, getattr(instance, field))
|
||||
first_index = self.createIndex(index, 0)
|
||||
last_index = self.createIndex(index, self.columnCount() - 1)
|
||||
self.dataChanged.emit(first_index, last_index)
|
||||
else:
|
||||
self.addInstance(instance)
|
||||
|
||||
def getInstanceById(self, instance_id):
|
||||
return self._instances.get(instance_id, None)
|
||||
|
||||
|
||||
class CloudInspectorView(QtGui.QWidget, Ui_CloudInspectorView):
|
||||
|
||||
"""
|
||||
Table view showing data coming from InstanceTableModel
|
||||
|
||||
Signals:
|
||||
instanceSelected(int) Emitted when users click and select an instance on the inspector.
|
||||
Param int is the ID of the instance
|
||||
"""
|
||||
instanceSelected = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent):
|
||||
super(QtGui.QWidget, self).__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._provider = None
|
||||
self._settings = None
|
||||
self._project_instances_id = []
|
||||
self._main_window = None
|
||||
|
||||
self._model = InstanceTableModel() # shortcut for self.uiInstancesTableView.model()
|
||||
self.uiInstancesTableView.setModel(self._model)
|
||||
self.uiInstancesTableView.verticalHeader().hide()
|
||||
self.uiInstancesTableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.uiInstancesTableView.horizontalHeader().setStretchLastSection(True)
|
||||
# connections
|
||||
self.uiInstancesTableView.customContextMenuRequested.connect(self._contextMenu)
|
||||
self.uiInstancesTableView.clicked.connect(self._rowChanged)
|
||||
self.uiCreateInstanceButton.clicked.connect(self._create_new_instance)
|
||||
|
||||
self._pollingTimer = QtCore.QTimer(self)
|
||||
self._pollingTimer.timeout.connect(self._polling_slot)
|
||||
|
||||
# map flavor ids to combobox indexes
|
||||
self.flavor_index_id = []
|
||||
|
||||
# A dictionary of {image_id, CloudBuilder}
|
||||
self._builders = {}
|
||||
|
||||
def _get_flavor_index(self, flavor_id):
|
||||
try:
|
||||
return self.flavor_index_id.index(flavor_id)
|
||||
except ValueError:
|
||||
return -1
|
||||
|
||||
def load(self, main_win, instance_ids):
|
||||
"""
|
||||
Fill the model data layer with instance info loaded from the topology file
|
||||
"""
|
||||
self._main_window = main_win
|
||||
self._provider = main_win.cloudProvider
|
||||
self._settings = main_win.cloudSettings()
|
||||
log.info('CloudInspectorView.load')
|
||||
|
||||
for instance_id in instance_ids:
|
||||
self._project_instances_id.append(instance_id)
|
||||
|
||||
update_thread = ListInstancesThread(self, self._provider)
|
||||
update_thread.instancesReady.connect(self._update_model)
|
||||
update_thread.start()
|
||||
self._pollingTimer.start(POLLING_TIMER)
|
||||
# fill sizes comboboxes
|
||||
for id, name in self._provider.list_flavors().items():
|
||||
self.uiCreateInstanceComboBox.addItem(name)
|
||||
self.flavor_index_id.append(id)
|
||||
# select default flavor
|
||||
new_instance_flavor = self._settings["new_instance_flavor"]
|
||||
self.uiCreateInstanceComboBox.setCurrentIndex(self._get_flavor_index(new_instance_flavor))
|
||||
|
||||
def addInstance(self, instance):
|
||||
"""
|
||||
Add a new instance to the inspector
|
||||
"""
|
||||
self._project_instances_id.append(instance.id)
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear contents and stop polling timer
|
||||
"""
|
||||
self._model.clear()
|
||||
self._pollingTimer.stop()
|
||||
self._project_instances_id = []
|
||||
|
||||
def _contextMenu(self, pos):
|
||||
# create actions
|
||||
delete_action = QtGui.QAction("Delete", self)
|
||||
delete_action.triggered.connect(self._deleteSelectedInstance)
|
||||
# create context menu and add actions
|
||||
menu = QtGui.QMenu(self.uiInstancesTableView)
|
||||
menu.addAction(delete_action)
|
||||
# show the menu
|
||||
menu.popup(self.uiInstancesTableView.viewport().mapToGlobal(pos))
|
||||
|
||||
def _deleteSelectedInstance(self):
|
||||
"""
|
||||
Delete the instance corresponding to the selected table row
|
||||
"""
|
||||
sel = self.uiInstancesTableView.selectedIndexes()
|
||||
if len(sel) and self._provider is not None:
|
||||
index = sel[0].row()
|
||||
instance = self._model.getInstance(index)
|
||||
|
||||
# warn user this is destructive
|
||||
msg = "Do you want to remove the instance and any devices running on it?"
|
||||
proceed = QtGui.QMessageBox.question(self, 'Warning', msg,
|
||||
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
|
||||
|
||||
if proceed == QtGui.QMessageBox.Yes:
|
||||
# disconnect and remove the server
|
||||
servers = Servers.instance()
|
||||
cs = servers.cloudServerById(instance.id)
|
||||
if cs is not None:
|
||||
servers.removeCloudServer(cs)
|
||||
|
||||
# remove instance from the the topology
|
||||
topology = Topology.instance()
|
||||
topology.removeInstance(instance.id)
|
||||
|
||||
delete_thread = DeleteInstanceThread(self, self._provider, instance)
|
||||
delete_thread.instanceDeleted.connect(self._main_window.remove_instance_from_project)
|
||||
delete_thread.start()
|
||||
|
||||
instance.name = 'Deleting...'
|
||||
self._model.updateInstanceFields(instance, ['name'])
|
||||
|
||||
def _rowChanged(self, index):
|
||||
"""
|
||||
This slot is invoked every time users change the current selected row on the
|
||||
inspector
|
||||
"""
|
||||
selection = self.uiInstancesTableView.selectionModel().selection()
|
||||
if selection.isEmpty():
|
||||
return
|
||||
|
||||
item = selection.indexes()[0]
|
||||
if item.isValid():
|
||||
instance = self._model.getInstance(item.row())
|
||||
self.instanceSelected.emit(instance.id)
|
||||
|
||||
def _polling_slot(self):
|
||||
"""
|
||||
Sync model data with instances status
|
||||
"""
|
||||
if self._provider is None:
|
||||
return
|
||||
|
||||
update_thread = ListInstancesThread(self, self._provider)
|
||||
update_thread.instancesReady.connect(self._update_model)
|
||||
update_thread.start()
|
||||
|
||||
def _instanceBuilt(self, id):
|
||||
"""
|
||||
This slot is called when instance has finished building.
|
||||
"""
|
||||
instance = self._model.getInstanceById(id)
|
||||
instance.state = RunningInstanceState.WS_CONNECTED
|
||||
self._model.updateInstanceFields(instance, ['state'])
|
||||
|
||||
if self._main_window.loading_cloud_project:
|
||||
project = self._main_window.project()
|
||||
path = project.topologyPath()
|
||||
with open(path, "r") as f:
|
||||
json_topology = json.load(f)
|
||||
topology = Topology.instance()
|
||||
topology.load(json_topology)
|
||||
self._main_window.loading_cloud_project = False
|
||||
|
||||
def _update_model(self, instances):
|
||||
if not instances:
|
||||
return
|
||||
|
||||
# Filter instances to only those in the current project
|
||||
project_instances = [i for i in instances if i.id in self._project_instances_id]
|
||||
|
||||
# populate underlying model if this is the first call
|
||||
if self._model.rowCount() == 0 and len(project_instances) > 0:
|
||||
self._populate_model(project_instances)
|
||||
self._rebuild_instances(project_instances)
|
||||
|
||||
instance_manager = CloudInstances.instance()
|
||||
instance_manager.update_instances(instances)
|
||||
|
||||
# Clean up removed instances
|
||||
real = set(i.id for i in project_instances)
|
||||
current = set(self._model.instanceIds)
|
||||
for i in current.difference(real):
|
||||
self._model.removeInstanceById(i)
|
||||
self.uiInstancesTableView.resizeColumnsToContents()
|
||||
|
||||
# Update instance status
|
||||
for i in project_instances:
|
||||
# get the customized instance state from self._model
|
||||
model_instance = self._model.getInstanceById(i.id)
|
||||
|
||||
# update model instance state if needed
|
||||
if i.state != RunningInstanceState.RUNNING:
|
||||
self._model.updateInstanceFields(i, ['state'])
|
||||
|
||||
def _populate_model(self, instances):
|
||||
log.info('CloudInspectorView._populate_model')
|
||||
self._model.flavors = self._provider.list_flavors()
|
||||
# filter instances for current project
|
||||
for inst in instances:
|
||||
self._model.addInstance(inst)
|
||||
self.uiInstancesTableView.resizeColumnsToContents()
|
||||
|
||||
def _create_new_instance(self):
|
||||
idx = self.uiCreateInstanceComboBox.currentIndex()
|
||||
flavor_id = self.flavor_index_id[idx]
|
||||
image_id = self._settings['default_image']
|
||||
|
||||
name, ok = QtGui.QInputDialog.getText(self,
|
||||
"New instance",
|
||||
"Choose a name for the instance and press Ok,\n"
|
||||
"then wait for the instance to appear in the inspector.")
|
||||
|
||||
if ok:
|
||||
self.createInstance(name, flavor_id, image_id)
|
||||
|
||||
def createInstance(self, instance_name, flavor_id, image_id):
|
||||
if not instance_name.endswith("-gns3"):
|
||||
instance_name += "-gns3"
|
||||
# TODO: Add a keys_dir to projectSettings
|
||||
ca_dir = os.path.join(self._main_window.projectSettings()["project_files_dir"], "keys")
|
||||
|
||||
builder = CloudBuilder(self, self._provider, ca_dir)
|
||||
builder.startAtCreate(instance_name, flavor_id, image_id)
|
||||
builder.instanceCreated.connect(self._main_window.add_instance_to_project)
|
||||
builder.instanceCreated.connect(CloudInstances.instance().add_instance)
|
||||
builder.instanceIdExists.connect(self._associateBuilderWithInstance)
|
||||
builder.instanceHasIP.connect(CloudInstances.instance().update_host_for_instance)
|
||||
builder.buildComplete.connect(self._instanceBuilt)
|
||||
builder.start()
|
||||
return builder
|
||||
|
||||
def _associateBuilderWithInstance(self, builder, instance_id):
|
||||
self._builders[instance_id] = builder
|
||||
|
||||
def _rebuild_instances(self, instances):
|
||||
# TODO: Add a keys_dir to projectSettings
|
||||
ca_dir = os.path.join(self._main_window.projectSettings()["project_files_dir"], "keys")
|
||||
|
||||
for instance in instances:
|
||||
log.debug('CloudInspectorView._rebuild_instances {}'.format(instance.name))
|
||||
builder = CloudBuilder(self, self._provider, ca_dir)
|
||||
cloud_instance = CloudInstances.instance().get_instance(instance.id)
|
||||
public_key = cloud_instance.public_key
|
||||
private_key = cloud_instance.private_key
|
||||
# Fake a KeyPair object because we don't store it.
|
||||
keypair = namedtuple('KeyPair', ['private_key', 'public_key'])(private_key, public_key)
|
||||
builder.startAtSetup(instance, keypair)
|
||||
builder.instanceCreated.connect(self._main_window.add_instance_to_project)
|
||||
builder.instanceCreated.connect(CloudInstances.instance().add_instance)
|
||||
builder.instanceIdExists.connect(self._associateBuilderWithInstance)
|
||||
builder.instanceHasIP.connect(CloudInstances.instance().update_host_for_instance)
|
||||
builder.buildComplete.connect(self._instanceBuilt)
|
||||
builder.start()
|
||||
return builder
|
||||
@@ -1,154 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Keeps track of all cloud instances the app has started.
|
||||
"""
|
||||
|
||||
from .qt import QtCore
|
||||
from gns3.topology import TopologyInstance
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudInstances(QtCore.QObject):
|
||||
|
||||
"""
|
||||
This class stores the instances that gns3 gui has started. This can be different than the list
|
||||
of instances in the topology that can be changed when switching projects. This list is not touched
|
||||
when switching projects and is stored in the .ini file.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CloudInstances, self).__init__(*args, **kwargs)
|
||||
self._instances = []
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only one instance of CloudInstances.
|
||||
|
||||
:returns: instance of CloudInstances
|
||||
"""
|
||||
|
||||
if not hasattr(CloudInstances, "_instance"):
|
||||
CloudInstances._instance = CloudInstances()
|
||||
return CloudInstances._instance
|
||||
|
||||
@property
|
||||
def instances(self):
|
||||
return self._instances
|
||||
|
||||
def clear(self):
|
||||
self._instances.clear()
|
||||
|
||||
def add(self, topology_instance):
|
||||
self._instances.append(topology_instance)
|
||||
|
||||
def add_instance(self, instance, keypair):
|
||||
if instance is None:
|
||||
return
|
||||
existing = self.get_instance(instance.id)
|
||||
if existing is None:
|
||||
ti = TopologyInstance(instance.name, instance.id, instance.extra['flavorId'],
|
||||
instance.extra['imageId'], keypair.private_key, keypair.public_key)
|
||||
self._instances.append(ti)
|
||||
self.save()
|
||||
|
||||
def update_instances(self, instances):
|
||||
"""
|
||||
Compare with the existing list of instances to purge instances that no
|
||||
longer exist.
|
||||
"""
|
||||
save_needed = False
|
||||
# Look for instances that have been deleted
|
||||
for stored in self._instances:
|
||||
found = False
|
||||
for dynamic in instances:
|
||||
if stored.id == dynamic.id:
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
self._instances.remove(stored)
|
||||
save_needed = True
|
||||
|
||||
if save_needed:
|
||||
self.save()
|
||||
|
||||
def update_host_for_instance(self, host, instance_id):
|
||||
"""
|
||||
Update the public IP for the instance.
|
||||
"""
|
||||
for instance in self.instances:
|
||||
if instance.id == instance_id:
|
||||
if instance.host != host:
|
||||
instance.host = host
|
||||
self.save()
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save the list of cloud instances to the config file
|
||||
"""
|
||||
log.debug('Saving cloud instances')
|
||||
settings = QtCore.QSettings()
|
||||
settings.beginGroup("CloudInstances")
|
||||
settings.remove("")
|
||||
|
||||
# Save the instances
|
||||
settings.beginWriteArray("cloud_instance", len(self._instances))
|
||||
index = 0
|
||||
for instance in self._instances:
|
||||
settings.setArrayIndex(index)
|
||||
for name in instance.fields():
|
||||
value = getattr(instance, name) if not None else ""
|
||||
log.debug('{}={}'.format(name, str(value)[0:60]))
|
||||
settings.setValue(name, value)
|
||||
index += 1
|
||||
settings.endArray()
|
||||
settings.endGroup()
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Load instance info from the config file to the topology
|
||||
"""
|
||||
log.debug('Loading cloud instances')
|
||||
settings = QtCore.QSettings()
|
||||
settings.beginGroup("CloudInstances")
|
||||
|
||||
# Load the instances
|
||||
size = settings.beginReadArray("cloud_instance")
|
||||
for index in range(0, size):
|
||||
settings.setArrayIndex(index)
|
||||
info = {}
|
||||
for name in TopologyInstance.fields():
|
||||
value = settings.value(name, "")
|
||||
log.debug('{}={}'.format(name, str(value)[0:60]))
|
||||
info[name] = value
|
||||
ti = TopologyInstance(**info)
|
||||
self._instances.append(ti)
|
||||
|
||||
def get_instance(self, instance_id):
|
||||
"""
|
||||
Retrieve a TopologyInstance objects if present
|
||||
"""
|
||||
for i in self._instances:
|
||||
if i.id == instance_id:
|
||||
return i
|
||||
return None
|
||||
120
gns3/compute.py
Normal file
120
gns3/compute.py
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
|
||||
|
||||
class Compute:
|
||||
"""
|
||||
A compute node on the remote server
|
||||
"""
|
||||
|
||||
def __init__(self, compute_id=None):
|
||||
if compute_id is None:
|
||||
compute_id = str(uuid.uuid4())
|
||||
self._compute_id = compute_id
|
||||
self._name = compute_id
|
||||
self._connected = False
|
||||
self._protocol = "http"
|
||||
self._host = None
|
||||
self._port = 3080
|
||||
self._user = None
|
||||
self._password = None
|
||||
self._cpu_usage_percent = None
|
||||
self._memory_usage_percent = None
|
||||
self._capabilities = {
|
||||
"node_types": []
|
||||
}
|
||||
|
||||
def id(self):
|
||||
return self._compute_id
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def setName(self, name):
|
||||
self._name = name
|
||||
|
||||
def connected(self):
|
||||
return self._connected
|
||||
|
||||
def setConnected(self, value):
|
||||
self._connected = value
|
||||
|
||||
def port(self):
|
||||
return self._port
|
||||
|
||||
def setPort(self, port):
|
||||
self._port = port
|
||||
|
||||
def user(self):
|
||||
return self._user
|
||||
|
||||
def setUser(self, user):
|
||||
self._user = user
|
||||
|
||||
def setPassword(self, password):
|
||||
self._password = password
|
||||
|
||||
def protocol(self):
|
||||
return self._protocol
|
||||
|
||||
def setProtocol(self, protocol):
|
||||
self._protocol = protocol
|
||||
|
||||
def host(self):
|
||||
return self._host
|
||||
|
||||
def setHost(self, host):
|
||||
self._host = host
|
||||
|
||||
def setCpuUsagePercent(self, usage):
|
||||
self._cpu_usage_percent = usage
|
||||
|
||||
def cpuUsagePercent(self):
|
||||
return self._cpu_usage_percent
|
||||
|
||||
def setMemoryUsagePercent(self, usage):
|
||||
self._memory_usage_percent = usage
|
||||
|
||||
def memoryUsagePercent(self):
|
||||
return self._memory_usage_percent
|
||||
|
||||
def capabilities(self):
|
||||
return self._capabilities
|
||||
|
||||
def setCapabilities(self, val):
|
||||
self._capabilities = val
|
||||
|
||||
def __str__(self):
|
||||
return self._compute_id
|
||||
|
||||
def __json__(self):
|
||||
return {
|
||||
"host": self._host,
|
||||
"port": self._port,
|
||||
"protocol": self._protocol,
|
||||
"user": self._user,
|
||||
"password": self._password,
|
||||
"name": self._name,
|
||||
"compute_id": self._compute_id
|
||||
}
|
||||
|
||||
def __eq__(self, v):
|
||||
if isinstance(v, Compute):
|
||||
return self.__json__() == v.__json__()
|
||||
return False
|
||||
226
gns3/compute_manager.py
Normal file
226
gns3/compute_manager.py
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .qt import QtCore
|
||||
|
||||
from .compute import Compute
|
||||
from .controller import Controller
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import urllib
|
||||
import datetime
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComputeManager(QtCore.QObject):
|
||||
created_signal = QtCore.Signal(str)
|
||||
updated_signal = QtCore.Signal(str)
|
||||
deleted_signal = QtCore.Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._computes = {}
|
||||
self._controller = Controller.instance()
|
||||
self._controller.connected_signal.connect(self._controllerConnectedSlot)
|
||||
self._controller.disconnected_signal.connect(self._controllerDisconnectedSlot)
|
||||
self._controllerConnectedSlot()
|
||||
|
||||
# If we receive fresh data from the notification feed no need to refresh via an API call
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
|
||||
self._timer = QtCore.QTimer()
|
||||
self._timer.setInterval(1000)
|
||||
self._refreshingComputes = False
|
||||
self._timer.timeout.connect(self._refreshComputesSlot)
|
||||
self._timer.start()
|
||||
|
||||
def _refreshComputesSlot(self):
|
||||
if self._refreshingComputes:
|
||||
return
|
||||
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 1:
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
self._refreshingComputes = True
|
||||
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
|
||||
|
||||
def _controllerConnectedSlot(self):
|
||||
if self._controller.connected():
|
||||
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
|
||||
|
||||
for compute in result:
|
||||
self.computeDataReceivedCallback(compute)
|
||||
|
||||
def computeDataReceivedCallback(self, compute):
|
||||
"""
|
||||
Called when we received data from a compute
|
||||
node.
|
||||
"""
|
||||
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
|
||||
new_node = False
|
||||
compute_id = compute["compute_id"]
|
||||
if compute_id not in self._computes:
|
||||
new_node = True
|
||||
self._computes[compute_id] = Compute(compute_id)
|
||||
|
||||
self._computes[compute_id].setName(compute["name"])
|
||||
self._computes[compute_id].setConnected(compute["connected"])
|
||||
self._computes[compute_id].setProtocol(compute["protocol"])
|
||||
self._computes[compute_id].setHost(compute["host"])
|
||||
self._computes[compute_id].setPort(compute["port"])
|
||||
self._computes[compute_id].setUser(compute["user"])
|
||||
self._computes[compute_id].setCpuUsagePercent(compute["cpu_usage_percent"])
|
||||
self._computes[compute_id].setMemoryUsagePercent(compute["memory_usage_percent"])
|
||||
self._computes[compute_id].setCapabilities(compute["capabilities"])
|
||||
|
||||
if new_node:
|
||||
self.created_signal.emit(compute_id)
|
||||
else:
|
||||
self.updated_signal.emit(compute_id)
|
||||
|
||||
def computeIsTheRemoteGNS3VM(self, compute):
|
||||
"""
|
||||
:returns: Boolean True if the remote server is the remote GNS3 VM
|
||||
"""
|
||||
if compute.id() != "local" and compute.id() != "vm":
|
||||
if self.vmCompute() and "GNS3 VM ({})".format(compute.name()) == self.vmCompute().name():
|
||||
return True
|
||||
return False
|
||||
|
||||
def computes(self):
|
||||
"""
|
||||
:returns: List of computes nodes
|
||||
"""
|
||||
computes = []
|
||||
for compute in self._computes.values():
|
||||
# We filter the remote GNS3 VM compute from the computes list
|
||||
if not self.computeIsTheRemoteGNS3VM(compute):
|
||||
computes.append(compute)
|
||||
return computes
|
||||
|
||||
def vmCompute(self):
|
||||
"""
|
||||
:returns: The GNS3 VM compute node or None
|
||||
"""
|
||||
try:
|
||||
return self._computes["vm"]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def localCompute(self):
|
||||
"""
|
||||
:returns: The local compute node or None
|
||||
"""
|
||||
try:
|
||||
return self._computes["local"]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def localPlatform(self):
|
||||
"""
|
||||
Return the platform of the local compute.
|
||||
|
||||
With a remote controller it could be different of our local platform
|
||||
"""
|
||||
c = self.localCompute()
|
||||
if c is None:
|
||||
return sys.platform
|
||||
return c.capabilities().get("platform", sys.platform)
|
||||
|
||||
def remoteComputes(self):
|
||||
"""
|
||||
:returns: List of non local and non VM computes
|
||||
"""
|
||||
return [c for c in self._computes.values() if c.id() != "local" and c.id() != "vm"]
|
||||
|
||||
def getCompute(self, compute_id):
|
||||
if compute_id.startswith("http:") or compute_id.startswith("https:"):
|
||||
u = urllib.parse.urlsplit(compute_id)
|
||||
for compute in self._computes.values():
|
||||
if "{}:{}".format(compute.host(), compute.port()) == u.netloc:
|
||||
return compute
|
||||
raise KeyError("Compute {} is missing.".format(compute_id))
|
||||
if compute_id not in self._computes:
|
||||
self._computes[compute_id] = Compute(compute_id)
|
||||
self.created_signal.emit(compute_id)
|
||||
return self._computes[compute_id]
|
||||
|
||||
def deleteCompute(self, compute_id):
|
||||
if compute_id in self._computes:
|
||||
compute = self._computes[compute_id]
|
||||
del self._computes[compute_id]
|
||||
self._controller.delete("/computes/" + compute_id, None)
|
||||
self.deleted_signal.emit(compute_id)
|
||||
|
||||
def updateList(self, computes):
|
||||
"""
|
||||
Sync an array of compute server with remote
|
||||
"""
|
||||
for compute_id in copy.copy(self._computes):
|
||||
# Delete compute on controller not in the new computes
|
||||
if compute_id in ["local", "vm"]:
|
||||
continue
|
||||
|
||||
if compute_id not in [c.id() for c in computes]:
|
||||
log.debug("Delete compute %s", compute_id)
|
||||
self.deleteCompute(compute_id)
|
||||
else:
|
||||
# Update the changed nodes
|
||||
for c in computes:
|
||||
if c.id() == compute_id and c != self._computes[compute_id]:
|
||||
log.debug("Update compute %s", compute_id)
|
||||
self._controller.put("/computes/" + compute_id, None, body=c.__json__())
|
||||
self._computes[compute_id] = c
|
||||
self.updated_signal.emit(compute_id)
|
||||
# Create the new nodes
|
||||
for compute in computes:
|
||||
if compute.id() not in self._computes:
|
||||
log.debug("Create compute %s", compute.id())
|
||||
self._controller.post("/computes", None, body=compute.__json__())
|
||||
self._computes[compute.id()] = compute
|
||||
self.created_signal.emit(compute.id())
|
||||
|
||||
@staticmethod
|
||||
def reset():
|
||||
ComputeManager._instance = None
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of ComputeManager.
|
||||
:returns: instance of ComputeManager
|
||||
"""
|
||||
|
||||
if not hasattr(ComputeManager, '_instance') or ComputeManager._instance is None:
|
||||
ComputeManager._instance = ComputeManager()
|
||||
return ComputeManager._instance
|
||||
155
gns3/compute_summary_view.py
Normal file
155
gns3/compute_summary_view.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Compute summary view that list all the compute, their status.
|
||||
"""
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
class ComputeItem(QtWidgets.QTreeWidgetItem):
|
||||
|
||||
"""
|
||||
Custom item for the QTreeWidget instance
|
||||
(topology summary view).
|
||||
|
||||
:param parent: parent widget
|
||||
:param compute: Compute instance
|
||||
"""
|
||||
|
||||
def __init__(self, parent, compute):
|
||||
|
||||
super().__init__(parent)
|
||||
self._compute = compute
|
||||
self._parent = parent
|
||||
self._status = "unknown"
|
||||
|
||||
self._refreshStatusSlot()
|
||||
|
||||
def _refreshStatusSlot(self):
|
||||
"""
|
||||
Changes the icon to show the node status (started, stopped etc.)
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
|
||||
usage = None
|
||||
text = self._compute.name()
|
||||
|
||||
if self._compute.cpuUsagePercent() is not None:
|
||||
text = "{} CPU {}%, RAM {}%".format(text, self._compute.cpuUsagePercent(), self._compute.memoryUsagePercent())
|
||||
|
||||
self.setText(0, text)
|
||||
self.setToolTip(0, text + " on " + self._compute.capabilities().get("platform", ""))
|
||||
|
||||
if self._compute.connected():
|
||||
self._status = "connected"
|
||||
if usage is None or (self._compute.cpuUsagePercent() < 90 and self._compute.memoryUsagePercent() < 90):
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_green.svg'))
|
||||
else:
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_yellow.svg'))
|
||||
else:
|
||||
if self._status == "unknown":
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_gray.svg'))
|
||||
else:
|
||||
self._status = "stopped"
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
|
||||
self._parent.sortItems(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
# 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):
|
||||
|
||||
"""
|
||||
Compute summary view implementation.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self._computes = {}
|
||||
|
||||
ComputeManager.instance().created_signal.connect(self._computeAddedSlot)
|
||||
ComputeManager.instance().updated_signal.connect(self._computeUpdatedSlot)
|
||||
ComputeManager.instance().deleted_signal.connect(self._computeRemovedSlot)
|
||||
for compute in ComputeManager.instance().computes():
|
||||
self._computeAddedSlot(compute.id())
|
||||
|
||||
def _computeAddedSlot(self, compute_id):
|
||||
"""
|
||||
Called when a compute is added to the list of computes
|
||||
|
||||
:params url: URL of the compute
|
||||
"""
|
||||
|
||||
compute = ComputeManager.instance().getCompute(compute_id)
|
||||
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
|
||||
return
|
||||
self._computes[compute_id] = ComputeItem(self, compute)
|
||||
|
||||
def _computeUpdatedSlot(self, compute_id):
|
||||
"""
|
||||
Called when a compute is updated
|
||||
|
||||
:params url: URL of the compute
|
||||
"""
|
||||
|
||||
if compute_id in self._computes:
|
||||
compute = ComputeManager.instance().getCompute(compute_id)
|
||||
# We hide the remote GNS3 VM
|
||||
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
|
||||
self._computeRemovedSlot(compute_id)
|
||||
else:
|
||||
self._computes[compute_id]._refreshStatusSlot()
|
||||
else:
|
||||
self._computeAddedSlot(compute_id)
|
||||
|
||||
def _computeRemovedSlot(self, compute_id):
|
||||
"""
|
||||
Called when a compute is removed to the list of computes
|
||||
|
||||
:params url: URL of the compute
|
||||
"""
|
||||
|
||||
if compute_id in self._computes:
|
||||
self.takeTopLevelItem(self.indexOfTopLevelItem(self._computes[compute_id]))
|
||||
del self._computes[compute_id]
|
||||
@@ -1,21 +0,0 @@
|
||||
!
|
||||
|
||||
kerberos password
|
||||
crypto RSA-key-pair %h.mydomain.com 0 1014940935
|
||||
30820155 02010030 0D06092A 864886F7 0D010101 05000482 013F3082 013B0201
|
||||
00024100 A7EA2920 73033037 689F8166 B6AEA7FF 91015466 7379FA4F D7B175C3
|
||||
8D5D1E56 89B00E73 D5553491 06D651DA 71213D18 3E4EAF44 8C5F05F1 E8C1FE47
|
||||
B07D5A1B 02030100 01024049 FE964106 6DD14199 8930ACE2 B3F4B45A 620B9F5A
|
||||
23D67A78 C26AF2D1 C8C72504 987ADD3E 2755DCC4 70AADB86 679171D7 54A9038F
|
||||
0EB080E7 8B514EB8 8A038102 2100D588 DF0A6D31 AEF5C231 5A4A3459 5D3FD973
|
||||
F1A13EA8 2C25D210 6ACD4733 39AF0221 00C94EC2 9428B371 2599E7EA 8C89E86C
|
||||
E188F689 3AFCFE7A 59B42810 E83DABBD 55022100 944FB792 D75ACDC9 96328F22
|
||||
C10F5CAC 2F4DCF83 0E30E250 F6813E9D 0B99F1B3 02204863 D126D428 0B05197E
|
||||
4362FC68 9F56CF18 D0AA6CB5 DA2B8DD4 66980D2D 47ED0221 00991914 B6CDC66E
|
||||
60AF0332 D5FB2771 B9F0317B 886E6E48 B86CDFDF 3FC1D48E CA
|
||||
quit
|
||||
305C300D 06092A86 4886F70D 01010105 00034B00 30480241 00A7EA29 20730330
|
||||
37689F81 66B6AEA7 FF910154 667379FA 4FD7B175 C38D5D1E 5689B00E 73D55534
|
||||
9106D651 DA71213D 183E4EAF 448C5F05 F1E8C1FE 47B07D5A 1B020301 0001
|
||||
quit
|
||||
end
|
||||
@@ -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,25 +21,20 @@ Handles commands typed in the GNS3 console.
|
||||
|
||||
import sys
|
||||
import cmd
|
||||
import logging
|
||||
import struct
|
||||
import sip
|
||||
import json
|
||||
from .qt import QtCore
|
||||
|
||||
from .node import Node
|
||||
from .qt import QtCore
|
||||
from .version import __version__
|
||||
try:
|
||||
from gns3converter import __version__ as gns3converter_version
|
||||
except ImportError:
|
||||
gns3converter_version = "Not installed"
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
cmd.Cmd.__init__(self)
|
||||
|
||||
def do_version(self, args):
|
||||
"""
|
||||
Show the version of GNS3 and its dependencies.
|
||||
@@ -49,7 +44,6 @@ class ConsoleCmd(cmd.Cmd):
|
||||
if hasattr(sys, "frozen"):
|
||||
compiled = "(compiled)"
|
||||
print("GNS3 version is {} {}".format(__version__, compiled))
|
||||
print("GNS3 Converter version is {}".format(gns3converter_version))
|
||||
print("Python version is {}.{}.{} ({}-bit) with {} encoding".format(sys.version_info[0],
|
||||
sys.version_info[1],
|
||||
sys.version_info[2],
|
||||
@@ -185,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.
|
||||
@@ -192,14 +204,9 @@ class ConsoleCmd(cmd.Cmd):
|
||||
:param node: Node instance
|
||||
"""
|
||||
|
||||
name = node.name()
|
||||
console_port = node.console()
|
||||
console_host = node.server().host
|
||||
try:
|
||||
from .telnet_console import telnetConsole
|
||||
telnetConsole(name, console_host, console_port)
|
||||
except (OSError, ValueError) as e:
|
||||
print("Cannot start console application: {}".format(e))
|
||||
from .telnet_console import nodeTelnetConsole
|
||||
nodeTelnetConsole(node, console_port)
|
||||
|
||||
def do_debug(self, args):
|
||||
"""
|
||||
@@ -212,16 +219,17 @@ class ConsoleCmd(cmd.Cmd):
|
||||
return
|
||||
|
||||
root = logging.getLogger()
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
|
||||
if len(args) == 1:
|
||||
level = int(args[0])
|
||||
if level == 0:
|
||||
print("Deactivating debugging")
|
||||
root.removeHandler(ch)
|
||||
for handler in root.handlers:
|
||||
if isinstance(handler, logging.StreamHandler):
|
||||
root.removeHandler(handler)
|
||||
root.setLevel(logging.INFO)
|
||||
else:
|
||||
root.addHandler(ch)
|
||||
root.addHandler(logging.StreamHandler(sys.stdout))
|
||||
if level == 1:
|
||||
print("Activating debugging")
|
||||
else:
|
||||
@@ -261,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
|
||||
node_id = None
|
||||
|
||||
# get the node ID
|
||||
for node in self._topology.nodes():
|
||||
if node.name() == node_name:
|
||||
node_id = node.id()
|
||||
break
|
||||
|
||||
if node_id is None:
|
||||
print("{}: no such device".format(node_name))
|
||||
continue
|
||||
|
||||
if "nodes" in topology["topology"]:
|
||||
for node in topology["topology"]["nodes"]:
|
||||
if node["id"] == node_id:
|
||||
print(json.dumps(node, sort_keys=True, indent=4))
|
||||
break
|
||||
|
||||
def do_show(self, args):
|
||||
"""
|
||||
Show detail information about every device in current lab:
|
||||
@@ -306,12 +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>
|
||||
"""
|
||||
|
||||
if '?' in args or args.strip() == "":
|
||||
@@ -321,8 +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)
|
||||
else:
|
||||
print(self.do_show.__doc__)
|
||||
|
||||
|
||||
@@ -15,21 +15,54 @@
|
||||
# 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
|
||||
from .version import __version__
|
||||
from .console_cmd import ConsoleCmd
|
||||
from .pycutext import PyCutExt
|
||||
from .modules import MODULES
|
||||
from .qt import QtCore
|
||||
from .local_config import LocalConfig
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleLogHandler(logging.StreamHandler):
|
||||
"""
|
||||
Display log event to the console
|
||||
"""
|
||||
|
||||
def emit(self, record):
|
||||
if sip.isdeleted(self._console_view):
|
||||
return
|
||||
|
||||
message = self.format(record)
|
||||
level_no = record.levelno
|
||||
if level_no >= logging.ERROR:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "error")
|
||||
elif level_no >= logging.WARNING:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "warning")
|
||||
elif level_no >= logging.INFO:
|
||||
# To avoid noise on console we display all event only if log level is debug
|
||||
# or if we force the display in the log record
|
||||
if "show" in record.__dict__ or logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "debug")
|
||||
elif level_no >= logging.DEBUG:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "debug")
|
||||
|
||||
|
||||
class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
|
||||
# Emit this signal to write a message on console
|
||||
write_message_signal = QtCore.Signal(str, str)
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
# Set the prompt PyCutExt
|
||||
@@ -39,12 +72,14 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
# Set introduction message
|
||||
bitness = struct.calcsize("P") * 8
|
||||
current_year = datetime.date.today().year
|
||||
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {}.\n" \
|
||||
"Copyright (c) 2006-{} GNS3 Technologies.".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, current_year)
|
||||
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {} and PyQt {}.\n" \
|
||||
"Copyright (c) 2006-{} GNS3 Technologies.\n" \
|
||||
"Use Help -> GNS3 Doctor to detect common issues." \
|
||||
"".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, Qt.PYQT_VERSION_STR, current_year)
|
||||
|
||||
# Parent class initialization
|
||||
try:
|
||||
PyCutExt.__init__(self, None, self.intro, parent=parent)
|
||||
super().__init__(None, self.intro, parent=parent)
|
||||
|
||||
# dynamically get all the available commands so we can color them
|
||||
methods = inspect.getmembers(self, predicate=inspect.ismethod)
|
||||
@@ -59,14 +94,39 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
except Exception as e:
|
||||
sys.stderr.write(e)
|
||||
|
||||
self._handleLogs()
|
||||
|
||||
if LocalConfig.instance().experimental():
|
||||
log.warning("WARNING: Experimental features enable. You can use some unfinished features and lost data.")
|
||||
|
||||
for module in MODULES:
|
||||
instance = module.instance()
|
||||
instance.notification_signal.connect(self.writeNotification)
|
||||
|
||||
self.write_message_signal.connect(self._writeMessageSlot)
|
||||
|
||||
# required for Cmd module (do_help etc.)
|
||||
self.stdout = sys.stdout
|
||||
self._topology = Topology.instance()
|
||||
|
||||
def _writeMessageSlot(self, message, level):
|
||||
if level == "error":
|
||||
self.write(message, error=True)
|
||||
elif level == "warning":
|
||||
self.write(message, warning=True)
|
||||
else:
|
||||
self.write(message)
|
||||
|
||||
def _handleLogs(self):
|
||||
"""
|
||||
Catch log message and display them
|
||||
"""
|
||||
|
||||
log = logging.getLogger()
|
||||
log_handler = ConsoleLogHandler()
|
||||
log_handler._console_view = self
|
||||
log.addHandler(log_handler)
|
||||
|
||||
def isatty(self):
|
||||
"""
|
||||
For exception handling purposes
|
||||
@@ -131,70 +191,64 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
"""
|
||||
|
||||
text = "Server notification: {}".format(message)
|
||||
self.write(text, error=True)
|
||||
self.write("\n")
|
||||
if details:
|
||||
self.write(details)
|
||||
self.write("\n")
|
||||
text += "\n" + details
|
||||
self.write_message_signal.emit(text, "info")
|
||||
|
||||
def writeError(self, node_id, message):
|
||||
def writeError(self, base_node_id, message):
|
||||
"""
|
||||
Write error messages.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: base node identifier
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
node = Topology.instance().getNode(node_id)
|
||||
node = Topology.instance().getNode(base_node_id)
|
||||
name = ""
|
||||
if node and node.name():
|
||||
name = " {}:".format(node.name())
|
||||
|
||||
text = "Error:{name} {message}".format(name=name,
|
||||
message=message)
|
||||
self.write(text, error=True)
|
||||
self.write("\n")
|
||||
self.write_message_signal.emit(text, "error")
|
||||
|
||||
def writeWarning(self, node_id, message):
|
||||
def writeWarning(self, base_node_id, message):
|
||||
"""
|
||||
Write warning messages.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: base node identifier
|
||||
:param message: warning message
|
||||
"""
|
||||
|
||||
node = Topology.instance().getNode(node_id)
|
||||
node = Topology.instance().getNode(base_node_id)
|
||||
name = ""
|
||||
if node and node.name():
|
||||
name = " {}:".format(node.name())
|
||||
|
||||
text = "Warning:{name} {message}".format(name=name,
|
||||
message=message)
|
||||
self.write(text, warning=True)
|
||||
self.write("\n")
|
||||
self.write_message_signal.emit(text, "warning")
|
||||
|
||||
def writeServerError(self, node_id, message):
|
||||
def writeServerError(self, base_node_id, message):
|
||||
"""
|
||||
Write server error messages coming from the server.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: Base node identifier
|
||||
:param code: error code
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
node = Topology.instance().getNode(node_id)
|
||||
node = Topology.instance().getNode(base_node_id)
|
||||
server = name = ""
|
||||
if node:
|
||||
if node.name():
|
||||
name = " {}:".format(node.name())
|
||||
server = "from {}:{}".format(node.server().host,
|
||||
node.server().port)
|
||||
server = "from {}".format(node.compute().name())
|
||||
|
||||
text = "Server error {server}:{name} {message}".format(server=server,
|
||||
name=name,
|
||||
message=message)
|
||||
self.write(text, error=True)
|
||||
self.write("\n")
|
||||
self.write_message_signal.emit(text.strip(), "error")
|
||||
|
||||
def _run(self):
|
||||
"""
|
||||
|
||||
338
gns3/controller.py
Normal file
338
gns3/controller.py
Normal file
@@ -0,0 +1,338 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import tempfile
|
||||
|
||||
from .qt import QtCore, QtGui, QtWidgets, qpartial, qslot
|
||||
from .symbol import Symbol
|
||||
from .local_server_config import LocalServerConfig
|
||||
from .settings import LOCAL_SERVER_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Controller(QtCore.QObject):
|
||||
"""
|
||||
An instance of the GNS3 server controller
|
||||
"""
|
||||
connected_signal = QtCore.Signal()
|
||||
disconnected_signal = QtCore.Signal()
|
||||
connection_failed_signal = QtCore.Signal()
|
||||
project_list_updated_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__()
|
||||
self._connected = False
|
||||
self._connecting = False
|
||||
self._cache_directory = tempfile.mkdtemp()
|
||||
self._http_client = None
|
||||
# If it's the first error we display an alert box to the user
|
||||
self._first_error = True
|
||||
self._error_dialog = None
|
||||
self._display_error = True
|
||||
self._projects = []
|
||||
|
||||
# If we do multiple call in order to download the same symbol we queue them
|
||||
self._static_asset_download_queue = {}
|
||||
|
||||
def host(self):
|
||||
return self._http_client.host()
|
||||
|
||||
def isRemote(self):
|
||||
"""
|
||||
:returns Boolean: True if the controller is remote
|
||||
"""
|
||||
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
|
||||
return not settings["auto_start"]
|
||||
|
||||
def connecting(self):
|
||||
"""
|
||||
:returns: True if connection is in progress
|
||||
"""
|
||||
return self._connecting
|
||||
|
||||
def connected(self):
|
||||
"""
|
||||
Is the controller connected
|
||||
"""
|
||||
return self._connected
|
||||
|
||||
def httpClient(self):
|
||||
"""
|
||||
:returns: HTTP client for connected to the controller
|
||||
"""
|
||||
return self._http_client
|
||||
|
||||
def setHttpClient(self, http_client):
|
||||
"""
|
||||
:param http_client: Instance of HTTP client to communicate with the server
|
||||
"""
|
||||
self._http_client = http_client
|
||||
if self._http_client:
|
||||
if self.isRemote():
|
||||
self._http_client.setMaxTimeDifferenceBetweenQueries(120)
|
||||
self._http_client.connection_connected_signal.connect(self._httpClientConnectedSlot)
|
||||
self._http_client.connection_disconnected_signal.connect(self._httpClientDisconnectedSlot)
|
||||
self._connectingToServer()
|
||||
|
||||
def 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.disconnected_signal.emit()
|
||||
self._connectingToServer()
|
||||
|
||||
def _versionGetSlot(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Called after the inital version get
|
||||
"""
|
||||
if error:
|
||||
if self._first_error:
|
||||
self._connecting = False
|
||||
self.connection_failed_signal.emit()
|
||||
if "message" in result and self._display_error:
|
||||
self._error_dialog = QtWidgets.QMessageBox(self.parent())
|
||||
self._error_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
self._error_dialog.setWindowTitle("Connection to server")
|
||||
self._error_dialog.setText("Error when connecting to the GNS3 server:\n{}".format(result["message"]))
|
||||
self._error_dialog.setIcon(QtWidgets.QMessageBox.Critical)
|
||||
self._error_dialog.show()
|
||||
# Try to connect again in x seconds
|
||||
QtCore.QTimer.singleShot(5000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_error))
|
||||
self._first_error = False
|
||||
else:
|
||||
self._first_error = True
|
||||
if self._error_dialog:
|
||||
self._error_dialog.reject()
|
||||
self._error_dialog = None
|
||||
|
||||
def _httpClientConnectedSlot(self):
|
||||
if not self._connected:
|
||||
self._connected = True
|
||||
self._connecting = False
|
||||
self.connected_signal.emit()
|
||||
self.refreshProjectList()
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("GET", *args, **kwargs)
|
||||
|
||||
def getCompute(self, path, compute_id, *args, **kwargs):
|
||||
"""
|
||||
API get on a specific compute
|
||||
"""
|
||||
compute_id = self.__fix_compute_id(compute_id)
|
||||
path = "/computes/{}{}".format(compute_id, path)
|
||||
return self.get(path, *args, **kwargs)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("POST", *args, **kwargs)
|
||||
|
||||
def postCompute(self, path, compute_id, *args, **kwargs):
|
||||
"""
|
||||
API post on a specific compute
|
||||
"""
|
||||
compute_id = self.__fix_compute_id(compute_id)
|
||||
path = "/computes/{}{}".format(compute_id, path)
|
||||
return self.post(path, *args, **kwargs)
|
||||
|
||||
def __fix_compute_id(self, compute_id):
|
||||
"""
|
||||
Support for remote server <= 1.5
|
||||
This fix should be not require after the 2.1
|
||||
when all the appliance template will be managed
|
||||
on server
|
||||
"""
|
||||
if compute_id.startswith("http:") or compute_id.startswith("https:"):
|
||||
from .compute_manager import ComputeManager
|
||||
try:
|
||||
return ComputeManager.instance().getCompute(compute_id).id()
|
||||
except KeyError:
|
||||
return compute_id
|
||||
return compute_id
|
||||
|
||||
def 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)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("DELETE", *args, **kwargs)
|
||||
|
||||
def createHTTPQuery(self, method, path, *args, **kwargs):
|
||||
"""
|
||||
Forward the query to the HTTP client or controller depending of the path
|
||||
"""
|
||||
if self._http_client:
|
||||
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
|
||||
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
return self._http_client.getSynchronous(endpoint, timeout)
|
||||
|
||||
def connectWebSocket(self, path, *args):
|
||||
return self._http_client.connectWebSocket(path)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of Controller.
|
||||
:returns: instance of Controller
|
||||
"""
|
||||
|
||||
if not hasattr(Controller, '_instance') or Controller._instance is None:
|
||||
Controller._instance = Controller()
|
||||
return Controller._instance
|
||||
|
||||
def getStatic(self, url, callback, 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:
|
||||
return
|
||||
|
||||
|
||||
path = self.getStaticCachedPath(url)
|
||||
|
||||
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._static_asset_download_queue[path] = [(callback, fallback, )]
|
||||
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, url, path))
|
||||
|
||||
def _getStaticCallback(self, url, path, result, error=False, raw_body=None, **kwargs):
|
||||
if path not in self._static_asset_download_queue:
|
||||
return
|
||||
|
||||
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.debug("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 getStaticCachedPath(self, url):
|
||||
"""
|
||||
Returns static cached (hashed) path
|
||||
:param url:
|
||||
:return:
|
||||
"""
|
||||
m = hashlib.md5()
|
||||
m.update(url.encode())
|
||||
if ".svg" in url:
|
||||
extension = ".svg"
|
||||
else:
|
||||
extension = ".png"
|
||||
path = os.path.join(self._cache_directory, m.hexdigest() + extension)
|
||||
return path
|
||||
|
||||
def getSymbolIcon(self, symbol_id, callback, fallback=None):
|
||||
"""
|
||||
Get a QIcon for a symbol from the controller
|
||||
|
||||
:param symbol_id: Symbol id
|
||||
:param callback: Callback to call when file is ready
|
||||
:param fallback: Fallback symbol if not found
|
||||
"""
|
||||
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 getSymbols(self, callback):
|
||||
self.get('/symbols', callback=callback)
|
||||
|
||||
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
|
||||
@@ -16,54 +16,82 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import psutil
|
||||
import os
|
||||
import platform
|
||||
import struct
|
||||
|
||||
try:
|
||||
import raven
|
||||
from raven.transport.http import HTTPTransport
|
||||
RAVEN_AVAILABLE = True
|
||||
except ImportError:
|
||||
# raven is not installed with deb package in order to simplify packaging
|
||||
RAVEN_AVAILABLE = False
|
||||
|
||||
|
||||
from .version import __version__
|
||||
from .servers import Servers
|
||||
from .utils.get_resource import get_resource
|
||||
from .version import __version__, __version_info__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Dev build
|
||||
if __version_info__[3] != 0:
|
||||
import faulthandler
|
||||
# Display a traceback in case of segfault crash. Usefull when frozen
|
||||
# Not enabled by default for security reason
|
||||
log.debug("Enable catching segfault")
|
||||
faulthandler.enable()
|
||||
|
||||
|
||||
class CrashReport:
|
||||
|
||||
"""
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "sync+https://ef1dcfaa26ed431a89466c05c4ddf9bf:286666300dbc454b8eebe59fa368c243@app.getsentry.com/38506"
|
||||
DSN = "sync+https://08a47076d6cf4eebb455e8e79ef1e78d:ee721159025d4e2ea044e119a8789ece@sentry.io/38506"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = os.path.join(os.getcwd(), "cacert.pem")
|
||||
if os.path.isfile(cacert):
|
||||
cacert = get_resource("cacert.pem")
|
||||
if cacert is not None and os.path.isfile(cacert):
|
||||
DSN += "?ca_certs={}".format(cacert)
|
||||
else:
|
||||
log.warning("The SSL certificate bundle file '{}' could not be found".format(cacert))
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
self._client = None
|
||||
# We don't want sentry making noise if an error is catched when you don't have internet
|
||||
sentry_errors = logging.getLogger('sentry.errors')
|
||||
sentry_errors.disabled = True
|
||||
|
||||
sentry_uncaught = logging.getLogger('sentry.errors.uncaught')
|
||||
sentry_uncaught.disabled = True
|
||||
|
||||
def captureException(self, exception, value, tb):
|
||||
if not RAVEN_AVAILABLE:
|
||||
return
|
||||
if os.path.exists(".git"):
|
||||
log.warning("A .git directory exist crash report is turn off for developers")
|
||||
return
|
||||
local_server = Servers.instance().localServerSettings()
|
||||
from .local_server import LocalServer
|
||||
from .local_config import LocalConfig
|
||||
|
||||
local_server = LocalServer.instance().localServerSettings()
|
||||
if local_server["report_errors"]:
|
||||
if self._client is None:
|
||||
self._client = raven.Client(CrashReport.DSN, release=__version__)
|
||||
self._client.tags_context({
|
||||
if not RAVEN_AVAILABLE:
|
||||
return
|
||||
|
||||
if os.path.exists(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)
|
||||
return
|
||||
|
||||
if hasattr(exception, "fingerprint"):
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, fingerprint=['{{ default }}', exception.fingerprint], transport=HTTPTransport)
|
||||
else:
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, transport=HTTPTransport)
|
||||
context = {
|
||||
"os:name": platform.system(),
|
||||
"os:release": platform.release(),
|
||||
"os:win_32": " ".join(platform.win32_ver()),
|
||||
@@ -75,13 +103,27 @@ class CrashReport:
|
||||
"python:bit": struct.calcsize("P") * 8,
|
||||
"python:encoding": sys.getdefaultencoding(),
|
||||
"python:frozen": "{}".format(hasattr(sys, "frozen"))
|
||||
})
|
||||
}
|
||||
context = self._add_qt_information(context)
|
||||
client.tags_context(context)
|
||||
try:
|
||||
report = self._client.captureException((exception, value, tb))
|
||||
report = client.captureException((exception, value, tb))
|
||||
except Exception as e:
|
||||
log.error("Can't send crash report to Sentry: {}".format(e))
|
||||
return
|
||||
log.info("Crash report sent with event ID: {}".format(self._client.get_ident(report)))
|
||||
log.debug("Crash report sent with event ID: {}".format(client.get_ident(report)))
|
||||
|
||||
def _add_qt_information(self, context):
|
||||
try:
|
||||
from .qt import QtCore
|
||||
import sip
|
||||
except ImportError:
|
||||
return context
|
||||
context["psutil:version"] = psutil.__version__
|
||||
context["pyqt:version"] = QtCore.PYQT_VERSION_STR
|
||||
context["qt:version"] = QtCore.QT_VERSION_STR
|
||||
context["sip:version"] = sip.SIP_VERSION_STR
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def instance(cls):
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtGui
|
||||
from ..qt import QtWidgets
|
||||
from ..version import __version__
|
||||
from ..ui.about_dialog_ui import Ui_AboutDialog
|
||||
|
||||
|
||||
class AboutDialog(QtGui.QDialog, Ui_AboutDialog):
|
||||
class AboutDialog(QtWidgets.QDialog, Ui_AboutDialog):
|
||||
|
||||
"""
|
||||
About dialog.
|
||||
@@ -28,7 +28,7 @@ class AboutDialog(QtGui.QDialog, Ui_AboutDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
# dynamically add the current version number
|
||||
|
||||
644
gns3/dialogs/appliance_wizard.py
Normal file
644
gns3/dialogs/appliance_wizard.py
Normal file
@@ -0,0 +1,644 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sip
|
||||
import shutil
|
||||
|
||||
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
|
||||
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
|
||||
from ..modules import Qemu
|
||||
from ..registry.appliance import Appliance, ApplianceError
|
||||
from ..registry.registry import Registry
|
||||
from ..registry.config import Config, ConfigException
|
||||
from ..registry.image import Image
|
||||
from ..utils import human_filesize
|
||||
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..compute_manager import ComputeManager
|
||||
from ..controller import Controller
|
||||
from ..local_config import LocalConfig
|
||||
from ..image_upload_manager import ImageUploadManager
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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
|
||||
# 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.uiApplianceVersionTreeWidget.currentItemChanged.connect(self._applianceVersionCurrentItemChangedSlot)
|
||||
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)
|
||||
|
||||
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
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
|
||||
|
||||
# symbols loaded from controller
|
||||
self._symbols = []
|
||||
|
||||
|
||||
def initializePage(self, page_id):
|
||||
"""
|
||||
Initialize Wizard pages.
|
||||
|
||||
:param page_id: page identifier
|
||||
"""
|
||||
super().initializePage(page_id)
|
||||
|
||||
if self._appliance["category"] == "guest":
|
||||
symbol = ":/symbols/computer.svg"
|
||||
else:
|
||||
symbol = ":/symbols/{}.svg".format(self._appliance["category"])
|
||||
self.page(page_id).setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(symbol))
|
||||
|
||||
if "qemu" in self._appliance:
|
||||
type = "qemu"
|
||||
elif "iou" in self._appliance:
|
||||
type = "iou"
|
||||
elif "docker" in self._appliance:
|
||||
type = "docker"
|
||||
elif "dynamips" in self._appliance:
|
||||
type = "dynamips"
|
||||
|
||||
if self.page(page_id) == self.uiInfoWizardPage:
|
||||
Controller.instance().getSymbols(self._getSymbolsCallback)
|
||||
|
||||
self.uiInfoWizardPage.setTitle(self._appliance["product_name"])
|
||||
self.uiDescriptionLabel.setText(self._appliance["description"])
|
||||
|
||||
info = (
|
||||
("Category", "category"),
|
||||
("Product", "product_name"),
|
||||
("Vendor", "vendor_name"),
|
||||
("Status", "status"),
|
||||
("Maintainer", "maintainer"),
|
||||
("Architecture", "qemu/arch"),
|
||||
("KVM", "qemu/kvm")
|
||||
)
|
||||
|
||||
self.uiInfoTreeWidget.clear()
|
||||
for (name, key) in info:
|
||||
if "/" in key:
|
||||
key, subkey = key.split("/")
|
||||
value = self._appliance.get(key, {}).get(subkey, None)
|
||||
else:
|
||||
value = self._appliance.get(key, None)
|
||||
if value is None:
|
||||
continue
|
||||
item = QtWidgets.QTreeWidgetItem([name + ":", value])
|
||||
font = item.font(0)
|
||||
font.setBold(True)
|
||||
item.setFont(0, font)
|
||||
self.uiInfoTreeWidget.addTopLevelItem(item)
|
||||
|
||||
elif self.page(page_id) == self.uiServerWizardPage:
|
||||
is_mac = ComputeManager.instance().localPlatform().startswith("darwin")
|
||||
is_win = ComputeManager.instance().localPlatform().startswith("win")
|
||||
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
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 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():
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
elif type != "dynamips":
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
|
||||
if ComputeManager.instance().vmCompute():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
elif ComputeManager.instance().localCompute() and self.uiLocalRadioButton.isEnabled():
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
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._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
|
||||
|
||||
elif self.page(page_id) == self.uiQemuWizardPage:
|
||||
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
|
||||
|
||||
elif self.page(page_id) == self.uiSummaryWizardPage:
|
||||
self.uiSummaryTreeWidget.clear()
|
||||
|
||||
for key in self._appliance[type]:
|
||||
item = QtWidgets.QTreeWidgetItem([key.replace('_', ' ').capitalize() + ":", str(self._appliance[type][key])])
|
||||
font = item.font(0)
|
||||
font.setBold(True)
|
||||
item.setFont(0, font)
|
||||
self.uiSummaryTreeWidget.addTopLevelItem(item)
|
||||
self.uiSummaryTreeWidget.resizeColumnToContents(0)
|
||||
|
||||
elif self.page(page_id) == self.uiUsageWizardPage:
|
||||
self.uiUsageTextEdit.setText("The appliance is available in the {} category. \n\n{}".format(
|
||||
self._appliance["category"].replace("_", " "),
|
||||
self._appliance.get("usage", ""))
|
||||
)
|
||||
|
||||
elif self.page(page_id) == self.uiCheckServerWizardPage:
|
||||
self.uiCheckServerLabel.setText("Please wait while checking server capacities...")
|
||||
if 'qemu' in self._appliance:
|
||||
if self._appliance['qemu'].get('kvm', 'require') == 'require':
|
||||
self._server_check = False # If the server as the capacities for running the appliance
|
||||
self.uiCheckServerLabel.setText("")
|
||||
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
|
||||
return
|
||||
self.uiCheckServerLabel.setText("GNS3 server requirements is OK you can continue the installation")
|
||||
self._server_check = True
|
||||
|
||||
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
|
||||
"""
|
||||
Check if server support KVM or not
|
||||
"""
|
||||
if error is None and "kvm" in result and self._appliance["qemu"]["arch"] in result["kvm"]:
|
||||
self._server_check = True
|
||||
self.uiCheckServerLabel.setText("GNS3 server requirements is OK you can continue the installation")
|
||||
else:
|
||||
if error:
|
||||
msg = result["message"]
|
||||
else:
|
||||
msg = "The remote server doesn't support KVM. You need a Linux server or the GNS3 VM with VMware and CPU virtualization instructions."
|
||||
self.uiCheckServerLabel.setText(msg)
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu", msg)
|
||||
self._server_check = False
|
||||
|
||||
def _uiServerWizardPage_isComplete(self):
|
||||
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
|
||||
|
||||
def _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
|
||||
"""
|
||||
|
||||
if self._refreshing:
|
||||
return
|
||||
self._refreshing = True
|
||||
|
||||
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()
|
||||
|
||||
@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()
|
||||
|
||||
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"
|
||||
|
||||
size += image.get("filesize", 0)
|
||||
image_widget = QtWidgets.QTreeWidgetItem(
|
||||
[
|
||||
"",
|
||||
image["filename"],
|
||||
human_filesize(image.get("filesize", 0)),
|
||||
image["status"],
|
||||
image["version"],
|
||||
image.get("md5sum", "")
|
||||
])
|
||||
if image["status"] == "Missing":
|
||||
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
|
||||
else:
|
||||
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
|
||||
|
||||
# Associated data stored are col 0: version, col 1: image
|
||||
image_widget.setData(0, QtCore.Qt.UserRole, version)
|
||||
image_widget.setData(1, QtCore.Qt.UserRole, image)
|
||||
image_widget.setData(2, QtCore.Qt.UserRole, self._appliance)
|
||||
top.addChild(image_widget)
|
||||
|
||||
font = top.font(0)
|
||||
font.setBold(True)
|
||||
top.setFont(0, font)
|
||||
|
||||
expand = True
|
||||
if status == "Missing files":
|
||||
top.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
|
||||
else:
|
||||
expand = False
|
||||
top.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
|
||||
|
||||
top.setData(2, QtCore.Qt.DisplayRole, human_filesize(size))
|
||||
top.setData(3, QtCore.Qt.DisplayRole, status)
|
||||
top.setData(2, QtCore.Qt.UserRole, self._appliance)
|
||||
top.setData(0, QtCore.Qt.UserRole, version)
|
||||
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
|
||||
# self.uiApplianceVersionTreeWidget.setCurrentItem(top)
|
||||
if expand:
|
||||
top.setExpanded(True)
|
||||
|
||||
if len(self._appliance["versions"]) > 0:
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(0)
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(1)
|
||||
self._refreshing = False
|
||||
|
||||
def _getSymbolsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.warning("Cannot load symbols from controller")
|
||||
else:
|
||||
self._symbols = result
|
||||
|
||||
def _refreshDialogWorker(self):
|
||||
"""
|
||||
Scan local directory in order to found the images on disk
|
||||
"""
|
||||
|
||||
# Docker do not have versions
|
||||
if "versions" not in self._appliance:
|
||||
return
|
||||
|
||||
for version in self._appliance["versions"]:
|
||||
for image in version["images"].values():
|
||||
img = self._registry.search_image_file(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
|
||||
"""
|
||||
self.uiDownloadPushButton.hide()
|
||||
self.uiImportPushButton.hide()
|
||||
self.uiExplainDownloadLabel.hide()
|
||||
|
||||
if current is None or sip.isdeleted(current):
|
||||
return
|
||||
|
||||
image = current.data(1, QtCore.Qt.UserRole)
|
||||
if image is not None:
|
||||
if "direct_download_url" in image or "download_url" in image:
|
||||
self.uiDownloadPushButton.show()
|
||||
self.uiImportPushButton.show()
|
||||
|
||||
@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 or sip.isdeleted(current):
|
||||
return
|
||||
|
||||
data = current.data(1, QtCore.Qt.UserRole)
|
||||
if data is not None:
|
||||
if "direct_download_url" in data:
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["direct_download_url"]))
|
||||
if "compression" in data:
|
||||
QtWidgets.QMessageBox.warning(self, "Add appliance", "The file is compressed with {} you need to uncompress it before using it.".format(data["compression"]))
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "Add appliance", "Download will redirect you where the required file can be downloaded, you may have to be registered with the vendor in order to download the file.")
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["download_url"]))
|
||||
|
||||
@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:
|
||||
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()
|
||||
|
||||
@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(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
|
||||
|
||||
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):
|
||||
"""
|
||||
Callback for getQemuBinariesFromServer.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu binaries", "{}".format(result["message"]))
|
||||
else:
|
||||
self.uiQemuListComboBox.clear()
|
||||
for qemu in result:
|
||||
if qemu["version"]:
|
||||
self.uiQemuListComboBox.addItem("{path} (v{version})".format(path=qemu["path"], version=qemu["version"]), qemu["path"])
|
||||
else:
|
||||
self.uiQemuListComboBox.addItem("{path}".format(path=qemu["path"]), qemu["path"])
|
||||
if self.uiQemuListComboBox.count() == 1:
|
||||
self.next()
|
||||
else:
|
||||
i = self.uiQemuListComboBox.findText(self._appliance["qemu"]["arch"], QtCore.Qt.MatchContains)
|
||||
if i != -1:
|
||||
self.uiQemuListComboBox.setCurrentIndex(i)
|
||||
|
||||
def _install(self, version):
|
||||
"""
|
||||
Install the appliance to GNS3
|
||||
|
||||
:params version: Version name
|
||||
"""
|
||||
|
||||
try:
|
||||
config = Config()
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
||||
return False
|
||||
|
||||
if version is None:
|
||||
appliance_configuration = self._appliance.copy()
|
||||
else:
|
||||
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._compute_id, self._symbols),
|
||||
allowed_exceptions=[ConfigException, OSError])
|
||||
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if not progress_dialog.exec_():
|
||||
return False
|
||||
|
||||
worker = WaitForLambdaWorker(lambda: config.save(), allowed_exceptions=[ConfigException, OSError])
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if progress_dialog.exec_():
|
||||
QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
|
||||
return True
|
||||
|
||||
def _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:
|
||||
return super().nextId() + 3
|
||||
elif "qemu" not in self._appliance:
|
||||
return super().nextId() + 1
|
||||
elif self.currentPage() == self.uiFilesWizardPage:
|
||||
if "qemu" not in self._appliance:
|
||||
return super().nextId() + 1
|
||||
return super().nextId()
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the settings.
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiFilesWizardPage:
|
||||
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"]))
|
||||
return False
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Would you like to install {} version {}?".format(appliance["name"], version["name"]),
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
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)
|
||||
return self._install(version["name"])
|
||||
else:
|
||||
return self._install(None)
|
||||
|
||||
elif self.currentPage() == self.uiServerWizardPage:
|
||||
if self.uiRemoteRadioButton.isChecked():
|
||||
if len(ComputeManager.instance().remoteComputes()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote server registered in your preferences")
|
||||
return False
|
||||
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
|
||||
self._compute_id = "vm"
|
||||
else:
|
||||
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._compute_id = "local"
|
||||
|
||||
elif self.currentPage() == self.uiQemuWizardPage:
|
||||
if self.uiQemuListComboBox.currentIndex() == -1:
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu binary", "No compatible Qemu binary selected")
|
||||
return False
|
||||
|
||||
elif self.currentPage() == self.uiCheckServerWizardPage:
|
||||
return self._server_check
|
||||
|
||||
return True
|
||||
|
||||
@qslot
|
||||
def _vmToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the VM radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
@qslot
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the remote server radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(True)
|
||||
self.uiRemoteServersGroupBox.show()
|
||||
|
||||
@qslot
|
||||
def _localToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the local server radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
75
gns3/dialogs/capture_dialog.py
Normal file
75
gns3/dialogs/capture_dialog.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.capture_dialog_ui import Ui_CaptureDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CaptureDialog(QtWidgets.QDialog, Ui_CaptureDialog):
|
||||
"""
|
||||
This dialog allow configure the packet capture
|
||||
"""
|
||||
|
||||
def __init__(self, parent, file_name, auto_start, ethernet_link=True):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
||||
|
||||
if ethernet_link:
|
||||
self.uiDataLinkTypeComboBox.addItem("Ethernet", "DLT_EN10MB")
|
||||
else:
|
||||
self.uiDataLinkTypeComboBox.addItem("Cisco HDLC", "DLT_C_HDLC")
|
||||
self.uiDataLinkTypeComboBox.addItem("Cisco PPP", "DLT_PPP_SERIAL")
|
||||
self.uiDataLinkTypeComboBox.addItem("Frame Relay", "DLT_FRELAY")
|
||||
self.uiDataLinkTypeComboBox.addItem("ATM", "DLT_ATM_RFC1483")
|
||||
|
||||
self.uiCaptureFileNameLineEdit.setText(file_name)
|
||||
self.uiStartCommandCheckBox.setChecked(auto_start)
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
if len(self.fileName()) == 0:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Packet capture", "Please provide a file name for the capture")
|
||||
return
|
||||
self.accept()
|
||||
|
||||
def fileName(self):
|
||||
return self.uiCaptureFileNameLineEdit.text()
|
||||
|
||||
def dataLink(self):
|
||||
"""
|
||||
Type of link for capture
|
||||
"""
|
||||
return self.uiDataLinkTypeComboBox.currentData()
|
||||
|
||||
def commandAutoStart(self):
|
||||
return self.uiStartCommandCheckBox.isChecked()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = CaptureDialog(main, "test.pcap")
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
print(dialog.dataLink())
|
||||
print(dialog.fileName())
|
||||
@@ -19,12 +19,12 @@
|
||||
Dialog to configure and update node settings using widget pages.
|
||||
"""
|
||||
|
||||
from ..qt import QtGui
|
||||
from ..qt import QtWidgets
|
||||
from ..ui.configuration_dialog_ui import Ui_configurationDialog
|
||||
from .node_configurator_dialog import ConfigurationError
|
||||
from .node_properties_dialog import ConfigurationError
|
||||
|
||||
|
||||
class ConfigurationDialog(QtGui.QDialog, Ui_configurationDialog):
|
||||
class ConfigurationDialog(QtWidgets.QDialog, Ui_configurationDialog):
|
||||
|
||||
"""
|
||||
Configuration dialog implementation.
|
||||
@@ -37,17 +37,22 @@ class ConfigurationDialog(QtGui.QDialog, Ui_configurationDialog):
|
||||
|
||||
def __init__(self, name, settings, configuration_page, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.uiTitleLabel.setText(name)
|
||||
self.setWindowTitle(configuration_page.windowTitle())
|
||||
self.uiConfigStackedWidget.addWidget(configuration_page)
|
||||
self.uiConfigStackedWidget.setCurrentWidget(configuration_page)
|
||||
self.setModal(True)
|
||||
configuration_page.loadSettings(settings)
|
||||
self._settings = settings
|
||||
self._configuration_page = configuration_page
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return self._settings
|
||||
|
||||
def on_uiButtonBox_clicked(self, button):
|
||||
"""
|
||||
Slot called when a button of the uiButtonBox is clicked.
|
||||
@@ -55,11 +60,11 @@ class ConfigurationDialog(QtGui.QDialog, Ui_configurationDialog):
|
||||
:param button: button that was clicked (QAbstractButton)
|
||||
"""
|
||||
|
||||
if button == self.uiButtonBox.button(QtGui.QDialogButtonBox.Cancel):
|
||||
QtGui.QDialog.reject(self)
|
||||
if button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
|
||||
QtWidgets.QDialog.reject(self)
|
||||
else:
|
||||
try:
|
||||
self._configuration_page.saveSettings(self._settings)
|
||||
except ConfigurationError:
|
||||
return
|
||||
QtGui.QDialog.accept(self)
|
||||
QtWidgets.QDialog.accept(self)
|
||||
|
||||
135
gns3/dialogs/console_command_dialog.py
Normal file
135
gns3/dialogs/console_command_dialog.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.ui.console_command_dialog_ui import Ui_uiConsoleCommandDialog
|
||||
from gns3.settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, \
|
||||
PRECONFIGURED_VNC_CONSOLE_COMMANDS, \
|
||||
PRECONFIGURED_SPICE_CONSOLE_COMMANDS, \
|
||||
CUSTOM_CONSOLE_COMMANDS_SETTINGS
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
|
||||
"""
|
||||
This dialog allow user to select the command used to start a
|
||||
console.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, console_type="telnet", current=None):
|
||||
"""
|
||||
:params console_type: telnet, serial, vnc or spice
|
||||
:params current: Current console command
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self._console_type = console_type
|
||||
self._current = current
|
||||
|
||||
self._settings = LocalConfig.instance().loadSectionSettings("CustomConsoleCommands", CUSTOM_CONSOLE_COMMANDS_SETTINGS)
|
||||
|
||||
self.uiCommandComboBox.currentIndexChanged.connect(self.commandComboBoxCurrentIndexChangedSlot)
|
||||
self.uiCommandPlainTextEdit.textChanged.connect(self.textChangedSlot)
|
||||
self.uiSavePushButton.clicked.connect(self.savePushButtonClickedSlot)
|
||||
self.uiRemovePushButton.clicked.connect(self.removePushButtonClickedSlot)
|
||||
|
||||
self._refreshList()
|
||||
|
||||
def _refreshList(self):
|
||||
if self._console_type == "telnet":
|
||||
self._consoles = copy.copy(PRECONFIGURED_TELNET_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
elif self._console_type == "vnc":
|
||||
self._consoles = copy.copy(PRECONFIGURED_VNC_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
elif self._console_type == "spice":
|
||||
self._consoles = copy.copy(PRECONFIGURED_SPICE_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
|
||||
self.uiCommandComboBox.clear()
|
||||
self.uiCommandComboBox.addItem("Custom", "")
|
||||
for name, cmd in sorted(self._consoles.items(), key=(lambda item: item[0].lower())):
|
||||
self.uiCommandComboBox.addItem(name, cmd)
|
||||
|
||||
if self._current:
|
||||
self.uiCommandPlainTextEdit.setPlainText(self._current)
|
||||
else:
|
||||
self.uiCommandComboBox.setCurrentIndex(1)
|
||||
|
||||
def removePushButtonClickedSlot(self):
|
||||
"""
|
||||
Remove the custom command from the custom list
|
||||
"""
|
||||
self._settings[self._console_type].pop(self.uiCommandComboBox.currentText())
|
||||
LocalConfig.instance().saveSectionSettings("CustomConsoleCommands", self._settings)
|
||||
self._current = None
|
||||
self._refreshList()
|
||||
|
||||
def savePushButtonClickedSlot(self):
|
||||
"""
|
||||
Save a custom command to the list
|
||||
"""
|
||||
name, ok = QtWidgets.QInputDialog.getText(self, "Add a command", "Command name:", QtWidgets.QLineEdit.Normal)
|
||||
command = self.uiCommandPlainTextEdit.toPlainText().strip()
|
||||
if ok and len(command) > 0:
|
||||
if command not in self._consoles.values():
|
||||
self._settings[self._console_type][name] = command
|
||||
self._current = command
|
||||
LocalConfig.instance().saveSectionSettings("CustomConsoleCommands", self._settings)
|
||||
self._refreshList()
|
||||
|
||||
def textChangedSlot(self):
|
||||
index = self.uiCommandComboBox.findData(self.uiCommandPlainTextEdit.toPlainText())
|
||||
if index == -1:
|
||||
index = 0
|
||||
self.uiCommandComboBox.setCurrentIndex(index)
|
||||
|
||||
def commandComboBoxCurrentIndexChangedSlot(self, index):
|
||||
self.uiRemovePushButton.hide()
|
||||
# Ignore custom command
|
||||
if index != 0:
|
||||
self.uiCommandPlainTextEdit.setPlainText(self.uiCommandComboBox.currentData())
|
||||
self.uiSavePushButton.hide()
|
||||
if self.uiCommandComboBox.currentText() in self._settings[self._console_type].keys():
|
||||
self.uiRemovePushButton.show()
|
||||
else:
|
||||
self.uiSavePushButton.show()
|
||||
|
||||
@staticmethod
|
||||
def getCommand(parent, console_type="telnet", current=None):
|
||||
dialog = ConsoleCommandDialog(parent, console_type=console_type, current=current)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
return (True, dialog.uiCommandPlainTextEdit.toPlainText().replace("\n", " "))
|
||||
return (False, None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
(ok, command) = ConsoleCommandDialog.getCommand(main, console_type="telnet", current=list(PRECONFIGURED_TELNET_CONSOLE_COMMANDS.items())[0][1])
|
||||
print(ok)
|
||||
print(command)
|
||||
|
||||
226
gns3/dialogs/doctor_dialog.py
Normal file
226
gns3/dialogs/doctor_dialog.py
Normal file
@@ -0,0 +1,226 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import psutil
|
||||
import platform
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import struct
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.doctor_dialog_ui import Ui_DoctorDialog
|
||||
from gns3.local_server import LocalServer
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3 import version
|
||||
from gns3.modules.vmware import VMware
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
"""
|
||||
This dialog allow user to detect error in his GNS3 installation.
|
||||
|
||||
If you want to add a test add a method starting by check. The
|
||||
check return a tuple result and a message in case of failure.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, console=False):
|
||||
|
||||
super().__init__(parent)
|
||||
self._console = console
|
||||
self.setupUi(self)
|
||||
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
|
||||
for method in sorted(dir(self)):
|
||||
if method.startswith('check'):
|
||||
try:
|
||||
self.write(getattr(self, method).__doc__ + "...")
|
||||
(res, msg) = getattr(self, method)()
|
||||
if res == 0:
|
||||
self.write('<span style="color: green"><strong>OK</strong></span>')
|
||||
elif res == 1:
|
||||
self.write('<span style="color: orange"><strong>WARNING</strong> {}</span>'.format(msg))
|
||||
elif res == 2:
|
||||
self.write('<span style="color: red"><strong>ERROR</strong> {}</span>'.format(msg))
|
||||
except Exception as e:
|
||||
log.error("GNS3 doctor exception detected: {}".format(e), exc_info=1)
|
||||
self.write('<span style="color: red"><strong>FAIL</strong> The doctor failed during this test with error: {} Please check on the forum.</span>'.format(str(e)))
|
||||
self.write("<br/>")
|
||||
|
||||
def write(self, text):
|
||||
"""
|
||||
Add text to the text windows
|
||||
"""
|
||||
if self._console:
|
||||
print(text)
|
||||
self.uiDoctorResultTextEdit.setHtml(self.uiDoctorResultTextEdit.toHtml() + text)
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
self.accept()
|
||||
|
||||
def checkLocalServerEnabled(self):
|
||||
"""Checking if the local server is enabled"""
|
||||
if LocalServer.instance().shouldLocalServerAutoStart() is False:
|
||||
return (2, "The local server is disabled. Go to Preferences -> Server -> Local Server and enable the local server.")
|
||||
return (0, None)
|
||||
|
||||
def checkDevVersionOfGNS3(self):
|
||||
"""Checking for stable GNS3 version"""
|
||||
if version.__version_info__[3] != 0:
|
||||
return (1, "You are using a unstable version of GNS3.")
|
||||
return (0, None)
|
||||
|
||||
def checkExperimentalFeaturesEnabled(self):
|
||||
"""Checking if experimental features are not enabled"""
|
||||
if LocalConfig.instance().experimental():
|
||||
return (1, "Experimental features are enabled. Turn them off by going to Preferences -> General -> Miscellaneous.")
|
||||
return (0, None)
|
||||
|
||||
def checkAVGInstalled(self):
|
||||
"""Checking if AVG software is not installed"""
|
||||
|
||||
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):
|
||||
"""Checking for amount of free virtual memory"""
|
||||
|
||||
if int(psutil.virtual_memory().available / (1024 * 1024)) < 600:
|
||||
return (2, "You have less than 600MB of available virtual memory, this could prevent nodes to start")
|
||||
return (0, None)
|
||||
|
||||
def checkVmrun(self):
|
||||
"""Checking if vmrun is installed"""
|
||||
vmrun = VMware.instance().findVmrun()
|
||||
if len(vmrun) == 0:
|
||||
return (1, "The vmrun executable could not be found, VMware VMs cannot be used")
|
||||
return (0, None)
|
||||
|
||||
def check64Bit(self):
|
||||
"""Check if processor is 64 bit"""
|
||||
if platform.architecture()[0] != "64bit":
|
||||
return (2, "The architecture {} is not supported.".format(platform.architecture()[0]))
|
||||
return (0, None)
|
||||
|
||||
def checkUbridgePermission(self):
|
||||
"""Check if ubridge has the correct permission"""
|
||||
if not sys.platform.startswith("win") and os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return (0, None)
|
||||
|
||||
path = LocalServer.instance().localServerSettings().get("ubridge_path")
|
||||
if path is None:
|
||||
return (0, None)
|
||||
if not os.path.exists(path):
|
||||
return (2, "Ubridge path {path} doesn't exists".format(path=path))
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
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"):
|
||||
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):
|
||||
"""Check if dynamips has the correct permission"""
|
||||
if not sys.platform.startswith("win") and os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return (0, None)
|
||||
|
||||
path = LocalServer.instance().localServerSettings().get("dynamips_path")
|
||||
if path is None:
|
||||
return (0, None)
|
||||
if not os.path.exists(path):
|
||||
return (2, "Dynamips path {path} doesn't exists".format(path=path))
|
||||
|
||||
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):
|
||||
"""Check if gns3 is not installed twice"""
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
return (0, None)
|
||||
|
||||
try:
|
||||
if os.path.exists("/usr/local/bin/gns3server") and os.path.exists("/usr/bin/gns3server"):
|
||||
return (2, "GNS3 is installed twice please remove it from /usr/local/bin")
|
||||
except OSError:
|
||||
pass
|
||||
return (0, None)
|
||||
|
||||
def _checkWindowsService(self, service_name):
|
||||
|
||||
import pywintypes
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
try:
|
||||
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
|
||||
return False
|
||||
except pywintypes.error as e:
|
||||
if e.winerror == 1060:
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
return True
|
||||
|
||||
def checkRPFServiceIsRunning(self):
|
||||
"""Check if the RPF service is running (required to use Ethernet NIOs)"""
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
return (0, None)
|
||||
|
||||
import pywintypes
|
||||
try:
|
||||
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
|
||||
return (2, "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot")
|
||||
except pywintypes.error as e:
|
||||
return (2, "Could not check if the NPF or Npcap service is running: {}".format(e.strerror))
|
||||
|
||||
return (0, None)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = DoctorDialog(main, console=True)
|
||||
# dialog.show()
|
||||
#exit_code = app.exec_()
|
||||
121
gns3/dialogs/edit_compute_dialog.py
Normal file
121
gns3/dialogs/edit_compute_dialog.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.compute import Compute
|
||||
from gns3.ui.edit_compute_dialog_ui import Ui_EditComputeDialog
|
||||
|
||||
|
||||
class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
|
||||
"""
|
||||
New compute dialog.
|
||||
|
||||
:param parent: parent widget.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, compute=None):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiEnableAuthenticationCheckBox.toggled.connect(self._enableAuthenticationSlot)
|
||||
self._compute = compute
|
||||
if self._compute:
|
||||
self.uiServerNameLineEdit.setText(self._compute.name())
|
||||
self.uiServerHostLineEdit.setText(self._compute.host())
|
||||
self.uiServerPortSpinBox.setValue(self._compute.port())
|
||||
|
||||
if self._compute.user():
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(True)
|
||||
self.uiServerUserLineEdit.setText(self._compute.user())
|
||||
else:
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(False)
|
||||
self.uiWarningLabel.setVisible(False)
|
||||
else:
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(False)
|
||||
self.uiWarningLabel.setVisible(False)
|
||||
self._enableAuthenticationSlot(self.uiEnableAuthenticationCheckBox.isChecked())
|
||||
|
||||
def _enableAuthenticationSlot(self, state):
|
||||
"""
|
||||
Slot to enable or not the authentication.
|
||||
"""
|
||||
|
||||
if self.uiEnableAuthenticationCheckBox.isChecked():
|
||||
self.uiServerUserLineEdit.setVisible(True)
|
||||
self.uiServerPasswordLineEdit.setVisible(True)
|
||||
self.uiServerUserLabel.setVisible(True)
|
||||
self.uiServerPasswordLabel.setVisible(True)
|
||||
else:
|
||||
self.uiServerUserLineEdit.setVisible(False)
|
||||
self.uiServerPasswordLineEdit.setVisible(False)
|
||||
self.uiServerUserLabel.setVisible(False)
|
||||
self.uiServerPasswordLabel.setVisible(False)
|
||||
|
||||
def compute(self):
|
||||
return self._compute
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
Adds a new remote compute.
|
||||
"""
|
||||
|
||||
host = self.uiServerHostLineEdit.text().strip()
|
||||
name = self.uiServerNameLineEdit.text().strip()
|
||||
protocol = "http"
|
||||
port = self.uiServerPortSpinBox.value()
|
||||
user = self.uiServerUserLineEdit.text().strip()
|
||||
password = self.uiServerPasswordLineEdit.text().strip()
|
||||
|
||||
if not re.match(r"^[a-zA-Z0-9\.{}-]+$".format("\u0370-\u1CDF\u2C00-\u30FF\u4E00-\u9FBF"), host):
|
||||
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server hostname {}".format(host))
|
||||
return
|
||||
if name == "gns3vm":
|
||||
QtWidgets.QMessageBox.critical(self, "Remote compute", "{} is a reserved name".format(name))
|
||||
return
|
||||
if len(name) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server name {}".format(name))
|
||||
return
|
||||
if port is None or port < 1:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server port {}".format(port))
|
||||
return
|
||||
|
||||
if not self._compute:
|
||||
self._compute = Compute()
|
||||
self._compute.setName(name)
|
||||
self._compute.setProtocol(protocol)
|
||||
self._compute.setHost(host)
|
||||
self._compute.setPort(port)
|
||||
if self.uiEnableAuthenticationCheckBox.isChecked():
|
||||
self._compute.setUser(user)
|
||||
self._compute.setPassword(password)
|
||||
else:
|
||||
self._compute.setUser(None)
|
||||
self._compute.setPassword(None)
|
||||
|
||||
super().accept()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = EditComputeDialog(main)
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
119
gns3/dialogs/edit_project_dialog.py
Normal file
119
gns3/dialogs/edit_project_dialog.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtWidgets, QtCore, qslot, qpartial
|
||||
from ..topology import Topology
|
||||
from ..ui.edit_project_dialog_ui import Ui_EditProjectDialog
|
||||
|
||||
|
||||
class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
"""
|
||||
Edit current project settings
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self._project = Topology.instance().project()
|
||||
self.uiProjectNameLineEdit.setText(self._project.name())
|
||||
self.uiProjectAutoOpenCheckBox.setChecked(self._project.autoOpen())
|
||||
self.uiProjectAutoCloseCheckBox.setChecked(not self._project.autoClose())
|
||||
self.uiProjectAutoStartCheckBox.setChecked(self._project.autoStart())
|
||||
self.uiSceneWidthSpinBox.setValue(self._project.sceneWidth())
|
||||
self.uiSceneHeightSpinBox.setValue(self._project.sceneHeight())
|
||||
self.uiGridSizeSpinBox.setValue(self._project.gridSize())
|
||||
|
||||
self.uiGlobalVariablesGrid.setAlignment(QtCore.Qt.AlignTop)
|
||||
|
||||
self.uiNewVarButton = QtWidgets.QPushButton('Add new variable', self)
|
||||
self.uiNewVarButton.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
self.uiNewVarButton.clicked.connect(self.onAddNewVariable)
|
||||
self.uiGlobalVariablesGrid.addWidget(self.uiNewVarButton, 0, 3, QtCore.Qt.AlignRight)
|
||||
|
||||
self._variables = self.setUpVariables()
|
||||
self.updateGlobalVariables()
|
||||
|
||||
def setUpVariables(self):
|
||||
new_variable = {"name": "", "value": ""}
|
||||
variables = self._project.variables()
|
||||
|
||||
if variables is not None:
|
||||
variables.append(new_variable)
|
||||
else:
|
||||
variables = [new_variable]
|
||||
return variables
|
||||
|
||||
def updateGlobalVariables(self):
|
||||
while True:
|
||||
item = self.uiGlobalVariablesGrid.takeAt(1)
|
||||
if item is None:
|
||||
break
|
||||
elif item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
for i, variable in enumerate(self._variables, start=1):
|
||||
nameLabel = QtWidgets.QLabel()
|
||||
nameLabel.setText("Name:")
|
||||
self.uiGlobalVariablesGrid.addWidget(nameLabel, i, 0)
|
||||
|
||||
nameEdit = QtWidgets.QLineEdit()
|
||||
nameEdit.setText(variable.get("name", ""))
|
||||
nameEdit.textChanged.connect(qpartial(self.onNameChange, variable))
|
||||
self.uiGlobalVariablesGrid.addWidget(nameEdit, i, 1)
|
||||
|
||||
valueLabel = QtWidgets.QLabel()
|
||||
valueLabel.setText("Value:")
|
||||
self.uiGlobalVariablesGrid.addWidget(valueLabel, i, 2)
|
||||
|
||||
valueEdit = QtWidgets.QLineEdit()
|
||||
valueEdit.setText(variable.get("value", ""))
|
||||
valueEdit.textChanged.connect(qpartial(self.onValueChange, variable))
|
||||
self.uiGlobalVariablesGrid.addWidget(valueEdit, i, 3)
|
||||
|
||||
@qslot
|
||||
def onAddNewVariable(self, event):
|
||||
self._variables += [{"name": "", "value": ""}]
|
||||
self.updateGlobalVariables()
|
||||
|
||||
def onNameChange(self, variable, text):
|
||||
variable["name"] = text
|
||||
|
||||
def onValueChange(self, variable, text):
|
||||
variable["value"] = text
|
||||
|
||||
def _cleanVariables(self):
|
||||
return [v for v in self._variables if v.get("name", "").strip() != ""]
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
Called when the dialog is closed.
|
||||
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
|
||||
if result:
|
||||
self._project.setName(self.uiProjectNameLineEdit.text())
|
||||
self._project.setAutoOpen(self.uiProjectAutoOpenCheckBox.isChecked())
|
||||
self._project.setAutoClose(not self.uiProjectAutoCloseCheckBox.isChecked())
|
||||
self._project.setAutoStart(self.uiProjectAutoStartCheckBox.isChecked())
|
||||
self._project.setSceneHeight(self.uiSceneHeightSpinBox.value())
|
||||
self._project.setSceneWidth(self.uiSceneWidthSpinBox.value())
|
||||
self._project.setGridSize(self.uiGridSizeSpinBox.value())
|
||||
self._project.setVariables(self._cleanVariables())
|
||||
self._project.update()
|
||||
super().done(result)
|
||||
@@ -15,11 +15,11 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from ..ui.exec_command_dialog_ui import Ui_ExecCommandDialog
|
||||
|
||||
|
||||
class ExecCommandDialog(QtGui.QDialog, Ui_ExecCommandDialog):
|
||||
class ExecCommandDialog(QtWidgets.QDialog, Ui_ExecCommandDialog):
|
||||
|
||||
"""
|
||||
Execute a command and display its output.
|
||||
@@ -27,7 +27,7 @@ class ExecCommandDialog(QtGui.QDialog, Ui_ExecCommandDialog):
|
||||
|
||||
def __init__(self, parent, command, params):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.setWindowTitle("Executing {}".format(command))
|
||||
@@ -57,4 +57,4 @@ class ExecCommandDialog(QtGui.QDialog, Ui_ExecCommandDialog):
|
||||
|
||||
self._process.kill()
|
||||
self._process.waitForFinished()
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
143
gns3/dialogs/export_debug_dialog.py
Normal file
143
gns3/dialogs/export_debug_dialog.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from zipfile import ZipFile
|
||||
import platform
|
||||
import psutil
|
||||
import os
|
||||
|
||||
from gns3.version import __version__
|
||||
from gns3.qt import QtWidgets, QtCore
|
||||
from gns3.ui.export_debug_dialog_ui import Ui_ExportDebugDialog
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.controller import Controller
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
"""
|
||||
This dialog allow user to export useful information
|
||||
for remote debugging by a GNS3 developers.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, project):
|
||||
|
||||
super().__init__(parent)
|
||||
self._project = project
|
||||
self.setupUi(self)
|
||||
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
if Controller.instance().isRemote():
|
||||
QtWidgets.QMessageBox.critical(self, "Debug", "Export debug information from a remote server is not supported")
|
||||
self.reject()
|
||||
return
|
||||
|
||||
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(self._path, 'w') as zip:
|
||||
zip.writestr("debug.txt", self._getDebugData())
|
||||
dir = LocalConfig.instance().configDirectory()
|
||||
for filename in os.listdir(dir):
|
||||
path = os.path.join(dir, filename)
|
||||
if os.path.isfile(path):
|
||||
zip.write(path, filename)
|
||||
|
||||
dir = 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()
|
||||
|
||||
def _getDebugData(self):
|
||||
try:
|
||||
connections = psutil.net_connections()
|
||||
# You need to be root for OSX
|
||||
except psutil.AccessDenied:
|
||||
connections = None
|
||||
|
||||
try:
|
||||
addrs = ["* {}: {}".format(key, val) for key, val in psutil.net_if_addrs().items()]
|
||||
except UnicodeDecodeError:
|
||||
addrs = ["INVALID ADDR WITH UNICODE CHARACTERS"]
|
||||
|
||||
data = """Version: {version}
|
||||
OS: {os}
|
||||
Python: {python}
|
||||
Qt: {qt}
|
||||
PyQt: {pyqt}
|
||||
CPU: {cpu}
|
||||
Memory: {memory}
|
||||
|
||||
Networks:
|
||||
{addrs}
|
||||
|
||||
Open connections:
|
||||
{connections}
|
||||
|
||||
Processus:
|
||||
""".format(
|
||||
version=__version__,
|
||||
qt=QtCore.QT_VERSION_STR,
|
||||
pyqt=QtCore.PYQT_VERSION_STR,
|
||||
os=platform.platform(),
|
||||
python=platform.python_version(),
|
||||
memory=psutil.virtual_memory(),
|
||||
cpu=psutil.cpu_times(),
|
||||
connections=connections,
|
||||
addrs="\n".join(addrs)
|
||||
)
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
psinfo = proc.as_dict(attrs=["name", "exe"])
|
||||
data += "* {} {}\n".format(psinfo["name"], psinfo["exe"])
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
return data
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
print(ExportDebugDialog(None)._getDebugData())
|
||||
72
gns3/dialogs/file_editor_dialog.py
Normal file
72
gns3/dialogs/file_editor_dialog.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.file_editor_dialog_ui import Ui_FileEditorDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
|
||||
"""
|
||||
This dialog allow user to detect error in his GNS3 installation.
|
||||
|
||||
If you want to add a test add a method starting by check. The
|
||||
check return a tuple result and a message in case of failure.
|
||||
"""
|
||||
|
||||
def __init__(self, target, path, parent=None, default=""):
|
||||
|
||||
if parent is None:
|
||||
from gns3.main_window import MainWindow
|
||||
parent = MainWindow.instance()
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._target = target
|
||||
self._path = path
|
||||
self._default = default
|
||||
|
||||
self.setWindowTitle(target.name() + " " + os.path.basename(path))
|
||||
|
||||
self.uiRefreshButton.pressed.connect(self._refreshSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Save).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
||||
|
||||
self._refreshSlot()
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
text = self.uiFileTextEdit.toPlainText()
|
||||
self._target.post("/files/" + self._path, self._saveCallback, body=text)
|
||||
|
||||
def _saveCallback(self, result, error=False, **kwargs):
|
||||
if not error:
|
||||
self.accept()
|
||||
|
||||
def _refreshSlot(self):
|
||||
self._target.get("/files/" + self._path, self._getCallback)
|
||||
|
||||
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
|
||||
if not error:
|
||||
self.uiFileTextEdit.setText(raw_body.decode("utf-8"))
|
||||
elif result.get("status") == 404:
|
||||
if self._default:
|
||||
self.uiFileTextEdit.setText(self._default)
|
||||
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)
|
||||
@@ -18,12 +18,12 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from ..qt import QtGui
|
||||
from ..qt import QtWidgets
|
||||
from ..topology import Topology
|
||||
from ..ui.idlepc_dialog_ui import Ui_IdlePCDialog
|
||||
|
||||
|
||||
class IdlePCDialog(QtGui.QDialog, Ui_IdlePCDialog):
|
||||
class IdlePCDialog(QtWidgets.QDialog, Ui_IdlePCDialog):
|
||||
|
||||
"""
|
||||
Idle-PC dialog.
|
||||
@@ -31,16 +31,17 @@ class IdlePCDialog(QtGui.QDialog, Ui_IdlePCDialog):
|
||||
|
||||
def __init__(self, router, idlepcs, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applySlot)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Help).clicked.connect(self._helpSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applySlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpSlot)
|
||||
|
||||
self._router = router
|
||||
self._idlepcs = idlepcs
|
||||
|
||||
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))
|
||||
@@ -59,8 +60,7 @@ Finding the right idle-pc value is a trial and error process, consisting of appl
|
||||
|
||||
Select each value that appears in the list and click Apply, and note the CPU usage a few moments later. When you have found the value that minimises the CPU usage, apply that value.
|
||||
"""
|
||||
|
||||
QtGui.QMessageBox.information(self, "Hints for Idle-PC", help_text)
|
||||
QtWidgets.QMessageBox.information(self, "Hints for Idle-PC", help_text)
|
||||
|
||||
def _applySlot(self):
|
||||
"""
|
||||
@@ -68,7 +68,7 @@ Select each value that appears in the list and click Apply, and note the CPU usa
|
||||
"""
|
||||
|
||||
if not self.uiComboBox.count():
|
||||
QtGui.QMessageBox.critical(self, "Idle-PC", "Sorry could not find a valid Idle-PC value, please check again with Cisco IOS in a different state")
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC", "Sorry could not find a valid Idle-PC value, please check again with Cisco IOS in a different state")
|
||||
return
|
||||
|
||||
idlepc = self.uiComboBox.itemData(self.uiComboBox.currentIndex())
|
||||
@@ -90,4 +90,4 @@ Select each value that appears in the list and click Apply, and note the CPU usa
|
||||
|
||||
if result:
|
||||
self._applySlot()
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
"""
|
||||
Dialog for importing cloud projects
|
||||
"""
|
||||
|
||||
from ..ui.import_cloud_project_dialog_ui import Ui_ImportCloudProjectDialog
|
||||
from ..qt import QtGui
|
||||
from ..cloud.utils import get_cloud_projects, DownloadProjectThread, DeleteProjectThread
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
|
||||
|
||||
class ImportCloudProjectDialog(QtGui.QDialog, Ui_ImportCloudProjectDialog):
|
||||
|
||||
"""
|
||||
Import cloud project dialog implementation.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, project_dest_path, images_dest_path, cloud_settings):
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.project_dest_path = project_dest_path
|
||||
self.images_dest_path = images_dest_path
|
||||
self.cloud_settings = cloud_settings
|
||||
|
||||
self.uiImportProjectAction.clicked.connect(self._importProject)
|
||||
self.uiDeleteProjectAction.clicked.connect(self._deleteProject)
|
||||
self._listCloudProjects()
|
||||
|
||||
def _listCloudProjects(self):
|
||||
self.listWidget.clear()
|
||||
self.projects = get_cloud_projects(self.cloud_settings)
|
||||
self.listWidget.addItems(list(self.projects.keys()))
|
||||
|
||||
def _importProject(self):
|
||||
project_file_name = self.projects[self.listWidget.currentItem().text()]
|
||||
|
||||
download_thread = DownloadProjectThread(
|
||||
self,
|
||||
project_file_name,
|
||||
self.project_dest_path,
|
||||
self.images_dest_path,
|
||||
self.cloud_settings
|
||||
)
|
||||
progress_dialog = ProgressDialog(download_thread, "Importing project", "Downloading project files...", "Cancel",
|
||||
parent=self.parent())
|
||||
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
self.close()
|
||||
|
||||
def _deleteProject(self):
|
||||
project_file_name = self.projects[self.listWidget.currentItem().text()]
|
||||
|
||||
button_clicked = QtGui.QMessageBox.question(
|
||||
self,
|
||||
"Delete project",
|
||||
"Are you sure you want to delete project " + self.listWidget.currentItem().text(),
|
||||
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
|
||||
QtGui.QMessageBox.Yes
|
||||
)
|
||||
|
||||
if button_clicked == QtGui.QMessageBox.Yes:
|
||||
delete_project_thread = DeleteProjectThread(self, project_file_name, self.cloud_settings)
|
||||
progress_dialog = ProgressDialog(delete_project_thread, "Deleting project", "Deleting project files...",
|
||||
"Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
self._listCloudProjects()
|
||||
121
gns3/dialogs/new_appliance_dialog.py
Normal file
121
gns3/dialogs/new_appliance_dialog.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore
|
||||
from gns3.ui.new_appliance_dialog_ui import Ui_NewApplianceDialog
|
||||
from gns3.dialogs.preferences_dialog import PreferencesDialog
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NewApplianceDialog(QtWidgets.QDialog, Ui_NewApplianceDialog):
|
||||
"""
|
||||
This dialog allow user to create a new appliance by opening
|
||||
the correct creation dialog
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiImportApplianceTemplatePushButton.clicked.connect(self._importApplianceTemplatePushButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpButtonClickedSlot)
|
||||
|
||||
def _importApplianceTemplatePushButtonClickedSlot(self):
|
||||
|
||||
self.accept()
|
||||
from gns3.main_window import MainWindow
|
||||
MainWindow.instance().openApplianceActionSlot()
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
|
||||
self.accept()
|
||||
dialog = PreferencesDialog(self.parent())
|
||||
if self.uiAddIOSRouterRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Dynamips").uiNewIOSRouterPushButton.clicked.emit(False)
|
||||
elif self.uiAddIOUDeviceRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "IOS on UNIX").uiNewIOUDevicePushButton.clicked.emit(False)
|
||||
elif self.uiAddQemuVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "QEMU").uiNewQemuVMPushButton.clicked.emit(False)
|
||||
elif self.uiAddVirtualBoxVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "VirtualBox").uiNewVirtualBoxVMPushButton.clicked.emit(False)
|
||||
elif self.uiAddVMwareVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "VMware").uiNewVMwareVMPushButton.clicked.emit(False)
|
||||
elif self.uiAddDockerVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Docker").uiNewDockerVMPushButton.clicked.emit(False)
|
||||
elif self.uiAddVPCSRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "VPCS").uiNewVPCSPushButton.clicked.emit(False)
|
||||
elif self.uiAddCloudRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Cloud nodes").uiNewCloudNodePushButton.clicked.emit(False)
|
||||
elif self.uiAddEthernetHubRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Ethernet hubs").uiNewEthernetHubPushButton.clicked.emit(False)
|
||||
elif self.uiAddEthernetSwitchRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Ethernet switches").uiNewEthernetSwitchPushButton.clicked.emit(False)
|
||||
else:
|
||||
return
|
||||
dialog.exec_()
|
||||
|
||||
def _helpButtonClickedSlot(self):
|
||||
|
||||
help_text = """<html><p>This dialog helps you to add an appliance template in GNS3. In all cases you must provide your own images.</p>
|
||||
<p>You can download appliance template files (.gns3appliance) from <a href="https://gns3.com/marketplace/appliances">the GNS3 website</a></p>
|
||||
<p>A template file provides community tested settings to run a specific appliance in GNS3.</p></html>
|
||||
"""
|
||||
QtWidgets.QMessageBox.information(self, "Help for adding a new appliance template", help_text)
|
||||
|
||||
def _setPreferencesPane(self, dialog, name):
|
||||
"""
|
||||
Finds the first child of the QTreeWidgetItem name.
|
||||
|
||||
:param dialog: PreferencesDialog instance
|
||||
:param name: QTreeWidgetItem name
|
||||
|
||||
:returns: current QWidget
|
||||
"""
|
||||
|
||||
panes = dialog.uiTreeWidget.findItems(name, QtCore.Qt.MatchFixedString)
|
||||
if len(panes) > 0:
|
||||
child_pane = panes[0].child(0)
|
||||
dialog.uiTreeWidget.setCurrentItem(child_pane)
|
||||
else:
|
||||
i = 0
|
||||
root = dialog.uiTreeWidget.invisibleRootItem()
|
||||
while i < root.childCount():
|
||||
root_item = root.child(i)
|
||||
x = 0
|
||||
while x < root_item.childCount():
|
||||
item = root_item.child(x)
|
||||
x += 1
|
||||
if item.text(0) == name:
|
||||
dialog.uiTreeWidget.setCurrentItem(item)
|
||||
i += 1
|
||||
dialog.addModifiedPage(dialog.uiStackedWidget.currentWidget())
|
||||
return dialog.uiStackedWidget.currentWidget()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = NewApplianceDialog(main)
|
||||
dialog._setPreferencesPane(PreferencesDialog(main), "Ethernet hubs").uiNewEthernetHubPushButton.clicked.emit(False)
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
@@ -1,145 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..ui.new_project_dialog_ui import Ui_NewProjectDialog
|
||||
from ..settings import ENABLE_CLOUD
|
||||
|
||||
|
||||
class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
|
||||
|
||||
"""
|
||||
New project dialog.
|
||||
|
||||
:param parent: parent widget.
|
||||
:param showed_from_startup: boolean to indicate if this dialog
|
||||
has been opened automatically when GNS3 started.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, showed_from_startup=False):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = parent
|
||||
self._project_settings = {}
|
||||
default_project_name = "untitled"
|
||||
self.uiNameLineEdit.setText(default_project_name)
|
||||
self.uiLocationLineEdit.setText(os.path.join(self._main_window.projectsDirPath(), default_project_name))
|
||||
|
||||
self.uiNameLineEdit.textEdited.connect(self._projectNameSlot)
|
||||
self.uiLocationBrowserToolButton.clicked.connect(self._projectPathSlot)
|
||||
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
|
||||
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
|
||||
if not ENABLE_CLOUD:
|
||||
self.uiCloudRadioButton.hide()
|
||||
|
||||
if not showed_from_startup:
|
||||
self.uiOpenProjectPushButton.hide()
|
||||
self.uiRecentProjectsPushButton.hide()
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
"""
|
||||
Event handler in order to properly handle escape.
|
||||
"""
|
||||
|
||||
if e.key() == QtCore.Qt.Key_Escape:
|
||||
self.close()
|
||||
|
||||
def _projectNameSlot(self, text):
|
||||
|
||||
project_dir = self._main_window.projectsDirPath()
|
||||
if os.path.dirname(self.uiLocationLineEdit.text()) == project_dir:
|
||||
self.uiLocationLineEdit.setText(os.path.join(project_dir, text))
|
||||
|
||||
def _projectPathSlot(self):
|
||||
"""
|
||||
Slot to select the a new project location.
|
||||
"""
|
||||
|
||||
path = QtGui.QFileDialog.getSaveFileName(self, "Project location", os.path.join(self._main_window.projectsDirPath(),
|
||||
self.uiNameLineEdit.text()))
|
||||
if path:
|
||||
self.uiLocationLineEdit.setText(path)
|
||||
|
||||
def getNewProjectSettings(self):
|
||||
|
||||
return self._project_settings
|
||||
|
||||
def _menuTriggeredSlot(self, action):
|
||||
"""
|
||||
Closes this dialog when a recent project
|
||||
has been opened.
|
||||
|
||||
:param action: ignored.
|
||||
"""
|
||||
|
||||
self.reject()
|
||||
|
||||
def _openProjectActionSlot(self):
|
||||
"""
|
||||
Opens a project and closes this dialog.
|
||||
"""
|
||||
|
||||
self._main_window.openProjectActionSlot()
|
||||
self.reject()
|
||||
|
||||
def _showRecentProjectsSlot(self):
|
||||
"""
|
||||
lot to show all the recent projects in a menu.
|
||||
"""
|
||||
|
||||
menu = QtGui.QMenu()
|
||||
menu.triggered.connect(self._menuTriggeredSlot)
|
||||
for action in self._main_window._recent_file_actions:
|
||||
menu.addAction(action)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
def done(self, result):
|
||||
|
||||
if result:
|
||||
project_name = self.uiNameLineEdit.text()
|
||||
project_location = self.uiLocationLineEdit.text()
|
||||
if self.uiCloudRadioButton.isChecked():
|
||||
project_type = "cloud"
|
||||
else:
|
||||
project_type = "local"
|
||||
|
||||
if not project_name:
|
||||
QtGui.QMessageBox.critical(self, "New project", "Project name is empty")
|
||||
return
|
||||
|
||||
if not project_location:
|
||||
QtGui.QMessageBox.critical(self, "New project", "Project location is empty")
|
||||
return
|
||||
|
||||
if os.path.isdir(project_location):
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
"New project",
|
||||
"Location {} already exists, overwrite it?".format(project_location),
|
||||
QtGui.QMessageBox.Yes,
|
||||
QtGui.QMessageBox.No)
|
||||
if reply == QtGui.QMessageBox.No:
|
||||
return
|
||||
|
||||
self._project_settings["project_name"] = project_name
|
||||
self._project_settings["project_path"] = os.path.join(project_location, project_name + ".gns3")
|
||||
self._project_settings["project_files_dir"] = project_location
|
||||
self._project_settings["project_type"] = project_type
|
||||
|
||||
QtGui.QDialog.done(self, result)
|
||||
@@ -19,17 +19,14 @@
|
||||
Dialog to configure and update node settings using widget pages.
|
||||
"""
|
||||
|
||||
from gns3.http_client import HTTPClient
|
||||
from gns3.progress import Progress
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..ui.node_configurator_dialog_ui import Ui_NodeConfiguratorDialog
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..ui.node_properties_dialog_ui import Ui_NodePropertiesDialog
|
||||
|
||||
|
||||
class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
|
||||
|
||||
"""
|
||||
Node configurator implementation.
|
||||
Node properties implementation.
|
||||
|
||||
:param node_items: list of NodeItem instances
|
||||
:param parent: parent widget
|
||||
@@ -37,41 +34,44 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
|
||||
def __init__(self, node_items, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._node_items = node_items
|
||||
self._parent_items = {}
|
||||
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Reset).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(False)
|
||||
|
||||
self.previousItem = None
|
||||
self.previousPage = None
|
||||
|
||||
# load the empty page widget by default
|
||||
self.uiEmptyPageWidget = self.uiConfigStackedWidget.findChildren(QtGui.QWidget, "uiEmptyPageWidget")[0]
|
||||
self.uiEmptyPageWidget = self.uiConfigStackedWidget.findChildren(QtWidgets.QWidget, "uiEmptyPageWidget")[0]
|
||||
self.uiConfigStackedWidget.setCurrentWidget(self.uiEmptyPageWidget)
|
||||
|
||||
self._loadNodeItems()
|
||||
self.splitter.setSizes([250, 600])
|
||||
self._loadNodeItems()
|
||||
|
||||
self.uiNodesTreeWidget.itemClicked.connect(self.showConfigurationPageSlot)
|
||||
HTTPClient.setProgressCallback(Progress(self, min_duration=0))
|
||||
|
||||
def _loadNodeItems(self):
|
||||
"""
|
||||
Loads the nodes into the Node configurator QTreeWidget
|
||||
Loads the nodes into the Node properties QTreeWidget
|
||||
"""
|
||||
|
||||
# create the parent (group) items
|
||||
for node_item in self._node_items:
|
||||
if not node_item.node().initialized():
|
||||
continue
|
||||
|
||||
# If something of one of the displayed nodes we reload everything
|
||||
node_item.node().updated_signal.connect(self.resetSettings)
|
||||
|
||||
group_name = " {} group".format(str(node_item.node()))
|
||||
parent = group_name
|
||||
if parent not in self._parent_items:
|
||||
item = QtGui.QTreeWidgetItem(self.uiNodesTreeWidget, [group_name])
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiNodesTreeWidget, [group_name])
|
||||
item.setIcon(0, QtGui.QIcon(node_item.node().defaultSymbol()))
|
||||
item.setExpanded(True)
|
||||
self._parent_items[parent] = item
|
||||
@@ -81,11 +81,24 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
if not node_item.node().initialized():
|
||||
continue
|
||||
parent = " {} group".format(str(node_item.node()))
|
||||
item = ConfigurationPageItem(self._parent_items[parent], node_item)
|
||||
ConfigurationPageItem(self._parent_items[parent], node_item)
|
||||
|
||||
# sort the tree
|
||||
self.uiNodesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
if len(self._node_items) == 1:
|
||||
parent = " {} group".format(str(node_item.node()))
|
||||
item = self._parent_items[parent].child(0)
|
||||
item.setSelected(True)
|
||||
self.uiNodesTreeWidget.setCurrentItem(item)
|
||||
self.showConfigurationPageSlot(item, 0)
|
||||
self.splitter.setSizes([0, 600])
|
||||
elif len(self._parent_items) > 0:
|
||||
# We have multiple node we select the first group
|
||||
item = next(iter(self._parent_items.values()))
|
||||
self.uiNodesTreeWidget.setCurrentItem(item)
|
||||
self.showConfigurationPageSlot(item, 0)
|
||||
|
||||
def showConfigurationPageSlot(self, item, column):
|
||||
"""
|
||||
Shows a configuration page widget.
|
||||
@@ -122,11 +135,19 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
self.uiConfigStackedWidget.setCurrentWidget(page)
|
||||
|
||||
if page != self.uiEmptyPageWidget:
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).setEnabled(True)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Reset).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).setEnabled(True)
|
||||
else:
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Reset).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(False)
|
||||
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):
|
||||
"""
|
||||
@@ -136,18 +157,17 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
"""
|
||||
|
||||
try:
|
||||
from gns3.main_window import MainWindow
|
||||
if button == self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply):
|
||||
if button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply):
|
||||
self.applySettings()
|
||||
elif button == self.uiButtonBox.button(QtGui.QDialogButtonBox.Reset):
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset):
|
||||
self.resetSettings()
|
||||
elif button == self.uiButtonBox.button(QtGui.QDialogButtonBox.Cancel):
|
||||
HTTPClient.setProgressCallback(Progress(MainWindow.instance()))
|
||||
QtGui.QDialog.reject(self)
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help):
|
||||
self.showHelp()
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
|
||||
QtWidgets.QDialog.reject(self)
|
||||
else:
|
||||
self.applySettings()
|
||||
HTTPClient.setProgressCallback(Progress(MainWindow.instance()))
|
||||
QtGui.QDialog.accept(self)
|
||||
QtWidgets.QDialog.accept(self)
|
||||
except ConfigurationError:
|
||||
pass
|
||||
|
||||
@@ -168,12 +188,10 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
# all children for that group
|
||||
self.previousItem = None
|
||||
self.previousNode = None
|
||||
settings = item.child(0).settings().copy()
|
||||
node = item.child(0).node()
|
||||
page.saveSettings(settings, node, group=True)
|
||||
settings = page.saveSettings({}, node, group=True)
|
||||
for index in range(0, item.childCount()):
|
||||
child = item.child(index)
|
||||
# child.node().update(settings) #TODO: delete
|
||||
child.settings().update(settings)
|
||||
|
||||
# update the nodes with the settings
|
||||
@@ -207,8 +225,16 @@ class NodeConfiguratorDialog(QtGui.QDialog, Ui_NodeConfiguratorDialog):
|
||||
child = item.child(index)
|
||||
child.setSettings(child.node().settings().copy())
|
||||
|
||||
def showHelp(self):
|
||||
"""
|
||||
Show contextual help for the current page.
|
||||
"""
|
||||
|
||||
class ConfigurationPageItem(QtGui.QTreeWidgetItem):
|
||||
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):
|
||||
|
||||
"""
|
||||
Item for the QTreeWidget instance.
|
||||
@@ -221,7 +247,7 @@ class ConfigurationPageItem(QtGui.QTreeWidgetItem):
|
||||
def __init__(self, parent, node_item):
|
||||
|
||||
self._node = node_item.node()
|
||||
QtGui.QTreeWidgetItem.__init__(self, parent, [self._node.name()])
|
||||
super().__init__(parent, [self._node.name()])
|
||||
|
||||
# return the configuration page widget used to configure the node.
|
||||
self._page = self._node.configPage()
|
||||
@@ -242,7 +268,7 @@ class ConfigurationPageItem(QtGui.QTreeWidgetItem):
|
||||
|
||||
def page(self):
|
||||
"""
|
||||
Returns the page widget to be displayed by the node configurator.
|
||||
Returns the page widget to be displayed by the node properties dialog.
|
||||
|
||||
:returns: QWidget instance
|
||||
"""
|
||||
@@ -285,4 +311,4 @@ class ConfigurationError(Exception):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
Exception.__init__(self)
|
||||
super().__init__()
|
||||
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_()
|
||||
@@ -19,19 +19,19 @@
|
||||
Dialog to load module and built-in preference pages.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from ..ui.preferences_dialog_ui import Ui_PreferencesDialog
|
||||
from ..pages.server_preferences_page import ServerPreferencesPage
|
||||
from ..pages.general_preferences_page import GeneralPreferencesPage
|
||||
from ..pages.cloud_preferences_page import CloudPreferencesPage
|
||||
from ..pages.packet_capture_preferences_page import PacketCapturePreferencesPage
|
||||
from ..pages.gns3_vm_preferences_page import GNS3VMPreferencesPage
|
||||
from ..modules import MODULES
|
||||
from ..settings import ENABLE_CLOUD
|
||||
from ..http_client import HTTPClient
|
||||
from ..progress import Progress
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
|
||||
"""
|
||||
Preferences dialog implementation.
|
||||
@@ -41,17 +41,40 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi(self)
|
||||
self._modified_pages = set()
|
||||
|
||||
# We adapt the max size to the screen resolution
|
||||
# We need to manually do that otherwise on small screen the windows
|
||||
# could be bigger than the screen instead of displaying scrollbars
|
||||
height = QtWidgets.QDesktopWidget().screenGeometry().height() - 100
|
||||
width = QtWidgets.QDesktopWidget().screenGeometry().width() - 100
|
||||
|
||||
# 980 is the default width
|
||||
if self.width() > width:
|
||||
self.resize(width, self.height())
|
||||
# 680 is the default height
|
||||
if self.height() > height:
|
||||
self.resize(self.width(), height)
|
||||
|
||||
self.uiTreeWidget.currentItemChanged.connect(self._showPreferencesPageSlot)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applyPreferences)
|
||||
self._applyButton = self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply)
|
||||
self._applyButton.clicked.connect(self._applyPreferences)
|
||||
self._applyButton.setEnabled(False)
|
||||
self._applyButton.setStyleSheet("QPushButton:disabled {color: gray}")
|
||||
self._items = []
|
||||
self._loadPreferencePages()
|
||||
|
||||
# select the first available page
|
||||
self.uiTreeWidget.setCurrentItem(self._items[0])
|
||||
HTTPClient.setProgressCallback(Progress(self, min_duration=0))
|
||||
|
||||
# set the maximum width based on the content of column 0
|
||||
self.uiTreeWidget.setMaximumWidth(self.uiTreeWidget.sizeHintForColumn(0) + 10)
|
||||
|
||||
# Something has change?
|
||||
self._modified_pages = set()
|
||||
|
||||
def _loadPreferencePages(self):
|
||||
"""
|
||||
@@ -62,20 +85,20 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
pages = [
|
||||
GeneralPreferencesPage,
|
||||
ServerPreferencesPage,
|
||||
GNS3VMPreferencesPage,
|
||||
PacketCapturePreferencesPage,
|
||||
]
|
||||
if ENABLE_CLOUD:
|
||||
pages.append(CloudPreferencesPage)
|
||||
|
||||
for page in pages:
|
||||
preferences_page = page(self)
|
||||
preferences_page.loadPreferences()
|
||||
name = preferences_page.windowTitle()
|
||||
item = QtGui.QTreeWidgetItem(self.uiTreeWidget)
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiTreeWidget)
|
||||
item.setText(0, name)
|
||||
item.setData(0, QtCore.Qt.UserRole, preferences_page)
|
||||
self.uiStackedWidget.addWidget(preferences_page)
|
||||
self._items.append(item)
|
||||
self._watchForChanges(preferences_page)
|
||||
|
||||
# load module preference pages
|
||||
for module in MODULES:
|
||||
@@ -83,19 +106,64 @@ class PreferencesDialog(QtGui.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 = QtGui.QTreeWidgetItem(parent)
|
||||
item = QtWidgets.QTreeWidgetItem(parent)
|
||||
item.setText(0, name)
|
||||
item.setData(0, QtCore.Qt.UserRole, preferences_page)
|
||||
self.uiStackedWidget.addWidget(preferences_page)
|
||||
self._items.append(item)
|
||||
if cls is preference_pages[0]:
|
||||
parent = item
|
||||
self._watchForChanges(preferences_page)
|
||||
|
||||
# expand all items by default
|
||||
self.uiTreeWidget.expandAll()
|
||||
|
||||
def _watchForChanges(self, preferences_page):
|
||||
"""
|
||||
Connect all the widget of a page to check if something has change
|
||||
"""
|
||||
|
||||
# Class name, changed signal
|
||||
widget_to_watch = {
|
||||
QtWidgets.QLineEdit: "textChanged",
|
||||
QtWidgets.QPlainTextEdit: "textChanged",
|
||||
# QtWidgets.QTreeWidget: "itemChanged",
|
||||
QtWidgets.QComboBox: "currentIndexChanged",
|
||||
QtWidgets.QSpinBox: "valueChanged",
|
||||
QtWidgets.QAbstractButton: "pressed"
|
||||
}
|
||||
|
||||
for widget, signal in widget_to_watch.items():
|
||||
for children in preferences_page.findChildren(widget):
|
||||
getattr(children, signal).connect(self._preferenceChangeSlot)
|
||||
|
||||
def _preferenceChangeSlot(self, *args):
|
||||
"""
|
||||
Called when something change in the preference dialog
|
||||
"""
|
||||
|
||||
# Found the page with the change
|
||||
widget = sender = self.sender()
|
||||
while widget.parent() != self.uiStackedWidget:
|
||||
widget = widget.parent()
|
||||
|
||||
if self.addModifiedPage(widget):
|
||||
log.debug("%s value has changed", sender.objectName())
|
||||
|
||||
def addModifiedPage(self, widget):
|
||||
"""
|
||||
:returns: True is the page is initialized and element added
|
||||
"""
|
||||
# The widget can trigger signal before the end of init due to async api call
|
||||
if not hasattr(widget, 'pageInitialized') or widget.pageInitialized():
|
||||
self._applyButton.setEnabled(True)
|
||||
self._modified_pages.add(widget)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _showPreferencesPageSlot(self, current, previous):
|
||||
"""
|
||||
Shows a preference page in the current dialog.
|
||||
@@ -116,7 +184,7 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
self.uiTitleLabel.setText("{} preferences".format(name))
|
||||
index = self.uiStackedWidget.indexOf(preferences_page)
|
||||
widget = self.uiStackedWidget.widget(index)
|
||||
self.uiStackedWidget.setMinimumSize(widget.size())
|
||||
# self.uiStackedWidget.setMinimumSize(widget.size())
|
||||
self.uiStackedWidget.resize(widget.size())
|
||||
self.uiStackedWidget.setCurrentIndex(index)
|
||||
|
||||
@@ -126,12 +194,14 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
"""
|
||||
|
||||
success = True
|
||||
for item in self._items:
|
||||
preferences_page = item.data(0, QtCore.Qt.UserRole)
|
||||
for preferences_page in list(self._modified_pages):
|
||||
ok = preferences_page.savePreferences()
|
||||
# if page.savePreferences() returns None, assume success
|
||||
if ok is not None and not ok:
|
||||
success = False
|
||||
if success:
|
||||
self._applyButton.setEnabled(False)
|
||||
self._modified_pages = set()
|
||||
return success
|
||||
|
||||
def reject(self):
|
||||
@@ -139,9 +209,17 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
Closes this dialog.
|
||||
"""
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
HTTPClient.setProgressCallback(Progress(MainWindow.instance()))
|
||||
QtGui.QDialog.reject(self)
|
||||
if len(self._modified_pages) > 0:
|
||||
# Get the title of pages with modifications
|
||||
pages_title = ', '.join([page.windowTitle() for page in self._modified_pages])
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"Preferences",
|
||||
"You have unsaved preferences in {}.\n\nContinue without saving?".format(pages_title),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
QtWidgets.QDialog.reject(self)
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
@@ -149,11 +227,10 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
"""
|
||||
|
||||
# close the nodes dock to refresh the node list
|
||||
main_window = self.parentWidget()
|
||||
from ..main_window import MainWindow
|
||||
main_window = MainWindow.instance()
|
||||
main_window.uiNodesDockWidget.setVisible(False)
|
||||
main_window.uiNodesDockWidget.setWindowTitle("")
|
||||
|
||||
if self._applyPreferences():
|
||||
from gns3.main_window import MainWindow
|
||||
HTTPClient.setProgressCallback(Progress(MainWindow.instance()))
|
||||
QtGui.QDialog.accept(self)
|
||||
QtWidgets.QDialog.accept(self)
|
||||
|
||||
104
gns3/dialogs/profile_select.py
Normal file
104
gns3/dialogs/profile_select.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.ui.profile_select_dialog_ui import Ui_ProfileSelectDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
"""
|
||||
This dialog allow user to choose a profile of settings
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
if parent is None:
|
||||
self._main = QtWidgets.QMainWindow()
|
||||
self._main.hide()
|
||||
parent = self._main
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.uiNewPushButton.clicked.connect(self._newPushButtonSlot)
|
||||
self.uiDeletePushButton.clicked.connect(self._deletePushButtonSlot)
|
||||
|
||||
# Center on screen
|
||||
screen = QtWidgets.QApplication.desktop().screenGeometry()
|
||||
self.move(screen.center() - self.rect().center())
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
path = os.path.join(appdata, "GNS3")
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
path = os.path.join(home, ".config", "GNS3")
|
||||
self.profiles_path = os.path.join(path, "profiles")
|
||||
|
||||
self.uiShowAtStartupCheckBox.setChecked(LocalConfig.instance().multiProfiles())
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self):
|
||||
self.uiProfileSelectComboBox.clear()
|
||||
self.uiProfileSelectComboBox.addItem("default")
|
||||
|
||||
try:
|
||||
if os.path.exists(self.profiles_path):
|
||||
for profil in sorted(os.listdir(self.profiles_path)):
|
||||
if not profil.startswith("."):
|
||||
self.uiProfileSelectComboBox.addItem(profil)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def profile(self):
|
||||
return self.uiProfileSelectComboBox.currentText()
|
||||
|
||||
def accept(self):
|
||||
LocalConfig.instance().setMultiProfiles(self.uiShowAtStartupCheckBox.isChecked())
|
||||
super().accept()
|
||||
|
||||
def _newPushButtonSlot(self):
|
||||
profile, ok = QtWidgets.QInputDialog.getText(self.parent(), "New profile", "Profile name:")
|
||||
if ok:
|
||||
self.uiProfileSelectComboBox.addItem(profile)
|
||||
self.uiProfileSelectComboBox.setCurrentText(profile)
|
||||
self.accept()
|
||||
|
||||
def _deletePushButtonSlot(self):
|
||||
profile = self.uiProfileSelectComboBox.currentText()
|
||||
if profile == "default":
|
||||
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", "You can't delete the default profile")
|
||||
else:
|
||||
try:
|
||||
shutil.rmtree(os.path.join(self.profiles_path, profile))
|
||||
self._refresh()
|
||||
except (OSError, PermissionError) as e:
|
||||
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", str(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
dialog = ProfileSelectDialog()
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
313
gns3/dialogs/project_dialog.py
Normal file
313
gns3/dialogs/project_dialog.py
Normal file
@@ -0,0 +1,313 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qslot
|
||||
from ..ui.project_dialog_ui import Ui_ProjectDialog
|
||||
from ..controller import Controller
|
||||
from ..topology import Topology
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
|
||||
"""
|
||||
New project dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, default_project_name="untitled", show_open_options=True):
|
||||
"""
|
||||
:param parent: parent widget.
|
||||
:param default_project_name: Project name by default
|
||||
:param show_open_options: If true allow to open a project from the dialog
|
||||
otherwise it's just for create a project
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = parent
|
||||
self._project_settings = {}
|
||||
self.uiNameLineEdit.setText(default_project_name)
|
||||
self.uiLocationLineEdit.setText(os.path.join(Topology.instance().projectsDirPath(), default_project_name))
|
||||
|
||||
self.uiNameLineEdit.textEdited.connect(self._projectNameSlot)
|
||||
self.uiLocationBrowserToolButton.clicked.connect(self._projectPathSlot)
|
||||
self.uiSettingsPushButton.clicked.connect(self._settingsClickedSlot)
|
||||
|
||||
if show_open_options:
|
||||
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
|
||||
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
|
||||
else:
|
||||
self.uiOpenProjectGroupBox.hide()
|
||||
self.uiProjectTabWidget.removeTab(1)
|
||||
|
||||
# If the controller is remote we hide option for local file system
|
||||
if Controller.instance().isRemote():
|
||||
self.uiLocationLabel.setVisible(False)
|
||||
self.uiLocationLineEdit.setVisible(False)
|
||||
self.uiLocationBrowserToolButton.setVisible(False)
|
||||
self.uiOpenProjectPushButton.setVisible(False)
|
||||
|
||||
self.uiProjectsTreeWidget.itemDoubleClicked.connect(self._projectsTreeWidgetDoubleClickedSlot)
|
||||
self.uiDeleteProjectButton.clicked.connect(self._deleteProjectSlot)
|
||||
self.uiDuplicateProjectPushButton.clicked.connect(self._duplicateProjectSlot)
|
||||
self.uiRefreshProjectsPushButton.clicked.connect(Controller.instance().refreshProjectList)
|
||||
Controller.instance().project_list_updated_signal.connect(self._updateProjectListSlot)
|
||||
self._updateProjectListSlot()
|
||||
Controller.instance().refreshProjectList()
|
||||
|
||||
def _settingsClickedSlot(self):
|
||||
"""
|
||||
When the user click on the settings button
|
||||
"""
|
||||
self.reject()
|
||||
self._main_window.preferencesActionSlot()
|
||||
|
||||
def _projectsTreeWidgetDoubleClickedSlot(self, item, column):
|
||||
self.done(True)
|
||||
|
||||
@qslot
|
||||
def _deleteProjectSlot(self, *args):
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Delete project", "No project selected")
|
||||
return
|
||||
|
||||
projects_to_delete = set()
|
||||
for project in self.uiProjectsTreeWidget.selectedItems():
|
||||
project_id = project.data(0, QtCore.Qt.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"Delete project",
|
||||
'Delete project "{}"?\nThis cannot be reverted.'.format(project_name),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
projects_to_delete.add(project_id)
|
||||
|
||||
for project_id in projects_to_delete:
|
||||
Controller.instance().deleteProject(project_id)
|
||||
|
||||
def _duplicateProjectSlot(self):
|
||||
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Duplicate project", "No project selected")
|
||||
return
|
||||
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) > 1:
|
||||
QtWidgets.QMessageBox.critical(self, "Duplicate project", "Please select only one project to duplicate")
|
||||
return
|
||||
|
||||
for project in self.uiProjectsTreeWidget.selectedItems():
|
||||
project_id = project.data(0, QtCore.Qt.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
new_project_name = project_name + "-1"
|
||||
existing_project_name = [p["name"] for p in Controller.instance().projects()]
|
||||
i = 1
|
||||
while new_project_name in existing_project_name:
|
||||
new_project_name = "{}-{}".format(project_name, i)
|
||||
i += 1
|
||||
|
||||
name, reply = QtWidgets.QInputDialog.getText(self,
|
||||
"Duplicate project",
|
||||
'Duplicate project "{}"?.'.format(project_name),
|
||||
QtWidgets.QLineEdit.Normal,
|
||||
new_project_name)
|
||||
name = name.strip()
|
||||
if reply and len(name) > 0:
|
||||
if Controller.instance().isRemote():
|
||||
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
|
||||
self._duplicateCallback,
|
||||
body={"name": name},
|
||||
progressText="Duplicating project '{}'...".format(name),
|
||||
timeout=None)
|
||||
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},
|
||||
progressText="Duplicating project '{}'...".format(name),
|
||||
timeout=None)
|
||||
|
||||
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)
|
||||
self.uiProjectsTreeWidget.setUpdatesEnabled(False)
|
||||
items = []
|
||||
for project in Controller.instance().projects():
|
||||
path = os.path.join(project["path"], project["filename"])
|
||||
item = QtWidgets.QTreeWidgetItem([project["name"], project["status"], path])
|
||||
item.setData(0, QtCore.Qt.UserRole, project["project_id"])
|
||||
item.setData(1, QtCore.Qt.UserRole, project["name"])
|
||||
item.setData(2, QtCore.Qt.UserRole, path)
|
||||
items.append(item)
|
||||
self.uiProjectsTreeWidget.addTopLevelItems(items)
|
||||
|
||||
if len(Controller.instance().projects()):
|
||||
self.uiDeleteProjectButton.setEnabled(True)
|
||||
|
||||
self.uiProjectsTreeWidget.header().setResizeContentsPrecision(100) # How many row is checked for the resize for performance reason
|
||||
self.uiProjectsTreeWidget.resizeColumnToContents(0)
|
||||
self.uiProjectsTreeWidget.resizeColumnToContents(1)
|
||||
self.uiProjectsTreeWidget.resizeColumnToContents(2)
|
||||
self.uiProjectsTreeWidget.sortItems(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiProjectsTreeWidget.setUpdatesEnabled(True)
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
"""
|
||||
Event handler in order to properly handle escape.
|
||||
"""
|
||||
|
||||
if e.key() == QtCore.Qt.Key_Escape:
|
||||
self.close()
|
||||
|
||||
def _projectNameSlot(self, text):
|
||||
|
||||
project_dir = Topology.instance().projectsDirPath()
|
||||
if os.path.dirname(self.uiLocationLineEdit.text()) == project_dir:
|
||||
self.uiLocationLineEdit.setText(os.path.join(project_dir, text))
|
||||
|
||||
def _projectPathSlot(self):
|
||||
"""
|
||||
Slot to select the a new project location.
|
||||
"""
|
||||
|
||||
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Project location", os.path.join(Topology.instance().projectsDirPath(),
|
||||
self.uiNameLineEdit.text()))
|
||||
|
||||
if path:
|
||||
self.uiNameLineEdit.setText(os.path.basename(path))
|
||||
self.uiLocationLineEdit.setText(path)
|
||||
|
||||
def getProjectSettings(self):
|
||||
|
||||
return self._project_settings
|
||||
|
||||
def _menuTriggeredSlot(self, action):
|
||||
"""
|
||||
Closes this dialog when a recent project
|
||||
has been opened.
|
||||
|
||||
:param action: ignored.
|
||||
"""
|
||||
|
||||
self.reject()
|
||||
|
||||
def _openProjectActionSlot(self):
|
||||
"""
|
||||
Opens a project and closes this dialog.
|
||||
"""
|
||||
|
||||
self._main_window.openProjectActionSlot()
|
||||
self.reject()
|
||||
|
||||
def _showRecentProjectsSlot(self):
|
||||
"""
|
||||
lot to show all the recent projects in a menu.
|
||||
"""
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.triggered.connect(self._menuTriggeredSlot)
|
||||
if Controller.instance().isRemote():
|
||||
for action in self._main_window.recent_project_actions:
|
||||
menu.addAction(action)
|
||||
else:
|
||||
for action in self._main_window.recent_file_actions:
|
||||
menu.addAction(action)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
def _overwriteProjectCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
# A 404 could arrive if someone else as deleted the project
|
||||
if "status" not in result or result["status"] != 404:
|
||||
return
|
||||
elif "message" in result:
|
||||
QtWidgets.QMessageBox.critical(self,
|
||||
"New Project",
|
||||
"Error while overwrite project: {}".format(result["message"]))
|
||||
Controller.instance().refreshProjectList()
|
||||
self.done(True)
|
||||
|
||||
def _newProject(self):
|
||||
self._project_settings["project_name"] = self.uiNameLineEdit.text().strip()
|
||||
if Controller.instance().isRemote():
|
||||
self._project_settings.pop("project_path", None)
|
||||
self._project_settings.pop("project_files_dir", None)
|
||||
else:
|
||||
project_location = self.uiLocationLineEdit.text().strip()
|
||||
if not project_location:
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project location is empty")
|
||||
return False
|
||||
|
||||
self._project_settings["project_path"] = os.path.join(project_location, self._project_settings["project_name"] + ".gns3")
|
||||
self._project_settings["project_files_dir"] = project_location
|
||||
|
||||
if len(self._project_settings["project_name"]) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project name is empty")
|
||||
return False
|
||||
|
||||
for existing_project in Controller.instance().projects():
|
||||
if self._project_settings["project_name"] == existing_project["name"] \
|
||||
or ("project_files_dir" in self._project_settings and self._project_settings["project_files_dir"] == existing_project["path"]):
|
||||
|
||||
if existing_project["status"] == "opened":
|
||||
QtWidgets.QMessageBox.critical(self,
|
||||
"New project",
|
||||
"Project {} is open you can not overwrite it".format(self._project_settings["project_name"]))
|
||||
return False
|
||||
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"New project",
|
||||
"Project {} already exists, overwrite it?".format(existing_project["name"]),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
Controller.instance().deleteProject(existing_project["project_id"], self._overwriteProjectCallback)
|
||||
|
||||
# In all cases we cancel the new project and if project success to delete
|
||||
# we will call done again
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def done(self, result):
|
||||
|
||||
if result:
|
||||
if self.uiProjectTabWidget.currentIndex() == 0:
|
||||
if not self._newProject():
|
||||
return
|
||||
else:
|
||||
current = self.uiProjectsTreeWidget.currentItem()
|
||||
if current is None:
|
||||
QtWidgets.QMessageBox.critical(self, "Open project", "No project selected")
|
||||
return
|
||||
|
||||
self._project_settings["project_id"] = current.data(0, QtCore.Qt.UserRole)
|
||||
self._project_settings["project_name"] = current.data(1, QtCore.Qt.UserRole)
|
||||
super().done(result)
|
||||
88
gns3/dialogs/project_welcome_dialog.py
Normal file
88
gns3/dialogs/project_welcome_dialog.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 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 copy
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore, qpartial
|
||||
from gns3.ui.project_welcome_dialog_ui import Ui_ProjectWelcomeDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
|
||||
"""
|
||||
This dialog shows when project is imported and global variables assigned to the project are missing.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, project):
|
||||
|
||||
super().__init__(parent)
|
||||
self._project = project
|
||||
self.setupUi(self)
|
||||
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
|
||||
self.gridLayout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.label.setOpenExternalLinks(True)
|
||||
|
||||
self._variables = self._getVariables(project)
|
||||
|
||||
self._loadReadme()
|
||||
self._addMisingVariablesEdits()
|
||||
|
||||
def _getVariables(self, project):
|
||||
variables = copy.copy(self._project.variables())
|
||||
if variables is None:
|
||||
variables = []
|
||||
return variables
|
||||
|
||||
def _addMisingVariablesEdits(self):
|
||||
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
|
||||
for i, variable in enumerate(missing, start=0):
|
||||
nameLabel = QtWidgets.QLabel()
|
||||
nameLabel.setText(variable.get("name", ""))
|
||||
self.gridLayout.addWidget(nameLabel, i, 0)
|
||||
|
||||
valueEdit = QtWidgets.QLineEdit()
|
||||
valueEdit.setText(variable.get("value", ""))
|
||||
valueEdit.textChanged.connect(qpartial(self.onValueChange, variable))
|
||||
self.gridLayout.addWidget(valueEdit, i, 1)
|
||||
|
||||
def _loadReadme(self):
|
||||
self._project.get("/files/README.txt", self._loadedReadme)
|
||||
|
||||
def _loadedReadme(self, result, error=False, raw_body=None, context={}, **kwargs):
|
||||
if not error:
|
||||
self.label.setText(raw_body.decode("utf-8"))
|
||||
|
||||
def onValueChange(self, variable, text):
|
||||
variable["value"] = text
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
|
||||
if len(missing) > 0:
|
||||
reply = QtWidgets.QMessageBox.warning(
|
||||
self, 'Missing values',
|
||||
'Are you sure you want to continue without providing missing values?',
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
self._project.setVariables(self._variables)
|
||||
self._project.update()
|
||||
self.accept()
|
||||
|
||||
432
gns3/dialogs/setup_wizard.py
Normal file
432
gns3/dialogs/setup_wizard.py
Normal file
@@ -0,0 +1,432 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets, QtGui, QtNetwork, qslot
|
||||
from gns3.controller import Controller
|
||||
from gns3.local_server import LocalServer
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
|
||||
|
||||
from ..settings import DEFAULT_LOCAL_SERVER_HOST
|
||||
from ..ui.setup_wizard_ui import Ui_SetupWizard
|
||||
from ..version import __version__
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
"""
|
||||
Base class for VM wizard.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._gns3_vm_settings = {
|
||||
"enable": True,
|
||||
"headless": False,
|
||||
"when_exit": "stop",
|
||||
"engine": "vmware",
|
||||
"vcpus": 1,
|
||||
"ram": 2048,
|
||||
"vmname": "GNS3 VM"
|
||||
}
|
||||
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
|
||||
|
||||
self.uiLocalServerToolButton.clicked.connect(self._localServerBrowserSlot)
|
||||
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('')
|
||||
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
|
||||
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
|
||||
self.uiVirtualBoxRadioButton.clicked.connect(self._listVirtualBoxVMsSlot)
|
||||
self.uiVMwareBannerButton.clicked.connect(self._VMwareBannerButtonClickedSlot)
|
||||
settings = parent.settings()
|
||||
self.uiShowCheckBox.setChecked(settings["hide_setup_wizard"])
|
||||
|
||||
# by default all radio buttons are unchecked
|
||||
self.uiVmwareRadioButton.setAutoExclusive(False)
|
||||
self.uiVirtualBoxRadioButton.setAutoExclusive(False)
|
||||
self.uiVmwareRadioButton.setChecked(False)
|
||||
self.uiVirtualBoxRadioButton.setChecked(False)
|
||||
|
||||
# Mandatory fields
|
||||
self.uiLocalServerWizardPage.registerField("path*", self.uiLocalServerPathLineEdit)
|
||||
|
||||
# load all available addresses
|
||||
for address in QtNetwork.QNetworkInterface.allAddresses():
|
||||
address_string = address.toString()
|
||||
if address.protocol() != QtNetwork.QAbstractSocket.IPv6Protocol:
|
||||
self.uiLocalServerHostComboBox.addItem(address_string, address.toString())
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.png"))
|
||||
else:
|
||||
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")
|
||||
self.uiLocalRadioButton.setText("Run the topologies on my computer")
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
self.uiLocalLabel.setVisible(False)
|
||||
|
||||
Controller.instance().connected_signal.connect(self._refreshLocalServerStatusSlot)
|
||||
Controller.instance().connection_failed_signal.connect(self._refreshLocalServerStatusSlot)
|
||||
|
||||
def _localServerBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select a local server.
|
||||
"""
|
||||
|
||||
filter = ""
|
||||
if sys.platform.startswith("win"):
|
||||
filter = "Executable (*.exe);;All files (*.*)"
|
||||
server_path = shutil.which("gns3server")
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select the local server", server_path, filter)
|
||||
if not path:
|
||||
return
|
||||
|
||||
self.uiLocalServerPathLineEdit.setText(path)
|
||||
|
||||
def _VMwareBannerButtonClickedSlot(self):
|
||||
if sys.platform.startswith("darwin"):
|
||||
url = "http://send.onenetworkdirect.net/z/621394/CD225091/"
|
||||
else:
|
||||
url = "http://send.onenetworkdirect.net/z/616207/CD225091/"
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
|
||||
|
||||
def _listVMwareVMsSlot(self):
|
||||
"""
|
||||
Slot to refresh the VMware VMs list.
|
||||
"""
|
||||
|
||||
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{version}.zip".format(version=__version__)
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('The GNS3 VM can <a href="{download_url}">downloaded here</a>.<br>Import the VM in your virtualization software and hit refresh.'.format(download_url=download_url))
|
||||
self.uiVirtualBoxRadioButton.setChecked(False)
|
||||
from gns3.modules import VMware
|
||||
settings = VMware.instance().settings()
|
||||
if not os.path.exists(settings["vmrun_path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed. You can download it from https://www.vmware.com/support/developer/vix-api/. After installation you need to restart GNS3.")
|
||||
return
|
||||
self._refreshVMListSlot()
|
||||
|
||||
def _listVirtualBoxVMsSlot(self):
|
||||
"""
|
||||
Slot to refresh the VirtualBox VMs list.
|
||||
"""
|
||||
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM on VirtualBox", "VirtualBox doesn't support nested virtualization, this means running Qemu based VM could be very slow")
|
||||
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format(version=__version__)
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('If you don\'t have the GNS3 Virtual Machine you can <a href="{download_url}">download it here</a>.<br>And import the VM in the virtualization software and hit refresh.'.format(download_url=download_url))
|
||||
self.uiVmwareRadioButton.setChecked(False)
|
||||
from gns3.modules import VirtualBox
|
||||
settings = VirtualBox.instance().settings()
|
||||
if not os.path.exists(settings["vboxmanage_path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "VirtualBox", "VBoxManage could not be found, VirtualBox is probably not installed. After installation you need to restart GNS3.")
|
||||
return
|
||||
self._refreshVMListSlot()
|
||||
|
||||
def _setPreferencesPane(self, dialog, name):
|
||||
"""
|
||||
Finds the first child of the QTreeWidgetItem name.
|
||||
|
||||
:param dialog: PreferencesDialog instance
|
||||
:param name: QTreeWidgetItem name
|
||||
|
||||
:returns: current QWidget
|
||||
"""
|
||||
|
||||
pane = dialog.uiTreeWidget.findItems(name, QtCore.Qt.MatchFixedString)[0]
|
||||
child_pane = pane.child(0)
|
||||
dialog.uiTreeWidget.setCurrentItem(child_pane)
|
||||
return dialog.uiStackedWidget.currentWidget()
|
||||
|
||||
def _getSettingsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while get gettings: {}".format(result["message"]))
|
||||
return
|
||||
self._gns3_vm_settings = result
|
||||
|
||||
def initializePage(self, page_id):
|
||||
"""
|
||||
Initialize Wizard pages.
|
||||
|
||||
:param page_id: page identifier
|
||||
"""
|
||||
|
||||
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":
|
||||
self.uiVmwareRadioButton.setChecked(True)
|
||||
self._listVMwareVMsSlot()
|
||||
elif self._GNS3VMSettings()["engine"] == "virtualbox":
|
||||
self.uiVirtualBoxRadioButton.setChecked(True)
|
||||
self._listVirtualBoxVMsSlot()
|
||||
self.uiCPUSpinBox.setValue(self._GNS3VMSettings()["vcpus"])
|
||||
self.uiRAMSpinBox.setValue(self._GNS3VMSettings()["ram"])
|
||||
|
||||
elif self.page(page_id) == self.uiLocalServerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self.uiLocalServerPathLineEdit.setText(local_server_settings["path"])
|
||||
index = self.uiLocalServerHostComboBox.findData(local_server_settings["host"])
|
||||
if index != -1:
|
||||
self.uiLocalServerHostComboBox.setCurrentIndex(index)
|
||||
self.uiLocalServerPortSpinBox.setValue(local_server_settings["port"])
|
||||
|
||||
elif self.page(page_id) == self.uiRemoteControllerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
if local_server_settings["host"] is None:
|
||||
self.uiRemoteMainServerHostLineEdit.setText(DEFAULT_LOCAL_SERVER_HOST)
|
||||
self.uiRemoteMainServerAuthCheckBox.setChecked(False)
|
||||
self.uiRemoteMainServerUserLineEdit.setText("")
|
||||
self.uiRemoteMainServerPasswordLineEdit.setText("")
|
||||
else:
|
||||
self.uiRemoteMainServerHostLineEdit.setText(local_server_settings["host"])
|
||||
self.uiRemoteMainServerAuthCheckBox.setChecked(local_server_settings["auth"])
|
||||
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["user"])
|
||||
self.uiRemoteMainServerPasswordLineEdit.setText(local_server_settings["password"])
|
||||
self.uiRemoteMainServerPortSpinBox.setValue(local_server_settings["port"])
|
||||
elif self.page(page_id) == self.uiLocalServerStatusWizardPage:
|
||||
self._refreshLocalServerStatusSlot()
|
||||
|
||||
elif self.page(page_id) == self.uiSummaryWizardPage:
|
||||
self.uiSummaryTreeWidget.clear()
|
||||
if self.uiLocalRadioButton.isChecked():
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self._addSummaryEntry("Server type:", "Local")
|
||||
self._addSummaryEntry("Path:", local_server_settings["path"])
|
||||
self._addSummaryEntry("Host:", local_server_settings["host"])
|
||||
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
|
||||
elif self.uiRemoteControllerRadioButton.isChecked():
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self._addSummaryEntry("Server type:", "Remote")
|
||||
self._addSummaryEntry("Host:", local_server_settings["host"])
|
||||
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
|
||||
self._addSummaryEntry("User:", local_server_settings["user"])
|
||||
else:
|
||||
self._addSummaryEntry("Server type:", "GNS3 Virtual Machine")
|
||||
self._addSummaryEntry("VM engine:", self._GNS3VMSettings()["engine"].capitalize())
|
||||
self._addSummaryEntry("VM name:", self._GNS3VMSettings()["vmname"])
|
||||
self._addSummaryEntry("VM vCPUs:", str(self._GNS3VMSettings()["vcpus"]))
|
||||
self._addSummaryEntry("VM RAM:", str(self._GNS3VMSettings()["ram"]) + " MB")
|
||||
|
||||
@qslot
|
||||
def _refreshLocalServerStatusSlot(self):
|
||||
"""
|
||||
Refresh the local server status page
|
||||
"""
|
||||
if Controller.instance().connected():
|
||||
self.uiLocalServerStatusLabel.setText("Connection to local server successful")
|
||||
Controller.instance().get("/gns3vm", self._getSettingsCallback)
|
||||
elif Controller.instance().connecting():
|
||||
self.uiLocalServerStatusLabel.setText("Please wait connection to the GNS3 server")
|
||||
else:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self.uiLocalServerStatusLabel.setText("Connection to local server failed.\n* Make sure GNS3 is allowed in your firewall.\n* Go back and try to change the server port\n* Please check with a browser if you can connect to {protocol}://{host}:{port}.\n* Try to run {path} in a terminal to see if you have an error if the above does not work.".format(protocol=local_server_settings["protocol"], host=local_server_settings["host"], port=local_server_settings["port"], path=local_server_settings["path"]))
|
||||
|
||||
def _GNS3VMSettings(self):
|
||||
return self._gns3_vm_settings
|
||||
|
||||
def _setGNS3VMSettings(self, settings):
|
||||
Controller.instance().put("/gns3vm", self._saveSettingsCallback, settings, timeout=60 * 5)
|
||||
|
||||
def _saveSettingsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while save settings: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def _addSummaryEntry(self, name, value):
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiSummaryTreeWidget, [name, value])
|
||||
item.setText(0, name)
|
||||
font = item.font(0)
|
||||
font.setBold(True)
|
||||
item.setFont(0, font)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the settings.
|
||||
"""
|
||||
|
||||
Controller.instance().setDisplayError(True)
|
||||
if self.currentPage() == self.uiVMWizardPage:
|
||||
vmname = self.uiVMListComboBox.currentText()
|
||||
if vmname:
|
||||
# save the GNS3 VM settings
|
||||
vm_settings = self._GNS3VMSettings()
|
||||
vm_settings["enable"] = True
|
||||
vm_settings["vmname"] = vmname
|
||||
|
||||
if self.uiVmwareRadioButton.isChecked():
|
||||
vm_settings["engine"] = "vmware"
|
||||
elif self.uiVirtualBoxRadioButton.isChecked():
|
||||
vm_settings["engine"] = "virtualbox"
|
||||
|
||||
# set the vCPU count and RAM
|
||||
vpcus = self.uiCPUSpinBox.value()
|
||||
ram = self.uiRAMSpinBox.value()
|
||||
if ram < 1024:
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM memory", "It is recommended to allocate a minimum of 1024 MB of memory to the GNS3 VM")
|
||||
vm_settings["vcpus"] = vpcus
|
||||
vm_settings["ram"] = ram
|
||||
|
||||
self._setGNS3VMSettings(vm_settings)
|
||||
else:
|
||||
if not self.uiVmwareRadioButton.isChecked() and not self.uiVirtualBoxRadioButton.isChecked():
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "Please select VMware or VirtualBox")
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "Please select a VM. If no VM is listed, check if the GNS3 VM is correctly imported and press refresh.")
|
||||
return False
|
||||
elif self.currentPage() == self.uiLocalServerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
local_server_settings["auto_start"] = True
|
||||
local_server_settings["path"] = self.uiLocalServerPathLineEdit.text().strip()
|
||||
local_server_settings["host"] = self.uiLocalServerHostComboBox.itemData(self.uiLocalServerHostComboBox.currentIndex())
|
||||
local_server_settings["port"] = self.uiLocalServerPortSpinBox.value()
|
||||
|
||||
if not os.path.isfile(local_server_settings["path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "Local server", "Could not find local server {}".format(local_server_settings["path"]))
|
||||
return False
|
||||
if not os.access(local_server_settings["path"], os.X_OK):
|
||||
QtWidgets.QMessageBox.critical(self, "Local server", "{} is not an executable".format(local_server_settings["path"]))
|
||||
return False
|
||||
|
||||
LocalServer.instance().updateLocalServerSettings(local_server_settings)
|
||||
LocalServer.instance().localServerAutoStartIfRequire()
|
||||
|
||||
elif self.currentPage() == self.uiRemoteControllerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
local_server_settings["auto_start"] = False
|
||||
local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
|
||||
local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
|
||||
local_server_settings["protocol"] = "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:
|
||||
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)
|
||||
|
||||
elif self.currentPage() == self.uiLocalServerStatusWizardPage:
|
||||
if not Controller.instance().connected():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _refreshVMListSlot(self):
|
||||
"""
|
||||
Refresh the list of VM available in VMware or VirtualBox.
|
||||
"""
|
||||
|
||||
if self.uiVmwareRadioButton.isChecked():
|
||||
Controller.instance().get("/gns3vm/engines/vmware/vms", self._getVMsFromServerCallback, progressText="Retrieving VMware VM list from server...")
|
||||
elif self.uiVirtualBoxRadioButton.isChecked():
|
||||
Controller.instance().get("/gns3vm/engines/virtualbox/vms", self._getVMsFromServerCallback, progressText="Retrieving VirtualBox VM list from server...")
|
||||
|
||||
def _getVMsFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for getVMsFromServer.
|
||||
|
||||
:param progress_dialog: QProgressDialog instance
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "VM List", "{}".format(result["message"]))
|
||||
else:
|
||||
self.uiVMListComboBox.clear()
|
||||
for vm in result:
|
||||
self.uiVMListComboBox.addItem(vm["vmname"])
|
||||
|
||||
index = self.uiVMListComboBox.findText(self._GNS3VMSettings()["vmname"])
|
||||
if index != -1:
|
||||
self.uiVMListComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
index = self.uiVMListComboBox.findText("GNS3 VM")
|
||||
if index != -1:
|
||||
self.uiVMListComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "GNS3 VM", "Could not find a VM named 'GNS3 VM', is it imported in VMware or VirtualBox?")
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
This dialog is closed.
|
||||
|
||||
:param result: ignored
|
||||
"""
|
||||
|
||||
Controller.instance().setDisplayError(True)
|
||||
settings = self.parentWidget().settings()
|
||||
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)
|
||||
|
||||
def nextId(self):
|
||||
"""
|
||||
Wizard rules!
|
||||
"""
|
||||
|
||||
current_id = self.currentId()
|
||||
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,20 +19,17 @@
|
||||
Dialog to manage the snapshots.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import re
|
||||
import time
|
||||
import os
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..utils.process_files_worker import ProcessFilesWorker
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from ..ui.snapshots_dialog_ui import Ui_SnapshotsDialog
|
||||
from ..topology import Topology
|
||||
from ..node import Node
|
||||
from ..controller import Controller
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
"""
|
||||
Snapshots dialog implementation.
|
||||
@@ -40,41 +37,38 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, parent, project_path, project_files_dir):
|
||||
def __init__(self, parent, project):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._project_path = project_path
|
||||
self._project_files_dir = os.path.join(project_files_dir, "project-files")
|
||||
self._project = project
|
||||
|
||||
self.uiCreatePushButton.clicked.connect(self._createSnapshotSlot)
|
||||
self.uiDeletePushButton.clicked.connect(self._deleteSnapshotSlot)
|
||||
self.uiRestorePushButton.clicked.connect(self._restoreSnapshotSlot)
|
||||
self.uiSnapshotsList.itemDoubleClicked.connect(self._snapshotDoubleClickedSlot)
|
||||
self._listSnaphosts()
|
||||
self._listSnapshots()
|
||||
|
||||
def _listSnaphosts(self):
|
||||
def _listSnapshots(self):
|
||||
"""
|
||||
Lists all available snapshots.
|
||||
"""
|
||||
|
||||
self.uiSnapshotsList.clear()
|
||||
snapshot_dir = os.path.join(self._project_files_dir, "snapshots")
|
||||
if not os.path.isdir(snapshot_dir):
|
||||
if self._project:
|
||||
Controller.instance().get("/projects/{}/snapshots".format(self._project.id()), self._listSnapshotsCallback)
|
||||
|
||||
def _listSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
return
|
||||
|
||||
for snapshot in os.listdir(snapshot_dir):
|
||||
match = re.search(r"^(.*)_([0-9]+)_([0-9]+)", snapshot)
|
||||
if match:
|
||||
snapshot_name = match.group(1)
|
||||
snapshot_date = match.group(2)[:2] + '/' + match.group(2)[2:4] + '/' + match.group(2)[4:]
|
||||
snapshot_time = match.group(3)[:2] + ':' + match.group(3)[2:4] + ':' + match.group(3)[4:]
|
||||
item = QtGui.QListWidgetItem(self.uiSnapshotsList)
|
||||
item.setText("{} on {} at {}".format(snapshot_name, snapshot_date, snapshot_time))
|
||||
item.setData(QtCore.Qt.UserRole, os.path.join(snapshot_dir, snapshot))
|
||||
|
||||
self.uiSnapshotsList.sortItems(QtCore.Qt.AscendingOrder)
|
||||
for snapshot in result:
|
||||
item = QtWidgets.QListWidgetItem(self.uiSnapshotsList)
|
||||
item.setText("{} on {}".format(snapshot["name"], datetime.fromtimestamp(snapshot["created_at"]).strftime("%d/%m/%y at %H:%M:%S")))
|
||||
item.setData(QtCore.Qt.UserRole, snapshot["snapshot_id"])
|
||||
|
||||
if self.uiSnapshotsList.count():
|
||||
self.uiSnapshotsList.setCurrentRow(0)
|
||||
@@ -89,17 +83,22 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
Slot to create a snapshot.
|
||||
"""
|
||||
|
||||
snapshot_name, ok = QtGui.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtGui.QLineEdit.Normal, "Unnamed")
|
||||
if ok and snapshot_name:
|
||||
from ..main_window import MainWindow
|
||||
MainWindow.instance().saveProject(self._project_path)
|
||||
snapshot_name = "{name}_{date}".format(name=snapshot_name, date=time.strftime("%d%m%y_%H%M%S"))
|
||||
snapshot_dir = os.path.join(self._project_files_dir, "snapshots", snapshot_name)
|
||||
worker = ProcessFilesWorker(os.path.dirname(self._project_path), snapshot_dir, skip_dirs=["snapshots"])
|
||||
progress_dialog = ProgressDialog(worker, "Creating snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
self._listSnaphosts()
|
||||
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.Normal, "Unnamed")
|
||||
if ok and snapshot_name and self._project:
|
||||
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()),
|
||||
self._createSnapshotsCallback,
|
||||
{"name": snapshot_name},
|
||||
progressText="Creation of snapshot '{}' in progress...".format(snapshot_name),
|
||||
timeout=None)
|
||||
|
||||
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
else:
|
||||
log.error("Cannot create snapshot of project")
|
||||
return
|
||||
self._listSnapshots()
|
||||
|
||||
def _deleteSnapshotSlot(self):
|
||||
"""
|
||||
@@ -108,9 +107,16 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
item = self.uiSnapshotsList.currentItem()
|
||||
if item:
|
||||
snapshot_path = item.data(QtCore.Qt.UserRole)
|
||||
shutil.rmtree(snapshot_path, ignore_errors=True)
|
||||
self._listSnaphosts()
|
||||
snapshot_id = item.data(QtCore.Qt.UserRole)
|
||||
Controller.instance().delete("/projects/{}/snapshots/{}".format(self._project.id(), snapshot_id), self._deleteSnapshotsCallback)
|
||||
|
||||
def _deleteSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
return
|
||||
self._listSnapshots()
|
||||
|
||||
def _restoreSnapshotSlot(self):
|
||||
"""
|
||||
@@ -119,63 +125,29 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
item = self.uiSnapshotsList.currentItem()
|
||||
if item:
|
||||
snapshot_path = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_path)
|
||||
snapshot_id = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_id)
|
||||
|
||||
def _restoreSnapshot(self, snapshot_path):
|
||||
def _restoreSnapshot(self, snapshot_id):
|
||||
"""
|
||||
Restores a snapshot.
|
||||
|
||||
:param snapshot_path: path to the snapshot
|
||||
:param snapshot_id: id of the snapshot
|
||||
"""
|
||||
|
||||
match = re.search(r"^(.*)_([0-9]+)_([0-9]+)", os.path.basename(snapshot_path))
|
||||
if match:
|
||||
snapshot_name = match.group(1)
|
||||
else:
|
||||
snapshot_name = "Unknown"
|
||||
reply = QtGui.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot \"{}\" was taken?".format(snapshot_name),
|
||||
QtGui.QMessageBox.Ok, QtGui.QMessageBox.Cancel)
|
||||
if reply == QtGui.QMessageBox.Cancel:
|
||||
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken, would you like to proceed?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
|
||||
if reply == QtWidgets.QMessageBox.Cancel:
|
||||
return
|
||||
|
||||
# stop all the nodes
|
||||
topology = Topology.instance()
|
||||
for node in topology.nodes():
|
||||
if hasattr(node, "start") and node.status() == Node.started:
|
||||
node.stop()
|
||||
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id),
|
||||
self._restoreSnapshotsCallback, progressText="Restoring snapshot...", timeout=None)
|
||||
|
||||
project_name, _ = os.path.splitext(os.path.basename(self._project_path))
|
||||
legacy_project_files_dir = os.path.join(snapshot_path, "{}-files".format(project_name))
|
||||
if os.path.exists(legacy_project_files_dir):
|
||||
# support for pre 1.3 snapshots
|
||||
for root, dirs, _ in os.walk(self._project_files_dir):
|
||||
dirs[:] = [d for d in dirs if d not in "snapshots"]
|
||||
for project_subdir in dirs:
|
||||
project_subdir_path = os.path.join(root, project_subdir)
|
||||
shutil.rmtree(project_subdir_path, ignore_errors=True)
|
||||
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
|
||||
dirs = os.listdir(legacy_project_files_dir)
|
||||
for snapshot_subdir in dirs:
|
||||
snapshot_subdir_path = os.path.join(legacy_project_files_dir, snapshot_subdir)
|
||||
worker = ProcessFilesWorker(snapshot_subdir_path, os.path.join(self._project_files_dir, snapshot_subdir))
|
||||
progress_dialog = ProgressDialog(worker, "Restoring snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
try:
|
||||
os.remove(self._project_path)
|
||||
shutil.copy(os.path.join(snapshot_path, os.path.basename(self._project_path)), self._project_path)
|
||||
except OSError as e:
|
||||
QtGui.QMessageBox.critical(self, "Restore snapshot", "Cannot restore snapshot: {}".format(e))
|
||||
else:
|
||||
worker = ProcessFilesWorker(snapshot_path, os.path.dirname(self._project_path), skip_dirs=["snapshots"])
|
||||
progress_dialog = ProgressDialog(worker, "Restoring snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
from ..main_window import MainWindow
|
||||
MainWindow.instance().loadSnapshot(self._project_path)
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
return
|
||||
self.accept()
|
||||
|
||||
def _snapshotDoubleClickedSlot(self, item):
|
||||
@@ -183,5 +155,5 @@ class SnapshotsDialog(QtGui.QDialog, Ui_SnapshotsDialog):
|
||||
Slot to restore a snapshot when it is double clicked.
|
||||
"""
|
||||
|
||||
snapshot_path = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_path)
|
||||
snapshot_id = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_id)
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
Style editor to edit Shape items.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
|
||||
from ..items.shape_item import ShapeItem
|
||||
|
||||
|
||||
class StyleEditorDialog(QtGui.QDialog, Ui_StyleEditorDialog):
|
||||
class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
"""
|
||||
Style editor dialog.
|
||||
@@ -34,13 +35,13 @@ class StyleEditorDialog(QtGui.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
def __init__(self, parent, items):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._items = items
|
||||
self.uiColorPushButton.clicked.connect(self._setColorSlot)
|
||||
self.uiBorderColorPushButton.clicked.connect(self._setBorderColorSlot)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
|
||||
self.uiBorderStyleComboBox.addItem("Solid", QtCore.Qt.SolidLine)
|
||||
self.uiBorderStyleComboBox.addItem("Dash", QtCore.Qt.DashLine)
|
||||
@@ -52,12 +53,18 @@ class StyleEditorDialog(QtGui.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(),
|
||||
@@ -74,7 +81,7 @@ class StyleEditorDialog(QtGui.QDialog, Ui_StyleEditorDialog):
|
||||
Slot to select the filling color.
|
||||
"""
|
||||
|
||||
color = QtGui.QColorDialog.getColor(self._color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel)
|
||||
color = QtWidgets.QColorDialog.getColor(self._color, self, "Select Color", QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self._color = color
|
||||
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
|
||||
@@ -87,7 +94,7 @@ class StyleEditorDialog(QtGui.QDialog, Ui_StyleEditorDialog):
|
||||
Slot to select the border color.
|
||||
"""
|
||||
|
||||
color = QtGui.QColorDialog.getColor(self._border_color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel)
|
||||
color = QtWidgets.QColorDialog.getColor(self._border_color, self, "Select Color", QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self._border_color = color
|
||||
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
|
||||
@@ -102,11 +109,17 @@ class StyleEditorDialog(QtGui.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):
|
||||
@@ -118,4 +131,4 @@ class StyleEditorDialog(QtGui.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
if result:
|
||||
self._applyPreferencesSlot()
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
@@ -16,15 +16,25 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Dialog to change the topology symbol of NodeItems
|
||||
Dialog to change node symbols.
|
||||
"""
|
||||
|
||||
from ..qt import QtSvg, QtCore, QtGui
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from ..ui.symbol_selection_dialog_ui import Ui_SymbolSelectionDialog
|
||||
from ..node import Node
|
||||
from ..local_server import LocalServer
|
||||
from ..controller import Controller
|
||||
from ..symbol import Symbol
|
||||
|
||||
|
||||
class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
|
||||
"""
|
||||
Symbol selection dialog.
|
||||
@@ -33,53 +43,55 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
|
||||
:param items: list of items
|
||||
"""
|
||||
|
||||
def __init__(self, parent, items=None, symbol=None, category=None):
|
||||
_symbols_dir = None
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
def __init__(self, parent, items=None, symbol=None):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._items = items
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
self.uiCustomSymbolRadioButton.toggled.connect(self._customSymbolToggledSlot)
|
||||
self.uiBuiltInSymbolRadioButton.toggled.connect(self._builtInSymbolToggledSlot)
|
||||
self.uiSearchLineEdit.textChanged.connect(self._searchTextChangedSlot)
|
||||
self.uiBuiltinSymbolOnlyCheckBox.toggled.connect(self._builtinSymbolOnlyToggledSlot)
|
||||
if not SymbolSelectionDialog._symbols_dir:
|
||||
SymbolSelectionDialog._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
|
||||
|
||||
selected_symbol = symbol
|
||||
selected_category = category
|
||||
if not self._items:
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).hide()
|
||||
|
||||
# current categories
|
||||
categories = {"Routers": Node.routers,
|
||||
"Switches": Node.switches,
|
||||
"End devices": Node.end_devices,
|
||||
"Security devices": Node.security_devices
|
||||
}
|
||||
|
||||
index = 0
|
||||
for name, category in categories.items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
if category == selected_category:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
index += 1
|
||||
else:
|
||||
self.uiCategoryLabel.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
custom_symbol = items[0].defaultRenderer().objectName()
|
||||
if not custom_symbol:
|
||||
symbol_name = items[0].node().defaultSymbol()
|
||||
else:
|
||||
symbol_name = custom_symbol
|
||||
selected_symbol = symbol_name
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).hide()
|
||||
|
||||
self.uiBuiltInSymbolRadioButton.setChecked(True)
|
||||
self.uiSymbolListWidget.setFocus()
|
||||
self.uiSymbolListWidget.setIconSize(QtCore.QSize(64, 64))
|
||||
symbol_resources = QtCore.QResource(":/symbols")
|
||||
for symbol in symbol_resources.children():
|
||||
if symbol.endswith(".normal.svg"):
|
||||
name = symbol[:-11]
|
||||
item = QtGui.QListWidgetItem(self.uiSymbolListWidget)
|
||||
item.setText(name)
|
||||
resource_path = ":/symbols/" + symbol
|
||||
svg_renderer = QtSvg.QSvgRenderer(resource_path)
|
||||
if resource_path == selected_symbol:
|
||||
self.uiSymbolListWidget.setCurrentItem(item)
|
||||
self._symbol_items = []
|
||||
|
||||
Controller.instance().get("/symbols", self._listSymbolsCallback)
|
||||
|
||||
def _listSymbolsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while listing symbols: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
self._symbol_items = []
|
||||
for symbol in result:
|
||||
symbol = Symbol(**symbol)
|
||||
name = os.path.splitext(symbol.filename())[0]
|
||||
item = QtWidgets.QListWidgetItem(self.uiSymbolListWidget)
|
||||
item.setData(QtCore.Qt.UserRole, symbol)
|
||||
self._symbol_items.append(item)
|
||||
item.setText(name)
|
||||
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
|
||||
item.setIcon(icon)
|
||||
|
||||
def render(item, path):
|
||||
svg_renderer = QImageSvgRenderer(path)
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
@@ -87,36 +99,96 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
|
||||
item.setIcon(icon)
|
||||
|
||||
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
|
||||
self.adjustSize()
|
||||
|
||||
def _builtinSymbolOnlyToggledSlot(self, checked):
|
||||
self._filter()
|
||||
|
||||
def _searchTextChangedSlot(self, text):
|
||||
self._filter()
|
||||
|
||||
def _filter(self):
|
||||
"""
|
||||
Hide element not matching the search
|
||||
"""
|
||||
text = self.uiSearchLineEdit.text()
|
||||
for item in self._symbol_items:
|
||||
if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not item.data(QtCore.Qt.UserRole).builtin():
|
||||
item.setHidden(True)
|
||||
else:
|
||||
if len(text.strip()) == 0 or text.strip().lower() in item.text().lower():
|
||||
item.setHidden(False)
|
||||
else:
|
||||
item.setHidden(True)
|
||||
|
||||
def _customSymbolToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the custom symbol radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiCustomSymbolGroupBox.setEnabled(True)
|
||||
self.uiCustomSymbolGroupBox.show()
|
||||
self.uiBuiltInGroupBox.setEnabled(False)
|
||||
self.uiBuiltInGroupBox.hide()
|
||||
self.adjustSize()
|
||||
|
||||
def _builtInSymbolToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the built-in symbol radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiCustomSymbolGroupBox.setEnabled(False)
|
||||
self.uiCustomSymbolGroupBox.hide()
|
||||
self.uiBuiltInGroupBox.setEnabled(True)
|
||||
self.uiBuiltInGroupBox.show()
|
||||
self.adjustSize()
|
||||
|
||||
def _applyPreferencesSlot(self):
|
||||
"""
|
||||
Applies the selected symbol to the items.
|
||||
"""
|
||||
|
||||
current = self.uiSymbolListWidget.currentItem()
|
||||
if current:
|
||||
name = current.text()
|
||||
path = ":/symbols/{}.normal.svg".format(name)
|
||||
default_renderer = QtSvg.QSvgRenderer(path)
|
||||
default_renderer.setObjectName(path)
|
||||
path = ":/symbols/{}.selected.svg".format(name)
|
||||
hover_renderer = QtSvg.QSvgRenderer(path)
|
||||
hover_renderer.setObjectName(path)
|
||||
for item in self._items:
|
||||
item.setDefaultRenderer(default_renderer)
|
||||
item.setHoverRenderer(hover_renderer)
|
||||
symbol_path = self.getSymbol()
|
||||
for item in self._items:
|
||||
item.setSymbol(symbol_path)
|
||||
return True
|
||||
|
||||
def getSymbols(self):
|
||||
def getSymbol(self):
|
||||
|
||||
current = self.uiSymbolListWidget.currentItem()
|
||||
if current:
|
||||
name = current.text()
|
||||
normal_symbol = ":/symbols/{}.normal.svg".format(name)
|
||||
selected_symbol = ":/symbols/{}.selected.svg".format(name)
|
||||
return normal_symbol, selected_symbol
|
||||
if self.uiSymbolListWidget.isEnabled():
|
||||
current = self.uiSymbolListWidget.currentItem()
|
||||
if current:
|
||||
return current.data(QtCore.Qt.UserRole).id()
|
||||
else:
|
||||
return os.path.basename(self.uiSymbolLineEdit.text())
|
||||
return None
|
||||
|
||||
def getCategory(self):
|
||||
def _symbolBrowserSlot(self):
|
||||
|
||||
return self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
# supported image file formats
|
||||
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.pbm *.pgm *.png *.ppm *.xbm *.xpm *.gif);;All files (*.*)"
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Image", SymbolSelectionDialog._symbols_dir, file_formats)
|
||||
if not path:
|
||||
return
|
||||
SymbolSelectionDialog._symbols_dir = os.path.dirname(path)
|
||||
|
||||
symbol_id = os.path.basename(path)
|
||||
Controller.instance().post("/symbols/" + symbol_id + "/raw", qpartial(self._finishSymbolUpload, path), body=pathlib.Path(path), progressText="Uploading {}".format(symbol_id), timeout=None)
|
||||
|
||||
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while uploading symbol: {}".format(path))
|
||||
return
|
||||
self.uiSymbolLineEdit.clear()
|
||||
self.uiSymbolLineEdit.setText(path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(path))
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
@@ -125,6 +197,9 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
|
||||
if result and self._items:
|
||||
self._applyPreferencesSlot()
|
||||
QtGui.QDialog.done(self, result)
|
||||
if result and self._items and not self._applyPreferencesSlot():
|
||||
result = 0
|
||||
super().done(result)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,12 +19,11 @@
|
||||
Text editor to edit Note items.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtWidgets, qslot
|
||||
from ..ui.text_editor_dialog_ui import Ui_TextEditorDialog
|
||||
|
||||
|
||||
class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
|
||||
class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
|
||||
"""
|
||||
Text editor dialog.
|
||||
|
||||
@@ -34,59 +33,76 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
|
||||
def __init__(self, parent, items):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._items = items
|
||||
self.uiFontPushButton.clicked.connect(self._setFontSlot)
|
||||
self.uiColorPushButton.clicked.connect(self._setColorSlot)
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
|
||||
# use the first item in the list as the model
|
||||
first_item = items[0]
|
||||
self._color = first_item.defaultTextColor()
|
||||
self._setColor(first_item.defaultTextColor())
|
||||
self.uiRotationSpinBox.setValue(first_item.rotation())
|
||||
self.uiColorPushButton.setStyleSheet("background-color: {}".format(self._color.name()))
|
||||
self.uiPlainTextEdit.setPlainText(first_item.toPlainText())
|
||||
self.uiPlainTextEdit.setFont(first_item.font())
|
||||
self.uiPlainTextEdit.setStyleSheet("color : {}".format(self._color.name()))
|
||||
|
||||
if not first_item.editable():
|
||||
self.uiPlainTextEdit.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
|
||||
|
||||
if len(self._items) == 1:
|
||||
self.uiApplyColorToAllItemsCheckBox.setChecked(True)
|
||||
self.uiApplyColorToAllItemsCheckBox.hide()
|
||||
self.uiApplyRotationToAllItemsCheckBox.setChecked(True)
|
||||
self.uiApplyRotationToAllItemsCheckBox.hide()
|
||||
self.uiApplyTextToAllItemsCheckBox.setChecked(True)
|
||||
self.uiApplyTextToAllItemsCheckBox.hide()
|
||||
|
||||
def _setFontSlot(self):
|
||||
def _setColor(self, color):
|
||||
self._color = color
|
||||
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(color.red(),
|
||||
color.green(),
|
||||
color.blue(),
|
||||
color.alpha()))
|
||||
self.uiPlainTextEdit.setStyleSheet("color: rgba({}, {}, {}, {});".format(color.red(),
|
||||
color.green(),
|
||||
color.blue(),
|
||||
color.alpha()))
|
||||
|
||||
@qslot
|
||||
def _setFontSlot(self, *args):
|
||||
"""
|
||||
Slot to select the font.
|
||||
"""
|
||||
|
||||
selected_font, ok = QtGui.QFontDialog.getFont(self.uiPlainTextEdit.font(), self)
|
||||
selected_font, ok = QtWidgets.QFontDialog.getFont(self.uiPlainTextEdit.font(), self,
|
||||
options=QtWidgets.QFontDialog.DontUseNativeDialog)
|
||||
if ok:
|
||||
self.uiPlainTextEdit.setFont(selected_font)
|
||||
|
||||
def _setColorSlot(self):
|
||||
@qslot
|
||||
def _setColorSlot(self, *args):
|
||||
"""
|
||||
Slot to select the color.
|
||||
"""
|
||||
|
||||
color = QtGui.QColorDialog.getColor(self._color, self)
|
||||
color = QtWidgets.QColorDialog.getColor(self._color, self, None, QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self._color = color
|
||||
self.uiColorPushButton.setStyleSheet("background-color: {}".format(self._color.name()))
|
||||
self.uiPlainTextEdit.setStyleSheet("color : {}".format(self._color.name()))
|
||||
self._setColor(color)
|
||||
|
||||
def _applyPreferencesSlot(self):
|
||||
@qslot
|
||||
def _applyPreferencesSlot(self, *args):
|
||||
"""
|
||||
Applies the new text settings.
|
||||
"""
|
||||
|
||||
for item in self._items:
|
||||
item.setDefaultTextColor(self._color)
|
||||
item.setFont(self.uiPlainTextEdit.font())
|
||||
item.setRotation(self.uiRotationSpinBox.value())
|
||||
if self.uiApplyColorToAllItemsCheckBox.isChecked():
|
||||
item.setDefaultTextColor(self._color)
|
||||
if self.uiApplyRotationToAllItemsCheckBox.isChecked():
|
||||
item.setRotation(self.uiRotationSpinBox.value())
|
||||
if item.editable() and self.uiApplyTextToAllItemsCheckBox.isChecked():
|
||||
item.setPlainText(self.uiPlainTextEdit.toPlainText())
|
||||
|
||||
@@ -99,4 +115,4 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
|
||||
if result:
|
||||
self._applyPreferencesSlot()
|
||||
QtGui.QDialog.done(self, result)
|
||||
super().done(result)
|
||||
|
||||
188
gns3/dialogs/vm_with_images_wizard.py
Normal file
188
gns3/dialogs/vm_with_images_wizard.py
Normal file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from .vm_wizard import VMWizard
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.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 parent: parent widget
|
||||
"""
|
||||
|
||||
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, parent)
|
||||
|
||||
def refreshImageStepsButtons(self):
|
||||
"""
|
||||
When changing the server type (remote or local)
|
||||
Refresh all the image selectors
|
||||
"""
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
radio_button.setChecked(radio_button.isChecked())
|
||||
|
||||
def _vmToggledSlot(self, checked):
|
||||
super()._vmToggledSlot(checked)
|
||||
if checked:
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
super()._remoteServerToggledSlot(checked)
|
||||
if checked:
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
def _localToggledSlot(self, checked):
|
||||
super()._localToggledSlot(checked)
|
||||
if checked:
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
def addImageSelector(self, radio_button, combo_box, line_edit, browser, image_selector, create_button=None, create_image_wizard=None, image_suffix=""):
|
||||
"""
|
||||
Add a remote image selector
|
||||
|
||||
:param radio_button: Radio button which toggle display of the listbox
|
||||
:param combo_box: The image choice combo box
|
||||
:param line_edit: The edit for the image
|
||||
:param browser: file upload browser button
|
||||
:param image_selector: function which display an image selector and return path
|
||||
:param create_button: Image create button None if you don't need one
|
||||
:param create_image_wizard: Wizard Class for creating a new image
|
||||
"""
|
||||
|
||||
combo_box.currentIndexChanged.connect(lambda index: self._imageListIndexChangedSlot(index, combo_box, line_edit))
|
||||
self._images_combo_boxes.add(combo_box)
|
||||
|
||||
browser.clicked.connect(lambda: self._imageBrowserSlot(line_edit, image_selector))
|
||||
|
||||
if create_button:
|
||||
assert create_image_wizard is not None
|
||||
create_button.clicked.connect(lambda: self._imageCreateSlot(line_edit, create_image_wizard, image_suffix))
|
||||
|
||||
self._existingImageToggledSlot(True, combo_box, line_edit, browser, create_button)
|
||||
radio_button.toggled.connect(lambda checked: self._existingImageToggledSlot(checked, combo_box, line_edit, browser, create_button))
|
||||
self._radio_existing_images_buttons.add(radio_button)
|
||||
|
||||
def _imageCreateSlot(self, line_edit, create_image_wizard, image_suffix):
|
||||
create_dialog = create_image_wizard(self, self.getSettings()["server"], self.uiNameLineEdit.text() + image_suffix)
|
||||
if QtWidgets.QDialog.Accepted == create_dialog.exec_():
|
||||
line_edit.setText(create_dialog.uiLocationLineEdit.text())
|
||||
|
||||
def _imageBrowserSlot(self, line_edit, image_selector):
|
||||
"""
|
||||
Slot to open a file browser and select an image.
|
||||
"""
|
||||
|
||||
path = image_selector(self, self._compute_id)
|
||||
if not path:
|
||||
return
|
||||
line_edit.clear()
|
||||
line_edit.setText(path)
|
||||
|
||||
def _imageListIndexChangedSlot(self, index, combo_box, line_edit):
|
||||
"""
|
||||
User select a different image in the combo box
|
||||
"""
|
||||
item = combo_box.itemData(index)
|
||||
if item and item["path"]:
|
||||
line_edit.setText(item["path"])
|
||||
else:
|
||||
line_edit.setText("")
|
||||
|
||||
def _existingImageToggledSlot(self, checked, combo_box, line_edit, browser, create_button):
|
||||
"""
|
||||
User select the option of using an existing image
|
||||
"""
|
||||
|
||||
if create_button:
|
||||
create_button.hide()
|
||||
|
||||
if checked:
|
||||
combo_box.show()
|
||||
browser.hide()
|
||||
line_edit.hide()
|
||||
if combo_box.count() > 0:
|
||||
line_edit.setText(combo_box.itemData(combo_box.currentIndex())["path"])
|
||||
else:
|
||||
combo_box.hide()
|
||||
line_edit.setText("")
|
||||
line_edit.show()
|
||||
browser.show()
|
||||
if create_button:
|
||||
create_button.show()
|
||||
|
||||
def loadImagesList(self, endpoint):
|
||||
"""
|
||||
Fill the list box with available Images"
|
||||
|
||||
:param endpoint: server endpoint with the list of Images
|
||||
"""
|
||||
|
||||
Controller.instance().getCompute(endpoint, self._compute_id, self._getImagesFromServerCallback)
|
||||
|
||||
def _getImagesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for loadImagesList.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Images", "Error while getting the VMs: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
# Wizard is closed
|
||||
if self.currentPage() is None:
|
||||
return
|
||||
|
||||
if len(result) == 0:
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
if radio_button.isChecked() and self._widgetOnCurrentPage(radio_button):
|
||||
for button in radio_button.parent().findChildren(QtWidgets.QRadioButton):
|
||||
if button != radio_button:
|
||||
button.setChecked(True)
|
||||
button.hide()
|
||||
else:
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
if self._widgetOnCurrentPage(radio_button):
|
||||
for button in radio_button.parent().findChildren(QtWidgets.QRadioButton):
|
||||
if button == radio_button:
|
||||
button.setChecked(True)
|
||||
button.show()
|
||||
|
||||
for combo_box in self._images_combo_boxes:
|
||||
if self._widgetOnCurrentPage(combo_box):
|
||||
combo_box.clear()
|
||||
for vm in result:
|
||||
combo_box.addItem(vm["path"], vm)
|
||||
|
||||
def _widgetOnCurrentPage(self, widget):
|
||||
"""
|
||||
:returns Boolean True if widget is current active Wizard page
|
||||
"""
|
||||
return self.currentPage().findChild(widget.__class__, widget.objectName()) is not None
|
||||
160
gns3/dialogs/vm_wizard.py
Normal file
160
gns3/dialogs/vm_wizard.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.controller import Controller
|
||||
|
||||
|
||||
class VMWizard(QtWidgets.QWizard):
|
||||
"""
|
||||
Base class for VM wizard.
|
||||
|
||||
:param devices: List of existing device for this type
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, devices, parent):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.setModal(True)
|
||||
|
||||
self._devices = devices
|
||||
self._local_server_disable = False
|
||||
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
|
||||
|
||||
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
if Controller.instance().isRemote():
|
||||
self.uiLocalRadioButton.setText("Run device on the main server")
|
||||
|
||||
# By default we use the local server
|
||||
self._compute_id = "local"
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
self._localToggledSlot(True)
|
||||
|
||||
if len(ComputeManager.instance().computes()) == 1:
|
||||
# skip the server page if we use the first server
|
||||
self.setStartId(1)
|
||||
|
||||
def _vmToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the VM radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the remote server radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(True)
|
||||
self.uiRemoteServersComboBox.setEnabled(True)
|
||||
self.uiRemoteServersGroupBox.show()
|
||||
|
||||
def _localToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the local server radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
def initializePage(self, page_id):
|
||||
|
||||
if self.page(page_id) == self.uiServerWizardPage:
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
|
||||
self.uiRemoteRadioButton.setEnabled(False)
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.setEnabled(False)
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
for compute in ComputeManager.instance().computes():
|
||||
if compute.id() == "local":
|
||||
self.uiLocalRadioButton.setEnabled(True)
|
||||
elif compute.id() == "vm":
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.setEnabled(True)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setEnabled(True)
|
||||
self.uiRemoteServersComboBox.addItem(compute.name(), compute.id())
|
||||
|
||||
if self.uiLocalRadioButton.isEnabled() and not self._local_server_disable:
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isEnabled():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
else:
|
||||
if self.uiRemoteRadioButton.isEnabled():
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
|
||||
def _disableLocalServer(self):
|
||||
"""
|
||||
Turn off the local server
|
||||
"""
|
||||
self._local_server_disable = True
|
||||
self.uiLocalRadioButton.hide()
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
self.setStartId(0)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
if hasattr(self, "uiNameWizardPage") and self.currentPage() == self.uiNameWizardPage:
|
||||
name = self.uiNameLineEdit.text()
|
||||
for device in self._devices.values():
|
||||
if device["name"] == name:
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "{} is already used, please choose another name".format(name))
|
||||
return False
|
||||
elif self.currentPage() == self.uiServerWizardPage:
|
||||
# If the local button is not visible it's because it's not supported
|
||||
if self.uiLocalRadioButton.isChecked() and self.uiLocalRadioButton.isHidden():
|
||||
QtWidgets.QMessageBox.critical(self, "New device", "Please configure before the GNS3 VM in order to use this device.")
|
||||
return False
|
||||
|
||||
if self.uiRemoteRadioButton.isChecked():
|
||||
if self.uiRemoteServersComboBox.count() == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote server registered in your preferences")
|
||||
return False
|
||||
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
|
||||
self._compute_id = "vm"
|
||||
else:
|
||||
if self.uiLocalRadioButton.isEnabled():
|
||||
self._compute_id = "local"
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Server", "No available server support this type of node. You probably need to setup the GNS3 VM")
|
||||
return False
|
||||
return True
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
191
gns3/image_manager.py
Normal file
191
gns3/image_manager.py
Normal file
@@ -0,0 +1,191 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import copy
|
||||
import pathlib
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
from gns3.settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.controller import Controller
|
||||
from gns3.utils.file_copy_worker import FileCopyWorker
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.registry.image import Image
|
||||
|
||||
|
||||
class ImageManager:
|
||||
|
||||
def __init__(self):
|
||||
# Remember if we already ask the user about this image for this server
|
||||
self._asked_for_this_image = {}
|
||||
|
||||
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.
|
||||
|
||||
:param parent: Parent window
|
||||
:param path: File path on computer
|
||||
:param server: The server where the images should be located
|
||||
:param node_type: Remote upload endpoint
|
||||
:returns path: Final path
|
||||
"""
|
||||
|
||||
if (server and server != "local") or Controller.instance().isRemote():
|
||||
return self._uploadImageToRemoteServer(source_path, server, node_type)
|
||||
else:
|
||||
destination_directory = self.getDirectoryForType(node_type)
|
||||
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(source_filename),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
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 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 source_path
|
||||
else:
|
||||
source_path = destination_path
|
||||
return source_path
|
||||
|
||||
def _uploadImageToRemoteServer(self, path, server, node_type):
|
||||
"""
|
||||
Upload image to remote server
|
||||
|
||||
:param path: File path on computer
|
||||
:param server: The server where the images should be located
|
||||
:param node_type: Image node_type
|
||||
:returns path: Final path
|
||||
"""
|
||||
|
||||
if node_type == 'QEMU':
|
||||
upload_endpoint = '/qemu/images'
|
||||
elif node_type == 'IOU':
|
||||
upload_endpoint = '/iou/images'
|
||||
elif node_type == 'DYNAMIPS':
|
||||
upload_endpoint = '/dynamips/images'
|
||||
else:
|
||||
raise Exception('Invalid node type')
|
||||
|
||||
filename = self._getRelativeImagePath(path, node_type).replace("\\", "/")
|
||||
Controller.instance().postCompute('{}/{}'.format(upload_endpoint, filename), server, None, body=pathlib.Path(path), progressText="Uploading {}".format(filename), timeout=None)
|
||||
return filename
|
||||
|
||||
def _getRelativeImagePath(self, path, node_type):
|
||||
"""
|
||||
Get a path relative to images directory path
|
||||
or just filename if the path is not located inside
|
||||
image directory
|
||||
|
||||
:param path: file path
|
||||
:param node_type: Type of vm
|
||||
:return: file path
|
||||
"""
|
||||
|
||||
if not path:
|
||||
return ""
|
||||
img_directory = self.getDirectoryForType(node_type)
|
||||
path = os.path.abspath(path)
|
||||
if os.path.commonprefix([img_directory, path]) == img_directory:
|
||||
return os.path.relpath(path, img_directory)
|
||||
return os.path.basename(path)
|
||||
|
||||
def getDirectory(self):
|
||||
"""
|
||||
Returns the images directory path.
|
||||
|
||||
:returns: path to the default images directory
|
||||
"""
|
||||
|
||||
return copy.copy(LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)['images_path'])
|
||||
|
||||
def getDirectoryForType(self, node_type):
|
||||
"""
|
||||
Return the path of local directory of the images
|
||||
of a specific node_type
|
||||
|
||||
:param node_type: Type of vm
|
||||
"""
|
||||
if node_type == 'DYNAMIPS':
|
||||
return os.path.join(self.getDirectory(), 'IOS')
|
||||
else:
|
||||
return os.path.join(self.getDirectory(), node_type)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of ImageManager.
|
||||
|
||||
:returns: instance of ImageManager
|
||||
"""
|
||||
|
||||
if not hasattr(ImageManager, '_instance') or ImageManager._instance is None:
|
||||
ImageManager._instance = ImageManager()
|
||||
return ImageManager._instance
|
||||
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)
|
||||
273
gns3/items/drawing_item.py
Normal file
273
gns3/items/drawing_item.py
Normal file
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtCore, QtWidgets, qslot, QtGui
|
||||
from .utils import colorFromSvg
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
import binascii
|
||||
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
|
||||
|
||||
"""
|
||||
Base class for non emulation item
|
||||
"""
|
||||
|
||||
def __init__(self, project=None, pos=None, drawing_id=None, svg=None, z=0, rotation=0, **kws):
|
||||
self._id = drawing_id
|
||||
if self._id is None:
|
||||
self._id = str(uuid.uuid4())
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._graphics_view = MainWindow.instance().uiGraphicsView
|
||||
self._main_window = MainWindow.instance()
|
||||
|
||||
self._project = project
|
||||
|
||||
# Store a hash of the SVG to avoid him
|
||||
# to be send if he doesn't change
|
||||
self._hash_svg = None
|
||||
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
if z:
|
||||
self.setZValue(z)
|
||||
if rotation:
|
||||
self.setRotation(rotation)
|
||||
|
||||
def drawing_id(self):
|
||||
return self._id
|
||||
|
||||
def create(self):
|
||||
if self._project:
|
||||
self._project.post("/drawings", self._createDrawingCallback, body=self.__json__())
|
||||
|
||||
def _createDrawingCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up drawing: {}".format(result["message"]))
|
||||
return False
|
||||
self._id = result["drawing_id"]
|
||||
self.updateDrawingCallback(result)
|
||||
|
||||
def updateDrawing(self):
|
||||
if self._id:
|
||||
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__(), showProgress=False)
|
||||
|
||||
@qslot
|
||||
def updateDrawingCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up drawing: {}".format(result["message"]))
|
||||
return False
|
||||
self.setPos(QtCore.QPoint(result["x"], result["y"]))
|
||||
self.setZValue(result["z"])
|
||||
self.setRotation(result["rotation"])
|
||||
if "svg" in result:
|
||||
self.fromSvg(result["svg"])
|
||||
|
||||
def handleKeyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
:return: Boolean True the event has been captured
|
||||
"""
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
if key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() == 0:
|
||||
self.setRotation(359)
|
||||
else:
|
||||
self.setRotation(self.rotation() - 1)
|
||||
return True
|
||||
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
return True
|
||||
return False
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
if not self.handleKeyPressEvent(event):
|
||||
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
|
||||
|
||||
def __json__(self):
|
||||
data = {
|
||||
"drawing_id": self._id,
|
||||
"x": int(self.pos().x()),
|
||||
"y": int(self.pos().y()),
|
||||
"z": int(self.zValue()),
|
||||
"rotation": int(self.rotation())
|
||||
}
|
||||
svg = self.toSvg()
|
||||
hash_svg = binascii.crc32(svg.encode())
|
||||
if hash_svg != self._hash_svg:
|
||||
data["svg"] = svg
|
||||
self._hash_svg = hash_svg
|
||||
return data
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtWidgets.QGraphicsItem.setZValue(self, value)
|
||||
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def delete(self, skip_controller=False):
|
||||
"""
|
||||
Deletes this drawing.
|
||||
|
||||
:param skip_controller: Do not replicate change on the controller (usefull when it's already deleted on controller)
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeDrawing(self)
|
||||
if self._id and not skip_controller:
|
||||
self._project.delete("/drawings/" + self._id, None, body=self.__json__())
|
||||
|
||||
def itemChange(self, change, value):
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
grid_size = self._graphics_view.gridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
tmp_x = (grid_size * round((self.x() + mid_x) / grid_size)) - mid_x
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
tmp_y = (grid_size * round((self.y() + mid_y) / grid_size)) - mid_y
|
||||
if tmp_x != self.x() and tmp_y != self.y():
|
||||
self.setPos(tmp_x, tmp_y)
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if not value:
|
||||
self.updateDrawing()
|
||||
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
|
||||
|
||||
def updateNode(self):
|
||||
self.updateDrawing()
|
||||
|
||||
def drawLayerInfo(self, painter):
|
||||
"""
|
||||
Draws the layer position.
|
||||
|
||||
:param painter: QPainter instance
|
||||
"""
|
||||
|
||||
if self.show_layer is False:
|
||||
return
|
||||
|
||||
brect = self.boundingRect()
|
||||
# don't draw anything if the object is too small
|
||||
if brect.width() < 20 or brect.height() < 20:
|
||||
return
|
||||
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
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
|
||||
@@ -19,35 +19,22 @@
|
||||
Graphical representation of an ellipse on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
import math
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .shape_item import ShapeItem
|
||||
|
||||
|
||||
class EllipseItem(ShapeItem, QtGui.QGraphicsEllipseItem):
|
||||
class EllipseItem(QtWidgets.QGraphicsEllipseItem, ShapeItem):
|
||||
|
||||
"""
|
||||
Class to draw an ellipse on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, pos=None, width=200, height=200):
|
||||
def __init__(self, width=200, height=200, **kws):
|
||||
super().__init__(width=width, height=height, **kws)
|
||||
|
||||
QtGui.QGraphicsEllipseItem.__init__(self, 0, 0, width, height)
|
||||
ShapeItem.__init__(self)
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.DashLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.setPen(pen)
|
||||
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
|
||||
self.setBrush(brush)
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this ellipse.
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeEllipse(self)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -58,19 +45,31 @@ class EllipseItem(ShapeItem, QtGui.QGraphicsEllipseItem):
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsEllipseItem.paint(self, painter, option, widget)
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def duplicate(self):
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Duplicates this ellipse item.
|
||||
|
||||
:return: EllipseItem instance
|
||||
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
|
||||
"""
|
||||
svg = ET.Element("svg")
|
||||
svg.set("width", str(self.rect().width()))
|
||||
svg.set("height", str(self.rect().height()))
|
||||
|
||||
ellipse = ET.SubElement(svg, "ellipse")
|
||||
ellipse.set("cx", str(math.floor(self.rect().width() / 2)))
|
||||
ellipse.set("rx", str(math.ceil(self.rect().width() / 2)))
|
||||
ellipse.set("cy", str(math.floor(self.rect().height() / 2)))
|
||||
ellipse.set("ry", str(math.ceil(self.rect().height() / 2)))
|
||||
|
||||
ellipse = self._styleSvg(ellipse)
|
||||
|
||||
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
|
||||
ellipse_item = EllipseItem(QtCore.QPointF(self.x() + 20, self.y() + 20), self.rect().width(), self.rect().height())
|
||||
ellipse_item.setPen(self.pen())
|
||||
ellipse_item.setBrush(self.brush())
|
||||
ellipse_item.setZValue(self.zValue())
|
||||
ellipse_item.setRotation(self.rotation())
|
||||
return ellipse_item
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
Graphical representation of an Ethernet link for QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .link_item import LinkItem
|
||||
from .note_item import NoteItem
|
||||
from ..ports.port import Port
|
||||
@@ -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):
|
||||
|
||||
LinkItem.__init__(self, source_item, source_port, destination_item, destination_port, link, adding_flag, multilink)
|
||||
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag)
|
||||
self._source_collision_offset = 0.0
|
||||
self._destination_collision_offset = 0.0
|
||||
|
||||
@@ -75,7 +74,7 @@ class EthernetLinkItem(LinkItem):
|
||||
:returns: QPainterPath instance
|
||||
"""
|
||||
|
||||
path = QtGui.QGraphicsPathItem.shape(self)
|
||||
path = QtWidgets.QGraphicsPathItem.shape(self)
|
||||
offset = self._point_size / 2
|
||||
if not self._adding_flag:
|
||||
if self.length:
|
||||
@@ -106,23 +105,26 @@ class EthernetLinkItem(LinkItem):
|
||||
:param widget: QWidget instance.
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
if not self._adding_flag and self._settings["draw_link_status_points"]:
|
||||
|
||||
# points disappears if nodes are too close to each others.
|
||||
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
|
||||
elif self._source_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
point1 = QtCore.QPointF(self.source + self.edge_offset) + QtCore.QPointF((self.dx * self._source_collision_offset) / self.length, (self.dy * self._source_collision_offset) / self.length)
|
||||
|
||||
# avoid any collision of the status point with the source node
|
||||
@@ -137,36 +139,33 @@ class EthernetLinkItem(LinkItem):
|
||||
self._source_collision_offset -= 10
|
||||
|
||||
source_port_label = self._source_port.label()
|
||||
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
source_port_label.setPlainText(self._source_port.shortName())
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, point1))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
if not self._source_port.isStub():
|
||||
source_port_name = self._source_port.name().replace(self._source_port.longNameType(),
|
||||
self._source_port.shortNameType())
|
||||
else:
|
||||
source_port_name = self._source_port.name()
|
||||
source_port_label.setPlainText(source_port_name)
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, point1))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
elif source_port_label and not source_port_label.isVisible():
|
||||
source_port_label.show()
|
||||
|
||||
elif source_port_label:
|
||||
source_port_label.show()
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
painter.drawPoint(point1)
|
||||
|
||||
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
|
||||
elif self._destination_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
point2 = QtCore.QPointF(self.destination - self.edge_offset) - QtCore.QPointF((self.dx * self._destination_collision_offset) / self.length, (self.dy * self._destination_collision_offset) / self.length)
|
||||
|
||||
# avoid any collision of the status point with the destination node
|
||||
@@ -181,22 +180,18 @@ class EthernetLinkItem(LinkItem):
|
||||
self._destination_collision_offset -= 10
|
||||
|
||||
destination_port_label = self._destination_port.label()
|
||||
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
destination_port_label.setPlainText(self._destination_port.shortName())
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, point2))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
if not self._destination_port.isStub():
|
||||
destination_port_name = self._destination_port.name().replace(self._destination_port.longNameType(),
|
||||
self._destination_port.shortNameType())
|
||||
else:
|
||||
destination_port_name = self._destination_port.name()
|
||||
destination_port_label.setPlainText(destination_port_name)
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, point2))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
elif destination_port_label and not destination_port_label.isVisible():
|
||||
destination_port_label.show()
|
||||
|
||||
elif destination_port_label:
|
||||
destination_port_label.show()
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(point2)
|
||||
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -19,45 +19,38 @@
|
||||
Graphical representation of an image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtSvg
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from .drawing_item import DrawingItem
|
||||
|
||||
|
||||
class ImageItem(QtGui.QGraphicsPixmapItem):
|
||||
class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
|
||||
|
||||
"""
|
||||
Class to insert an image on the scene.
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
def __init__(self, image_path=None, pos=None, svg=None, **kws):
|
||||
|
||||
def __init__(self, pixmap, image_path, pos=None):
|
||||
|
||||
QtGui.QGraphicsPixmapItem.__init__(self, pixmap)
|
||||
self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)
|
||||
self.setTransformationMode(QtCore.Qt.SmoothTransformation)
|
||||
self._image_path = image_path
|
||||
# Because we call the Qt C++ code we need to handle the case of pos is None otherwise we will get a conversion error
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
super().__init__(pos=pos, **kws)
|
||||
else:
|
||||
super().__init__(**kws)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this image item.
|
||||
"""
|
||||
if self._image_path:
|
||||
renderer = QImageSvgRenderer(image_path)
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeImage(self)
|
||||
# By default center the image
|
||||
if pos is None:
|
||||
x = self.pos().x() - (self.boundingRect().width() / 2)
|
||||
y = self.pos().y() - (self.boundingRect().height() / 2)
|
||||
self.setPos(x, y)
|
||||
|
||||
def duplicate(self):
|
||||
"""
|
||||
Duplicates this image item.
|
||||
|
||||
:return: ImageItem instance
|
||||
"""
|
||||
|
||||
image_item = ImageItem(self.pixmap(), self._image_path, QtCore.QPointF(self.x() + 20, self.y() + 20))
|
||||
image_item.setZValue(self.zValue())
|
||||
return image_item
|
||||
if svg:
|
||||
svg = self.fromSvg(svg)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -68,69 +61,22 @@ class ImageItem(QtGui.QGraphicsPixmapItem):
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsPixmapItem.paint(self, painter, option, widget)
|
||||
|
||||
if self.show_layer is False:
|
||||
return
|
||||
|
||||
brect = self.boundingRect()
|
||||
# don't draw anything if the object is too small
|
||||
if brect.width() < 20 or brect.height() < 20:
|
||||
return
|
||||
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
Sets Z value of the item
|
||||
:param value: z layer
|
||||
"""
|
||||
return DrawingItem.setZValue(self, value)
|
||||
|
||||
QtGui.QGraphicsPixmapItem.setZValue(self, value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
def fromSvg(self, svg):
|
||||
renderer = QImageSvgRenderer(svg)
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
def dump(self):
|
||||
def toSvg(self):
|
||||
"""
|
||||
Returns a representation of this image item.
|
||||
|
||||
:returns: dictionary
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
|
||||
image_info = {"path": self._image_path,
|
||||
"x": self.x(),
|
||||
"y": self.y()}
|
||||
|
||||
if self.zValue() != 0:
|
||||
image_info["z"] = self.zValue()
|
||||
|
||||
return image_info
|
||||
|
||||
def load(self, image_info):
|
||||
"""
|
||||
Loads an image representation
|
||||
(from a topology file).
|
||||
|
||||
:param image_info: representation of the image item (dictionary)
|
||||
"""
|
||||
|
||||
# load mandatory properties
|
||||
x = image_info["x"]
|
||||
y = image_info["y"]
|
||||
self.setPos(x, y)
|
||||
|
||||
# load optional properties
|
||||
z = image_info.get("z")
|
||||
if z is not None:
|
||||
self.setZValue(z)
|
||||
return self.renderer().svg()
|
||||
|
||||
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,12 +21,25 @@ Link items are graphical representation of a link on the QGraphicsScene
|
||||
"""
|
||||
|
||||
import math
|
||||
import struct
|
||||
import sys
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
|
||||
|
||||
from ..packet_capture import PacketCapture
|
||||
from ..dialogs.filter_dialog import FilterDialog
|
||||
|
||||
|
||||
class LinkItem(QtGui.QGraphicsPathItem):
|
||||
class SvgIconItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
def __init__(self, symbol, parent):
|
||||
|
||||
QtSvg.QGraphicsSvgItem.__init__(self, symbol, parent)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
|
||||
self.parentItem().mousePressEvent(event)
|
||||
event.accept()
|
||||
|
||||
|
||||
class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
"""
|
||||
Base class for link items.
|
||||
@@ -37,16 +50,16 @@ class LinkItem(QtGui.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):
|
||||
|
||||
QtGui.QGraphicsPathItem.__init__(self)
|
||||
self.setAcceptsHoverEvents(True)
|
||||
self.setZValue(-1)
|
||||
super().__init__()
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setZValue(-0.5)
|
||||
self._link = None
|
||||
|
||||
from ..main_window import MainWindow
|
||||
@@ -63,10 +76,6 @@ class LinkItem(QtGui.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
|
||||
@@ -76,9 +85,20 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
# indicates if the link is being hovered
|
||||
self._hovered = False
|
||||
|
||||
# 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._drawSymbol)
|
||||
self._link.delete_link_signal.connect(self._linkDeletedSlot)
|
||||
self.setFlag(self.ItemIsFocusable)
|
||||
source_item.addLink(self)
|
||||
destination_item.addLink(self)
|
||||
@@ -90,12 +110,10 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
|
||||
self.adjust()
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete this link
|
||||
"""
|
||||
|
||||
@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())
|
||||
@@ -103,11 +121,25 @@ class LinkItem(QtGui.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)
|
||||
|
||||
@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):
|
||||
"""
|
||||
Delete this link
|
||||
"""
|
||||
self._link.deleteLink()
|
||||
if self in self.scene().items():
|
||||
self.scene().removeItem(self)
|
||||
|
||||
def link(self):
|
||||
"""
|
||||
@@ -172,6 +204,14 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
|
||||
cls._draw_port_labels = state
|
||||
|
||||
def resetPortLabels(self):
|
||||
"""
|
||||
Resets the port label positions.
|
||||
"""
|
||||
|
||||
self._source_port.deleteLabel()
|
||||
self._destination_port.deleteLabel()
|
||||
|
||||
def populateLinkContextualMenu(self, menu):
|
||||
"""
|
||||
Adds device actions to the link contextual menu.
|
||||
@@ -179,39 +219,58 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
:param menu: QMenu instance
|
||||
"""
|
||||
|
||||
if not self._source_port.capturing() or not self._destination_port.capturing():
|
||||
if not self._link.capturing():
|
||||
# start capture
|
||||
start_capture_action = QtGui.QAction("Start capture", menu)
|
||||
start_capture_action = QtWidgets.QAction("Start capture", menu)
|
||||
start_capture_action.setIcon(QtGui.QIcon(':/icons/capture-start.svg'))
|
||||
start_capture_action.triggered.connect(self._startCaptureActionSlot)
|
||||
menu.addAction(start_capture_action)
|
||||
|
||||
if self._source_port.capturing() or self._destination_port.capturing():
|
||||
if self._link.capturing():
|
||||
# stop capture
|
||||
stop_capture_action = QtGui.QAction("Stop capture", menu)
|
||||
stop_capture_action = QtWidgets.QAction("Stop capture", menu)
|
||||
stop_capture_action.setIcon(QtGui.QIcon(':/icons/capture-stop.svg'))
|
||||
stop_capture_action.triggered.connect(self._stopCaptureActionSlot)
|
||||
menu.addAction(stop_capture_action)
|
||||
|
||||
# start wireshark
|
||||
start_wireshark_action = QtGui.QAction("Start Wireshark", menu)
|
||||
start_wireshark_action = QtWidgets.QAction("Start Wireshark", menu)
|
||||
start_wireshark_action.setIcon(QtGui.QIcon(":/icons/wireshark.png"))
|
||||
start_wireshark_action.triggered.connect(self._startWiresharkActionSlot)
|
||||
menu.addAction(start_wireshark_action)
|
||||
|
||||
if sys.platform.startswith("win") and struct.calcsize("P") * 8 == 64:
|
||||
# Windows 64-bit only (Solarwinds RTV limitation).
|
||||
analyze_action = QtGui.QAction("Analyze capture", menu)
|
||||
if PacketCapture.instance().packetAnalyzerAvailable():
|
||||
analyze_action = QtWidgets.QAction("Analyze capture", menu)
|
||||
analyze_action.setIcon(QtGui.QIcon(':/icons/rtv.png'))
|
||||
analyze_action.triggered.connect(self._analyzeCaptureActionSlot)
|
||||
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 = QtGui.QAction("Delete", menu)
|
||||
delete_action = QtWidgets.QAction("Delete", menu)
|
||||
delete_action.setIcon(QtGui.QIcon(':/icons/delete.svg'))
|
||||
delete_action.triggered.connect(self._deleteActionSlot)
|
||||
menu.addAction(delete_action)
|
||||
|
||||
@qslot
|
||||
def mousePressEvent(self, event):
|
||||
"""
|
||||
Called when the link is clicked and shows a contextual menu.
|
||||
@@ -224,15 +283,15 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
# send a escape key to the main window to cancel the link addition
|
||||
from ..main_window import MainWindow
|
||||
key = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Escape, QtCore.Qt.NoModifier)
|
||||
QtGui.QApplication.sendEvent(MainWindow.instance(), key)
|
||||
QtWidgets.QApplication.sendEvent(MainWindow.instance(), key)
|
||||
return
|
||||
|
||||
# create the contextual menu
|
||||
self.setAcceptsHoverEvents(False)
|
||||
menu = QtGui.QMenu()
|
||||
self.setAcceptHoverEvents(False)
|
||||
menu = QtWidgets.QMenu()
|
||||
self.populateLinkContextualMenu(menu)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
self.setAcceptsHoverEvents(True)
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._hovered = False
|
||||
self.adjust()
|
||||
|
||||
@@ -243,7 +302,7 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
#On pressing backspace or delete key, the selected link gets deleted
|
||||
# On pressing backspace or delete key, the selected link gets deleted
|
||||
if event.key() == QtCore.Qt.Key_Delete or event.key() == QtCore.Qt.Key_Backspace:
|
||||
self._deleteActionSlot()
|
||||
return
|
||||
@@ -262,26 +321,7 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
ports = {}
|
||||
if self._source_port.packetCaptureSupported() and not self._source_port.capturing():
|
||||
for dlt_name, dlt in self._source_port.dataLinkTypes().items():
|
||||
port = "{} port {} ({} encapsulation: {})".format(self._source_item.node().name(), self._source_port.name(), dlt_name, dlt)
|
||||
ports[port] = [self._source_item.node(), self._source_port, dlt]
|
||||
|
||||
if self._destination_port.packetCaptureSupported() and not self._destination_port.capturing():
|
||||
for dlt_name, dlt in self._destination_port.dataLinkTypes().items():
|
||||
port = "{} port {} ({} encapsulation: {})".format(self._destination_item.node().name(), self._destination_port.name(), dlt_name, dlt)
|
||||
ports[port] = [self._destination_item.node(), self._destination_port, dlt]
|
||||
|
||||
if not ports:
|
||||
QtGui.QMessageBox.critical(self._main_window, "Packet capture", "Packet capture is not supported on this link")
|
||||
return
|
||||
|
||||
selection, ok = QtGui.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", list(ports.keys()), 0, False)
|
||||
if ok:
|
||||
if selection in ports:
|
||||
node, port, dlt = ports[selection]
|
||||
node.startPacketCapture(port, port.captureFileName(node.name()), dlt)
|
||||
PacketCapture.instance().startCapture(self._link)
|
||||
|
||||
def _stopCaptureActionSlot(self):
|
||||
"""
|
||||
@@ -289,21 +329,7 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
if self._source_port.capturing() and self._destination_port.capturing():
|
||||
ports = {}
|
||||
source_port = "{} port {}".format(self._source_item.node().name(), self._source_port.name())
|
||||
ports[source_port] = [self._source_item.node(), self._source_port]
|
||||
destination_port = "{} port {}".format(self._destination_item.node().name(), self._destination_port.name())
|
||||
ports[destination_port] = [self._destination_item.node(), self._destination_port]
|
||||
selection, ok = QtGui.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", list(ports.keys()), 0, False)
|
||||
if ok:
|
||||
if selection in ports:
|
||||
node, port = ports[selection]
|
||||
node.stopPacketCapture(port)
|
||||
elif self._source_port.capturing():
|
||||
self._source_item.node().stopPacketCapture(self._source_port)
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_item.node().stopPacketCapture(self._destination_port)
|
||||
PacketCapture.instance().stopCapture(self._link)
|
||||
|
||||
def _startWiresharkActionSlot(self):
|
||||
"""
|
||||
@@ -311,22 +337,7 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
try:
|
||||
if self._source_port.capturing() and self._destination_port.capturing():
|
||||
ports = ["{} port {}".format(self._source_item.node().name(), self._source_port.name()),
|
||||
"{} port {}".format(self._destination_item.node().name(), self._destination_port.name())]
|
||||
selection, ok = QtGui.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", ports, 0, False)
|
||||
if ok:
|
||||
if selection.endswith(self._source_port.name()):
|
||||
self._source_port.startPacketCaptureReader()
|
||||
else:
|
||||
self._destination_port.startPacketCaptureReader()
|
||||
elif self._source_port.capturing():
|
||||
self._source_port.startPacketCaptureReader()
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_port.startPacketCaptureReader()
|
||||
except OSError as e:
|
||||
QtGui.QMessageBox.critical(self._main_window, "Packet capture", "Cannot start Wireshark: {}".format(e))
|
||||
PacketCapture.instance().startPacketCaptureReader(self._link)
|
||||
|
||||
def _analyzeCaptureActionSlot(self):
|
||||
"""
|
||||
@@ -335,21 +346,9 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
"""
|
||||
|
||||
try:
|
||||
if self._source_port.capturing() and self._destination_port.capturing():
|
||||
ports = ["{} port {}".format(self._source_item.node().name(), self._source_port.name()),
|
||||
"{} port {}".format(self._destination_item.node().name(), self._destination_port.name())]
|
||||
selection, ok = QtGui.QInputDialog.getItem(self._main_window, "Capture analyzer", "Please select a port:", ports, 0, False)
|
||||
if ok:
|
||||
if selection.endswith(self._source_port.name()):
|
||||
self._source_port.startPacketCaptureAnalyzer()
|
||||
else:
|
||||
self._destination_port.startPacketCaptureAnalyzer()
|
||||
elif self._source_port.capturing():
|
||||
self._source_port.startPacketCaptureAnalyzer()
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_port.startPacketCaptureAnalyzer()
|
||||
PacketCapture.instance().startPacketCaptureAnalyzer(self._link)
|
||||
except OSError as e:
|
||||
QtGui.QMessageBox.critical(self._main_window, "Capture analyzer", "Cannot start the packet capture analyzer program: {}".format(e))
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Capture analyzer", "Cannot start the packet capture analyzer program: {}".format(e))
|
||||
|
||||
def setHovered(self, value):
|
||||
"""
|
||||
@@ -382,6 +381,7 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
|
||||
self.setHovered(False)
|
||||
|
||||
@qslot
|
||||
def adjust(self):
|
||||
"""
|
||||
Computes the source point and destination point.
|
||||
@@ -391,7 +391,7 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
# links must always be below node items on the scene
|
||||
if not self._adding_flag:
|
||||
min_zvalue = min([self._source_item.zValue(), self._destination_item.zValue()])
|
||||
self.setZValue(min_zvalue - 1)
|
||||
self.setZValue(min_zvalue - 0.5)
|
||||
|
||||
self.prepareGeometryChange()
|
||||
source_rect = self._source_item.boundingRect()
|
||||
@@ -409,15 +409,54 @@ class LinkItem(QtGui.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.
|
||||
@@ -429,3 +468,92 @@ class LinkItem(QtGui.QGraphicsPathItem):
|
||||
self.destination = scene_point
|
||||
self.adjust()
|
||||
self.update()
|
||||
|
||||
@qslot
|
||||
def _drawSymbol(self, *args):
|
||||
"""
|
||||
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.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()
|
||||
|
||||
136
gns3/items/logo_item.py
Normal file
136
gns3/items/logo_item.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 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 urllib.parse
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from ..controller import Controller
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LogoItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
Margin for the logo
|
||||
"""
|
||||
MARGIN = 20
|
||||
|
||||
"""
|
||||
Logo for the scene.
|
||||
|
||||
:param logo_path: Path to the logo (remote)
|
||||
:param logo_url: URL which needs to be open user clicks on the logo
|
||||
:param project: Current project
|
||||
"""
|
||||
|
||||
def __init__(self, logo_path, logo_url, project):
|
||||
super().__init__()
|
||||
|
||||
self._logo_path = logo_path
|
||||
self._logo_url = logo_url
|
||||
self._project = project
|
||||
|
||||
# Temporary symbol during loading
|
||||
renderer = QImageSvgRenderer(":/icons/reload.svg")
|
||||
renderer.setObjectName("symbol_loading")
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
effect = QtWidgets.QGraphicsColorizeEffect()
|
||||
effect.setColor(QtGui.QColor("black"))
|
||||
effect.setStrength(0.8)
|
||||
self.setGraphicsEffect(effect)
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
# set graphical settings for this item
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._main_window = MainWindow.instance()
|
||||
self._settings = self._main_window.uiGraphicsView.settings()
|
||||
|
||||
self.updatePosition()
|
||||
|
||||
self._main_window.uiGraphicsView.viewport().installEventFilter(self)
|
||||
|
||||
remote_file = urllib.parse.quote('project-files/images/{}'.format(logo_path))
|
||||
|
||||
Controller.instance().getStatic(
|
||||
'/projects/{}/files/{}'.format(project.id(), remote_file),
|
||||
self.updateImage
|
||||
)
|
||||
|
||||
# make it the last one
|
||||
self.setZValue(-2)
|
||||
|
||||
def eventFilter(self, source, event):
|
||||
if event.type() == QtCore.QEvent.Paint:
|
||||
self.updatePosition()
|
||||
return QtWidgets.QWidget.eventFilter(self, source, event)
|
||||
|
||||
|
||||
def updateImage(self, local_path):
|
||||
renderer = QImageSvgRenderer(local_path)
|
||||
renderer.setObjectName("project_logo")
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
|
||||
def updatePosition(self):
|
||||
"""
|
||||
Updates position to be located in the right bottom corner
|
||||
"""
|
||||
logo_rect = self.boundingRect()
|
||||
width = self._main_window.uiGraphicsView.viewport().width()
|
||||
height = self._main_window.uiGraphicsView.viewport().height()
|
||||
rect = self._main_window.uiGraphicsView.mapToScene(QtCore.QRect(0, 0, width, height)).boundingRect()
|
||||
x = rect.x() + rect.width() - self.MARGIN - logo_rect.width()
|
||||
y = rect.y() + rect.height() - self.MARGIN - logo_rect.height()
|
||||
|
||||
# update only when changes
|
||||
if [int(self.x()), int(self.y())] != [int(x), int(y)]:
|
||||
self.setX(x)
|
||||
self.setY(y)
|
||||
self.update()
|
||||
|
||||
def hoverEnterEvent(self, event):
|
||||
"""
|
||||
Handles all hover enter events for this item.
|
||||
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
if self._logo_url is not None:
|
||||
self.graphicsEffect().setEnabled(True)
|
||||
|
||||
def hoverLeaveEvent(self, event):
|
||||
"""
|
||||
Handles all hover leave events for this item.
|
||||
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
url = QtCore.QUrl(self._logo_url)
|
||||
if not QtGui.QDesktopServices.openUrl(url):
|
||||
QtWidgets.QMessageBox.warning(self, 'Open Url', 'Could not open url')
|
||||
@@ -19,8 +19,17 @@
|
||||
Graphical representation of a node on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui, 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
|
||||
from ..controller import Controller
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
@@ -29,47 +38,46 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
Node for the scene.
|
||||
|
||||
:param node: Node instance
|
||||
:param default_symbol: Default symbol for the node representation on the scene
|
||||
:param hover_symbol: Hover symbol when the node is hovered on the scene
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
|
||||
def __init__(self, node, default_symbol=None, hover_symbol=None):
|
||||
|
||||
QtSvg.QGraphicsSvgItem.__init__(self)
|
||||
def __init__(self, node):
|
||||
super().__init__()
|
||||
|
||||
# attached node
|
||||
self._node = node
|
||||
# link items connected to this node item.
|
||||
self._links = []
|
||||
self._symbol = None
|
||||
|
||||
# says if the attached node has been initialized
|
||||
# by the server.
|
||||
self._initialized = False
|
||||
|
||||
# node label
|
||||
self._node_label = None
|
||||
|
||||
# link items connected to this node item.
|
||||
self._links = []
|
||||
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
|
||||
self.setZValue(self._node.z())
|
||||
|
||||
# Temporary symbol during loading
|
||||
renderer = QImageSvgRenderer(":/icons/reload.svg")
|
||||
renderer.setObjectName("symbol_loading")
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
effect = QtWidgets.QGraphicsColorizeEffect()
|
||||
effect.setColor(QtGui.QColor("black"))
|
||||
effect.setStrength(0.8)
|
||||
self.setGraphicsEffect(effect)
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
# set graphical settings for this node
|
||||
self.setFlag(QtSvg.QGraphicsSvgItem.ItemIsMovable)
|
||||
self.setFlag(QtSvg.QGraphicsSvgItem.ItemIsSelectable)
|
||||
self.setFlag(QtSvg.QGraphicsSvgItem.ItemIsFocusable)
|
||||
self.setFlag(QtSvg.QGraphicsSvgItem.ItemSendsGeometryChanges)
|
||||
self.setAcceptsHoverEvents(True)
|
||||
self.setZValue(1)
|
||||
|
||||
# create renderers using symbols paths/resources
|
||||
if default_symbol:
|
||||
self._default_renderer = QtSvg.QSvgRenderer(default_symbol)
|
||||
if default_symbol != node.defaultSymbol():
|
||||
self._default_renderer.setObjectName(default_symbol)
|
||||
else:
|
||||
self._default_renderer = QtSvg.QSvgRenderer(node.defaultSymbol())
|
||||
if hover_symbol:
|
||||
self._hover_renderer = QtSvg.QSvgRenderer(hover_symbol)
|
||||
if hover_symbol != node.hoverSymbol():
|
||||
self._hover_renderer.setObjectName(hover_symbol)
|
||||
else:
|
||||
self._hover_renderer = QtSvg.QSvgRenderer(node.hoverSymbol())
|
||||
self.setSharedRenderer(self._default_renderer)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
# connect signals to know about some events
|
||||
# e.g. when the node has been started, stopped or suspended etc.
|
||||
@@ -79,70 +87,72 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
node.suspended_signal.connect(self.suspendedSlot)
|
||||
node.updated_signal.connect(self.updatedSlot)
|
||||
node.deleted_signal.connect(self.deletedSlot)
|
||||
node.delete_links_signal.connect(self.deleteLinksSlot)
|
||||
node.error_signal.connect(self.errorSlot)
|
||||
node.server_error_signal.connect(self.serverErrorSlot)
|
||||
|
||||
# used when a port has been selected from the contextual menu
|
||||
self._selected_port = None
|
||||
|
||||
# says if the attached node has been initialized
|
||||
# by the server.
|
||||
self._initialized = False
|
||||
|
||||
# contains the last error message received
|
||||
# from the server.
|
||||
self._last_error = None
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._main_window = MainWindow.instance()
|
||||
if self._main_window.uiSnapToGridAction.isChecked():
|
||||
self._snapToGrid()
|
||||
self._settings = self._main_window.uiGraphicsView.settings()
|
||||
|
||||
def defaultRenderer(self):
|
||||
if node.initialized():
|
||||
self.createdSlot(node.id())
|
||||
|
||||
def _snapToGrid(self):
|
||||
|
||||
grid_size = self._main_window.uiGraphicsView.gridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
x = (grid_size * round((self.x() + mid_x) / grid_size)) - mid_x
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
y = (grid_size * round((self.y() + mid_y) / grid_size)) - mid_y
|
||||
self.setPos(x, y)
|
||||
|
||||
def updateNode(self):
|
||||
"""
|
||||
Returns the default QSvgRenderer.
|
||||
|
||||
:return: QSvgRenderer instance
|
||||
Sync change to the node
|
||||
"""
|
||||
self._node.setGraphics(self)
|
||||
|
||||
return self._default_renderer
|
||||
|
||||
def setDefaultRenderer(self, default_renderer):
|
||||
@qslot
|
||||
def setSymbol(self, symbol):
|
||||
"""
|
||||
Sets new default QSvgRenderer.
|
||||
|
||||
:param default_renderer: QSvgRenderer instance
|
||||
:param symbol: Change the symbol path
|
||||
"""
|
||||
# create renderer using symbols path/resource
|
||||
if symbol is None:
|
||||
symbol = self._node.defaultSymbol()
|
||||
if self._symbol != symbol:
|
||||
self._symbol = symbol
|
||||
|
||||
self._default_renderer = default_renderer
|
||||
self.setSharedRenderer(self._default_renderer)
|
||||
# Temporary symbol during loading
|
||||
renderer = QImageSvgRenderer(":/icons/reload.svg")
|
||||
renderer.setObjectName("symbol_loading")
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
def hoverRenderer(self):
|
||||
"""
|
||||
Returns the hover QSvgRenderer.
|
||||
Controller.instance().getStatic(Symbol(symbol_id=symbol).url(), self._symbolLoadedCallback)
|
||||
|
||||
:return: QSvgRenderer instance
|
||||
"""
|
||||
def symbol(self):
|
||||
return self._symbol
|
||||
|
||||
return self._hover_renderer
|
||||
|
||||
def setHoverRenderer(self, hover_renderer):
|
||||
"""
|
||||
Sets new hover QSvgRenderer.
|
||||
|
||||
:param hover_renderer: QSvgRenderer instance
|
||||
"""
|
||||
|
||||
self._hover_renderer = hover_renderer
|
||||
|
||||
def setUnsavedState(self):
|
||||
"""
|
||||
Indicates the project is in a unsaved state.
|
||||
"""
|
||||
|
||||
from ..main_window import MainWindow
|
||||
main_window = MainWindow.instance()
|
||||
main_window.setUnsavedState()
|
||||
@qslot
|
||||
def _symbolLoadedCallback(self, path, *args):
|
||||
renderer = QImageSvgRenderer(path, fallback=":/icons/cancel.svg")
|
||||
renderer.setObjectName(path)
|
||||
self.setSharedRenderer(renderer)
|
||||
if self._node.settings().get("symbol") != self._symbol:
|
||||
self.updateNode()
|
||||
if not self._initialized:
|
||||
self._showLabel()
|
||||
self._initialized = True
|
||||
self.updateNode()
|
||||
|
||||
def node(self):
|
||||
"""
|
||||
@@ -153,27 +163,44 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
return self._node
|
||||
|
||||
def addLink(self, link):
|
||||
def setPos(self, *args):
|
||||
super().setPos(*args)
|
||||
self._node.setSettingValue("x", int(self.x()))
|
||||
self._node.setSettingValue("y", int(self.y()))
|
||||
|
||||
@qslot
|
||||
def addLink(self, link_item, *args):
|
||||
"""
|
||||
Adds a link items to this node item.
|
||||
|
||||
:param link: LinkItem instance
|
||||
"""
|
||||
|
||||
self._links.append(link)
|
||||
self._node.updated_signal.emit()
|
||||
self.setUnsavedState()
|
||||
if not sip.isdeleted(link_item):
|
||||
self._links.append(link_item)
|
||||
link_item.link().delete_link_signal.connect(self._removeLink)
|
||||
link_item.link().updated_link_signal.connect(self._linkUpdatedSlot)
|
||||
self._node.updated_signal.emit()
|
||||
|
||||
def removeLink(self, link):
|
||||
@qslot
|
||||
def _linkUpdatedSlot(self, *args):
|
||||
"""
|
||||
When a link change we also notify the listener of the node
|
||||
"""
|
||||
self._node.updated_signal.emit()
|
||||
|
||||
@qslot
|
||||
def _removeLink(self, link_id, *args):
|
||||
"""
|
||||
Removes a link items from this node item.
|
||||
|
||||
:param link: LinkItem instance
|
||||
"""
|
||||
|
||||
if link in self._links:
|
||||
self._links.remove(link)
|
||||
self.setUnsavedState()
|
||||
for link_item in self._links:
|
||||
if link_item.link().id() == link_id:
|
||||
self._links.remove(link_item)
|
||||
return
|
||||
|
||||
def links(self):
|
||||
"""
|
||||
@@ -184,19 +211,21 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
return self._links
|
||||
|
||||
def createdSlot(self, node_id):
|
||||
@qslot
|
||||
def createdSlot(self, base_node_id, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a the node has been created/initialized.
|
||||
|
||||
:param node_id: node identifier (integer)
|
||||
:param base_node_id: base node identifier (integer)
|
||||
"""
|
||||
|
||||
self._initialized = True
|
||||
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
|
||||
self.setSymbol(self._node.symbol())
|
||||
self.update()
|
||||
self._showLabel()
|
||||
|
||||
def startedSlot(self):
|
||||
@qslot
|
||||
def startedSlot(self, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a the node has started.
|
||||
@@ -205,7 +234,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
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.
|
||||
@@ -214,7 +244,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
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.
|
||||
@@ -223,62 +254,55 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
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._node_label:
|
||||
if self._node_label.toPlainText() != self._node.name():
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
self._centerLabel()
|
||||
self.setUnsavedState()
|
||||
self.setSymbol(self._node.settings().get("symbol"))
|
||||
self.setPos(self._node.settings().get("x", 0), self._node.settings().get("y", 0))
|
||||
self.setZValue(self._node.settings().get("z", 0))
|
||||
|
||||
self._updateLabel()
|
||||
|
||||
# update the link tooltips in case the
|
||||
# node name has changed
|
||||
for link in self._links:
|
||||
link.setCustomToolTip()
|
||||
|
||||
def deleteLinksSlot(self):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a all the links must be deleted.
|
||||
"""
|
||||
|
||||
for link in self._links.copy():
|
||||
link.delete()
|
||||
|
||||
def deletedSlot(self):
|
||||
@qslot
|
||||
def deletedSlot(self, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when the node has been deleted.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
if not self.scene():
|
||||
return
|
||||
self._node.removeAllocatedName()
|
||||
if self in self.scene().items():
|
||||
self.scene().removeItem(self)
|
||||
self.setUnsavedState()
|
||||
|
||||
def serverErrorSlot(self, node_id, message):
|
||||
@qslot
|
||||
def serverErrorSlot(self, base_node_id, message, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when the node has received an error from the server.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: base node identifier
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
self._last_error = "{message}".format(message=message)
|
||||
|
||||
def errorSlot(self, node_id, message):
|
||||
@qslot
|
||||
def errorSlot(self, base_node_id, message, *args):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when the node wants to report an error.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: base node identifier
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
@@ -307,14 +331,11 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
return self._node_label
|
||||
|
||||
def setLabel(self, label):
|
||||
def _labelUnselectedSlot(self):
|
||||
"""
|
||||
Sets the node label.
|
||||
|
||||
:param label: NoteItem instance.
|
||||
Called when user unselect the label
|
||||
"""
|
||||
|
||||
self._node_label = label
|
||||
self.updateNode()
|
||||
|
||||
def _centerLabel(self):
|
||||
"""
|
||||
@@ -328,6 +349,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
label_x_pos = node_middle.x() - text_middle.x()
|
||||
label_y_pos = -25
|
||||
self._node_label.setPos(label_x_pos, label_y_pos)
|
||||
return
|
||||
|
||||
def _showLabel(self):
|
||||
"""
|
||||
@@ -336,9 +358,29 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
if not self._node_label:
|
||||
self._node_label = NoteItem(self)
|
||||
self._node_label.item_unselected_signal.connect(self._labelUnselectedSlot)
|
||||
self._node_label.setEditable(False)
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
self._updateLabel()
|
||||
self._node.setSettingValue("label", self._node_label.dump())
|
||||
|
||||
def _updateLabel(self):
|
||||
"""
|
||||
Update the label using the informations stored in the node
|
||||
"""
|
||||
if not self._node_label:
|
||||
return
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
label_data = self._node.settings().get("label")
|
||||
|
||||
if self._node_label.toPlainText() != label_data["text"]:
|
||||
self._node_label.setPlainText(label_data["text"])
|
||||
self._node_label.setStyle(label_data.get("style", ""))
|
||||
self._node_label.setRotation(label_data.get("rotation", 0))
|
||||
if label_data["x"] is None:
|
||||
self._centerLabel()
|
||||
self.updateNode()
|
||||
else:
|
||||
self._node_label.setPos(label_data["x"], label_data["y"])
|
||||
|
||||
def connectToPort(self, unavailable_ports=[]):
|
||||
"""
|
||||
@@ -350,10 +392,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
self._selected_port = None
|
||||
menu = QtGui.QMenu()
|
||||
menu = QtWidgets.QMenu()
|
||||
ports = self._node.ports()
|
||||
if not ports:
|
||||
QtGui.QMessageBox.critical(self.scene().parent(), "Link", "No port available, please configure this device")
|
||||
QtWidgets.QMessageBox.critical(self.scene().parent(), "Link", "No port available, please configure this device")
|
||||
return None
|
||||
|
||||
# sort the ports
|
||||
@@ -369,7 +411,6 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
ports_dict[port.portNumber()] = port
|
||||
else:
|
||||
ports_dict[port.name()] = port
|
||||
|
||||
try:
|
||||
ports = sorted(ports_dict.keys(), key=int)
|
||||
except ValueError:
|
||||
@@ -378,6 +419,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
# show a contextual menu for the user to choose a port
|
||||
for port in ports:
|
||||
port_object = ports_dict[port]
|
||||
log.debug("Node '{}' Port {} Type {}".format(self.node(), port_object.name(), type(port_object.name())))
|
||||
if port in unavailable_ports:
|
||||
# this port cannot be chosen by the user (grayed out)
|
||||
action = menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port_object.name())
|
||||
@@ -415,20 +457,27 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param value: value of the change
|
||||
"""
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
grid_size = self._main_window.uiGraphicsView.gridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
value.setX((grid_size * round((value.x() + mid_x) / grid_size)) - mid_x)
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
value.setY((grid_size * round((value.y() + mid_y) / grid_size)) - mid_y)
|
||||
|
||||
# dynamically change the renderer when this node item is selected/unselected.
|
||||
if change == QtSvg.QGraphicsSvgItem.ItemSelectedChange:
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if value:
|
||||
self.setSharedRenderer(self._hover_renderer)
|
||||
self.graphicsEffect().setEnabled(True)
|
||||
else:
|
||||
self.setSharedRenderer(self._default_renderer)
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
self.updateNode()
|
||||
|
||||
# adjust link item positions when this node is moving or has changed.
|
||||
if change == QtSvg.QGraphicsSvgItem.ItemPositionChange or change == QtSvg.QGraphicsSvgItem.ItemPositionHasChanged:
|
||||
self.setUnsavedState()
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange or change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
|
||||
for link in self._links:
|
||||
link.adjust()
|
||||
|
||||
return QtGui.QGraphicsItem.itemChange(self, change, value)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -441,8 +490,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
# don't show the selection rectangle
|
||||
if not self._settings["draw_rectangle_selected_item"]:
|
||||
option.state = QtGui.QStyle.State_None
|
||||
QtSvg.QGraphicsSvgItem.paint(self, painter, option, widget)
|
||||
option.state = QtWidgets.QStyle.State_None
|
||||
super().paint(painter, option, widget)
|
||||
|
||||
if not self._initialized or self.show_layer:
|
||||
brect = self.boundingRect()
|
||||
@@ -466,7 +515,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtSvg.QGraphicsSvgItem.setZValue(self, value)
|
||||
super().setZValue(value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
@@ -490,13 +539,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
self.setCustomToolTip()
|
||||
# dynamically change the renderer when this node item is hovered.
|
||||
if not self.isSelected():
|
||||
self.setSharedRenderer(self._hover_renderer)
|
||||
# effect = QtGui.QGraphicsColorizeEffect()
|
||||
# effect.setColor(QtGui.QColor("black"))
|
||||
# effect.setStrength(0.8)
|
||||
# self.setGraphicsEffect(effect)
|
||||
self.graphicsEffect().setEnabled(True)
|
||||
|
||||
def hoverLeaveEvent(self, event):
|
||||
"""
|
||||
@@ -505,7 +549,13 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
# dynamically change the renderer back to the default when this node item is not hovered anymore.
|
||||
if not self.isSelected():
|
||||
self.setSharedRenderer(self._default_renderer)
|
||||
# self.graphicsEffect().setEnabled(False)
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
def mouseRelease(self):
|
||||
"""
|
||||
Handle all mouse release for this item.
|
||||
It the item is select but mouse is not on it the event
|
||||
is send also
|
||||
"""
|
||||
self.updateNode()
|
||||
|
||||
@@ -19,24 +19,26 @@
|
||||
Graphical representation of a note on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from .utils import colorFromSvg
|
||||
|
||||
|
||||
class NoteItem(QtGui.QGraphicsTextItem):
|
||||
|
||||
class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
"""
|
||||
Text note for the QGraphicsView.
|
||||
|
||||
:param parent: optional parent
|
||||
"""
|
||||
item_unselected_signal = QtCore.Signal()
|
||||
|
||||
show_layer = False
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
QtGui.QGraphicsTextItem.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
|
||||
main_window = MainWindow.instance()
|
||||
view_settings = main_window.uiGraphicsView.settings()
|
||||
qt_font = QtGui.QFont()
|
||||
@@ -59,6 +61,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
|
||||
Topology.instance().removeNote(self)
|
||||
|
||||
def editable(self):
|
||||
@@ -101,7 +104,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
else:
|
||||
QtGui.QGraphicsTextItem.keyPressEvent(self, event)
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def editText(self):
|
||||
"""
|
||||
@@ -132,7 +135,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
:param event: QFocusEvent instance
|
||||
"""
|
||||
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsFocusable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False)
|
||||
cursor = self.textCursor()
|
||||
if cursor.hasSelection():
|
||||
cursor.clearSelection()
|
||||
@@ -142,7 +145,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
# delete the note if empty
|
||||
self.delete()
|
||||
return
|
||||
return QtGui.QGraphicsTextItem.focusOutEvent(self, event)
|
||||
return super().focusOutEvent(event)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -153,7 +156,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsTextItem.paint(self, painter, option, widget)
|
||||
super().paint(painter, option, widget)
|
||||
|
||||
if self.show_layer is False or self.parentItem():
|
||||
return
|
||||
@@ -169,7 +172,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
painter.drawText(QtCore.QPointF(center.x(), center.y()), zval)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
@@ -178,7 +181,7 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsTextItem.setZValue(self, value)
|
||||
super().setZValue(value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
@@ -186,6 +189,54 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def setStyle(self, styles):
|
||||
"""
|
||||
Set text style using a SVG style
|
||||
"""
|
||||
font = QtGui.QFont()
|
||||
for style in styles.split(";"):
|
||||
if ":" in style:
|
||||
key, val = style.split(":")
|
||||
key = key.strip()
|
||||
val = val.strip()
|
||||
|
||||
if key == "font-size":
|
||||
font.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()
|
||||
color.setBlue(new_color.blue())
|
||||
color.setRed(new_color.red())
|
||||
color.setGreen(new_color.green())
|
||||
self.setDefaultTextColor(color)
|
||||
elif key == "fill-opacity":
|
||||
color = self.defaultTextColor()
|
||||
color.setAlphaF(float(val))
|
||||
self.setDefaultTextColor(color)
|
||||
self.setFont(font)
|
||||
|
||||
def itemChange(self, change, value):
|
||||
"""
|
||||
Notifies this node item that some part of the item's state changes.
|
||||
|
||||
:param change: GraphicsItemChange type
|
||||
:param value: value of the change
|
||||
"""
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if value == 0:
|
||||
self.item_unselected_signal.emit()
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this note.
|
||||
@@ -194,63 +245,29 @@ class NoteItem(QtGui.QGraphicsTextItem):
|
||||
"""
|
||||
|
||||
note_info = {"text": self.toPlainText(),
|
||||
"x": self.x(),
|
||||
"y": self.y()}
|
||||
"x": int(self.x()),
|
||||
"y": int(self.y()),
|
||||
"rotation": int(self.rotation())}
|
||||
|
||||
note_info["font"] = self.font().toString()
|
||||
note_info["color"] = self.defaultTextColor().name()
|
||||
if self.rotation() != 0:
|
||||
note_info["rotation"] = self.rotation()
|
||||
if self.zValue() != 2:
|
||||
note_info["z"] = self.zValue()
|
||||
style = ""
|
||||
|
||||
style += "font-family: {};".format(self.font().family())
|
||||
style += "font-size: {};".format(self.font().pointSizeF())
|
||||
|
||||
if self.font().italic():
|
||||
style += "font-style: italic;"
|
||||
|
||||
if self.font().bold():
|
||||
style += "font-weight: bold;"
|
||||
|
||||
if self.font().strikeOut():
|
||||
style += "text-decoration: line-through;"
|
||||
elif self.font().underline():
|
||||
style += "text-decoration: underline;"
|
||||
|
||||
style += "fill: {};".format("#" + hex(self.defaultTextColor().rgba())[4:])
|
||||
style += "fill-opacity: {};".format(self.defaultTextColor().alphaF())
|
||||
|
||||
note_info["style"] = style
|
||||
|
||||
return note_info
|
||||
|
||||
def load(self, note_info):
|
||||
"""
|
||||
Loads a note representation
|
||||
(from a topology file).
|
||||
|
||||
:param note_info: representation of the note (dictionary)
|
||||
"""
|
||||
|
||||
# load mandatory properties
|
||||
text = note_info["text"]
|
||||
x = note_info["x"]
|
||||
y = note_info["y"]
|
||||
|
||||
self.setPlainText(text)
|
||||
self.setPos(x, y)
|
||||
|
||||
# load optional properties
|
||||
font = note_info.get("font")
|
||||
color = note_info.get("color")
|
||||
rotation = note_info.get("rotation")
|
||||
z = note_info.get("z")
|
||||
|
||||
if font:
|
||||
qt_font = QtGui.QFont()
|
||||
if qt_font.fromString(font):
|
||||
self.setFont(qt_font)
|
||||
if color:
|
||||
self.setDefaultTextColor(QtGui.QColor(color))
|
||||
if rotation is not None:
|
||||
self.setRotation(float(rotation))
|
||||
if z is not None:
|
||||
self.setZValue(z)
|
||||
|
||||
def duplicate(self):
|
||||
"""
|
||||
Duplicates this node item.
|
||||
|
||||
:return: NoteItem instance
|
||||
"""
|
||||
|
||||
note_item = NoteItem(self.parent())
|
||||
note_item.setPlainText(self.toPlainText())
|
||||
note_item.setPos(self.x() + 20, self.y() + 20)
|
||||
note_item.setZValue(self.zValue())
|
||||
note_item.setFont(self.font())
|
||||
note_item.setDefaultTextColor(self.defaultTextColor())
|
||||
note_item.setRotation(self.rotation())
|
||||
return note_item
|
||||
|
||||
@@ -19,35 +19,20 @@
|
||||
Graphical representation of a rectangle on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .shape_item import ShapeItem
|
||||
|
||||
|
||||
class RectangleItem(ShapeItem, QtGui.QGraphicsRectItem):
|
||||
class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
|
||||
"""
|
||||
Class to draw a rectangle on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, pos=None, width=200, height=100):
|
||||
|
||||
QtGui.QGraphicsRectItem.__init__(self, 0, 0, width, height)
|
||||
ShapeItem.__init__(self)
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.setPen(pen)
|
||||
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
|
||||
self.setBrush(brush)
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this rectangle.
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeRectangle(self)
|
||||
def __init__(self, width=200, height=100, **kws):
|
||||
super().__init__(width=width, height=height, **kws)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -58,19 +43,28 @@ class RectangleItem(ShapeItem, QtGui.QGraphicsRectItem):
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsRectItem.paint(self, painter, option, widget)
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def duplicate(self):
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Duplicates this rectangle item.
|
||||
|
||||
:return: RectangleItem instance
|
||||
Sets Z value of the item
|
||||
:param value: z layer
|
||||
"""
|
||||
return ShapeItem.setZValue(self, value)
|
||||
|
||||
rectangle_item = RectangleItem(QtCore.QPointF(self.x() + 20, self.y() + 20), self.rect().width(), self.rect().height())
|
||||
rectangle_item.setPen(self.pen())
|
||||
rectangle_item.setBrush(self.brush())
|
||||
rectangle_item.setZValue(self.zValue())
|
||||
rectangle_item.setRotation(self.rotation())
|
||||
return rectangle_item
|
||||
def toSvg(self):
|
||||
"""
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
svg = ET.Element("svg")
|
||||
svg.set("width", str(int(self.rect().width())))
|
||||
svg.set("height", str(int(self.rect().height())))
|
||||
|
||||
rect = ET.SubElement(svg, "rect")
|
||||
rect.set("width", str(int(self.rect().width())))
|
||||
rect.set("height", str(int(self.rect().height())))
|
||||
|
||||
rect = self._styleSvg(rect)
|
||||
|
||||
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
|
||||
@@ -20,7 +20,7 @@ Graphical representation of a Serial link on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import math
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .link_item import LinkItem
|
||||
from .note_item import NoteItem
|
||||
from ..ports.port import Port
|
||||
@@ -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):
|
||||
|
||||
LinkItem.__init__(self, source_item, source_port, destination_item, destination_port, link, adding_flag, multilink)
|
||||
super().__init__(source_item, source_port, destination_item, destination_port, link, adding_flag)
|
||||
|
||||
def adjust(self):
|
||||
"""
|
||||
@@ -79,8 +78,8 @@ class SerialLinkItem(LinkItem):
|
||||
scale_vect_diag = math.sqrt(scale_vect.x() ** 2 + scale_vect.y() ** 2)
|
||||
scale_coef = scale_vect_diag / 40.0
|
||||
|
||||
self.source = QtCore.QPointF(self.source.x() + scale_vect.x() / scale_coef, self.source.y() + scale_vect.y() / scale_coef)
|
||||
self.destination = QtCore.QPointF(self.destination.x() - scale_vect.x() / scale_coef, self.destination.y() - scale_vect.y() / scale_coef)
|
||||
self.source_point = QtCore.QPointF(self.source.x() + scale_vect.x() / scale_coef, self.source.y() + scale_vect.y() / scale_coef)
|
||||
self.destination_point = QtCore.QPointF(self.destination.x() - scale_vect.x() / scale_coef, self.destination.y() - scale_vect.y() / scale_coef)
|
||||
|
||||
def shape(self):
|
||||
"""
|
||||
@@ -89,11 +88,11 @@ class SerialLinkItem(LinkItem):
|
||||
:returns: QPainterPath instance
|
||||
"""
|
||||
|
||||
path = QtGui.QGraphicsPathItem.shape(self)
|
||||
path = QtWidgets.QGraphicsPathItem.shape(self)
|
||||
offset = self._point_size / 2
|
||||
point = self.source
|
||||
point = self.source_point
|
||||
path.addEllipse(point.x() - offset, point.y() - offset, self._point_size, self._point_size)
|
||||
point = self.destination
|
||||
point = self.destination_point
|
||||
path.addEllipse(point.x() - offset, point.y() - offset, self._point_size, self._point_size)
|
||||
return path
|
||||
|
||||
@@ -106,7 +105,7 @@ class SerialLinkItem(LinkItem):
|
||||
:param widget: QWidget instance.
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
|
||||
if not self._adding_flag and self._settings["draw_link_status_points"]:
|
||||
|
||||
@@ -115,67 +114,62 @@ class SerialLinkItem(LinkItem):
|
||||
return
|
||||
|
||||
# source point color
|
||||
if self._source_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
elif self._source_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
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
|
||||
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_label.setPlainText(self._source_port.shortName())
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, self.source))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
if not self._source_port.isStub():
|
||||
source_port_name = self._source_port.name().replace(self._source_port.longNameType(),
|
||||
self._source_port.shortNameType())
|
||||
else:
|
||||
source_port_name = self._source_port.name()
|
||||
source_port_label.setPlainText(source_port_name)
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, self.source))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
elif source_port_label and not source_port_label.isVisible():
|
||||
source_port_label.show()
|
||||
|
||||
elif source_port_label:
|
||||
source_port_label.show()
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.source)
|
||||
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
|
||||
elif self._destination_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
|
||||
destination_port_label = self._destination_port.label()
|
||||
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
destination_port_label.setPlainText(self._destination_port.shortName())
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, self.destination))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
if not self._destination_port.isStub():
|
||||
destination_port_name = self._destination_port.name().replace(self._destination_port.longNameType(),
|
||||
self._destination_port.shortNameType())
|
||||
else:
|
||||
destination_port_name = self._destination_port.name()
|
||||
destination_port_label.setPlainText(destination_port_name)
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, self.destination))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
elif destination_port_label and not destination_port_label.isVisible():
|
||||
destination_port_label.show()
|
||||
|
||||
elif destination_port_label:
|
||||
destination_port_label.show()
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.destination)
|
||||
painter.drawPoint(self.destination_point)
|
||||
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -19,46 +19,38 @@
|
||||
Base class for shape items (Rectangle, ellipse etc.).
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui
|
||||
import xml.etree.ElementTree as ET
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .drawing_item import DrawingItem
|
||||
from .utils import colorFromSvg
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShapeItem:
|
||||
class ShapeItem(DrawingItem):
|
||||
|
||||
"""
|
||||
Base class to draw shapes on the scene.
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
def __init__(self, width=200, height=200, svg=None, **kws):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemIsFocusable | QtGui.QGraphicsItem.ItemIsSelectable)
|
||||
self.setAcceptsHoverEvents(True)
|
||||
super().__init__(svg=svg, **kws)
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._border = 5
|
||||
self._edge = None
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._graphics_view = MainWindow.instance().uiGraphicsView
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
if key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() > -360.0:
|
||||
self.setRotation(self.rotation() - 1)
|
||||
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
if svg is None:
|
||||
self.setRect(0, 0, width, height)
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.setPen(pen)
|
||||
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
|
||||
self.setBrush(brush)
|
||||
else:
|
||||
QtGui.QGraphicsItem.keyPressEvent(self, event)
|
||||
self.fromSvg(svg)
|
||||
if self._id is None:
|
||||
self.create()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""
|
||||
@@ -69,22 +61,22 @@ class ShapeItem:
|
||||
|
||||
self.update()
|
||||
if event.pos().x() > (self.rect().right() - self._border):
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "right"
|
||||
|
||||
elif event.pos().x() < (self.rect().left() + self._border):
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "left"
|
||||
|
||||
elif event.pos().y() < (self.rect().top() + self._border):
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "top"
|
||||
|
||||
elif event.pos().y() > (self.rect().bottom() - self._border):
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "bottom"
|
||||
|
||||
QtGui.QGraphicsItem.mousePressEvent(self, event)
|
||||
QtWidgets.QGraphicsItem.mousePressEvent(self, event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""
|
||||
@@ -94,9 +86,9 @@ class ShapeItem:
|
||||
"""
|
||||
|
||||
self.update()
|
||||
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
|
||||
self._edge = None
|
||||
QtGui.QGraphicsItem.mouseReleaseEvent(self, event)
|
||||
QtWidgets.QGraphicsItem.mouseReleaseEvent(self, event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""
|
||||
@@ -145,7 +137,7 @@ class ShapeItem:
|
||||
self.setPos(scenePos.x(), self.y())
|
||||
self._edge = "left"
|
||||
|
||||
QtGui.QGraphicsItem.mouseMoveEvent(self, event)
|
||||
QtWidgets.QGraphicsItem.mouseMoveEvent(self, event)
|
||||
|
||||
def hoverMoveEvent(self, event):
|
||||
"""
|
||||
@@ -178,128 +170,32 @@ class ShapeItem:
|
||||
if self.zValue() >= 0:
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def drawLayerInfo(self, painter):
|
||||
def fromSvg(self, svg):
|
||||
"""
|
||||
Draws the layer position.
|
||||
|
||||
:param painter: QPainter instance
|
||||
Import element informations from an SVG
|
||||
"""
|
||||
|
||||
if self.show_layer is False:
|
||||
return
|
||||
|
||||
brect = self.boundingRect()
|
||||
# don't draw anything if the object is too small
|
||||
if brect.width() < 20 or brect.height() < 20:
|
||||
return
|
||||
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtGui.QGraphicsItem.setZValue(self, value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this shape item.
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
shape_info = {"width": self.rect().width(),
|
||||
"height": self.rect().height(),
|
||||
"x": self.x(),
|
||||
"y": self.y()}
|
||||
|
||||
brush = self.brush()
|
||||
if brush.color() != QtCore.Qt.white:
|
||||
shape_info["color"] = brush.color().name()
|
||||
if brush.color().alpha() != 255:
|
||||
shape_info["transparency"] = brush.color().alpha()
|
||||
|
||||
pen = self.pen()
|
||||
if pen.color() != QtCore.Qt.black:
|
||||
shape_info["border_color"] = pen.color().name()
|
||||
if pen.color().alpha() != 255:
|
||||
shape_info["border_transparency"] = pen.color().alpha()
|
||||
if pen.width() != 2:
|
||||
shape_info["border_width"] = pen.width()
|
||||
if pen.style() != QtCore.Qt.SolidLine:
|
||||
shape_info["border_style"] = pen.style()
|
||||
|
||||
if self.rotation() != 0:
|
||||
shape_info["rotation"] = self.rotation()
|
||||
if self.zValue() != 0:
|
||||
shape_info["z"] = self.zValue()
|
||||
|
||||
return shape_info
|
||||
|
||||
def load(self, shape_info):
|
||||
"""
|
||||
Loads a representation of this shape item.
|
||||
(from a topology file).
|
||||
|
||||
:param shape_info: representation of the shape item (dictionary)
|
||||
"""
|
||||
|
||||
# load mandatory properties
|
||||
width = shape_info["width"]
|
||||
height = shape_info["height"]
|
||||
x = shape_info["x"]
|
||||
y = shape_info["y"]
|
||||
svg = ET.fromstring(svg)
|
||||
width = float(svg.get("width", self.rect().width()))
|
||||
height = float(svg.get("height", self.rect().height()))
|
||||
self.setRect(0, 0, width, height)
|
||||
self.setPos(x, y)
|
||||
|
||||
# load optional properties
|
||||
z = shape_info.get("z")
|
||||
color = shape_info.get("color")
|
||||
transparency = shape_info.get("transparency")
|
||||
border_color = shape_info.get("border_color")
|
||||
border_transparency = shape_info.get("border_transparency")
|
||||
border_width = shape_info.get("border_width")
|
||||
border_style = shape_info.get("border_style")
|
||||
rotation = shape_info.get("rotation")
|
||||
pen = QtGui.QPen()
|
||||
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
|
||||
|
||||
if color:
|
||||
color = QtGui.QColor(color)
|
||||
else:
|
||||
color = QtGui.QColor(255, 255, 255)
|
||||
if transparency is not None:
|
||||
color.setAlpha(transparency)
|
||||
self.setBrush(QtGui.QBrush(color))
|
||||
if len(svg):
|
||||
pen = self._penFromSVGElement(svg[0])
|
||||
if svg[0].get("fill"):
|
||||
new_color = colorFromSvg(svg[0].get("fill"))
|
||||
color = brush.color()
|
||||
color.setBlue(new_color.blue())
|
||||
color.setRed(new_color.red())
|
||||
color.setGreen(new_color.green())
|
||||
brush.setColor(color)
|
||||
if svg[0].get("fill-opacity"):
|
||||
color = brush.color()
|
||||
color.setAlphaF(float(svg[0].get("fill-opacity")))
|
||||
brush.setColor(color)
|
||||
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
if border_color:
|
||||
border_color = QtGui.QColor(border_color)
|
||||
else:
|
||||
border_color = pen.color()
|
||||
if border_transparency:
|
||||
border_color.setAlpha(border_transparency)
|
||||
pen.setColor(border_color)
|
||||
if border_width is not None:
|
||||
pen.setWidth(int(border_width))
|
||||
if border_style is not None:
|
||||
pen.setStyle(QtCore.Qt.PenStyle(border_style))
|
||||
self.setPen(pen)
|
||||
|
||||
if rotation is not None:
|
||||
self.setRotation(rotation)
|
||||
if z is not None:
|
||||
self.setZValue(z)
|
||||
self.setBrush(brush)
|
||||
self.update()
|
||||
|
||||
210
gns3/items/text_item.py
Normal file
210
gns3/items/text_item.py
Normal file
@@ -0,0 +1,210 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a note on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from .drawing_item import DrawingItem
|
||||
from .utils import colorFromSvg
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
"""
|
||||
Text item for the QGraphicsView.
|
||||
"""
|
||||
|
||||
def __init__(self, svg=None, **kws):
|
||||
|
||||
super().__init__(**kws)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
|
||||
main_window = MainWindow.instance()
|
||||
view_settings = main_window.uiGraphicsView.settings()
|
||||
qt_font = QtGui.QFont()
|
||||
qt_font.fromString(view_settings["default_label_font"])
|
||||
self.setDefaultTextColor(QtGui.QColor(view_settings["default_label_color"]))
|
||||
self.setFont(qt_font)
|
||||
|
||||
if svg:
|
||||
try:
|
||||
svg = self.fromSvg(svg)
|
||||
except ET.ParseError as e:
|
||||
log.warning(str(e))
|
||||
|
||||
if self._id is None:
|
||||
self.create()
|
||||
|
||||
def editText(self):
|
||||
"""
|
||||
Edit mode for this note.
|
||||
"""
|
||||
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
|
||||
self.setSelected(True)
|
||||
self.setFocus()
|
||||
cursor = self.textCursor()
|
||||
cursor.select(QtGui.QTextCursor.Document)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
"""
|
||||
Handles all mouse double click events.
|
||||
|
||||
:param event: QMouseEvent instance
|
||||
"""
|
||||
|
||||
self.editText()
|
||||
|
||||
def focusOutEvent(self, event):
|
||||
"""
|
||||
Handles all focus out events.
|
||||
|
||||
:param event: QFocusEvent instance
|
||||
"""
|
||||
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False)
|
||||
cursor = self.textCursor()
|
||||
if cursor.hasSelection():
|
||||
cursor.clearSelection()
|
||||
self.setTextCursor(cursor)
|
||||
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
|
||||
if not self.toPlainText():
|
||||
# delete the note if empty
|
||||
self.delete()
|
||||
return
|
||||
else:
|
||||
self.updateDrawing()
|
||||
return super().focusOutEvent(event)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
Paints the contents of an item in local coordinates.
|
||||
|
||||
:param painter: QPainter instance
|
||||
:param option: QStyleOptionGraphicsItem instance
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def 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
|
||||
"""
|
||||
svg = ET.Element("svg")
|
||||
svg.set("width", str(int(self.boundingRect().width())))
|
||||
svg.set("height", str(int(self.boundingRect().height())))
|
||||
|
||||
text = ET.SubElement(svg, "text")
|
||||
text.set("font-family", self.font().family())
|
||||
text.set("font-size", str(self.font().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()
|
||||
|
||||
svg = ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
return svg
|
||||
|
||||
def fromSvg(self, 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()
|
||||
color = text.get("fill")
|
||||
if color:
|
||||
new_color = colorFromSvg(color)
|
||||
color = self.defaultTextColor()
|
||||
color.setBlue(new_color.blue())
|
||||
color.setRed(new_color.red())
|
||||
color.setGreen(new_color.green())
|
||||
self.setDefaultTextColor(color)
|
||||
|
||||
opacity = text.get("fill-opacity")
|
||||
if opacity:
|
||||
color = self.defaultTextColor()
|
||||
color.setAlphaF(float(opacity))
|
||||
self.setDefaultTextColor(color)
|
||||
|
||||
font.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)
|
||||
|
||||
def editable(self):
|
||||
"""
|
||||
Returns either the note is editable or not.
|
||||
|
||||
:return: boolean
|
||||
"""
|
||||
|
||||
return True
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
if not self.handleKeyPressEvent(event):
|
||||
super().keyPressEvent(event)
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2013 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -15,33 +15,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Interface for dummy NIOs (mostly for tests or fake connections).
|
||||
"""
|
||||
|
||||
from .nio import NIO
|
||||
from ..qt import QtGui
|
||||
|
||||
|
||||
class NIONull(NIO):
|
||||
|
||||
def colorFromSvg(value):
|
||||
"""
|
||||
NULL NIO.
|
||||
Transform a color coming from a SVG file to a Qcolor
|
||||
"""
|
||||
|
||||
def __init__(self, identifier):
|
||||
|
||||
NIO.__init__(self)
|
||||
self._identifier = identifier
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "NIO_Null"
|
||||
|
||||
def identifier(self):
|
||||
"""
|
||||
Returns the identifier used by this NIO.
|
||||
|
||||
:returns: the identifier
|
||||
"""
|
||||
|
||||
return self._identifier
|
||||
value = value.strip('#')
|
||||
if value == "":
|
||||
value = "000000"
|
||||
if len(value) == 6: # If alpha channel is missing
|
||||
value = "ff" + value
|
||||
value = int(value, base=16)
|
||||
return QtGui.QColor.fromRgba(value)
|
||||
580
gns3/link.py
580
gns3/link.py
@@ -19,9 +19,14 @@
|
||||
Manages and stores everything needed for a connection between 2 devices.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sip
|
||||
import uuid
|
||||
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .controller import Controller
|
||||
|
||||
from .qt import QtCore
|
||||
from .nios.nio_udp import NIOUDP
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -36,24 +41,28 @@ class Link(QtCore.QObject):
|
||||
:param source_port: source Port instance
|
||||
:param destination_node: destination Node instance
|
||||
:param destination_port: destination Port instance
|
||||
:param stub: indicates if the link is connected to a stub device like a Cloud
|
||||
"""
|
||||
|
||||
# signals used to let the GUI view know about link
|
||||
# additions and deletions.
|
||||
add_link_signal = QtCore.Signal(int)
|
||||
delete_link_signal = QtCore.Signal(int)
|
||||
updated_link_signal = QtCore.Signal(int)
|
||||
error_link_signal = QtCore.Signal(int)
|
||||
|
||||
_instance_count = 1
|
||||
|
||||
def __init__(self, source_node, source_port, destination_node, destination_port):
|
||||
def __init__(self, source_node, source_port, destination_node, destination_port, link_id=None, **link_data):
|
||||
"""
|
||||
:param link_data: Link information from the API
|
||||
"""
|
||||
|
||||
super(Link, self).__init__()
|
||||
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
|
||||
@@ -63,53 +72,206 @@ class Link(QtCore.QObject):
|
||||
self._source_port = source_port
|
||||
self._destination_node = destination_node
|
||||
self._destination_port = destination_port
|
||||
self._source_nio = None
|
||||
self._destination_nio = None
|
||||
self._source_nio_active = False
|
||||
self._destination_nio_active = False
|
||||
self._source_label = None
|
||||
self._destination_label = None
|
||||
self._link_id = link_id
|
||||
self._capturing = False
|
||||
self._deleting = False
|
||||
self._capture_file_path = None
|
||||
self._capture_file = None
|
||||
self._initialized = False
|
||||
self._filters = {}
|
||||
self._suspend = False
|
||||
|
||||
if source_port.isStub() or destination_port.isStub():
|
||||
self._stub = True
|
||||
# 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 information when reloading
|
||||
self._creator = False
|
||||
|
||||
self._nodes = []
|
||||
|
||||
self._source_node.addLink(self)
|
||||
self._destination_node.addLink(self)
|
||||
|
||||
body = self._prepareParams()
|
||||
if self._link_id:
|
||||
link_data["link_id"] = self._link_id
|
||||
self._linkCreatedCallback(link_data)
|
||||
else:
|
||||
self._stub = False
|
||||
self._link_id = str(uuid.uuid4())
|
||||
self._creator = True
|
||||
Controller.instance().post("/projects/{project_id}/links".format(project_id=source_node.project().id()), self._linkCreatedCallback, body=body)
|
||||
|
||||
# we must request UDP information if the NIO is a NIO UDP and before
|
||||
# it can be created.
|
||||
if not self._stub:
|
||||
def _parseResponse(self, result):
|
||||
self._capturing = result.get("capturing", False)
|
||||
|
||||
# connect signals used when a NIO has been created by a node
|
||||
# and this NIO need to be attached to a port connected to this link
|
||||
source_node.nio_signal.connect(self.newNIOSlot)
|
||||
destination_node.nio_signal.connect(self.newNIOSlot)
|
||||
|
||||
# currently, we support only NIO_UDP for normal connections (non-stub).
|
||||
if not source_port.defaultNio() == NIOUDP:
|
||||
raise NotImplementedError()
|
||||
|
||||
self._source_udp = None
|
||||
self._destination_udp = None
|
||||
|
||||
# connect signals used to receive a UDP port and host allocated by a node
|
||||
source_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
|
||||
destination_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
|
||||
|
||||
# request the UDP info for each node
|
||||
source_node.allocateUDPPort(self._source_port.id())
|
||||
destination_node.allocateUDPPort(self._destination_port.id())
|
||||
else:
|
||||
# handle stub connections (to a cloud for instance).
|
||||
if not source_port.isStub() and destination_port.isStub():
|
||||
source_node.nio_signal.connect(self.newNIOSlot)
|
||||
self._source_nio = self._destination_port.defaultNio()
|
||||
self._source_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._source_node.addNIO(self._source_port, self._source_nio)
|
||||
elif not destination_port.isStub() and source_port.isStub():
|
||||
destination_node.nio_signal.connect(self.newNIOSlot)
|
||||
self._destination_nio = self._source_port.defaultNio()
|
||||
self._destination_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._destination_node.addNIO(self._destination_port, self._destination_nio)
|
||||
# If the controller is remote the capture path should be rewrite to something local
|
||||
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:
|
||||
log.error("both ports are stub!")
|
||||
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
|
||||
|
||||
def addPortLabel(self, port, label):
|
||||
if port.adapterNumber() == self._source_port.adapterNumber() and port.portNumber() == self._source_port.portNumber() and port.destinationNode() == self._destination_node:
|
||||
self._source_label = label
|
||||
else:
|
||||
self._destination_label = label
|
||||
label.item_unselected_signal.connect(self.update)
|
||||
if self.creator():
|
||||
self.update()
|
||||
else:
|
||||
self._updateLabels()
|
||||
|
||||
def update(self):
|
||||
if not self._link_id or self.deleting():
|
||||
return
|
||||
body = self._prepareParams()
|
||||
Controller.instance().put("/projects/{project_id}/links/{link_id}".format(project_id=self._source_node.project().id(), link_id=self._link_id), self.updateLinkCallback, body=body)
|
||||
|
||||
def 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"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
def _updateLabels(self):
|
||||
for node in self._nodes:
|
||||
if node["node_id"] == self._source_node.node_id() and node["adapter_number"] == self._source_port.adapterNumber() and node["port_number"] == self._source_port.portNumber():
|
||||
self._updateLabel(self._source_label, node["label"])
|
||||
elif node["node_id"] == self._destination_node.node_id() and node["adapter_number"] == self._destination_port.adapterNumber() and node["port_number"] == self._destination_port.portNumber():
|
||||
self._updateLabel(self._destination_label, node["label"])
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def _updateLabel(self, label, label_data):
|
||||
if not label or sip.isdeleted(label):
|
||||
return
|
||||
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 = {
|
||||
"nodes": [
|
||||
{
|
||||
"node_id": self._source_node.node_id(),
|
||||
"adapter_number": self._source_port.adapterNumber(),
|
||||
"port_number": self._source_port.portNumber(),
|
||||
},
|
||||
{
|
||||
"node_id": self._destination_node.node_id(),
|
||||
"adapter_number": self._destination_port.adapterNumber(),
|
||||
"port_number": self._destination_port.portNumber()
|
||||
}
|
||||
],
|
||||
"filters": self._filters,
|
||||
"suspend": self._suspend
|
||||
}
|
||||
if self._source_port.label():
|
||||
body["nodes"][0]["label"] = self._source_port.label().dump()
|
||||
if self._destination_port.label():
|
||||
body["nodes"][1]["label"] = self._destination_port.label().dump()
|
||||
return body
|
||||
|
||||
def _linkCreatedCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Create link", "Error while creating link: {}".format(result["message"]))
|
||||
self.deleteLink(skip_controller=True)
|
||||
return
|
||||
|
||||
self._initialized = True
|
||||
|
||||
# let the GUI know about this link has been created
|
||||
self.add_link_signal.emit(self._id)
|
||||
self._source_port.setLinkId(self._id)
|
||||
self._source_port.setLink(self)
|
||||
self._source_port.setDestinationNode(self._destination_node)
|
||||
self._source_port.setDestinationPort(self._destination_port)
|
||||
self._destination_port.setLinkId(self._id)
|
||||
self._destination_port.setLink(self)
|
||||
self._destination_port.setDestinationNode(self._source_node)
|
||||
self._destination_port.setDestinationPort(self._source_port)
|
||||
|
||||
self._link_id = result["link_id"]
|
||||
self._parseResponse(result)
|
||||
|
||||
def link_id(self):
|
||||
return self._link_id
|
||||
|
||||
def deleting(self):
|
||||
"""
|
||||
Is the link being deleted
|
||||
"""
|
||||
return self._deleting
|
||||
|
||||
def setDeleting(self):
|
||||
"""
|
||||
Mark this link as being deleted
|
||||
"""
|
||||
|
||||
self._deleting = True
|
||||
|
||||
def capturing(self):
|
||||
"""
|
||||
Is a capture running on the link?
|
||||
"""
|
||||
return self._capturing
|
||||
|
||||
def capture_file_path(self):
|
||||
"""
|
||||
Path of the capture file
|
||||
"""
|
||||
return self._capture_file_path
|
||||
|
||||
def project(self):
|
||||
return self._source_node.project()
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
@@ -121,34 +283,128 @@ 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())
|
||||
|
||||
def deleteLink(self):
|
||||
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):
|
||||
"""
|
||||
:returns: File name for a capture on this link
|
||||
"""
|
||||
capture_file_name = "{}_{}_to_{}_{}".format(
|
||||
self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
return re.sub("[^0-9A-Za-z_-]", "", capture_file_name)
|
||||
|
||||
def deleteLink(self, skip_controller=False):
|
||||
"""
|
||||
Deletes this link.
|
||||
"""
|
||||
|
||||
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({})
|
||||
else:
|
||||
self.setDeleting()
|
||||
Controller.instance().delete("/projects/{project_id}/links/{link_id}".format(project_id=self.project().id(),
|
||||
link_id=self._link_id),
|
||||
self._linkDeletedCallback)
|
||||
|
||||
def _linkDeletedCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Called after the link is remove from the topology
|
||||
"""
|
||||
if error:
|
||||
log.error("Error while deleting link: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
# delete the NIOs on both source and destination nodes
|
||||
if self._source_port.nio():
|
||||
self._source_node.deleteNIO(self._source_port)
|
||||
self._source_port.setFree()
|
||||
self._source_node.deleteLink(self)
|
||||
self._source_node.updated_signal.emit()
|
||||
if self._destination_port.nio():
|
||||
self._destination_node.deleteNIO(self._destination_port)
|
||||
self._destination_port.setFree()
|
||||
self._destination_node.deleteLink(self)
|
||||
self._destination_node.updated_signal.emit()
|
||||
|
||||
# let the GUI know about this link has been deleted
|
||||
self.delete_link_signal.emit(self._id)
|
||||
|
||||
def startCapture(self, data_link_type, capture_file_name):
|
||||
data = {
|
||||
"capture_file_name": capture_file_name,
|
||||
"data_link_type": data_link_type
|
||||
}
|
||||
Controller.instance().post(
|
||||
"/projects/{project_id}/links/{link_id}/start_capture".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id),
|
||||
self._startCaptureCallback,
|
||||
body=data)
|
||||
|
||||
def _startCaptureCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while starting capture on link: {}".format(result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
def _downloadPcapProgress(self, content, server=None, context={}, **kwargs):
|
||||
"""
|
||||
Called for each part of the file of the PCAP
|
||||
"""
|
||||
if not self._capture_file_path:
|
||||
return
|
||||
self._capture_file.write(content)
|
||||
self._capture_file.flush()
|
||||
|
||||
def stopCapture(self):
|
||||
if Controller.instance().isRemote():
|
||||
if self._capture_file:
|
||||
self._capture_file.close()
|
||||
self._capture_file = None
|
||||
if self._capture_file_path 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, e))
|
||||
self._capture_file_path = None
|
||||
Controller.instance().post(
|
||||
"/projects/{project_id}/links/{link_id}/stop_capture".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id),
|
||||
self._stopCaptureCallback)
|
||||
|
||||
def _stopCaptureCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while stopping capture on link: {}".format(result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
def get(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP Get from a link
|
||||
"""
|
||||
Controller.instance().get(
|
||||
"/projects/{project_id}/links/{link_id}{path}".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id,
|
||||
path=path),
|
||||
callback,
|
||||
**kwargs)
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
Returns this link identifier.
|
||||
@@ -194,194 +450,24 @@ class Link(QtCore.QObject):
|
||||
|
||||
return self._destination_port
|
||||
|
||||
def UDPPortAllocatedSlot(self, node_id, port_id, lport):
|
||||
def getNodePort(self, node):
|
||||
"""
|
||||
Slot to receive events from Node instances
|
||||
when a UDP port has been allocated in order to create a NIO UDP.
|
||||
Search the port in the link corresponding to this node
|
||||
|
||||
:param node_id: node identifier
|
||||
:param port_id: port identifier
|
||||
:param lport: local UDP port
|
||||
:returns: Node instance
|
||||
"""
|
||||
if self._destination_node == node:
|
||||
return self._destination_port
|
||||
return self._source_port
|
||||
|
||||
# check that the node is connected to this link as a source
|
||||
if node_id == self._source_node.id() and port_id == self._source_port.id():
|
||||
laddr = self._source_node.server().host
|
||||
self._source_udp = (lport, laddr)
|
||||
# disconnect the signal has we don't expect new source UDP info for this link.
|
||||
self._source_node.allocate_udp_nio_signal.disconnect(self.UDPPortAllocatedSlot)
|
||||
|
||||
log.debug("{} has allocated UDP port {} for host {}".format(self._source_node.name(),
|
||||
lport,
|
||||
laddr))
|
||||
|
||||
# check that the node is connected to this link as a destination
|
||||
elif node_id == self._destination_node.id() and port_id == self._destination_port.id():
|
||||
laddr = self._destination_node.server().host
|
||||
self._destination_udp = (lport, laddr)
|
||||
# disconnect the signal has we don't expect new source UDP info for this link.
|
||||
self._destination_node.allocate_udp_nio_signal.disconnect(self.UDPPortAllocatedSlot)
|
||||
|
||||
log.debug("{} has allocated UDP port {} for host {}".format(self._destination_node.name(),
|
||||
lport,
|
||||
laddr))
|
||||
|
||||
if self._source_udp and self._destination_udp:
|
||||
|
||||
# we got UDP info from both source and destination nodes
|
||||
# meaning we can proceed with the creation of UDP NIOs
|
||||
lport, laddr = self._source_udp
|
||||
rport, raddr = self._destination_udp
|
||||
|
||||
self._source_nio = NIOUDP(lport, raddr, rport)
|
||||
self._destination_nio = NIOUDP(rport, laddr, lport)
|
||||
|
||||
self._source_udp = None
|
||||
self._destination_udp = None
|
||||
|
||||
log.debug("creating UDP tunnel from {}:{} to {}:{} ".format(laddr, lport, raddr, rport))
|
||||
|
||||
# add the UDP NIOs to the nodes
|
||||
self._source_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._source_node.addNIO(self._source_port, self._source_nio)
|
||||
self._destination_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._destination_node.addNIO(self._destination_port, self._destination_nio)
|
||||
|
||||
def newNIOSlot(self, node_id, port_id):
|
||||
def filters(self):
|
||||
"""
|
||||
Slot to receive events from Node instances
|
||||
when a NIO has been created on the server
|
||||
and are active.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param port_id: port identifier
|
||||
:returns: List the filters active on the node
|
||||
"""
|
||||
return self._filters
|
||||
|
||||
# in very rare cases link is already deleted
|
||||
if self is None:
|
||||
return
|
||||
|
||||
# check that the node is connected to this link as a source
|
||||
if node_id == self._source_node.id() and port_id == self._source_port.id():
|
||||
self._source_nio_active = True
|
||||
# disconnect the signal has we don't expect new source NIO for this link.
|
||||
self._source_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
|
||||
# check that the node is connected to this link as a destination
|
||||
elif node_id == self._destination_node.id() and port_id == self._destination_port.id():
|
||||
self._destination_nio_active = True
|
||||
# disconnect the signal has we don't expect new destination NIO for this link.
|
||||
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
|
||||
if not self._stub and self._source_nio_active and self._destination_nio_active:
|
||||
# both NIOs are active now.
|
||||
self._addToSourcePort(self._source_nio)
|
||||
self._addToDestinationPort(self._destination_nio)
|
||||
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self._source_nio_active = False
|
||||
self._destination_nio_active = False
|
||||
|
||||
# let the GUI know about this link has been created
|
||||
self.add_link_signal.emit(self._id)
|
||||
elif self._stub and self._source_nio_active:
|
||||
self._addToSourcePort(self._source_nio)
|
||||
# add the NIO to destination to show the port is not free.
|
||||
self._addToDestinationPort(self._source_nio)
|
||||
self._source_nio_active = False
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self.add_link_signal.emit(self._id)
|
||||
elif self._stub and self._destination_nio_active:
|
||||
# add the NIO to source to show the port is not free.
|
||||
self._addToSourcePort(self._destination_nio)
|
||||
self._addToDestinationPort(self._destination_nio)
|
||||
self._destination_nio_active = False
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self.add_link_signal.emit(self._id)
|
||||
|
||||
def _addToSourcePort(self, nio):
|
||||
def setFilters(self, filters):
|
||||
"""
|
||||
Adds a NIO, a link id and a description to the source port.
|
||||
|
||||
:param nio: NIO instance
|
||||
:params filters: List of filters
|
||||
"""
|
||||
|
||||
self._source_port.setNio(nio)
|
||||
self._source_port.setLinkId(self._id)
|
||||
self._source_port.setDestinationNode(self._destination_node)
|
||||
self._source_port.setDestinationPort(self._destination_port)
|
||||
|
||||
log.debug("{} attached to {} on port {}".format(nio,
|
||||
self._source_node.name(),
|
||||
self._source_port.name()))
|
||||
|
||||
def _addToDestinationPort(self, nio):
|
||||
"""
|
||||
Adds a NIO, a link id and a description to the destination port.
|
||||
|
||||
:param nio: NIO instance
|
||||
"""
|
||||
|
||||
self._destination_port.setNio(nio)
|
||||
self._destination_port.setLinkId(self._id)
|
||||
self._destination_port.setDestinationNode(self._source_node)
|
||||
self._destination_port.setDestinationPort(self._source_port)
|
||||
|
||||
log.debug("{} attached to {} on port {}".format(nio,
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name()))
|
||||
|
||||
def cancelNIOSlot(self, node_id):
|
||||
"""
|
||||
Slot to receive events from Node instances
|
||||
when a NIO has been canceled because of an
|
||||
error returned by the server.
|
||||
|
||||
:param node_id: node identifier
|
||||
"""
|
||||
|
||||
if not self._stub:
|
||||
|
||||
try:
|
||||
# the destination node has canceled its NIO allocation
|
||||
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
except TypeError:
|
||||
# ignore TypeError: 'method' object is not connected
|
||||
pass
|
||||
|
||||
try:
|
||||
# the source node has canceled its NIO allocation
|
||||
self._source_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
except TypeError:
|
||||
# ignore TypeError: 'method' object is not connected
|
||||
pass
|
||||
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
else:
|
||||
if self._source_node.id() == node_id:
|
||||
self._source_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
else:
|
||||
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
|
||||
self._source_nio_active = False
|
||||
self._destination_nio_active = False
|
||||
self.deleteLink()
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this link.
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
return {"id": self.id(),
|
||||
"description": str(self),
|
||||
"source_node_id": self._source_node.id(),
|
||||
"source_port_id": self._source_port.id(),
|
||||
"destination_node_id": self._destination_node.id(),
|
||||
"destination_port_id": self._destination_port.id(),
|
||||
}
|
||||
self._filters = filters
|
||||
|
||||
@@ -18,56 +18,88 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import copy
|
||||
|
||||
import psutil
|
||||
|
||||
from .qt import QtCore, QtWidgets, qslot
|
||||
from .version import __version__
|
||||
from .utils import parse_version
|
||||
from .controller import Controller
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalConfig:
|
||||
class LocalConfig(QtCore.QObject):
|
||||
|
||||
"""
|
||||
Handles the local GUI settings.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
config_changed_signal = QtCore.Signal()
|
||||
# When this signal is emit the config is saved on controller
|
||||
save_on_controller_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, config_file=None):
|
||||
"""
|
||||
:param config_file: Path to the config file (override all other config, usefull for tests)
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self._profile = None
|
||||
self._config_file = config_file
|
||||
# Security to avoid pushing to the controller settings before
|
||||
# we get the original settings from controller
|
||||
self._settings_retrieved_from_controller = False
|
||||
self._migrateOldConfigPath()
|
||||
self._resetLoadConfig()
|
||||
self._monitoring_changes = False
|
||||
Controller.instance().connected_signal.connect(self.refreshConfigFromController)
|
||||
self.save_on_controller_signal.connect(self._saveOnController)
|
||||
|
||||
def _monitorChanges(self):
|
||||
"""
|
||||
Poll the remote server waiting for settings update
|
||||
"""
|
||||
if self._monitoring_changes:
|
||||
return
|
||||
self._monitoring_changes = True
|
||||
self._timer = QtCore.QTimer()
|
||||
self._timer.setInterval(5000)
|
||||
self._refreshingSettings = False
|
||||
self._timer.timeout.connect(self.refreshConfigFromController)
|
||||
self._timer.start()
|
||||
|
||||
def _resetLoadConfig(self):
|
||||
"""
|
||||
Reload the config from scratch everything is clean
|
||||
|
||||
"""
|
||||
self._settings = {}
|
||||
|
||||
self._last_config_changed = None
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_gui.ini"
|
||||
else:
|
||||
filename = "gns3_gui.conf"
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
appname = "gns3.net"
|
||||
else:
|
||||
appname = "GNS3"
|
||||
appname = "GNS3"
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
|
||||
# On windows, the system wide configuration file location is %COMMON_APPDATA%/GNS3/gns3_gui.conf
|
||||
common_appdata = os.path.expandvars("%COMMON_APPDATA%")
|
||||
system_wide_config_file = os.path.join(common_appdata, appname, filename)
|
||||
|
||||
# On windows, the user specific configuration file location is %APPDATA%/GNS3/gns3_gui.conf
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
self._config_file = os.path.join(appdata, appname, filename)
|
||||
|
||||
else:
|
||||
|
||||
# On UNIX-like platforms, the system wide configuration file location is /etc/xdg/GNS3/gns3_gui.conf
|
||||
system_wide_config_file = os.path.join("/etc/xdg", appname, filename)
|
||||
|
||||
# On UNIX-like platforms, the user specific configuration file location is /etc/xdg/GNS3/gns3_gui.conf
|
||||
home = os.path.expanduser("~")
|
||||
self._config_file = os.path.join(home, ".config", appname, filename)
|
||||
if not self._config_file:
|
||||
self._config_file = os.path.join(self.configDirectory(), filename)
|
||||
|
||||
# First load system wide settings
|
||||
if os.path.exists(system_wide_config_file):
|
||||
self._settings = self._readConfig(system_wide_config_file)
|
||||
if not self._settings:
|
||||
log.warning("No system wide settings loaded from {}".format(system_wide_config_file))
|
||||
self._readConfig(system_wide_config_file)
|
||||
|
||||
config_file_in_cwd = os.path.join(os.getcwd(), filename)
|
||||
if os.path.exists(config_file_in_cwd):
|
||||
@@ -85,32 +117,227 @@ class LocalConfig:
|
||||
user_settings = self._readConfig(self._config_file)
|
||||
# overwrite system wide settings with user specific ones
|
||||
self._settings.update(user_settings)
|
||||
self._writeConfig()
|
||||
self._migrateOldConfig()
|
||||
self.writeConfig()
|
||||
|
||||
def profile(self):
|
||||
"""
|
||||
:returns: Current settings profile
|
||||
"""
|
||||
return self._profile
|
||||
|
||||
def setProfile(self, profile):
|
||||
previous_profile = self._profile
|
||||
if profile == "default":
|
||||
self._profile = None
|
||||
else:
|
||||
self._profile = profile
|
||||
if previous_profile != self._profile:
|
||||
self._config_file = None
|
||||
self._resetLoadConfig()
|
||||
|
||||
@qslot
|
||||
def refreshConfigFromController(self):
|
||||
"""
|
||||
Refresh the configuration from the controller
|
||||
"""
|
||||
controller = Controller.instance()
|
||||
if controller.connected():
|
||||
self._refreshingSettings = True
|
||||
controller.get("/settings", self._getSettingsCallback, showProgress=False)
|
||||
self._monitorChanges()
|
||||
|
||||
def _getSettingsCallback(self, result, error=False, **kwargs):
|
||||
self._refreshingSettings = False
|
||||
if error:
|
||||
log.debug("Can't get settings from controller")
|
||||
return
|
||||
if result == {} and self._settings != {}:
|
||||
self._settings_retrieved_from_controller = True
|
||||
self.save_on_controller_signal.emit()
|
||||
return
|
||||
|
||||
# The server return an uuid to keep track of settings version
|
||||
if self._settings.get("modification_uuid") != result.get("modification_uuid"):
|
||||
self._settings.update(result)
|
||||
# Update already loaded section
|
||||
for section in self._settings.keys():
|
||||
if isinstance(self._settings[section], dict):
|
||||
self.loadSectionSettings(section, self._settings[section])
|
||||
self.config_changed_signal.emit()
|
||||
self._settings_retrieved_from_controller = True
|
||||
|
||||
def configDirectory(self):
|
||||
"""
|
||||
Get the configuration directory
|
||||
"""
|
||||
if sys.platform.startswith("win"):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
path = os.path.join(appdata, "GNS3")
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
path = os.path.join(home, ".config", "GNS3")
|
||||
|
||||
if self._profile is not None:
|
||||
path = os.path.join(path, "profiles", self._profile)
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
# In < 1.4 on Mac the config was in a gns3.net directory
|
||||
# We have move to same location as Linux
|
||||
if sys.platform.startswith("darwin"):
|
||||
old_path = os.path.join(os.path.expanduser("~"), ".config", "gns3.net")
|
||||
new_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3")
|
||||
if os.path.exists(old_path) and not os.path.exists(new_path):
|
||||
try:
|
||||
shutil.copytree(old_path, new_path)
|
||||
except OSError as e:
|
||||
log.error("Can't copy the old config: %s", str(e))
|
||||
|
||||
def _migrateOldConfig(self):
|
||||
"""
|
||||
Migrate pre 1.4 config
|
||||
"""
|
||||
|
||||
# Display an error if settings come from a more recent version of GNS3
|
||||
# patch level version are compatible (ex 1.5.3 and 1.5.2). But if you open
|
||||
# settings from 1.6.1 with 1.5.1 you will have an error
|
||||
if "version" in self._settings:
|
||||
if parse_version(self._settings["version"])[:2] > parse_version(__version__)[:2]:
|
||||
QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
|
||||
QtWidgets.QMessageBox.critical(None, "Version error", "Your settings are for version {} of GNS3. You cannot use a previous version of GNS3 without risking losing data. If you want to reset delete the settings in {}".format(self._settings["version"], self.configDirectory()))
|
||||
# Exit immediately not clean but we want to avoid any side effect that could corrupt the file
|
||||
sys.exit(1)
|
||||
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.0alpha1"):
|
||||
|
||||
servers = self._settings.get("Servers", {})
|
||||
|
||||
if "LocalServer" in self._settings:
|
||||
servers["local_server"] = copy.copy(self._settings["LocalServer"])
|
||||
|
||||
# We migrate the server binary for OSX due to the change from py2app to CX freeze
|
||||
if servers["local_server"]["path"] == "/Applications/GNS3.app/Contents/Resources/server/Contents/MacOS/gns3server":
|
||||
servers["local_server"]["path"] = "gns3server"
|
||||
|
||||
if "RemoteServers" in self._settings:
|
||||
servers["remote_servers"] = copy.copy(self._settings["RemoteServers"])
|
||||
|
||||
self._settings["Servers"] = servers
|
||||
|
||||
if "GUI" in self._settings:
|
||||
main_window = self._settings.get("MainWindow", {})
|
||||
main_window["hide_getting_started_dialog"] = self._settings["GUI"].get("hide_getting_started_dialog", False)
|
||||
self._settings["MainWindow"] = main_window
|
||||
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.1dev2"):
|
||||
if sys.platform.startswith("darwin"):
|
||||
from .settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, DEFAULT_TELNET_CONSOLE_COMMAND
|
||||
|
||||
if "MainWindow" in self._settings:
|
||||
if self._settings["MainWindow"].get("telnet_console_command") not in PRECONFIGURED_TELNET_CONSOLE_COMMANDS.values():
|
||||
self._settings["MainWindow"]["telnet_console_command"] = DEFAULT_TELNET_CONSOLE_COMMAND
|
||||
|
||||
# Migrate 1.X to 2.0
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
|
||||
if "Qemu" in self._settings:
|
||||
# The internet VM is replaced by the nat Node
|
||||
# we remove it from the list of available VM
|
||||
vms = []
|
||||
for vm in self._settings["Qemu"].get("vms", []):
|
||||
if vm.get("hda_disk_image") != "core-linux-6.4-internet-0.1.img":
|
||||
vms.append(vm)
|
||||
self._settings["Qemu"]["vms"] = vms
|
||||
|
||||
# Starting with 2.0.0dev5 IOU licence is stored in the settings
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
|
||||
if "IOU" in self._settings and "iourc_path" in self._settings["IOU"] and "iourc_content" not in self._settings["IOU"]:
|
||||
try:
|
||||
with open(self._settings["IOU"]["iourc_path"], "r", encoding="utf-8") as f:
|
||||
self._settings["IOU"]["iourc_content"] = f.read().replace("\r\n", "\n")
|
||||
del self._settings["IOU"]["iourc_path"]
|
||||
except OSError as e:
|
||||
log.warning("Can't import IOU licence {}: {}".format(self._settings["IOU"]["iourc_path"], str(e)))
|
||||
except UnicodeDecodeError as e:
|
||||
log.warning("Non ascii characters in iourc file {}, please remove them: {}".format(self._settings["IOU"]["iourc_path"], str(e)))
|
||||
|
||||
def _readConfig(self, config_path):
|
||||
"""
|
||||
Read the configuration file.
|
||||
"""
|
||||
|
||||
log.debug("Load config from %s", config_path)
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
self._last_config_changed = os.stat(config_path).st_mtime
|
||||
config = json.load(f)
|
||||
self._settings.update(config)
|
||||
except (ValueError, OSError) as e:
|
||||
log.error("Could not read the config file {}: {}".format(self._config_file, e))
|
||||
|
||||
# Update already loaded section
|
||||
for section in self._settings.keys():
|
||||
if isinstance(self._settings[section], dict):
|
||||
self.loadSectionSettings(section, self._settings[section])
|
||||
|
||||
return dict()
|
||||
|
||||
def _writeConfig(self):
|
||||
def writeConfig(self):
|
||||
"""
|
||||
Write the configuration file.
|
||||
"""
|
||||
|
||||
self._settings["version"] = __version__
|
||||
try:
|
||||
with open(self._config_file, "w", encoding="utf-8") as f:
|
||||
temporary = os.path.join(os.path.dirname(self._config_file), "gns3_gui.tmp")
|
||||
with open(temporary, "w", encoding="utf-8") as f:
|
||||
json.dump(self._settings, f, sort_keys=True, indent=4)
|
||||
shutil.move(temporary, self._config_file)
|
||||
log.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.save_on_controller_signal.emit()
|
||||
|
||||
@qslot
|
||||
def _saveOnController(self, *args):
|
||||
"""
|
||||
Save some settings on controller for the transition from
|
||||
GUI to a central controller. Will be removed later
|
||||
"""
|
||||
if Controller.instance().connected() and self._settings_retrieved_from_controller:
|
||||
# We save only non user specific sections
|
||||
section_to_save_on_controller = ["Builtin", "Docker", "IOU", "Qemu", "VMware", "VPCS", "TraceNG", "VirtualBox", "GraphicsView", "Dynamips"]
|
||||
controller_settings = {}
|
||||
for key, val in self._settings.items():
|
||||
if key in section_to_save_on_controller:
|
||||
controller_settings[key] = val
|
||||
# We want only the VM settings on the server
|
||||
elif key == "Server":
|
||||
controller_settings["Server"]["vm"] = self._settings["Server"]["vm"]
|
||||
Controller.instance().post("/settings", None, body=controller_settings)
|
||||
|
||||
def checkConfigChanged(self):
|
||||
|
||||
try:
|
||||
if self._last_config_changed and self._last_config_changed < os.stat(self._config_file).st_mtime:
|
||||
log.debug("Client config has changed, reloading it...")
|
||||
self._readConfig(self._config_file)
|
||||
self.config_changed_signal.emit()
|
||||
except OSError as e:
|
||||
log.error("Error when checking for changes {}: {}".format(self._config_file, str(e)))
|
||||
|
||||
def configFilePath(self):
|
||||
"""
|
||||
@@ -128,8 +355,8 @@ class LocalConfig:
|
||||
:returns: path to the config file.
|
||||
"""
|
||||
|
||||
self._settings = self._readConfig(self._config_file)
|
||||
self._config_file = config_file
|
||||
self._resetLoadConfig()
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
@@ -138,7 +365,7 @@ class LocalConfig:
|
||||
:returns: settings (dict)
|
||||
"""
|
||||
|
||||
return self._readConfig(self._config_file)
|
||||
return copy.deepcopy(self._settings)
|
||||
|
||||
def setSettings(self, settings):
|
||||
"""
|
||||
@@ -147,8 +374,10 @@ class LocalConfig:
|
||||
:param settings: settings to save (dict)
|
||||
"""
|
||||
|
||||
self._settings.update(settings)
|
||||
self._writeConfig()
|
||||
if self._settings != settings:
|
||||
self._settings.update(settings)
|
||||
self.writeConfig()
|
||||
self.config_changed_signal.emit()
|
||||
|
||||
def loadSectionSettings(self, section, default_settings):
|
||||
"""
|
||||
@@ -160,24 +389,31 @@ class LocalConfig:
|
||||
"""
|
||||
|
||||
settings = self.settings().get(section, dict())
|
||||
changed = False
|
||||
|
||||
missing = False
|
||||
# use default values for missing settings
|
||||
for name, value in default_settings.items():
|
||||
if name not in settings:
|
||||
settings[name] = value
|
||||
missing = True
|
||||
def _copySettings(local, default):
|
||||
"""
|
||||
Copy only existing settings, ignore the other.
|
||||
Add default values if require.
|
||||
"""
|
||||
nonlocal changed
|
||||
|
||||
if section not in self._settings:
|
||||
self._settings[section] = {}
|
||||
self._settings[section].update(settings)
|
||||
# use default values for missing settings
|
||||
for name, value in default.items():
|
||||
if name not in local:
|
||||
local[name] = value
|
||||
changed = True
|
||||
elif isinstance(value, dict):
|
||||
local[name] = _copySettings(local[name], default[name])
|
||||
return local
|
||||
|
||||
# If a value was not in setting file we store it
|
||||
# on disk
|
||||
if missing:
|
||||
self.saveSectionSettings(section, settings)
|
||||
settings = _copySettings(settings, default_settings)
|
||||
self._settings[section] = settings
|
||||
|
||||
return settings
|
||||
if changed:
|
||||
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):
|
||||
"""
|
||||
@@ -189,8 +425,72 @@ class LocalConfig:
|
||||
|
||||
if section not in self._settings:
|
||||
self._settings[section] = {}
|
||||
self._settings[section].update(settings)
|
||||
self._writeConfig()
|
||||
|
||||
if self._settings[section] != settings:
|
||||
self._settings[section].update(copy.deepcopy(settings))
|
||||
log.debug("Section %s has changed. Saving configuration", section)
|
||||
self.writeConfig()
|
||||
else:
|
||||
log.debug("Section %s has not changed. Skip saving configuration", section)
|
||||
|
||||
def experimental(self):
|
||||
"""
|
||||
:returns: Boolean. True if experimental features allowed
|
||||
"""
|
||||
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["experimental_features"]
|
||||
|
||||
def hdpi(self):
|
||||
"""
|
||||
:returns: Boolean. True if hdpi is allowed
|
||||
"""
|
||||
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["hdpi"]
|
||||
|
||||
def multiProfiles(self):
|
||||
"""
|
||||
:returns: Boolean. True if multi_profiles is enabled
|
||||
"""
|
||||
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["multi_profiles"]
|
||||
|
||||
def setMultiProfiles(self, value):
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
settings = self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)
|
||||
settings["multi_profiles"] = value
|
||||
self.saveSectionSettings("MainWindow", settings)
|
||||
|
||||
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)
|
||||
|
||||
def showInterfaceLabelsOnNewProject(self):
|
||||
"""
|
||||
:returns: Boolean. True if show_interface_labels_on_new_project is enabled
|
||||
"""
|
||||
|
||||
from gns3.settings import GRAPHICS_VIEW_SETTINGS
|
||||
return self.loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS) \
|
||||
.get("show_interface_labels_on_new_project", False)
|
||||
|
||||
def setShowInterfaceLabelsOnNewProject(self, value):
|
||||
from gns3.settings import GRAPHICS_VIEW_SETTINGS
|
||||
settings = self.loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS)
|
||||
settings["show_interface_labels_on_new_project"] = value
|
||||
self.saveSectionSettings("GraphicsView", settings)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
@@ -200,6 +500,44 @@ class LocalConfig:
|
||||
:returns: instance of LocalConfig
|
||||
"""
|
||||
|
||||
if not hasattr(LocalConfig, "_instance"):
|
||||
if not hasattr(LocalConfig, "_instance") or LocalConfig._instance is None:
|
||||
LocalConfig._instance = LocalConfig()
|
||||
return LocalConfig._instance
|
||||
|
||||
@staticmethod
|
||||
def isMainGui():
|
||||
"""
|
||||
:returns: Return true if we are the main gui (first gui to start)
|
||||
"""
|
||||
|
||||
my_pid = os.getpid()
|
||||
pid_path = os.path.join(LocalConfig.instance().configDirectory(), "gns3_gui.pid")
|
||||
|
||||
if os.path.exists(pid_path):
|
||||
try:
|
||||
with open(pid_path) as f:
|
||||
pid = int(f.read())
|
||||
if pid != my_pid:
|
||||
try:
|
||||
process = psutil.Process(pid=pid)
|
||||
ps_name = process.name()
|
||||
except (OSError, psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
else:
|
||||
if "gns3" in ps_name or "python" in ps_name:
|
||||
# Process run under the same user id
|
||||
if sys.platform.startswith("win") or process.uids()[0] == os.getuid():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
except (OSError, ValueError) as e:
|
||||
log.critical("Can't read pid file %s: %s", pid_path, str(e))
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(pid_path, 'w+') as f:
|
||||
f.write(str(my_pid))
|
||||
except OSError as e:
|
||||
log.critical("Can't write pid file %s: %s", pid_path, str(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
601
gns3/local_server.py
Normal file
601
gns3/local_server.py
Normal file
@@ -0,0 +1,601 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import copy
|
||||
import stat
|
||||
import shlex
|
||||
import socket
|
||||
import shutil
|
||||
import random
|
||||
import string
|
||||
import struct
|
||||
import psutil
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore, qslot
|
||||
from gns3.settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.utils.http import getSynchronous
|
||||
from gns3.utils.sudo import sudo
|
||||
from gns3.http_client import HTTPClient
|
||||
from gns3.controller import Controller
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StopLocalServerWorker(QtCore.QObject):
|
||||
"""
|
||||
Worker for displaying a progress dialog when closing
|
||||
the server
|
||||
"""
|
||||
# signals to update the progress dialog.
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
finished = QtCore.pyqtSignal()
|
||||
updated = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, local_server_process):
|
||||
super().__init__()
|
||||
self._local_server_process = local_server_process
|
||||
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):
|
||||
QtCore.QTimer.singleShot(1000, self._callbackSlot)
|
||||
|
||||
def cancel(self):
|
||||
return
|
||||
|
||||
|
||||
class LocalServer(QtCore.QObject):
|
||||
"""
|
||||
Manage the local server process
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
# Remember if the server was started by us or not
|
||||
self._server_started_by_me = False
|
||||
self._local_server_path = ""
|
||||
self._local_server_process = None
|
||||
|
||||
super().__init__()
|
||||
self._parent = parent
|
||||
self._config_directory = LocalConfig.instance().configDirectory()
|
||||
self._settings = {}
|
||||
self.localServerSettings()
|
||||
self._port = self._settings.get("port", 3080)
|
||||
if not self._settings.get("auto_start", True):
|
||||
if self._settings.get("host") is None:
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
else:
|
||||
self._http_client = None
|
||||
|
||||
self._stopping = False
|
||||
self._timer = QtCore.QTimer()
|
||||
self._timer.setInterval(5000)
|
||||
self._timer.timeout.connect(self._checkLocalServerRunningSlot)
|
||||
self._timer.start()
|
||||
|
||||
def _pid_path(self):
|
||||
"""
|
||||
:returns: Path of the PID file
|
||||
"""
|
||||
return os.path.join(self._config_directory, "gns3_server.pid")
|
||||
|
||||
def parent(self):
|
||||
"""
|
||||
Parent window
|
||||
"""
|
||||
if self._parent is None:
|
||||
from gns3.main_window import MainWindow
|
||||
return MainWindow.instance()
|
||||
return self._parent
|
||||
|
||||
def _checkWindowsService(self, service_name):
|
||||
import pywintypes
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
try:
|
||||
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
|
||||
return False
|
||||
except pywintypes.error as e:
|
||||
if e.winerror == 1060: # service is not installed
|
||||
return False
|
||||
else:
|
||||
log.error("Could not check if the {} service is running: {}".format(service_name, e.strerror))
|
||||
return True
|
||||
|
||||
def _checkUbridgePermissions(self):
|
||||
"""
|
||||
Checks that uBridge can interact with network interfaces.
|
||||
"""
|
||||
|
||||
path = os.path.abspath(self._settings["ubridge_path"])
|
||||
|
||||
if not path or len(path) == 0 or not os.path.exists(path) or not os.path.isfile(path):
|
||||
return False
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
# do not check anything on Windows
|
||||
return True
|
||||
|
||||
if os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return True
|
||||
|
||||
request_setuid = False
|
||||
if sys.platform.startswith("linux"):
|
||||
# test if the executable has the CAP_NET_RAW capability (Linux only)
|
||||
try:
|
||||
# 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)))
|
||||
request_setuid = True
|
||||
|
||||
if sys.platform.startswith("darwin") or request_setuid:
|
||||
try:
|
||||
if os.stat(path).st_uid != 0 or not os.stat(path).st_mode & stat.S_ISUID:
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge? All admin users on the system will be able to read packet from the network interfaces.",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set root permissions to uBridge {}: {}".format(path, str(e)))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _passwordGenerate(self):
|
||||
"""
|
||||
Generate a random password
|
||||
"""
|
||||
return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||
|
||||
def localServerSettings(self):
|
||||
"""
|
||||
Returns the local server settings.
|
||||
|
||||
:returns: local server settings (dict)
|
||||
"""
|
||||
|
||||
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
|
||||
self._settings = copy.copy(settings)
|
||||
|
||||
# user & password
|
||||
if settings["auth"] is True and not settings["user"].strip():
|
||||
settings["user"] = "admin"
|
||||
settings["password"] = self._passwordGenerate()
|
||||
|
||||
# local GNS3 server path
|
||||
local_server_path = shutil.which(settings["path"].strip())
|
||||
if local_server_path is None:
|
||||
default_server_path = shutil.which("gns3server")
|
||||
if default_server_path is not None:
|
||||
settings["path"] = os.path.abspath(default_server_path)
|
||||
else:
|
||||
settings["path"] = os.path.abspath(local_server_path)
|
||||
|
||||
# uBridge path
|
||||
ubridge_path = shutil.which(settings["ubridge_path"].strip())
|
||||
if ubridge_path is None:
|
||||
default_ubridge_path = shutil.which("ubridge")
|
||||
if default_ubridge_path is not None:
|
||||
settings["ubridge_path"] = os.path.abspath(default_ubridge_path)
|
||||
else:
|
||||
settings["ubridge_path"] = os.path.abspath(ubridge_path)
|
||||
|
||||
if self._settings != settings:
|
||||
self.updateLocalServerSettings(settings)
|
||||
return settings
|
||||
|
||||
def updateLocalServerSettings(self, new_settings):
|
||||
"""
|
||||
Update the local server settings. Keep the key not in new_settings
|
||||
"""
|
||||
old_settings = copy.copy(self._settings)
|
||||
if not self._settings:
|
||||
self._settings = new_settings
|
||||
else:
|
||||
self._settings.update(new_settings)
|
||||
self._port = self._settings["port"]
|
||||
LocalServerConfig.instance().saveSettings("Server", self._settings)
|
||||
|
||||
# Settings have changed we need to restart the server
|
||||
if old_settings != self._settings:
|
||||
if self._settings["auto_start"]:
|
||||
# We restart the local server only if we really need. Auth can be hot change
|
||||
settings_require_restart = ('host', 'port', 'path')
|
||||
need_restart = False
|
||||
for s in settings_require_restart:
|
||||
if old_settings.get(s) != self._settings.get(s):
|
||||
need_restart = True
|
||||
|
||||
if need_restart:
|
||||
self.stopLocalServer(wait=True)
|
||||
|
||||
self.localServerAutoStartIfRequire()
|
||||
# If the controller is remote:
|
||||
else:
|
||||
self.stopLocalServer(wait=True)
|
||||
|
||||
if self._settings.get("host") is None:
|
||||
self._http_client = None
|
||||
else:
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
|
||||
def shouldLocalServerAutoStart(self):
|
||||
"""
|
||||
Returns either the local server
|
||||
is automatically started on startup.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._settings["auto_start"] and self._settings["host"] is not None
|
||||
|
||||
def localServerPath(self):
|
||||
"""
|
||||
Returns the local server path.
|
||||
|
||||
:returns: path to local server program.
|
||||
"""
|
||||
|
||||
return self._settings["path"]
|
||||
|
||||
def _killAlreadyRunningServer(self):
|
||||
"""
|
||||
Kill a running zombie server (started by a gui that no longer exists)
|
||||
This will not kill server started by hand.
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(self._pid_path()):
|
||||
with open(self._pid_path()) as f:
|
||||
pid = int(f.read())
|
||||
process = psutil.Process(pid=pid)
|
||||
log.info("Kill already running server with PID %d", pid)
|
||||
process.kill()
|
||||
except (OSError, ValueError, psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
# Permission issue, or process no longer exists, or file is empty
|
||||
return
|
||||
|
||||
def localServerAutoStartIfRequire(self):
|
||||
"""
|
||||
Try to start the embed gns3 server.
|
||||
"""
|
||||
|
||||
if not self.shouldLocalServerAutoStart():
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
return
|
||||
|
||||
if self.isLocalServerRunning() and self._server_started_by_me:
|
||||
return True
|
||||
|
||||
# We check if two gui are not launched at the same time
|
||||
# to avoid killing the server of the other GUI
|
||||
if not LocalConfig.isMainGui():
|
||||
log.info("Not the main GUI, will not auto start the server")
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
return True
|
||||
|
||||
if self.isLocalServerRunning():
|
||||
log.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()
|
||||
|
||||
if not self.isLocalServerRunning():
|
||||
if not self.initLocalServer():
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not start the local server process: {}".format(self._settings["path"]))
|
||||
return False
|
||||
if not self.startLocalServer():
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not start the local server process: {}".format(self._settings["path"]))
|
||||
return False
|
||||
|
||||
if self.parent():
|
||||
worker = WaitForConnectionWorker(self._settings["host"], self._port)
|
||||
progress_dialog = ProgressDialog(worker,
|
||||
"Local server",
|
||||
"Connecting to server {} on port {}...".format(self._settings["host"], self._port),
|
||||
"Cancel", busy=True, parent=self.parent())
|
||||
progress_dialog.show()
|
||||
if not progress_dialog.exec_():
|
||||
return False
|
||||
self._server_started_by_me = True
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
|
||||
return True
|
||||
|
||||
def initLocalServer(self):
|
||||
"""
|
||||
Initialize the local server.
|
||||
"""
|
||||
|
||||
self._checkUbridgePermissions()
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Error", "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
|
||||
return False
|
||||
|
||||
self._port = self._settings["port"]
|
||||
|
||||
# check the local server path
|
||||
local_server_path = self.localServerPath()
|
||||
if not local_server_path:
|
||||
log.warn("No local server is configured")
|
||||
return False
|
||||
if not os.path.isfile(local_server_path):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not find local server {}".format(local_server_path))
|
||||
return False
|
||||
elif not os.access(local_server_path, os.X_OK):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "{} is not an executable".format(local_server_path))
|
||||
return False
|
||||
|
||||
try:
|
||||
# check if the local address still exists
|
||||
for res in socket.getaddrinfo(self._settings["host"], 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
|
||||
af, socktype, proto, _, sa = res
|
||||
with socket.socket(af, socktype, proto) as sock:
|
||||
sock.bind(sa)
|
||||
break
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not bind with {}: {} (please check your host binding setting in the preferences)".format(self._settings["host"], e))
|
||||
return False
|
||||
|
||||
try:
|
||||
# check if the port is already taken
|
||||
find_unused_port = False
|
||||
for res in socket.getaddrinfo(self._settings["host"], self._port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
|
||||
af, socktype, proto, _, sa = res
|
||||
with socket.socket(af, socktype, proto) as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(sa)
|
||||
break
|
||||
except OSError as e:
|
||||
log.warning("Could not use socket {}:{} {}".format(self._settings["host"], self._port, e))
|
||||
find_unused_port = True
|
||||
|
||||
if find_unused_port:
|
||||
# find an alternate port for the local server
|
||||
old_port = self._port
|
||||
try:
|
||||
self._port = self._findUnusedLocalPort(self._settings["host"])
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not find an unused port for the local server: {}".format(e))
|
||||
return False
|
||||
log.warning("The server port {} is already in use, fallback to port {}".format(old_port, self._port))
|
||||
return True
|
||||
|
||||
def _findUnusedLocalPort(self, host):
|
||||
"""
|
||||
Find an unused port.
|
||||
|
||||
:param host: server hosts
|
||||
|
||||
:returns: port number
|
||||
"""
|
||||
|
||||
with socket.socket() as s:
|
||||
s.bind((host, 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
def startLocalServer(self):
|
||||
"""
|
||||
Starts the local server process.
|
||||
"""
|
||||
|
||||
self._stopping = False
|
||||
path = self.localServerPath()
|
||||
command = '"{executable}" --local'.format(executable=path)
|
||||
|
||||
if LocalConfig.instance().profile():
|
||||
command += " --profile {}".format(LocalConfig.instance().profile())
|
||||
|
||||
if self._settings["allow_console_from_anywhere"]:
|
||||
# allow connections to console from remote addresses
|
||||
command += " --allow"
|
||||
|
||||
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
||||
command += " --debug"
|
||||
|
||||
settings_dir = self._config_directory
|
||||
if os.path.isdir(settings_dir):
|
||||
# save server logging info to a file in the settings directory
|
||||
logpath = os.path.join(settings_dir, "gns3_server.log")
|
||||
if os.path.isfile(logpath):
|
||||
# delete the previous log file
|
||||
try:
|
||||
os.remove(logpath)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as e:
|
||||
log.warn("could not delete server log file {}: {}".format(logpath, e))
|
||||
command += ' --log="{}" --pid="{}"'.format(logpath, self._pid_path())
|
||||
|
||||
log.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, stderr=subprocess.PIPE)
|
||||
else:
|
||||
# use arguments on other platforms
|
||||
args = shlex.split(command)
|
||||
self._local_server_process = subprocess.Popen(args, stderr=subprocess.PIPE)
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.warning('Could not start local server "{}": {}'.format(command, e))
|
||||
return False
|
||||
|
||||
log.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.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
try:
|
||||
if self._local_server_process and self._local_server_process.poll() is None:
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def isLocalServerRunning(self):
|
||||
"""
|
||||
Synchronous check if a server is already running on this host.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
status, json_data = getSynchronous(self._settings["protocol"], self._settings["host"], self._port, "version",
|
||||
timeout=2, user=self._settings["user"], password=self._settings["password"])
|
||||
|
||||
if 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)
|
||||
if version is None:
|
||||
log.debug("Server is not a GNS3 server")
|
||||
return False
|
||||
return True
|
||||
|
||||
def stopLocalServer(self, wait=False):
|
||||
"""
|
||||
Stops the local server.
|
||||
|
||||
:param wait: wait for the server to stop
|
||||
"""
|
||||
|
||||
if self.localServerProcessIsRunning():
|
||||
self._stopping = True
|
||||
log.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()
|
||||
if wait:
|
||||
worker = StopLocalServerWorker(self._local_server_process)
|
||||
progress_dialog = ProgressDialog(worker, "Local server", "Waiting for the local server to stop...", None, busy=True, parent=self.parent())
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
if self._local_server_process.returncode is None:
|
||||
self._killLocalServer()
|
||||
self._server_started_by_me = False
|
||||
|
||||
def _killLocalServer(self):
|
||||
# the local server couldn't be stopped with the normal procedure
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
self._local_server_process.send_signal(signal.CTRL_BREAK_EVENT)
|
||||
else:
|
||||
self._local_server_process.send_signal(signal.SIGINT)
|
||||
# If the process is already dead we received a permission error
|
||||
# it's a race condition between the timeout and send signal
|
||||
except (PermissionError, SystemError):
|
||||
pass
|
||||
try:
|
||||
# wait for the server to stop for maximum x seconds
|
||||
self._local_server_process.wait(timeout=60)
|
||||
except subprocess.TimeoutExpired:
|
||||
proceed = QtWidgets.QMessageBox.question(self.parent(),
|
||||
"Local server",
|
||||
"The Local server cannot be stopped, would you like to kill it?",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
self._local_server_process.kill()
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of LocalServer.
|
||||
:returns: instance of LocalServer
|
||||
"""
|
||||
|
||||
if not hasattr(LocalServer, '_instance') or LocalServer._instance is None:
|
||||
LocalServer._instance = LocalServer()
|
||||
return LocalServer._instance
|
||||
|
||||
|
||||
def main():
|
||||
import pprint
|
||||
|
||||
pp = pprint.PrettyPrinter(indent=4)
|
||||
print("Local server config")
|
||||
local_server = LocalServer(False)
|
||||
pp.pprint(local_server.localServerSettings())
|
||||
local_server.localServerAutoStart()
|
||||
local_server.stopLocalServer()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -18,7 +18,6 @@
|
||||
import os
|
||||
import sys
|
||||
import configparser
|
||||
from gns3.qt import QtCore
|
||||
|
||||
|
||||
import logging
|
||||
@@ -31,25 +30,25 @@ class LocalServerConfig:
|
||||
Local server configuration.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, config_file=None):
|
||||
|
||||
appname = "GNS3"
|
||||
|
||||
self._config = configparser.RawConfigParser()
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_server.ini"
|
||||
else:
|
||||
filename = "gns3_server.conf"
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
appname = "gns3.net"
|
||||
if config_file:
|
||||
self._config_file = config_file
|
||||
else:
|
||||
appname = "GNS3"
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_server.ini"
|
||||
else:
|
||||
filename = "gns3_server.conf"
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
self._config_file = os.path.join(appdata, appname, filename)
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
self._config_file = os.path.join(home, ".config", appname, filename)
|
||||
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
|
||||
@@ -58,6 +57,14 @@ class LocalServerConfig:
|
||||
log.error("Could not create the local server configuration {}: {}".format(self._config_file, e))
|
||||
self.readConfig()
|
||||
|
||||
def setConfigFile(self, path):
|
||||
"""
|
||||
Change the location of the server config (use for test)
|
||||
"""
|
||||
self._config = configparser.RawConfigParser()
|
||||
self._config_file = path
|
||||
self.readConfig()
|
||||
|
||||
def readConfig(self):
|
||||
"""
|
||||
Read the configuration file.
|
||||
@@ -65,7 +72,7 @@ class LocalServerConfig:
|
||||
|
||||
try:
|
||||
self._config.read(self._config_file, encoding="utf-8")
|
||||
except (OSError, configparser.Error) as e:
|
||||
except (OSError, configparser.Error, UnicodeEncodeError, UnicodeDecodeError) as e:
|
||||
log.error("Could not read the local server configuration {}: {}".format(self._config_file, e))
|
||||
|
||||
def writeConfig(self):
|
||||
@@ -80,13 +87,12 @@ class LocalServerConfig:
|
||||
except (OSError, configparser.Error) as e:
|
||||
log.error("Could not write the local server configuration {}: {}".format(self._config_file, e))
|
||||
|
||||
def loadSettings(self, section, default_settings, types):
|
||||
def loadSettings(self, section, default_settings):
|
||||
"""
|
||||
Get all the settings from a given section.
|
||||
|
||||
:param section: section name
|
||||
:param default_settings: setting names and default values (dict)
|
||||
:param types: setting types (dict)
|
||||
|
||||
:returns: settings (dict)
|
||||
"""
|
||||
@@ -96,14 +102,16 @@ class LocalServerConfig:
|
||||
|
||||
settings = {}
|
||||
for name, default in default_settings.items():
|
||||
if types[name] is int:
|
||||
settings[name] = self._config[section].getint(name, default)
|
||||
elif types[name] is bool:
|
||||
if isinstance(default, bool):
|
||||
settings[name] = self._config[section].getboolean(name, default)
|
||||
elif types[name] is float:
|
||||
elif isinstance(default, int):
|
||||
settings[name] = self._config[section].getint(name, default)
|
||||
elif isinstance(default, float):
|
||||
settings[name] = self._config[section].getfloat(name, default)
|
||||
else:
|
||||
settings[name] = self._config[section].get(name, default)
|
||||
if settings[name] == "None":
|
||||
settings[name] = None
|
||||
|
||||
# sync with the config file
|
||||
self.saveSettings(section, settings)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
class ColouredFormatter(logging.Formatter):
|
||||
@@ -74,20 +75,54 @@ class ColouredStreamHandler(logging.StreamHandler):
|
||||
stream.write(msg)
|
||||
stream.write(self.terminator)
|
||||
self.flush()
|
||||
# On OSX when frozen flush raise a BrokenPipeError
|
||||
# On OSX when frozen flush raise a BrokenPipeError
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
def init_logger(level, quiet=False):
|
||||
def init_logger(level, logfile, quiet=False):
|
||||
if sys.platform.startswith("win"):
|
||||
stream_handler = logging.StreamHandler(sys.stdout)
|
||||
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
|
||||
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])
|
||||
logging.getLogger().addHandler(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))
|
||||
except FileExistsError:
|
||||
pass
|
||||
handler = logging.FileHandler(logfile, "w")
|
||||
handler.formatter = logging.Formatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
|
||||
log.addHandler(handler)
|
||||
except OSError as e:
|
||||
log.warn("could not log to {}: {}".format(logfile, e))
|
||||
|
||||
log.info('Log level: {}'.format(logging.getLevelName(level)))
|
||||
|
||||
return logging.getLogger()
|
||||
|
||||
192
gns3/main.py
192
gns3/main.py
@@ -16,6 +16,20 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import faulthandler
|
||||
|
||||
# Try to install updates & restart application if an update is installed
|
||||
try:
|
||||
import gns3.update_manager
|
||||
if gns3.update_manager.UpdateManager().installDownloadedUpdates():
|
||||
print("Update installed restart the application")
|
||||
python = sys.executable
|
||||
os.execl(python, *sys.argv)
|
||||
except Exception as e:
|
||||
print("Fail update installation: {}".format(str(e)))
|
||||
|
||||
|
||||
# WARNING
|
||||
# Due to buggy user machines we choose to put this as the first loading modules
|
||||
@@ -27,29 +41,29 @@ from gns3.utils.get_resource import get_resource
|
||||
|
||||
|
||||
import datetime
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import time
|
||||
import locale
|
||||
import argparse
|
||||
import signal
|
||||
import psutil
|
||||
|
||||
try:
|
||||
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
|
||||
|
||||
from gns3.logger import init_logger
|
||||
from gns3.crash_report import CrashReport
|
||||
from gns3.utils.analytics import AnalyticsClient
|
||||
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.application import Application
|
||||
from gns3.utils import parse_version
|
||||
from gns3.dialogs.profile_select import ProfileSelectDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
try:
|
||||
from gns3.qt import QtCore, QtGui, DEFAULT_BINDING
|
||||
except ImportError:
|
||||
raise RuntimeError("Can't import Qt modules: Qt and/or PyQt is probably not installed correctly...")
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.version import __version__
|
||||
|
||||
|
||||
@@ -97,15 +111,48 @@ 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"):
|
||||
sys.argv = [a for a in sys.argv if not a.startswith("-psn_")]
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("project", help="load a GNS3 project (.gns3)", metavar="path", nargs="?")
|
||||
parser.add_argument("--version", help="show the version", action="version", version=__version__)
|
||||
parser.add_argument("--debug", help="print out debug messages", action="store_true", default=False)
|
||||
parser.add_argument("-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 and hasattr(sys, "frozen"):
|
||||
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
|
||||
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
|
||||
frozen_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||
if sys.platform.startswith("darwin"):
|
||||
frozen_dirs = [
|
||||
frozen_dir,
|
||||
os.path.normpath(os.path.join(frozen_dir, '..', 'Resources'))
|
||||
]
|
||||
elif sys.platform.startswith("win"):
|
||||
frozen_dirs = [
|
||||
frozen_dir,
|
||||
os.path.normpath(os.path.join(frozen_dir, 'dynamips')),
|
||||
os.path.normpath(os.path.join(frozen_dir, 'vpcs')),
|
||||
os.path.normpath(os.path.join(frozen_dir, 'traceng'))
|
||||
]
|
||||
|
||||
os.environ["PATH"] = os.pathsep.join(frozen_dirs) + os.pathsep + os.environ.get("PATH", "")
|
||||
|
||||
if options.project:
|
||||
os.chdir(frozen_dir)
|
||||
|
||||
def exceptionHook(exception, value, tb):
|
||||
|
||||
@@ -138,28 +185,15 @@ 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")
|
||||
|
||||
# we only support Python 2 version >= 2.7 and Python 3 version >= 3.3
|
||||
if sys.version_info < (2, 7):
|
||||
raise RuntimeError("Python 2.7 or higher is required")
|
||||
elif sys.version_info[0] == 3 and sys.version_info < (3, 3):
|
||||
raise RuntimeError("Python 3.3 or higher is required")
|
||||
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))
|
||||
|
||||
def version(version_string):
|
||||
return [int(i) for i in version_string.split('.')]
|
||||
|
||||
if version(QtCore.QT_VERSION_STR) < version("4.6"):
|
||||
raise RuntimeError("Requirement is Qt version 4.6 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
|
||||
# 4.8.3 because of QSettings (http://pyqt.sourceforge.net/Docs/PyQt4/pyqt_qsettings.html)
|
||||
if DEFAULT_BINDING == "PyQt" and version(QtCore.BINDING_VERSION_STR) < version("4.8.3"):
|
||||
raise RuntimeError("Requirement is PyQt version 4.8.3 or higher, got version {}".format(QtCore.BINDING_VERSION_STR))
|
||||
|
||||
if DEFAULT_BINDING == "PySide" and version(QtCore.BINDING_VERSION_STR) < version("1.0"):
|
||||
raise RuntimeError("Requirement is PySide version 1.0 or higher, got version {}".format(QtCore.BINDING_VERSION_STR))
|
||||
if parse_version(psutil.__version__) < parse_version("2.2.1"):
|
||||
raise SystemExit("Requirement is psutil version 2.2.1 or higher, got version {}".format(psutil.__version__))
|
||||
|
||||
# check for the correct locale
|
||||
# (UNIX/Linux only)
|
||||
@@ -181,7 +215,7 @@ def main():
|
||||
import win32con
|
||||
import win32gui
|
||||
except ImportError:
|
||||
raise RuntimeError("Python for Windows extensions must be installed.")
|
||||
raise SystemExit("Python for Windows extensions must be installed.")
|
||||
|
||||
if not options.debug:
|
||||
try:
|
||||
@@ -191,46 +225,86 @@ def main():
|
||||
except win32console.error as e:
|
||||
print("warning: could not allocate console: {}".format(e))
|
||||
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
local_config = LocalConfig.instance()
|
||||
|
||||
# this info is necessary for QSettings
|
||||
app.setOrganizationName("GNS3")
|
||||
app.setOrganizationDomain("gns3.net")
|
||||
app.setApplicationName("GNS3")
|
||||
app.setApplicationVersion(__version__)
|
||||
global app
|
||||
app = Application(sys.argv, hdpi=local_config.hdpi())
|
||||
|
||||
formatter = logging.Formatter("[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d] %(message)s",
|
||||
datefmt="%y%m%d %H:%M:%S")
|
||||
if local_config.multiProfiles() and not options.profile:
|
||||
profile_select = ProfileSelectDialog()
|
||||
profile_select.show()
|
||||
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)
|
||||
|
||||
# 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)
|
||||
init_logger(logging.DEBUG, logfile)
|
||||
elif options.quiet:
|
||||
init_logger(logging.ERROR, logfile)
|
||||
else:
|
||||
root_logger = init_logger(logging.INFO)
|
||||
init_logger(logging.INFO, logfile)
|
||||
|
||||
# save client logging info to a file
|
||||
logfile = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "gns3_gui.log")
|
||||
try:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(QtCore.QSettings().fileName()))
|
||||
except FileExistsError:
|
||||
pass
|
||||
handler = logging.FileHandler(logfile, "w")
|
||||
root_logger.addHandler(handler)
|
||||
except OSError as e:
|
||||
log.warn("could not log to {}: {}".format(logfile, e))
|
||||
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('Log level: {}'.format(logging.getLevelName(log.getEffectiveLevel())))
|
||||
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(os.path.dirname(QtCore.QSettings().fileName()), exception_file_path)
|
||||
exception_file_path = os.path.join(LocalConfig.instance().configDirectory(), exception_file_path)
|
||||
|
||||
# We disallow to run GNS3 from outside the /Applications folder to avoid
|
||||
# issue when people run GNS3 from the .dmg
|
||||
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
|
||||
if not os.path.realpath(sys.executable).startswith("/Applications"):
|
||||
QtWidgets.QMessageBox.critical(None, "Error", "You need to copy GNS3 in your /Applications folder before using it.")
|
||||
sys.exit(1)
|
||||
|
||||
global mainwindow
|
||||
startup_file = app.open_file_at_startup
|
||||
if not startup_file:
|
||||
startup_file = options.project
|
||||
|
||||
mainwindow = MainWindow(open_file=startup_file)
|
||||
|
||||
# On OSX we can receive the file to open from a system event
|
||||
# loadPath is smart and will load only if a path is present
|
||||
app.file_open_signal.connect(lambda path: mainwindow.loadPath(path))
|
||||
|
||||
# Manage Ctrl + C or kill command
|
||||
def sigint_handler(*args):
|
||||
log.info("Signal received exiting the application")
|
||||
app.closeAllWindows()
|
||||
orig_sigint = signal.signal(signal.SIGINT, sigint_handler)
|
||||
orig_sigterm = signal.signal(signal.SIGTERM, sigint_handler)
|
||||
|
||||
mainwindow = MainWindow(options.project)
|
||||
mainwindow.show()
|
||||
|
||||
exit_code = app.exec_()
|
||||
|
||||
signal.signal(signal.SIGINT, orig_sigint)
|
||||
signal.signal(signal.SIGTERM, orig_sigterm)
|
||||
|
||||
delattr(MainWindow, "_instance")
|
||||
app.deleteLater()
|
||||
|
||||
# We force deleting the app object otherwise it's segfault on Fedora
|
||||
del app
|
||||
# We force a full garbage collect before exit
|
||||
# for unknow reason otherwise Qt Segfault on OSX in some
|
||||
# conditions
|
||||
import gc
|
||||
gc.collect()
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
2093
gns3/main_window.py
2093
gns3/main_window.py
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,11 @@ from gns3.modules.builtin import Builtin
|
||||
from gns3.modules.dynamips import Dynamips
|
||||
from gns3.modules.iou import IOU
|
||||
from gns3.modules.vpcs import VPCS
|
||||
from gns3.modules.traceng import TraceNG
|
||||
from gns3.modules.virtualbox import VirtualBox
|
||||
from gns3.modules.qemu import Qemu
|
||||
from gns3.modules.vmware import VMware
|
||||
from gns3.modules.docker import Docker
|
||||
|
||||
MODULES = [VPCS, Dynamips, IOU, VirtualBox, Qemu, Builtin]
|
||||
#FIXME: removed TraceNG
|
||||
MODULES = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker]
|
||||
|
||||
@@ -19,13 +19,23 @@
|
||||
Built-in module implementation.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui
|
||||
from gns3.servers import Servers
|
||||
from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
from .cloud import Cloud
|
||||
from .host import Host
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
|
||||
from ..module import Module
|
||||
from .cloud import Cloud
|
||||
from .nat import Nat
|
||||
from .ethernet_hub import EthernetHub
|
||||
from .ethernet_switch import EthernetSwitch
|
||||
from .frame_relay_switch import FrameRelaySwitch
|
||||
from .atm_switch import ATMSwitch
|
||||
|
||||
from .settings import (
|
||||
BUILTIN_SETTINGS,
|
||||
CLOUD_SETTINGS,
|
||||
ETHERNET_HUB_SETTINGS,
|
||||
ETHERNET_SWITCH_SETTINGS
|
||||
)
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -38,9 +48,145 @@ class Builtin(Module):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Module.__init__(self)
|
||||
super().__init__()
|
||||
|
||||
self._settings = {}
|
||||
self._nodes = []
|
||||
self._cloud_nodes = {}
|
||||
self._nat_nodes = {}
|
||||
self._ethernet_hubs = {}
|
||||
self._ethernet_switches = {}
|
||||
|
||||
# load the settings
|
||||
self._loadSettings()
|
||||
|
||||
def configChangedSlot(self):
|
||||
|
||||
pass
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns the module settings
|
||||
|
||||
:returns: module settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._settings
|
||||
|
||||
def setSettings(self, settings):
|
||||
"""Sets the module settings
|
||||
|
||||
:param settings: module settings (dictionary)
|
||||
"""
|
||||
|
||||
self._settings.update(settings)
|
||||
self._saveSettings()
|
||||
|
||||
def _saveSettings(self):
|
||||
"""
|
||||
Saves the settings to the persistent settings file.
|
||||
"""
|
||||
|
||||
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
|
||||
|
||||
def _loadSettings(self):
|
||||
"""
|
||||
Loads the settings from the persistent settings file.
|
||||
"""
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
self._settings = local_config.loadSectionSettings(self.__class__.__name__, BUILTIN_SETTINGS)
|
||||
self._loadNodes()
|
||||
|
||||
def _loadBuilinNodesPerType(self, node_dict, node_type, default_settings):
|
||||
|
||||
settings = LocalConfig.instance().settings()
|
||||
if node_type in settings.get(self.__class__.__name__, {}):
|
||||
for device in settings[self.__class__.__name__][node_type]:
|
||||
name = device.get("name")
|
||||
server = device.get("server")
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
if key in node_dict or not name or not server:
|
||||
continue
|
||||
node_settings = default_settings.copy()
|
||||
node_settings.update(device)
|
||||
node_dict[key] = node_settings
|
||||
|
||||
def _loadNodes(self):
|
||||
"""
|
||||
Load the built-in nodes from the persistent settings file.
|
||||
"""
|
||||
|
||||
self._loadBuilinNodesPerType(self._cloud_nodes, "cloud_nodes", CLOUD_SETTINGS)
|
||||
self._loadBuilinNodesPerType(self._ethernet_hubs, "ethernet_hubs", ETHERNET_HUB_SETTINGS)
|
||||
self._loadBuilinNodesPerType(self._ethernet_switches, "ethernet_switches", ETHERNET_SWITCH_SETTINGS)
|
||||
|
||||
def _saveNodes(self):
|
||||
"""
|
||||
Saves the built-in nodes to the persistent settings file.
|
||||
"""
|
||||
|
||||
self._settings["cloud_nodes"] = list(self._cloud_nodes.values())
|
||||
self._settings["ethernet_hubs"] = list(self._ethernet_hubs.values())
|
||||
self._settings["ethernet_switches"] = list(self._ethernet_switches.values())
|
||||
self._saveSettings()
|
||||
|
||||
def cloudNodes(self):
|
||||
"""
|
||||
Returns cloud nodes settings.
|
||||
|
||||
:returns: Cloud nodes settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._cloud_nodes
|
||||
|
||||
def setCloudNodes(self, new_cloud_nodes):
|
||||
"""
|
||||
Sets cloud nodes settings.
|
||||
|
||||
:param new_cloud_nodes: cloud nodes settings (dictionary)
|
||||
"""
|
||||
|
||||
self._cloud_nodes = new_cloud_nodes.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def ethernetHubs(self):
|
||||
"""
|
||||
Returns Ethernet hubs settings.
|
||||
|
||||
:returns: Ethernet hubs settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._ethernet_hubs
|
||||
|
||||
def setEthernetHubs(self, new_ethernet_hubs):
|
||||
"""
|
||||
Sets Ethernet hubs settings.
|
||||
|
||||
:param new_ethernet_hubs: Ethernet hubs settings (dictionary)
|
||||
"""
|
||||
|
||||
self._ethernet_hubs = new_ethernet_hubs.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def ethernetSwitches(self):
|
||||
"""
|
||||
Returns Ethernet switches settings.
|
||||
|
||||
:returns: Ethernet switches settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._ethernet_switches
|
||||
|
||||
def setEthernetSwitches(self, new_ethernet_switches):
|
||||
"""
|
||||
Sets Ethernet switches settings.
|
||||
|
||||
:param new_ethernet_switches: Ethernet switches settings (dictionary)
|
||||
"""
|
||||
|
||||
self._ethernet_switches = new_ethernet_switches.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def addNode(self, node):
|
||||
"""
|
||||
@@ -66,85 +212,20 @@ class Builtin(Module):
|
||||
Resets the module.
|
||||
"""
|
||||
|
||||
log.info("Built-in module reset")
|
||||
self._nodes.clear()
|
||||
|
||||
def allocateServer(self, node_class):
|
||||
def instantiateNode(self, node_class, server, project):
|
||||
"""
|
||||
Allocates a server.
|
||||
|
||||
:param node_class: Node object
|
||||
|
||||
:returns: allocated server (HTTPClient instance)
|
||||
"""
|
||||
|
||||
# check all other modules to find if they
|
||||
# are using a local server
|
||||
using_local_server = []
|
||||
from gns3.modules import MODULES
|
||||
for module in MODULES:
|
||||
instance = module.instance()
|
||||
if instance != self:
|
||||
module_settings = instance.settings()
|
||||
if "use_local_server" in module_settings:
|
||||
using_local_server.append(module_settings["use_local_server"])
|
||||
|
||||
# allocate a server for the node
|
||||
servers = Servers.instance()
|
||||
local_server = servers.localServer()
|
||||
remote_servers = servers.remoteServers()
|
||||
|
||||
if not all(using_local_server) and len(remote_servers):
|
||||
# a module is not using a local server
|
||||
|
||||
if True not in using_local_server and len(remote_servers) == 1:
|
||||
# no module is using a local server and there is only one
|
||||
# remote server available, so no need to ask the user.
|
||||
return next(iter(servers))
|
||||
|
||||
server_list = []
|
||||
server_list.append("Local server ({}:{})".format(local_server.host, local_server.port))
|
||||
for remote_server in remote_servers:
|
||||
server_list.append("{}".format(remote_server))
|
||||
|
||||
# TODO: move this to graphics_view
|
||||
from gns3.main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
(selection, ok) = QtGui.QInputDialog.getItem(mainwindow, "Server", "Please choose a server", server_list, 0, False)
|
||||
if ok:
|
||||
if selection.startswith("Local server"):
|
||||
return local_server
|
||||
else:
|
||||
return remote_servers[selection]
|
||||
else:
|
||||
raise ModuleError("Please select a server")
|
||||
return local_server
|
||||
|
||||
def createNode(self, node_class, server, project):
|
||||
"""
|
||||
Creates a new node.
|
||||
Instantiate a new node.
|
||||
|
||||
:param node_class: Node object
|
||||
:param server: HTTPClient instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("creating node {}".format(node_class))
|
||||
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def setupNode(self, node, node_name):
|
||||
"""
|
||||
Setups a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("configuring node {}".format(node))
|
||||
node.setup()
|
||||
|
||||
@staticmethod
|
||||
def findAlternativeInterface(node, missing_interface):
|
||||
|
||||
@@ -156,15 +237,15 @@ class Builtin(Module):
|
||||
available_interfaces.append(interface["name"])
|
||||
|
||||
if available_interfaces:
|
||||
selection, ok = QtGui.QInputDialog.getItem(mainwindow,
|
||||
"Cloud interfaces", "Interface {} could not be found\nPlease select an alternative from your existing interfaces:".format(missing_interface),
|
||||
available_interfaces, 0, False)
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(mainwindow,
|
||||
"Cloud interfaces", "Interface {} could not be found\nPlease select an alternative from your existing interfaces:".format(missing_interface),
|
||||
available_interfaces, 0, False)
|
||||
if ok:
|
||||
return selection
|
||||
QtGui.QMessageBox.warning(mainwindow, "Cloud interface", "No alternative interface chosen to replace {} on this host, this may lead to issues".format(missing_interface))
|
||||
QtWidgets.QMessageBox.warning(mainwindow, "Cloud interface", "No alternative interface chosen to replace {} on this host, this may lead to issues".format(missing_interface))
|
||||
return None
|
||||
else:
|
||||
QtGui.QMessageBox.critical(mainwindow, "Cloud interface", "Could not find interface {} on this host".format(missing_interface))
|
||||
QtWidgets.QMessageBox.critical(mainwindow, "Cloud interface", "Could not find interface {} on this host".format(missing_interface))
|
||||
return missing_interface
|
||||
|
||||
@staticmethod
|
||||
@@ -179,6 +260,22 @@ class Builtin(Module):
|
||||
return globals()[name]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def getNodeType(name, platform=None):
|
||||
if name == "cloud":
|
||||
return Cloud
|
||||
elif name == "nat":
|
||||
return Nat
|
||||
elif name == "ethernet_hub":
|
||||
return EthernetHub
|
||||
elif name == "ethernet_switch":
|
||||
return EthernetSwitch
|
||||
elif name == "frame_relay_switch":
|
||||
return FrameRelaySwitch
|
||||
elif name == "atm_switch":
|
||||
return ATMSwitch
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def classes():
|
||||
"""
|
||||
@@ -187,7 +284,7 @@ class Builtin(Module):
|
||||
:returns: list of classes
|
||||
"""
|
||||
|
||||
return [Cloud, Host]
|
||||
return [Nat, Cloud, EthernetHub, EthernetSwitch, FrameRelaySwitch, ATMSwitch]
|
||||
|
||||
def nodes(self):
|
||||
"""
|
||||
@@ -196,13 +293,37 @@ class Builtin(Module):
|
||||
"""
|
||||
|
||||
nodes = []
|
||||
for node_class in Builtin.classes():
|
||||
# add custom cloud node templates
|
||||
for cloud_node in self._cloud_nodes.values():
|
||||
nodes.append(
|
||||
{"class": node_class.__name__,
|
||||
"name": node_class.symbolName(),
|
||||
"categories": node_class.categories(),
|
||||
"default_symbol": node_class.defaultSymbol(),
|
||||
"hover_symbol": node_class.hoverSymbol()}
|
||||
{"class": Cloud.__name__,
|
||||
"name": cloud_node["name"],
|
||||
"server": cloud_node["server"],
|
||||
"symbol": cloud_node["symbol"],
|
||||
"categories": [cloud_node["category"]]
|
||||
}
|
||||
)
|
||||
|
||||
# add custom Ethernet hub templates
|
||||
for hub in self._ethernet_hubs.values():
|
||||
nodes.append(
|
||||
{"class": EthernetHub.__name__,
|
||||
"name": hub["name"],
|
||||
"server": hub["server"],
|
||||
"symbol": hub["symbol"],
|
||||
"categories": [hub["category"]]
|
||||
}
|
||||
)
|
||||
|
||||
# add custom Ethernet switch templates
|
||||
for switch in self._ethernet_switches.values():
|
||||
nodes.append(
|
||||
{"class": EthernetSwitch.__name__,
|
||||
"name": switch["name"],
|
||||
"server": switch["server"],
|
||||
"symbol": switch["symbol"],
|
||||
"categories": [switch["category"]]
|
||||
}
|
||||
)
|
||||
return nodes
|
||||
|
||||
@@ -212,7 +333,12 @@ class Builtin(Module):
|
||||
:returns: QWidget object list
|
||||
"""
|
||||
|
||||
return []
|
||||
from .pages.builtin_preferences_page import BuiltinPreferencesPage
|
||||
from .pages.cloud_preferences_page import CloudPreferencesPage
|
||||
from .pages.ethernet_hub_preferences_page import EthernetHubPreferencesPage
|
||||
from .pages.ethernet_switch_preferences_page import EthernetSwitchPreferencesPage
|
||||
|
||||
return [BuiltinPreferencesPage, EthernetHubPreferencesPage, EthernetSwitchPreferencesPage, CloudPreferencesPage]
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
|
||||
193
gns3/modules/builtin/atm_switch.py
Normal file
193
gns3/modules/builtin/atm_switch.py
Normal file
@@ -0,0 +1,193 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ATMSwitch(Node):
|
||||
|
||||
"""
|
||||
ATM switch.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
URL_PREFIX = "atm_switch"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"mappings": {}})
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this ATM switch.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this ATM switch.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """ATM switch {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's Node ID is {node_id}
|
||||
Hardware is Dynamips emulated simple ATM switch
|
||||
Switch's server runs on {host}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self._compute.name())
|
||||
|
||||
port_info = ""
|
||||
mapping = re.compile(r"""^([0-9]*):([0-9]*):([0-9]*)$""")
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " Port {} is empty\n".format(port.name())
|
||||
else:
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
for source, destination in self._settings["mappings"].items():
|
||||
match_source_mapping = mapping.search(source)
|
||||
match_destination_mapping = mapping.search(destination)
|
||||
if match_source_mapping and match_destination_mapping:
|
||||
source_port, source_vpi, source_vci = match_source_mapping.group(1, 2, 3)
|
||||
destination_port, destination_vpi, destination_vci = match_destination_mapping.group(1, 2, 3)
|
||||
else:
|
||||
source_port, source_vpi = source.split(":")
|
||||
destination_port, destination_vpi = destination.split(":")
|
||||
source_vci = destination_vci = 0
|
||||
|
||||
if port.name() == source_port or port.name() == destination_port:
|
||||
if port.name() == source_port:
|
||||
vpi1 = source_vpi
|
||||
vci1 = source_vci
|
||||
port = destination_port
|
||||
vci2 = destination_vci
|
||||
vpi2 = destination_vpi
|
||||
else:
|
||||
vpi1 = destination_vpi
|
||||
vci1 = destination_vci
|
||||
port = source_port
|
||||
vci2 = source_vci
|
||||
vpi2 = source_vpi
|
||||
|
||||
if vci1 and vci2:
|
||||
port_info += " incoming VPI {vpi1} and VCI {vci1} is switched to port {port} outgoing VPI {vpi2} and VCI {vci2}\n".format(vpi1=vpi1,
|
||||
vci1=vci1,
|
||||
port=port,
|
||||
vpi2=vpi2,
|
||||
vci2=vci2)
|
||||
else:
|
||||
port_info += " incoming VPI {vpi1} is switched to port {port} outgoing VPI {vpi2}\n".format(vpi1=vpi1,
|
||||
port=port,
|
||||
vpi2=vpi2)
|
||||
|
||||
break
|
||||
|
||||
return info + port_info
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this ATM switch
|
||||
(to be saved in a topology file).
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
atmsw = super().dump()
|
||||
if self._settings["mappings"]:
|
||||
atmsw["properties"]["mappings"] = self._settings["mappings"]
|
||||
return atmsw
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
|
||||
from .pages.atm_switch_configuration_page import ATMSwitchConfigurationPage
|
||||
return ATMSwitchConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/atm_switch.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "ATM switch"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.switches]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "ATM switch"
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -15,22 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
NIO implementation on the client side (in the form of a pseudo node represented as a cloud).
|
||||
Asynchronously sends JSON messages to the GNS3 server and receives responses with callbacks.
|
||||
"""
|
||||
|
||||
import re
|
||||
from gns3.node import Node
|
||||
from gns3.ports.port import Port
|
||||
from gns3.nios.nio_generic_ethernet import NIOGenericEthernet
|
||||
from gns3.nios.nio_linux_ethernet import NIOLinuxEthernet
|
||||
from gns3.nios.nio_nat import NIONAT
|
||||
from gns3.nios.nio_udp import NIOUDP
|
||||
from gns3.nios.nio_tap import NIOTAP
|
||||
from gns3.nios.nio_unix import NIOUNIX
|
||||
from gns3.nios.nio_vde import NIOVDE
|
||||
from gns3.nios.nio_null import NIONull
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -39,276 +24,80 @@ log = logging.getLogger(__name__)
|
||||
class Cloud(Node):
|
||||
|
||||
"""
|
||||
Dynamips cloud.
|
||||
Cloud node
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
_name_instance_count = 1
|
||||
URL_PREFIX = "cloud"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
Node.__init__(self, module, server, project)
|
||||
|
||||
log.info("cloud is being created")
|
||||
# create an unique id and name
|
||||
self._name_id = Cloud._name_instance_count
|
||||
Cloud._name_instance_count += 1
|
||||
super().__init__(module, server, project)
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self._interfaces = {}
|
||||
self._cloud_settings = {"ports_mapping": []}
|
||||
self.settings().update(self._cloud_settings)
|
||||
|
||||
name = "Cloud {}".format(self._name_id)
|
||||
self.setStatus(Node.started) # this is an always-on node
|
||||
self._defaults = {}
|
||||
self._ports = []
|
||||
self._initial_settings = None
|
||||
self._settings = {"name": name,
|
||||
"interfaces": {},
|
||||
"nios": []}
|
||||
def interfaces(self):
|
||||
|
||||
def delete(self):
|
||||
return self._interfaces
|
||||
|
||||
def _createCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Deletes this cloud.
|
||||
"""
|
||||
|
||||
# first delete all the links attached to this node
|
||||
self.delete_links_signal.emit()
|
||||
self.deleted_signal.emit()
|
||||
|
||||
def setup(self, name=None, initial_settings={}):
|
||||
"""
|
||||
Setups this cloud.
|
||||
|
||||
:param name: optional name for this cloud
|
||||
"""
|
||||
|
||||
if name:
|
||||
self._settings["name"] = name
|
||||
|
||||
if initial_settings:
|
||||
self._initial_settings = initial_settings
|
||||
|
||||
self._server.get("/interfaces", self._setupCallback)
|
||||
|
||||
def _setupCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for setup.
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
if "ports_mapping" in result:
|
||||
self._settings["ports_mapping"] = result["ports_mapping"].copy()
|
||||
|
||||
if error:
|
||||
log.error("error while setting up {}: {}".format(self.name(), result["message"]))
|
||||
# a warning message instead of a error is more appropriate here
|
||||
self.warning_signal.emit(self.id(), result["message"])
|
||||
else:
|
||||
self._settings["interfaces"] = result.copy()
|
||||
if "interfaces" in result:
|
||||
self._interfaces = result["interfaces"].copy()
|
||||
|
||||
if self._initial_settings and "nios" in self._initial_settings and self._initial_settings["nios"]:
|
||||
self._initial_settings["interfaces"] = {}
|
||||
self.update(self._initial_settings)
|
||||
else:
|
||||
log.info("cloud {} has been created".format(self.name()))
|
||||
self.setInitialized(True)
|
||||
self.created_signal.emit(self.id())
|
||||
|
||||
def _createNIOUDP(self, nio):
|
||||
"""
|
||||
Creates a NIO UDP.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_udp:(\d+):(.+):(\d+)$""", nio)
|
||||
if match:
|
||||
lport = int(match.group(1))
|
||||
rhost = match.group(2)
|
||||
rport = int(match.group(3))
|
||||
return NIOUDP(lport, rhost, rport)
|
||||
return None
|
||||
|
||||
def _createNIOGenericEthernet(self, nio):
|
||||
"""
|
||||
Creates a NIO Generic Ethernet.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_gen_eth:(.+)$""", nio)
|
||||
if match:
|
||||
ethernet_device = match.group(1)
|
||||
return NIOGenericEthernet(ethernet_device)
|
||||
return None
|
||||
|
||||
def _createNIOLinuxEthernet(self, nio):
|
||||
"""
|
||||
Creates a NIO Linux Ethernet.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_gen_linux:(.+)$""", nio)
|
||||
if match:
|
||||
linux_device = match.group(1)
|
||||
return NIOLinuxEthernet(linux_device)
|
||||
return None
|
||||
|
||||
def _createNIONAT(self, nio):
|
||||
"""
|
||||
Creates a NIO NAT.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_nat:(.+)$""", nio)
|
||||
if match:
|
||||
identifier = match.group(1)
|
||||
return NIONAT(identifier)
|
||||
return None
|
||||
|
||||
def _createNIOTAP(self, nio):
|
||||
"""
|
||||
Creates a NIO TAP.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_tap:(.+)$""", nio)
|
||||
if match:
|
||||
tap_device = match.group(1)
|
||||
return NIOTAP(tap_device)
|
||||
return None
|
||||
|
||||
def _createNIOUNIX(self, nio):
|
||||
"""
|
||||
Creates a NIO UNIX.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_unix:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
local_file = match.group(1)
|
||||
remote_file = match.group(2)
|
||||
return NIOUNIX(local_file, remote_file)
|
||||
return None
|
||||
|
||||
def _createNIOVDE(self, nio):
|
||||
"""
|
||||
Creates a NIO VDE.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_vde:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
control_file = match.group(1)
|
||||
local_file = match.group(2)
|
||||
return NIOVDE(control_file, local_file)
|
||||
return None
|
||||
|
||||
def _createNIONull(self, nio):
|
||||
"""
|
||||
Creates a NIO Null.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_null:(.+)$""", nio)
|
||||
if match:
|
||||
identifier = match.group(1)
|
||||
return NIONull(identifier)
|
||||
return None
|
||||
|
||||
def _allocateNIO(self, nio):
|
||||
"""
|
||||
Allocate a new NIO object.
|
||||
|
||||
:param nio: NIO description
|
||||
|
||||
:returns: NIO instance
|
||||
"""
|
||||
|
||||
nio_object = None
|
||||
if nio.lower().startswith("nio_udp"):
|
||||
nio_object = self._createNIOUDP(nio)
|
||||
if nio.lower().startswith("nio_gen_eth"):
|
||||
nio_object = self._createNIOGenericEthernet(nio)
|
||||
if nio.lower().startswith("nio_gen_linux"):
|
||||
nio_object = self._createNIOLinuxEthernet(nio)
|
||||
if nio.lower().startswith("nio_nat"):
|
||||
nio_object = self._createNIONAT(nio)
|
||||
if nio.lower().startswith("nio_tap"):
|
||||
nio_object = self._createNIOTAP(nio)
|
||||
if nio.lower().startswith("nio_unix"):
|
||||
nio_object = self._createNIOUNIX(nio)
|
||||
if nio.lower().startswith("nio_vde"):
|
||||
nio_object = self._createNIOVDE(nio)
|
||||
if nio.lower().startswith("nio_null"):
|
||||
nio_object = self._createNIONull(nio)
|
||||
if nio_object is None:
|
||||
log.error("Could not create NIO object from {}".format(nio))
|
||||
return nio_object
|
||||
|
||||
def update(self, new_settings):
|
||||
def update(self, new_settings, force=False):
|
||||
"""
|
||||
Updates the settings for this cloud.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
:param force: force this node to update
|
||||
"""
|
||||
|
||||
updated = False
|
||||
if "nios" in new_settings:
|
||||
nios = new_settings["nios"]
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params or force:
|
||||
self._update(params)
|
||||
|
||||
# add ports
|
||||
for nio in nios:
|
||||
if nio in self._settings["nios"]:
|
||||
# port already created for this NIO
|
||||
continue
|
||||
nio_object = self._allocateNIO(nio)
|
||||
if nio_object is None:
|
||||
continue
|
||||
port = Port(nio, nio_object, stub=True)
|
||||
port.setStatus(Port.started)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been added".format(nio))
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
# delete ports
|
||||
for nio in self._settings["nios"]:
|
||||
if nio not in nios:
|
||||
for port in self._ports.copy():
|
||||
if port.name() == nio:
|
||||
self._ports.remove(port)
|
||||
updated = True
|
||||
log.debug("port {} has been deleted".format(nio))
|
||||
break
|
||||
:param result: server response
|
||||
"""
|
||||
|
||||
self._settings["nios"] = new_settings["nios"].copy()
|
||||
if "ports_mapping" in result:
|
||||
self._settings["ports_mapping"] = result["ports_mapping"].copy()
|
||||
|
||||
if "name" in new_settings and new_settings["name"] != self.name():
|
||||
self._settings["name"] = new_settings["name"]
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
log.info("cloud {} has been updated".format(self.name()))
|
||||
self.updated_signal.emit()
|
||||
|
||||
def deleteNIO(self, port):
|
||||
|
||||
pass
|
||||
if "interfaces" in result:
|
||||
self._interfaces = result["interfaces"].copy()
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this cloud.
|
||||
|
||||
:returns: formated string
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Cloud device {name} is always-on
|
||||
This is a pseudo-device for external connections
|
||||
""".format(name=self.name())
|
||||
This is a node for external connections
|
||||
Device run on {host}
|
||||
""".format(name=self.name(),
|
||||
host=self.compute().name())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
@@ -318,121 +107,11 @@ This is a pseudo-device for external connections
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
# add the Windows interface name
|
||||
match = re.search(r"""^nio_gen_eth:(\\device\\npf_.+)$""", port.name())
|
||||
if match:
|
||||
for interface in self._settings["interfaces"]:
|
||||
if interface["name"].lower() == match.group(1):
|
||||
port_info += " Windows name: {}\n".format(interface["description"])
|
||||
break
|
||||
|
||||
return info + port_info
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this cloud
|
||||
(to be saved in a topology file).
|
||||
|
||||
:returns: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
cloud = {"id": self.id(),
|
||||
"type": self.__class__.__name__,
|
||||
"description": str(self),
|
||||
"properties": {"name": self.name(),
|
||||
"nios": self._settings["nios"]},
|
||||
"server_id": self._server.id(),
|
||||
}
|
||||
|
||||
# add the ports
|
||||
if self._ports:
|
||||
ports = cloud["ports"] = []
|
||||
for port in self._ports:
|
||||
ports.append(port.dump())
|
||||
|
||||
return cloud
|
||||
|
||||
def load(self, node_info):
|
||||
"""
|
||||
Loads a cloud representation
|
||||
(from a topology file).
|
||||
|
||||
:param node_info: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
self.node_info = node_info
|
||||
settings = node_info["properties"]
|
||||
name = settings.pop("name")
|
||||
self.updated_signal.connect(self._updatePortSettings)
|
||||
log.info("cloud {} is loading".format(name))
|
||||
self.setup(name, settings)
|
||||
|
||||
def _updatePortSettings(self):
|
||||
"""
|
||||
Updates port settings when loading a topology.
|
||||
"""
|
||||
|
||||
self.updated_signal.disconnect(self._updatePortSettings)
|
||||
# update the port with the correct IDs
|
||||
if "ports" in self.node_info:
|
||||
ports = self.node_info["ports"]
|
||||
for topology_port in ports:
|
||||
for port in self._ports:
|
||||
if topology_port["name"] == port.name():
|
||||
port.setId(topology_port["id"])
|
||||
if topology_port["name"].startswith("nio_gen_eth") or topology_port["name"].startswith("nio_linux_eth"):
|
||||
# lookup if the interface exists
|
||||
available_interface = False
|
||||
topology_port_name = topology_port["name"].split(':', 1)[1]
|
||||
for interface in self._settings["interfaces"]:
|
||||
if interface["name"] == topology_port_name:
|
||||
available_interface = True
|
||||
break
|
||||
if not available_interface:
|
||||
alternative_interface = self._module.findAlternativeInterface(self, topology_port_name)
|
||||
if alternative_interface:
|
||||
if topology_port["name"] in self._settings["nios"]:
|
||||
self._settings["nios"].remove(topology_port["name"])
|
||||
topology_port["name"] = topology_port["name"].replace(topology_port_name, alternative_interface)
|
||||
nio = self._allocateNIO(topology_port["name"])
|
||||
port.setDefaultNio(nio)
|
||||
port.setName(topology_port["name"])
|
||||
self._settings["nios"].append(topology_port["name"])
|
||||
|
||||
log.info("cloud {} has been created".format(self.name()))
|
||||
self.setInitialized(True)
|
||||
self.created_signal.emit(self.id())
|
||||
|
||||
def name(self):
|
||||
"""
|
||||
Returns the name of this cloud.
|
||||
|
||||
:returns: name (string)
|
||||
"""
|
||||
|
||||
return self._settings["name"]
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns all this cloud settings.
|
||||
|
||||
:returns: settings dictionary
|
||||
"""
|
||||
|
||||
return self._settings
|
||||
|
||||
def ports(self):
|
||||
"""
|
||||
Returns all the ports for this cloud.
|
||||
|
||||
:returns: list of Port instances
|
||||
"""
|
||||
|
||||
return self._ports
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node configurator.
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
@@ -448,17 +127,7 @@ This is a pseudo-device for external connections
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/cloud.normal.svg"
|
||||
|
||||
@staticmethod
|
||||
def hoverSymbol():
|
||||
"""
|
||||
Returns the symbol to use when the cloud is hovered.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/cloud.selected.svg"
|
||||
return ":/symbols/cloud.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
@@ -470,7 +139,7 @@ This is a pseudo-device for external connections
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node category (integer)
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.end_devices]
|
||||
|
||||
57
gns3/modules/builtin/dialogs/cloud_wizard.py
Normal file
57
gns3/modules/builtin/dialogs/cloud_wizard.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Wizard for cloud nodes.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.node import Node
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.cloud_wizard_ui import Ui_CloudNodeWizard
|
||||
from .. import Builtin
|
||||
|
||||
|
||||
class CloudWizard(VMWizard, Ui_CloudNodeWizard):
|
||||
|
||||
"""
|
||||
Wizard to create a cloud node template.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, cloud_nodes, parent):
|
||||
|
||||
super().__init__(cloud_nodes, parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/cloud.svg"))
|
||||
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
|
||||
def getSettings(self):
|
||||
"""
|
||||
Returns the settings set in this Wizard.
|
||||
|
||||
:return: settings dict
|
||||
"""
|
||||
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"category": Node.end_devices,
|
||||
"server": self._compute_id}
|
||||
|
||||
return settings
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user