mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-05-17 08:56:06 +03:00
Compare commits
892 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d1bff782c | ||
|
|
4e3eee2383 | ||
|
|
da8aa0d2fd | ||
|
|
5b4481c43a | ||
|
|
593cb8c1fd | ||
|
|
210cf63fe2 | ||
|
|
3b178013c0 | ||
|
|
6e44d6b919 | ||
|
|
6b520b8036 | ||
|
|
803782b9d8 | ||
|
|
d3d6ca3f2e | ||
|
|
f545c793f8 | ||
|
|
47d6a4fef6 | ||
|
|
8862b608cf | ||
|
|
76832ab83f | ||
|
|
fed245fd34 | ||
|
|
3e0f1affd0 | ||
|
|
2110c2805e | ||
|
|
46cfdd8314 | ||
|
|
f8f648c2b6 | ||
|
|
7cd0187f33 | ||
|
|
4d8f362f11 | ||
|
|
469eaa4737 | ||
|
|
c921224b30 | ||
|
|
61487b2e2f | ||
|
|
9affca495e | ||
|
|
9d8886a640 | ||
|
|
98cfec1b77 | ||
|
|
aed174953e | ||
|
|
f0feea8262 | ||
|
|
e2aeaf0a78 | ||
|
|
b92bb94875 | ||
|
|
c56db59353 | ||
|
|
a87c4e21d7 | ||
|
|
ed99a989d7 | ||
|
|
f9a4c9399a | ||
|
|
efb5c8ca9a | ||
|
|
0946dff3a0 | ||
|
|
d7d96b10e5 | ||
|
|
0c0b2d5cb3 | ||
|
|
450fbc9af3 | ||
|
|
469ee8fab8 | ||
|
|
6ccfcaf76e | ||
|
|
520e857874 | ||
|
|
012c7b4241 | ||
|
|
1d71cd5bf0 | ||
|
|
17d1a7f4ed | ||
|
|
0cd5c08c6b | ||
|
|
20ac503fe9 | ||
|
|
5f737c2c7c | ||
|
|
eb1a37be36 | ||
|
|
07c64b5432 | ||
|
|
ce981d1c49 | ||
|
|
32a9f2556e | ||
|
|
7f08675121 | ||
|
|
1dc3c13df2 | ||
|
|
6a6e86b325 | ||
|
|
d96277882a | ||
|
|
ecec917752 | ||
|
|
ea9c1a8ee1 | ||
|
|
cfbb09fb57 | ||
|
|
dc8aa1fb92 | ||
|
|
786cc8aa65 | ||
|
|
4a353e08e3 | ||
|
|
1371921586 | ||
|
|
cd8696a714 | ||
|
|
17799719d6 | ||
|
|
2a59013604 | ||
|
|
1c46299dd9 | ||
|
|
628d7cb909 | ||
|
|
b23c92c0fb | ||
|
|
49ce5a9f38 | ||
|
|
4575ea9f6d | ||
|
|
fd6a00df6a | ||
|
|
58ab4b424a | ||
|
|
1ea1abf582 | ||
|
|
e8caab74f4 | ||
|
|
9fce393fd1 | ||
|
|
827c11ae97 | ||
|
|
eb370d5672 | ||
|
|
7732aaf9a5 | ||
|
|
63161eb760 | ||
|
|
5dba814d1b | ||
|
|
aecdc71f3a | ||
|
|
3209c1d0e6 | ||
|
|
2b3fb53ef2 | ||
|
|
cbbbece0e5 | ||
|
|
56d742b19f | ||
|
|
1f566a31cf | ||
|
|
10d75e15da | ||
|
|
17def7e00a | ||
|
|
106afd0987 | ||
|
|
bba9c5e1d8 | ||
|
|
ae8e8013d4 | ||
|
|
3a5f1d60f9 | ||
|
|
3f6eb61382 | ||
|
|
32bfff381d | ||
|
|
f68a8ea829 | ||
|
|
50066b2f12 | ||
|
|
21a99d4376 | ||
|
|
f97d3041b8 | ||
|
|
31d6a065b0 | ||
|
|
20bf63dbbf | ||
|
|
1c3e0ef640 | ||
|
|
5b58d3ab6d | ||
|
|
554c9205f3 | ||
|
|
543a8e7c33 | ||
|
|
69ef35c674 | ||
|
|
45102a07b6 | ||
|
|
f0b8b22e8a | ||
|
|
d94f5a2d8c | ||
|
|
a768661c05 | ||
|
|
4657b005b6 | ||
|
|
e71da830b0 | ||
|
|
ebf2563200 | ||
|
|
e8eaa00244 | ||
|
|
d750e7a427 | ||
|
|
bfc8adc904 | ||
|
|
4de38ea590 | ||
|
|
cc0c6d0a7a | ||
|
|
d1d0810233 | ||
|
|
ee3c758bb7 | ||
|
|
8f077456b1 | ||
|
|
a29f3e35c0 | ||
|
|
b12cb5c939 | ||
|
|
ba646f5efa | ||
|
|
edafc29cdc | ||
|
|
5aa67d18c0 | ||
|
|
8067aaadd4 | ||
|
|
4a012c4d88 | ||
|
|
7f234aa648 | ||
|
|
dbbcdf0f73 | ||
|
|
a6c56a0963 | ||
|
|
466d427295 | ||
|
|
e5b8bdc106 | ||
|
|
25a6b6b3b1 | ||
|
|
39723a2212 | ||
|
|
9cd0597879 | ||
|
|
c2472bcb22 | ||
|
|
b9caf7216a | ||
|
|
6b23de94b0 | ||
|
|
ab1324ffba | ||
|
|
21bcfde8f3 | ||
|
|
3616bd6c85 | ||
|
|
740e9bab87 | ||
|
|
198cf833e9 | ||
|
|
21f5a64b07 | ||
|
|
fc3781550a | ||
|
|
a9a2a541c0 | ||
|
|
8998c07e0e | ||
|
|
ba01a89af1 | ||
|
|
eae07d62ad | ||
|
|
23903cf0c9 | ||
|
|
4d908fd855 | ||
|
|
bb0e67be4f | ||
|
|
d285e62c04 | ||
|
|
44d70de687 | ||
|
|
752c516f82 | ||
|
|
e1ec6c5771 | ||
|
|
e8308869d9 | ||
|
|
484c5abe9d | ||
|
|
c85112978d | ||
|
|
e57f6db9f0 | ||
|
|
edee26c77c | ||
|
|
fe222b873f | ||
|
|
1acf44de21 | ||
|
|
f8bb6661dd | ||
|
|
ac50dffabd | ||
|
|
fbb28a4325 | ||
|
|
3e47267e35 | ||
|
|
0f9aab9230 | ||
|
|
a5cf5e16b7 | ||
|
|
7f8269bb44 | ||
|
|
097458d108 | ||
|
|
2e0ce6afe0 | ||
|
|
f4cafac9c7 | ||
|
|
7f132fdc36 | ||
|
|
6b7d629755 | ||
|
|
b7ccc37ea5 | ||
|
|
bbe2826c77 | ||
|
|
dda0447839 | ||
|
|
9398dd0840 | ||
|
|
6e3c5c1bb8 | ||
|
|
a83208178b | ||
|
|
f1b7c0e176 | ||
|
|
429c2ab650 | ||
|
|
68e2a0ee39 | ||
|
|
b27c024449 | ||
|
|
7bbd337801 | ||
|
|
7c545e3860 | ||
|
|
5b6491a23f | ||
|
|
12e4f8445d | ||
|
|
52418ed94a | ||
|
|
a1496bffd4 | ||
|
|
911f6305fa | ||
|
|
c6594d4845 | ||
|
|
538adc4817 | ||
|
|
961c5652ea | ||
|
|
c0ecf3ccc4 | ||
|
|
33bc644688 | ||
|
|
6b38b58633 | ||
|
|
938e9129cd | ||
|
|
8f381a4720 | ||
|
|
c1fd434f75 | ||
|
|
2440faf3f5 | ||
|
|
0bd480eabb | ||
|
|
dc3d762799 | ||
|
|
1d139cee4d | ||
|
|
d033268cd9 | ||
|
|
82fdeb3a49 | ||
|
|
3462109ef2 | ||
|
|
b6e5a588bf | ||
|
|
4e2a80e379 | ||
|
|
fc61079132 | ||
|
|
f48f4eacd2 | ||
|
|
92e2920212 | ||
|
|
ebb2d8bb73 | ||
|
|
ade748c3b1 | ||
|
|
ec334508a6 | ||
|
|
ed88eaa620 | ||
|
|
524f911293 | ||
|
|
fa9fc0ff8d | ||
|
|
cf73db25b4 | ||
|
|
dd79939140 | ||
|
|
6d0afd39d7 | ||
|
|
1d7a6611bd | ||
|
|
15aa4c6001 | ||
|
|
6f42208323 | ||
|
|
45b3c17c97 | ||
|
|
1ddf3e6388 | ||
|
|
d4f0f76e57 | ||
|
|
e0c06ecd78 | ||
|
|
f839bbf877 | ||
|
|
52c06f4730 | ||
|
|
a8593bb39e | ||
|
|
396e871b3b | ||
|
|
ba9298735a | ||
|
|
f4bae20592 | ||
|
|
189852862e | ||
|
|
ba59d69536 | ||
|
|
26de22f105 | ||
|
|
d47b5040cf | ||
|
|
dcf133b297 | ||
|
|
b50fe81d86 | ||
|
|
1c8e166393 | ||
|
|
6afdb18bdb | ||
|
|
a454283357 | ||
|
|
9369ad9645 | ||
|
|
be997d4d25 | ||
|
|
d1b9185764 | ||
|
|
2b98e48420 | ||
|
|
ec21134920 | ||
|
|
9b73d652d3 | ||
|
|
3c67b70ff3 | ||
|
|
60f58064b3 | ||
|
|
ca305cefa4 | ||
|
|
8ce928aec2 | ||
|
|
9ff8816273 | ||
|
|
42d5d4b542 | ||
|
|
2cf64a99de | ||
|
|
7e942a7753 | ||
|
|
23d467f688 | ||
|
|
fede614716 | ||
|
|
c31b5dd7a9 | ||
|
|
3ba811e675 | ||
|
|
f6a738fe3e | ||
|
|
4b577e96dd | ||
|
|
c6ed354629 | ||
|
|
ec324f9b01 | ||
|
|
cbfd59498e | ||
|
|
0216bc8b4d | ||
|
|
a8477597ab | ||
|
|
4c7965d70f | ||
|
|
28acb2911f | ||
|
|
e240dbad6b | ||
|
|
fac27d9df9 | ||
|
|
8d183a3283 | ||
|
|
3af5046d0f | ||
|
|
c8397a1ef7 | ||
|
|
b419891950 | ||
|
|
2c1ba697bd | ||
|
|
3000a9aa7f | ||
|
|
2f8541c543 | ||
|
|
5c3d4b2ab6 | ||
|
|
f44ac8cba5 | ||
|
|
7ef49fbca7 | ||
|
|
5ccf5778a2 | ||
|
|
6030d5e019 | ||
|
|
6de8880937 | ||
|
|
08c89c4fac | ||
|
|
e411d497c4 | ||
|
|
e037835769 | ||
|
|
a5f4ec0135 | ||
|
|
3ee68b22bd | ||
|
|
154f10a686 | ||
|
|
e5320c318f | ||
|
|
07ea6207c1 | ||
|
|
12398881f8 | ||
|
|
27a8e3c7f8 | ||
|
|
d92ff1abe3 | ||
|
|
e97b3b6a42 | ||
|
|
5ee3f73213 | ||
|
|
a30aa2f5f1 | ||
|
|
98bb6590aa | ||
|
|
4250e961a3 | ||
|
|
3c46a3a72d | ||
|
|
c82d262975 | ||
|
|
aa84d100b1 | ||
|
|
e51477d989 | ||
|
|
e4a29f30e3 | ||
|
|
3d8bd16536 | ||
|
|
c55442a517 | ||
|
|
45e0080726 | ||
|
|
bb013804d4 | ||
|
|
95558ec2e6 | ||
|
|
f1cd31baa6 | ||
|
|
cf7176559d | ||
|
|
2504085db2 | ||
|
|
75d3b61783 | ||
|
|
5da8e77d01 | ||
|
|
5b56d54030 | ||
|
|
3de38d2ccb | ||
|
|
76553ff102 | ||
|
|
67b2d145da | ||
|
|
d5ee1ea5d2 | ||
|
|
69b8c07c0a | ||
|
|
dbe73eb8d7 | ||
|
|
ba0559bf08 | ||
|
|
706f89debb | ||
|
|
ec0be9e22b | ||
|
|
0e6fa597ec | ||
|
|
f81450c65a | ||
|
|
38cbe70aaa | ||
|
|
9fd07b6379 | ||
|
|
c84b262303 | ||
|
|
0150515338 | ||
|
|
47d335f4c9 | ||
|
|
ffc08361ce | ||
|
|
ab90f5f458 | ||
|
|
a0d6a43b51 | ||
|
|
20d4f73f56 | ||
|
|
5204184029 | ||
|
|
9915beeb8e | ||
|
|
1ea383fce2 | ||
|
|
2744e669b4 | ||
|
|
8fd9ec5319 | ||
|
|
a5f3164feb | ||
|
|
3d949df14c | ||
|
|
6a5764fda9 | ||
|
|
f312d57165 | ||
|
|
973793e6b6 | ||
|
|
2a7ce661da | ||
|
|
a85f99185a | ||
|
|
d511d0f5f8 | ||
|
|
b92a589762 | ||
|
|
6ab2d63bdc | ||
|
|
0de6bfe7e1 | ||
|
|
b024eb63e9 | ||
|
|
f144103bca | ||
|
|
7c1af696b9 | ||
|
|
c0b26aff48 | ||
|
|
9601e4e6f2 | ||
|
|
88708c2a8d | ||
|
|
8eff12194d | ||
|
|
b0520b2bd4 | ||
|
|
17d2c023bf | ||
|
|
ce9fdea0a0 | ||
|
|
24d7dacb4e | ||
|
|
bb36765407 | ||
|
|
250db92ce0 | ||
|
|
d59ec39505 | ||
|
|
5e9ae04dc1 | ||
|
|
ddb0fccda3 | ||
|
|
9b22a52f14 | ||
|
|
948878bfdd | ||
|
|
7340abbaa9 | ||
|
|
1c1ea50adc | ||
|
|
4ea0528bf2 | ||
|
|
49005e6add | ||
|
|
5484c039b5 | ||
|
|
daaf71b6d2 | ||
|
|
450f0e006b | ||
|
|
a6a967fbde | ||
|
|
1a6293709e | ||
|
|
2ed53225e0 | ||
|
|
b8798fbda5 | ||
|
|
c2ac68be49 | ||
|
|
368de32faa | ||
|
|
98d01cbfa0 | ||
|
|
ad62bb7832 | ||
|
|
637061663a | ||
|
|
c137198985 | ||
|
|
946efb61de | ||
|
|
cb74a8e12f | ||
|
|
c42fecaea3 | ||
|
|
088b020ac0 | ||
|
|
af507e7668 | ||
|
|
204ff1f8fd | ||
|
|
8c7e8e412a | ||
|
|
030169dc10 | ||
|
|
e877adca35 | ||
|
|
18dc8fab14 | ||
|
|
60018612b1 | ||
|
|
0410c446fc | ||
|
|
18486e4772 | ||
|
|
3c9787effb | ||
|
|
664da8ee3d | ||
|
|
b4da9b7bae | ||
|
|
5ef612815b | ||
|
|
06d7ed783f | ||
|
|
cc5b55a7ce | ||
|
|
c8e1602a26 | ||
|
|
43688cb9bd | ||
|
|
eb34715178 | ||
|
|
b7d78b92fc | ||
|
|
ab7930d3d9 | ||
|
|
c684e63be2 | ||
|
|
4c610acfa4 | ||
|
|
37f74824f1 | ||
|
|
5ccf8c414d | ||
|
|
913f0d5e4a | ||
|
|
061bac0cc6 | ||
|
|
20ff8a19f6 | ||
|
|
53ba302515 | ||
|
|
4b43dfb77c | ||
|
|
f8555f4008 | ||
|
|
75c3092724 | ||
|
|
89e274d040 | ||
|
|
f9619d79ae | ||
|
|
7fd9f39c36 | ||
|
|
bb732bc202 | ||
|
|
481e6c3450 | ||
|
|
7ad663cc2a | ||
|
|
ec59cd87bd | ||
|
|
d4a0b21206 | ||
|
|
05d9ee8499 | ||
|
|
3e0242ada7 | ||
|
|
a72ece5c18 | ||
|
|
63baa2eff0 | ||
|
|
b91fd4a0c2 | ||
|
|
718217e332 | ||
|
|
ba71e560f9 | ||
|
|
1989ec3a40 | ||
|
|
c202c5e4be | ||
|
|
71830dd69f | ||
|
|
37a7fdfa68 | ||
|
|
cb074be0a1 | ||
|
|
08784158c1 | ||
|
|
0efe006cad | ||
|
|
4a663a5910 | ||
|
|
a559bd4ae4 | ||
|
|
2539abd445 | ||
|
|
e76f1ca5cc | ||
|
|
bc338b6232 | ||
|
|
ddb581623a | ||
|
|
486faf6718 | ||
|
|
a081dcddb8 | ||
|
|
c4160ec942 | ||
|
|
f38d9ef525 | ||
|
|
6639108354 | ||
|
|
a63a097341 | ||
|
|
94bad69198 | ||
|
|
e9057e75a0 | ||
|
|
b80b86d365 | ||
|
|
5ebb3011d3 | ||
|
|
81300fd40e | ||
|
|
d4dda2a285 | ||
|
|
5a4342d4b8 | ||
|
|
94fc5e6c4f | ||
|
|
a3e81fbf2e | ||
|
|
514eb97eac | ||
|
|
7637039cb2 | ||
|
|
e46c92e92f | ||
|
|
ac989b191b | ||
|
|
c971cef31b | ||
|
|
c1af2df780 | ||
|
|
eaaa141be9 | ||
|
|
226169cdc6 | ||
|
|
42a4c89f20 | ||
|
|
38ade919df | ||
|
|
6458f88d1c | ||
|
|
1e936da469 | ||
|
|
f90ec81fca | ||
|
|
141578a1e1 | ||
|
|
e1d2bcca20 | ||
|
|
a5435280d7 | ||
|
|
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 | ||
|
|
d51c96f105 | ||
|
|
a47b839cc2 | ||
|
|
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 | ||
|
|
80f3dab152 | ||
|
|
eef4d6e9fd | ||
|
|
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 | ||
|
|
78e7b78315 | ||
|
|
b3f8170e01 | ||
|
|
18321f5347 | ||
|
|
734fcdfe9e | ||
|
|
6d74ce4070 | ||
|
|
159d21af9a | ||
|
|
713feff11f | ||
|
|
64c5ca712e | ||
|
|
1572a6f67f | ||
|
|
fcee5c6916 | ||
|
|
378d454e1e | ||
|
|
eb90950be1 | ||
|
|
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 | ||
|
|
8a303e4563 | ||
|
|
842ad8ae26 | ||
|
|
466c349642 | ||
|
|
1356fd9c69 | ||
|
|
2d1c9444c5 | ||
|
|
22d7815d8e | ||
|
|
53487d5937 | ||
|
|
ab729d8f67 | ||
|
|
eb5c10de3d | ||
|
|
1b6d534b8e | ||
|
|
a9b5b9eda2 | ||
|
|
ce12eb86e8 | ||
|
|
d4ffbd9f97 | ||
|
|
4c01a465ac | ||
|
|
012bc1e406 | ||
|
|
05ba772715 | ||
|
|
9ea57f511b | ||
|
|
5aaa2d7280 | ||
|
|
497eb19369 | ||
|
|
70049aa877 | ||
|
|
ece7930cb1 | ||
|
|
c7df589857 | ||
|
|
8bcc92f319 | ||
|
|
dedde63b60 | ||
|
|
a81d1443f9 | ||
|
|
e69089f4cf | ||
|
|
0c052542b3 | ||
|
|
00e402f28c | ||
|
|
0742b282a3 | ||
|
|
2b588aa0bf | ||
|
|
92fb8418ab | ||
|
|
9c7dbc864e | ||
|
|
25aebaa46c | ||
|
|
0e30b3cf5f | ||
|
|
755667c4d5 | ||
|
|
16dbdf70d9 | ||
|
|
806c7479ee | ||
|
|
cc8b84725a | ||
|
|
e01701614e | ||
|
|
efaffac801 | ||
|
|
c58366e9cb | ||
|
|
068ebcdea0 | ||
|
|
51f2b4bfa8 | ||
|
|
168e4ab86e | ||
|
|
7ae18ff82a | ||
|
|
c694173f9d | ||
|
|
b58b92c9f0 | ||
|
|
3ddb2e70d4 | ||
|
|
05966a9119 | ||
|
|
8ea24e9920 | ||
|
|
47f34fd5af | ||
|
|
89321a6cad | ||
|
|
6690ba7108 |
34
.github/ISSUE_TEMPLATE/gns3-bug-report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/gns3-bug-report.md
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: GNS3 bug report
|
||||
about: Create a report to help us fix a bug
|
||||
title: 'Short description of the bug'
|
||||
labels: Bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Before you start**
|
||||
Please open an issue only if you suspect there is a bug or any problem with GNS3. Go to https://gns3.com/community for any other questions or for requesting help with GNS3.
|
||||
|
||||
You may also post this issue directly on the GNS3 server repository if you know the bug comes from the server: https://github.com/GNS3/gns3-server/issues/new
|
||||
|
||||
**Describe the bug**
|
||||
Please provide a clear and detailed description of what the bug is.
|
||||
|
||||
**GNS3 version and operating system (please complete the following information):**
|
||||
- OS: [e.g. Windows, Linux or macOS]
|
||||
- GNS3 version [e.g. 2.1.14]
|
||||
- Any use of the GNS3 VM or remote server (ESXi, bare metal etc.)
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Screenshots or videos**
|
||||
If applicable, add screenshots (e.g. of the topology and/or error message) or links to videos to help explain the problem. This will help us a lot to quickly find the bug and fix it.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
10
.github/ISSUE_TEMPLATE/gns3-development.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/gns3-development.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: GNS3 development
|
||||
about: Any question or discussion regarding GNS3 development
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
25
.github/ISSUE_TEMPLATE/gns3-feature-request.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/gns3-feature-request.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: GNS3 feature request
|
||||
about: Suggest an idea for GNS3
|
||||
title: 'Short description of the feature request'
|
||||
labels: Enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Before you start**
|
||||
Please check if a similar feature request has already been submitted.
|
||||
|
||||
You may also post this issue directly on the GNS3 server repository if you know the feature request only applies to the server: https://github.com/GNS3/gns3-server/issues/new
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen. If applicable, please provide screenshots
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -60,3 +60,6 @@ keys
|
||||
updates
|
||||
.cache
|
||||
__pycache__
|
||||
|
||||
# Virtualenv
|
||||
env
|
||||
|
||||
@@ -6,8 +6,13 @@ notifications:
|
||||
script:
|
||||
- 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=
|
||||
|
||||
522
CHANGELOG
522
CHANGELOG
@@ -1,5 +1,527 @@
|
||||
# Change Log
|
||||
|
||||
## 2.2.0rc5 09/09/2019
|
||||
|
||||
* Adjust size for setup dialog and remove question about running the wizard again. Ref #2846
|
||||
|
||||
## 2.2.0rc4 30/08/2019
|
||||
|
||||
* Fix issue when asking to run the setup wizard again. Ref #2846
|
||||
* Remove warning about VirtualBox not supporting nested virtualization. Ref https://github.com/GNS3/gns3-server/issues/1610
|
||||
* Ask user if they want to see the wizard again. Ref #2846
|
||||
|
||||
## 2.2.0rc3 12/08/2019
|
||||
|
||||
* Revert to jsonschema 2.6.0 due to packaging problem.
|
||||
|
||||
## 2.2.0rc2 10/08/2019
|
||||
|
||||
* Bump jsonschema to version 3.0.2
|
||||
* Fix "Unable to change Remote Main Server IP". Fixes #2823
|
||||
* Fix "AttributeError: 'QGraphicsTextItem' object has no attribute 'locked'". Fixes #2814
|
||||
* Fix a minor typo in the setup wizard
|
||||
|
||||
## 2.2.0b4 11/07/2019
|
||||
|
||||
* Fix issue preventing to open the QFileDialog in the correct directory.
|
||||
* Remove unused edit readme action. Fixes #2816
|
||||
* Remove deprecated Qemu parameter to run legacy ASA VMs. Fixes #2827
|
||||
* Upload images on remote controller. Fixes #2828
|
||||
* Preferences dialog: send API request only if connected to controller
|
||||
* Fix AttributeError: 'QGraphicsTextItem' object has no attribute 'locked'. Fixes #2814
|
||||
* Fix KeyError: 'chassis' when converting old IOS templates. Fixes #2813
|
||||
|
||||
## 2.2.0b3 15/06/2019
|
||||
|
||||
* Fix template migration issues from GUI to controller. Fixes https://github.com/GNS3/gns3-gui/issues/2803
|
||||
* %guest-cid% variable implementation for Qemu VMs. Fixes https://github.com/GNS3/gns3-gui/issues/2804
|
||||
* Increase timeout from 2 to 5 seconds for synchronous check. Ref #2805
|
||||
|
||||
## 2.2.0b2 29/05/2019
|
||||
|
||||
* Fix KeyError: 'endpoint' issue. Fixes #2802
|
||||
* Fix wrong aligment of symbols in saved/exported projects. Fixes #2800
|
||||
* Replace urllib.request by Qt implementation for local server synchronous check. Fixes #2793
|
||||
* Support snapshots for portable projects. Fixes https://github.com/GNS3/gns3-gui/issues/2792
|
||||
* Fix event notification problem for projects and how snapshots are restored.
|
||||
* Do not close the nodes dock widget when creating project.
|
||||
* Fix no scan for images on remote controller. Fixes #2799
|
||||
* Use QNetworkAccessManager to download custom appliance symbols.
|
||||
* Experimental auto upgrade should not be available for "frozen" app. Fixes #2797
|
||||
* Don't allow link labels to be moved for locked nodes. Fixes #2794
|
||||
* Catch more OSError/PermissionError when checking md5 on remote images. Fixes #2582
|
||||
* Fix exception when grid size is 0. Fixes #2790
|
||||
* Catch PermissionError when scanning local image directories. Fixes #2791
|
||||
|
||||
## 2.1.20 29/05/2019
|
||||
|
||||
* Fix KeyError: 'endpoint' issue. Fixes #2802
|
||||
|
||||
## 2.1.19 28/05/2019
|
||||
|
||||
* Fix wrong aligment of symbols in saved/exported projects. Fixes #2800
|
||||
* Replace urllib.request by Qt implementation for local server synchronous check. Fixes #2793
|
||||
* Set grid's minimum to 5. Fixes #2795
|
||||
|
||||
## 2.1.18 22/05/2019
|
||||
|
||||
* Fix error in HTTPConnection.request for Python3.6. Fixes #2793
|
||||
* Catch more OSError/PermissionError when checking md5 on remote images. Fixes #2582
|
||||
* Fix exception when grid size is 0. Fixes #2790
|
||||
* Catch PermissionError when scanning local image directories. Fixes #2791
|
||||
* Revert "Make sure the latest PyQt5 version 5.12.x is used on Windows." Ref #2778
|
||||
|
||||
## 2.2.0b1 21/05/2019
|
||||
|
||||
* Change behavior when an IOU license is verified. Fixes https://github.com/GNS3/gns3-server/issues/1555
|
||||
* Fix cannot load new profile. Fixes #2784
|
||||
* Fix remote packet capture when controller is also remote. Fixes #2785
|
||||
* Set console type to "none" by default for Ethernet switches and add a warning if trying to use "telnet". Fixes https://github.com/GNS3/gns3-gui/issues/2776
|
||||
* Add tooltip for symbol theme support in general preferences. Fixes #2770
|
||||
* Support for persistent docker volumes
|
||||
|
||||
## 2.1.17 17/05/2019
|
||||
|
||||
* No changes.
|
||||
|
||||
## 2.2.0a5 15/04/2019
|
||||
|
||||
* Revert "Drop old Qemu support (Windows and macOS) and legacy ASA support." Ref https://github.com/GNS3/gns3-server/issues/1579
|
||||
* Do not make NPF or NPCAP service mandatory to start the local server on Windows.
|
||||
* Do not try to upload a local image that is already installed on the local server.
|
||||
* Back to the major.minor version for config files. Ref https://github.com/GNS3/gns3-gui/issues/2756
|
||||
* Some adjustments with compute WebSocket handling. Ref https://github.com/GNS3/gns3-server/issues/1564
|
||||
* Fix AttributeError: 'GraphicsView' object has no attribute '_import_config_dir'. Fixes #2768
|
||||
* Do not try to lock a SvgIconItem. Fixes #2766
|
||||
* Prevent locked nodes to be deleted. Fixes https://github.com/GNS3/gns3-gui/issues/2764
|
||||
* Add PuTTY 0.71 and mark GNS3 PuTTY as deprecated. Fixes #2758
|
||||
* Fix bug with IOS platform detection. Fixes #2760
|
||||
|
||||
## 2.1.16 15/04/2019
|
||||
|
||||
* Do not make NPF or NPCAP service mandatory to start the local server on Windows.
|
||||
* Fix OverflowError error with progress dialog. Fixes #2767
|
||||
* More fixes for stuck progress window. Fixes #2765
|
||||
* Fix adding multiple devices - stuck progress window. Fixes #2765
|
||||
* Make sure the latest PyQt5 version 5.12.x is used on Windows.
|
||||
* Show a warning when a config export is not supported. Ref #2762
|
||||
|
||||
## 2.1.15 21/03/2019
|
||||
|
||||
* No changes on the GUI.
|
||||
|
||||
## 2.2.0a4 05/04/2019
|
||||
|
||||
* Use the full version number for path to config files. Ref https://github.com/GNS3/gns3-gui/issues/2756
|
||||
* Fix error message when shutting down GUI without a started server.
|
||||
* Fix remote packet capture and make sure packet capture is stopped when deleting an NIO. Fixes https://github.com/GNS3/gns3-gui/issues/2753
|
||||
* Store config files in version specific location
|
||||
* Update pytest from 4.3.1 to 4.4.0
|
||||
* Fix error messages on closing GNS3 application. Fixes https://github.com/GNS3/gns3-gui/issues/2750
|
||||
* Fix bug when list of files for an appliance is not displayed.
|
||||
* Update 'local' to 'bundled' in server & gui, Fixes: #1561
|
||||
|
||||
## 2.2.0a3 25/03/2019
|
||||
|
||||
* Fix bug when changing symbol. Fixes #2740
|
||||
* Fix issue when images are not uploaded from appliance wizard. Ref https://github.com/GNS3/gns3-gui/issues/2738
|
||||
|
||||
## 2.2.0a2 14/03/2019
|
||||
|
||||
* Try to handle stacked widget layout differently. Ref #2605
|
||||
* Early support for symbol themes.
|
||||
* Download custom appliance symbols from GitHub Fix symbol cache issue. Ref https://github.com/GNS3/gns3-gui/issues/2671 Fix temporary directory for symbols was not deleted Fix temporary appliance file was not deleted
|
||||
* New export project wizard.
|
||||
* Update paths for binaries moved to the MacOS directory in GNS3.app
|
||||
* Prevent to change layer position for locked items. Ref #2679
|
||||
* Display available appliances in a hierarchical folder structure. Fixes #2702
|
||||
* Handle locking/unlocking items independently from the layer position.
|
||||
* Better description to why an appliance cannot be installed.
|
||||
* Force jsonschema dependency to 2.6.0
|
||||
* Fix broken idle-pc support. Fixes #1515
|
||||
|
||||
## 2.2.0a1 29/01/2019
|
||||
|
||||
* Fix default NAT interface not restored on Windows. Fixes #2681
|
||||
* Merge and improvements to the setup wizard. Fixes #2676.
|
||||
* Adjust the setup wizard (VMware image size, layouts).
|
||||
* Refactor appliance wizard.
|
||||
* Natural sorting support for custom adapters tree widget.
|
||||
* Add the word "template" to configuration dialog titles.
|
||||
* Reorder node contextual menu.
|
||||
* Option to limit the size of node symbols (activated by default). Ref #2674.
|
||||
* Resize SVG node symbol only when height is above 80px. Ref #2674 Work on str instead of binary when resizing SVG symbol.
|
||||
* Automatically resize SVG symbols that are too big. Ref #2674.
|
||||
* Bigger new template wizard.
|
||||
* Fix DeprecationWarning: invalid escape sequence. Fixes https://github.com/GNS3/gns3-gui/issues/2670
|
||||
* Use theme icons in other contextual menus. Fixes #2669
|
||||
* Change some text regarding appliance installation.
|
||||
* Handle errors when creating template from appliance.
|
||||
* Template creation from an appliance.
|
||||
* Basic support to create new template from appliance.
|
||||
* Fix race condition when trying to automatically open a console and the project is already running. Fixes #1493.
|
||||
* Fix issue with IOS c7200 templates and usage variable.
|
||||
* Add usage instructions to node tooltip. Ref #2662.
|
||||
* Smaller node info dialog.
|
||||
* New node information dialog to display general, usage and command line information. Ref https://github.com/GNS3/gns3-gui/issues/2662 https://github.com/GNS3/gns3-gui/issues/2656
|
||||
* Support "usage" field for Dynamips, IOU, VirtualBox and VMware. Fixes https://github.com/GNS3/gns3-gui/issues/2657
|
||||
* Add "new template" entry to File menu. Fixes #2658
|
||||
* Fix bug with filter in add template. Fixes #2651.
|
||||
* Fix missing method '_newApplianceActionSlot'. Fixes #2643.
|
||||
* Use "template" to name what we use to create new nodes.
|
||||
* Use project instead of topology where appropriate.
|
||||
* Make sure nothing is named "compute server".
|
||||
* Use "node" instead of "appliance" for grid support.
|
||||
* Support for differing grid sizes for appliances and drawings. Requires corresponding commit on gns3-server.
|
||||
* New projects can be created with show grid/snap to grid.
|
||||
* Disallow changing layer of a locked object. Ref #2513.
|
||||
* Cosmetic changes regarding appliances.
|
||||
* Fix issue when duplicating an appliance on GUI side.
|
||||
* Fix issue to access configuration pages for Ethernet switch and hub appliances.
|
||||
* Fix small bugs when using the new appliance management API.
|
||||
* Fix bug with custom adapters and categories for Docker VM. Fixes https://github.com/GNS3/gns3-gui/issues/2613
|
||||
* Fix bug with categories with Docker appliances.
|
||||
* Schema validation for appliance API. Ref #1427.
|
||||
* Remove generic controller settings API endpoint.
|
||||
* Fix conflict between the two websocket streams (project & controller).
|
||||
* Fix platform.linux_distribution() is deprecated. Fixes https://github.com/GNS3/gns3-gui/issues/2578
|
||||
* Allow multiple appliances to be installed. Ref #2490
|
||||
* Add more information about appliance templates.
|
||||
* New appliance wizard to install an appliance from different sources. Ref #2490
|
||||
* Redesign appliance handling part 1. Ref #2490 - Removed appliance templates from device dock - Use new controller notification stream - Fixed device update and remove from device dock
|
||||
* Fix "Network session error" issues. Fixes #2560.
|
||||
* Set default layer for newly created nodes to 1 and 2 for all other drawings. Ref #2513.
|
||||
* Deactivate TraceNG module
|
||||
* Main menu actions to WebUI and Light Web Interface
|
||||
* Enable TraceNG module
|
||||
* Add Solar-Putty command line. Ref #2519.
|
||||
* Fix issues when locking/unlocking items. Ref #2513.
|
||||
* Fix tests for default note font/color.
|
||||
* Console support for clouds (to connect to external devices or services). Fixes #2500.
|
||||
* Fix LabelItem tests.
|
||||
* Separate appliance font from note font. Fixes #2477.
|
||||
* Do not include spaces in link description (%d replacement) for packet analyzer command. Ref #2485.
|
||||
* Fix error when trying to open project. Fixes #2508
|
||||
* Launch packet capture analyzer command without creating pipe.
|
||||
* Streamline appliance wizard. Fixes #2224.
|
||||
* Fix "Node list view not updated when renaming or deleting appliance template". Fixes #2356.
|
||||
* Automatically resize the Custom adapters configuration dialog. Fixes #2467.
|
||||
* Change size of custom adapters configuration dialog. Ref #2467.
|
||||
* Improve node tooltips. Fixes #2462.
|
||||
* Do not activate "console auto start" by default. Ref #1910.
|
||||
* Support for console auto start. Fixes #1910
|
||||
* Add custom_adapters setting support for appliance files. Ref #2361.
|
||||
* Possibility to customize port names and adapter types for Qemu, VirtualBox, VMware and Docker. Fixes #2361. MAC addresses can customized for Qemu as well.
|
||||
* Allow to have the projects with the same name in different locations. Fixes #2380.
|
||||
* Save state feature for VirtualBox and VMware. New "On close" setting to select the action to execute when closing/stopping a Qemu/VirtualBox/VMware VM.
|
||||
* Support for suspend to disk / resume (Qemu). Ref #725.
|
||||
* Fix bug with 'none' console type for Ethernet switch. Fix some tests related to traceng.
|
||||
* Allow to resize a Qemu VM disk (extend only). Ref #2382.
|
||||
* Allow to select the default NAT interface in preferences for local server.
|
||||
* Fix missing lock and unlock icons in resources.
|
||||
* Consistent icon styles for contextual menu. Fixes #1272.
|
||||
* Spice with agent support for Qemu VMs. Fixes #2355.
|
||||
* Fix zoom-in zoom-out step values. Ref #2457.
|
||||
* Support for console type "none" for all VMs. Fixes #2452.
|
||||
* Allow to copy Dynamips, IOU, Qemu and Docker templates in preferences. Fixes #2451.
|
||||
* Support for none console type (Qemu & Docker only)
|
||||
* Support Qemu with HAXM acceleration.
|
||||
* Use PyQt 5.10 and change AV build to use MSVS2017
|
||||
* PyQt5.10 support, Ref. #2434
|
||||
* Allow to accept a different md5 hash than the one in the appliance file. Ref. server#1246
|
||||
* Critical information during upload file with different md5, Ref. #1246
|
||||
* Restore locked item state.
|
||||
* Bump to version 2.2.0dev1 & refresh resources/ui files.
|
||||
* Have the contextual menu use icons from the active style. Ref #1272.
|
||||
* Individually lock or unlock an item on the scene. Fixes #1228.
|
||||
* Improve lock and unlock all items so some actions can still be performed on objects. Fixes #1134.
|
||||
* Lock or unlock all items button. Fixes #1134.
|
||||
* Move console to all devices icon after the separation bar. Ref #1272
|
||||
* Lock icons. Ref #1134.
|
||||
|
||||
## 2.1.14 27/02/2019
|
||||
|
||||
* Better description to why an appliance cannot be installed.
|
||||
|
||||
## 2.1.13 26/02/2019
|
||||
|
||||
* Disable computer hibernation detection mechanism. Ref #2678
|
||||
* Add some advice for request timeout message. Fixes #2652
|
||||
* Show/Hide interface labels when status points are not shown. Fixes #2690
|
||||
* Do not print critical message twice on stderr. Replace QMessageBox calls with no parent by log.error()/log.warning().
|
||||
* Show critical messages before the main window runs.
|
||||
* Avoid using PyQt5.Qt, which imports unneeded stuff. Fixes #2592
|
||||
* Fix SIP import error with recent PyQt versions. Fixes #2709
|
||||
* Upgrade to Qt 5.12. Fixes #2636
|
||||
* Adjust the setup wizard (VMware image size, layouts).
|
||||
|
||||
## 2.1.12 23/01/2019
|
||||
|
||||
* Option to resize SVG symbols that are too big (height above 80px, activated by default). Ref #2674.
|
||||
* Update VMware banners and links.
|
||||
* Allow users to refresh the template list in the nodes view panel.
|
||||
* Fix Dynamips decompress doesn't work with relative images. Fixes #2648.
|
||||
* Update download URL for "Check For Update".
|
||||
|
||||
## 2.1.11 28/09/2018
|
||||
|
||||
* Handle deleted SIP objects.
|
||||
* Update paths for UltraVNC and VirtViewer.
|
||||
* Indicate if Solar-PuTTY is included or not. Fixes #2595
|
||||
* Fix bad link to installation instructions in README.rst. Fixes #2590
|
||||
* Downgrade to Qt 5.9. Fixes #2592.
|
||||
|
||||
## 2.1.10 15/09/2018
|
||||
|
||||
* Fix small errors like unhandled exceptions etc.
|
||||
* Fix when appliance version is not available for Dynamips/IOU/Qemu. Fixes #2585.
|
||||
* Fix issue when installing appliance with no version selected. Fixes #2585.
|
||||
* Check for existing appliance name across all emulator types. Fixes #2584.
|
||||
* Improve the invalid port format detection. Fixes https://github.com/GNS3/gns3-gui/issues/2580
|
||||
* Catch OSError/PermissionError when checking md5 on remote image. Fixes #2582.
|
||||
* Fix UnicodeDecodeError in file editor. Fixes #2581.
|
||||
* Catch import error for win32serviceutil. Fixes #2583.
|
||||
* Fix bug with empty project ID when creating a new node. Fixes #2366
|
||||
* Fix various small errors, mostly about non-existing C/C++ objects.
|
||||
* Send extra controller and compute information in crash reports.
|
||||
* Update setup.py and fix minor issues.
|
||||
* Set the default delay console all value to 1500ms if using Solar-PuTTY.
|
||||
* Make Solar-Putty the default if installed. Ref #2519.
|
||||
* Fix issue with custom appliance. Fixes https://github.com/GNS3/gns3-registry/issues/361
|
||||
* Forbid controller and compute servers to be different versions. Report last compute server error to clients and display in the server summary.
|
||||
* Fix issue with appliance categories. Fixes https://github.com/GNS3/gns3-registry/issues/361
|
||||
* Add compute information to crash reports.
|
||||
* Add controller version in Sentry bug reports.
|
||||
* Backport: Fix "Network session error" issues. Fixes #2560.
|
||||
* Add SolarPutty command line. Fixes #2519.
|
||||
* Add missing Qemu boot priority values. Fixes https://github.com/GNS3/gns3-server/issues/1385
|
||||
* Update PyQt5 from version 5.8 to version 5.10. Fixes #2564.
|
||||
|
||||
## 2.1.9 13/08/2018
|
||||
|
||||
* Fix incorrect short port names in topology summary. Fixes https://github.com/GNS3/gns3-gui/issues/2562
|
||||
* Add compute version in server summary tooltip.
|
||||
* Fix test for Qemu boot priority. Fixes #2548.
|
||||
* Fix boot priority missing when installing an appliance. Fixes #2548.
|
||||
* Support PATH with UTF-8 characters in OSX telnet console, fixes #2537
|
||||
* Allow users to accept different MD5 hashes for preconfigured appliances. Fixes #2526.
|
||||
* Do not try to update drawing if it is being deleted. Ref #2483.
|
||||
* Catch exception when loading invalid appliance file.
|
||||
|
||||
## 2.1.8 14/06/2018
|
||||
|
||||
* Add error information when cannot access/read IOS/IOU config file. Ref #2501
|
||||
* Fallback when using process name to bring console to front.
|
||||
* Use process name to bring console to front. Fixes #2514.
|
||||
|
||||
## 2.1.7 12/06/2018
|
||||
|
||||
* Do not try to update link if it is being deleted. Fixes #2483.
|
||||
* Fix can't add SVG image to project. Fixes #2502
|
||||
* Remove unwanted trailing characters and other white spaces when reading .md5sum files. Fixes #2498.
|
||||
* Update interface sequence number check. Fixes #2491.
|
||||
* Logo should not have context menu, Fixes: #2507
|
||||
* Update logo position only when changes, Fixes: #2506
|
||||
|
||||
## 2.1.6 22/05/2018
|
||||
|
||||
* Ask for global variables when project is loaded
|
||||
* Add/Edit global variables of project
|
||||
* Rename tabs at Edit Project
|
||||
* Global variables tab on Edit project
|
||||
* Support of supplier logo and url
|
||||
* Add missing crowdfunder name in About dialog.
|
||||
* Project variables and supplier
|
||||
* No timeout when duplicating a project.
|
||||
* No timeout when restoring snapshot.
|
||||
* Add advanced settings for docker and ExtraHosts param, Ref. #2482
|
||||
* Replace "not supported" by "none" in topology summary view.
|
||||
|
||||
## 2.1.5 18/04/2018
|
||||
|
||||
* Fix Qemu binary list locks when a version is deleted. Fixes #2474.
|
||||
* Fix invalid answer from the PyPi server. Fixes #2473.
|
||||
* Fix wrong wizard page name.
|
||||
* Grid size support for projects. Fixes #2469.
|
||||
* Remove 'include INSTALL' from MANIFEST. Fixes #2470.
|
||||
* Check for valid IP address and prevent to run on non-Windows platforms.
|
||||
|
||||
## 2.1.4 12/03/2018
|
||||
|
||||
* Update node on server on any change, Fixes: #2429
|
||||
* Mark IOU layer 1 keepalive messages feature as non-functional. Fixes #2431.
|
||||
* Images refresh when added via settings, Fixes:#2423
|
||||
* Emit project_loaded_signal after project creation
|
||||
* Add option Show interface labels on new project, Ref. #2308
|
||||
* Improve finding pyuic3.exe on Windows
|
||||
* Use debug for error downloading file messages. Fixes #2398.
|
||||
* Refresh buttons in the cloud node to query the server for available interfaces. Fixes #2416.
|
||||
* Handle Certifacte Error, Ref. gns3-server#1262
|
||||
* Backward compatibility for tests, Ref. #2405?
|
||||
* Use UTF-8 for IOURC file migration.
|
||||
* Look for symbols on controller, Ref. #2405
|
||||
* Display an error message if Telnet console program cannot be executed.
|
||||
|
||||
## 2.1.3 19/01/2018
|
||||
|
||||
* Change messages when there are different client and server versions. Fixes #2391.
|
||||
* Fix "Transport selection via DSN is deprecated" message. Sync is configured with HTTPTransport.
|
||||
* Refresh CPU/RAM info every 1 second. Ref #2262.
|
||||
* Only check for AVG on Windows
|
||||
* Improve the search for VBoxManage.
|
||||
* Allow telnet console to node with name containing double quotes. Fixes #2371.
|
||||
|
||||
## 2.1.2 08/01/2018
|
||||
|
||||
* Update VMware promotion in setup wizard.
|
||||
* Confirm exit. Fixes #2359.
|
||||
* Fix with .exe build
|
||||
|
||||
## 2.1.1 22/12/2017
|
||||
|
||||
* Fix dragging appliance into topology from nodes window, fixes: #2363
|
||||
* Fix Appliances in Docked mode, fixes: #2362
|
||||
* Create local variable in order to debug issue in the next occurrence, #2366
|
||||
* Fix ParseError: not well-formed (invalid token), #2364
|
||||
* Fix local variable 'vm' referenced before assignment #2365
|
||||
* Fix: 'NodesDockWidget' object has no attribute 'uiNodesView', #2362
|
||||
* Tentative fix for packet capture not working correctly when remote main server is configured. Ref #2111.
|
||||
* Log Qt messages with log.debug() instead of log.info().
|
||||
* Fix auto idle-pc from preferences. Fixes #2344.
|
||||
* Snapshoting project without timeout but with button. Ref. #2314
|
||||
* Improve validation for idle-pc.
|
||||
* Activate faulthandler.
|
||||
* Add PATH to OS X console commands
|
||||
* Use raw triple quotes in large console settings This eliminates one level of quoting
|
||||
* Fix issue in node summary when console is not supported by a node.
|
||||
* Remove unused symbols. Fixes #2320.
|
||||
* Show console information in Topology Summary Dock. Fixes #2258.
|
||||
* New option: require KVM. If false, Qemu VMs will not be prevented to run without KVM.
|
||||
* Implement variable replacement for Qemu VM options.
|
||||
* Show on what server a node is installed in the servers summary pane. Fixes #2279.
|
||||
* Add more info when cannot remove capture file after stopping packet capture in a remote project. Ref #1223.
|
||||
* Do not overwrites the disk images when copied to default directory. Fixes #2326.
|
||||
* Only replace quoted telnet for macOS Telnet commands. Ref #2328.
|
||||
* Support Telnet path containing spaces. Ref #2328.
|
||||
* Fix problem when embedded telnet client path contains a space on macOS. Ref #2328.
|
||||
* Do not launch console for builtin nodes when using the "Console to all nodes" button. Fixes #2309.
|
||||
* Update frame_relay_switch_configuration_page_ui.py
|
||||
* Turn off timeout for node creation
|
||||
|
||||
## 2.1.0 09/11/2017
|
||||
|
||||
* Update dynamips binary on OSX
|
||||
|
||||
## 2.1.0rc4 07/11/2017
|
||||
|
||||
* Accurate upload progress dialogs for large files
|
||||
* Disable direct file upload on default
|
||||
* Add registry version 5
|
||||
* Direct file upload enabled on default
|
||||
* Progress Dialog: don't count finished queries done in background
|
||||
* Add debug messages to file upload
|
||||
* Image Upload Manager for uploading
|
||||
* Fix race condition on NodesDockWidget, fixes: #2304
|
||||
* Do not write an error message when importing non existing config from a directory. Fixes #2296.
|
||||
* Fix bug when replacing Telnet path on OSX. Ref #2274.
|
||||
* Back to development on 2.1.0rc3
|
||||
|
||||
## 2.1.0rc3 19/10/2017
|
||||
|
||||
* Add debug when using Telnet path on OSX. Ref #2274.
|
||||
* Force to use the telnet client embedded in DMG. Ref #2274.
|
||||
* Upload directly to compute - experimental feature
|
||||
* Filter additional QXcbConnection log messages
|
||||
* Do not add missing file extension for screenshot file names on Mac. Fixes #2287.
|
||||
* Log Qt messages as info instead of error. Ref #2281.
|
||||
|
||||
## 2.1.0rc2 04/10/2017
|
||||
|
||||
* Only show "can't get settings from controller" message in debug mode.
|
||||
* Remove explicit Telnet path on OS X. Ref #2274
|
||||
* Disable WebSocket notification for lower PyQT version than 5.6. Fixes #2272
|
||||
* Increase timeout to 5 minutes when creating and restoring a snapshot.
|
||||
* Add more information when a request timeouts. Ref #2277.
|
||||
* Do not show the progress dialog when moving a node. Ref #2275.
|
||||
* Increase timer before showing a progress dialog from 250ms to 500ms. Ref #2275.
|
||||
* Use embedded Telnet client on OS X. Ref #2274.
|
||||
* Fix small bug when adding an appliance template and the name already exists.
|
||||
* Use RAW sockets by default on Linux for VMware VM connections.
|
||||
* Increase timeout to get compute servers from controller. Ref #2269.
|
||||
* Fix "Node doesn't exist" after deletion, but still on the canvas. Fixes #2266.
|
||||
* Make sure the warning button icon appears in cloud properties dialog on Windows. Fixes #2245.
|
||||
* Fix bug when cancelling the importation of a configuration file. Fixes #2260.
|
||||
|
||||
## 2.1.0rc1 13/09/2017
|
||||
|
||||
* Fix missing spice console option in appliance template schema. Fixes #2255.
|
||||
|
||||
## 2.1.0b2 05/09/2017
|
||||
|
||||
* Fix resources dependencies for cloud configuration page (Fixes: #2251)
|
||||
* Disabled possibility of moving items under zero layer (Fixes #2220)
|
||||
* dialog-warning.svg fallback for themed icon (Ref. #2245)
|
||||
* Change width of packet filters dialog (Fixes #2244)
|
||||
* Fix high CPU usage when using packet filters. Fixes #2240.
|
||||
* Toggle Node menu item (Fixes #2227)
|
||||
* Fixes multiselection styles change crash on LineItem (#2216)
|
||||
* Fixes loading symbols for QEMU at Edit Page (#2214)
|
||||
* Fixes exception when right click on Dynamips router in the device dock (#2211)
|
||||
* Update frame_relay_switch_configuration_page.ui
|
||||
|
||||
## 2.1.0b1 04/08/2017
|
||||
|
||||
* Info added to the Nat node
|
||||
* Add missing popup information in cloud and docker node
|
||||
* Handle invalid json in websockets
|
||||
* Avoid invalid bad request error when receiving partial answer
|
||||
* Catch parse error for broken SVG
|
||||
* Filter QXcbConnection log messages
|
||||
* Catch class 'PyQt5.QtNetwork.QNetworkReply'> returned a result with an error set
|
||||
* Fix KeyError: 'overlay_notifications'
|
||||
|
||||
## 2.1.0a2 31/07/2017
|
||||
|
||||
* Fix permission error when importing a project on a remote server
|
||||
* Fix RecursionError
|
||||
* Fix 'NodesDockWidget' object has no attribute 'loadPath'
|
||||
* Fix 'MainWindow' object has no attribute '_settings
|
||||
* Fix object has no attribute 'warning_signal'
|
||||
* Fix timeout issues when using an appliance
|
||||
* Make sure ubridge path is not a directory
|
||||
|
||||
## 2.1.0a1 24/07/2017
|
||||
|
||||
* Packet filtering
|
||||
* Suspend a link
|
||||
* Duplicate a node
|
||||
* Move config to central server
|
||||
* Appliance templates on server
|
||||
|
||||
## 2.0.3 13/06/2017
|
||||
|
||||
* Display error when we can't export files
|
||||
* Fix auth header not sent is some conditions
|
||||
* If we have auth issue at server startup continue to get better error
|
||||
* Do not override IOU configuration file when you change the image
|
||||
* Fix some PNG loading issues on Windows
|
||||
* Handle label with missing elements
|
||||
* Support floating value for font size
|
||||
* Handle partial json in a response
|
||||
* Add Dominik as a new team member
|
||||
|
||||
## 2.0.2 30/05/2017
|
||||
|
||||
* Show a default symbol in case of corrupted file
|
||||
* When another gui is already running exit instead of proper close to avoid any issue
|
||||
* Fix duplicate on remote server use wrong location
|
||||
* Display the location of settings when we disallow opening due to old release
|
||||
* Improve search for dynamips in development on OSX
|
||||
* Fix error display when loading a .png custom symbol
|
||||
* Fix a crash in the progress dialog
|
||||
* Fix a race condition when exporting a closed project
|
||||
* Fix RuntimeError: wrapped C/C++ object of type NodeItem has been deleted
|
||||
|
||||
## 2.0.1 16/05/2017
|
||||
|
||||
* Improve inline help. Fixes #1999. Add a warning about wifi interfaces in the cloud. Fixes #1902.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Run tests inside a container
|
||||
FROM ubuntu:vivid
|
||||
FROM ubuntu:18.04
|
||||
|
||||
MAINTAINER GNS3 Team
|
||||
|
||||
#ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --force-yes python3.4 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3.4-dev xvfb
|
||||
RUN apt-get install -y --force-yes python3.6 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3.6-dev xvfb
|
||||
RUN apt-get clean
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@ ADD . /src
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
CMD xvfb-run python3.4 -m pytest -vv
|
||||
CMD xvfb-run python3.6 -m pytest -vv
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
include README.rst
|
||||
include AUTHORS
|
||||
include INSTALL
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
|
||||
@@ -13,7 +13,7 @@ GNS3 GUI repository.
|
||||
Installation
|
||||
------------
|
||||
|
||||
https://gns3.com/support/docs
|
||||
Please see https://docs.gns3.com/
|
||||
|
||||
Development
|
||||
-------------
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version: '{build}-{branch}'
|
||||
|
||||
image: Visual Studio 2015
|
||||
image: Visual Studio 2017
|
||||
|
||||
platform: x64
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-rrequirements.txt
|
||||
|
||||
pep8==1.7.0
|
||||
pytest==3.0.7
|
||||
pytest-pythonpath==0.7.1 # useful for running tests outside tox
|
||||
pytest-timeout==1.2.0
|
||||
pytest==4.4.1
|
||||
pytest-pythonpath==0.7.3 # useful for running tests outside tox
|
||||
pytest-timeout==1.3.3
|
||||
|
||||
93
gns3/appliance_manager.py
Normal file
93
gns3/appliance_manager.py
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/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 .local_config import LocalConfig
|
||||
from .settings import GENERAL_SETTINGS
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApplianceManager(QtCore.QObject):
|
||||
"""
|
||||
Manager for appliances.
|
||||
"""
|
||||
|
||||
appliances_changed_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self._appliances = []
|
||||
self._controller = Controller.instance()
|
||||
self._controller.connected_signal.connect(self.refresh)
|
||||
self._controller.disconnected_signal.connect(self._controllerDisconnectedSlot)
|
||||
|
||||
def refresh(self, update=False):
|
||||
"""
|
||||
Gets the appliances from the controller.
|
||||
"""
|
||||
|
||||
if self._controller.connected():
|
||||
settings = LocalConfig.instance().loadSectionSettings("MainWindow", GENERAL_SETTINGS)
|
||||
symbol_theme = settings["symbol_theme"]
|
||||
if update is True:
|
||||
self._controller.get("/appliances?update=yes&symbol_theme={}".format(symbol_theme), self._listAppliancesCallback, progressText="Downloading appliances from online registry...")
|
||||
else:
|
||||
self._controller.get("/appliances?symbol_theme={}".format(symbol_theme), self._listAppliancesCallback)
|
||||
|
||||
def _controllerDisconnectedSlot(self):
|
||||
"""
|
||||
Called when the controller has been disconnected.
|
||||
"""
|
||||
|
||||
self._appliances = []
|
||||
self.appliances_changed_signal.emit()
|
||||
|
||||
def appliances(self):
|
||||
"""
|
||||
Returns the appliances.
|
||||
|
||||
:returns: array of appliances
|
||||
"""
|
||||
|
||||
return self._appliances
|
||||
|
||||
def _listAppliancesCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback to get the appliances.
|
||||
"""
|
||||
|
||||
if error is True:
|
||||
log.error("Error while getting appliances list: {}".format(result.get("message", "unknown")))
|
||||
return
|
||||
self._appliances = result
|
||||
self.appliances_changed_signal.emit()
|
||||
|
||||
@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
|
||||
@@ -22,13 +22,11 @@ Base class for node classes.
|
||||
from .qt import QtCore
|
||||
from .ports.port import Port
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseNode(QtCore.QObject):
|
||||
|
||||
"""
|
||||
BaseNode implementation.
|
||||
|
||||
@@ -49,7 +47,6 @@ class BaseNode(QtCore.QObject):
|
||||
server_error_signal = QtCore.Signal(int, str)
|
||||
|
||||
_instance_count = 1
|
||||
_allocated_names = set()
|
||||
|
||||
# node statuses
|
||||
stopped = 0
|
||||
@@ -57,14 +54,15 @@ class BaseNode(QtCore.QObject):
|
||||
suspended = 2
|
||||
|
||||
# node categories
|
||||
routers = 0
|
||||
switches = 1
|
||||
end_devices = 2
|
||||
security_devices = 3
|
||||
routers = "router"
|
||||
switches = "switch"
|
||||
end_devices = "guest"
|
||||
security_devices = "firewall"
|
||||
|
||||
def __init__(self, module, compute, project):
|
||||
|
||||
super().__init__()
|
||||
|
||||
# create an unique ID
|
||||
self._id = BaseNode._instance_count
|
||||
BaseNode._instance_count += 1
|
||||
@@ -81,19 +79,48 @@ class BaseNode(QtCore.QObject):
|
||||
|
||||
def links(self):
|
||||
"""
|
||||
Links connected to the node
|
||||
Links connected to this node
|
||||
"""
|
||||
|
||||
return self._links
|
||||
|
||||
def addLink(self, link):
|
||||
"""
|
||||
Add a link connected to this node
|
||||
|
||||
:param link: link object
|
||||
"""
|
||||
|
||||
self._links.add(link)
|
||||
|
||||
def deleteLink(self, link):
|
||||
"""
|
||||
Delete a link connected to this node
|
||||
|
||||
:param link: link object
|
||||
"""
|
||||
|
||||
try:
|
||||
self._links.remove(link)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def state(self):
|
||||
"""
|
||||
Returns a human readable status of this node.
|
||||
|
||||
:returns: string
|
||||
"""
|
||||
|
||||
status = self.status()
|
||||
if status == self.started:
|
||||
return "started"
|
||||
elif status == self.stopped:
|
||||
return "stopped"
|
||||
elif status == self.suspended:
|
||||
return "suspended"
|
||||
return "unknown"
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
"""
|
||||
@@ -176,19 +203,16 @@ class BaseNode(QtCore.QObject):
|
||||
# set ports as started
|
||||
port.setStatus(Port.started)
|
||||
self.started_signal.emit()
|
||||
log.info("{} has started".format(self.name()))
|
||||
elif status == self.stopped:
|
||||
for port in self._ports:
|
||||
# set ports as stopped
|
||||
port.setStatus(Port.stopped)
|
||||
self.stopped_signal.emit()
|
||||
log.info("{} has stopped".format(self.name()))
|
||||
elif status == self.suspended:
|
||||
for port in self._ports:
|
||||
# set ports as suspended
|
||||
port.setStatus(Port.suspended)
|
||||
self.suspended_signal.emit()
|
||||
log.info("{} has suspended".format(self.name()))
|
||||
|
||||
def initialized(self):
|
||||
"""
|
||||
@@ -227,59 +251,6 @@ class BaseNode(QtCore.QObject):
|
||||
|
||||
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
|
||||
@@ -326,3 +297,46 @@ class BaseNode(QtCore.QObject):
|
||||
"""
|
||||
|
||||
self._project.delete(path, callback, context=context, **kwargs)
|
||||
|
||||
@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 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()
|
||||
203
gns3/compute.py
203
gns3/compute.py
@@ -20,10 +20,11 @@ import uuid
|
||||
|
||||
class Compute:
|
||||
"""
|
||||
A compute node on the remote server
|
||||
An instance of a compute.
|
||||
"""
|
||||
|
||||
def __init__(self, compute_id=None):
|
||||
|
||||
if compute_id is None:
|
||||
compute_id = str(uuid.uuid4())
|
||||
self._compute_id = compute_id
|
||||
@@ -36,85 +37,211 @@ class Compute:
|
||||
self._password = None
|
||||
self._cpu_usage_percent = None
|
||||
self._memory_usage_percent = None
|
||||
self._capabilities = {
|
||||
"node_types": []
|
||||
}
|
||||
self._capabilities = {"node_types": []}
|
||||
self._last_error = None
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
Returns the compute ID.
|
||||
|
||||
:returns: compute identifier
|
||||
"""
|
||||
|
||||
return self._compute_id
|
||||
|
||||
def name(self):
|
||||
"""
|
||||
Returns the compute name.
|
||||
|
||||
:returns: compute name
|
||||
"""
|
||||
|
||||
return self._name
|
||||
|
||||
def setName(self, name):
|
||||
"""
|
||||
Sets the compute name.
|
||||
|
||||
:param name: compute name
|
||||
"""
|
||||
|
||||
self._name = name
|
||||
|
||||
def connected(self):
|
||||
"""
|
||||
Returns whether or not there is a connection to the compute.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._connected
|
||||
|
||||
def setConnected(self, value):
|
||||
"""
|
||||
Sets whether or not there is a connection to the compute.
|
||||
|
||||
:param value: boolean
|
||||
"""
|
||||
|
||||
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):
|
||||
"""
|
||||
Returns the compute host.
|
||||
|
||||
:returns: host (string)
|
||||
"""
|
||||
|
||||
return self._host
|
||||
|
||||
def setHost(self, host):
|
||||
"""
|
||||
Sets the compute host.
|
||||
|
||||
:param host: host (string)
|
||||
"""
|
||||
|
||||
self._host = host
|
||||
|
||||
def setCpuUsagePercent(self, usage):
|
||||
self._cpu_usage_percent = usage
|
||||
def port(self):
|
||||
"""
|
||||
Returns the compute port number.
|
||||
|
||||
:returns: port number (integer)
|
||||
"""
|
||||
|
||||
return self._port
|
||||
|
||||
def setPort(self, port):
|
||||
"""
|
||||
Sets the compute port number.
|
||||
|
||||
:param port: port number (integer)
|
||||
"""
|
||||
|
||||
self._port = port
|
||||
|
||||
def user(self):
|
||||
"""
|
||||
Returns the compute user for HTTP authentication.
|
||||
|
||||
:returns: user (string)
|
||||
"""
|
||||
|
||||
return self._user
|
||||
|
||||
def setUser(self, user):
|
||||
"""
|
||||
Sets the compute user for HTTP authentication.
|
||||
|
||||
:param user: user (string)
|
||||
"""
|
||||
|
||||
self._user = user
|
||||
|
||||
def setPassword(self, password):
|
||||
"""
|
||||
Returns the compute password for HTTP authentication.
|
||||
|
||||
:returns: password (string)
|
||||
"""
|
||||
|
||||
self._password = password
|
||||
|
||||
def protocol(self):
|
||||
"""
|
||||
Returns the compute protocol.
|
||||
|
||||
:returns: protocol (string)
|
||||
"""
|
||||
|
||||
return self._protocol
|
||||
|
||||
def setProtocol(self, protocol):
|
||||
"""
|
||||
Sets the compute protocol.
|
||||
|
||||
:param protocol: protocol (string)
|
||||
"""
|
||||
|
||||
self._protocol = protocol
|
||||
|
||||
def cpuUsagePercent(self):
|
||||
"""
|
||||
Returns the compute CPU usage.
|
||||
|
||||
:returns: CPU usage (integer)
|
||||
"""
|
||||
|
||||
return self._cpu_usage_percent
|
||||
|
||||
def setCpuUsagePercent(self, usage):
|
||||
"""
|
||||
Sets the compute CPU usage.
|
||||
|
||||
:param usage: CPU usage (integer)
|
||||
"""
|
||||
|
||||
self._cpu_usage_percent = usage
|
||||
|
||||
def setMemoryUsagePercent(self, usage):
|
||||
"""
|
||||
Returns the compute memory usage.
|
||||
|
||||
:returns: memory usage (integer)
|
||||
"""
|
||||
|
||||
self._memory_usage_percent = usage
|
||||
|
||||
def memoryUsagePercent(self):
|
||||
"""
|
||||
Sets the compute memory usage.
|
||||
|
||||
:param usage: memory usage (integer)
|
||||
"""
|
||||
|
||||
return self._memory_usage_percent
|
||||
|
||||
def capabilities(self):
|
||||
"""
|
||||
Returns the compute capabilities
|
||||
|
||||
:returns: capabilities (dictionary)
|
||||
"""
|
||||
|
||||
return self._capabilities
|
||||
|
||||
def setCapabilities(self, val):
|
||||
self._capabilities = val
|
||||
def setCapabilities(self, value):
|
||||
"""
|
||||
Sets the compute capabilities
|
||||
|
||||
:param value: capabilities (dictionary)
|
||||
"""
|
||||
|
||||
self._capabilities = value
|
||||
|
||||
def setLastError(self, last_error):
|
||||
self._last_error = last_error
|
||||
|
||||
def lastError(self):
|
||||
return self._last_error
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .qt import QtCore
|
||||
|
||||
from .compute import Compute
|
||||
from .controller import Controller
|
||||
|
||||
@@ -31,11 +30,16 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComputeManager(QtCore.QObject):
|
||||
"""
|
||||
Manager for computes.
|
||||
"""
|
||||
|
||||
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()
|
||||
@@ -43,7 +47,7 @@ class ComputeManager(QtCore.QObject):
|
||||
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
|
||||
# No need to refresh via an API call if we received fresh data from the notification feed
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
|
||||
self._timer = QtCore.QTimer()
|
||||
@@ -53,24 +57,40 @@ class ComputeManager(QtCore.QObject):
|
||||
self._timer.start()
|
||||
|
||||
def _refreshComputesSlot(self):
|
||||
"""
|
||||
Called when computes are refreshed.
|
||||
"""
|
||||
|
||||
if self._refreshingComputes:
|
||||
return
|
||||
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 5:
|
||||
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=15)
|
||||
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
|
||||
|
||||
def _controllerConnectedSlot(self):
|
||||
"""
|
||||
Called when connected to a compute.
|
||||
"""
|
||||
|
||||
if self._controller.connected():
|
||||
self._refreshingComputes = True
|
||||
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=15)
|
||||
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
|
||||
|
||||
def _controllerDisconnectedSlot(self):
|
||||
"""
|
||||
Called when disconnected from a compute.
|
||||
"""
|
||||
|
||||
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):
|
||||
"""
|
||||
Callback to list computes.
|
||||
"""
|
||||
|
||||
self._refreshingComputes = False
|
||||
if error is True:
|
||||
log.error("Error while getting compute list: {}".format(result["message"]))
|
||||
@@ -81,9 +101,9 @@ class ComputeManager(QtCore.QObject):
|
||||
|
||||
def computeDataReceivedCallback(self, compute):
|
||||
"""
|
||||
Called when we received data from a compute
|
||||
node.
|
||||
Called when we received data from a compute node.
|
||||
"""
|
||||
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
|
||||
new_node = False
|
||||
@@ -101,6 +121,7 @@ class ComputeManager(QtCore.QObject):
|
||||
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"])
|
||||
self._computes[compute_id].setLastError(compute.get("last_error"))
|
||||
|
||||
if new_node:
|
||||
self.created_signal.emit(compute_id)
|
||||
@@ -109,8 +130,9 @@ class ComputeManager(QtCore.QObject):
|
||||
|
||||
def computeIsTheRemoteGNS3VM(self, compute):
|
||||
"""
|
||||
:returns: Boolean True if the remote server is the remote GNS3 VM
|
||||
: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
|
||||
@@ -120,6 +142,7 @@ class ComputeManager(QtCore.QObject):
|
||||
"""
|
||||
:returns: List of computes nodes
|
||||
"""
|
||||
|
||||
computes = []
|
||||
for compute in self._computes.values():
|
||||
# We filter the remote GNS3 VM compute from the computes list
|
||||
@@ -131,6 +154,7 @@ class ComputeManager(QtCore.QObject):
|
||||
"""
|
||||
:returns: The GNS3 VM compute node or None
|
||||
"""
|
||||
|
||||
try:
|
||||
return self._computes["vm"]
|
||||
except KeyError:
|
||||
@@ -140,6 +164,7 @@ class ComputeManager(QtCore.QObject):
|
||||
"""
|
||||
:returns: The local compute node or None
|
||||
"""
|
||||
|
||||
try:
|
||||
return self._computes["local"]
|
||||
except KeyError:
|
||||
@@ -151,6 +176,7 @@ class ComputeManager(QtCore.QObject):
|
||||
|
||||
With a remote controller it could be different of our local platform
|
||||
"""
|
||||
|
||||
c = self.localCompute()
|
||||
if c is None:
|
||||
return sys.platform
|
||||
@@ -160,31 +186,45 @@ class ComputeManager(QtCore.QObject):
|
||||
"""
|
||||
: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):
|
||||
"""
|
||||
Gets a compute by ID
|
||||
|
||||
:param compute_id: compute identifier
|
||||
:returns: compute
|
||||
"""
|
||||
|
||||
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))
|
||||
raise KeyError("Compute ID {} 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):
|
||||
"""
|
||||
Deletes a compute by ID
|
||||
|
||||
:param compute_id: compute identifier
|
||||
"""
|
||||
|
||||
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)
|
||||
self._controller.delete("/computes/{compute_id}".format(compute_id=compute_id), None)
|
||||
self.deleted_signal.emit(compute_id)
|
||||
|
||||
def updateList(self, computes):
|
||||
"""
|
||||
Sync an array of compute server with remote
|
||||
Sync an array of compute 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"]:
|
||||
|
||||
@@ -19,17 +19,16 @@
|
||||
Compute summary view that list all the compute, their status.
|
||||
"""
|
||||
|
||||
import sip
|
||||
|
||||
from .qt import QtGui, QtCore, QtWidgets
|
||||
from .compute_manager import ComputeManager
|
||||
from .topology import Topology
|
||||
from .node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComputeItem(QtWidgets.QTreeWidgetItem):
|
||||
|
||||
"""
|
||||
Custom item for the QTreeWidget instance
|
||||
(topology summary view).
|
||||
@@ -62,25 +61,47 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
|
||||
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"
|
||||
self.setToolTip(0, "Server {} version {} running on {}".format(self._compute.name(),
|
||||
self._compute.capabilities().get("version", "n/a"),
|
||||
self._compute.capabilities().get("platform", "")))
|
||||
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":
|
||||
last_error = self._compute.lastError()
|
||||
if last_error:
|
||||
self.setToolTip(0, "Failed to connect to {}: {}".format(self._compute.name(), last_error))
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
|
||||
elif self._status == "unknown":
|
||||
self.setToolTip(0, "Discovering or connecting to {}...".format(self._compute.name()))
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_gray.svg'))
|
||||
else:
|
||||
self._status = "stopped"
|
||||
self.setToolTip(0, "{} is stopped or cannot be reached".format(self._compute.name()))
|
||||
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.
|
||||
|
||||
@@ -90,9 +111,7 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
|
||||
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)
|
||||
|
||||
@@ -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,15 +21,16 @@ Handles commands typed in the GNS3 console.
|
||||
|
||||
import sys
|
||||
import cmd
|
||||
import logging
|
||||
import struct
|
||||
import sip
|
||||
import json
|
||||
from .qt import sip
|
||||
|
||||
from .node import Node
|
||||
from .qt import QtCore
|
||||
from .version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
@@ -177,6 +178,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.
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import sip
|
||||
from .qt import sip
|
||||
import struct
|
||||
import inspect
|
||||
import datetime
|
||||
import platform
|
||||
|
||||
from .qt import QtCore, Qt
|
||||
from .qt import QtCore
|
||||
from .topology import Topology
|
||||
from .version import __version__
|
||||
from .console_cmd import ConsoleCmd
|
||||
@@ -75,7 +75,7 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
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)
|
||||
"".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, QtCore.PYQT_VERSION_STR, current_year)
|
||||
|
||||
# Parent class initialization
|
||||
try:
|
||||
@@ -110,6 +110,9 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
self._topology = Topology.instance()
|
||||
|
||||
def _writeMessageSlot(self, message, level):
|
||||
"""
|
||||
Write a message in the console.
|
||||
"""
|
||||
if level == "error":
|
||||
self.write(message, error=True)
|
||||
elif level == "warning":
|
||||
|
||||
@@ -18,11 +18,14 @@
|
||||
import os
|
||||
import hashlib
|
||||
import tempfile
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
from .qt import QtCore, QtGui, QtWidgets, qpartial, qslot
|
||||
from .qt import QtCore, QtNetwork, QtGui, QtWidgets, QtWebSockets, qpartial, qslot
|
||||
from .symbol import Symbol
|
||||
from .local_server_config import LocalServerConfig
|
||||
from .settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.utils import parse_version
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -30,35 +33,44 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class Controller(QtCore.QObject):
|
||||
"""
|
||||
An instance of the GNS3 server controller
|
||||
An instance of the 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):
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self._connected = False
|
||||
self._connecting = False
|
||||
self._cache_directory = tempfile.mkdtemp()
|
||||
self._notification_stream = None
|
||||
self._version = None
|
||||
self._cache_directory = tempfile.TemporaryDirectory(suffix="-gns3")
|
||||
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 = []
|
||||
self._websocket = QtWebSockets.QWebSocket()
|
||||
|
||||
# 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 version(self):
|
||||
return self._version
|
||||
|
||||
def isRemote(self):
|
||||
"""
|
||||
:returns Boolean: True if the controller is remote
|
||||
"""
|
||||
|
||||
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
|
||||
return not settings["auto_start"]
|
||||
|
||||
@@ -66,24 +78,28 @@ class Controller(QtCore.QObject):
|
||||
"""
|
||||
: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
|
||||
:returns: HTTP client to connect 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():
|
||||
@@ -92,10 +108,18 @@ class Controller(QtCore.QObject):
|
||||
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
|
||||
|
||||
@@ -103,6 +127,7 @@ class Controller(QtCore.QObject):
|
||||
"""
|
||||
Connection process as started
|
||||
"""
|
||||
|
||||
self._connected = False
|
||||
self._connecting = True
|
||||
self.get('/version', self._versionGetSlot)
|
||||
@@ -112,11 +137,13 @@ class Controller(QtCore.QObject):
|
||||
self._connected = False
|
||||
self.disconnected_signal.emit()
|
||||
self._connectingToServer()
|
||||
self.stopListenNotifications()
|
||||
|
||||
def _versionGetSlot(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Called after the inital version get
|
||||
Called after the initial version get
|
||||
"""
|
||||
|
||||
if error:
|
||||
if self._first_error:
|
||||
self._connecting = False
|
||||
@@ -128,7 +155,7 @@ class Controller(QtCore.QObject):
|
||||
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
|
||||
# Try to connect again in 5 seconds
|
||||
QtCore.QTimer.singleShot(5000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_error))
|
||||
self._first_error = False
|
||||
else:
|
||||
@@ -136,32 +163,43 @@ class Controller(QtCore.QObject):
|
||||
if self._error_dialog:
|
||||
self._error_dialog.reject()
|
||||
self._error_dialog = None
|
||||
self._version = result.get("version")
|
||||
|
||||
def _httpClientConnectedSlot(self):
|
||||
|
||||
if not self._connected:
|
||||
self._connected = True
|
||||
self._connecting = False
|
||||
self.connected_signal.emit()
|
||||
self.refreshProjectList()
|
||||
self._startListenNotifications()
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("POST", *args, **kwargs)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("GET", *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 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)
|
||||
@@ -170,9 +208,10 @@ class Controller(QtCore.QObject):
|
||||
"""
|
||||
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
|
||||
when all the templates will be managed on server
|
||||
"""
|
||||
|
||||
#FIXME: remove this?
|
||||
if compute_id.startswith("http:") or compute_id.startswith("https:"):
|
||||
from .compute_manager import ComputeManager
|
||||
try:
|
||||
@@ -181,22 +220,32 @@ class Controller(QtCore.QObject):
|
||||
return compute_id
|
||||
return compute_id
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("PUT", *args, **kwargs)
|
||||
def getEndpoint(self, path, compute_id, *args, **kwargs):
|
||||
"""
|
||||
API post on a specific compute
|
||||
"""
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("DELETE", *args, **kwargs)
|
||||
compute_id = self.__fix_compute_id(compute_id)
|
||||
path = "/computes/endpoint/{}{}".format(compute_id, path)
|
||||
return self.get(path, *args, **kwargs)
|
||||
|
||||
def putCompute(self, path, compute_id, *args, **kwargs):
|
||||
"""
|
||||
API put on a specific compute
|
||||
"""
|
||||
|
||||
compute_id = self.__fix_compute_id(compute_id)
|
||||
path = "/computes/{}{}".format(compute_id, path)
|
||||
return self.put(path, *args, **kwargs)
|
||||
|
||||
def createHTTPQuery(self, method, path, *args, **kwargs):
|
||||
"""
|
||||
Forward the query to the HTTP client or controller depending of the path
|
||||
"""
|
||||
|
||||
if self._http_client:
|
||||
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
|
||||
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
return self._http_client.getSynchronous(endpoint, timeout)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
@@ -208,37 +257,41 @@ class Controller(QtCore.QObject):
|
||||
Controller._instance = Controller()
|
||||
return Controller._instance
|
||||
|
||||
def getStatic(self, url, callback):
|
||||
def getStatic(self, url, callback, fallback=None):
|
||||
"""
|
||||
Get a URL from the /static on controller and cache it on disk
|
||||
|
||||
:param url: URL without the protocol and host part
|
||||
:param callback: Callback to call when file is ready
|
||||
:param fallback: Fallback url in case of error
|
||||
"""
|
||||
|
||||
if not self._http_client:
|
||||
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)
|
||||
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)
|
||||
self._static_asset_download_queue[path].append((callback, fallback, ))
|
||||
else:
|
||||
self._static_asset_download_queue[path] = [callback]
|
||||
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:
|
||||
log.error("Error while downloading file: {}".format(url))
|
||||
if path in self._static_asset_download_queue:
|
||||
del self._static_asset_download_queue[path]
|
||||
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:
|
||||
@@ -247,24 +300,82 @@ class Controller(QtCore.QObject):
|
||||
log.error("Can't write to {}: {}".format(path, str(e)))
|
||||
return
|
||||
log.debug("File stored {} for {}".format(path, url))
|
||||
for callback in self._static_asset_download_queue[path]:
|
||||
for callback, fallback in self._static_asset_download_queue[path]:
|
||||
callback(path)
|
||||
del self._static_asset_download_queue[path]
|
||||
|
||||
def getSymbolIcon(self, symbol_id, callback):
|
||||
def getStaticCachedPath(self, url):
|
||||
"""
|
||||
Returns static cached (hashed) path
|
||||
|
||||
:param url:
|
||||
"""
|
||||
m = hashlib.md5()
|
||||
m.update(url.encode())
|
||||
if ".svg" in url:
|
||||
extension = ".svg"
|
||||
else:
|
||||
extension = ".png"
|
||||
path = os.path.join(self._cache_directory.name, m.hexdigest() + extension)
|
||||
return path
|
||||
|
||||
def clearStaticCache(self):
|
||||
"""
|
||||
Clear the cache directory.
|
||||
"""
|
||||
|
||||
for filename in os.listdir(self._cache_directory.name):
|
||||
if filename.endswith(".svg") or filename.endswith(".png"):
|
||||
try:
|
||||
os.remove(os.path.join(self._cache_directory.name, filename))
|
||||
except OSError as e:
|
||||
log.debug("Error deleting cached symbol '{}':{}".format(filename, e))
|
||||
continue
|
||||
|
||||
def getSymbolIcon(self, symbol_id, callback, fallback=None):
|
||||
"""
|
||||
Get a QIcon for a symbol from the controller
|
||||
|
||||
:param url: URL without the protocol and host part
|
||||
:param symbol_id: Symbol id
|
||||
:param callback: Callback to call when file is ready
|
||||
:param fallback: Fallback symbol if not found
|
||||
"""
|
||||
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback))
|
||||
if symbol_id is None:
|
||||
self.getStatic(Symbol(fallback).url(), qpartial(self._getIconCallback, callback))
|
||||
else:
|
||||
if fallback:
|
||||
fallback = Symbol(fallback).url()
|
||||
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback), fallback=fallback)
|
||||
|
||||
def _getIconCallback(self, callback, path):
|
||||
|
||||
pixmap = QtGui.QPixmap(path)
|
||||
if pixmap.isNull():
|
||||
log.debug("Invalid symbol {}".format(path))
|
||||
path = ":/icons/cancel.svg"
|
||||
icon = QtGui.QIcon()
|
||||
icon.addFile(path)
|
||||
callback(icon)
|
||||
|
||||
def uploadSymbol(self, symbol_id, path):
|
||||
|
||||
self.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, result.get("message", "unknown")))
|
||||
return
|
||||
|
||||
# Refresh the templates list
|
||||
from .template_manager import TemplateManager
|
||||
TemplateManager.instance().templates_changed_signal.emit()
|
||||
|
||||
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))
|
||||
|
||||
@@ -290,3 +401,78 @@ class Controller(QtCore.QObject):
|
||||
|
||||
def projects(self):
|
||||
return self._projects
|
||||
|
||||
def _startListenNotifications(self):
|
||||
if not self.connected():
|
||||
return
|
||||
|
||||
# Due to bug in Qt on some version we need a dedicated network manager
|
||||
self._notification_network_manager = QtNetwork.QNetworkAccessManager()
|
||||
self._notification_stream = None
|
||||
|
||||
# Qt websocket before Qt 5.6 doesn't support auth
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.6.0") or parse_version(QtCore.PYQT_VERSION_STR) < parse_version("5.6.0"):
|
||||
self._notification_stream = Controller.instance().createHTTPQuery("GET", "/notifications", self._endListenNotificationCallback,
|
||||
downloadProgressCallback=self._event_received,
|
||||
networkManager=self._notification_network_manager,
|
||||
timeout=None,
|
||||
showProgress=False,
|
||||
ignoreErrors=True)
|
||||
|
||||
else:
|
||||
self._notification_stream = self._http_client.connectWebSocket(self._websocket, "/notifications/ws")
|
||||
self._notification_stream.textMessageReceived.connect(self._websocket_event_received)
|
||||
self._notification_stream.error.connect(self._websocket_error)
|
||||
|
||||
def stopListenNotifications(self):
|
||||
if self._notification_stream:
|
||||
log.debug("Stop listening for notifications from controller")
|
||||
stream = self._notification_stream
|
||||
self._notification_stream = None
|
||||
stream.abort()
|
||||
self._notification_network_manager = None
|
||||
|
||||
def _endListenNotificationCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
If notification stream disconnect we reconnect to it
|
||||
"""
|
||||
if self._notification_stream:
|
||||
self._notification_stream = None
|
||||
self._startListenNotifications()
|
||||
|
||||
@qslot
|
||||
def _websocket_error(self, error):
|
||||
if self._notification_stream:
|
||||
log.error("Websocket notification stream error: {}".format(self._notification_stream.errorString()))
|
||||
self._notification_stream = None
|
||||
self._startListenNotifications()
|
||||
|
||||
@qslot
|
||||
def _websocket_event_received(self, event):
|
||||
try:
|
||||
self._event_received(json.loads(event))
|
||||
except ValueError as e:
|
||||
log.error("Invalid event received: {}".format(e))
|
||||
|
||||
def _event_received(self, result, *args, **kwargs):
|
||||
|
||||
# Log only relevant events
|
||||
if result["action"] not in ("ping", "compute.updated"):
|
||||
log.debug("Event received from controller stream: {}".format(result))
|
||||
if result["action"] == "template.created" or result["action"] == "template.updated":
|
||||
from gns3.template_manager import TemplateManager
|
||||
TemplateManager.instance().templateDataReceivedCallback(result["event"])
|
||||
elif result["action"] == "template.deleted":
|
||||
from gns3.template_manager import TemplateManager
|
||||
TemplateManager.instance().deleteTemplateCallback(result["event"])
|
||||
elif result["action"] == "compute.created" or result["action"] == "compute.updated":
|
||||
from .compute_manager import ComputeManager
|
||||
ComputeManager.instance().computeDataReceivedCallback(result["event"])
|
||||
elif result["action"] == "log.error":
|
||||
log.error(result["event"]["message"])
|
||||
elif result["action"] == "log.warning":
|
||||
log.warning(result["event"]["message"])
|
||||
elif result["action"] == "log.info":
|
||||
log.info(result["event"]["message"], extra={"show": True})
|
||||
elif result["action"] == "ping":
|
||||
pass
|
||||
|
||||
@@ -20,6 +20,7 @@ import psutil
|
||||
import os
|
||||
import platform
|
||||
import struct
|
||||
import distro
|
||||
|
||||
try:
|
||||
import raven
|
||||
@@ -41,7 +42,7 @@ if __version_info__[3] != 0:
|
||||
import faulthandler
|
||||
# Display a traceback in case of segfault crash. Usefull when frozen
|
||||
# Not enabled by default for security reason
|
||||
log.info("Enable catching segfault")
|
||||
log.debug("Enable catching segfault")
|
||||
faulthandler.enable()
|
||||
|
||||
|
||||
@@ -51,7 +52,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "sync+https://b9912fe90df3496e98ccfb44d9b9baf0:ac42a40e66314b48883243402b957290@sentry.io/38506"
|
||||
DSN = "https://cef6948387d14919929d7e3db6b11835:064728e23bf043f99100cf271bcd5547@sentry.io/38506"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = get_resource("cacert.pem")
|
||||
if cacert is not None and os.path.isfile(cacert):
|
||||
@@ -70,11 +71,20 @@ class CrashReport:
|
||||
|
||||
def captureException(self, exception, value, tb):
|
||||
from .local_server import LocalServer
|
||||
from .local_config import LocalConfig
|
||||
from .controller import Controller
|
||||
from .compute_manager import ComputeManager
|
||||
|
||||
local_server = LocalServer.instance().localServerSettings()
|
||||
if local_server["report_errors"]:
|
||||
if not RAVEN_AVAILABLE:
|
||||
return
|
||||
|
||||
if os.path.exists(LocalConfig.instance().runAsRootPath()):
|
||||
log.warning("User has run application as root. Crash reports are disabled.")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
if os.path.exists(".git"):
|
||||
log.warning("A .git directory exist crash report is turn off for developers. Instant exit")
|
||||
sys.exit(1)
|
||||
@@ -89,27 +99,41 @@ class CrashReport:
|
||||
"os:release": platform.release(),
|
||||
"os:win_32": " ".join(platform.win32_ver()),
|
||||
"os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]),
|
||||
"os:linux": " ".join(platform.linux_distribution()),
|
||||
"os:linux": " ".join(distro.linux_distribution()),
|
||||
"python:version": "{}.{}.{}".format(sys.version_info[0],
|
||||
sys.version_info[1],
|
||||
sys.version_info[2]),
|
||||
"python:bit": struct.calcsize("P") * 8,
|
||||
"python:encoding": sys.getdefaultencoding(),
|
||||
"python:frozen": "{}".format(hasattr(sys, "frozen"))
|
||||
"python:frozen": "{}".format(hasattr(sys, "frozen")),
|
||||
}
|
||||
|
||||
# extra controller and compute information
|
||||
extra_context = {"controller:version": Controller.instance().version(),
|
||||
"controller:host": Controller.instance().host(),
|
||||
"controller:connected": Controller.instance().connected()}
|
||||
for index, compute in enumerate(ComputeManager.instance().computes()):
|
||||
extra_context["compute{}:id".format(index)] = compute.id()
|
||||
extra_context["compute{}:name".format(index)] = compute.name(),
|
||||
extra_context["compute{}:host".format(index)] = compute.host(),
|
||||
extra_context["compute{}:connected".format(index)] = compute.connected()
|
||||
extra_context["compute{}:platform".format(index)] = compute.capabilities().get("platform")
|
||||
extra_context["compute{}:version".format(index)] = compute.capabilities().get("version")
|
||||
|
||||
context = self._add_qt_information(context)
|
||||
client.tags_context(context)
|
||||
client.extra_context(extra_context)
|
||||
try:
|
||||
report = client.captureException((exception, value, tb))
|
||||
except Exception as e:
|
||||
log.error("Can't send crash report to Sentry: {}".format(e))
|
||||
return
|
||||
log.info("Crash report sent with event ID: {}".format(client.get_ident(report)))
|
||||
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
|
||||
from .qt import sip
|
||||
except ImportError:
|
||||
return context
|
||||
context["psutil:version"] = psutil.__version__
|
||||
|
||||
@@ -16,14 +16,18 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sip
|
||||
from ..qt import sip
|
||||
import shutil
|
||||
|
||||
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
|
||||
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
|
||||
from ..template_manager import TemplateManager
|
||||
from ..template import Template
|
||||
from ..modules import Qemu
|
||||
from ..registry.appliance import Appliance, ApplianceError
|
||||
from ..registry.registry import Registry
|
||||
from ..registry.config import Config, ConfigException
|
||||
from ..registry.config import Config
|
||||
from ..registry.appliance_to_template import ApplianceToTemplate
|
||||
from ..registry.image import Image
|
||||
from ..utils import human_filesize
|
||||
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
|
||||
@@ -31,103 +35,133 @@ 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
|
||||
from ..image_manager import ImageManager
|
||||
|
||||
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._server_check = False
|
||||
self._template_created = False
|
||||
self._path = path
|
||||
# Count how many images are curently uploading
|
||||
|
||||
# count how many images are being uploaded
|
||||
self._image_uploading_count = 0
|
||||
|
||||
# symbols loaded from controller
|
||||
self._symbols = []
|
||||
|
||||
# connect slots
|
||||
self.images_changed_signal.connect(self._refreshVersions)
|
||||
self.versions_changed_signal.connect(self._versionRefreshedSlot)
|
||||
self.uiRefreshPushButton.clicked.connect(self.images_changed_signal.emit)
|
||||
self.uiDownloadPushButton.clicked.connect(self._downloadPushButtonClickedSlot)
|
||||
self.uiImportPushButton.clicked.connect(self._importPushButtonClickedSlot)
|
||||
self.uiApplianceVersionTreeWidget.currentItemChanged.connect(self._applianceVersionCurrentItemChangedSlot)
|
||||
self.uiCreateVersionPushButton.clicked.connect(self._createVersionPushButtonClickedSlot)
|
||||
self.allowCustomFiles.clicked.connect(self._allowCustomFilesChangedSlot)
|
||||
|
||||
#FIXME: deactivate the create version feature (confusing and maybe not necessary, TBD)
|
||||
self.uiCreateVersionPushButton.hide()
|
||||
|
||||
# directories where to search for images
|
||||
images_directories = list()
|
||||
|
||||
for emulator in ("QEMU", "IOU", "DYNAMIPS"):
|
||||
emulator_images_dir = ImageManager.instance().getDirectoryForType(emulator)
|
||||
if os.path.exists(emulator_images_dir):
|
||||
images_directories.append(emulator_images_dir)
|
||||
|
||||
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)
|
||||
|
||||
# registry to search for images
|
||||
self._registry = Registry(images_directories)
|
||||
self._registry.image_list_changed_signal.connect(self.images_changed_signal.emit)
|
||||
|
||||
# appliance object
|
||||
self._appliance = Appliance(self._registry, self._path)
|
||||
self.setWindowTitle("Install {} appliance".format(self._appliance["name"]))
|
||||
|
||||
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)
|
||||
# add a custom button to show appliance information
|
||||
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Appliance info")
|
||||
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
|
||||
self.customButtonClicked.connect(self._showApplianceInfoSlot)
|
||||
|
||||
# customize the server selection
|
||||
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 the appliance on the main server")
|
||||
self.uiLocalRadioButton.setText("Install the appliance on the main server")
|
||||
else:
|
||||
if not path.endswith('.builtin.gns3a'):
|
||||
destination = None
|
||||
try:
|
||||
destination = Config().appliances_dir
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", "Could not find configuration file: {}".format(e))
|
||||
except ValueError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", "Invalid configuration file: {}".format(e))
|
||||
if destination:
|
||||
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(), "Cannot copy {} to {}".format(path, destination), str(e))
|
||||
|
||||
self.uiServerWizardPage.isComplete = self._uiServerWizardPage_isComplete
|
||||
|
||||
def initializePage(self, page_id):
|
||||
"""
|
||||
Initialize Wizard pages.
|
||||
Initialize wizard pages.
|
||||
|
||||
:param page_id: page identifier
|
||||
"""
|
||||
|
||||
super().initializePage(page_id)
|
||||
|
||||
# add symbol
|
||||
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.uiServerWizardPage:
|
||||
|
||||
if self.page(page_id) == self.uiInfoWizardPage:
|
||||
self.uiInfoWizardPage.setTitle(self._appliance["product_name"])
|
||||
self.uiDescriptionLabel.setText(self._appliance["description"])
|
||||
Controller.instance().getSymbols(self._getSymbolsCallback)
|
||||
|
||||
info = (
|
||||
("Category", "category"),
|
||||
("Product", "product_name"),
|
||||
("Vendor", "vendor_name"),
|
||||
("Status", "status"),
|
||||
("Maintainer", "maintainer"),
|
||||
("Architecture", "qemu/arch"),
|
||||
("KVM", "qemu/kvm")
|
||||
)
|
||||
if "qemu" in self._appliance:
|
||||
emulator_type = "qemu"
|
||||
elif "iou" in self._appliance:
|
||||
emulator_type = "iou"
|
||||
elif "docker" in self._appliance:
|
||||
emulator_type = "docker"
|
||||
elif "dynamips" in self._appliance:
|
||||
emulator_type = "dynamips"
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "Appliance", "Could not determine the emulator type")
|
||||
|
||||
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)
|
||||
is_mac = ComputeManager.instance().localPlatform().startswith("darwin")
|
||||
is_win = ComputeManager.instance().localPlatform().startswith("win")
|
||||
|
||||
elif self.page(page_id) == self.uiServerWizardPage:
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
if len(ComputeManager.instance().remoteComputes()) == 0:
|
||||
self.uiRemoteRadioButton.setEnabled(False)
|
||||
@@ -141,12 +175,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
if ComputeManager.instance().localPlatform() is None:
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
elif (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
|
||||
if type == "qemu":
|
||||
# Qemu has issues on OSX and Windows we disallow usage of the local server
|
||||
elif is_mac or is_win:
|
||||
if emulator_type == "qemu":
|
||||
# disallow usage of the local server because Qemu has issues on OSX and Windows
|
||||
if not LocalConfig.instance().experimental():
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
elif type != "dynamips":
|
||||
elif emulator_type != "dynamips":
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
|
||||
if ComputeManager.instance().vmCompute():
|
||||
@@ -158,74 +192,139 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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, "GNS3 VM", "The GNS3 VM is not available, please configure the GNS3 VM before adding a new appliance.")
|
||||
|
||||
elif self.page(page_id) == self.uiFilesWizardPage:
|
||||
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
|
||||
if Controller.instance().isRemote() or self._compute_id != "local":
|
||||
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
|
||||
else:
|
||||
self.images_changed_signal.emit()
|
||||
|
||||
elif self.page(page_id) == self.uiQemuWizardPage:
|
||||
if self._appliance['qemu'].get('kvm', 'require') == 'require':
|
||||
self._server_check = False
|
||||
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
|
||||
else:
|
||||
self._server_check = True
|
||||
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
|
||||
self.uiUsageTextEdit.setText("The template will be available in the {} category.\n\n{}".format(self._appliance["category"].replace("_", " "), self._appliance.get("usage", "")))
|
||||
|
||||
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
|
||||
"""
|
||||
Check if server support KVM or not
|
||||
Check if the server supports 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)
|
||||
msg = "The selected server does not support KVM. A Linux server or the GNS3 VM running in VMware is required."
|
||||
QtWidgets.QMessageBox.critical(self, "KVM support", 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)
|
||||
def _imageUploadedCallback(self, result, error=False, context=None, **kwargs):
|
||||
if context is None:
|
||||
context = {}
|
||||
image_path = context.get("image_path", "unknown")
|
||||
if error:
|
||||
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
|
||||
else:
|
||||
log.info("Image '{}' has been successfully uploaded".format(image_path))
|
||||
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
|
||||
|
||||
def _showApplianceInfoSlot(self):
|
||||
"""
|
||||
Shows appliance information.
|
||||
"""
|
||||
|
||||
info = (("Product", "product_name"),
|
||||
("Vendor", "vendor_name"),
|
||||
("Availability", "availability"),
|
||||
("Status", "status"),
|
||||
("Maintainer", "maintainer"))
|
||||
|
||||
if "qemu" in self._appliance:
|
||||
qemu_info = (("vCPUs", "qemu/cpus"),
|
||||
("RAM", "qemu/ram"),
|
||||
("Adapters", "qemu/adapters"),
|
||||
("Adapter type", "qemu/adapter_type"),
|
||||
("Console type", "qemu/console_type"),
|
||||
("Architecture", "qemu/arch"),
|
||||
("Console type", "qemu/console_type"),
|
||||
("KVM", "qemu/kvm"))
|
||||
info = info + qemu_info
|
||||
|
||||
elif "docker" in self._appliance:
|
||||
docker_info = (("Image", "docker/image"),
|
||||
("Adapters", "docker/adapters"),
|
||||
("Console type", "docker/console_type"))
|
||||
info = info + docker_info
|
||||
|
||||
elif "iou" in self._appliance:
|
||||
iou_info = (("RAM", "iou/ram"),
|
||||
("NVRAM", "iou/nvram"),
|
||||
("Ethernet adapters", "iou/ethernet_adapters"),
|
||||
("Serial adapters", "iou/serial_adapters"))
|
||||
info = info + iou_info
|
||||
|
||||
elif "dynamips" in self._appliance:
|
||||
dynamips_info = (("Platform", "dynamips/platform"),
|
||||
("Chassis", "dynamips/chassis"),
|
||||
("Midplane", "dynamips/midplane"),
|
||||
("NPE", "dynamips/npe"),
|
||||
("RAM", "dynamips/ram"),
|
||||
("NVRAM", "dynamips/nvram"),
|
||||
("slot0", "dynamips/slot0"),
|
||||
("slot1", "dynamips/slot1"),
|
||||
("slot2", "dynamips/slot2"),
|
||||
("slot3", "dynamips/slot3"),
|
||||
("slot4", "dynamips/slot4"),
|
||||
("slot5", "dynamips/slot5"),
|
||||
("slot6", "dynamips/slot6"),
|
||||
("wic0", "dynamips/wic0"),
|
||||
("wic1", "dynamips/wic1"),
|
||||
("wic2", "dynamips/wic2"))
|
||||
info = info + dynamips_info
|
||||
|
||||
text_info = ""
|
||||
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
|
||||
text_info += "<span style='font-weight:bold;'>{}</span>: {}<br>".format(name, value)
|
||||
|
||||
msgbox = QtWidgets.QMessageBox(self)
|
||||
msgbox.setWindowTitle("Appliance information")
|
||||
msgbox.setStyleSheet("QLabel{min-width: 600px;}") # TODO: resize details box QTextEdit{min-height: 500px;}
|
||||
msgbox.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
msgbox.setText(text_info)
|
||||
msgbox.setDetailedText(self._appliance["description"])
|
||||
msgbox.exec_()
|
||||
|
||||
@qslot
|
||||
def _refreshVersions(self, *args):
|
||||
"""
|
||||
Refresh the list of files for different version of the appliance
|
||||
Refresh the list of files for different versions 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.")
|
||||
|
||||
self.uiFilesWizardPage.setSubTitle("Please select one version of " + self._appliance["product_name"] + " and import the required files. Files are searched in your downloads and GNS3 images directories by default")
|
||||
worker = WaitForLambdaWorker(lambda: self._refreshDialogWorker())
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Scanning directories for files...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
@@ -235,13 +334,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
"""
|
||||
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"])])
|
||||
top = QtWidgets.QTreeWidgetItem(self.uiApplianceVersionTreeWidget, ["{} version {}".format(self._appliance["product_name"], version["name"])])
|
||||
size = 0
|
||||
status = "Ready to install"
|
||||
for image in version["images"].values():
|
||||
@@ -249,19 +349,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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", "")
|
||||
])
|
||||
image_widget = QtWidgets.QTreeWidgetItem([image["filename"],
|
||||
human_filesize(image.get("filesize", 0)),
|
||||
image["status"]])
|
||||
if image["status"] == "Missing":
|
||||
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
|
||||
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("red")))
|
||||
else:
|
||||
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
|
||||
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
|
||||
image_widget.setToolTip(2, image["path"])
|
||||
|
||||
# Associated data stored are col 0: version, col 1: image
|
||||
image_widget.setData(0, QtCore.Qt.UserRole, version)
|
||||
@@ -275,28 +370,38 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
expand = True
|
||||
if status == "Missing files":
|
||||
top.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
|
||||
top.setForeground(2, QtGui.QBrush(QtGui.QColor("red")))
|
||||
else:
|
||||
expand = False
|
||||
top.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
|
||||
top.setForeground(2, 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(1, QtCore.Qt.DisplayRole, human_filesize(size))
|
||||
top.setData(2, QtCore.Qt.DisplayRole, status)
|
||||
top.setData(0, QtCore.Qt.UserRole, version)
|
||||
top.setData(2, QtCore.Qt.UserRole, self._appliance)
|
||||
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)
|
||||
for column in range(self.uiApplianceVersionTreeWidget.columnCount()):
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(column)
|
||||
|
||||
self._refreshing = False
|
||||
|
||||
def _getSymbolsCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback to retrieve the appliance symbols.
|
||||
"""
|
||||
|
||||
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
|
||||
Scan local directory in order to find images on the disk
|
||||
"""
|
||||
|
||||
# Docker do not have versions
|
||||
@@ -305,11 +410,20 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
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"))
|
||||
img = self._registry.search_image_file(self._appliance.emulator(),
|
||||
image["filename"],
|
||||
image.get("md5sum"),
|
||||
image.get("filesize"),
|
||||
strict_md5_check=not self.allowCustomFiles.isChecked())
|
||||
if img:
|
||||
image["status"] = "Found"
|
||||
if img.location == "local":
|
||||
image["status"] = "Found locally"
|
||||
else:
|
||||
compute = ComputeManager.instance().getCompute(self._compute_id)
|
||||
image["status"] = "Found on {}".format(compute.name())
|
||||
image["md5sum"] = img.md5sum
|
||||
image["filesize"] = img.filesize
|
||||
image["path"] = img.path
|
||||
else:
|
||||
image["status"] = "Missing"
|
||||
self._refreshing = False
|
||||
@@ -320,9 +434,9 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
"""
|
||||
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
|
||||
@@ -336,9 +450,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
@qslot
|
||||
def _downloadPushButtonClickedSlot(self, *args):
|
||||
"""
|
||||
Called when user want to download an appliance images.
|
||||
He should have selected the file before.
|
||||
Called when user wants to download an appliance image.
|
||||
The file should be selected first.
|
||||
"""
|
||||
|
||||
if self._refreshing:
|
||||
return False
|
||||
|
||||
@@ -352,7 +467,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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"]))
|
||||
QtWidgets.QMessageBox.warning(self, "Add appliance", "The file is compressed with '{}', it must be uncompressed first".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"]))
|
||||
@@ -375,9 +490,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
@qslot
|
||||
def _importPushButtonClickedSlot(self, *args):
|
||||
"""
|
||||
Called when user want to import an appliance images.
|
||||
He should have selected the file before.
|
||||
Called when user wants to import an appliance images.
|
||||
The file should be selected first.
|
||||
"""
|
||||
|
||||
if self._refreshing:
|
||||
return False
|
||||
|
||||
@@ -393,12 +509,17 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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
|
||||
reply = QtWidgets.QMessageBox.question(self, "Add appliance",
|
||||
"This is not the correct file. The MD5 sum is {} and should be {}.\nDo you want to accept it at your own risks?".format(image.md5sum, disk["md5sum"]),
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
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(self._compute_id, callback=self._imageUploadedCallback)
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -426,19 +547,16 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
def _install(self, version):
|
||||
"""
|
||||
Install the appliance to GNS3
|
||||
Install the appliance in GNS3
|
||||
|
||||
:params version: Version name
|
||||
:params version: appliance 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()
|
||||
if "docker" not in appliance_configuration:
|
||||
# only Docker do not have version
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
@@ -446,50 +564,83 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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"])
|
||||
template_manager = TemplateManager().instance()
|
||||
while len(appliance_configuration["name"]) == 0 or not template_manager.is_name_available(appliance_configuration["name"]):
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add template", "The name \"{}\" is already used by another template".format(appliance_configuration["name"]))
|
||||
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add template", "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), 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
|
||||
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, self._symbols, parent=self)
|
||||
TemplateManager.instance().createTemplate(Template(new_template), callback=self._templateCreatedCallback)
|
||||
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
|
||||
#worker = WaitForLambdaWorker(lambda: self._create_template(appliance_configuration, self._compute_id), allowed_exceptions=[ConfigException, OSError])
|
||||
#progress_dialog = ProgressDialog(worker, "Add template", "Installing a new template...", None, busy=True, parent=self)
|
||||
#progress_dialog.show()
|
||||
#if progress_dialog.exec_():
|
||||
# QtWidgets.QMessageBox.information(self.parent(), "Add template", "{} template has been installed!".format(appliance_configuration["name"]))
|
||||
# return True
|
||||
#return False
|
||||
|
||||
def _uploadImages(self, version):
|
||||
# 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 _templateCreatedCallback(self, result, error=False, **kwargs):
|
||||
|
||||
if error is True:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add template", "The template cannot be created: {}".format(result.get("message", "unknown")))
|
||||
return
|
||||
|
||||
QtWidgets.QMessageBox.information(self.parent(), "Add template", "The appliance has been installed and a template named '{}' has been successfully created!".format(result["name"]))
|
||||
self._template_created = True
|
||||
self.done(True)
|
||||
|
||||
def _uploadImages(self, name, version):
|
||||
"""
|
||||
Upload an image to the compute
|
||||
Upload an image the compute.
|
||||
"""
|
||||
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
try:
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
except ApplianceError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Appliance","Cannot install {} version {}: {}".format(name, version, e))
|
||||
return
|
||||
for image in appliance_configuration["images"]:
|
||||
if image["location"] == "local":
|
||||
if not Controller.instance().isRemote() and self._compute_id == "local" and image["path"].startswith(ImageManager.instance().getDirectory()):
|
||||
log.debug("{} is already on the local server".format(image["path"]))
|
||||
return
|
||||
image = Image(self._appliance.emulator(), image["path"], filename=image["filename"])
|
||||
image.upload(self._compute_id, self._applianceImageUploadedCallback)
|
||||
image_upload_manager = ImageUploadManager(image, Controller.instance(), self._compute_id, self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
|
||||
image_upload_manager.upload()
|
||||
self._image_uploading_count += 1
|
||||
|
||||
def _applianceImageUploadedCallback(self, result, error=False, **kwargs):
|
||||
self._image_uploading_count -= 1
|
||||
def _applianceImageUploadedCallback(self, result, error=False, context=None, **kwargs):
|
||||
if context is None:
|
||||
context = {}
|
||||
image_path = context.get("image_path", "unknown")
|
||||
if error:
|
||||
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
|
||||
else:
|
||||
log.info("Image '{}' has been successfully uploaded".format(image_path))
|
||||
self._image_uploading_count -= 1
|
||||
|
||||
def nextId(self):
|
||||
if self.currentPage() == self.uiServerWizardPage:
|
||||
if "docker" in self._appliance:
|
||||
return super().nextId() + 3
|
||||
# skip Qemu binary selection and files pages if this is a Docker appliance
|
||||
return super().nextId() + 2
|
||||
elif "qemu" not in self._appliance:
|
||||
return super().nextId() + 1
|
||||
elif self.currentPage() == self.uiFilesWizardPage:
|
||||
if "qemu" not in self._appliance:
|
||||
# skip the Qemu binary selection page if not a Qemu appliance
|
||||
return super().nextId() + 1
|
||||
return super().nextId()
|
||||
|
||||
@@ -499,6 +650,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiFilesWizardPage:
|
||||
# validate the files page
|
||||
|
||||
if self._refreshing:
|
||||
return False
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
@@ -508,20 +661,26 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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"]))
|
||||
try:
|
||||
self._appliance.search_images_for_version(version["name"])
|
||||
except ApplianceError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Appliance", "Cannot install {} version {}: {}".format(appliance["name"], version["name"], e))
|
||||
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"])
|
||||
|
||||
self._uploadImages(appliance["name"], 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
|
||||
# validate the usage page
|
||||
|
||||
if self._template_created:
|
||||
return True
|
||||
if self._image_uploading_count > 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for appliance files to be uploaded")
|
||||
return False
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
if current:
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
@@ -530,9 +689,11 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
return self._install(None)
|
||||
|
||||
elif self.currentPage() == self.uiServerWizardPage:
|
||||
# validate the server page
|
||||
|
||||
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")
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote servers configured in your preferences")
|
||||
return False
|
||||
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
|
||||
@@ -541,20 +702,20 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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)
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and macOS is not supported by the GNS3 team. Do you want to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
|
||||
self._compute_id = "local"
|
||||
|
||||
elif self.currentPage() == self.uiQemuWizardPage:
|
||||
# validate the Qemu
|
||||
|
||||
if self._server_check is False:
|
||||
QtWidgets.QMessageBox.critical(self, "Checking for KVM support", "Please wait for the server to reply...")
|
||||
return False
|
||||
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
|
||||
@@ -564,6 +725,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
@@ -590,3 +752,21 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
@qslot
|
||||
def _allowCustomFilesChangedSlot(self, checked):
|
||||
"""
|
||||
Slot for when user want to upload images which don't match md5
|
||||
|
||||
:param checked: if allows or doesn't allow custom files
|
||||
:return:
|
||||
"""
|
||||
if checked:
|
||||
reply = QtWidgets.QMessageBox.question(self, "Custom files",
|
||||
"This option allows files with different MD5 checksums. This feature is only for advanced users and can lead "
|
||||
"to unexpected problems. Do you want to proceed?",
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
self.allowCustomFiles.setChecked(False)
|
||||
return False
|
||||
|
||||
@@ -23,6 +23,7 @@ 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
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
|
||||
|
||||
def __init__(self, parent, console_type="telnet", current=None):
|
||||
"""
|
||||
:params console_type: telnet, serial or vnc
|
||||
:params console_type: telnet, serial, vnc or spice
|
||||
:params current: Current console command
|
||||
"""
|
||||
super().__init__(parent)
|
||||
@@ -62,6 +63,9 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
|
||||
elif self._console_type == "vnc":
|
||||
self._consoles = copy.copy(PRECONFIGURED_VNC_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
elif self._console_type.startswith("spice"):
|
||||
self._consoles = copy.copy(PRECONFIGURED_SPICE_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
|
||||
self.uiCommandComboBox.clear()
|
||||
self.uiCommandComboBox.addItem("Custom", "")
|
||||
|
||||
200
gns3/dialogs/custom_adapters_configuration_dialog.py
Normal file
200
gns3/dialogs/custom_adapters_configuration_dialog.py
Normal file
@@ -0,0 +1,200 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Custom adapters configuration.
|
||||
"""
|
||||
|
||||
import textwrap
|
||||
import re
|
||||
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from ..ui.custom_adapters_configuration_dialog_ui import Ui_CustomAdaptersConfigurationDialog
|
||||
|
||||
|
||||
class NoEditDelegate(QtWidgets.QStyledItemDelegate):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtWidgets.QStyledItemDelegate.__init__(self, parent=parent)
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
return None
|
||||
|
||||
|
||||
class TreeWidgetItem(QtWidgets.QTreeWidgetItem):
|
||||
|
||||
def __lt__(self, other):
|
||||
column = self.treeWidget().sortColumn()
|
||||
key1 = self.text(column)
|
||||
key2 = other.text(column)
|
||||
return self.natural_sort_key(key1) < self.natural_sort_key(key2)
|
||||
|
||||
@staticmethod
|
||||
def natural_sort_key(key):
|
||||
regex = r'(\d*\.\d+|\d+)'
|
||||
parts = re.split(regex, key)
|
||||
return tuple((e if i % 2 == 0 else float(e)) for i, e in enumerate(parts))
|
||||
|
||||
|
||||
class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConfigurationDialog):
|
||||
"""
|
||||
Custom adapters configuration dialog.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, ports, custom_adapters, default_adapter_type=None, adapter_types=None, base_mac_address=None, parent=None):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self._ports = ports
|
||||
self._default_adapter_type = default_adapter_type
|
||||
self._adapter_types = adapter_types
|
||||
self._custom_adapters = custom_adapters
|
||||
self._base_mac_address = base_mac_address
|
||||
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).clicked.connect(self._resetSlot)
|
||||
|
||||
if self._default_adapter_type and self._adapter_types:
|
||||
self.uiAdaptersTreeWidget.setColumnCount(3)
|
||||
self.uiAdaptersTreeWidget.headerItem().setText(2, "Adapter type")
|
||||
|
||||
if self._base_mac_address:
|
||||
self.uiAdaptersTreeWidget.setColumnCount(4)
|
||||
self.uiAdaptersTreeWidget.headerItem().setText(3, "MAC address")
|
||||
|
||||
self._populateWidgets()
|
||||
|
||||
# resize to fit the tree widget
|
||||
width = 0
|
||||
for column in range(self.uiAdaptersTreeWidget.columnCount()):
|
||||
width += 20 + self.uiAdaptersTreeWidget.columnWidth(column)
|
||||
self.resize(QtCore.QSize(width, self.height()))
|
||||
|
||||
def _getCustomAdapterSettings(self, adapter_number):
|
||||
|
||||
for custom_adapter in self._custom_adapters:
|
||||
if custom_adapter["adapter_number"] == adapter_number:
|
||||
return custom_adapter
|
||||
return {}
|
||||
|
||||
def _MacToInteger(self, mac_address):
|
||||
"""
|
||||
Convert a macaddress with the format 00:0c:29:11:b0:0a to a int
|
||||
|
||||
:param mac_address: The mac address
|
||||
|
||||
:returns: Integer
|
||||
"""
|
||||
|
||||
return int(mac_address.replace(":", ""), 16)
|
||||
|
||||
def _IntegerToMac(self, integer):
|
||||
"""
|
||||
Convert an integer to a mac address
|
||||
"""
|
||||
|
||||
return ":".join(textwrap.wrap("%012x" % (integer), width=2))
|
||||
|
||||
def _populateWidgets(self):
|
||||
|
||||
adapter_number = 0
|
||||
for port_name in self._ports:
|
||||
item = TreeWidgetItem(self.uiAdaptersTreeWidget)
|
||||
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
|
||||
item.setText(0, "Adapter {}".format(adapter_number))
|
||||
item.setData(0, QtCore.Qt.UserRole, adapter_number)
|
||||
item.setData(1, QtCore.Qt.UserRole, port_name)
|
||||
custom_adapter = self._getCustomAdapterSettings(adapter_number)
|
||||
item.setText(1, custom_adapter.get("port_name", port_name))
|
||||
|
||||
if self._default_adapter_type and self._adapter_types:
|
||||
combobox = QtWidgets.QComboBox(self)
|
||||
if type(self._adapter_types) == list:
|
||||
for adapter_type in self._adapter_types:
|
||||
combobox.addItem("{}".format(adapter_type))
|
||||
else:
|
||||
index = 0
|
||||
for adapter_type, adapter_description in self._adapter_types.items():
|
||||
combobox.addItem("{}".format(adapter_type))
|
||||
combobox.setItemData(index, adapter_description, QtCore.Qt.ToolTipRole)
|
||||
index += 1
|
||||
adapter_type_index = combobox.findText(custom_adapter.get("adapter_type", self._default_adapter_type))
|
||||
combobox.setCurrentIndex(adapter_type_index)
|
||||
self.uiAdaptersTreeWidget.setItemWidget(item, 2, combobox)
|
||||
|
||||
if self._base_mac_address:
|
||||
self.uiAdaptersTreeWidget.addTopLevelItem(item)
|
||||
line_edit = QtWidgets.QLineEdit(self)
|
||||
line_edit.setInputMask("HH:HH:HH:HH:HH:HH;_")
|
||||
mac_address = self._IntegerToMac(self._MacToInteger(self._base_mac_address) + adapter_number)
|
||||
line_edit.setText(custom_adapter.get("mac_address", mac_address))
|
||||
self.uiAdaptersTreeWidget.setItemWidget(item, 3, line_edit)
|
||||
adapter_number += 1
|
||||
|
||||
self.uiAdaptersTreeWidget.setItemDelegateForColumn(0, NoEditDelegate(self))
|
||||
self.uiAdaptersTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiAdaptersTreeWidget.setSortingEnabled(True)
|
||||
|
||||
for column in range(self.uiAdaptersTreeWidget.columnCount()):
|
||||
self.uiAdaptersTreeWidget.resizeColumnToContents(column)
|
||||
|
||||
def _resetSlot(self):
|
||||
|
||||
self.uiAdaptersTreeWidget.clear()
|
||||
self._custom_adapters.clear()
|
||||
self._populateWidgets()
|
||||
|
||||
def _updateCustomAdapters(self):
|
||||
|
||||
self._custom_adapters.clear()
|
||||
for row in range(self.uiAdaptersTreeWidget.topLevelItemCount()):
|
||||
custom_adapter_settings = {}
|
||||
item = self.uiAdaptersTreeWidget.topLevelItem(row)
|
||||
port_name = item.text(1)
|
||||
adapter_number = item.data(0, QtCore.Qt.UserRole)
|
||||
custom_adapter_settings["adapter_number"] = adapter_number
|
||||
original_port_name = item.data(1, QtCore.Qt.UserRole)
|
||||
if original_port_name != port_name:
|
||||
custom_adapter_settings["port_name"] = port_name
|
||||
if self._default_adapter_type and self._adapter_types:
|
||||
adapter_type = self.uiAdaptersTreeWidget.itemWidget(item, 2).currentText()
|
||||
if self._default_adapter_type != adapter_type:
|
||||
custom_adapter_settings["adapter_type"] = adapter_type
|
||||
if self._base_mac_address:
|
||||
mac_address = self.uiAdaptersTreeWidget.itemWidget(item, 3).text()
|
||||
if mac_address and mac_address != ":::::":
|
||||
if not re.search(r"""^([0-9a-fA-F]{2}[:]){5}[0-9a-fA-F]{2}$""", mac_address):
|
||||
QtWidgets.QMessageBox.critical(self, "MAC address", "Invalid MAC address (format required: hh:hh:hh:hh:hh:hh)")
|
||||
return
|
||||
default_mac_address = self._IntegerToMac(self._MacToInteger(self._base_mac_address) + adapter_number)
|
||||
if mac_address != default_mac_address:
|
||||
custom_adapter_settings["mac_address"] = mac_address
|
||||
if len(custom_adapter_settings) > 1:
|
||||
# only save if there is more than the adapter_number key
|
||||
self._custom_adapters.append(custom_adapter_settings.copy())
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
Called when the dialog is closed.
|
||||
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
|
||||
if result:
|
||||
self._updateCustomAdapters()
|
||||
super().done(result)
|
||||
@@ -95,13 +95,14 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
def checkAVGInstalled(self):
|
||||
"""Checking if AVG software is not installed"""
|
||||
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
psinfo = proc.as_dict(["exe"])
|
||||
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
|
||||
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
if sys.platform.startswith("win32"):
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
psinfo = proc.as_dict(["exe"])
|
||||
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
|
||||
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
return (0, None)
|
||||
|
||||
def checkFreeRam(self):
|
||||
@@ -136,22 +137,15 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
if not os.path.exists(path):
|
||||
return (2, "Ubridge path {path} doesn't exists".format(path=path))
|
||||
|
||||
request_setuid = False
|
||||
if sys.platform.startswith("linux"):
|
||||
try:
|
||||
if "security.capability" in os.listxattr(path):
|
||||
caps = os.getxattr(path, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
return (2, "Ubridge requires CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
|
||||
else:
|
||||
# capabilities not supported
|
||||
request_setuid = True
|
||||
except AttributeError:
|
||||
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 (Python bug)".format(path=path))
|
||||
return (1, "Could not determine if CAP_NET_RAW capability is set for uBridge: {}".format(e))
|
||||
|
||||
if sys.platform.startswith("darwin") or request_setuid:
|
||||
if 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)
|
||||
|
||||
@@ -41,9 +41,6 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
self.uiServerHostLineEdit.setText(self._compute.host())
|
||||
self.uiServerPortSpinBox.setValue(self._compute.port())
|
||||
|
||||
index = self.uiServerProtocolComboBox.findText(self._compute.protocol().upper())
|
||||
self.uiServerProtocolComboBox.setCurrentIndex(index)
|
||||
|
||||
if self._compute.user():
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(True)
|
||||
self.uiServerUserLineEdit.setText(self._compute.user())
|
||||
@@ -81,7 +78,7 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
|
||||
host = self.uiServerHostLineEdit.text().strip()
|
||||
name = self.uiServerNameLineEdit.text().strip()
|
||||
protocol = self.uiServerProtocolComboBox.currentText().lower()
|
||||
protocol = "http"
|
||||
port = self.uiServerPortSpinBox.value()
|
||||
user = self.uiServerUserLineEdit.text().strip()
|
||||
password = self.uiServerPasswordLineEdit.text().strip()
|
||||
|
||||
@@ -15,7 +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/>.
|
||||
|
||||
from ..qt import QtWidgets
|
||||
from ..qt import QtWidgets, QtCore, qslot, qpartial
|
||||
from ..topology import Topology
|
||||
from ..ui.edit_project_dialog_ui import Ui_EditProjectDialog
|
||||
|
||||
@@ -36,6 +36,69 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
self.uiProjectAutoStartCheckBox.setChecked(self._project.autoStart())
|
||||
self.uiSceneWidthSpinBox.setValue(self._project.sceneWidth())
|
||||
self.uiSceneHeightSpinBox.setValue(self._project.sceneHeight())
|
||||
self.uiNodeGridSizeSpinBox.setValue(self._project.nodeGridSize())
|
||||
self.uiDrawingGridSizeSpinBox.setValue(self._project.drawingGridSize())
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -51,5 +114,8 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
self._project.setAutoStart(self.uiProjectAutoStartCheckBox.isChecked())
|
||||
self._project.setSceneHeight(self.uiSceneHeightSpinBox.value())
|
||||
self._project.setSceneWidth(self.uiSceneWidthSpinBox.value())
|
||||
self._project.setNodeGridSize(self.uiNodeGridSizeSpinBox.value())
|
||||
self._project.setDrawingGridSize(self.uiDrawingGridSizeSpinBox.value())
|
||||
self._project.setVariables(self._cleanVariables())
|
||||
self._project.update()
|
||||
super().done(result)
|
||||
|
||||
@@ -62,7 +62,7 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
self._exportDebugCallback({}, error=True)
|
||||
|
||||
def _exportDebugCallback(self, result, error=False, **kwargs):
|
||||
log.info("Export debug information to %s", self._path)
|
||||
log.debug("Export debug information to %s", self._path)
|
||||
|
||||
try:
|
||||
with ZipFile(self._path, 'w') as zip:
|
||||
|
||||
@@ -66,7 +66,7 @@ class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
|
||||
|
||||
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
|
||||
if not error:
|
||||
self.uiFileTextEdit.setText(raw_body.decode("utf-8"))
|
||||
self.uiFileTextEdit.setText(raw_body.decode("utf-8", errors="ignore"))
|
||||
elif result.get("status") == 404:
|
||||
if self._default:
|
||||
self.uiFileTextEdit.setText(self._default)
|
||||
|
||||
178
gns3/dialogs/filter_dialog.py
Normal file
178
gns3/dialogs/filter_dialog.py
Normal file
@@ -0,0 +1,178 @@
|
||||
# -*- 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
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
|
||||
|
||||
"""
|
||||
Filter dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, link):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self._link = link
|
||||
self._filters = {}
|
||||
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:
|
||||
log.warning("Error while listing information about the link: {}".format(result["message"]))
|
||||
return
|
||||
self._filters = result
|
||||
self._initialized = True
|
||||
self._updateUiSlot()
|
||||
|
||||
@qslot
|
||||
def _updateUiSlot(self, *args):
|
||||
|
||||
# Empty the main layout
|
||||
while True:
|
||||
item = self.uiVerticalLayout.takeAt(0)
|
||||
if item is None:
|
||||
break
|
||||
elif item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
if len(self._filters) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Link", "No filter available for this link. Try with a different node type.")
|
||||
self.reject()
|
||||
|
||||
self._tabWidget = QtWidgets.QTabWidget(self)
|
||||
for i, filter in enumerate(self._filters):
|
||||
tab = QtWidgets.QWidget()
|
||||
self._tabWidget.addTab(tab, filter['name'])
|
||||
self._tabWidget.setTabToolTip(i, filter['description'])
|
||||
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_red.svg'))
|
||||
vlayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
gridLayout = QtWidgets.QGridLayout()
|
||||
line = 0
|
||||
filter["spinBoxes"] = []
|
||||
filter["textEdits"] = []
|
||||
|
||||
nb_spin = 0
|
||||
|
||||
for param in filter["parameters"]:
|
||||
label = QtWidgets.QLabel()
|
||||
label.setText(param["name"] + ":")
|
||||
gridLayout.addWidget(label, line, 0, 1, 1)
|
||||
|
||||
if param["type"] == "int":
|
||||
spinBox = QtWidgets.QSpinBox()
|
||||
filter["spinBoxes"].append(spinBox)
|
||||
spinBox.setMinimum(param["minimum"])
|
||||
spinBox.setMaximum(param["maximum"])
|
||||
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(spinBox.sizePolicy().hasHeightForWidth())
|
||||
spinBox.setSizePolicy(sizePolicy)
|
||||
try:
|
||||
value = self._link.filters()[filter["type"]][nb_spin]
|
||||
spinBox.setValue(value)
|
||||
if value != 0:
|
||||
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_green.svg'))
|
||||
except(KeyError, IndexError):
|
||||
pass
|
||||
nb_spin += 1
|
||||
gridLayout.addWidget(spinBox, line, 1, 1, 1)
|
||||
unit = QtWidgets.QLabel()
|
||||
unit.setText(param["unit"])
|
||||
gridLayout.addWidget(unit, line, 2, 1, 1)
|
||||
elif param["type"] == "text":
|
||||
textEdit = QtWidgets.QTextEdit()
|
||||
textEdit.setAcceptRichText(False)
|
||||
filter["textEdits"].append(textEdit)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
textEdit.setMinimumWidth(300)
|
||||
textEdit.setSizePolicy(sizePolicy)
|
||||
try:
|
||||
text = self._link.filters()[filter["type"]][0]
|
||||
textEdit.setPlainText(text)
|
||||
if text:
|
||||
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_green.svg'))
|
||||
except(KeyError, IndexError):
|
||||
pass
|
||||
gridLayout.addWidget(textEdit, line, 1, 1, 1)
|
||||
|
||||
line += 1
|
||||
|
||||
spacerItem = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
gridLayout.addItem(spacerItem, line, 0, 1, 1)
|
||||
vlayout.addLayout(gridLayout)
|
||||
tab.setLayout(vlayout)
|
||||
|
||||
self.uiVerticalLayout.addWidget(self._tabWidget)
|
||||
|
||||
@qslot
|
||||
def _applyPreferencesSlot(self, *args):
|
||||
new_filters = {}
|
||||
for filter in self._filters:
|
||||
new_filters[filter["type"]] = []
|
||||
for spinBox in filter["spinBoxes"]:
|
||||
new_filters[filter["type"]].append(spinBox.value())
|
||||
for spinBox in filter["textEdits"]:
|
||||
new_filters[filter["type"]].append(spinBox.toPlainText())
|
||||
self._link.setFilters(new_filters)
|
||||
self._link.update()
|
||||
|
||||
@qslot
|
||||
def _helpSlot(self, *args):
|
||||
help_text = "Filters are applied to packets in both direction.\n\n"
|
||||
|
||||
filter_nb = 0
|
||||
for filter in self._filters:
|
||||
help_text += "{}: {}".format(filter["name"], filter["description"])
|
||||
filter_nb += 1
|
||||
if len(self._filters) != filter_nb:
|
||||
help_text += "\n\n"
|
||||
|
||||
QtWidgets.QMessageBox.information(self, "Help for filters", help_text)
|
||||
|
||||
@qslot
|
||||
def _resetSlot(self, *args):
|
||||
|
||||
filters = {}
|
||||
self._link.setFilters(filters)
|
||||
self._link.update()
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
Called when the dialog is closed.
|
||||
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
if result and self._initialized:
|
||||
self._applyPreferencesSlot()
|
||||
super().done(result)
|
||||
@@ -40,7 +40,8 @@ class IdlePCDialog(QtWidgets.QDialog, Ui_IdlePCDialog):
|
||||
self._idlepcs = idlepcs
|
||||
|
||||
for value in self._idlepcs:
|
||||
match = re.search(r"^(0x[0-9a-f]+)\s+\[(\d+)\]$", value)
|
||||
# validate idle-pc format, e.g. 0x60c09aa0
|
||||
match = re.search(r"^(0x[0-9a-f]{8})\s+\[(\d+)\]$", value)
|
||||
if match:
|
||||
idlepc = match.group(1)
|
||||
count = int(match.group(2))
|
||||
@@ -61,7 +62,7 @@ Select each value that appears in the list and click Apply, and note the CPU usa
|
||||
"""
|
||||
QtWidgets.QMessageBox.information(self, "Hints for Idle-PC", help_text)
|
||||
|
||||
def _applySlot(self):
|
||||
def _applySlot(self, update_template=False):
|
||||
"""
|
||||
Applies an Idle-PC value.
|
||||
"""
|
||||
@@ -77,8 +78,9 @@ Select each value that appears in the list and click Apply, and note the CPU usa
|
||||
if hasattr(node, "idlepc") and node.settings()["image"] == ios_image:
|
||||
node.setIdlepc(idlepc)
|
||||
|
||||
# apply the idle-pc to templates with the same IOS image
|
||||
self._router.module().updateImageIdlepc(ios_image, idlepc)
|
||||
if update_template:
|
||||
# apply the idle-pc to templates with the same IOS image
|
||||
self._router.module().updateImageIdlepc(ios_image, idlepc)
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
@@ -88,5 +90,5 @@ Select each value that appears in the list and click Apply, and note the CPU usa
|
||||
"""
|
||||
|
||||
if result:
|
||||
self._applySlot()
|
||||
self._applySlot(update_template=True)
|
||||
super().done(result)
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# -*- 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_()
|
||||
288
gns3/dialogs/new_template_wizard.py
Normal file
288
gns3/dialogs/new_template_wizard.py
Normal file
@@ -0,0 +1,288 @@
|
||||
# -*- 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 sys
|
||||
import tempfile
|
||||
import json
|
||||
import sip
|
||||
import os
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets, qpartial
|
||||
from gns3.controller import Controller
|
||||
from gns3.appliance_manager import ApplianceManager
|
||||
|
||||
from ..ui.new_template_wizard_ui import Ui_NewTemplateWizard
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
|
||||
"""
|
||||
New template wizard.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
|
||||
|
||||
# add a custom button to show appliance information
|
||||
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Update from online registry")
|
||||
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
|
||||
self.customButtonClicked.connect(self._downloadAppliancesSlot)
|
||||
self.button(QtWidgets.QWizard.CustomButton1).hide()
|
||||
self.uiFilterLineEdit.textChanged.connect(self._filterTextChangedSlot)
|
||||
ApplianceManager.instance().appliances_changed_signal.connect(self._appliancesChangedSlot)
|
||||
|
||||
def _downloadAppliancesSlot(self):
|
||||
"""
|
||||
Request server to update appliances from online registry.
|
||||
"""
|
||||
|
||||
ApplianceManager.instance().refresh(update=True)
|
||||
Controller.instance().clearStaticCache()
|
||||
|
||||
def _appliancesChangedSlot(self):
|
||||
"""
|
||||
Called when the appliances have been updated.
|
||||
"""
|
||||
|
||||
self._get_appliances_from_server()
|
||||
QtWidgets.QMessageBox.information(self, "Appliances", "Appliances are up-to-date!")
|
||||
|
||||
def _filterTextChangedSlot(self, text):
|
||||
self._get_appliances_from_server(appliance_filter=text)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
|
||||
if item is None or sip.isdeleted(item):
|
||||
return
|
||||
item.setIcon(0, icon)
|
||||
|
||||
def _get_tooltip_text(self, appliance):
|
||||
"""
|
||||
Gets the appliance information to be displayed in the tooltip.
|
||||
"""
|
||||
|
||||
info = (("Product", "product_name"),
|
||||
("Vendor", "vendor_name"),
|
||||
("Availability", "availability"),
|
||||
("Status", "status"),
|
||||
("Maintainer", "maintainer"))
|
||||
|
||||
if "qemu" in appliance:
|
||||
qemu_info = (("vCPUs", "qemu/cpus"),
|
||||
("RAM", "qemu/ram"),
|
||||
("Adapters", "qemu/adapters"),
|
||||
("Adapter type", "qemu/adapter_type"),
|
||||
("Console type", "qemu/console_type"),
|
||||
("Architecture", "qemu/arch"),
|
||||
("Console type", "qemu/console_type"),
|
||||
("KVM", "qemu/kvm"))
|
||||
info = info + qemu_info
|
||||
|
||||
elif "docker" in appliance:
|
||||
docker_info = (("Image", "docker/image"),
|
||||
("Adapters", "docker/adapters"),
|
||||
("Console type", "docker/console_type"))
|
||||
info = info + docker_info
|
||||
|
||||
elif "iou" in appliance:
|
||||
iou_info = (("RAM", "iou/ram"),
|
||||
("NVRAM", "iou/nvram"),
|
||||
("Ethernet adapters", "iou/ethernet_adapters"),
|
||||
("Serial adapters", "iou/serial_adapters"))
|
||||
info = info + iou_info
|
||||
|
||||
elif "dynamips" in appliance:
|
||||
dynamips_info = (("Platform", "dynamips/platform"),
|
||||
("Chassis", "dynamips/chassis"),
|
||||
("Midplane", "dynamips/midplane"),
|
||||
("NPE", "dynamips/npe"),
|
||||
("RAM", "dynamips/ram"),
|
||||
("NVRAM", "dynamips/nvram"),
|
||||
("slot0", "dynamips/slot0"),
|
||||
("slot1", "dynamips/slot1"),
|
||||
("slot2", "dynamips/slot2"),
|
||||
("slot3", "dynamips/slot3"),
|
||||
("slot4", "dynamips/slot4"),
|
||||
("slot5", "dynamips/slot5"),
|
||||
("slot6", "dynamips/slot6"),
|
||||
("wic0", "dynamips/wic0"),
|
||||
("wic1", "dynamips/wic1"),
|
||||
("wic2", "dynamips/wic2"))
|
||||
info = info + dynamips_info
|
||||
|
||||
text_info = ""
|
||||
for (name, key) in info:
|
||||
if "/" in key:
|
||||
key, subkey = key.split("/")
|
||||
value = appliance.get(key, {}).get(subkey, None)
|
||||
else:
|
||||
value = appliance.get(key, None)
|
||||
if value is None:
|
||||
continue
|
||||
text_info += "<span style='font-weight:bold;'>{}</span>: {}<br>".format(name, value)
|
||||
|
||||
return text_info
|
||||
|
||||
def _get_appliances_from_server(self, appliance_filter=None):
|
||||
"""
|
||||
Gets the appliances from the server and display them.
|
||||
"""
|
||||
|
||||
self.uiAppliancesTreeWidget.clear()
|
||||
parent_routers = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
|
||||
parent_routers.setText(0, "Routers")
|
||||
parent_routers.setFlags(parent_routers.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
parent_switches = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
|
||||
parent_switches.setText(0, "Switches")
|
||||
parent_switches.setFlags(parent_switches.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
parent_guests = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
|
||||
parent_guests.setText(0, "Guests")
|
||||
parent_guests.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
parent_firewalls = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
|
||||
parent_firewalls.setText(0, "Firewalls")
|
||||
parent_firewalls.setFlags(parent_firewalls.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
self.uiAppliancesTreeWidget.expandAll()
|
||||
|
||||
for appliance in ApplianceManager.instance().appliances():
|
||||
if appliance_filter is None:
|
||||
appliance_filter = self.uiFilterLineEdit.text().strip()
|
||||
if appliance_filter and appliance_filter.lower() not in appliance["name"].lower():
|
||||
continue
|
||||
|
||||
if appliance["category"] == "router":
|
||||
item = QtWidgets.QTreeWidgetItem(parent_routers)
|
||||
elif appliance["category"].endswith("switch"):
|
||||
item = QtWidgets.QTreeWidgetItem(parent_switches)
|
||||
elif appliance["category"] == "firewall":
|
||||
item = QtWidgets.QTreeWidgetItem(parent_firewalls)
|
||||
elif appliance["category"] == "guest":
|
||||
item = QtWidgets.QTreeWidgetItem(parent_guests)
|
||||
if appliance["builtin"]:
|
||||
appliance_name = appliance["name"]
|
||||
else:
|
||||
appliance_name = "{} (custom)".format(appliance["name"])
|
||||
|
||||
item.setText(0, appliance_name)
|
||||
#item.setText(1, appliance["category"].capitalize().replace("_", " "))
|
||||
|
||||
if "qemu" in appliance:
|
||||
item.setText(1, "Qemu")
|
||||
elif "iou" in appliance:
|
||||
item.setText(1, "IOU")
|
||||
elif "dynamips" in appliance:
|
||||
item.setText(1, "Dynamips")
|
||||
elif "docker" in appliance:
|
||||
item.setText(1, "Docker")
|
||||
else:
|
||||
item.setText(1, "N/A")
|
||||
|
||||
item.setText(2, appliance["vendor_name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, appliance)
|
||||
|
||||
#item.setSizeHint(0, QtCore.QSize(32, 32))
|
||||
item.setToolTip(0, self._get_tooltip_text(appliance))
|
||||
Controller.instance().getSymbolIcon(appliance.get("symbol"), qpartial(self._setItemIcon, item),
|
||||
fallback=":/symbols/" + appliance["category"] + ".svg")
|
||||
|
||||
self.uiAppliancesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiAppliancesTreeWidget.resizeColumnToContents(0)
|
||||
if not appliance_filter:
|
||||
self.uiAppliancesTreeWidget.collapseAll()
|
||||
|
||||
def initializePage(self, page_id):
|
||||
"""
|
||||
Initialize Wizard pages.
|
||||
|
||||
:param page_id: page identifier
|
||||
"""
|
||||
|
||||
super().initializePage(page_id)
|
||||
if self.page(page_id) == self.uiApplianceFromServerWizardPage:
|
||||
self.button(QtWidgets.QWizard.CustomButton1).show()
|
||||
self.setButtonText(QtWidgets.QWizard.FinishButton, "&Install")
|
||||
self._get_appliances_from_server()
|
||||
else:
|
||||
self.button(QtWidgets.QWizard.CustomButton1).hide()
|
||||
|
||||
def cleanupPage(self, page_id):
|
||||
"""
|
||||
Restore button default settings on the first page.
|
||||
"""
|
||||
|
||||
self.button(QtWidgets.QWizard.CustomButton1).hide()
|
||||
self.setButtonText(QtWidgets.QWizard.FinishButton, "&Finish")
|
||||
super().cleanupPage(page_id)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates if an appliance can be installed.
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiSelectTemplateSourceWizardPage and not Controller.instance().connected():
|
||||
QtWidgets.QMessageBox.critical(self, "New template", "There is no connection to the server")
|
||||
return False
|
||||
elif self.currentPage() == self.uiApplianceFromServerWizardPage:
|
||||
if not self.uiAppliancesTreeWidget.selectedItems():
|
||||
QtWidgets.QMessageBox.critical(self, "New template", "Please select an appliance to install!")
|
||||
return False
|
||||
return True
|
||||
|
||||
def nextId(self):
|
||||
"""
|
||||
Wizard rules!
|
||||
"""
|
||||
|
||||
current_id = self.currentId()
|
||||
if self.page(current_id) == self.uiSelectTemplateSourceWizardPage and \
|
||||
(self.uiImportApplianceFromFileRadioButton.isChecked() or self.uiCreateTemplateManuallyRadioButton.isChecked()):
|
||||
self.done(True)
|
||||
return super().nextId()
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
This dialog is closed.
|
||||
"""
|
||||
|
||||
super().done(result)
|
||||
if result:
|
||||
#ApplianceManager.instance().appliances_changed_signal.disconnect(self._appliancesChangedSlot)
|
||||
from gns3.main_window import MainWindow
|
||||
if self.currentPage() == self.uiApplianceFromServerWizardPage:
|
||||
items = self.uiAppliancesTreeWidget.selectedItems()
|
||||
for item in items:
|
||||
f = tempfile.NamedTemporaryFile(mode="w+", suffix=".builtin.gns3a", delete=False)
|
||||
json.dump(item.data(0, QtCore.Qt.UserRole), f)
|
||||
f.close()
|
||||
MainWindow.instance().loadPath(f.name)
|
||||
try:
|
||||
os.remove(f.name)
|
||||
except OSError:
|
||||
pass
|
||||
elif self.uiCreateTemplateManuallyRadioButton.isChecked():
|
||||
MainWindow.instance().preferencesActionSlot()
|
||||
elif self.uiImportApplianceFromFileRadioButton.isChecked():
|
||||
from gns3.main_window import MainWindow
|
||||
MainWindow.instance().openApplianceActionSlot()
|
||||
56
gns3/dialogs/node_info_dialog.py
Normal file
56
gns3/dialogs/node_info_dialog.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Dialog to show node information.
|
||||
"""
|
||||
|
||||
from ..qt import QtWidgets
|
||||
from ..ui.node_info_dialog_ui import Ui_NodeInfoDialog
|
||||
|
||||
|
||||
class NodeInfoDialog(QtWidgets.QDialog, Ui_NodeInfoDialog):
|
||||
|
||||
"""
|
||||
Node information dialog.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, node, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
general_info = node.info()
|
||||
usage_info = node.usage()
|
||||
command_line_info = node.commandLine()
|
||||
self.setWindowTitle(node.name())
|
||||
|
||||
# General tab
|
||||
self.uiGeneralTextBrowser.setPlainText(general_info)
|
||||
|
||||
# Usage tab
|
||||
if not usage_info:
|
||||
usage_info = "No usage information has been provided for this node."
|
||||
self.uiUsageTextBrowser.setPlainText(usage_info)
|
||||
|
||||
# Command line tab
|
||||
if command_line_info is None:
|
||||
command_line_info = "Command line information is not supported for this type of node."
|
||||
elif len(command_line_info) == 0:
|
||||
command_line_info = "Please start the node in order to get the command line information."
|
||||
self.uiCommandLineTextBrowser.setPlainText(command_line_info)
|
||||
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_()
|
||||
@@ -184,10 +184,17 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
self.uiTitleLabel.setText("{} preferences".format(name))
|
||||
index = self.uiStackedWidget.indexOf(preferences_page)
|
||||
widget = self.uiStackedWidget.widget(index)
|
||||
# self.uiStackedWidget.setMinimumSize(widget.size())
|
||||
self.uiStackedWidget.resize(widget.size())
|
||||
#self.uiStackedWidget.setMinimumSize(widget.size()) # FIXME: this seems to not work on Windows and OSX
|
||||
#self.uiStackedWidget.resize(widget.size())
|
||||
self.uiStackedWidget.setCurrentIndex(index)
|
||||
|
||||
for index in range(0, self.uiStackedWidget.count()):
|
||||
page = self.uiStackedWidget.widget(index)
|
||||
if self.uiStackedWidget.currentIndex() == index:
|
||||
page.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
else:
|
||||
page.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
|
||||
|
||||
def _applyPreferences(self):
|
||||
"""
|
||||
Saves all the preferences.
|
||||
@@ -226,11 +233,5 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
Saves the preferences and closes this dialog.
|
||||
"""
|
||||
|
||||
# close the nodes dock to refresh the node list
|
||||
from ..main_window import MainWindow
|
||||
main_window = MainWindow.instance()
|
||||
main_window.uiNodesDockWidget.setVisible(False)
|
||||
main_window.uiNodesDockWidget.setWindowTitle("")
|
||||
|
||||
if self._applyPreferences():
|
||||
QtWidgets.QDialog.accept(self)
|
||||
|
||||
@@ -22,6 +22,7 @@ import shutil
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.ui.profile_select_dialog_ui import Ui_ProfileSelectDialog
|
||||
from gns3.version import __version_info__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -39,8 +40,8 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
self._main.hide()
|
||||
parent = self._main
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.setupUi(self)
|
||||
self.uiNewPushButton.clicked.connect(self._newPushButtonSlot)
|
||||
self.uiDeletePushButton.clicked.connect(self._deletePushButtonSlot)
|
||||
|
||||
@@ -48,12 +49,13 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
screen = QtWidgets.QApplication.desktop().screenGeometry()
|
||||
self.move(screen.center() - self.rect().center())
|
||||
|
||||
version = "{}.{}".format(__version_info__[0], __version_info__[1])
|
||||
if sys.platform.startswith("win"):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
path = os.path.join(appdata, "GNS3")
|
||||
path = os.path.join(appdata, "GNS3", version)
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
path = os.path.join(home, ".config", "GNS3")
|
||||
path = os.path.join(home, ".config", "GNS3", version)
|
||||
self.profiles_path = os.path.join(path, "profiles")
|
||||
|
||||
self.uiShowAtStartupCheckBox.setChecked(LocalConfig.instance().multiProfiles())
|
||||
@@ -65,9 +67,9 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
|
||||
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)
|
||||
for profile in sorted(os.listdir(self.profiles_path)):
|
||||
if not profile.startswith("."):
|
||||
self.uiProfileSelectComboBox.addItem(profile)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@@ -79,7 +81,7 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
super().accept()
|
||||
|
||||
def _newPushButtonSlot(self):
|
||||
profile, ok = QtWidgets.QInputDialog.getText(self.parent(), "New profile", "Profile name:")
|
||||
profile, ok = QtWidgets.QInputDialog.getText(self, "New profile", "Profile name:")
|
||||
if ok:
|
||||
self.uiProfileSelectComboBox.addItem(profile)
|
||||
self.uiProfileSelectComboBox.setCurrentText(profile)
|
||||
@@ -88,13 +90,13 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
def _deletePushButtonSlot(self):
|
||||
profile = self.uiProfileSelectComboBox.currentText()
|
||||
if profile == "default":
|
||||
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", "You can't delete the default profile")
|
||||
QtWidgets.QMessageBox.critical(self, "Delete profile", "The default profile cannot be deleted")
|
||||
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))
|
||||
QtWidgets.QMessageBox.critical(self, "Cannot delete profile", str(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qslot
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qslot, sip_is_deleted
|
||||
from ..ui.project_dialog_ui import Ui_ProjectDialog
|
||||
from ..controller import Controller
|
||||
from ..topology import Topology
|
||||
@@ -91,6 +91,8 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
|
||||
projects_to_delete = set()
|
||||
for project in self.uiProjectsTreeWidget.selectedItems():
|
||||
if sip_is_deleted(project):
|
||||
continue
|
||||
project_id = project.data(0, QtCore.Qt.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
@@ -106,6 +108,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
Controller.instance().deleteProject(project_id)
|
||||
|
||||
def _duplicateProjectSlot(self):
|
||||
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Duplicate project", "No project selected")
|
||||
return
|
||||
@@ -132,11 +135,23 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
new_project_name)
|
||||
name = name.strip()
|
||||
if reply and len(name) > 0:
|
||||
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id), self._duplicateCallback, body={"name": name})
|
||||
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 duplicate project: {}".format(result["message"]))
|
||||
log.error("Error while duplicating project: {}".format(result["message"]))
|
||||
return
|
||||
Controller.instance().refreshProjectList()
|
||||
|
||||
@@ -260,17 +275,17 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
|
||||
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"]):
|
||||
and ("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"]))
|
||||
'Project "{}" is opened, it cannot be overwritten'.format(self._project_settings["project_name"]))
|
||||
return False
|
||||
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"New project",
|
||||
"Project {} already exists, overwrite it?".format(existing_project["name"]),
|
||||
'Project "{}" already exists in location "{}", overwrite it?'.format(existing_project["name"], existing_project["path"]),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
|
||||
131
gns3/dialogs/project_export_wizard.py
Normal file
131
gns3/dialogs/project_export_wizard.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# -*- 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 sys
|
||||
import datetime
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
from ..local_server import LocalServer
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..utils.export_project_worker import ExportProjectWorker
|
||||
from ..ui.export_project_wizard_ui import Ui_ExportProjectWizard
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
|
||||
"""
|
||||
Export project wizard.
|
||||
"""
|
||||
|
||||
def __init__(self, project, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._project = project
|
||||
self._path = None
|
||||
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.uiCompressionComboBox.addItem("None", "none")
|
||||
self.uiCompressionComboBox.addItem("Zip compression (deflate)", "zip")
|
||||
self.uiCompressionComboBox.addItem("Bzip2 compression", "bzip2")
|
||||
self.uiCompressionComboBox.addItem("Lzma compression", "lzma")
|
||||
|
||||
# set zip compression by default
|
||||
self.uiCompressionComboBox.setCurrentIndex(1)
|
||||
self.helpRequested.connect(self._showHelpSlot)
|
||||
self.uiPathBrowserToolButton.clicked.connect(self._pathBrowserSlot)
|
||||
|
||||
readme_text = "Project: '{}' created on {}\nAuthor: John Doe <john.doe@example.com>\n\nNo project description was given".format(self._project.name(), datetime.date.today())
|
||||
self.uiReadmeTextEdit.setPlainText(readme_text)
|
||||
|
||||
def _pathBrowserSlot(self):
|
||||
|
||||
directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
|
||||
if len(directory) == 0:
|
||||
directory = LocalServer.instance().localServerSettings()["projects_path"]
|
||||
|
||||
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export portable project", directory,
|
||||
"GNS3 Portable Project (*.gns3project *.gns3p)",
|
||||
"GNS3 Portable Project (*.gns3project *.gns3p)")
|
||||
if path is None or len(path) == 0:
|
||||
return
|
||||
|
||||
self.uiPathLineEdit.setText(path)
|
||||
|
||||
def _showHelpSlot(self):
|
||||
|
||||
include_image_help = """Including base images means additional images will not be requested to
|
||||
import the project on another computer, however the resulting file will be much bigger.
|
||||
Also, you are responsible to check if you have the right to distribute the image(s) as part of the project.
|
||||
"""
|
||||
QtWidgets.QMessageBox.information(self, "Help about export a project", include_image_help)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates if the project can be exported.
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiExportOptionsWizardPage:
|
||||
path = self.uiPathLineEdit.text().strip()
|
||||
if not path:
|
||||
QtWidgets.QMessageBox.critical(self, "Export project", "Please select a path where to export the project")
|
||||
return False
|
||||
|
||||
if not path.endswith(".gns3project") and not path.endswith(".gns3p"):
|
||||
path += ".gns3project"
|
||||
try:
|
||||
open(path, 'wb+').close()
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Export project", "Cannot export project to '{}': {}".format(path, e))
|
||||
return False
|
||||
self._path = path
|
||||
elif self.currentPage() == self.uiProjectReadmeWizardPage:
|
||||
text = self.uiReadmeTextEdit.toPlainText().strip()
|
||||
if text:
|
||||
self._project.post("/files/README.txt", self._saveReadmeCallback, body=text)
|
||||
return True
|
||||
|
||||
def _saveReadmeCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Export project", "Could not created readme file")
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
This dialog is closed.
|
||||
"""
|
||||
|
||||
if result:
|
||||
if self.uiIncludeImagesCheckBox.isChecked():
|
||||
include_images = "yes"
|
||||
else:
|
||||
include_images = "no"
|
||||
if self.uiIncludeSnapshotsCheckBox.isChecked():
|
||||
include_snapshots = "yes"
|
||||
else:
|
||||
include_snapshots = "no"
|
||||
compression = self.uiCompressionComboBox.currentData()
|
||||
export_worker = ExportProjectWorker(self._project, self._path, include_images, include_snapshots, compression)
|
||||
progress_dialog = ProgressDialog(export_worker, "Exporting project", "Exporting portable project files...", "Cancel", parent=self, create_thread=False)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
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()
|
||||
|
||||
@@ -22,8 +22,7 @@ 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 gns3.utils.interfaces import interfaces
|
||||
|
||||
from ..settings import DEFAULT_LOCAL_SERVER_HOST
|
||||
from ..ui.setup_wizard_ui import Ui_SetupWizard
|
||||
@@ -44,6 +43,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.adjustSize()
|
||||
|
||||
self._gns3_vm_settings = {
|
||||
"enable": True,
|
||||
@@ -62,7 +62,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
self.uiLocalServerToolButton.clicked.connect(self._localServerBrowserSlot)
|
||||
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('')
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText("")
|
||||
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
|
||||
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
|
||||
self.uiVirtualBoxRadioButton.clicked.connect(self._listVirtualBoxVMsSlot)
|
||||
@@ -81,20 +81,22 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
# load all available addresses
|
||||
for address in QtNetwork.QNetworkInterface.allAddresses():
|
||||
address_string = address.toString()
|
||||
if address.protocol() != QtNetwork.QAbstractSocket.IPv6Protocol:
|
||||
self.uiLocalServerHostComboBox.addItem(address_string, address.toString())
|
||||
if address.protocol() in [QtNetwork.QAbstractSocket.IPv4Protocol, QtNetwork.QAbstractSocket.IPv6Protocol]:
|
||||
address_string = address.toString()
|
||||
if address_string.startswith("169.254") or address_string.startswith("fe80"):
|
||||
# ignore link-local addresses, could not use https://doc.qt.io/qt-5/qhostaddress.html#isLinkLocal
|
||||
# because it was introduced in Qt 5.11
|
||||
continue
|
||||
self.uiLocalServerHostComboBox.addItem(address_string, address_string)
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.jpg"))
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.png"))
|
||||
else:
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.jpg"))
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.png"))
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
self.uiVMRadioButton.setText("Run the topologies in an isolated and standard VM")
|
||||
self.uiLocalRadioButton.setText("Run the topologies on my computer")
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
self.uiLocalLabel.setVisible(False)
|
||||
self.uiLocalLabel.setText("Dependencies like Dynamips and Qemu must be manually installed")
|
||||
|
||||
Controller.instance().connected_signal.connect(self._refreshLocalServerStatusSlot)
|
||||
Controller.instance().connection_failed_signal.connect(self._refreshLocalServerStatusSlot)
|
||||
@@ -116,9 +118,9 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
def _VMwareBannerButtonClickedSlot(self):
|
||||
if sys.platform.startswith("darwin"):
|
||||
url = "http://send.onenetworkdirect.net/z/616461/CD225091/"
|
||||
url = "http://send.onenetworkdirect.net/z/621395/CD225091/"
|
||||
else:
|
||||
url = "http://send.onenetworkdirect.net/z/616460/CD225091/"
|
||||
url = "http://send.onenetworkdirect.net/z/616207/CD225091/"
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
|
||||
|
||||
def _listVMwareVMsSlot(self):
|
||||
@@ -141,7 +143,6 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
Slot to refresh the VirtualBox VMs list.
|
||||
"""
|
||||
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM on VirtualBox", "VirtualBox doesn't support nested virtualization, this means running Qemu based VM could be very slow")
|
||||
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format(version=__version__)
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('If you don\'t have the GNS3 Virtual Machine you can <a href="{download_url}">download it here</a>.<br>And import the VM in the virtualization software and hit refresh.'.format(download_url=download_url))
|
||||
self.uiVmwareRadioButton.setChecked(False)
|
||||
@@ -201,6 +202,19 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
index = self.uiLocalServerHostComboBox.findData(local_server_settings["host"])
|
||||
if index != -1:
|
||||
self.uiLocalServerHostComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
if self.uiVMRadioButton.isChecked():
|
||||
# Try to bind with the IP address allocated for VMnet1
|
||||
for interface in interfaces():
|
||||
if "vmnet1" in interface["name"].lower():
|
||||
index = self.uiLocalServerHostComboBox.findText(interface["ip_address"])
|
||||
break
|
||||
else:
|
||||
index = self.uiLocalServerHostComboBox.findText(DEFAULT_LOCAL_SERVER_HOST)
|
||||
|
||||
if index != -1:
|
||||
self.uiLocalServerHostComboBox.setCurrentIndex(index)
|
||||
|
||||
self.uiLocalServerPortSpinBox.setValue(local_server_settings["port"])
|
||||
|
||||
elif self.page(page_id) == self.uiRemoteControllerWizardPage:
|
||||
@@ -216,7 +230,6 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["user"])
|
||||
self.uiRemoteMainServerPasswordLineEdit.setText(local_server_settings["password"])
|
||||
self.uiRemoteMainServerPortSpinBox.setValue(local_server_settings["port"])
|
||||
self.uiRemoteMainServerProtocolComboBox.setCurrentText(local_server_settings["protocol"])
|
||||
elif self.page(page_id) == self.uiLocalServerStatusWizardPage:
|
||||
self._refreshLocalServerStatusSlot()
|
||||
|
||||
@@ -246,14 +259,16 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
"""
|
||||
Refresh the local server status page
|
||||
"""
|
||||
|
||||
self.uiLocalServerTextEdit.clear()
|
||||
if Controller.instance().connected():
|
||||
self.uiLocalServerStatusLabel.setText("Connection to local server successful")
|
||||
self.uiLocalServerTextEdit.setText("Connection to the local GNS3 server has been successful!")
|
||||
Controller.instance().get("/gns3vm", self._getSettingsCallback)
|
||||
elif Controller.instance().connecting():
|
||||
self.uiLocalServerStatusLabel.setText("Please wait connection to the GNS3 server")
|
||||
self.uiLocalServerTextEdit.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"]))
|
||||
self.uiLocalServerTextEdit.setText("Connection to local server failed. Please try one of the following:\n\n- Make sure GNS3 is allowed to run by your firewall.\n- Go back and try to change the server host binding and/or the port\n- 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.".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
|
||||
@@ -264,7 +279,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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"]))
|
||||
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while saving settings: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def _addSummaryEntry(self, name, value):
|
||||
@@ -324,14 +339,15 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
return False
|
||||
|
||||
LocalServer.instance().updateLocalServerSettings(local_server_settings)
|
||||
LocalServer.instance().localServerAutoStartIfRequire()
|
||||
if not LocalServer.instance().localServerAutoStartIfRequired():
|
||||
return False
|
||||
|
||||
elif self.currentPage() == self.uiRemoteControllerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
local_server_settings["auto_start"] = False
|
||||
local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
|
||||
local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
|
||||
local_server_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText()
|
||||
local_server_settings["protocol"] = "http"
|
||||
local_server_settings["user"] = self.uiRemoteMainServerUserLineEdit.text()
|
||||
local_server_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
|
||||
local_server_settings["auth"] = self.uiRemoteMainServerAuthCheckBox.isChecked()
|
||||
@@ -402,8 +418,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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()
|
||||
|
||||
settings["hide_setup_wizard"] = not self.uiShowCheckBox.isChecked()
|
||||
self.parentWidget().setSettings(settings)
|
||||
super().done(result)
|
||||
|
||||
|
||||
@@ -85,12 +85,18 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.Normal, "Unnamed")
|
||||
if ok and snapshot_name and self._project:
|
||||
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()), self._createSnapshotsCallback, {"name": snapshot_name})
|
||||
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()
|
||||
|
||||
@@ -105,6 +111,7 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
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"])
|
||||
@@ -127,13 +134,16 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
:param snapshot_id: id of the snapshot
|
||||
"""
|
||||
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
|
||||
|
||||
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
|
||||
|
||||
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id), self._restoreSnapshotsCallback)
|
||||
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id),
|
||||
self._restoreSnapshotsCallback, progressText="Restoring snapshot...", timeout=None)
|
||||
|
||||
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
|
||||
@@ -21,6 +21,7 @@ Style editor to edit Shape items.
|
||||
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
|
||||
from ..items.shape_item import ShapeItem
|
||||
|
||||
|
||||
class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
@@ -52,12 +53,18 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
# use the first item in the list as the model
|
||||
first_item = items[0]
|
||||
pen = first_item.pen()
|
||||
brush = first_item.brush()
|
||||
self._color = brush.color()
|
||||
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
|
||||
self._color.green(),
|
||||
self._color.blue(),
|
||||
self._color.alpha()))
|
||||
if hasattr(first_item, "brush"): # Line don't have brush
|
||||
brush = first_item.brush()
|
||||
self._color = brush.color()
|
||||
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
|
||||
self._color.green(),
|
||||
self._color.blue(),
|
||||
self._color.alpha()))
|
||||
else:
|
||||
self.uiColorLabel.hide()
|
||||
self.uiColorPushButton.hide()
|
||||
self._color = None
|
||||
|
||||
self._border_color = pen.color()
|
||||
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
|
||||
self._border_color.green(),
|
||||
@@ -102,11 +109,17 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
border_style = QtCore.Qt.PenStyle(self.uiBorderStyleComboBox.itemData(self.uiBorderStyleComboBox.currentIndex()))
|
||||
pen = QtGui.QPen(self._border_color, self.uiBorderWidthSpinBox.value(), border_style, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
brush = QtGui.QBrush(self._color)
|
||||
if self._color:
|
||||
brush = QtGui.QBrush(self._color)
|
||||
else:
|
||||
brush = None
|
||||
|
||||
for item in self._items:
|
||||
item.setPen(pen)
|
||||
item.setBrush(brush)
|
||||
# on multiselection it's possible to select many type of items
|
||||
# but brush can be applied only on ShapeItem,
|
||||
if brush and isinstance(item, ShapeItem):
|
||||
item.setBrush(brush)
|
||||
item.setRotation(self.uiRotationSpinBox.value())
|
||||
|
||||
def done(self, result):
|
||||
|
||||
@@ -22,10 +22,9 @@ Dialog to change node symbols.
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qpartial, sip_is_deleted
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from ..ui.symbol_selection_dialog_ui import Ui_SymbolSelectionDialog
|
||||
from ..local_server import LocalServer
|
||||
from ..controller import Controller
|
||||
from ..symbol import Symbol
|
||||
|
||||
@@ -56,7 +55,6 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
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)
|
||||
|
||||
@@ -64,9 +62,10 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).hide()
|
||||
|
||||
self.uiBuiltInSymbolRadioButton.setChecked(True)
|
||||
self.uiSymbolListWidget.setFocus()
|
||||
self.uiSymbolListWidget.setIconSize(QtCore.QSize(64, 64))
|
||||
self.uiSymbolTreeWidget.setFocus()
|
||||
self.uiSymbolTreeWidget.setIconSize(QtCore.QSize(64, 64))
|
||||
self._symbol_items = []
|
||||
self._parents = {}
|
||||
|
||||
Controller.instance().get("/symbols", self._listSymbolsCallback)
|
||||
|
||||
@@ -78,33 +77,39 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
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)
|
||||
theme = symbol.theme()
|
||||
if theme not in self._parents:
|
||||
parent = QtWidgets.QTreeWidgetItem(self.uiSymbolTreeWidget)
|
||||
parent.setText(0, theme)
|
||||
font = parent.font(0)
|
||||
font.setBold(True)
|
||||
parent.setFont(0, font)
|
||||
parent.setFlags(parent.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
self._parents[theme] = parent
|
||||
else:
|
||||
parent = self._parents[theme]
|
||||
|
||||
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)
|
||||
name = os.path.splitext(symbol.filename())[0]
|
||||
item = QtWidgets.QTreeWidgetItem(parent)
|
||||
item.setData(0, QtCore.Qt.UserRole, symbol)
|
||||
item.setToolTip(0, symbol.id())
|
||||
self._symbol_items.append(item)
|
||||
item.setText(0, name)
|
||||
|
||||
def render(item, path):
|
||||
if sip_is_deleted(item):
|
||||
return
|
||||
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)
|
||||
svg_renderer.render(QtGui.QPainter(image))
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
|
||||
item.setIcon(icon)
|
||||
item.setIcon(0, icon)
|
||||
|
||||
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
|
||||
self.adjustSize()
|
||||
|
||||
def _builtinSymbolOnlyToggledSlot(self, checked):
|
||||
self._filter()
|
||||
|
||||
def _searchTextChangedSlot(self, text):
|
||||
self._filter()
|
||||
|
||||
@@ -114,10 +119,10 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
"""
|
||||
text = self.uiSearchLineEdit.text()
|
||||
for item in self._symbol_items:
|
||||
if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not item.data(QtCore.Qt.UserRole).builtin():
|
||||
if not item.data(0, QtCore.Qt.UserRole).builtin():
|
||||
item.setHidden(True)
|
||||
else:
|
||||
if len(text.strip()) == 0 or text.strip().lower() in item.text().lower():
|
||||
if len(text.strip()) == 0 or text.strip().lower() in item.text(0).lower():
|
||||
item.setHidden(False)
|
||||
else:
|
||||
item.setHidden(True)
|
||||
@@ -156,16 +161,18 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
"""
|
||||
|
||||
symbol_path = self.getSymbol()
|
||||
if not symbol_path:
|
||||
return False
|
||||
for item in self._items:
|
||||
item.setSymbol(symbol_path)
|
||||
return True
|
||||
|
||||
def getSymbol(self):
|
||||
|
||||
if self.uiSymbolListWidget.isEnabled():
|
||||
current = self.uiSymbolListWidget.currentItem()
|
||||
if current:
|
||||
return current.data(QtCore.Qt.UserRole).id()
|
||||
if self.uiSymbolTreeWidget.isEnabled():
|
||||
current = self.uiSymbolTreeWidget.currentItem()
|
||||
if current and current.parent():
|
||||
return current.data(0, QtCore.Qt.UserRole).id()
|
||||
else:
|
||||
return os.path.basename(self.uiSymbolLineEdit.text())
|
||||
return None
|
||||
@@ -184,7 +191,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
|
||||
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while uploading symbol: {}".format(path))
|
||||
log.error("Error while uploading symbol: {}: {}".format(path, result.get("message", "unknown")))
|
||||
return
|
||||
self.uiSymbolLineEdit.clear()
|
||||
self.uiSymbolLineEdit.setText(path)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
Text editor to edit Note items.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtWidgets, qslot
|
||||
from ..qt import QtCore, QtWidgets, qslot, sip_is_deleted
|
||||
from ..ui.text_editor_dialog_ui import Ui_TextEditorDialog
|
||||
|
||||
|
||||
@@ -98,6 +98,8 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
|
||||
"""
|
||||
|
||||
for item in self._items:
|
||||
if sip_is_deleted(item):
|
||||
continue
|
||||
item.setFont(self.uiPlainTextEdit.font())
|
||||
if self.uiApplyColorToAllItemsCheckBox.isChecked():
|
||||
item.setDefaultTextColor(self._color)
|
||||
|
||||
@@ -88,7 +88,7 @@ class VMWithImagesWizard(VMWizard):
|
||||
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)
|
||||
create_dialog = create_image_wizard(self, self.getSettings()["compute_id"], self.uiNameLineEdit.text() + image_suffix)
|
||||
if QtWidgets.QDialog.Accepted == create_dialog.exec_():
|
||||
line_edit.setText(create_dialog.uiLocationLineEdit.text())
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ Graphical view on the scene where items are drawn.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sip
|
||||
import pickle
|
||||
from .qt import sip
|
||||
import sys
|
||||
|
||||
from .qt import QtCore, QtGui, QtNetwork, QtWidgets, qpartial, qslot
|
||||
from .items.node_item import NodeItem
|
||||
@@ -31,38 +31,43 @@ from .link import Link
|
||||
from .node import Node
|
||||
from .modules import MODULES
|
||||
from .modules.module_error import ModuleError
|
||||
from .modules.builtin import Builtin
|
||||
from .settings import GRAPHICS_VIEW_SETTINGS
|
||||
from .topology import Topology
|
||||
from .template_manager import TemplateManager
|
||||
from .dialogs.style_editor_dialog import StyleEditorDialog
|
||||
from .dialogs.text_editor_dialog import TextEditorDialog
|
||||
from .dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from .dialogs.idlepc_dialog import IdlePCDialog
|
||||
from .dialogs.console_command_dialog import ConsoleCommandDialog
|
||||
from .dialogs.file_editor_dialog import FileEditorDialog
|
||||
from .dialogs.node_info_dialog import NodeInfoDialog
|
||||
from .local_config import LocalConfig
|
||||
from .progress import Progress
|
||||
from .utils.server_select import server_select
|
||||
from .compute_manager import ComputeManager
|
||||
from .utils.get_icon import get_icon
|
||||
|
||||
# link items
|
||||
from .items.link_item import LinkItem
|
||||
from .items.link_item import LinkItem, SvgIconItem
|
||||
from .items.ethernet_link_item import EthernetLinkItem
|
||||
from .items.serial_link_item import SerialLinkItem
|
||||
|
||||
# other items
|
||||
from .items.note_item import NoteItem
|
||||
from .items.label_item import LabelItem
|
||||
from .items.text_item import TextItem
|
||||
from .items.shape_item import ShapeItem
|
||||
from .items.drawing_item import DrawingItem
|
||||
from .items.rectangle_item import RectangleItem
|
||||
from .items.line_item import LineItem
|
||||
from .items.ellipse_item import EllipseItem
|
||||
from .items.image_item import ImageItem
|
||||
from .items.logo_item import LogoItem
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
"""
|
||||
Graphics view that displays the scene.
|
||||
|
||||
@@ -82,8 +87,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_note = False
|
||||
self._adding_rectangle = False
|
||||
self._adding_ellipse = False
|
||||
self._adding_line = False
|
||||
self._newlink = None
|
||||
self._dragging = False
|
||||
self._grid_size = 75
|
||||
self._drawing_grid_size = 25
|
||||
self._last_mouse_position = None
|
||||
self._topology = Topology.instance()
|
||||
self._background_warning_msgbox = QtWidgets.QErrorMessage(self)
|
||||
@@ -103,16 +111,49 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter)
|
||||
|
||||
# default directories for QFileDialog
|
||||
self._import_configs_from_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
|
||||
self._import_config_dir = ""
|
||||
self._export_configs_to_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
|
||||
self._export_config_dir = ""
|
||||
|
||||
self._import_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
|
||||
self._export_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
|
||||
self._local_addresses = ['0.0.0.0', '127.0.0.1', 'localhost', '::1', '0:0:0:0:0:0:0:1', '::', QtNetwork.QHostInfo.localHostName()]
|
||||
|
||||
def setSceneSize(self, width, height):
|
||||
self.scene().setSceneRect(-(width / 2), -(height / 2), width, height)
|
||||
|
||||
def setZoom(self, zoom):
|
||||
"""
|
||||
Sets zoom of the Graphics View
|
||||
:param zoom:
|
||||
:return:
|
||||
"""
|
||||
if zoom:
|
||||
factor = zoom / 100.
|
||||
self.scale(factor, factor)
|
||||
|
||||
def setNodeGridSize(self, grid_size):
|
||||
"""
|
||||
Sets the grid size for nodes.
|
||||
"""
|
||||
self._grid_size = grid_size
|
||||
|
||||
def nodeGridSize(self):
|
||||
"""
|
||||
Returns the grid size for nodes.
|
||||
:return: integer
|
||||
"""
|
||||
return self._grid_size
|
||||
|
||||
def setDrawingGridSize(self, grid_size):
|
||||
"""
|
||||
Sets the grid size for drawings
|
||||
"""
|
||||
self._drawing_grid_size = grid_size
|
||||
|
||||
def drawingGridSize(self):
|
||||
"""
|
||||
Returns the grid size for drawings
|
||||
:return: integer
|
||||
"""
|
||||
return self._drawing_grid_size
|
||||
|
||||
def setEnabled(self, enabled):
|
||||
|
||||
if enabled is False:
|
||||
@@ -122,6 +163,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self.scene().addItem(item)
|
||||
super().setEnabled(enabled)
|
||||
|
||||
self.toggleUiDeviceMenu()
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Remove all the items from the scene and
|
||||
@@ -144,9 +187,13 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# clear the topology summary
|
||||
self._main_window.uiTopologySummaryTreeWidget.clear()
|
||||
|
||||
# reset the lock button
|
||||
self._main_window.uiLockAllAction.setChecked(False)
|
||||
|
||||
# clear all objects on the scene
|
||||
self.scene().clear()
|
||||
|
||||
|
||||
def _loadSettings(self):
|
||||
"""
|
||||
Loads the settings from the persistent settings file.
|
||||
@@ -233,6 +280,20 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_ellipse = False
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addLine(self, state):
|
||||
"""
|
||||
Adds a line.
|
||||
|
||||
:param state: boolean
|
||||
"""
|
||||
|
||||
if state:
|
||||
self._adding_line = True
|
||||
self.setCursor(QtCore.Qt.PointingHandCursor)
|
||||
else:
|
||||
self._adding_line = False
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addImage(self, image_path):
|
||||
"""
|
||||
Adds an image.
|
||||
@@ -245,6 +306,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self.scene().addItem(image_item)
|
||||
self._topology.addDrawing(image_item)
|
||||
|
||||
def addLogo(self, logo_path, logo_url):
|
||||
logo_item = LogoItem(logo_path, logo_url, self._topology.project())
|
||||
self.scene().addItem(logo_item)
|
||||
|
||||
def addLink(self, source_node, source_port, destination_node, destination_port, **link_data):
|
||||
"""
|
||||
Creates a Link instance representing a connection between 2 devices.
|
||||
@@ -365,12 +430,16 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
is_not_link = True
|
||||
is_not_logo = True
|
||||
|
||||
item = self.itemAt(event.pos())
|
||||
if item and sip.isdeleted(item):
|
||||
return
|
||||
|
||||
if item and (isinstance(item, LinkItem) or isinstance(item.parentItem(), LinkItem)):
|
||||
is_not_link = False
|
||||
if item and (isinstance(item, LogoItem) or isinstance(item.parentItem(), LogoItem)):
|
||||
is_not_logo = False
|
||||
else:
|
||||
for it in self.scene().items():
|
||||
if isinstance(it, LinkItem):
|
||||
@@ -390,20 +459,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
item.setSelected(False)
|
||||
else:
|
||||
item.setSelected(True)
|
||||
elif is_not_link and event.button() == QtCore.Qt.RightButton and not self._adding_link:
|
||||
elif is_not_link and is_not_logo and event.button() == QtCore.Qt.RightButton and not self._adding_link:
|
||||
if item and not sip.isdeleted(item):
|
||||
# Prevent right clicking on a selected item from de-selecting all other items
|
||||
if not item.isSelected():
|
||||
if not event.modifiers() & QtCore.Qt.ControlModifier:
|
||||
for it in self.scene().items():
|
||||
it.setSelected(False)
|
||||
if item.zValue() < 0:
|
||||
item.setFlag(item.ItemIsSelectable, True)
|
||||
item.setSelected(True)
|
||||
self._showDeviceContextualMenu(QtGui.QCursor.pos())
|
||||
if not sip.isdeleted(item) and item.zValue() < 0:
|
||||
item.setFlag(item.ItemIsSelectable, False)
|
||||
|
||||
else:
|
||||
self._showDeviceContextualMenu(QtGui.QCursor.pos())
|
||||
# when more than one item is selected display the contextual menu even if mouse is not above an item
|
||||
@@ -417,7 +481,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._userNodeLinking(event, item)
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_note:
|
||||
pos = self.mapToScene(event.pos())
|
||||
note = self.createDrawingItem("text", pos.x(), pos.y(), 1)
|
||||
note = self.createDrawingItem("text", pos.x(), pos.y(), 2)
|
||||
pos_x = note.pos().x()
|
||||
pos_y = note.pos().y() - (note.boundingRect().height() / 2)
|
||||
note.setPos(pos_x, pos_y)
|
||||
@@ -427,19 +491,27 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_note = False
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_rectangle:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("rect", pos.x(), pos.y(), 0)
|
||||
self.createDrawingItem("rect", pos.x(), pos.y(), 1)
|
||||
self._main_window.uiDrawRectangleAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_rectangle = False
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_ellipse:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("ellipse", pos.x(), pos.y(), 0)
|
||||
self.createDrawingItem("ellipse", pos.x(), pos.y(), 1)
|
||||
self._main_window.uiDrawEllipseAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_ellipse = False
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_line:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("line", pos.x(), pos.y(), 1)
|
||||
self._main_window.uiDrawLineAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_line = False
|
||||
else:
|
||||
super().mousePressEvent(event)
|
||||
|
||||
self.toggleUiDeviceMenu()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""
|
||||
Handles all mouse release events.
|
||||
@@ -462,6 +534,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
item.setSelected(True)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
self.toggleUiDeviceMenu()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
"""
|
||||
Handles zoom in or out using the mouse wheel.
|
||||
@@ -474,6 +548,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if delta is not None and delta.x() == 0:
|
||||
# CTRL is pressed then use the mouse wheel to zoom in or out.
|
||||
self.scaleView(pow(2.0, delta.y() / 240.0))
|
||||
self._topology.project().setZoom(round(self.transform().m11() * 100))
|
||||
self._topology.project().update()
|
||||
else:
|
||||
super().wheelEvent(event)
|
||||
|
||||
@@ -486,6 +562,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if factor < 0.10 or factor > 10:
|
||||
return
|
||||
self.scale(scale_factor, scale_factor)
|
||||
self._main_window.uiStatusBar.showMessage("Zoom: {}%".format(round(self.transform().m11() * 100)), 2000)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
@@ -495,9 +572,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
if event.key() == QtCore.Qt.Key_Delete:
|
||||
# check if we are editing an NoteItem instance, then send the delete key event to it
|
||||
# check if we are editing an LabelItem instance, then send the delete key event to it
|
||||
for item in self.scene().selectedItems():
|
||||
if (isinstance(item, NoteItem) or isinstance(item, TextItem)) and item.hasFocus():
|
||||
if (isinstance(item, LabelItem) or isinstance(item, TextItem)) and item.hasFocus():
|
||||
super().keyPressEvent(event)
|
||||
return
|
||||
self.deleteActionSlot()
|
||||
@@ -554,9 +631,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self.configureSlot()
|
||||
return
|
||||
else:
|
||||
if sys.platform.startswith("win") and item.node().bringToFront():
|
||||
return
|
||||
self.consoleFromItems(self.scene().selectedItems())
|
||||
return
|
||||
elif isinstance(item, NoteItem) and isinstance(item.parentItem(), NodeItem):
|
||||
elif isinstance(item, LabelItem) and isinstance(item.parentItem(), NodeItem):
|
||||
if item.parentItem().node().initialized():
|
||||
item.parentItem().setSelected(True)
|
||||
self.changeHostnameActionSlot()
|
||||
@@ -587,7 +666,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
# check if what is dragged is handled by this view
|
||||
if event.mimeData().hasFormat("application/x-gns3-node") or event.mimeData().hasFormat("text/uri-list"):
|
||||
if event.mimeData().hasFormat("text/uri-list") \
|
||||
or event.mimeData().hasFormat("application/x-gns3-template"):
|
||||
event.acceptProposedAction()
|
||||
event.accept()
|
||||
else:
|
||||
@@ -601,10 +681,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
# check if what has been dropped is handled by this view
|
||||
if event.mimeData().hasFormat("application/x-gns3-node"):
|
||||
data = event.mimeData().data("application/x-gns3-node")
|
||||
# load the pickled node data
|
||||
node_data = pickle.loads(data)
|
||||
if event.mimeData().hasFormat("application/x-gns3-template"):
|
||||
template_id = event.mimeData().data("application/x-gns3-template").data().decode()
|
||||
event.setDropAction(QtCore.Qt.CopyAction)
|
||||
event.accept()
|
||||
if event.keyboardModifiers() == QtCore.Qt.ShiftModifier:
|
||||
@@ -615,12 +693,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
for node_number in range(integer):
|
||||
x = event.pos().x() - (150 / 2) + (node_number % max_nodes_per_line) * offset
|
||||
y = event.pos().y() - (70 / 2) + (node_number // max_nodes_per_line) * offset
|
||||
node_item = self.createNode(node_data, QtCore.QPoint(x, y))
|
||||
if node_item is None:
|
||||
# stop if there is any error
|
||||
if self.createNodeFromTemplateId(template_id, QtCore.QPoint(x, y)) is False:
|
||||
event.ignore()
|
||||
break
|
||||
else:
|
||||
self.createNode(node_data, event.pos())
|
||||
if self.createNodeFromTemplateId(template_id, event.pos()) is False:
|
||||
event.ignore()
|
||||
elif event.mimeData().hasFormat("text/uri-list") and event.mimeData().hasUrls():
|
||||
# This should not arrive but we received bug report with it...
|
||||
if len(event.mimeData().urls()) == 0:
|
||||
@@ -659,139 +737,147 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
return
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configPage"), items)):
|
||||
# Action: Configure node
|
||||
configure_action = QtWidgets.QAction("Configure", menu)
|
||||
configure_action.setIcon(QtGui.QIcon(':/icons/configuration.svg'))
|
||||
configure_action.setIcon(get_icon("configuration.svg"))
|
||||
configure_action.triggered.connect(self.configureActionSlot)
|
||||
menu.addAction(configure_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and item.node().console() is not None, items)):
|
||||
console_action = QtWidgets.QAction("Console", menu)
|
||||
console_action.setIcon(get_icon("console.svg"))
|
||||
console_action.triggered.connect(self.consoleActionSlot)
|
||||
menu.addAction(console_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole"), items)):
|
||||
aux_console_action = QtWidgets.QAction("Auxiliary console", menu)
|
||||
aux_console_action.setIcon(get_icon("aux-console.svg"))
|
||||
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
|
||||
menu.addAction(aux_console_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
start_action = QtWidgets.QAction("Start", menu)
|
||||
start_action.setIcon(get_icon("start.svg"))
|
||||
start_action.triggered.connect(self.startActionSlot)
|
||||
menu.addAction(start_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
suspend_action = QtWidgets.QAction("Suspend", menu)
|
||||
suspend_action.setIcon(get_icon("pause.svg"))
|
||||
suspend_action.triggered.connect(self.suspendActionSlot)
|
||||
menu.addAction(suspend_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
stop_action = QtWidgets.QAction("Stop", menu)
|
||||
stop_action.setIcon(get_icon("stop.svg"))
|
||||
stop_action.triggered.connect(self.stopActionSlot)
|
||||
menu.addAction(stop_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
reload_action = QtWidgets.QAction("Reload", menu)
|
||||
reload_action.setIcon(get_icon("reload.svg"))
|
||||
reload_action.triggered.connect(self.reloadActionSlot)
|
||||
menu.addAction(reload_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and item.node().console() is not None, items)):
|
||||
console_edit_action = QtWidgets.QAction("Custom console", menu)
|
||||
console_edit_action.setIcon(get_icon("console_edit.svg"))
|
||||
console_edit_action.triggered.connect(self.customConsoleActionSlot)
|
||||
menu.addAction(console_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
|
||||
# Action: Change hostname
|
||||
change_hostname_action = QtWidgets.QAction("Change hostname", menu)
|
||||
change_hostname_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
|
||||
change_hostname_action.setIcon(get_icon("show-hostname.svg"))
|
||||
change_hostname_action.triggered.connect(self.changeHostnameActionSlot)
|
||||
menu.addAction(change_hostname_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
|
||||
# Action: Change symbol
|
||||
change_symbol_action = QtWidgets.QAction("Change symbol", menu)
|
||||
change_symbol_action.setIcon(QtGui.QIcon(':/icons/node_conception.svg'))
|
||||
change_symbol_action.setIcon(get_icon("node_conception.svg"))
|
||||
change_symbol_action.triggered.connect(self.changeSymbolActionSlot)
|
||||
menu.addAction(change_symbol_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, DrawingItem) or isinstance(item, NodeItem), items)):
|
||||
duplicate_action = QtWidgets.QAction("Duplicate", menu)
|
||||
duplicate_action.setIcon(get_icon("duplicate.svg"))
|
||||
duplicate_action.triggered.connect(self.duplicateActionSlot)
|
||||
menu.addAction(duplicate_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "info"), items)):
|
||||
# Action: Show node information
|
||||
show_node_info_action = QtWidgets.QAction("Show node information", menu)
|
||||
show_node_info_action.setIcon(get_icon("help.svg"))
|
||||
show_node_info_action.triggered.connect(self.showNodeInfoSlot)
|
||||
menu.addAction(show_node_info_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "nodeDir"), items)):
|
||||
# Action: Show in file manager
|
||||
show_in_file_manager_action = QtWidgets.QAction("Show in file manager", menu)
|
||||
show_in_file_manager_action.setIcon(QtGui.QIcon(':/icons/open.svg'))
|
||||
show_in_file_manager_action.setIcon(get_icon("open.svg"))
|
||||
show_in_file_manager_action.triggered.connect(self.showInFileManagerSlot)
|
||||
menu.addAction(show_in_file_manager_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "console"), items)):
|
||||
console_action = QtWidgets.QAction("Console", menu)
|
||||
console_action.setIcon(QtGui.QIcon(':/icons/console.svg'))
|
||||
console_action.triggered.connect(self.consoleActionSlot)
|
||||
menu.addAction(console_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "console"), items)):
|
||||
console_edit_action = QtWidgets.QAction("Custom console", menu)
|
||||
console_edit_action.setIcon(QtGui.QIcon(':/icons/console_edit.svg'))
|
||||
console_edit_action.triggered.connect(self.customConsoleActionSlot)
|
||||
menu.addAction(console_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole"), items)):
|
||||
aux_console_action = QtWidgets.QAction("Auxiliary console", menu)
|
||||
aux_console_action.setIcon(QtGui.QIcon(':/icons/aux-console.svg'))
|
||||
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
|
||||
menu.addAction(aux_console_action)
|
||||
if sys.platform.startswith("win") and True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"), items)):
|
||||
# Action: bring console or window to front (Windows only)
|
||||
bring_to_front_action = QtWidgets.QAction("Bring to front", menu)
|
||||
bring_to_front_action.setIcon(get_icon("front.svg"))
|
||||
bring_to_front_action.triggered.connect(self.bringToFrontSlot)
|
||||
menu.addAction(bring_to_front_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
|
||||
import_config_action = QtWidgets.QAction("Import config", menu)
|
||||
import_config_action.setIcon(QtGui.QIcon(':/icons/import_config.svg'))
|
||||
import_config_action.setIcon(get_icon("import.svg"))
|
||||
import_config_action.triggered.connect(self.importConfigActionSlot)
|
||||
menu.addAction(import_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
|
||||
export_config_action = QtWidgets.QAction("Export config", menu)
|
||||
export_config_action.setIcon(QtGui.QIcon(':/icons/export_config.svg'))
|
||||
export_config_action.setIcon(get_icon("export.svg"))
|
||||
export_config_action.triggered.connect(self.exportConfigActionSlot)
|
||||
menu.addAction(export_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
|
||||
export_config_action = QtWidgets.QAction("Edit config", menu)
|
||||
export_config_action.setIcon(QtGui.QIcon(':/icons/edit.svg'))
|
||||
export_config_action.setIcon(get_icon("edit.svg"))
|
||||
export_config_action.triggered.connect(self.editConfigActionSlot)
|
||||
menu.addAction(export_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "idlepc"), items)):
|
||||
idlepc_action = QtWidgets.QAction("Idle-PC", menu)
|
||||
idlepc_action.setIcon(QtGui.QIcon(':/icons/calculate.svg'))
|
||||
idlepc_action.setIcon(get_icon("calculate.svg"))
|
||||
idlepc_action.triggered.connect(self.idlepcActionSlot)
|
||||
menu.addAction(idlepc_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "idlepc"), items)):
|
||||
auto_idlepc_action = QtWidgets.QAction("Auto Idle-PC", menu)
|
||||
auto_idlepc_action.setIcon(QtGui.QIcon(':/icons/calculate.svg'))
|
||||
auto_idlepc_action.setIcon(get_icon("calculate.svg"))
|
||||
auto_idlepc_action.triggered.connect(self.autoIdlepcActionSlot)
|
||||
menu.addAction(auto_idlepc_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
start_action = QtWidgets.QAction("Start", menu)
|
||||
start_action.setIcon(QtGui.QIcon(':/icons/start.svg'))
|
||||
start_action.triggered.connect(self.startActionSlot)
|
||||
menu.addAction(start_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
suspend_action = QtWidgets.QAction("Suspend", menu)
|
||||
suspend_action.setIcon(QtGui.QIcon(':/icons/pause.svg'))
|
||||
suspend_action.triggered.connect(self.suspendActionSlot)
|
||||
menu.addAction(suspend_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
stop_action = QtWidgets.QAction("Stop", menu)
|
||||
stop_action.setIcon(QtGui.QIcon(':/icons/stop.svg'))
|
||||
stop_action.triggered.connect(self.stopActionSlot)
|
||||
menu.addAction(stop_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
reload_action = QtWidgets.QAction("Reload", menu)
|
||||
reload_action.setIcon(QtGui.QIcon(':/icons/reload.svg'))
|
||||
reload_action.triggered.connect(self.reloadActionSlot)
|
||||
menu.addAction(reload_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, DrawingItem), items)):
|
||||
duplicate_action = QtWidgets.QAction("Duplicate", menu)
|
||||
duplicate_action.setIcon(QtGui.QIcon(':/icons/new.svg'))
|
||||
duplicate_action.triggered.connect(self.duplicateActionSlot)
|
||||
menu.addAction(duplicate_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NoteItem), items)):
|
||||
if True in list(map(lambda item: isinstance(item, LabelItem), items)):
|
||||
text_edit_action = QtWidgets.QAction("Text edit", menu)
|
||||
text_edit_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
|
||||
text_edit_action.setIcon(get_icon("show-hostname.svg"))
|
||||
text_edit_action.triggered.connect(self.textEditActionSlot)
|
||||
menu.addAction(text_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, TextItem), items)):
|
||||
text_edit_action = QtWidgets.QAction("Text edit", menu)
|
||||
text_edit_action.setIcon(QtGui.QIcon(':/icons/edit.svg'))
|
||||
text_edit_action.setIcon(get_icon("edit.svg"))
|
||||
text_edit_action.triggered.connect(self.textEditActionSlot)
|
||||
menu.addAction(text_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, ShapeItem), items)):
|
||||
if True in list(map(lambda item: isinstance(item, ShapeItem) or isinstance(item, LineItem), items)):
|
||||
style_action = QtWidgets.QAction("Style", menu)
|
||||
style_action.setIcon(QtGui.QIcon(':/icons/drawing.svg'))
|
||||
style_action.setIcon(get_icon("node_conception.svg"))
|
||||
style_action.triggered.connect(self.styleActionSlot)
|
||||
menu.addAction(style_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "commandLine"), items)):
|
||||
# Action: Get command line
|
||||
show_in_file_manager_action = QtWidgets.QAction("Command line", menu)
|
||||
show_in_file_manager_action.setIcon(QtGui.QIcon(':/icons/console.svg'))
|
||||
show_in_file_manager_action.triggered.connect(self.getCommandLineSlot)
|
||||
menu.addAction(show_in_file_manager_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NoteItem), items)) and False in list(map(lambda item: item.parentItem() is None, items)):
|
||||
if True in list(map(lambda item: isinstance(item, LabelItem), items)) and False in list(map(lambda item: item.parentItem() is None, items)):
|
||||
# action only for port labels
|
||||
reset_label_position_action = QtWidgets.QAction("Reset position", menu)
|
||||
reset_label_position_action.setIcon(QtGui.QIcon(':/icons/reset.svg'))
|
||||
reset_label_position_action.setIcon(get_icon("reset.svg"))
|
||||
reset_label_position_action.triggered.connect(self.resetLabelPositionActionSlot)
|
||||
menu.addAction(reset_label_position_action)
|
||||
|
||||
@@ -800,27 +886,41 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
if len(items) > 1:
|
||||
horizontal_align_action = QtWidgets.QAction("Align horizontally", menu)
|
||||
horizontal_align_action.setIcon(QtGui.QIcon(':/icons/horizontally.svg'))
|
||||
horizontal_align_action.setIcon(get_icon("horizontally.svg"))
|
||||
horizontal_align_action.triggered.connect(self.horizontalAlignmentSlot)
|
||||
menu.addAction(horizontal_align_action)
|
||||
|
||||
vertical_align_action = QtWidgets.QAction("Align vertically", menu)
|
||||
vertical_align_action.setIcon(QtGui.QIcon(':/icons/vertically.svg'))
|
||||
vertical_align_action.setIcon(get_icon("vertically.svg"))
|
||||
vertical_align_action.triggered.connect(self.verticalAlignmentSlot)
|
||||
menu.addAction(vertical_align_action)
|
||||
|
||||
raise_layer_action = QtWidgets.QAction("Raise one layer", menu)
|
||||
raise_layer_action.setIcon(QtGui.QIcon(':/icons/raise_z_value.svg'))
|
||||
raise_layer_action.setIcon(get_icon("raise_z_value.svg"))
|
||||
raise_layer_action.triggered.connect(self.raiseLayerActionSlot)
|
||||
menu.addAction(raise_layer_action)
|
||||
|
||||
lower_layer_action = QtWidgets.QAction("Lower one layer", menu)
|
||||
lower_layer_action.setIcon(QtGui.QIcon(':/icons/lower_z_value.svg'))
|
||||
lower_layer_action.setIcon(get_icon("lower_z_value.svg"))
|
||||
lower_layer_action.triggered.connect(self.lowerLayerActionSlot)
|
||||
menu.addAction(lower_layer_action)
|
||||
|
||||
if len(items) > 1:
|
||||
lock_action = QtWidgets.QAction("Lock or unlock items", menu)
|
||||
lock_action.setIcon(get_icon("lock.svg"))
|
||||
else:
|
||||
item = items[0]
|
||||
if item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable:
|
||||
lock_action = QtWidgets.QAction("Lock item", menu)
|
||||
lock_action.setIcon(get_icon("lock.svg"))
|
||||
else:
|
||||
lock_action = QtWidgets.QAction("Unlock item", menu)
|
||||
lock_action.setIcon(get_icon("unlock.svg"))
|
||||
lock_action.triggered.connect(self.lockActionSlot)
|
||||
menu.addAction(lock_action)
|
||||
|
||||
delete_action = QtWidgets.QAction("Delete", menu)
|
||||
delete_action.setIcon(QtGui.QIcon(':/icons/delete.svg'))
|
||||
delete_action.setIcon(get_icon("delete.svg"))
|
||||
delete_action.triggered.connect(self.deleteActionSlot)
|
||||
menu.addAction(delete_action)
|
||||
|
||||
@@ -952,6 +1052,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# returns True to ignore this node.
|
||||
return True
|
||||
|
||||
# TightVNC has lack support of IPv6 host at this moment
|
||||
if "vncviewer" in node.consoleCommand() and ":" in node.consoleHost():
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self, "TightVNC", "TightVNC (vncviewer) may not start because of lack of IPv6 support.")
|
||||
|
||||
try:
|
||||
node.openConsole(aux=aux)
|
||||
except (OSError, ValueError) as e:
|
||||
@@ -969,7 +1074,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
nodes = {}
|
||||
node_initialized = False
|
||||
for item in items:
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized():
|
||||
if isinstance(item, NodeItem) and item.node().console() is not None and item.node().initialized():
|
||||
node_initialized = True
|
||||
if item.node().status() == Node.started:
|
||||
node = item.node()
|
||||
@@ -989,6 +1094,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._main_window.run_later(counter, callback)
|
||||
counter += delay
|
||||
|
||||
def consoleFromAllItems(self):
|
||||
"""
|
||||
Console from all scene items, except builtin devices.
|
||||
"""
|
||||
|
||||
items = [item for item in self.scene().items()
|
||||
if not (isinstance(item, NodeItem) and isinstance(item.node().module(), Builtin))]
|
||||
self.consoleFromItems(items)
|
||||
|
||||
def consoleActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the console action in the
|
||||
@@ -1005,8 +1119,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
current_cmd = None
|
||||
console_type = "telnet"
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
|
||||
if item.node().consoleType() not in ("telnet", "serial", "vnc"):
|
||||
if isinstance(item, NodeItem) and item.node().console() is not None and item.node().initialized() and item.node().status() == Node.started:
|
||||
if item.node().consoleType() not in ("telnet", "serial", "vnc", "spice", "spice+agent"):
|
||||
continue
|
||||
current_cmd = item.node().consoleCommand()
|
||||
console_type = item.node().consoleType()
|
||||
@@ -1014,9 +1128,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
(ok, cmd) = ConsoleCommandDialog.getCommand(self, console_type=console_type, current=current_cmd)
|
||||
if ok:
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
|
||||
if isinstance(item, NodeItem) and item.node().console() is not None and item.node().initialized() and item.node().status() == Node.started:
|
||||
node = item.node()
|
||||
if node.consoleType() not in ("telnet", "serial", "vnc"):
|
||||
if node.consoleType() not in ("telnet", "serial", "vnc", "spice", "spice+agent"):
|
||||
continue
|
||||
try:
|
||||
node.openConsole(command=cmd)
|
||||
@@ -1083,15 +1197,14 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if not ok:
|
||||
continue
|
||||
|
||||
if not self._import_config_dir:
|
||||
self._import_config_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
|
||||
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self,
|
||||
"Import {}".format(os.path.basename(config_file)),
|
||||
self._import_config_dir,
|
||||
self._import_config_directory,
|
||||
"All files (*.*);;Config files (*.cfg)",
|
||||
"Config files (*.cfg)")
|
||||
self._import_config_dir = os.path.dirname(path)
|
||||
if not path:
|
||||
continue
|
||||
self._import_config_directory = os.path.dirname(path)
|
||||
item.node().importFile(config_file, path)
|
||||
|
||||
def editConfigActionSlot(self):
|
||||
@@ -1132,44 +1245,39 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if not items:
|
||||
return
|
||||
|
||||
if not self._export_configs_to_dir:
|
||||
self._export_configs_to_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
|
||||
|
||||
for item in items:
|
||||
for config_file in item.node().configFiles():
|
||||
path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_configs_to_dir, item.node().name() + "_" + os.path.basename(config_file)), "All files (*.*);;Config files (*.cfg)")
|
||||
|
||||
path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_config_directory, item.node().name() + "_" + os.path.basename(config_file)), "All files (*.*);;Config files (*.cfg)")
|
||||
if not path:
|
||||
continue
|
||||
|
||||
self._export_configs_to_dir = os.path.dirname(path)
|
||||
|
||||
self._export_config_directory = os.path.dirname(path)
|
||||
item.node().exportFile(config_file, path)
|
||||
|
||||
def getCommandLineSlot(self):
|
||||
def showNodeInfoSlot(self):
|
||||
"""
|
||||
Slot to receive events from the get command line action in the
|
||||
Slot to receive events from the show node info action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
items = self.scene().selectedItems()
|
||||
if len(items) != 1:
|
||||
QtWidgets.QMessageBox.critical(self, "Command line", "Please select only one router")
|
||||
QtWidgets.QMessageBox.critical(self, "Show node information", "Please select only one node")
|
||||
return
|
||||
item = items[0]
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "commandLine"):
|
||||
router = item.node()
|
||||
if router.commandLine() is None:
|
||||
QtWidgets.QMessageBox.warning(self, "Command line", "Get command line is not supported for this type of node.")
|
||||
elif router.commandLine() == '':
|
||||
QtWidgets.QMessageBox.warning(self, "Command line", "Please start the node in order to get the command line.")
|
||||
else:
|
||||
dialog = QtWidgets.QInputDialog(self)
|
||||
dialog.setOptions(QtWidgets.QInputDialog.NoButtons)
|
||||
dialog.setLabelText("Command used to start the VM:")
|
||||
dialog.setTextValue(router.commandLine())
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
if isinstance(item, NodeItem):
|
||||
dialog = NodeInfoDialog(item.node(), parent=self)
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
def bringToFrontSlot(self):
|
||||
"""
|
||||
Slot to receive events from the bring to front action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"):
|
||||
item.node().bringToFront()
|
||||
|
||||
def idlepcActionSlot(self):
|
||||
"""
|
||||
@@ -1195,7 +1303,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC", "Error: {}".format(result["message"]))
|
||||
else:
|
||||
router = context["router"]
|
||||
log.info("{} has received Idle-PC proposals".format(router.name()))
|
||||
log.debug("{} has received Idle-PC proposals".format(router.name()))
|
||||
idlepcs = result
|
||||
if idlepcs and idlepcs[0] != "0x0":
|
||||
dialog = IdlePCDialog(router, idlepcs, parent=self)
|
||||
@@ -1229,7 +1337,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
router = context["router"]
|
||||
idlepc = result["idlepc"]
|
||||
log.info("{} has received the auto idle-pc value: {}".format(router.name(), idlepc))
|
||||
log.debug("{} has received the auto idle-pc value: {}".format(router.name(), idlepc))
|
||||
router.setIdlepc(idlepc)
|
||||
# apply Idle-PC to all routers with the same IOS image
|
||||
ios_image = os.path.basename(router.settings()["image"])
|
||||
@@ -1238,9 +1346,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
node.setIdlepc(idlepc)
|
||||
# apply the idle-pc to templates with the same IOS image
|
||||
router.module().updateImageIdlepc(ios_image, idlepc)
|
||||
QtWidgets.QMessageBox.information(self, "Auto Idle-PC", "Idle-PC value {} has been applied on {} and all routers with IOS image {}".format(idlepc,
|
||||
router.name(),
|
||||
ios_image))
|
||||
QtWidgets.QMessageBox.information(self, "Auto Idle-PC", "Idle-PC value {} has been applied on {} and all templates with IOS image {}".format(idlepc,
|
||||
router.name(),
|
||||
ios_image))
|
||||
|
||||
def duplicateActionSlot(self):
|
||||
"""
|
||||
@@ -1259,6 +1367,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
type = "image"
|
||||
self.createDrawingItem(type, item.pos().x() + 20, item.pos().y() + 20, item.zValue(), rotation=item.rotation(), svg=item.toSvg())
|
||||
elif isinstance(item, NodeItem):
|
||||
item.node().duplicate(item.pos().x() + 20, item.pos().y() + 20, item.zValue())
|
||||
|
||||
def styleActionSlot(self):
|
||||
"""
|
||||
@@ -1268,7 +1378,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, ShapeItem):
|
||||
if isinstance(item, ShapeItem) or isinstance(item, LineItem):
|
||||
items.append(item)
|
||||
if items:
|
||||
style_dialog = StyleEditorDialog(self._main_window, items)
|
||||
@@ -1283,7 +1393,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NoteItem) or isinstance(item, TextItem):
|
||||
if isinstance(item, LabelItem) or isinstance(item, TextItem):
|
||||
items.append(item)
|
||||
if items:
|
||||
text_edit_dialog = TextEditorDialog(self._main_window, items)
|
||||
@@ -1297,7 +1407,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NoteItem) and item.parentItem():
|
||||
if isinstance(item, LabelItem) and item.parentItem():
|
||||
links = item.parentItem().links()
|
||||
for port in item.parentItem().node().ports():
|
||||
# find the correct port associated with the label
|
||||
@@ -1346,8 +1456,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if item.parentItem() is None:
|
||||
current_zvalue = item.zValue()
|
||||
item.setZValue(current_zvalue + 1)
|
||||
if not (item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable):
|
||||
log.error("Cannot move object to a upper layer because it is locked")
|
||||
continue
|
||||
item.setZValue(item.zValue() + 1)
|
||||
item.updateNode()
|
||||
item.update()
|
||||
|
||||
@@ -1359,13 +1471,28 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if item.parentItem() is None:
|
||||
current_zvalue = item.zValue()
|
||||
item.setZValue(current_zvalue - 1)
|
||||
if not (item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable):
|
||||
log.error("Cannot move object to a lower layer because it is locked")
|
||||
continue
|
||||
item.setZValue(item.zValue() - 1)
|
||||
item.updateNode()
|
||||
item.update()
|
||||
|
||||
if item.zValue() == -1:
|
||||
self._background_warning_msgbox.showMessage("Object moved to a background layer. You will now have to use the right-click action to select this object in the future and raise it to layer 0 to be able to move it")
|
||||
def lockActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the lock action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if not isinstance(item, LinkItem) and not isinstance(item, LabelItem) and not isinstance(item, SvgIconItem):
|
||||
if item.locked() is True:
|
||||
item.setLocked(False)
|
||||
else:
|
||||
item.setLocked(True)
|
||||
if item.parentItem() is None:
|
||||
item.updateNode()
|
||||
item.update()
|
||||
|
||||
def deleteActionSlot(self):
|
||||
"""
|
||||
@@ -1376,7 +1503,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
selected_nodes = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem):
|
||||
selected_nodes.append(item.node())
|
||||
node = item.node()
|
||||
if node.locked():
|
||||
QtWidgets.QMessageBox.critical(self, "Delete", "Cannot delete node '{}' because it is locked".format(node.name()))
|
||||
return
|
||||
selected_nodes.append(node)
|
||||
if selected_nodes:
|
||||
if len(selected_nodes) > 1:
|
||||
question = "Do you want to permanently delete these {} nodes?".format(len(selected_nodes))
|
||||
@@ -1393,6 +1524,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
elif item.parentItem() is None:
|
||||
item.delete()
|
||||
|
||||
self.scene().clearSelection()
|
||||
self.toggleUiDeviceMenu()
|
||||
|
||||
def allocateCompute(self, node_data, module_instance):
|
||||
"""
|
||||
Allocates a server.
|
||||
@@ -1402,59 +1536,29 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
from .main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
|
||||
if "server" in node_data:
|
||||
if "compute_id" in node_data:
|
||||
try:
|
||||
return ComputeManager.instance().getCompute(node_data["server"])
|
||||
return ComputeManager.instance().getCompute(node_data["compute_id"])
|
||||
except KeyError:
|
||||
raise ModuleError("Compute {} doesn't exists".format(node_data["server"]))
|
||||
raise ModuleError("Compute {} doesn't exists".format(node_data["compute_id"]))
|
||||
|
||||
server = server_select(mainwindow, node_data.get("node_type"))
|
||||
if server is None:
|
||||
raise ModuleError("Please select a server")
|
||||
return server
|
||||
|
||||
def createNode(self, node_data, pos):
|
||||
def createNodeFromTemplateId(self, template_id, pos):
|
||||
"""
|
||||
Creates a new node on the scene.
|
||||
|
||||
:param node_data: node data to create a new node
|
||||
:param pos: position of the drop event
|
||||
|
||||
:returns: NodeItem instance
|
||||
Ask the server to create a node using this template
|
||||
"""
|
||||
try:
|
||||
node_module = None
|
||||
for module in MODULES:
|
||||
instance = module.instance()
|
||||
node_class = module.getNodeClass(node_data["class"])
|
||||
if node_class in instance.classes():
|
||||
node_module = instance
|
||||
break
|
||||
|
||||
if not node_module:
|
||||
raise ModuleError("Could not find any module for {}".format(node_class))
|
||||
if self._topology.project() is None:
|
||||
return
|
||||
node = node_module.instantiateNode(node_class, self.allocateCompute(node_data, instance), self._topology.project())
|
||||
# If no server is available a ValueError is raised
|
||||
except (ModuleError, ValueError) as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Node creation", "{}".format(e))
|
||||
return
|
||||
|
||||
pos = self.mapToScene(pos)
|
||||
node_item = self.createNodeItem(node, node_data["symbol"], pos.x(), pos.y())
|
||||
node.setGraphics(node_item)
|
||||
try:
|
||||
node_module.createNode(node, node_data["name"])
|
||||
except ModuleError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Node creation", "{}".format(e))
|
||||
return
|
||||
return node_item
|
||||
return TemplateManager().instance().createNodeFromTemplateId(self._topology.project(), template_id, pos.x(), pos.y())
|
||||
|
||||
def createNodeItem(self, node, symbol, x, y):
|
||||
node.setSymbol(symbol)
|
||||
node.setPos(x, y)
|
||||
node_item = NodeItem(node)
|
||||
|
||||
self.scene().addItem(node_item)
|
||||
self._topology.addNode(node)
|
||||
|
||||
@@ -1470,22 +1574,23 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
node = Topology.instance().getNode(node_id)
|
||||
name = "Node"
|
||||
if node:
|
||||
if node.name():
|
||||
name = node.name()
|
||||
if node and node.name():
|
||||
name = node.name()
|
||||
if self._main_window and not sip.isdeleted(self._main_window):
|
||||
QtWidgets.QMessageBox.critical(self._main_window, name, message.strip())
|
||||
|
||||
def createDrawingItem(self, type, x, y, z, rotation=0, svg=None, drawing_id=None):
|
||||
def createDrawingItem(self, type, x, y, z, locked=False, rotation=0, svg=None, drawing_id=None):
|
||||
|
||||
if type == "ellipse":
|
||||
item = EllipseItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
item = EllipseItem(pos=QtCore.QPoint(x, y), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "rect":
|
||||
item = RectangleItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
item = RectangleItem(pos=QtCore.QPoint(x, y), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "line":
|
||||
item = LineItem(pos=QtCore.QPoint(x, y), dst=QtCore.QPoint(200, 0), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "image":
|
||||
item = ImageItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
item = ImageItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, locked=locked, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "text":
|
||||
item = TextItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
item = TextItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, locked=locked, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
|
||||
if drawing_id is None:
|
||||
item.create()
|
||||
@@ -1497,19 +1602,31 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
def drawBackground(self, painter, rect):
|
||||
super().drawBackground(painter, rect)
|
||||
if self._main_window.uiShowGridAction.isChecked():
|
||||
gridSize = 75
|
||||
grids = [(self.drawingGridSize(), QtGui.QColor(208, 208, 208)),
|
||||
(self.nodeGridSize(), QtGui.QColor(190, 190, 190))]
|
||||
painter.save()
|
||||
painter.setPen(QtGui.QPen(QtGui.QColor(190, 190, 190)))
|
||||
for (grid, colour) in grids:
|
||||
if not grid:
|
||||
continue
|
||||
painter.setPen(QtGui.QPen(colour))
|
||||
|
||||
left = int(rect.left()) - (int(rect.left()) % gridSize)
|
||||
top = int(rect.top()) - (int(rect.top()) % gridSize)
|
||||
left = int(rect.left()) - (int(rect.left()) % grid)
|
||||
top = int(rect.top()) - (int(rect.top()) % grid)
|
||||
|
||||
x = left
|
||||
while x < rect.right():
|
||||
painter.drawLine(x, rect.top(), x, rect.bottom())
|
||||
x += gridSize
|
||||
y = top
|
||||
while y < rect.bottom():
|
||||
painter.drawLine(rect.left(), y, rect.right(), y)
|
||||
y += gridSize
|
||||
x = left
|
||||
while x < rect.right():
|
||||
painter.drawLine(x, rect.top(), x, rect.bottom())
|
||||
x += grid
|
||||
y = top
|
||||
while y < rect.bottom():
|
||||
painter.drawLine(rect.left(), y, rect.right(), y)
|
||||
y += grid
|
||||
painter.restore()
|
||||
|
||||
def toggleUiDeviceMenu(self):
|
||||
""" Hook which enables/disables uiDeviceMenu based on the current items selection"""
|
||||
items = self.scene().selectedItems()
|
||||
if len(items) > 0:
|
||||
self._main_window.uiDeviceMenu.setEnabled(True)
|
||||
else:
|
||||
self._main_window.uiDeviceMenu.setEnabled(False)
|
||||
|
||||
@@ -15,7 +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/>.
|
||||
|
||||
import sip
|
||||
from .qt import sip
|
||||
import json
|
||||
import copy
|
||||
import http
|
||||
@@ -25,6 +25,8 @@ import base64
|
||||
import datetime
|
||||
import ipaddress
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
|
||||
from .version import __version__, __version_info__
|
||||
from .qt import QtCore, QtNetwork, qpartial, sip_is_deleted
|
||||
@@ -45,21 +47,17 @@ class HTTPClient(QtCore.QObject):
|
||||
"""
|
||||
HTTP client.
|
||||
|
||||
:param settings: Dictionnary with connection information to the server
|
||||
:param settings: Dictionary with connection information to the server
|
||||
:param network_manager: A QT network manager
|
||||
"""
|
||||
|
||||
# How many times we need to retry a connection
|
||||
MAX_RETRY_CONNECTION = 5
|
||||
|
||||
# Callback class used for displaying progress
|
||||
_progress_callback = None
|
||||
|
||||
connection_connected_signal = QtCore.Signal()
|
||||
connection_disconnected_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, settings, network_manager=None):
|
||||
|
||||
def __init__(self, settings, network_manager=None, max_retry_connection=5):
|
||||
super().__init__()
|
||||
|
||||
self._protocol = settings.get("protocol", "http")
|
||||
@@ -74,8 +72,9 @@ class HTTPClient(QtCore.QObject):
|
||||
self._port = int(settings["port"])
|
||||
self._user = settings.get("user", None)
|
||||
self._password = settings.get("password", None)
|
||||
# How many time we have retry connection
|
||||
# How many time we have already retried connection
|
||||
self._retry = 0
|
||||
self._max_retry_connection = max_retry_connection
|
||||
self._connected = False
|
||||
self._shutdown = False # Shutdown in progress
|
||||
self._accept_insecure_certificate = settings.get("accept_insecure_certificate", None)
|
||||
@@ -84,7 +83,6 @@ class HTTPClient(QtCore.QObject):
|
||||
# query and disconnect if time is too long between two query
|
||||
self._last_query_timestamp = None
|
||||
self._max_time_difference_between_queries = None
|
||||
|
||||
if network_manager:
|
||||
self._network_manager = network_manager
|
||||
else:
|
||||
@@ -167,6 +165,25 @@ class HTTPClient(QtCore.QObject):
|
||||
self.createHTTPQuery("POST", "/shutdown", None, showProgress=False)
|
||||
self._shutdown = True
|
||||
|
||||
def getNetworkManager(self):
|
||||
"""
|
||||
:return: instance of NetworkManager
|
||||
"""
|
||||
return self._network_manager
|
||||
|
||||
def setMaxRetryConnection(self, retries):
|
||||
"""
|
||||
Sets how many times we need to retry a connection
|
||||
:param retries: integer
|
||||
"""
|
||||
self._max_retry_connection = retries
|
||||
|
||||
def getMaxRetryConnection(self):
|
||||
"""
|
||||
Returns how many times we need to retry a connection
|
||||
"""
|
||||
return self._max_retry_connection
|
||||
|
||||
def _notify_progress_start_query(self, query_id, progress_text, response):
|
||||
"""
|
||||
Called when a query start
|
||||
@@ -190,16 +207,16 @@ class HTTPClient(QtCore.QObject):
|
||||
Called when a query upload progress
|
||||
"""
|
||||
if not sip_is_deleted(HTTPClient._progress_callback):
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
|
||||
|
||||
def _notify_progress_download(self, query_id, sent, total):
|
||||
"""
|
||||
Called when a query download progress
|
||||
"""
|
||||
if not sip_is_deleted(HTTPClient._progress_callback):
|
||||
# abs() for maxium because sometimes the system send negative
|
||||
# abs() for maximum because sometimes the system send negative
|
||||
# values
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, abs(total))
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
|
||||
|
||||
@classmethod
|
||||
def setProgressCallback(cls, progress_callback):
|
||||
@@ -253,6 +270,7 @@ class HTTPClient(QtCore.QObject):
|
||||
prefix="/v2",
|
||||
params={},
|
||||
networkManager=None,
|
||||
eventsHandler=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Call the remote server, if not connected, check connection before
|
||||
@@ -270,6 +288,8 @@ class HTTPClient(QtCore.QObject):
|
||||
:param timeout: Delay in seconds before raising a timeout
|
||||
:param prefix: Prefix to the path
|
||||
:param networkManager: QNetworkAccessManager None use the default
|
||||
:param eventsHandler: Handler receiving and triggering events like `updated`, `cancelled`.
|
||||
If not specified and showProgress is `True` then `ProgressDialog` receives them.
|
||||
:param params: Query arguments parameters
|
||||
:returns: QNetworkReply
|
||||
"""
|
||||
@@ -281,16 +301,17 @@ class HTTPClient(QtCore.QObject):
|
||||
if self._shutdown:
|
||||
return
|
||||
|
||||
# TODO: clean this
|
||||
# We try to detect computer hibernation
|
||||
# if time between two query is too long we trigger a disconnect
|
||||
if self._max_time_difference_between_queries:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
if self._last_query_timestamp is not None and now > self._last_query_timestamp + self._max_time_difference_between_queries:
|
||||
log.warning("Synchronisation lost with the server.")
|
||||
self.disconnect()
|
||||
self._last_query_timestamp = None
|
||||
return
|
||||
self._last_query_timestamp = now
|
||||
# if self._max_time_difference_between_queries:
|
||||
# now = datetime.datetime.now().timestamp()
|
||||
# if self._last_query_timestamp is not None and now > self._last_query_timestamp + self._max_time_difference_between_queries:
|
||||
# log.warning("Synchronisation lost with the server.")
|
||||
# self.disconnect()
|
||||
# self._last_query_timestamp = None
|
||||
# return
|
||||
# self._last_query_timestamp = now
|
||||
|
||||
request = qpartial(self._executeHTTPQuery, method, path, qpartial(callback), body, context,
|
||||
downloadProgressCallback=downloadProgressCallback,
|
||||
@@ -301,16 +322,17 @@ class HTTPClient(QtCore.QObject):
|
||||
server=server,
|
||||
timeout=timeout,
|
||||
prefix=prefix,
|
||||
eventsHandler=eventsHandler,
|
||||
params=params)
|
||||
|
||||
if self._connected:
|
||||
return request()
|
||||
else:
|
||||
self._query_waiting_connections.append((request, callback))
|
||||
# If we are not connected and we enqueue the first query we open the conection
|
||||
# enqueue the first query and open the connection if we are not connected
|
||||
if len(self._query_waiting_connections) == 1:
|
||||
log.info("Connection to {}".format(self.url()))
|
||||
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=5, showProgress=False)
|
||||
log.debug("Connection to {}".format(self.url()))
|
||||
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=10, showProgress=False)
|
||||
|
||||
def _connectionError(self, callback, msg="", server=None):
|
||||
"""
|
||||
@@ -327,7 +349,7 @@ class HTTPClient(QtCore.QObject):
|
||||
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall. And that server version is {}.".format(self.url(), __version__)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
callback({"message": msg}, error=True, server=server, connection_error=True)
|
||||
self._query_waiting_connections = []
|
||||
|
||||
def _retryConnection(self, server=None):
|
||||
@@ -347,7 +369,7 @@ class HTTPClient(QtCore.QObject):
|
||||
"""
|
||||
|
||||
if error is not False:
|
||||
if self._retry < self.MAX_RETRY_CONNECTION:
|
||||
if self._retry < self.getMaxRetryConnection():
|
||||
self._retryConnection(server=server)
|
||||
return
|
||||
for request, callback in self._query_waiting_connections:
|
||||
@@ -356,7 +378,7 @@ class HTTPClient(QtCore.QObject):
|
||||
return
|
||||
|
||||
if "version" not in params or "local" not in params:
|
||||
if self._retry < self.MAX_RETRY_CONNECTION:
|
||||
if self._retry < self.getMaxRetryConnection():
|
||||
self._retryConnection(server=server)
|
||||
return
|
||||
msg = "The remote server {} is not a GNS3 server".format(self.url())
|
||||
@@ -368,21 +390,22 @@ class HTTPClient(QtCore.QObject):
|
||||
return
|
||||
|
||||
if params["version"].split("-")[0] != __version__.split("-")[0]:
|
||||
msg = "Client version {} differs with server version {}".format(__version__, params["version"])
|
||||
log.error(msg)
|
||||
msg = "Client version {} is not the same as server (controller) version {}".format(__version__, params["version"])
|
||||
# Stable release
|
||||
if __version_info__[3] == 0:
|
||||
log.error(msg)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
return
|
||||
# We don't allow different major version to interact even with dev build
|
||||
elif parse_version(__version__)[:2] != parse_version(params["version"])[:2]:
|
||||
log.error(msg)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
return
|
||||
log.warning("Use a different client and server version can create bugs. Use it at your own risk.")
|
||||
log.warning("{}\nUsing different versions may result in unexpected problems. Please upgrade or use at your own risk.".format(msg))
|
||||
|
||||
self._connected = True
|
||||
self._retry = 0
|
||||
@@ -441,7 +464,49 @@ class HTTPClient(QtCore.QObject):
|
||||
request.setRawHeader(b"Authorization", auth_string.encode())
|
||||
return request
|
||||
|
||||
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, networkManager=None, **kwargs):
|
||||
def connectWebSocket(self, websocket, path, prefix="/v2"):
|
||||
"""
|
||||
Path of the websocket endpoint
|
||||
"""
|
||||
host = self._getHostForQuery()
|
||||
request = websocket.request()
|
||||
ws_url = "ws://{host}:{port}{prefix}{path}".format(host=host, port=self._port, path=path, prefix=prefix)
|
||||
log.debug("Connecting to WebSocket endpoint: {}".format(ws_url))
|
||||
request.setUrl(QtCore.QUrl(ws_url))
|
||||
self._addAuth(request)
|
||||
websocket.open(request)
|
||||
return websocket
|
||||
|
||||
def _getHostForQuery(self):
|
||||
"""
|
||||
Get hostname that could be use by Qt
|
||||
"""
|
||||
try:
|
||||
ip = self._host.rsplit('%', 1)[0]
|
||||
ipaddress.IPv6Address(ip) # remove any scope ID
|
||||
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
|
||||
host = "[{}]".format(ip)
|
||||
except ipaddress.AddressValueError:
|
||||
host = self._host
|
||||
return host
|
||||
|
||||
def _paramsToQueryString(self, params):
|
||||
"""
|
||||
:param params: Dictionary of query string parameters
|
||||
:returns: String of the query string
|
||||
"""
|
||||
if params == {}:
|
||||
query_string = ""
|
||||
else:
|
||||
query_string = "?"
|
||||
params = params.copy()
|
||||
for key, value in params.copy().items():
|
||||
if value is None:
|
||||
del params[key]
|
||||
query_string += urllib.parse.urlencode(params)
|
||||
return query_string
|
||||
|
||||
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, networkManager=None, eventsHandler=None, **kwargs):
|
||||
"""
|
||||
Call the remote server
|
||||
|
||||
@@ -457,22 +522,14 @@ class HTTPClient(QtCore.QObject):
|
||||
:param ignoreErrors: Ignore connection error (usefull to not closing a connection when notification feed is broken)
|
||||
:param server: The server where the query is executed
|
||||
:param timeout: Delay in seconds before raising a timeout
|
||||
:param eventsHandler: Handler receiving and triggering events like `updated`, `cancelled`.
|
||||
If not specified and showProgress is `True` then `ProgressDialog` receives them.
|
||||
:param params: Query arguments parameters
|
||||
:returns: QNetworkReply
|
||||
"""
|
||||
|
||||
try:
|
||||
ip = self._host.rsplit('%', 1)[0]
|
||||
ipaddress.IPv6Address(ip) # remove any scope ID
|
||||
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
|
||||
host = "[{}]".format(ip)
|
||||
except ipaddress.AddressValueError:
|
||||
host = self._host
|
||||
|
||||
if params == {}:
|
||||
query_string = ""
|
||||
else:
|
||||
query_string = "?" + urllib.parse.urlencode(params)
|
||||
host = self._getHostForQuery()
|
||||
query_string = self._paramsToQueryString(params)
|
||||
|
||||
log.debug("{method} {protocol}://{host}:{port}{prefix}{path} {body}{query_string}".format(method=method, protocol=self._protocol, host=host, port=self._port, path=path, body=body, prefix=prefix, query_string=query_string))
|
||||
if self._user:
|
||||
@@ -491,7 +548,11 @@ class HTTPClient(QtCore.QObject):
|
||||
if not networkManager:
|
||||
networkManager = self._network_manager
|
||||
|
||||
response = networkManager.sendCustomRequest(request, method.encode(), body)
|
||||
try:
|
||||
response = networkManager.sendCustomRequest(request, method.encode(), body)
|
||||
except SystemError as e:
|
||||
log.error("Can't send query: {}".format(str(e)))
|
||||
return
|
||||
|
||||
context = copy.copy(context)
|
||||
context["query_id"] = str(uuid.uuid4())
|
||||
@@ -502,8 +563,11 @@ class HTTPClient(QtCore.QObject):
|
||||
if downloadProgressCallback is not None:
|
||||
response.readyRead.connect(qpartial(self._readyReadySlot, response, downloadProgressCallback, context, server))
|
||||
|
||||
if not sip_is_deleted(HTTPClient._progress_callback) and HTTPClient._progress_callback.progress_dialog():
|
||||
request_canceled = qpartial(self._requestCanceled, response, context)
|
||||
request_canceled = qpartial(self._requestCanceled, response, context)
|
||||
|
||||
if eventsHandler is not None:
|
||||
eventsHandler.canceled.connect(request_canceled)
|
||||
elif not sip_is_deleted(HTTPClient._progress_callback) and HTTPClient._progress_callback.progress_dialog():
|
||||
HTTPClient._progress_callback.progress_dialog().canceled.connect(request_canceled)
|
||||
|
||||
if showProgress:
|
||||
@@ -514,7 +578,7 @@ class HTTPClient(QtCore.QObject):
|
||||
self._notify_progress_start_query(context["query_id"], progressText, response)
|
||||
|
||||
if timeout is not None:
|
||||
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response))
|
||||
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response, timeout))
|
||||
|
||||
return response
|
||||
|
||||
@@ -549,14 +613,14 @@ class HTTPClient(QtCore.QObject):
|
||||
else:
|
||||
callback(content, server=server, context=context)
|
||||
|
||||
def _timeoutSlot(self, response):
|
||||
def _timeoutSlot(self, response, timeout):
|
||||
"""
|
||||
Beware it's call for all request you need to check the status of the response
|
||||
"""
|
||||
# We check if we received HTTP headers
|
||||
if not sip.isdeleted(response) and response.isRunning() and not len(response.rawHeaderList()) > 0:
|
||||
if not response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
log.warn("Timeout request {}".format(response.url().toString()))
|
||||
log.warning("Timeout after {} seconds for request {}. Please check the connection is not blocked by a firewall or an anti-virus.".format(timeout, response.url().toString()))
|
||||
response.abort()
|
||||
|
||||
def disconnect(self):
|
||||
@@ -569,14 +633,14 @@ class HTTPClient(QtCore.QObject):
|
||||
def _requestCanceled(self, response, context):
|
||||
|
||||
if response.isRunning() and not response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
log.warn("Aborting request for {}".format(response.url().toString()))
|
||||
log.warning("Aborting request for {}".format(response.url().toString()))
|
||||
response.abort()
|
||||
if "query_id" in context:
|
||||
self._notify_progress_end_query(context["query_id"])
|
||||
|
||||
def _processError(self, response, server, callback, context, request_body, ignore_errors, error_code):
|
||||
if error_code != QtNetwork.QNetworkReply.NoError:
|
||||
error_message = response.errorString()
|
||||
error_message = "{} ({}:{})".format(response.errorString(), self._host, self._port)
|
||||
|
||||
if not ignore_errors:
|
||||
log.debug("Response error: %s for %s (error: %d)", error_message, response.url().toString(), error_code)
|
||||
@@ -586,7 +650,10 @@ class HTTPClient(QtCore.QObject):
|
||||
|
||||
if error_code < 200 or error_code == 403:
|
||||
if error_code == QtNetwork.QNetworkReply.OperationCanceledError: # It's legit to cancel do not disconnect
|
||||
error_message = "Operation timeout" # It's more clear than cancel, because cancel is trigger by us when we timeout
|
||||
error_message = "Operation timeout" # It's clearer than cancel because cancel is triggered by us when we timeout
|
||||
elif error_code == QtNetwork.QNetworkReply.NetworkSessionFailedError:
|
||||
# ignore the network session failed error to let the network manager recover from it
|
||||
return
|
||||
elif not ignore_errors:
|
||||
self.disconnect()
|
||||
if callback is not None:
|
||||
@@ -607,7 +674,7 @@ class HTTPClient(QtCore.QObject):
|
||||
if not body or content_type != "application/json":
|
||||
callback({"message": error_message}, error=True, server=server, context=context)
|
||||
else:
|
||||
log.debug(body)
|
||||
# log.debug(body)
|
||||
try:
|
||||
callback(json.loads(body), error=True, server=server, context=context)
|
||||
except ValueError:
|
||||
@@ -637,9 +704,12 @@ class HTTPClient(QtCore.QObject):
|
||||
except UnicodeDecodeError:
|
||||
body = None
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
log.debug(body)
|
||||
if body and len(body.strip(" \n\t")) > 0 and content_type == "application/json":
|
||||
params = json.loads(body)
|
||||
try:
|
||||
params = json.loads(body)
|
||||
except ValueError: # Partial JSON
|
||||
params = {}
|
||||
status = 504
|
||||
else:
|
||||
params = {}
|
||||
if callback is not None:
|
||||
@@ -657,44 +727,72 @@ class HTTPClient(QtCore.QObject):
|
||||
e = HttpBadRequest(body)
|
||||
raise e
|
||||
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
def getSynchronous(self, method, endpoint, prefix="/v2", timeout=5):
|
||||
"""
|
||||
Synchronous check if a server is running
|
||||
|
||||
:returns: Tuple (Status code, json of anwser). Status 0 is a non HTTP error
|
||||
:returns: Tuple (Status code, json of answer). Status 0 is a non HTTP error
|
||||
"""
|
||||
try:
|
||||
url = "{protocol}://{host}:{port}/v2/{endpoint}".format(protocol=self._protocol, host=self._host, port=self._port, endpoint=endpoint)
|
||||
|
||||
if self._user is not None and len(self._user) > 0:
|
||||
log.debug("Synchronous get {} with user '{}'".format(url, self._user))
|
||||
auth_handler = urllib.request.HTTPBasicAuthHandler()
|
||||
auth_handler.add_password(realm="GNS3 server",
|
||||
uri=url,
|
||||
user=self._user,
|
||||
passwd=self._password)
|
||||
opener = urllib.request.build_opener(auth_handler)
|
||||
urllib.request.install_opener(opener)
|
||||
else:
|
||||
log.debug("Synchronous get {} (no authentication)".format(url))
|
||||
response = urllib.request.urlopen(url, timeout=timeout)
|
||||
content_type = response.getheader("CONTENT-TYPE")
|
||||
if response.status == 200:
|
||||
host = self._getHostForQuery()
|
||||
|
||||
log.debug("{method} {protocol}://{host}:{port}{prefix}{endpoint}".format(method=method, protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
|
||||
if self._user:
|
||||
url = QtCore.QUrl("{protocol}://{user}@{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, user=self._user, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
|
||||
else:
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
|
||||
|
||||
request = self._request(url)
|
||||
request = self._addAuth(request)
|
||||
request.setRawHeader(b"User-Agent", "GNS3 QT Client v{version}".format(version=__version__).encode())
|
||||
|
||||
try:
|
||||
response = self._network_manager.sendCustomRequest(request, method.encode())
|
||||
except SystemError as e:
|
||||
log.error("Can't send query: {}".format(str(e)))
|
||||
return
|
||||
|
||||
loop = QtCore.QEventLoop()
|
||||
response.finished.connect(loop.quit)
|
||||
|
||||
if timeout is not None:
|
||||
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response, timeout))
|
||||
|
||||
if not loop.isRunning():
|
||||
loop.exec_()
|
||||
|
||||
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
log.debug("Error while connecting to local server {}".format(response.errorString()))
|
||||
return status, None
|
||||
else:
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
if status == 200:
|
||||
if content_type == "application/json":
|
||||
content = response.read()
|
||||
content = bytes(response.readAll())
|
||||
json_data = json.loads(content.decode("utf-8"))
|
||||
return response.status, json_data
|
||||
return status, json_data
|
||||
else:
|
||||
return response.status, None
|
||||
except http.client.InvalidURL as e:
|
||||
log.warn("Invalid local server url: {}".format(e))
|
||||
return 0, None
|
||||
except urllib.error.URLError:
|
||||
# Connection refused. It's a normal behavior if server is not started
|
||||
return 0, None
|
||||
except urllib.error.HTTPError as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return e.code, None
|
||||
except (OSError, http.client.BadStatusLine, ValueError) as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return status, None
|
||||
|
||||
return 0, None
|
||||
|
||||
@classmethod
|
||||
def fromUrl(cls, url, network_manager=None, base_settings=None):
|
||||
"""
|
||||
Returns HttpClient instance based on the url
|
||||
:param url: Url to parse
|
||||
:param network_manager: Optional network_manager
|
||||
:param base_settings: Source of the settings, if necessary
|
||||
:return: HttpClient
|
||||
"""
|
||||
settings = {}
|
||||
if base_settings is not None:
|
||||
settings.update(**base_settings)
|
||||
parse_results = urllib.parse.urlparse(url)
|
||||
settings['protocol'] = parse_results.scheme
|
||||
settings['host'] = parse_results.hostname
|
||||
settings['port'] = parse_results.port
|
||||
settings['user'] = parse_results.username
|
||||
settings['password'] = parse_results.password
|
||||
return cls(settings, network_manager)
|
||||
|
||||
@@ -25,6 +25,7 @@ from gns3.settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.controller import Controller
|
||||
from gns3.utils.file_copy_worker import FileCopyWorker
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.registry.image import Image
|
||||
|
||||
|
||||
class ImageManager:
|
||||
@@ -33,7 +34,29 @@ class ImageManager:
|
||||
# Remember if we already ask the user about this image for this server
|
||||
self._asked_for_this_image = {}
|
||||
|
||||
def askCopyUploadImage(self, parent, path, server, node_type):
|
||||
def _getUniqueDestinationPath(self, source_image, node_type, path):
|
||||
"""
|
||||
Get a unique destination path (with counter).
|
||||
"""
|
||||
|
||||
if not os.path.exists(path):
|
||||
return path
|
||||
path, extension = os.path.splitext(path)
|
||||
counter = 1
|
||||
new_path = "{}-{}{}".format(path, counter, extension)
|
||||
while os.path.exists(new_path):
|
||||
destination_image = Image(node_type, new_path, filename=os.path.basename(new_path))
|
||||
try:
|
||||
if source_image.md5sum == destination_image.md5sum:
|
||||
# the source and destination images are identical
|
||||
return new_path
|
||||
except OSError:
|
||||
continue
|
||||
counter += 1
|
||||
new_path = "{}-{}{}".format(path, counter, extension)
|
||||
return new_path
|
||||
|
||||
def askCopyUploadImage(self, parent, source_path, server, node_type):
|
||||
"""
|
||||
Ask user for copying the image to the default directory or upload
|
||||
it to remote server.
|
||||
@@ -46,34 +69,51 @@ class ImageManager:
|
||||
"""
|
||||
|
||||
if (server and server != "local") or Controller.instance().isRemote():
|
||||
return self._uploadImageToRemoteServer(path, server, node_type)
|
||||
return self._uploadImageToRemoteServer(source_path, server, node_type)
|
||||
else:
|
||||
destination_directory = self.getDirectoryForType(node_type)
|
||||
if os.path.normpath(os.path.dirname(path)) != destination_directory:
|
||||
# the IOS image is not in the default images directory
|
||||
destination_path = os.path.join(destination_directory, os.path.basename(source_path))
|
||||
source_filename = os.path.basename(source_path)
|
||||
destination_filename = os.path.basename(destination_path)
|
||||
if os.path.normpath(os.path.dirname(source_path)) != destination_directory:
|
||||
# the image is not in the default images directory
|
||||
if source_filename == destination_filename:
|
||||
# the filename already exists in the default images directory
|
||||
source_image = Image(node_type, source_path, filename=source_filename)
|
||||
destination_image = Image(node_type, destination_path, filename=destination_filename)
|
||||
try:
|
||||
if source_image.md5sum == destination_image.md5sum:
|
||||
# the source and destination images are identical
|
||||
return source_path
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(parent, 'Image', 'Cannot compare image file {} with {}: {}.'.format(source_path, destination_path, str(e)))
|
||||
return source_path
|
||||
# find a new unique path to avoid overwriting existing destination file
|
||||
destination_path = self._getUniqueDestinationPath(source_image, node_type, destination_path)
|
||||
|
||||
reply = QtWidgets.QMessageBox.question(parent,
|
||||
'Image',
|
||||
'Would you like to copy {} to the default images directory'.format(os.path.basename(path)),
|
||||
'Would you like to copy {} to the default images directory'.format(source_filename),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
destination_path = os.path.join(destination_directory, os.path.basename(path))
|
||||
try:
|
||||
os.makedirs(destination_directory, exist_ok=True)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(parent, 'Image', 'Could not create destination directory {}: {}'.format(destination_directory, str(e)))
|
||||
return path
|
||||
worker = FileCopyWorker(path, destination_path)
|
||||
progress_dialog = ProgressDialog(worker, 'Image', 'Copying {}'.format(os.path.basename(path)), 'Cancel', busy=True, parent=parent)
|
||||
return source_path
|
||||
|
||||
worker = FileCopyWorker(source_path, destination_path)
|
||||
progress_dialog = ProgressDialog(worker, 'Image', 'Copying {}'.format(source_filename), 'Cancel', busy=True, parent=parent)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
errors = progress_dialog.errors()
|
||||
if errors:
|
||||
QtWidgets.QMessageBox.critical(parent, 'Image', '{}'.format(''.join(errors)))
|
||||
return path
|
||||
return source_path
|
||||
else:
|
||||
path = destination_path
|
||||
return path
|
||||
source_path = destination_path
|
||||
return source_path
|
||||
|
||||
def _uploadImageToRemoteServer(self, path, server, node_type):
|
||||
"""
|
||||
@@ -95,22 +135,9 @@ class ImageManager:
|
||||
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 _askForUploadMissingImage(self, filename, server):
|
||||
from gns3.main_window import MainWindow
|
||||
parent = MainWindow.instance()
|
||||
reply = QtWidgets.QMessageBox.warning(parent,
|
||||
'Image',
|
||||
'{} is missing on server {} but exist on your computer. Do you want to upload it?'.format(filename, server.url()),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _getRelativeImagePath(self, path, node_type):
|
||||
"""
|
||||
Get a path relative to images directory path
|
||||
@@ -149,7 +176,7 @@ class ImageManager:
|
||||
if node_type == 'DYNAMIPS':
|
||||
return os.path.join(self.getDirectory(), 'IOS')
|
||||
else:
|
||||
return os.path.join(self.getDirectory(), node_type)
|
||||
return os.path.join(self.getDirectory(), node_type.upper())
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
|
||||
90
gns3/image_upload_manager.py
Normal file
90
gns3/image_upload_manager.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# -*- 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 os
|
||||
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 not os.path.exists(self._image.path):
|
||||
log.error("Image '{}' could not be found".format(self._image.path))
|
||||
return
|
||||
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.debug("Uploading image '{}' to compute".format(self._image.path))
|
||||
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),
|
||||
context={"image_path": self._image.path}, progressText="Uploading {}".format(self._image.filename), timeout=None, prefix="")
|
||||
|
||||
def _fileUploadToController(self):
|
||||
log.debug("Uploading image '{}' to controller".format(self._image.path))
|
||||
self._controller.postCompute(self._getComputePath(), self._compute_id, self._callback, body=pathlib.Path(self._image.path),
|
||||
context={"image_path": self._image.path}, progressText="Uploading {}".format(self._image.filename), timeout=None)
|
||||
@@ -15,7 +15,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtCore, QtWidgets, qslot
|
||||
from ..qt import QtCore, QtWidgets, qslot, QtGui
|
||||
from .utils import colorFromSvg
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
@@ -24,6 +25,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DrawingItem:
|
||||
# Map QT stroke to SVG style
|
||||
QT_DASH_TO_SVG = {
|
||||
QtCore.Qt.SolidLine: "",
|
||||
QtCore.Qt.NoPen: None,
|
||||
QtCore.Qt.DashLine: "25, 25",
|
||||
QtCore.Qt.DotLine: "5, 25",
|
||||
QtCore.Qt.DashDotLine: "5, 25, 25",
|
||||
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
|
||||
}
|
||||
|
||||
show_layer = False
|
||||
|
||||
@@ -31,8 +41,10 @@ class DrawingItem:
|
||||
Base class for non emulation item
|
||||
"""
|
||||
|
||||
def __init__(self, project=None, pos=None, drawing_id=None, svg=None, z=0, rotation=0, **kws):
|
||||
def __init__(self, project=None, pos=None, drawing_id=None, svg=None, z=0, locked=False, rotation=0, **kws):
|
||||
self._id = drawing_id
|
||||
self._deleting = False
|
||||
self._locked = locked
|
||||
if self._id is None:
|
||||
self._id = str(uuid.uuid4())
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
@@ -54,6 +66,8 @@ class DrawingItem:
|
||||
if rotation:
|
||||
self.setRotation(rotation)
|
||||
|
||||
self.setLocked(locked)
|
||||
|
||||
def drawing_id(self):
|
||||
return self._id
|
||||
|
||||
@@ -71,14 +85,14 @@ class DrawingItem:
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up drawing: {}".format(result["message"]))
|
||||
log.error("Error while creating 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__())
|
||||
if self._id and not self.deleting() and self._project:
|
||||
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__(), showProgress=False)
|
||||
|
||||
@qslot
|
||||
def updateDrawingCallback(self, result, error=False, **kwargs):
|
||||
@@ -91,10 +105,11 @@ class DrawingItem:
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up drawing: {}".format(result["message"]))
|
||||
log.error("Error while updating drawing: {}".format(result["message"]))
|
||||
return False
|
||||
self.setPos(QtCore.QPoint(result["x"], result["y"]))
|
||||
self.setZValue(result["z"])
|
||||
self.setLocked(result["locked"])
|
||||
self.setRotation(result["rotation"])
|
||||
if "svg" in result:
|
||||
self.fromSvg(result["svg"])
|
||||
@@ -138,6 +153,7 @@ class DrawingItem:
|
||||
"x": int(self.pos().x()),
|
||||
"y": int(self.pos().y()),
|
||||
"z": int(self.zValue()),
|
||||
"locked": self._locked,
|
||||
"rotation": int(self.rotation())
|
||||
}
|
||||
svg = self.toSvg()
|
||||
@@ -147,20 +163,39 @@ class DrawingItem:
|
||||
self._hash_svg = hash_svg
|
||||
return data
|
||||
|
||||
def setZValue(self, value):
|
||||
def locked(self):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
Is the drawing locked
|
||||
"""
|
||||
|
||||
return self._locked
|
||||
|
||||
def setLocked(self, locked):
|
||||
"""
|
||||
Sets the locked value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtWidgets.QGraphicsItem.setZValue(self, value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
if locked is True:
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
self._locked = locked
|
||||
|
||||
def deleting(self):
|
||||
"""
|
||||
Is the drawing being deleted
|
||||
"""
|
||||
|
||||
return self._deleting
|
||||
|
||||
def setDeleting(self):
|
||||
"""
|
||||
Mark this drawing as being deleted
|
||||
"""
|
||||
|
||||
self._deleting = True
|
||||
|
||||
def delete(self, skip_controller=False):
|
||||
"""
|
||||
@@ -169,6 +204,7 @@ class DrawingItem:
|
||||
:param skip_controller: Do not replicate change on the controller (usefull when it's already deleted on controller)
|
||||
"""
|
||||
|
||||
self.setDeleting()
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeDrawing(self)
|
||||
@@ -177,11 +213,11 @@ class DrawingItem:
|
||||
|
||||
def itemChange(self, change, value):
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
GRID_SIZE = 75
|
||||
grid_size = self._graphics_view.drawingGridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
tmp_x = (GRID_SIZE * round((self.x() + mid_x) / GRID_SIZE)) - mid_x
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -215,3 +251,48 @@ class DrawingItem:
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
def _styleSvg(self, element):
|
||||
"""
|
||||
Add style from the shape item to the SVG element that we will
|
||||
export
|
||||
"""
|
||||
style = ""
|
||||
pen = self.pen()
|
||||
if hasattr(self, "brush"): # Line don't have a brush
|
||||
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
|
||||
element.set("fill-opacity", str(self.brush().color().alphaF()))
|
||||
|
||||
dasharray = self.QT_DASH_TO_SVG[pen.style()]
|
||||
if dasharray is None: # No border to the element
|
||||
return element
|
||||
elif dasharray == "":
|
||||
pass # Solid line
|
||||
else:
|
||||
element.set("stroke-dasharray", dasharray)
|
||||
element.set("stroke-width", str(pen.width()))
|
||||
element.set("stroke", "#" + hex(pen.color().rgba())[4:])
|
||||
return element
|
||||
|
||||
def _penFromSVGElement(self, svg):
|
||||
"""
|
||||
Get a pen from a SVG element
|
||||
|
||||
:param svg:
|
||||
"""
|
||||
pen = QtGui.QPen()
|
||||
if svg.get("stroke-width"):
|
||||
pen.setWidth(int(svg.get("stroke-width")))
|
||||
if svg.get("stroke"):
|
||||
pen.setColor(colorFromSvg(svg.get("stroke")))
|
||||
# Map SVG stroke style (border of the element to the Qt version)
|
||||
if not svg.get("stroke"):
|
||||
pen.setStyle(QtCore.Qt.NoPen)
|
||||
else:
|
||||
pen.setStyle(QtCore.Qt.SolidLine)
|
||||
stroke = svg.get("stroke-dasharray")
|
||||
if stroke:
|
||||
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
|
||||
if svg_stroke == stroke:
|
||||
pen.setStyle(qt_stroke)
|
||||
return pen
|
||||
|
||||
@@ -22,7 +22,7 @@ Graphical representation of an ellipse on the QGraphicsScene.
|
||||
import math
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..qt import QtWidgets
|
||||
from .shape_item import ShapeItem
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Graphical representation of an Ethernet link for QGraphicsScene.
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .link_item import LinkItem
|
||||
from .note_item import NoteItem
|
||||
from .label_item import LabelItem
|
||||
from ..ports.port import Port
|
||||
|
||||
|
||||
@@ -106,20 +106,20 @@ class EthernetLinkItem(LinkItem):
|
||||
"""
|
||||
|
||||
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
if not self._adding_flag and self._settings["draw_link_status_points"]:
|
||||
if not self._adding_flag:
|
||||
|
||||
# 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
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._source_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
@@ -141,26 +141,28 @@ class EthernetLinkItem(LinkItem):
|
||||
source_port_label = self._source_port.label()
|
||||
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
source_port_label = LabelItem(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:
|
||||
source_port_label.setFlag(source_port_label.ItemIsMovable, not self._source_item.locked())
|
||||
source_port_label.show()
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
painter.drawPoint(point1)
|
||||
if self._settings["draw_link_status_points"]:
|
||||
painter.drawPoint(point1)
|
||||
|
||||
if self._destination_port.status() == Port.started:
|
||||
if self._link.suspended() or self._destination_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
@@ -182,16 +184,18 @@ class EthernetLinkItem(LinkItem):
|
||||
destination_port_label = self._destination_port.label()
|
||||
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
destination_port_label = LabelItem(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:
|
||||
destination_port_label.setFlag(destination_port_label.ItemIsMovable, not self._destination_item.locked())
|
||||
destination_port_label.show()
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(point2)
|
||||
if self._settings["draw_link_status_points"]:
|
||||
painter.drawPoint(point2)
|
||||
|
||||
self._drawCaptureSymbol()
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -52,6 +52,9 @@ class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
|
||||
if svg:
|
||||
svg = self.fromSvg(svg)
|
||||
|
||||
if 'z' in kws.keys():
|
||||
self.setZValue(kws['z'])
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
Paints the contents of an item in local coordinates.
|
||||
|
||||
@@ -15,20 +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/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a note on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from .utils import colorFromSvg
|
||||
|
||||
|
||||
class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
class LabelItem(QtWidgets.QGraphicsTextItem):
|
||||
"""
|
||||
Text note for the QGraphicsView.
|
||||
Label for links and nodes.
|
||||
|
||||
:param parent: optional parent
|
||||
"""
|
||||
|
||||
item_unselected_signal = QtCore.Signal()
|
||||
|
||||
show_layer = False
|
||||
@@ -45,8 +42,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
qt_font.fromString(view_settings["default_label_font"])
|
||||
self.setDefaultTextColor(QtGui.QColor(view_settings["default_label_color"]))
|
||||
self.setFont(qt_font)
|
||||
self.setFlag(self.ItemIsMovable)
|
||||
self.setFlag(self.ItemIsSelectable)
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable)
|
||||
self.setZValue(2)
|
||||
self._editable = True
|
||||
|
||||
@@ -174,34 +170,19 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x(), center.y()), zval)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
super().setZValue(value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def setStyle(self, styles):
|
||||
def setStyle(self, new_style):
|
||||
"""
|
||||
Set text style using a SVG style
|
||||
"""
|
||||
font = QtGui.QFont()
|
||||
for style in styles.split(";"):
|
||||
for style in new_style.split(";"):
|
||||
if ":" in style:
|
||||
key, val = style.split(":")
|
||||
key = key.strip()
|
||||
val = val.strip()
|
||||
|
||||
if key == "font-size":
|
||||
font.setPointSize(int(val))
|
||||
font.setPointSizeF(float(val))
|
||||
elif key == "font-family":
|
||||
font.setFamily(val)
|
||||
elif key == "font-style" and val == "italic":
|
||||
@@ -252,7 +233,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
style = ""
|
||||
|
||||
style += "font-family: {};".format(self.font().family())
|
||||
style += "font-size: {};".format(self.font().pointSize())
|
||||
style += "font-size: {};".format(self.font().pointSizeF())
|
||||
|
||||
if self.font().italic():
|
||||
style += "font-style: italic;"
|
||||
216
gns3/items/line_item.py
Normal file
216
gns3/items/line_item.py
Normal file
@@ -0,0 +1,216 @@
|
||||
# -*- 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 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
|
||||
"""
|
||||
|
||||
# locked objects don't need cursors
|
||||
if not self.locked():
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
# locked objects don't need cursors
|
||||
if not self.locked():
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
@@ -21,12 +21,14 @@ Link items are graphical representation of a link on the QGraphicsScene
|
||||
"""
|
||||
|
||||
import math
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot, sip_is_deleted
|
||||
|
||||
from ..packet_capture import PacketCapture
|
||||
from ..dialogs.filter_dialog import FilterDialog
|
||||
from ..utils.get_icon import get_icon
|
||||
|
||||
|
||||
class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
|
||||
class SvgIconItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
def __init__(self, symbol, parent):
|
||||
|
||||
@@ -34,7 +36,8 @@ class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
|
||||
self.parentItem().mousePressEvent(event)
|
||||
if self.parentItem():
|
||||
self.parentItem().mousePressEvent(event)
|
||||
event.accept()
|
||||
|
||||
|
||||
@@ -86,11 +89,17 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
# QGraphicsSvgItem to indicate a capture
|
||||
self._capturing_item = None
|
||||
# QGraphicsSvgItem to indicate a filter is applied
|
||||
self._filter_item = None
|
||||
# QGraphicsSvgItem to indicate we suspend a link
|
||||
self._suspend_item = None
|
||||
# QGraphicsSvgItem to indicate a filter is applied and a capture is active
|
||||
self._filter_capturing_item = None
|
||||
|
||||
if not self._adding_flag:
|
||||
# there is a destination
|
||||
self._link = link
|
||||
self._link.updated_link_signal.connect(self._drawCaptureSymbol)
|
||||
self._link.updated_link_signal.connect(self._drawSymbol)
|
||||
self._link.delete_link_signal.connect(self._linkDeletedSlot)
|
||||
self.setFlag(self.ItemIsFocusable)
|
||||
source_item.addLink(self)
|
||||
@@ -118,6 +127,16 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
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
|
||||
@@ -205,14 +224,14 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
if not self._link.capturing():
|
||||
# start capture
|
||||
start_capture_action = QtWidgets.QAction("Start capture", menu)
|
||||
start_capture_action.setIcon(QtGui.QIcon(':/icons/capture-start.svg'))
|
||||
start_capture_action.setIcon(get_icon('capture-start.svg'))
|
||||
start_capture_action.triggered.connect(self._startCaptureActionSlot)
|
||||
menu.addAction(start_capture_action)
|
||||
|
||||
if self._link.capturing():
|
||||
# stop capture
|
||||
stop_capture_action = QtWidgets.QAction("Stop capture", menu)
|
||||
stop_capture_action.setIcon(QtGui.QIcon(':/icons/capture-stop.svg'))
|
||||
stop_capture_action.setIcon(get_icon('capture-stop.svg'))
|
||||
stop_capture_action.triggered.connect(self._stopCaptureActionSlot)
|
||||
menu.addAction(stop_capture_action)
|
||||
|
||||
@@ -228,12 +247,32 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
analyze_action.triggered.connect(self._analyzeCaptureActionSlot)
|
||||
menu.addAction(analyze_action)
|
||||
|
||||
if self._link.suspended() is False:
|
||||
# Edit filters
|
||||
filter_action = QtWidgets.QAction("Packet filters", menu)
|
||||
filter_action.setIcon(get_icon('filter.svg'))
|
||||
filter_action.triggered.connect(self._filterActionSlot)
|
||||
menu.addAction(filter_action)
|
||||
|
||||
# Suspend link
|
||||
suspend_action = QtWidgets.QAction("Suspend", menu)
|
||||
suspend_action.setIcon(get_icon('pause.svg'))
|
||||
suspend_action.triggered.connect(self._suspendActionSlot)
|
||||
menu.addAction(suspend_action)
|
||||
else:
|
||||
# Resume link
|
||||
resume_action = QtWidgets.QAction("Resume", menu)
|
||||
resume_action.setIcon(get_icon('start.svg'))
|
||||
resume_action.triggered.connect(self._suspendActionSlot)
|
||||
menu.addAction(resume_action)
|
||||
|
||||
# delete
|
||||
delete_action = QtWidgets.QAction("Delete", menu)
|
||||
delete_action.setIcon(QtGui.QIcon(':/icons/delete.svg'))
|
||||
delete_action.setIcon(get_icon('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.
|
||||
@@ -249,14 +288,15 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
QtWidgets.QApplication.sendEvent(MainWindow.instance(), key)
|
||||
return
|
||||
|
||||
# create the contextual menu
|
||||
self.setAcceptHoverEvents(False)
|
||||
menu = QtWidgets.QMenu()
|
||||
self.populateLinkContextualMenu(menu)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._hovered = False
|
||||
self.adjust()
|
||||
if not sip_is_deleted(self):
|
||||
# create the contextual menu
|
||||
self.setAcceptHoverEvents(False)
|
||||
menu = QtWidgets.QMenu()
|
||||
self.populateLinkContextualMenu(menu)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._hovered = False
|
||||
self.adjust()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
@@ -433,19 +473,90 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
self.update()
|
||||
|
||||
@qslot
|
||||
def _drawCaptureSymbol(self, *args):
|
||||
def _drawSymbol(self, *args):
|
||||
"""
|
||||
Draws a capture symbol in the middle of the link to indicate a capture is active.
|
||||
Draws a symbol in the middle of the link to indicate a capture, a suspend or a filter is active.
|
||||
"""
|
||||
|
||||
#FIXME: refactor ugly symbol management
|
||||
if not self._adding_flag:
|
||||
if self._link.capturing() and self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._capturing_item is None:
|
||||
self._capturing_item = SvgCaptureItem(':/icons/inspect.svg', self)
|
||||
self._capturing_item.setScale(0.6)
|
||||
self._capturing_item.setPos(link_center)
|
||||
if not self._capturing_item.isVisible():
|
||||
self._capturing_item.show()
|
||||
elif self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
|
||||
if self._link.suspended():
|
||||
if self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._suspend_item is None:
|
||||
self._suspend_item = SvgIconItem(':/icons/pause.svg', self)
|
||||
self._suspend_item.setScale(0.6)
|
||||
if not self._suspend_item.isVisible():
|
||||
self._suspend_item.show()
|
||||
self._suspend_item.setPos(link_center)
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
elif self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
|
||||
elif self._link.capturing() and len(self._link.filters()) > 0:
|
||||
if self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._filter_capturing_item is None:
|
||||
self._filter_capturing_item = SvgIconItem(':/icons/filter-capture.svg', self)
|
||||
self._filter_capturing_item.setScale(0.6)
|
||||
if not self._filter_capturing_item.isVisible():
|
||||
self._filter_capturing_item.show()
|
||||
self._filter_capturing_item.setPos(link_center)
|
||||
elif self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
|
||||
elif self._link.capturing():
|
||||
if self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._capturing_item is None:
|
||||
self._capturing_item = SvgIconItem(':/icons/inspect.svg', self)
|
||||
self._capturing_item.setScale(0.6)
|
||||
self._capturing_item.setPos(link_center)
|
||||
if not self._capturing_item.isVisible():
|
||||
self._capturing_item.show()
|
||||
elif self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
|
||||
elif len(self._link.filters()) > 0:
|
||||
if self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._filter_item is None:
|
||||
self._filter_item = SvgIconItem(':/icons/filter.svg', self)
|
||||
self._filter_item.setScale(0.6)
|
||||
if not self._filter_item.isVisible():
|
||||
self._filter_item.show()
|
||||
self._filter_item.setPos(link_center)
|
||||
elif self._filter_item:
|
||||
self._filter_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
|
||||
else:
|
||||
if self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
|
||||
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,11 +19,11 @@
|
||||
Graphical representation of a node on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import sip
|
||||
from ..qt import sip
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from .note_item import NoteItem
|
||||
from .label_item import LabelItem
|
||||
from ..symbol import Symbol
|
||||
from ..controller import Controller
|
||||
|
||||
@@ -41,7 +41,6 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
GRID_SIZE = 75
|
||||
|
||||
def __init__(self, node):
|
||||
super().__init__()
|
||||
@@ -51,6 +50,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
# link items connected to this node item.
|
||||
self._links = []
|
||||
self._symbol = None
|
||||
self._locked = False
|
||||
|
||||
# says if the attached node has been initialized
|
||||
# by the server.
|
||||
@@ -60,7 +60,6 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
self._node_label = None
|
||||
|
||||
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
|
||||
self.setZValue(self._node.z())
|
||||
|
||||
# Temporary symbol during loading
|
||||
renderer = QImageSvgRenderer(":/icons/reload.svg")
|
||||
@@ -80,6 +79,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
# update z value and locked state
|
||||
self.setLocked(self._node.locked())
|
||||
self.setZValue(self._node.z())
|
||||
|
||||
# connect signals to know about some events
|
||||
# e.g. when the node has been started, stopped or suspended etc.
|
||||
node.created_signal.connect(self.createdSlot)
|
||||
@@ -100,26 +103,17 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._main_window = MainWindow.instance()
|
||||
if self._main_window.uiSnapToGridAction.isChecked():
|
||||
self._snapToGrid()
|
||||
self._settings = self._main_window.uiGraphicsView.settings()
|
||||
|
||||
if node.initialized():
|
||||
self.createdSlot(node.id())
|
||||
|
||||
def _snapToGrid(self):
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
x = (self.GRID_SIZE * round((self.x() + mid_x) / self.GRID_SIZE)) - mid_x
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
y = (self.GRID_SIZE * round((self.y() + mid_y) / self.GRID_SIZE)) - mid_y
|
||||
self.setPos(x, y)
|
||||
|
||||
def updateNode(self):
|
||||
"""
|
||||
Sync change to the node
|
||||
"""
|
||||
if self._initialized:
|
||||
self._node.setGraphics(self)
|
||||
|
||||
self._node.setGraphics(self)
|
||||
|
||||
@qslot
|
||||
def setSymbol(self, symbol):
|
||||
@@ -142,10 +136,16 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
def symbol(self):
|
||||
return self._symbol
|
||||
|
||||
def _symbolLoadedCallback(self, path):
|
||||
renderer = QImageSvgRenderer(path)
|
||||
@qslot
|
||||
def _symbolLoadedCallback(self, path, *args):
|
||||
|
||||
renderer = QImageSvgRenderer(path, fallback=":/icons/cancel.svg")
|
||||
renderer.setObjectName(path)
|
||||
self.setSharedRenderer(renderer)
|
||||
if self._settings["limit_size_node_symbols"] is True and renderer.defaultSize().height() > 80:
|
||||
# resize the SVG
|
||||
renderer.resize(80)
|
||||
self.setSharedRenderer(renderer)
|
||||
if self._node.settings().get("symbol") != self._symbol:
|
||||
self.updateNode()
|
||||
if not self._initialized:
|
||||
@@ -263,7 +263,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
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.setLocked(self._node.settings().get("locked", False))
|
||||
self._updateLabel()
|
||||
|
||||
# update the link tooltips in case the
|
||||
@@ -356,7 +356,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
if not self._node_label:
|
||||
self._node_label = NoteItem(self)
|
||||
self._node_label = LabelItem(self)
|
||||
self._node_label.item_unselected_signal.connect(self._labelUnselectedSlot)
|
||||
self._node_label.setEditable(False)
|
||||
self._updateLabel()
|
||||
@@ -364,7 +364,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
def _updateLabel(self):
|
||||
"""
|
||||
Update the label using the informations stored in the node
|
||||
Update the label using the information stored in the node
|
||||
"""
|
||||
if not self._node_label:
|
||||
return
|
||||
@@ -373,8 +373,15 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
if self._node_label.toPlainText() != label_data["text"]:
|
||||
self._node_label.setPlainText(label_data["text"])
|
||||
self._node_label.setStyle(label_data["style"])
|
||||
self._node_label.setRotation(label_data["rotation"])
|
||||
|
||||
style = label_data.get("style")
|
||||
if style:
|
||||
self._node_label.setStyle(style)
|
||||
self._node_label.setRotation(label_data.get("rotation", 0))
|
||||
|
||||
if self._node.locked():
|
||||
self._node_label.setFlag(self.ItemIsMovable, False)
|
||||
|
||||
if label_data["x"] is None:
|
||||
self._centerLabel()
|
||||
self.updateNode()
|
||||
@@ -457,10 +464,11 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
grid_size = self._main_window.uiGraphicsView.nodeGridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
value.setX((self.GRID_SIZE * round((value.x() + mid_x) / self.GRID_SIZE)) - mid_x)
|
||||
value.setX((grid_size * round((value.x() + mid_x) / grid_size)) - mid_x)
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
value.setY((self.GRID_SIZE * round((value.y() + mid_y) / self.GRID_SIZE)) - mid_y)
|
||||
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 == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
@@ -514,20 +522,31 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
super().setZValue(value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
for link in self._links:
|
||||
link.adjust()
|
||||
|
||||
def locked(self):
|
||||
|
||||
return self._locked
|
||||
|
||||
def setLocked(self, locked):
|
||||
"""
|
||||
Sets the locked value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
if locked is True:
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
if self._node_label:
|
||||
self._node_label.setFlag(self.ItemIsSelectable, False)
|
||||
self._node_label.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
if self._node_label:
|
||||
self._node_label.setFlag(self.ItemIsSelectable, True)
|
||||
self._node_label.setFlag(self.ItemIsMovable, True)
|
||||
for link in self._links:
|
||||
link.adjust()
|
||||
self._locked = locked
|
||||
|
||||
def hoverEnterEvent(self, event):
|
||||
"""
|
||||
|
||||
@@ -61,4 +61,3 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
rect = self._styleSvg(rect)
|
||||
|
||||
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Graphical representation of a Serial link on the QGraphicsScene.
|
||||
import math
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .link_item import LinkItem
|
||||
from .note_item import NoteItem
|
||||
from .label_item import LabelItem
|
||||
from ..ports.port import Port
|
||||
|
||||
|
||||
@@ -107,50 +107,52 @@ class SerialLinkItem(LinkItem):
|
||||
|
||||
QtWidgets.QGraphicsPathItem.paint(self, painter, option, widget)
|
||||
|
||||
if not self._adding_flag and self._settings["draw_link_status_points"]:
|
||||
if not self._adding_flag:
|
||||
|
||||
# points disappears if nodes are too close to each others.
|
||||
if self.length < 80:
|
||||
return
|
||||
|
||||
# source point color
|
||||
if self._source_port.status() == Port.started:
|
||||
if self._link.suspended() or self._source_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.yellow
|
||||
elif self._source_port.status() == Port.started:
|
||||
# port is active
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.green
|
||||
elif self._source_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.yellow
|
||||
else:
|
||||
shape = QtCore.Qt.SquareCap
|
||||
color = QtCore.Qt.red
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
|
||||
source_port_label = self._source_port.label()
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
source_port_label = LabelItem(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:
|
||||
source_port_label.setFlag(source_port_label.ItemIsMovable, not self._source_item.locked())
|
||||
source_port_label.show()
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.source_point)
|
||||
if self._settings["draw_link_status_points"]:
|
||||
painter.drawPoint(self.source_point)
|
||||
|
||||
# destination point color
|
||||
if self._destination_port.status() == Port.started:
|
||||
if self._link.suspended() or self._destination_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
@@ -160,16 +162,18 @@ class SerialLinkItem(LinkItem):
|
||||
destination_port_label = self._destination_port.label()
|
||||
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
destination_port_label = LabelItem(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:
|
||||
destination_port_label.setFlag(destination_port_label.ItemIsMovable, not self._destination_item.locked())
|
||||
destination_port_label.show()
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.destination_point)
|
||||
if self._settings["draw_link_status_points"]:
|
||||
painter.drawPoint(self.destination_point)
|
||||
|
||||
self._drawCaptureSymbol()
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -20,7 +20,7 @@ Base class for shape items (Rectangle, ellipse etc.).
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .drawing_item import DrawingItem
|
||||
from .utils import colorFromSvg
|
||||
|
||||
@@ -30,17 +30,6 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class ShapeItem(DrawingItem):
|
||||
|
||||
# Map QT stroke to SVG style
|
||||
QT_DASH_TO_SVG = {
|
||||
QtCore.Qt.SolidLine: "",
|
||||
QtCore.Qt.NoPen: None,
|
||||
QtCore.Qt.DashLine: "25, 25",
|
||||
QtCore.Qt.DotLine: "5, 25",
|
||||
QtCore.Qt.DashDotLine: "5, 25, 25",
|
||||
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
Base class to draw shapes on the scene.
|
||||
"""
|
||||
@@ -51,6 +40,7 @@ class ShapeItem(DrawingItem):
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._border = 5
|
||||
self._edge = None
|
||||
self._originally_movable = True
|
||||
|
||||
if svg is None:
|
||||
self.setRect(0, 0, width, height)
|
||||
@@ -71,6 +61,7 @@ class ShapeItem(DrawingItem):
|
||||
"""
|
||||
|
||||
self.update()
|
||||
self._originally_movable = self.flags() & QtWidgets.QGraphicsItem.ItemIsMovable
|
||||
if event.pos().x() > (self.rect().right() - self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "right"
|
||||
@@ -86,7 +77,6 @@ class ShapeItem(DrawingItem):
|
||||
elif event.pos().y() > (self.rect().bottom() - self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "bottom"
|
||||
|
||||
QtWidgets.QGraphicsItem.mousePressEvent(self, event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
@@ -97,7 +87,7 @@ class ShapeItem(DrawingItem):
|
||||
"""
|
||||
|
||||
self.update()
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, self._originally_movable)
|
||||
self._edge = None
|
||||
QtWidgets.QGraphicsItem.mouseReleaseEvent(self, event)
|
||||
|
||||
@@ -157,8 +147,8 @@ class ShapeItem(DrawingItem):
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
# objects on the background layer don't need cursors
|
||||
if self.zValue() >= 0:
|
||||
# locked objects don't need cursors
|
||||
if not self.locked():
|
||||
if event.pos().x() > (self.rect().right() - self._border):
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
|
||||
elif event.pos().x() < (self.rect().left() + self._border):
|
||||
@@ -177,31 +167,10 @@ class ShapeItem(DrawingItem):
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
# objects on the background layer don't need cursors
|
||||
if self.zValue() >= 0:
|
||||
# locked objects don't need cursors
|
||||
if not self.locked():
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def _styleSvg(self, element):
|
||||
"""
|
||||
Add style from the shape item to the SVG element that we will
|
||||
export
|
||||
"""
|
||||
style = ""
|
||||
pen = self.pen()
|
||||
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
|
||||
element.set("fill-opacity", str(self.brush().color().alphaF()))
|
||||
|
||||
dasharray = self.QT_DASH_TO_SVG[pen.style()]
|
||||
if dasharray is None: # No border to the element
|
||||
return element
|
||||
elif dasharray == "":
|
||||
pass # Solid line
|
||||
else:
|
||||
element.set("stroke-dasharray", dasharray)
|
||||
element.set("stroke-width", str(pen.width()))
|
||||
element.set("stroke", "#" + hex(pen.color().rgba())[4:])
|
||||
return element
|
||||
|
||||
def fromSvg(self, svg):
|
||||
"""
|
||||
Import element informations from an SVG
|
||||
@@ -215,10 +184,7 @@ class ShapeItem(DrawingItem):
|
||||
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
|
||||
|
||||
if len(svg):
|
||||
if svg[0].get("stroke-width"):
|
||||
pen.setWidth(int(svg[0].get("stroke-width")))
|
||||
if svg[0].get("stroke"):
|
||||
pen.setColor(colorFromSvg(svg[0].get("stroke")))
|
||||
pen = self._penFromSVGElement(svg[0])
|
||||
if svg[0].get("fill"):
|
||||
new_color = colorFromSvg(svg[0].get("fill"))
|
||||
color = brush.color()
|
||||
@@ -231,17 +197,6 @@ class ShapeItem(DrawingItem):
|
||||
color.setAlphaF(float(svg[0].get("fill-opacity")))
|
||||
brush.setColor(color)
|
||||
|
||||
# Map SVG stroke style (border of the element to the Qt version)
|
||||
if not svg[0].get("stroke"):
|
||||
pen.setStyle(QtCore.Qt.NoPen)
|
||||
else:
|
||||
pen.setStyle(QtCore.Qt.SolidLine)
|
||||
stroke = svg[0].get("stroke-dasharray")
|
||||
if stroke:
|
||||
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
|
||||
if svg_stroke == stroke:
|
||||
pen.setStyle(qt_stroke)
|
||||
|
||||
self.setPen(pen)
|
||||
self.setBrush(brush)
|
||||
self.update()
|
||||
|
||||
@@ -15,10 +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/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a note on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
@@ -44,8 +40,8 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
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"]))
|
||||
qt_font.fromString(view_settings["default_note_font"])
|
||||
self.setDefaultTextColor(QtGui.QColor(view_settings["default_note_color"]))
|
||||
self.setFont(qt_font)
|
||||
|
||||
if svg:
|
||||
@@ -54,6 +50,10 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
except ET.ParseError as e:
|
||||
log.warning(str(e))
|
||||
|
||||
# re-evaluate `z` position after creation
|
||||
if 'z' in kws.keys():
|
||||
self.setZValue(kws['z'])
|
||||
|
||||
if self._id is None:
|
||||
self.create()
|
||||
|
||||
@@ -121,7 +121,7 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
|
||||
text = ET.SubElement(svg, "text")
|
||||
text.set("font-family", self.font().family())
|
||||
text.set("font-size", str(self.font().pointSize()))
|
||||
text.set("font-size", str(self.font().pointSizeF()))
|
||||
if self.font().italic():
|
||||
text.set("font-style", "italic")
|
||||
if self.font().bold():
|
||||
@@ -138,7 +138,19 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
return svg
|
||||
|
||||
def fromSvg(self, svg):
|
||||
svg = ET.fromstring(svg)
|
||||
|
||||
# sometimes we receive \0 at the end of string inside <svg> element
|
||||
try:
|
||||
svg = svg.replace("\u0000", "")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
svg = ET.fromstring(svg)
|
||||
except ET.ParseError:
|
||||
self.setPlainText("Unable to parse `text_item`")
|
||||
return
|
||||
|
||||
text = svg[0]
|
||||
|
||||
font = QtGui.QFont()
|
||||
@@ -157,7 +169,7 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
color.setAlphaF(float(opacity))
|
||||
self.setDefaultTextColor(color)
|
||||
|
||||
font.setPointSize(int(text.get("font-size", self.font().pointSize())))
|
||||
font.setPointSizeF(float(text.get("font-size", self.font().pointSizeF())))
|
||||
font.setFamily(text.get("font-family", self.font().family()))
|
||||
if text.get("font-style") == "italic":
|
||||
font.setItalic(True)
|
||||
|
||||
201
gns3/link.py
201
gns3/link.py
@@ -21,10 +21,10 @@ Manages and stores everything needed for a connection between 2 devices.
|
||||
|
||||
import os
|
||||
import re
|
||||
import sip
|
||||
from .qt import sip
|
||||
import uuid
|
||||
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .qt import QtCore
|
||||
from .controller import Controller
|
||||
|
||||
|
||||
@@ -59,10 +59,10 @@ class Link(QtCore.QObject):
|
||||
|
||||
super().__init__()
|
||||
|
||||
log.info("adding link from {} {} to {} {}".format(source_node.name(),
|
||||
source_port.name(),
|
||||
destination_node.name(),
|
||||
destination_port.name()))
|
||||
log.debug("adding link from {} {} to {} {}".format(source_node.name(),
|
||||
source_port.name(),
|
||||
destination_node.name(),
|
||||
destination_port.name()))
|
||||
|
||||
# create an unique ID
|
||||
self._id = Link._instance_count
|
||||
@@ -76,13 +76,17 @@ class Link(QtCore.QObject):
|
||||
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._capture_compute_id = None
|
||||
self._initialized = False
|
||||
self._filters = {}
|
||||
self._suspend = False
|
||||
|
||||
# Boolean if True we are creatin the first instance of this node
|
||||
# Boolean if True we are creating the first instance of this node
|
||||
# if false the node already exist in the topology
|
||||
# use to avoid erasing informations when reloading
|
||||
# use to avoid erasing information when reloading
|
||||
self._creator = False
|
||||
|
||||
self._nodes = []
|
||||
@@ -100,35 +104,48 @@ class Link(QtCore.QObject):
|
||||
Controller.instance().post("/projects/{project_id}/links".format(project_id=source_node.project().id()), self._linkCreatedCallback, body=body)
|
||||
|
||||
def _parseResponse(self, result):
|
||||
self._capturing = result.get("capturing", False)
|
||||
|
||||
# If the controller is remote the capture path should be rewrite to something local
|
||||
if Controller.instance().isRemote():
|
||||
if self._capture_file_path is None and result.get("capture_file_path", None) is not None:
|
||||
self._capture_file = QtCore.QTemporaryFile()
|
||||
self._capture_file.open(QtCore.QFile.WriteOnly)
|
||||
self._capture_file.setAutoRemove(True)
|
||||
self._capture_file_path = self._capture_file.fileName()
|
||||
Controller.instance().get(
|
||||
"/projects/{project_id}/links/{link_id}/pcap".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id),
|
||||
None,
|
||||
showProgress=False,
|
||||
downloadProgressCallback=self._downloadPcapProgress,
|
||||
ignoreErrors=True, # If something is wrong avoid disconnect us from server
|
||||
timeout=None)
|
||||
else:
|
||||
self._capture_file_path = result["capture_file_path"]
|
||||
self._capturing = result.get("capturing", False)
|
||||
if self._capturing:
|
||||
self._capture_compute_id = result.get("capture_compute_id", None)
|
||||
self._capture_file_path = result.get("capture_file_path", None)
|
||||
if Controller.instance().isRemote() or (self._capture_compute_id and self._capture_compute_id != "local"):
|
||||
# We need to stream the pcap file content if the controller or compute is remote
|
||||
if Controller.instance().isRemote() or self._capture_file_path is 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()
|
||||
else:
|
||||
self._capture_file = QtCore.QFile(self._capture_file_path)
|
||||
self._capture_file.open(QtCore.QFile.WriteOnly)
|
||||
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)
|
||||
log.debug("Capturing packets to '{}'".format(self._capture_file_path))
|
||||
|
||||
if "nodes" in result:
|
||||
self._nodes = result["nodes"]
|
||||
self._updateLabels()
|
||||
if "filters" in result:
|
||||
self._filters = result["filters"]
|
||||
if "suspend" in result:
|
||||
self._suspend = result["suspend"]
|
||||
self.updated_link_signal.emit(self._id)
|
||||
|
||||
def creator(self):
|
||||
return self._creator
|
||||
|
||||
def suspended(self):
|
||||
return self._suspend
|
||||
|
||||
def toggleSuspend(self):
|
||||
self._suspend = not self._suspend
|
||||
self.update()
|
||||
|
||||
def initialized(self):
|
||||
return self._initialized
|
||||
|
||||
@@ -144,14 +161,20 @@ class Link(QtCore.QObject):
|
||||
self._updateLabels()
|
||||
|
||||
def update(self):
|
||||
if not self._link_id:
|
||||
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"]))
|
||||
log.warning("Error while updating link: {}".format(result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
@@ -167,10 +190,14 @@ class Link(QtCore.QObject):
|
||||
def _updateLabel(self, label, label_data):
|
||||
if not label or sip.isdeleted(label):
|
||||
return
|
||||
label.setPlainText(label_data["text"])
|
||||
label.setPos(label_data["x"], label_data["y"])
|
||||
label.setStyle(label_data["style"])
|
||||
label.setRotation(label_data["rotation"])
|
||||
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 = {
|
||||
@@ -185,7 +212,9 @@ class Link(QtCore.QObject):
|
||||
"adapter_number": self._destination_port.adapterNumber(),
|
||||
"port_number": self._destination_port.portNumber()
|
||||
}
|
||||
]
|
||||
],
|
||||
"filters": self._filters,
|
||||
"suspend": self._suspend
|
||||
}
|
||||
if self._source_port.label():
|
||||
body["nodes"][0]["label"] = self._source_port.label().dump()
|
||||
@@ -195,7 +224,7 @@ class Link(QtCore.QObject):
|
||||
|
||||
def _linkCreatedCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Create link", "Error while creating link: {}".format(result["message"]))
|
||||
log.warning("Error while creating link: {}".format(result["message"]))
|
||||
self.deleteLink(skip_controller=True)
|
||||
return
|
||||
|
||||
@@ -218,6 +247,19 @@ class Link(QtCore.QObject):
|
||||
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?
|
||||
@@ -243,10 +285,18 @@ class Link(QtCore.QObject):
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Link from {} port {} to {} port {}".format(self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
description = "Link from {} port {} to {} port {}".format(self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
|
||||
if self.capturing():
|
||||
description += "\nPacket capture is active"
|
||||
|
||||
for filter_type in self._filters.keys():
|
||||
description += "\nPacket filter '{}' is active".format(filter_type)
|
||||
|
||||
return description
|
||||
|
||||
def capture_file_name(self):
|
||||
"""
|
||||
@@ -257,23 +307,25 @@ class Link(QtCore.QObject):
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
return re.sub("[^0-9A-Za-z_-]", "", capture_file_name)
|
||||
return re.sub(r"[^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)
|
||||
link_id=self._link_id),
|
||||
self._linkDeletedCallback)
|
||||
|
||||
def _linkDeletedCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -298,62 +350,57 @@ class Link(QtCore.QObject):
|
||||
"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)
|
||||
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)
|
||||
#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 Controller.instance().isRemote() or (self._capture_compute_id and self._capture_compute_id != "local"):
|
||||
if self._capture_file:
|
||||
self._capture_file.close()
|
||||
self._capture_file = None
|
||||
if self._capture_file_path:
|
||||
try:
|
||||
os.remove(self._capture_file_path)
|
||||
except OSError as e:
|
||||
log.error("Can't remove file {}".format(self._capture_file_path))
|
||||
# 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("Cannot 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)
|
||||
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)
|
||||
#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)
|
||||
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):
|
||||
"""
|
||||
@@ -409,3 +456,15 @@ class Link(QtCore.QObject):
|
||||
if self._destination_node == node:
|
||||
return self._destination_port
|
||||
return self._source_port
|
||||
|
||||
def filters(self):
|
||||
"""
|
||||
:returns: List the filters active on the node
|
||||
"""
|
||||
return self._filters
|
||||
|
||||
def setFilters(self, filters):
|
||||
"""
|
||||
:params filters: List of filters
|
||||
"""
|
||||
self._filters = filters
|
||||
|
||||
@@ -23,10 +23,11 @@ import copy
|
||||
|
||||
import psutil
|
||||
|
||||
from .qt import QtCore, QtWidgets, qslot
|
||||
from .version import __version__
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .version import __version__, __version_info__
|
||||
from .utils import parse_version
|
||||
from .controller import Controller
|
||||
from .local_server_config import LocalServerConfig
|
||||
from .settings import LOCAL_SERVER_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -39,38 +40,17 @@ class LocalConfig(QtCore.QObject):
|
||||
"""
|
||||
|
||||
config_changed_signal = QtCore.Signal()
|
||||
# When this signal is emit the config is saved on controller
|
||||
save_on_controller_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, config_file=None):
|
||||
"""
|
||||
:param config_file: Path to the config file (override all other config, usefull for tests)
|
||||
:param config_file: Path to the config file (override all other config, useful 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):
|
||||
"""
|
||||
@@ -109,8 +89,25 @@ class LocalConfig(QtCore.QObject):
|
||||
try:
|
||||
# create the config file if it doesn't exist
|
||||
os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
|
||||
with open(self._config_file, "w", encoding="utf-8") as f:
|
||||
json.dump({"version": __version__, "type": "settings"}, f)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
old_config_path = os.path.join(os.path.expandvars("%APPDATA%"), "GNS3", filename)
|
||||
else:
|
||||
old_config_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3", filename)
|
||||
|
||||
# TODO: migrate versioned config file from a previous version of GNS3 (for instance 2.2 -> 2.3) + support profiles
|
||||
if os.path.exists(old_config_path):
|
||||
# migrate post version 2.2.0 configuration file
|
||||
shutil.copyfile(old_config_path, self._config_file)
|
||||
# reset the local server path and ubridge path
|
||||
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
|
||||
settings["path"] = ""
|
||||
settings["ubridge_path"] = ""
|
||||
LocalServerConfig.instance().saveSettings("Server", settings)
|
||||
else:
|
||||
# create a new config
|
||||
with open(self._config_file, "w", encoding="utf-8") as f:
|
||||
json.dump({"version": __version__, "type": "settings"}, f)
|
||||
except OSError as e:
|
||||
log.error("Could not create the config file {}: {}".format(self._config_file, e))
|
||||
|
||||
@@ -136,53 +133,31 @@ class LocalConfig(QtCore.QObject):
|
||||
self._config_file = None
|
||||
self._resetLoadConfig()
|
||||
|
||||
@qslot
|
||||
def refreshConfigFromController(self):
|
||||
"""
|
||||
Refresh the configuration from the controller
|
||||
"""
|
||||
controller = Controller.instance()
|
||||
if controller.connected():
|
||||
self._refreshingSettings = True
|
||||
controller.get("/settings", self._getSettingsCallback, showProgress=False)
|
||||
self._monitorChanges()
|
||||
|
||||
def _getSettingsCallback(self, result, error=False, **kwargs):
|
||||
self._refreshingSettings = False
|
||||
if error:
|
||||
log.error("Can't get settings from controller")
|
||||
return
|
||||
if result == {} and self._settings != {}:
|
||||
self._settings_retrieved_from_controller = True
|
||||
self.save_on_controller_signal.emit()
|
||||
return
|
||||
|
||||
# The server return an uuid to keep track of settings version
|
||||
if self._settings.get("modification_uuid") != result.get("modification_uuid"):
|
||||
self._settings.update(result)
|
||||
# Update already loaded section
|
||||
for section in self._settings.keys():
|
||||
if isinstance(self._settings[section], dict):
|
||||
self.loadSectionSettings(section, self._settings[section])
|
||||
self.config_changed_signal.emit()
|
||||
self._settings_retrieved_from_controller = True
|
||||
|
||||
def configDirectory(self):
|
||||
"""
|
||||
Get the configuration directory
|
||||
"""
|
||||
|
||||
version = "{}.{}".format(__version_info__[0], __version_info__[1])
|
||||
if sys.platform.startswith("win"):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
path = os.path.join(appdata, "GNS3")
|
||||
path = os.path.join(appdata, "GNS3", version)
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
path = os.path.join(home, ".config", "GNS3")
|
||||
path = os.path.join(home, ".config", "GNS3", version)
|
||||
|
||||
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
|
||||
@@ -191,8 +166,9 @@ class LocalConfig(QtCore.QObject):
|
||||
# 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"):
|
||||
version = "{}.{}".format(__version_info__[0], __version_info__[1])
|
||||
old_path = os.path.join(os.path.expanduser("~"), ".config", "gns3.net")
|
||||
new_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3")
|
||||
new_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3", version)
|
||||
if os.path.exists(old_path) and not os.path.exists(new_path):
|
||||
try:
|
||||
shutil.copytree(old_path, new_path)
|
||||
@@ -201,7 +177,7 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
def _migrateOldConfig(self):
|
||||
"""
|
||||
Migrate pre 1.4 config
|
||||
Migrate config from a previous version.
|
||||
"""
|
||||
|
||||
# Display an error if settings come from a more recent version of GNS3
|
||||
@@ -209,28 +185,31 @@ class LocalConfig(QtCore.QObject):
|
||||
# settings from 1.6.1 with 1.5.1 you will have an error
|
||||
if "version" in self._settings:
|
||||
if parse_version(self._settings["version"])[:2] > parse_version(__version__)[:2]:
|
||||
QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
|
||||
QtWidgets.QMessageBox.critical(None, "Version error", "Your settings are for version {} of GNS3. You cannot use a previous version of GNS3 without risking losing data.".format(self._settings["version"]))
|
||||
app = QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
|
||||
error_message = "Settings are for version {} of GNS3. It is not possible to use a previous version of GNS3 without risking losing data. Delete the settings in '{}' to start GNS3".format(self._settings["version"], self.configDirectory())
|
||||
QtWidgets.QMessageBox.critical(False, "Version error", error_message)
|
||||
# Exit immediately not clean but we want to avoid any side effect that could corrupt the file
|
||||
QtCore.QTimer.singleShot(0, app.quit)
|
||||
app.exec_()
|
||||
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", {})
|
||||
servers = self._settings.get("Servers", {})
|
||||
|
||||
if "LocalServer" in self._settings:
|
||||
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
|
||||
# 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:
|
||||
if "RemoteServers" in self._settings:
|
||||
servers["remote_servers"] = copy.copy(self._settings["RemoteServers"])
|
||||
|
||||
self._settings["Servers"] = servers
|
||||
self._settings["Servers"] = servers
|
||||
|
||||
if "GUI" in self._settings:
|
||||
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
|
||||
@@ -243,7 +222,7 @@ class LocalConfig(QtCore.QObject):
|
||||
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
|
||||
# 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
|
||||
@@ -254,22 +233,24 @@ class LocalConfig(QtCore.QObject):
|
||||
vms.append(vm)
|
||||
self._settings["Qemu"]["vms"] = vms
|
||||
|
||||
# Starting with 2.0.0dev5 IOU licence is stored in the settings
|
||||
# Starting with 2.0.0dev5 IOU licence is stored in the settings
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
|
||||
if "IOU" in self._settings and "iourc_path" in self._settings["IOU"] and "iourc_content" not in self._settings["IOU"]:
|
||||
try:
|
||||
with open(self._settings["IOU"]["iourc_path"], "r") as f:
|
||||
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.warn("Can't import IOU licence {}: {}".format(self._settings["IOU"]["iourc_path"], str(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.info("Load config from %s", config_path)
|
||||
log.debug("Load config from %s", config_path)
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
self._last_config_changed = os.stat(config_path).st_mtime
|
||||
@@ -296,35 +277,16 @@ class LocalConfig(QtCore.QObject):
|
||||
with open(temporary, "w", encoding="utf-8") as f:
|
||||
json.dump(self._settings, f, sort_keys=True, indent=4)
|
||||
shutil.move(temporary, self._config_file)
|
||||
log.info("Configuration save to %s", self._config_file)
|
||||
log.debug("Configuration save to %s", self._config_file)
|
||||
self._last_config_changed = os.stat(self._config_file).st_mtime
|
||||
except (ValueError, OSError) as e:
|
||||
log.error("Could not write the config file {}: {}".format(self._config_file, e))
|
||||
self.save_on_controller_signal.emit()
|
||||
|
||||
@qslot
|
||||
def _saveOnController(self, *args):
|
||||
"""
|
||||
Save some settings on controller for the transition from
|
||||
GUI to a central controller. Will be removed later
|
||||
"""
|
||||
if Controller.instance().connected() and self._settings_retrieved_from_controller:
|
||||
# We save only non user specific sections
|
||||
section_to_save_on_controller = ["Builtin", "Docker", "IOU", "Qemu", "VMware", "VPCS", "VirtualBox", "GraphicsView", "Dynamips"]
|
||||
controller_settings = {}
|
||||
for key, val in self._settings.items():
|
||||
if key in section_to_save_on_controller:
|
||||
controller_settings[key] = val
|
||||
# We want only the VM settings on the server
|
||||
elif key == "Server":
|
||||
controller_settings["Server"]["vm"] = self._settings["Server"]["vm"]
|
||||
Controller.instance().post("/settings", None, body=controller_settings)
|
||||
|
||||
def checkConfigChanged(self):
|
||||
|
||||
try:
|
||||
if self._last_config_changed and self._last_config_changed < os.stat(self._config_file).st_mtime:
|
||||
log.info("Client config has changed, reloading it...")
|
||||
log.debug("Client config has changed, reloading it...")
|
||||
self._readConfig(self._config_file)
|
||||
self.config_changed_signal.emit()
|
||||
except OSError as e:
|
||||
@@ -402,9 +364,8 @@ class LocalConfig(QtCore.QObject):
|
||||
self._settings[section] = settings
|
||||
|
||||
if changed:
|
||||
log.info("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
|
||||
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):
|
||||
@@ -420,7 +381,7 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
if self._settings[section] != settings:
|
||||
self._settings[section].update(copy.deepcopy(settings))
|
||||
log.info("Section %s has changed. Saving configuration", section)
|
||||
log.debug("Section %s has changed. Saving configuration", section)
|
||||
self.writeConfig()
|
||||
else:
|
||||
log.debug("Section %s has not changed. Skip saving configuration", section)
|
||||
@@ -455,6 +416,50 @@ class LocalConfig(QtCore.QObject):
|
||||
settings["multi_profiles"] = value
|
||||
self.saveSectionSettings("MainWindow", settings)
|
||||
|
||||
def directFileUpload(self):
|
||||
"""
|
||||
:returns: Boolean. True if direct_file_upload is enabled
|
||||
"""
|
||||
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["direct_file_upload"]
|
||||
|
||||
def setDirectFileUpload(self, value):
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
settings = self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)
|
||||
settings["direct_file_upload"] = value
|
||||
self.saveSectionSettings("MainWindow", settings)
|
||||
|
||||
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)
|
||||
|
||||
def showGridOnNewProject(self):
|
||||
"""
|
||||
:returns: Boolean. True if show_grid_on_new_project is enabled
|
||||
"""
|
||||
|
||||
from gns3.settings import GRAPHICS_VIEW_SETTINGS
|
||||
return self.loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS).get("show_grid_on_new_project", False)
|
||||
|
||||
def snapToGridOnNewProject(self):
|
||||
"""
|
||||
:returns: Boolean. True if snap_to_grid_on_new_project is enabled
|
||||
"""
|
||||
|
||||
from gns3.settings import GRAPHICS_VIEW_SETTINGS
|
||||
return self.loadSectionSettings("GraphicsView", GRAPHICS_VIEW_SETTINGS).get("snap_to_grid_on_new_project", False)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
|
||||
@@ -30,13 +30,12 @@ import signal
|
||||
import subprocess
|
||||
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore
|
||||
from gns3.qt import QtWidgets, QtCore, qslot
|
||||
from gns3.settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
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
|
||||
@@ -59,17 +58,20 @@ class StopLocalServerWorker(QtCore.QObject):
|
||||
def __init__(self, local_server_process):
|
||||
super().__init__()
|
||||
self._local_server_process = local_server_process
|
||||
self._precision = 100 # In MS
|
||||
self._remaining_trial = int(10 * (1000 / self._precision))
|
||||
|
||||
@qslot
|
||||
def _callbackSlot(self, *params):
|
||||
self._local_server_process.poll()
|
||||
if self._local_server_process.returncode is None and self._remaining_trial > 0:
|
||||
self._remaining_trial -= 1
|
||||
QtCore.QTimer.singleShot(self._precision, self._callbackSlot)
|
||||
else:
|
||||
self.finished.emit()
|
||||
|
||||
def run(self):
|
||||
precision = 1
|
||||
remaining_trial = 4 / precision # 4 Seconds
|
||||
while remaining_trial > 0:
|
||||
if self._local_server_process.returncode is None:
|
||||
remaining_trial -= 1
|
||||
self.thread().sleep(precision)
|
||||
else:
|
||||
break
|
||||
self.finished.emit()
|
||||
QtCore.QTimer.singleShot(1000, self._callbackSlot)
|
||||
|
||||
def cancel(self):
|
||||
return
|
||||
@@ -121,18 +123,23 @@ class LocalServer(QtCore.QObject):
|
||||
return self._parent
|
||||
|
||||
def _checkWindowsService(self, service_name):
|
||||
import pywintypes
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
try:
|
||||
import pywintypes
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
except ImportError as e:
|
||||
log.error("Could not check if the {} service is running: {}".format(service_name, e))
|
||||
|
||||
try:
|
||||
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
|
||||
return False
|
||||
except pywintypes.error as e:
|
||||
if e.winerror == 1060:
|
||||
if e.winerror == 1060: # service is not installed
|
||||
return False
|
||||
else:
|
||||
log.error("Could not check if the {} service is running: {}".format(service_name, e.strerror))
|
||||
|
||||
return True
|
||||
|
||||
def _checkUbridgePermissions(self):
|
||||
@@ -142,7 +149,7 @@ class LocalServer(QtCore.QObject):
|
||||
|
||||
path = os.path.abspath(self._settings["ubridge_path"])
|
||||
|
||||
if not path or len(path) == 0 or not os.path.exists(path):
|
||||
if not path or len(path) == 0 or not os.path.exists(path) or not os.path.isfile(path):
|
||||
return False
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
@@ -157,28 +164,23 @@ class LocalServer(QtCore.QObject):
|
||||
if sys.platform.startswith("linux"):
|
||||
# test if the executable has the CAP_NET_RAW capability (Linux only)
|
||||
try:
|
||||
if "security.capability" in os.listxattr(path):
|
||||
caps = os.getxattr(path, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge? All users on the system will be able to read packet from the network interfaces.",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
sudo(["setcap", "cap_net_admin,cap_net_raw=ep"])
|
||||
else:
|
||||
# capabilities not supported
|
||||
request_setuid = True
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if "security.capability" not in os.listxattr(path) or not struct.unpack("<IIIII", os.getxattr(path, "security.capability"))[1] & 1 << 13:
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge? All users on the system will be able to read packet from the network interfaces.",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
sudo(["setcap", "cap_net_admin,cap_net_raw=ep", path])
|
||||
except AttributeError:
|
||||
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
|
||||
log.warning("Could not determine if CAP_NET_RAW capability is set for uBridge (Python bug)")
|
||||
return True
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set CAP_NET_RAW capability to uBridge {}: {}".format(path, str(e)))
|
||||
return False
|
||||
request_setuid = True
|
||||
|
||||
if sys.platform.startswith("darwin") or request_setuid:
|
||||
try:
|
||||
@@ -186,7 +188,7 @@ class LocalServer(QtCore.QObject):
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge? All admin users on the system will be able to read packet from the network interfaces.",
|
||||
"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:
|
||||
@@ -264,7 +266,7 @@ class LocalServer(QtCore.QObject):
|
||||
if need_restart:
|
||||
self.stopLocalServer(wait=True)
|
||||
|
||||
self.localServerAutoStartIfRequire()
|
||||
self.localServerAutoStartIfRequired()
|
||||
# If the controller is remote:
|
||||
else:
|
||||
self.stopLocalServer(wait=True)
|
||||
@@ -310,9 +312,9 @@ class LocalServer(QtCore.QObject):
|
||||
# Permission issue, or process no longer exists, or file is empty
|
||||
return
|
||||
|
||||
def localServerAutoStartIfRequire(self):
|
||||
def localServerAutoStartIfRequired(self):
|
||||
"""
|
||||
Try to start the embed gns3 server.
|
||||
Try to start the embedded gns3 server.
|
||||
"""
|
||||
|
||||
if not self.shouldLocalServerAutoStart():
|
||||
@@ -332,7 +334,7 @@ class LocalServer(QtCore.QObject):
|
||||
return True
|
||||
|
||||
if self.isLocalServerRunning():
|
||||
log.info("A local server already running on this host")
|
||||
log.debug("A local server already running on this host")
|
||||
# Try to kill the server. The server can be still running after
|
||||
# if the server was started by hand
|
||||
self._killAlreadyRunningServer()
|
||||
@@ -357,7 +359,6 @@ class LocalServer(QtCore.QObject):
|
||||
self._server_started_by_me = True
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
|
||||
return True
|
||||
|
||||
def initLocalServer(self):
|
||||
@@ -369,15 +370,13 @@ class LocalServer(QtCore.QObject):
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Error", "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
|
||||
return False
|
||||
log.warning("The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
|
||||
|
||||
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")
|
||||
log.warning("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))
|
||||
@@ -464,10 +463,10 @@ class LocalServer(QtCore.QObject):
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as e:
|
||||
log.warn("could not delete server log file {}: {}".format(logpath, e))
|
||||
log.warning("could not delete server log file {}: {}".format(logpath, e))
|
||||
command += ' --log="{}" --pid="{}"'.format(logpath, self._pid_path())
|
||||
|
||||
log.info("Starting local server process with {}".format(command))
|
||||
log.debug("Starting local server process with {}".format(command))
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
# use the string on Windows
|
||||
@@ -480,7 +479,7 @@ class LocalServer(QtCore.QObject):
|
||||
log.warning('Could not start local server "{}": {}'.format(command, e))
|
||||
return False
|
||||
|
||||
log.info("Local server process has started (PID={})".format(self._local_server_process.pid))
|
||||
log.debug("Local server process has started (PID={})".format(self._local_server_process.pid))
|
||||
return True
|
||||
|
||||
def _checkLocalServerRunningSlot(self):
|
||||
@@ -514,10 +513,12 @@ class LocalServer(QtCore.QObject):
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
status, json_data = getSynchronous(self._settings["protocol"], self._settings["host"], self._port, "version",
|
||||
timeout=2, user=self._settings["user"], password=self._settings["password"])
|
||||
|
||||
if json_data is None or status != 200:
|
||||
status, json_data = HTTPClient(self._settings).getSynchronous("GET", "/version")
|
||||
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)
|
||||
@@ -535,7 +536,7 @@ class LocalServer(QtCore.QObject):
|
||||
|
||||
if self.localServerProcessIsRunning():
|
||||
self._stopping = True
|
||||
log.info("Stopping local server (PID={})".format(self._local_server_process.pid))
|
||||
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()
|
||||
|
||||
@@ -138,6 +138,18 @@ class LocalServerConfig:
|
||||
if changed:
|
||||
self.writeConfig()
|
||||
|
||||
def deleteSetting(self, section, name):
|
||||
"""
|
||||
Delete a specific setting in a given section.
|
||||
|
||||
:param section: section name
|
||||
:param name: setting name to delete
|
||||
"""
|
||||
|
||||
if section in self._config and name in self._config[section]:
|
||||
del self._config[section][name]
|
||||
self.writeConfig()
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
|
||||
@@ -121,7 +121,7 @@ def init_logger(level, logfile, quiet=False):
|
||||
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.warning("could not log to {}: {}".format(logfile, e))
|
||||
|
||||
log.info('Log level: {}'.format(logging.getLevelName(level)))
|
||||
|
||||
|
||||
34
gns3/main.py
34
gns3/main.py
@@ -18,6 +18,7 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import faulthandler
|
||||
|
||||
# Try to install updates & restart application if an update is installed
|
||||
try:
|
||||
@@ -89,13 +90,13 @@ def locale_check():
|
||||
log.error("could not determine the current locale: {}".format(e))
|
||||
if not language and not encoding:
|
||||
try:
|
||||
log.warn("could not find a default locale, switching to C.UTF-8...")
|
||||
log.warning("could not find a default locale, switching to C.UTF-8...")
|
||||
locale.setlocale(locale.LC_ALL, ("C", "UTF-8"))
|
||||
except locale.Error as e:
|
||||
log.error("could not switch to the C.UTF-8 locale: {}".format(e))
|
||||
raise SystemExit
|
||||
elif encoding != "UTF-8":
|
||||
log.warn("your locale {}.{} encoding is not UTF-8, switching to the UTF-8 version...".format(language, encoding))
|
||||
log.warning("your locale {}.{} encoding is not UTF-8, switching to the UTF-8 version...".format(language, encoding))
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, (language, "UTF-8"))
|
||||
except locale.Error as e:
|
||||
@@ -110,6 +111,9 @@ def main():
|
||||
Entry point for GNS3 GUI.
|
||||
"""
|
||||
|
||||
# Get Python tracebacks explicitly, on a fault like segfault
|
||||
faulthandler.enable()
|
||||
|
||||
# Sometimes (for example at first launch) the OSX app service launcher add
|
||||
# an extra argument starting with -psn_. We filter it
|
||||
if sys.platform.startswith("darwin"):
|
||||
@@ -133,15 +137,13 @@ def main():
|
||||
# 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'))
|
||||
]
|
||||
frozen_dirs = [frozen_dir]
|
||||
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, 'vpcs')),
|
||||
os.path.normpath(os.path.join(frozen_dir, 'traceng'))
|
||||
]
|
||||
|
||||
os.environ["PATH"] = os.pathsep.join(frozen_dirs) + os.pathsep + os.environ.get("PATH", "")
|
||||
@@ -184,8 +186,8 @@ def main():
|
||||
if sys.version_info < (3, 4):
|
||||
raise SystemExit("Python 3.4 or higher is required")
|
||||
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.0.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.0.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.5.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.5.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
|
||||
if parse_version(psutil.__version__) < parse_version("2.2.1"):
|
||||
raise SystemExit("Requirement is psutil version 2.2.1 or higher, got version {}".format(psutil.__version__))
|
||||
@@ -228,8 +230,10 @@ def main():
|
||||
if local_config.multiProfiles() and not options.profile:
|
||||
profile_select = ProfileSelectDialog()
|
||||
profile_select.show()
|
||||
profile_select.exec_()
|
||||
options.profile = profile_select.profile()
|
||||
if profile_select.exec_():
|
||||
options.profile = profile_select.profile()
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
# Init the config
|
||||
if options.config:
|
||||
@@ -261,7 +265,10 @@ def main():
|
||||
# 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.")
|
||||
error_message = "GNS3.app must be moved to the '/Applications' folder before it can be used"
|
||||
QtWidgets.QMessageBox.critical(False, "Loading error", error_message)
|
||||
QtCore.QTimer.singleShot(0, app.quit)
|
||||
app.exec_()
|
||||
sys.exit(1)
|
||||
|
||||
global mainwindow
|
||||
@@ -285,7 +292,6 @@ def main():
|
||||
mainwindow.show()
|
||||
|
||||
exit_code = app.exec_()
|
||||
|
||||
signal.signal(signal.SIGINT, orig_sigint)
|
||||
signal.signal(signal.SIGTERM, orig_sigterm)
|
||||
|
||||
@@ -294,7 +300,7 @@ def main():
|
||||
# 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
|
||||
# for unknown reason otherwise Qt Segfault on OSX in some
|
||||
# conditions
|
||||
import gc
|
||||
gc.collect()
|
||||
|
||||
@@ -42,16 +42,21 @@ from .dialogs.edit_project_dialog import EditProjectDialog
|
||||
from .dialogs.setup_wizard import SetupWizard
|
||||
from .settings import GENERAL_SETTINGS
|
||||
from .items.node_item import NodeItem
|
||||
from .items.link_item import LinkItem
|
||||
from .items.link_item import LinkItem, SvgIconItem
|
||||
from .items.shape_item import ShapeItem
|
||||
from .items.label_item import LabelItem
|
||||
from .topology import Topology
|
||||
from .http_client import HTTPClient
|
||||
from .progress import Progress
|
||||
from .update_manager import UpdateManager
|
||||
from .utils.analytics import AnalyticsClient
|
||||
from .dialogs.appliance_wizard import ApplianceWizard
|
||||
from .dialogs.new_appliance_dialog import NewApplianceDialog
|
||||
from .dialogs.new_template_wizard import NewTemplateWizard
|
||||
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
|
||||
from .status_bar import StatusBarHandler
|
||||
from .registry.appliance import ApplianceError
|
||||
from .template_manager import TemplateManager
|
||||
from .appliance_manager import ApplianceManager
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -67,14 +72,24 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
# signal to tell the view if the user is adding a link or not
|
||||
adding_link_signal = QtCore.pyqtSignal(bool)
|
||||
|
||||
# Signal of settings updates
|
||||
settings_updated_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None, open_file=None):
|
||||
"""
|
||||
:param open_file: Open this file instead of asking for a new project
|
||||
"""
|
||||
|
||||
super().__init__(parent)
|
||||
self._settings = {}
|
||||
|
||||
self.setupUi(self)
|
||||
|
||||
self._notif_dialog = NotifDialog(self)
|
||||
# Setup logger
|
||||
logging.getLogger().addHandler(NotifDialogHandler(self._notif_dialog))
|
||||
logging.getLogger().addHandler(StatusBarHandler(self.uiStatusBar))
|
||||
|
||||
self._open_file_at_startup = open_file
|
||||
|
||||
MainWindow._instance = self
|
||||
@@ -82,8 +97,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
topology.setMainWindow(self)
|
||||
topology.project_changed_signal.connect(self._projectChangedSlot)
|
||||
Controller.instance().setParent(self)
|
||||
LocalServer.instance().setParent(self)
|
||||
|
||||
self._settings = {}
|
||||
HTTPClient.setProgressCallback(Progress.instance(self))
|
||||
|
||||
self._first_file_load = True
|
||||
@@ -96,11 +111,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.recent_project_actions = []
|
||||
self._start_time = time.time()
|
||||
local_config = LocalConfig.instance()
|
||||
local_config.config_changed_signal.connect(self._localConfigChangedSlot)
|
||||
#local_config.config_changed_signal.connect(self._localConfigChangedSlot)
|
||||
self._local_config_timer = QtCore.QTimer(self)
|
||||
self._local_config_timer.timeout.connect(local_config.checkConfigChanged)
|
||||
self._local_config_timer.start(1000) # milliseconds
|
||||
self._analytics_client = AnalyticsClient()
|
||||
self._template_manager = TemplateManager().instance()
|
||||
self._appliance_manager = ApplianceManager().instance()
|
||||
|
||||
# restore the geometry and state of the main window.
|
||||
self.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["geometry"].encode()))
|
||||
@@ -114,6 +131,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
action.setIconText("All devices")
|
||||
self.uiDocksMenu.addAction(action)
|
||||
|
||||
# Sometimes the parent seem invalid https://github.com/GNS3/gns3-gui/issues/2182
|
||||
self.uiNodesDockWidget.setParent(self)
|
||||
# make sure the dock widget is not open
|
||||
self.uiNodesDockWidget.setVisible(False)
|
||||
|
||||
@@ -122,6 +141,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._export_configs_to_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
|
||||
self._screenshots_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
|
||||
self._pictures_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
|
||||
self._appliance_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
|
||||
self._portable_project_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
|
||||
self._project_dir = None
|
||||
|
||||
# add recent file actions to the File menu
|
||||
for i in range(0, self._maxrecent_files):
|
||||
@@ -150,8 +172,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
# restore the style
|
||||
self._setStyle(self._settings.get("style"))
|
||||
|
||||
if self._settings["hide_new_appliance_template_button"]:
|
||||
self.uiNewAppliancePushButton.hide()
|
||||
if self._settings.get("hide_new_template_button"):
|
||||
self.uiNewTemplatePushButton.hide()
|
||||
|
||||
self.setWindowTitle("[*] GNS3")
|
||||
|
||||
@@ -168,7 +190,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiSnapshotAction,
|
||||
self.uiEditProjectAction,
|
||||
self.uiDeleteProjectAction,
|
||||
self.uiImportExportConfigsAction
|
||||
self.uiImportExportConfigsAction,
|
||||
self.uiLockAllAction
|
||||
]
|
||||
|
||||
# This widgets are not enabled if it's a remote controller (no access to the local file system)
|
||||
@@ -188,6 +211,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiNewProjectAction.triggered.connect(self._newProjectActionSlot)
|
||||
self.uiOpenProjectAction.triggered.connect(self.openProjectActionSlot)
|
||||
self.uiOpenApplianceAction.triggered.connect(self.openApplianceActionSlot)
|
||||
self.uiNewTemplateAction.triggered.connect(self._newTemplateActionSlot)
|
||||
self.uiSaveProjectAsAction.triggered.connect(self._saveProjectAsActionSlot)
|
||||
self.uiExportProjectAction.triggered.connect(self._exportProjectActionSlot)
|
||||
self.uiImportProjectAction.triggered.connect(self._importProjectActionSlot)
|
||||
@@ -212,9 +236,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiResetPortLabelsAction.triggered.connect(self._resetPortLabelsActionSlot)
|
||||
self.uiShowPortNamesAction.triggered.connect(self._showPortNamesActionSlot)
|
||||
self.uiShowGridAction.triggered.connect(self._showGridActionSlot)
|
||||
self.uiSnapToGridAction.triggered.connect(self._snapToGridActionSlot)
|
||||
self.uiLockAllAction.triggered.connect(self._lockActionSlot)
|
||||
|
||||
# tool menu connections
|
||||
self.uiWebInterfaceAction.triggered.connect(self._openWebInterfaceActionSlot)
|
||||
self.uiWebInterfaceAction.triggered.connect(self._openLightWebInterfaceActionSlot)
|
||||
self.uiWebUIAction.triggered.connect(self._openWebInterfaceActionSlot)
|
||||
|
||||
# control menu connections
|
||||
self.uiStartAllAction.triggered.connect(self._startAllActionSlot)
|
||||
@@ -232,7 +259,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiInsertImageAction.triggered.connect(self._insertImageActionSlot)
|
||||
self.uiDrawRectangleAction.triggered.connect(self._drawRectangleActionSlot)
|
||||
self.uiDrawEllipseAction.triggered.connect(self._drawEllipseActionSlot)
|
||||
self.uiEditReadmeAction.triggered.connect(self._editReadmeActionSlot)
|
||||
self.uiDrawLineAction.triggered.connect(self._drawLineActionSlot)
|
||||
|
||||
# help menu connections
|
||||
self.uiOnlineHelpAction.triggered.connect(self._onlineHelpActionSlot)
|
||||
@@ -252,12 +279,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiBrowseAllDevicesAction.triggered.connect(self._browseAllDevicesActionSlot)
|
||||
self.uiAddLinkAction.triggered.connect(self._addLinkActionSlot)
|
||||
|
||||
# new appliance button
|
||||
self.uiNewAppliancePushButton.clicked.connect(self._newApplianceActionSlot)
|
||||
# new template button
|
||||
self.uiNewTemplatePushButton.clicked.connect(self._newTemplateActionSlot)
|
||||
|
||||
# connect the signal to the view
|
||||
self.adding_link_signal.connect(self.uiGraphicsView.addingLinkSlot)
|
||||
|
||||
# connect to the signal when settings change
|
||||
self.settings_updated_signal.connect(self.settingsChangedSlot)
|
||||
|
||||
def _loadSettings(self):
|
||||
"""
|
||||
Loads the settings from the persistent settings file.
|
||||
@@ -290,17 +320,59 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._settings.update(new_settings)
|
||||
# save the settings
|
||||
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
|
||||
self.settings_updated_signal.emit()
|
||||
|
||||
def _openLightWebInterfaceActionSlot(self):
|
||||
if Controller.instance().connected():
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(Controller.instance().httpClient().fullUrl()))
|
||||
|
||||
def _openWebInterfaceActionSlot(self):
|
||||
if Controller.instance().connected():
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(Controller.instance().httpClient().fullUrl()))
|
||||
base_url = Controller.instance().httpClient().fullUrl()
|
||||
webui_url = "{}/static/web-ui/bundled".format(base_url)
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(webui_url))
|
||||
|
||||
def _showGridActionSlot(self):
|
||||
"""
|
||||
Called when we ask to display the grid
|
||||
"""
|
||||
self.showGrid(self.uiShowGridAction.isChecked())
|
||||
|
||||
self.uiGraphicsView.viewport().update()
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setShowGrid(self.uiShowGridAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def _snapToGridActionSlot(self):
|
||||
"""
|
||||
Called when user click on the snap to grid menu item
|
||||
:return: None
|
||||
"""
|
||||
self.snapToGrid(self.uiSnapToGridAction.isChecked())
|
||||
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setSnapToGrid(self.uiSnapToGridAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def _lockActionSlot(self):
|
||||
"""
|
||||
Called when user click on the lock menu item
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if self.uiGraphicsView.isEnabled():
|
||||
for item in self.uiGraphicsView.items():
|
||||
if not isinstance(item, LinkItem) and not isinstance(item, LabelItem) and not isinstance(item, SvgIconItem):
|
||||
if self.uiLockAllAction.isChecked() and not item.locked():
|
||||
item.setLocked(True)
|
||||
elif not self.uiLockAllAction.isChecked() and item.locked():
|
||||
item.setLocked(False)
|
||||
if item.parentItem() is None:
|
||||
item.updateNode()
|
||||
item.update()
|
||||
|
||||
def analyticsClient(self):
|
||||
"""
|
||||
@@ -313,48 +385,44 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Slot called to create a new project.
|
||||
"""
|
||||
|
||||
# prevents race condition
|
||||
if self._project_dialog is not None:
|
||||
return
|
||||
|
||||
self._project_dialog = ProjectDialog(self)
|
||||
self._project_dialog.show()
|
||||
create_new_project = self._project_dialog.exec_()
|
||||
# Close the device dock so it repopulates. Done in case switching between cloud and local.
|
||||
self.uiNodesDockWidget.setVisible(False)
|
||||
self.uiNodesDockWidget.setWindowTitle("")
|
||||
|
||||
if create_new_project:
|
||||
Topology.instance().createLoadProject(self._project_dialog.getProjectSettings())
|
||||
|
||||
self._project_dialog = None
|
||||
|
||||
def _newApplianceActionSlot(self):
|
||||
def _newTemplateActionSlot(self):
|
||||
"""
|
||||
Called when user want to create a new appliance
|
||||
Called when user want to create a new template.
|
||||
"""
|
||||
dialog = NewApplianceDialog(self)
|
||||
|
||||
dialog = NewTemplateWizard(self)
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
# No projects
|
||||
if Topology.instance().project() is None:
|
||||
if self._open_file_at_startup:
|
||||
self.loadPath(self._open_file_at_startup)
|
||||
self._open_file_at_startup = None
|
||||
else:
|
||||
self._newProjectActionSlot()
|
||||
|
||||
@qslot
|
||||
def openApplianceActionSlot(self, *args):
|
||||
"""
|
||||
Slot called to open an appliance.
|
||||
"""
|
||||
|
||||
directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
|
||||
if len(directory) == 0:
|
||||
directory = self._appliance_dir
|
||||
if not os.path.exists(self._appliance_dir):
|
||||
directory = Topology.instance().projectsDirPath()
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open appliance", directory,
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Import appliance", directory,
|
||||
"All files (*.*);;GNS3 Appliance (*.gns3appliance *.gns3a)",
|
||||
"GNS3 Appliance (*.gns3appliance *.gns3a)")
|
||||
if path:
|
||||
self.loadPath(path)
|
||||
self._appliance_dir = os.path.dirname(path)
|
||||
|
||||
def openProjectActionSlot(self):
|
||||
"""
|
||||
@@ -365,11 +433,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
# If the server is remote we use the new project windows with the project library
|
||||
self._newProjectActionSlot()
|
||||
else:
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open project", Topology.instance().projectsDirPath(),
|
||||
directory = self._project_dir
|
||||
if self._project_dir is None or not os.path.exists(self._project_dir):
|
||||
directory = Topology.instance().projectsDirPath()
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open project", directory,
|
||||
"All files (*.*);;GNS3 Project (*.gns3);;GNS3 Portable Project (*.gns3project *.gns3p);;NET files (*.net)",
|
||||
"GNS3 Project (*.gns3)")
|
||||
if path:
|
||||
self.loadPath(path)
|
||||
self._project_dir = os.path.dirname(path)
|
||||
|
||||
def openRecentFileSlot(self):
|
||||
"""
|
||||
@@ -454,6 +526,19 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._project_dialog = None
|
||||
self._refreshVisibleWidgets()
|
||||
|
||||
@qslot
|
||||
def settingsChangedSlot(self, *args):
|
||||
"""
|
||||
Called when settings are updated
|
||||
"""
|
||||
# It covers case when project is not set
|
||||
# and we need to refresh template manager
|
||||
# and appliance manager
|
||||
project = Topology.instance().project()
|
||||
if project is None:
|
||||
self._template_manager.instance().refresh()
|
||||
self._appliance_manager.instance().refresh()
|
||||
|
||||
def _refreshVisibleWidgets(self):
|
||||
"""
|
||||
Refresh widgets that should be visible or not
|
||||
@@ -479,7 +564,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
def _importExportConfigsActionSlot(self):
|
||||
"""
|
||||
Slot called when importing and exporting configs
|
||||
for the entire topology.
|
||||
for the entire project.
|
||||
"""
|
||||
|
||||
options = ["Export configs to a directory", "Import configs from a directory"]
|
||||
@@ -537,6 +622,60 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
# TODO: quality option
|
||||
return image.save(path)
|
||||
|
||||
def showLayers(self, show_layers):
|
||||
"""
|
||||
Shows layers in GUI
|
||||
:param show_layers: boolean
|
||||
:return: None
|
||||
"""
|
||||
NodeItem.show_layer = show_layers
|
||||
ShapeItem.show_layer = show_layers
|
||||
for item in self.uiGraphicsView.items():
|
||||
item.update()
|
||||
|
||||
def showGrid(self, show_grid):
|
||||
"""
|
||||
Shows grid in GUI
|
||||
:param show_grid: boolean
|
||||
:return: None
|
||||
"""
|
||||
self.uiGraphicsView.viewport().update()
|
||||
|
||||
def snapToGrid(self, snap_to_grid):
|
||||
"""
|
||||
Snap to grid in GUI
|
||||
:param snap_to_grid: boolean
|
||||
:return: None
|
||||
"""
|
||||
self.uiGraphicsView.viewport().update()
|
||||
|
||||
def showInterfaceLabels(self, show_interface_labels):
|
||||
"""
|
||||
Show interface labels in GUI
|
||||
:param show_interface_labels: boolean
|
||||
:return: None
|
||||
"""
|
||||
LinkItem.showPortLabels(show_interface_labels)
|
||||
for item in self.uiGraphicsView.scene().items():
|
||||
if isinstance(item, LinkItem):
|
||||
item.adjust()
|
||||
|
||||
def _updateZoomSettings(self, zoom=None):
|
||||
"""
|
||||
Updates zoom settings
|
||||
:param zoom integer optional, when not provided then calculated from current view
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if zoom is None:
|
||||
zoom = round(self.uiGraphicsView.transform().m11() * 100)
|
||||
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setZoom(zoom)
|
||||
project.update()
|
||||
|
||||
def _screenshotActionSlot(self):
|
||||
"""
|
||||
Slot called to take a screenshot of the scene.
|
||||
@@ -549,10 +688,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
self._screenshots_dir = os.path.dirname(path)
|
||||
|
||||
# add the extension if missing
|
||||
file_format = "." + selected_filter[:4].lower().strip()
|
||||
if not path.endswith(file_format):
|
||||
path += file_format
|
||||
# add the extension if missing (Mac OS automatically adds an extension already)
|
||||
if not sys.platform.startswith("darwin"):
|
||||
file_format = "." + selected_filter[:4].lower().strip()
|
||||
if not path.endswith(file_format):
|
||||
path += file_format
|
||||
|
||||
if not self.createScreenshot(path):
|
||||
QtWidgets.QMessageBox.critical(self, "Screenshot", "Could not create screenshot file {}".format(path))
|
||||
@@ -603,16 +743,18 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Slot called to scale in the view.
|
||||
"""
|
||||
|
||||
factor_in = pow(2.0, 120 / 240.0)
|
||||
factor_in = pow(2.0, 60 / 240.0)
|
||||
self.uiGraphicsView.scaleView(factor_in)
|
||||
self._updateZoomSettings()
|
||||
|
||||
def _zoomOutActionSlot(self):
|
||||
"""
|
||||
Slot called to scale out the view.
|
||||
"""
|
||||
|
||||
factor_out = pow(2.0, -120 / 240.0)
|
||||
factor_out = pow(2.0, -60 / 240.0)
|
||||
self.uiGraphicsView.scaleView(factor_out)
|
||||
self._updateZoomSettings()
|
||||
|
||||
def _zoomResetActionSlot(self):
|
||||
"""
|
||||
@@ -620,6 +762,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
self.uiGraphicsView.resetTransform()
|
||||
self._updateZoomSettings()
|
||||
|
||||
def _fitInViewActionSlot(self):
|
||||
"""
|
||||
@@ -635,11 +778,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Slot called to show the layer positions on the scene.
|
||||
"""
|
||||
self.showLayers(self.uiShowLayersAction.isChecked())
|
||||
|
||||
NodeItem.show_layer = self.uiShowLayersAction.isChecked()
|
||||
ShapeItem.show_layer = self.uiShowLayersAction.isChecked()
|
||||
for item in self.uiGraphicsView.items():
|
||||
item.update()
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setShowLayers(self.uiShowLayersAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def _resetPortLabelsActionSlot(self):
|
||||
"""
|
||||
@@ -656,10 +801,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Slot called to show the port names on the scene.
|
||||
"""
|
||||
|
||||
LinkItem.showPortLabels(self.uiShowPortNamesAction.isChecked())
|
||||
for item in self.uiGraphicsView.scene().items():
|
||||
if isinstance(item, LinkItem):
|
||||
item.adjust()
|
||||
self.showInterfaceLabels(self.uiShowPortNamesAction.isChecked())
|
||||
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setShowInterfaceLabels(self.uiShowPortNamesAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def _startAllActionSlot(self):
|
||||
"""
|
||||
@@ -716,7 +864,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Slot called when connecting to all the nodes using the console.
|
||||
"""
|
||||
|
||||
self.uiGraphicsView.consoleFromItems(self.uiGraphicsView.scene().items())
|
||||
self.uiGraphicsView.consoleFromAllItems()
|
||||
|
||||
def _addNoteActionSlot(self):
|
||||
"""
|
||||
@@ -737,7 +885,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
self._pictures_dir = os.path.dirname(path)
|
||||
|
||||
image = QtGui.QPixmap(path)
|
||||
QtGui.QPixmap(path)
|
||||
self.uiGraphicsView.addImage(path)
|
||||
|
||||
def _drawRectangleActionSlot(self):
|
||||
@@ -754,6 +902,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
self.uiGraphicsView.addEllipse(self.uiDrawEllipseAction.isChecked())
|
||||
|
||||
def _drawLineActionSlot(self):
|
||||
"""
|
||||
Slot called when adding a line on the scene.
|
||||
"""
|
||||
|
||||
self.uiGraphicsView.addLine(self.uiDrawLineAction.isChecked())
|
||||
|
||||
def _onlineHelpActionSlot(self):
|
||||
"""
|
||||
Slot to launch a browser pointing to the documentation page.
|
||||
@@ -781,9 +936,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
setup_wizard.show()
|
||||
res = setup_wizard.exec_()
|
||||
# start and connect to the local server if needed
|
||||
LocalServer.instance().localServerAutoStartIfRequire()
|
||||
if res:
|
||||
self._newApplianceActionSlot()
|
||||
LocalServer.instance().localServerAutoStartIfRequired()
|
||||
|
||||
def _aboutQtActionSlot(self):
|
||||
"""
|
||||
@@ -842,8 +995,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
else:
|
||||
self.uiNodesDockWidget.setWindowTitle(title)
|
||||
self.uiNodesDockWidget.setVisible(True)
|
||||
self.uiNodesView.clear()
|
||||
self.uiNodesView.populateNodesView(category)
|
||||
self.uiNodesDockWidget.populateNodesView(category)
|
||||
|
||||
def _localConfigChangedSlot(self):
|
||||
"""
|
||||
@@ -912,11 +1064,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._settings["preferences_dialog_geometry"] = bytes(dialog.saveGeometry().toBase64()).decode()
|
||||
self.setSettings(self._settings)
|
||||
|
||||
def _editReadmeActionSlot(self):
|
||||
"""
|
||||
Slot to edit the README file
|
||||
"""
|
||||
Topology.instance().editReadme()
|
||||
def resizeEvent(self, event):
|
||||
self._notif_dialog.resize()
|
||||
super().resizeEvent(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
@@ -940,6 +1090,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
:param event: QCloseEvent
|
||||
"""
|
||||
|
||||
if Topology.instance().project():
|
||||
reply = QtWidgets.QMessageBox.question(self, "Confirm Exit", "Are you sure you want to exit GNS3?",
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
event.ignore()
|
||||
return
|
||||
|
||||
progress = Progress.instance()
|
||||
progress.setAllowCancelQuery(True)
|
||||
progress.setCancelButtonText("Force quit")
|
||||
@@ -965,6 +1122,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._settings["state"] = bytes(self.saveState().toBase64()).decode()
|
||||
self.setSettings(self._settings)
|
||||
|
||||
Controller.instance().stopListenNotifications()
|
||||
server = LocalServer.instance()
|
||||
server.stopLocalServer(wait=True)
|
||||
|
||||
@@ -997,12 +1155,31 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
reply = QtWidgets.QMessageBox.warning(self, "GNS3", "Another GNS3 GUI is already running. Continue?",
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
self.close()
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
run_as_root_path = LocalConfig.instance().runAsRootPath()
|
||||
|
||||
if not sys.platform.startswith("win") and os.geteuid() == 0:
|
||||
# touches file to know that user has run GNS3 as root and to prevent
|
||||
# from running as user
|
||||
if not os.path.exists(run_as_root_path):
|
||||
try:
|
||||
open(run_as_root_path, 'a').close()
|
||||
except OSError as e:
|
||||
log.warning("Cannot write `run_as_root` file due to: {}".format(str(e)))
|
||||
|
||||
QtWidgets.QMessageBox.warning(self, "Root", "Running GNS3 as root is not recommended and could be dangerous")
|
||||
|
||||
if not sys.platform.startswith("win") and os.geteuid() != 0 and os.path.exists(run_as_root_path):
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self, "Run as user",
|
||||
"GNS3 has been previously run as root. It is not possible "
|
||||
"to change to another user and GNS3 will be shutdown. Please delete the '{}' file "
|
||||
"and start the program again.".format(run_as_root_path))
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
# restore debug level
|
||||
if self._settings["debug_level"]:
|
||||
root = logging.getLogger()
|
||||
@@ -1022,7 +1199,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._setupWizardActionSlot()
|
||||
else:
|
||||
# start and connect to the local server if needed
|
||||
LocalServer.instance().localServerAutoStartIfRequire()
|
||||
LocalServer.instance().localServerAutoStartIfRequired()
|
||||
if self._open_file_at_startup:
|
||||
self.loadPath(self._open_file_at_startup)
|
||||
self._open_file_at_startup = None
|
||||
@@ -1151,7 +1328,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
try:
|
||||
if file_path and os.path.exists(file_path):
|
||||
action = self.recent_file_actions[index]
|
||||
action.setText(" {}. {}".format(index + 1, os.path.basename(file_path)))
|
||||
duplicate = False
|
||||
for file_path_2 in self._settings["recent_files"]:
|
||||
if file_path != file_path_2 and os.path.basename(file_path) == os.path.basename(file_path_2):
|
||||
duplicate = True
|
||||
break
|
||||
if duplicate:
|
||||
action.setText(" {}. {} [{}]".format(index + 1, os.path.basename(file_path), file_path))
|
||||
else:
|
||||
action.setText(" {}. {}".format(index + 1, os.path.basename(file_path)))
|
||||
action.setData(file_path)
|
||||
action.setVisible(True)
|
||||
index += 1
|
||||
@@ -1196,14 +1381,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Slot called to import a portable project
|
||||
"""
|
||||
directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
|
||||
if len(directory) == 0:
|
||||
|
||||
directory = self._portable_project_dir
|
||||
if not os.path.exists(directory):
|
||||
directory = Topology.instance().projectsDirPath()
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open appliance", directory,
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open portable project", directory,
|
||||
"All files (*.*);;GNS3 Portable Project (*.gns3project *.gns3p)",
|
||||
"GNS3 Portable Project (*.gns3project *.gns3p)")
|
||||
if path:
|
||||
Topology.instance().importProject(path)
|
||||
self._portable_project_dir = os.path.dirname(path)
|
||||
|
||||
def _editProjectActionSlot(self):
|
||||
if Topology.instance().project() is None:
|
||||
|
||||
@@ -19,9 +19,12 @@ 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 = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker, TraceNG]
|
||||
#FIXME: deactivate TraceNG module
|
||||
MODULES = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker]
|
||||
|
||||
@@ -21,6 +21,7 @@ Built-in module implementation.
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
|
||||
from ..module import Module
|
||||
from .cloud import Cloud
|
||||
@@ -29,14 +30,7 @@ from .ethernet_hub import EthernetHub
|
||||
from .ethernet_switch import EthernetSwitch
|
||||
from .frame_relay_switch import FrameRelaySwitch
|
||||
from .atm_switch import ATMSwitch
|
||||
|
||||
from .settings import (
|
||||
BUILTIN_SETTINGS,
|
||||
CLOUD_SETTINGS,
|
||||
NAT_SETTINGS,
|
||||
ETHERNET_HUB_SETTINGS,
|
||||
ETHERNET_SWITCH_SETTINGS
|
||||
)
|
||||
from .settings import BUILTIN_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -50,39 +44,8 @@ class Builtin(Module):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._settings = {}
|
||||
self._nodes = []
|
||||
self._cloud_nodes = {}
|
||||
self._nat_nodes = {}
|
||||
self._ethernet_hubs = {}
|
||||
self._ethernet_switches = {}
|
||||
|
||||
# load the settings
|
||||
self._loadSettings()
|
||||
|
||||
def configChangedSlot(self):
|
||||
|
||||
pass
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns the module settings
|
||||
|
||||
:returns: module settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._settings
|
||||
|
||||
def setSettings(self, settings):
|
||||
"""Sets the module settings
|
||||
|
||||
:param settings: module settings (dictionary)
|
||||
"""
|
||||
|
||||
self._settings.update(settings)
|
||||
self._saveSettings()
|
||||
|
||||
def _saveSettings(self):
|
||||
"""
|
||||
Saves the settings to the persistent settings file.
|
||||
@@ -90,6 +53,15 @@ class Builtin(Module):
|
||||
|
||||
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
|
||||
|
||||
server_settings = {}
|
||||
config = LocalServerConfig.instance()
|
||||
if self._settings["default_nat_interface"]:
|
||||
# save some settings to the local server config file
|
||||
server_settings["default_nat_interface"] = self._settings["default_nat_interface"]
|
||||
config.saveSettings(self.__class__.__name__, server_settings)
|
||||
else:
|
||||
config.deleteSetting(self.__class__.__name__, "default_nat_interface")
|
||||
|
||||
def _loadSettings(self):
|
||||
"""
|
||||
Loads the settings from the persistent settings file.
|
||||
@@ -97,219 +69,48 @@ class Builtin(Module):
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
self._settings = local_config.loadSectionSettings(self.__class__.__name__, BUILTIN_SETTINGS)
|
||||
self._loadNodes()
|
||||
|
||||
def _loadBuilinNodesPerType(self, node_dict, node_type, default_settings):
|
||||
|
||||
settings = LocalConfig.instance().settings()
|
||||
if node_type in settings.get(self.__class__.__name__, {}):
|
||||
for device in settings[self.__class__.__name__][node_type]:
|
||||
name = device.get("name")
|
||||
server = device.get("server")
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
if key in node_dict or not name or not server:
|
||||
continue
|
||||
node_settings = default_settings.copy()
|
||||
node_settings.update(device)
|
||||
node_dict[key] = node_settings
|
||||
|
||||
def _loadNodes(self):
|
||||
"""
|
||||
Load the built-in nodes from the persistent settings file.
|
||||
"""
|
||||
|
||||
self._loadBuilinNodesPerType(self._cloud_nodes, "cloud_nodes", CLOUD_SETTINGS)
|
||||
self._loadBuilinNodesPerType(self._ethernet_hubs, "ethernet_hubs", ETHERNET_HUB_SETTINGS)
|
||||
self._loadBuilinNodesPerType(self._ethernet_switches, "ethernet_switches", ETHERNET_SWITCH_SETTINGS)
|
||||
|
||||
def _saveNodes(self):
|
||||
"""
|
||||
Saves the built-in nodes to the persistent settings file.
|
||||
"""
|
||||
|
||||
self._settings["cloud_nodes"] = list(self._cloud_nodes.values())
|
||||
self._settings["ethernet_hubs"] = list(self._ethernet_hubs.values())
|
||||
self._settings["ethernet_switches"] = list(self._ethernet_switches.values())
|
||||
self._saveSettings()
|
||||
|
||||
def cloudNodes(self):
|
||||
"""
|
||||
Returns cloud nodes settings.
|
||||
|
||||
:returns: Cloud nodes settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._cloud_nodes
|
||||
|
||||
def setCloudNodes(self, new_cloud_nodes):
|
||||
"""
|
||||
Sets cloud nodes settings.
|
||||
|
||||
:param new_cloud_nodes: cloud nodes settings (dictionary)
|
||||
"""
|
||||
|
||||
self._cloud_nodes = new_cloud_nodes.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def ethernetHubs(self):
|
||||
"""
|
||||
Returns Ethernet hubs settings.
|
||||
|
||||
:returns: Ethernet hubs settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._ethernet_hubs
|
||||
|
||||
def setEthernetHubs(self, new_ethernet_hubs):
|
||||
"""
|
||||
Sets Ethernet hubs settings.
|
||||
|
||||
:param new_ethernet_hubs: Ethernet hubs settings (dictionary)
|
||||
"""
|
||||
|
||||
self._ethernet_hubs = new_ethernet_hubs.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def ethernetSwitches(self):
|
||||
"""
|
||||
Returns Ethernet switches settings.
|
||||
|
||||
:returns: Ethernet switches settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._ethernet_switches
|
||||
|
||||
def setEthernetSwitches(self, new_ethernet_switches):
|
||||
"""
|
||||
Sets Ethernet switches settings.
|
||||
|
||||
:param new_ethernet_switches: Ethernet switches settings (dictionary)
|
||||
"""
|
||||
|
||||
self._ethernet_switches = new_ethernet_switches.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def addNode(self, node):
|
||||
"""
|
||||
Adds a node to this module.
|
||||
|
||||
:param node: Node instance
|
||||
"""
|
||||
|
||||
self._nodes.append(node)
|
||||
|
||||
def removeNode(self, node):
|
||||
"""
|
||||
Removes a node from this module.
|
||||
|
||||
:param node: Node instance
|
||||
"""
|
||||
|
||||
if node in self._nodes:
|
||||
self._nodes.remove(node)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the module.
|
||||
"""
|
||||
|
||||
self._nodes.clear()
|
||||
|
||||
def instantiateNode(self, node_class, server, project):
|
||||
"""
|
||||
Instantiate a new node.
|
||||
|
||||
:param node_class: Node object
|
||||
:param server: HTTPClient instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("creating node {}".format(node))
|
||||
if isinstance(node, Cloud):
|
||||
for key, info in self._cloud_nodes.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
|
||||
return
|
||||
elif isinstance(node, Nat):
|
||||
for key, info in self._nat_nodes.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(default_name_format=default_name_format)
|
||||
return
|
||||
elif isinstance(node, EthernetHub):
|
||||
for key, info in self._ethernet_hubs.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
|
||||
return
|
||||
elif isinstance(node, EthernetSwitch):
|
||||
for key, info in self._ethernet_switches.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
|
||||
return
|
||||
node.create()
|
||||
|
||||
@staticmethod
|
||||
def findAlternativeInterface(node, missing_interface):
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
|
||||
available_interfaces = []
|
||||
for interface in node.settings()["interfaces"]:
|
||||
available_interfaces.append(interface["name"])
|
||||
|
||||
if available_interfaces:
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(mainwindow,
|
||||
"Cloud interfaces", "Interface {} could not be found\nPlease select an alternative from your existing interfaces:".format(missing_interface),
|
||||
available_interfaces, 0, False)
|
||||
if ok:
|
||||
return selection
|
||||
QtWidgets.QMessageBox.warning(mainwindow, "Cloud interface", "No alternative interface chosen to replace {} on this host, this may lead to issues".format(missing_interface))
|
||||
return None
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(mainwindow, "Cloud interface", "Could not find interface {} on this host".format(missing_interface))
|
||||
return missing_interface
|
||||
|
||||
@staticmethod
|
||||
def getNodeClass(name):
|
||||
def configurationPage(node_type):
|
||||
"""
|
||||
Returns the object with the corresponding name.
|
||||
Returns the configuration page for this module.
|
||||
|
||||
:param name: object name
|
||||
:returns: QWidget object
|
||||
"""
|
||||
|
||||
if name in globals():
|
||||
return globals()[name]
|
||||
from .pages.ethernet_hub_configuration_page import EthernetHubConfigurationPage
|
||||
from .pages.ethernet_switch_configuration_page import EthernetSwitchConfigurationPage
|
||||
from .pages.cloud_configuration_page import CloudConfigurationPage
|
||||
if node_type == "ethernet_hub":
|
||||
return EthernetHubConfigurationPage
|
||||
elif node_type == "ethernet_switch":
|
||||
return EthernetSwitchConfigurationPage
|
||||
elif node_type == "cloud":
|
||||
return CloudConfigurationPage
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def getNodeType(name, platform=None):
|
||||
if name == "cloud":
|
||||
def getNodeClass(node_type, platform=None):
|
||||
"""
|
||||
Returns the class corresponding to node type.
|
||||
|
||||
:param node_type: node type (string)
|
||||
:param platform: not used
|
||||
|
||||
:returns: class or None
|
||||
"""
|
||||
|
||||
if node_type == "cloud":
|
||||
return Cloud
|
||||
elif name == "nat":
|
||||
elif node_type == "nat":
|
||||
return Nat
|
||||
elif name == "ethernet_hub":
|
||||
elif node_type == "ethernet_hub":
|
||||
return EthernetHub
|
||||
elif name == "ethernet_switch":
|
||||
elif node_type == "ethernet_switch":
|
||||
return EthernetSwitch
|
||||
elif name == "frame_relay_switch":
|
||||
elif node_type == "frame_relay_switch":
|
||||
return FrameRelaySwitch
|
||||
elif name == "atm_switch":
|
||||
elif node_type == "atm_switch":
|
||||
return ATMSwitch
|
||||
return None
|
||||
|
||||
@@ -323,59 +124,6 @@ class Builtin(Module):
|
||||
|
||||
return [Nat, Cloud, EthernetHub, EthernetSwitch, FrameRelaySwitch, ATMSwitch]
|
||||
|
||||
def nodes(self):
|
||||
"""
|
||||
Returns all the node data necessary to represent a node
|
||||
in the nodes view and create a node on the scene.
|
||||
"""
|
||||
|
||||
nodes = []
|
||||
for node_class in Builtin.classes():
|
||||
nodes.append(
|
||||
{"class": node_class.__name__,
|
||||
"name": node_class.symbolName(),
|
||||
"categories": node_class.categories(),
|
||||
"symbol": node_class.defaultSymbol(),
|
||||
"builtin": True,
|
||||
"node_type": node_class.URL_PREFIX
|
||||
}
|
||||
)
|
||||
|
||||
# add custom cloud node templates
|
||||
for cloud_node in self._cloud_nodes.values():
|
||||
nodes.append(
|
||||
{"class": Cloud.__name__,
|
||||
"name": cloud_node["name"],
|
||||
"server": cloud_node["server"],
|
||||
"symbol": cloud_node["symbol"],
|
||||
"categories": [cloud_node["category"]]
|
||||
}
|
||||
)
|
||||
|
||||
# add custom Ethernet hub templates
|
||||
for hub in self._ethernet_hubs.values():
|
||||
nodes.append(
|
||||
{"class": EthernetHub.__name__,
|
||||
"name": hub["name"],
|
||||
"server": hub["server"],
|
||||
"symbol": hub["symbol"],
|
||||
"categories": [hub["category"]]
|
||||
}
|
||||
)
|
||||
|
||||
# add custom Ethernet switch templates
|
||||
for switch in self._ethernet_switches.values():
|
||||
nodes.append(
|
||||
{"class": EthernetSwitch.__name__,
|
||||
"name": switch["name"],
|
||||
"server": switch["server"],
|
||||
"symbol": switch["symbol"],
|
||||
"categories": [switch["category"]]
|
||||
}
|
||||
)
|
||||
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
def preferencePages():
|
||||
"""
|
||||
@@ -400,3 +148,10 @@ class Builtin(Module):
|
||||
if not hasattr(Builtin, "_instance"):
|
||||
Builtin._instance = Builtin()
|
||||
return Builtin._instance
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Returns the module name.
|
||||
"""
|
||||
|
||||
return "builtin"
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
@@ -34,6 +33,7 @@ class ATMSwitch(Node):
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
URL_PREFIX = "atm_switch"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
@@ -44,50 +44,6 @@ class ATMSwitch(Node):
|
||||
self._always_on = True
|
||||
self.settings().update({"mappings": {}})
|
||||
|
||||
def create(self, name=None, node_id=None, mappings=None, default_name_format="ATM{0}"):
|
||||
"""
|
||||
Creates this ATM switch.
|
||||
|
||||
:param name: optional name for this switch.
|
||||
:param node_id: Node identifier on the server
|
||||
:param mappings: mappings to be automatically added when creating this ATM switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if mappings:
|
||||
params["mappings"] = mappings
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
: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.
|
||||
@@ -96,14 +52,14 @@ class ATMSwitch(Node):
|
||||
"""
|
||||
|
||||
info = """ATM switch {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's Node ID is {node_id}
|
||||
Running on server {host} with port {port}
|
||||
Local ID is {id} and server 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())
|
||||
host=self._compute.name(),
|
||||
port=self._compute.port())
|
||||
|
||||
port_info = ""
|
||||
mapping = re.compile(r"""^([0-9]*):([0-9]*):([0-9]*)$""")
|
||||
@@ -154,41 +110,6 @@ class ATMSwitch(Node):
|
||||
|
||||
return info + port_info
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this ATM switch
|
||||
(to be saved in a topology file).
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
atmsw = super().dump()
|
||||
if self._settings["mappings"]:
|
||||
atmsw["properties"]["mappings"] = self._settings["mappings"]
|
||||
return atmsw
|
||||
|
||||
def load(self, node_info):
|
||||
"""
|
||||
Loads an ATM switch representation
|
||||
(from a topology file).
|
||||
|
||||
:param node_info: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
super().load(node_info)
|
||||
properties = node_info["properties"]
|
||||
name = properties.pop("name")
|
||||
|
||||
# ATM switches do not have an UUID before version 2.0
|
||||
node_id = properties.get("node_id", str(uuid.uuid4()))
|
||||
|
||||
mappings = {}
|
||||
if "mappings" in properties:
|
||||
mappings = properties["mappings"]
|
||||
|
||||
log.info("ATM switch {} is loading".format(name))
|
||||
self.create(name, node_id, mappings)
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
@@ -209,11 +130,6 @@ class ATMSwitch(Node):
|
||||
|
||||
return ":/symbols/atm_switch.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "ATM switch"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gns3.node import Node
|
||||
from .settings import CLOUD_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -36,56 +37,32 @@ class Cloud(Node):
|
||||
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._interfaces = {}
|
||||
self._cloud_settings = {"ports_mapping": []}
|
||||
self._cloud_settings = {"ports_mapping": [],
|
||||
"remote_console_host": CLOUD_SETTINGS["remote_console_host"],
|
||||
"remote_console_port": CLOUD_SETTINGS["remote_console_port"],
|
||||
"remote_console_type": CLOUD_SETTINGS["remote_console_type"],
|
||||
"remote_console_http_path": CLOUD_SETTINGS["remote_console_http_path"]
|
||||
}
|
||||
self.settings().update(self._cloud_settings)
|
||||
|
||||
def interfaces(self):
|
||||
|
||||
return self._interfaces
|
||||
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="Cloud{0}"):
|
||||
"""
|
||||
Creates this cloud.
|
||||
|
||||
:param name: optional name for this cloud
|
||||
:param node_id: Node identifier on the server
|
||||
:param ports: ports to be automatically added when creating this cloud
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result, error=False, **kwargs):
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
if "ports_mapping" in result:
|
||||
self._settings["ports_mapping"] = result["ports_mapping"].copy()
|
||||
|
||||
if "interfaces" in result:
|
||||
self._interfaces = result["interfaces"].copy()
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this cloud.
|
||||
|
||||
: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.
|
||||
@@ -93,12 +70,42 @@ class Cloud(Node):
|
||||
:param result: server response
|
||||
"""
|
||||
|
||||
if "ports_mapping" in result:
|
||||
self._settings["ports_mapping"] = result["ports_mapping"].copy()
|
||||
|
||||
if "interfaces" in result:
|
||||
self._interfaces = result["interfaces"].copy()
|
||||
|
||||
def consoleType(self):
|
||||
"""
|
||||
Get the console type.
|
||||
"""
|
||||
|
||||
return self.settings()["remote_console_type"]
|
||||
|
||||
def consoleHost(self):
|
||||
"""
|
||||
Returns the host to connect to the console.
|
||||
|
||||
:returns: host (string)
|
||||
"""
|
||||
|
||||
return self.settings()["remote_console_host"]
|
||||
|
||||
def console(self):
|
||||
"""
|
||||
Returns the console port number of this node
|
||||
|
||||
:returns: port number
|
||||
"""
|
||||
|
||||
return self.settings()["remote_console_port"]
|
||||
|
||||
def consoleHttpPath(self):
|
||||
"""
|
||||
Returns the path of the web ui
|
||||
|
||||
:returns: string
|
||||
"""
|
||||
return self._settings["remote_console_http_path"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this cloud.
|
||||
@@ -106,9 +113,23 @@ class Cloud(Node):
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Cloud device {name} is always-on
|
||||
This is a node for external connections
|
||||
""".format(name=self.name())
|
||||
info = """Cloud {name} is always-on
|
||||
Running on server {host} with port {port}
|
||||
""".format(name=self.name(),
|
||||
host=self.compute().name(),
|
||||
port=self.compute().port())
|
||||
|
||||
if self.consoleType() != "none":
|
||||
info += """ Remote console is {console_host} on port {console} and type is {console_type}
|
||||
""".format(console_host=self.consoleHost(),
|
||||
console=self.console(),
|
||||
console_type=self.consoleType())
|
||||
if self.consoleType() in ("http", "https"):
|
||||
info += """ Remote console HTTP path is '{console_http_path}'
|
||||
""".format(console_http_path=self.consoleHttpPath())
|
||||
else:
|
||||
info += """ No remote console configured
|
||||
"""
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
@@ -140,11 +161,6 @@ This is a node for external connections
|
||||
|
||||
return ":/symbols/cloud.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Cloud"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
|
||||
@@ -20,17 +20,15 @@ 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.
|
||||
Wizard to create a cloud node.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
@@ -51,7 +49,6 @@ class CloudWizard(VMWizard, Ui_CloudNodeWizard):
|
||||
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"category": Node.end_devices,
|
||||
"server": self._compute_id}
|
||||
"compute_id": self._compute_id}
|
||||
|
||||
return settings
|
||||
|
||||
@@ -24,13 +24,12 @@ from gns3.node import Node
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.ethernet_hub_wizard_ui import Ui_EthernetHubWizard
|
||||
from .. import Builtin
|
||||
|
||||
|
||||
class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
|
||||
|
||||
"""
|
||||
Wizard to create an Ethernet hub template.
|
||||
Wizard to create an Ethernet hub.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
@@ -57,7 +56,7 @@ class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"symbol": ":/symbols/hub.svg",
|
||||
"category": Node.switches,
|
||||
"server": self._compute_id,
|
||||
"compute_id": self._compute_id,
|
||||
"ports_mapping": ports}
|
||||
|
||||
return settings
|
||||
|
||||
@@ -24,13 +24,12 @@ from gns3.node import Node
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.ethernet_switch_wizard_ui import Ui_EthernetSwitchWizard
|
||||
from .. import Builtin
|
||||
|
||||
|
||||
class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
|
||||
|
||||
"""
|
||||
Wizard to create an Ethernet switch template.
|
||||
Wizard to create an Ethernet switch.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
@@ -60,7 +59,7 @@ class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"symbol": ":/symbols/ethernet_switch.svg",
|
||||
"category": Node.switches,
|
||||
"server": self._compute_id,
|
||||
"compute_id": self._compute_id,
|
||||
"ports_mapping": ports}
|
||||
|
||||
return settings
|
||||
|
||||
@@ -29,6 +29,7 @@ class EthernetHub(Node):
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
URL_PREFIX = "ethernet_hub"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
@@ -39,51 +40,6 @@ class EthernetHub(Node):
|
||||
self._always_on = True
|
||||
self.settings().update({"ports_mapping": []})
|
||||
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="Hub{0}"):
|
||||
"""
|
||||
Creates this hub.
|
||||
|
||||
:param name: optional name for this hub
|
||||
:param node_id: node identifier on the server
|
||||
:param ports: ports to automatically be added when creating this hub
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this Ethernet hub.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if "name" in new_settings:
|
||||
params["name"] = new_settings["name"]
|
||||
if "ports_mapping" in new_settings:
|
||||
params["ports_mapping"] = new_settings["ports_mapping"]
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this Ethernet hub.
|
||||
@@ -92,13 +48,13 @@ class EthernetHub(Node):
|
||||
"""
|
||||
|
||||
info = """Ethernet hub {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's node ID is {node_id}
|
||||
Hub's server runs on {host}
|
||||
Running on server {host} with port {port}
|
||||
Local ID is {id} and server ID is {node_id}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self.compute().name())
|
||||
host=self.compute().name(),
|
||||
port=self.compute().port())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
@@ -130,11 +86,6 @@ class EthernetHub(Node):
|
||||
|
||||
return ":/symbols/hub.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Ethernet hub"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
|
||||
@@ -22,7 +22,6 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EthernetSwitch(Node):
|
||||
|
||||
"""
|
||||
Ethernet switch.
|
||||
|
||||
@@ -30,6 +29,7 @@ class EthernetSwitch(Node):
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
URL_PREFIX = "ethernet_switch"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
@@ -38,51 +38,7 @@ class EthernetSwitch(Node):
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"ports_mapping": []})
|
||||
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="SW{0}"):
|
||||
"""
|
||||
Creates this Ethernet switch.
|
||||
|
||||
:param name: optional name for this switch
|
||||
:param node_id: node identifier on the server
|
||||
:param ports: ports to be automatically added when creating this switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this Ethernet switch.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
self.settings().update({"ports_mapping": [], "console_type": "none"})
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
@@ -92,13 +48,16 @@ class EthernetSwitch(Node):
|
||||
"""
|
||||
|
||||
info = """Ethernet switch {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's Node ID is {node_id}
|
||||
Switch's server runs on {host}
|
||||
Running on server {host} with port {port}
|
||||
Local ID is {id} and server ID is {node_id}
|
||||
Console is on port {console} and type is {console_type}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self.compute().name())
|
||||
host=self.compute().name(),
|
||||
port=self.compute().port(),
|
||||
console=self._settings["console"],
|
||||
console_type=self._settings["console_type"])
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
@@ -150,11 +109,6 @@ class EthernetSwitch(Node):
|
||||
|
||||
return ":/symbols/ethernet_switch.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Ethernet switch"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
|
||||
@@ -41,50 +41,6 @@ class FrameRelaySwitch(Node):
|
||||
self._always_on = True
|
||||
self.settings().update({"mappings": {}})
|
||||
|
||||
def create(self, name=None, node_id=None, mappings={}, default_name_format="FR{0}"):
|
||||
"""
|
||||
Creates this Frame Relay switch.
|
||||
|
||||
:param name: name for this switch.
|
||||
:param node_id: node identifier on the server
|
||||
:param mappings: mappings to be automatically added when creating this Frame relay switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if mappings:
|
||||
params["mappings"] = mappings
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this Frame Relay switch.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this Frame Relay switch.
|
||||
@@ -93,14 +49,13 @@ class FrameRelaySwitch(Node):
|
||||
"""
|
||||
|
||||
info = """Frame relay switch {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's Node ID is {node_id}
|
||||
Running on server {host} with port {port}
|
||||
Local ID is {id} and server ID is {node_id}
|
||||
Hardware is Dynamips emulated simple Frame relay switch
|
||||
Switch's server runs on {host}:{port}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self._compute.host(),
|
||||
host=self._compute.name(),
|
||||
port=self._compute.port())
|
||||
|
||||
port_info = ""
|
||||
@@ -151,11 +106,6 @@ class FrameRelaySwitch(Node):
|
||||
|
||||
return ":/symbols/frame_relay_switch.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Frame Relay switch"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
|
||||
@@ -42,56 +42,6 @@ class Nat(Node):
|
||||
self._nat_settings = {}
|
||||
self.settings().update(self._nat_settings)
|
||||
|
||||
def interfaces(self):
|
||||
|
||||
return self._interfaces
|
||||
|
||||
def create(self, name=None, node_id=None, default_name_format="Nat{0}"):
|
||||
"""
|
||||
Creates this nat.
|
||||
|
||||
:param name: optional name for this nat
|
||||
:param node_id: Node identifier on the server
|
||||
"""
|
||||
|
||||
params = {}
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while creating nat: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this nat.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
if error:
|
||||
log.error("Error while creating nat: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this nat.
|
||||
@@ -99,9 +49,11 @@ class Nat(Node):
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Nat device {name} is always-on
|
||||
This is a node for external connections
|
||||
""".format(name=self.name())
|
||||
info = """NAT node {name} is always-on
|
||||
Running on server {host} with port {port}
|
||||
""".format(name=self.name(),
|
||||
host=self.compute().name(),
|
||||
port=self.compute().port())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
@@ -123,11 +75,6 @@ This is a node for external connections
|
||||
|
||||
return ":/symbols/cloud.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Nat"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
|
||||
@@ -35,8 +35,8 @@ class ATMSwitchConfigurationPage(QtWidgets.QWidget, Ui_atmSwitchConfigPageWidget
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
self._mapping = {}
|
||||
self._node = None
|
||||
|
||||
# connect slots
|
||||
self.uiAddPushButton.clicked.connect(self._addMappingSlot)
|
||||
self.uiDeletePushButton.clicked.connect(self._deleteMappingSlot)
|
||||
self.uiMappingTreeWidget.itemActivated.connect(self._mappingSelectedSlot)
|
||||
|
||||
@@ -16,45 +16,69 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for Built-in preferences.
|
||||
Configuration page for builtins preferences.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.utils.interfaces import interfaces
|
||||
|
||||
from .. import Builtin
|
||||
from ..ui.builtin_preferences_page_ui import Ui_BuiltinPreferencesPageWidget
|
||||
from ..settings import BUILTIN_SETTINGS
|
||||
|
||||
|
||||
class BuiltinPreferencesPage(QtWidgets.QWidget, Ui_BuiltinPreferencesPageWidget):
|
||||
"""QWidget preference page for Built-in."""
|
||||
|
||||
"""
|
||||
QWidget preference page for builtins.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
# connect signals
|
||||
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
|
||||
|
||||
def _restoreDefaultsSlot(self):
|
||||
"""Slot to populate the page widgets with the default settings."""
|
||||
"""
|
||||
Slot to populate the page widgets with the default settings.
|
||||
"""
|
||||
|
||||
self._populateWidgets(BUILTIN_SETTINGS)
|
||||
|
||||
def _populateWidgets(self, settings):
|
||||
"""Populates the widgets with the settings.
|
||||
"""
|
||||
Populates the widgets with the settings.
|
||||
|
||||
:param settings: Built-in settings
|
||||
:param settings: builtins settings
|
||||
"""
|
||||
|
||||
self.uiNATInterfaceComboBox.clear()
|
||||
self.uiNATInterfaceComboBox.addItem("")
|
||||
for interface in interfaces():
|
||||
self.uiNATInterfaceComboBox.addItem(interface["name"])
|
||||
|
||||
# load the default NAT interface
|
||||
index = self.uiNATInterfaceComboBox.findText(settings["default_nat_interface"])
|
||||
if index != -1:
|
||||
self.uiNATInterfaceComboBox.setCurrentIndex(index)
|
||||
|
||||
def loadPreferences(self):
|
||||
"""Loads Built-in preferences."""
|
||||
"""
|
||||
Loads builtins preferences.
|
||||
"""
|
||||
|
||||
builtin_settings = Builtin.instance().settings()
|
||||
self._populateWidgets(builtin_settings)
|
||||
|
||||
def savePreferences(self):
|
||||
"""Saves Built-in preferences."""
|
||||
"""
|
||||
Saves builtins preferences.
|
||||
"""
|
||||
|
||||
new_settings = {}
|
||||
|
||||
# save the default NAT interface
|
||||
default_nat_interface = self.uiNATInterfaceComboBox.currentText()
|
||||
new_settings["default_nat_interface"] = default_nat_interface
|
||||
Builtin.instance().setSettings(new_settings)
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
Configuration page for clouds.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from ....dialogs.node_properties_dialog import ConfigurationError
|
||||
from gns3.controller import Controller
|
||||
from gns3.node import Node
|
||||
|
||||
@@ -50,6 +51,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiEthernetWarningPushButton.clicked.connect(self._EthernetWarningSlot)
|
||||
self.uiAddEthernetPushButton.clicked.connect(self._EthernetAddSlot)
|
||||
self.uiAddAllEthernetPushButton.clicked.connect(self._EthernetAddAllSlot)
|
||||
self.uiRefreshEthernetPushButton.clicked.connect(self._EthernetRefreshSlot)
|
||||
self.uiDeleteEthernetPushButton.clicked.connect(self._EthernetDeleteSlot)
|
||||
|
||||
# connect TAP slots
|
||||
@@ -57,6 +59,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiTAPListWidget.itemSelectionChanged.connect(self._TAPChangedSlot)
|
||||
self.uiAddTAPPushButton.clicked.connect(self._TAPAddSlot)
|
||||
self.uiAddAllTAPPushButton.clicked.connect(self._TAPAddAllSlot)
|
||||
self.uiRefreshTAPPushButton.clicked.connect(self._TAPRefreshSlot)
|
||||
self.uiDeleteTAPPushButton.clicked.connect(self._TAPDeleteSlot)
|
||||
|
||||
# connect UDP slots
|
||||
@@ -65,8 +68,29 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiAddUDPPushButton.clicked.connect(self._UDPAddSlot)
|
||||
self.uiDeleteUDPPushButton.clicked.connect(self._UDPDeleteSlot)
|
||||
|
||||
# connect other slots
|
||||
self.uiShowSpecialInterfacesCheckBox.stateChanged.connect(self._showSpecialInterfacesSlot)
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
self.uiConsoleTypeComboBox.currentTextChanged.connect(self._consoleTypeChangedSlot)
|
||||
|
||||
# add an icon to the warning button
|
||||
icon = QtGui.QIcon.fromTheme("dialog-warning")
|
||||
if icon.isNull():
|
||||
icon = QtGui.QIcon(':/icons/dialog-warning.svg')
|
||||
self.uiEthernetWarningPushButton.setIcon(icon)
|
||||
|
||||
def _refreshInterfaces(self):
|
||||
"""
|
||||
Refresh the network interfaces.
|
||||
"""
|
||||
|
||||
if self._node:
|
||||
self._interfaces = self._node.interfaces()
|
||||
self._loadNetworkInterfaces(self._interfaces)
|
||||
try:
|
||||
self._node.updated_signal.disconnect(self._refreshInterfaces)
|
||||
except (TypeError, RuntimeError):
|
||||
pass # was not connected
|
||||
|
||||
def _EthernetChangedSlot(self):
|
||||
"""
|
||||
@@ -115,6 +139,15 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
interface = self.uiEthernetComboBox.itemText(index)
|
||||
self._EthernetAddSlot(interface)
|
||||
|
||||
def _EthernetRefreshSlot(self):
|
||||
"""
|
||||
Refresh all Ethernet interfaces.
|
||||
"""
|
||||
|
||||
if self._node:
|
||||
self._node.update({}, force=True)
|
||||
self._node.updated_signal.connect(self._refreshInterfaces)
|
||||
|
||||
def _EthernetDeleteSlot(self):
|
||||
"""
|
||||
Deletes the selected Ethernet interface.
|
||||
@@ -193,6 +226,15 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
interface = self.uiTAPComboBox.itemText(index)
|
||||
self._TAPAddSlot(interface)
|
||||
|
||||
def _TAPRefreshSlot(self):
|
||||
"""
|
||||
Refresh all TAP interfaces.
|
||||
"""
|
||||
|
||||
if self._node:
|
||||
self._node.update({}, force=True)
|
||||
self._node.updated_signal.connect(self._refreshInterfaces)
|
||||
|
||||
def _TAPDeleteSlot(self):
|
||||
"""
|
||||
Deletes a TAP interface.
|
||||
@@ -312,6 +354,9 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiUDPNameLineEdit.setText("UDP tunnel {}".format(nb_tunnels + 1))
|
||||
|
||||
def _showSpecialInterfacesSlot(self, state):
|
||||
"""
|
||||
Shows special Ethernet interfaces.
|
||||
"""
|
||||
|
||||
self.uiEthernetComboBox.clear()
|
||||
index = 0
|
||||
@@ -338,6 +383,9 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
|
||||
|
||||
def _loadNetworkInterfaces(self, interfaces):
|
||||
"""
|
||||
Loads Ethernet and TAP interfaces.
|
||||
"""
|
||||
|
||||
self.uiEthernetComboBox.clear()
|
||||
index = 0
|
||||
@@ -369,6 +417,25 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
self._interfaces = result
|
||||
self._loadNetworkInterfaces(result)
|
||||
|
||||
def _consoleTypeChangedSlot(self, console_type):
|
||||
"""
|
||||
Slot called when the console type has been changed.
|
||||
|
||||
:param console_type: console type
|
||||
"""
|
||||
|
||||
if console_type in ("http", "https"):
|
||||
self.uiConsoleHttpPathLineEdit.setEnabled(True)
|
||||
else:
|
||||
self.uiConsoleHttpPathLineEdit.setEnabled(False)
|
||||
|
||||
if console_type != "none":
|
||||
self.uiConsoleHostLineEdit.setEnabled(True)
|
||||
self.uiConsolePortSpinBox.setEnabled(True)
|
||||
else:
|
||||
self.uiConsoleHostLineEdit.setEnabled(False)
|
||||
self.uiConsolePortSpinBox.setEnabled(False)
|
||||
|
||||
def loadSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Loads the cloud settings.
|
||||
@@ -380,13 +447,18 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
|
||||
if not group:
|
||||
self.uiNameLineEdit.setText(settings["name"])
|
||||
self.uiConsoleHostLineEdit.setText(settings["remote_console_host"])
|
||||
self.uiConsolePortSpinBox.setValue(settings["remote_console_port"])
|
||||
index = self.uiConsoleTypeComboBox.findText(settings["remote_console_type"])
|
||||
if index != -1:
|
||||
self.uiConsoleTypeComboBox.setCurrentIndex(index)
|
||||
self.uiConsoleHttpPathLineEdit.setText(settings["remote_console_http_path"])
|
||||
else:
|
||||
self.uiNameLineEdit.setEnabled(False)
|
||||
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# rename the label from "Name" to "Template name"
|
||||
self.uiNameLabel.setText("Template name:")
|
||||
|
||||
# load the default name format
|
||||
@@ -401,7 +473,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
if index != -1:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
|
||||
Controller.instance().getCompute("/network/interfaces", settings["server"],
|
||||
Controller.instance().getCompute("/network/interfaces", settings["compute_id"],
|
||||
self._getInterfacesFromServerCallback,
|
||||
progressText="Retrieving network interfaces...")
|
||||
|
||||
@@ -459,6 +531,17 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
|
||||
if not group:
|
||||
settings["name"] = self.uiNameLineEdit.text()
|
||||
console_host = self.uiConsoleHostLineEdit.text().strip()
|
||||
|
||||
if self.uiConsoleTypeComboBox.currentText().lower() != "none":
|
||||
if not console_host:
|
||||
QtWidgets.QMessageBox.critical(self, "Console host", "Console host cannot be blank if console type is not set to none")
|
||||
raise ConfigurationError()
|
||||
|
||||
settings["remote_console_host"] = console_host
|
||||
settings["remote_console_port"] = self.uiConsolePortSpinBox.value()
|
||||
settings["remote_console_type"] = self.uiConsoleTypeComboBox.currentText().lower()
|
||||
settings["remote_console_http_path"] = self.uiConsoleHttpPathLineEdit.text().strip()
|
||||
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
@@ -21,13 +21,14 @@ Configuration page for cloud node preferences.
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from gns3.qt import QtCore, QtWidgets, qpartial
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.template_manager import TemplateManager
|
||||
from gns3.controller import Controller
|
||||
from gns3.template import Template
|
||||
|
||||
from .. import Builtin
|
||||
from ..settings import CLOUD_SETTINGS
|
||||
from ..ui.cloud_preferences_page_ui import Ui_CloudPreferencesPageWidget
|
||||
from ..pages.cloud_configuration_page import CloudConfigurationPage
|
||||
@@ -53,6 +54,11 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
self.uiCloudNodesTreeWidget.itemSelectionChanged.connect(self._cloudNodeChangedSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
Adds a new section to the tree widget.
|
||||
|
||||
:param name: section name
|
||||
"""
|
||||
|
||||
section_item = QtWidgets.QTreeWidgetItem(self.uiCloudNodeInfoTreeWidget)
|
||||
section_item.setText(0, name)
|
||||
@@ -62,15 +68,25 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, cloud_node):
|
||||
"""
|
||||
Refreshes the content of the tree widget.
|
||||
"""
|
||||
|
||||
self.uiCloudNodeInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", cloud_node["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template ID:", cloud_node.get("template_id", "none")])
|
||||
if cloud_node["remote_console_type"] != "none":
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Console host:", cloud_node["remote_console_host"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Console port:", "{}".format(cloud_node["remote_console_port"])])
|
||||
if cloud_node["remote_console_type"] in ("http", "https"):
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Console HTTP path:", cloud_node["remote_console_http_path"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Console type:", cloud_node["remote_console_type"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", cloud_node["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["server"]).name()])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["compute_id"]).name()])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -81,7 +97,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
|
||||
def _cloudNodeChangedSlot(self):
|
||||
"""
|
||||
Loads a selected cloud node template from the tree widget.
|
||||
Loads a selected cloud nodes from the tree widget.
|
||||
"""
|
||||
|
||||
selection = self.uiCloudNodesTreeWidget.selectedItems()
|
||||
@@ -98,14 +114,14 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
|
||||
def _newCloudNodeSlot(self):
|
||||
"""
|
||||
Creates a new cloud node template.
|
||||
Creates a new cloud node.
|
||||
"""
|
||||
|
||||
wizard = CloudWizard(self._cloud_nodes, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_cloud_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_cloud_settings["server"], name=new_cloud_settings["name"])
|
||||
key = "{server}:{name}".format(server=new_cloud_settings["compute_id"], name=new_cloud_settings["name"])
|
||||
self._cloud_nodes[key] = CLOUD_SETTINGS.copy()
|
||||
self._cloud_nodes[key].update(new_cloud_settings)
|
||||
|
||||
@@ -119,7 +135,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
|
||||
def _editCloudNodeSlot(self):
|
||||
"""
|
||||
Edits a cloud node template.
|
||||
Edits a cloud node.
|
||||
"""
|
||||
|
||||
item = self.uiCloudNodesTreeWidget.currentItem()
|
||||
@@ -132,10 +148,10 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(cloud_node["symbol"], qpartial(self._setItemIcon, item))
|
||||
if cloud_node["name"] != item.text(0):
|
||||
new_key = "{server}:{name}".format(server=cloud_node["server"], name=cloud_node["name"])
|
||||
new_key = "{server}:{name}".format(server=cloud_node["compute_id"], name=cloud_node["name"])
|
||||
if new_key in self._cloud_nodes:
|
||||
QtWidgets.QMessageBox.critical(self, "Cloud node", "Cloud node name {} already exists for server {}".format(cloud_node["name"],
|
||||
cloud_node["server"]))
|
||||
cloud_node["compute_id"]))
|
||||
cloud_node["name"] = item.text(0)
|
||||
return
|
||||
self._cloud_nodes[new_key] = self._cloud_nodes[key]
|
||||
@@ -146,7 +162,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
|
||||
def _deleteCloudNodeSlot(self):
|
||||
"""
|
||||
Deletes a cloud node template.
|
||||
Deletes a cloud node.
|
||||
"""
|
||||
|
||||
for item in self.uiCloudNodesTreeWidget.selectedItems():
|
||||
@@ -160,10 +176,17 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
Loads the cloud node preferences.
|
||||
"""
|
||||
|
||||
builtin_module = Builtin.instance()
|
||||
self._cloud_nodes = copy.deepcopy(builtin_module.cloudNodes())
|
||||
self._items.clear()
|
||||
self._cloud_nodes = {}
|
||||
templates = TemplateManager.instance().templates()
|
||||
for template_id, template in templates.items():
|
||||
if template.template_type() == "cloud" and not template.builtin():
|
||||
name = template.name()
|
||||
server = template.compute_id()
|
||||
#TODO: use template id for the key
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
self._cloud_nodes[key] = copy.deepcopy(template.settings())
|
||||
|
||||
self._items.clear()
|
||||
for key, cloud_node in self._cloud_nodes.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiCloudNodesTreeWidget)
|
||||
item.setText(0, cloud_node["name"])
|
||||
@@ -177,6 +200,10 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
"""
|
||||
Sets an item icon.
|
||||
"""
|
||||
|
||||
item.setIcon(0, icon)
|
||||
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
@@ -185,4 +212,11 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
Saves the cloud node preferences.
|
||||
"""
|
||||
|
||||
Builtin.instance().setCloudNodes(self._cloud_nodes)
|
||||
templates = []
|
||||
for template in TemplateManager.instance().templates().values():
|
||||
if template.template_type() != "cloud":
|
||||
templates.append(template)
|
||||
for template_settings in self._cloud_nodes.values():
|
||||
templates.append(Template(template_settings))
|
||||
TemplateManager.instance().updateList(templates)
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ class EthernetHubConfigurationPage(QtWidgets.QWidget, Ui_ethernetHubConfigPageWi
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# rename the label from "Name" to "Template name"
|
||||
self.uiNameLabel.setText("Template name:")
|
||||
|
||||
# load the default name format
|
||||
|
||||
@@ -21,14 +21,15 @@ Configuration page for Ethernet hub preferences.
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from gns3.qt import QtCore, QtWidgets, qpartial
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.template_manager import TemplateManager
|
||||
from gns3.controller import Controller
|
||||
from gns3.template import Template
|
||||
|
||||
from .. import Builtin
|
||||
from ..settings import ETHERNET_HUB_SETTINGS
|
||||
from ..ui.ethernet_hub_preferences_page_ui import Ui_EthernetHubPreferencesPageWidget
|
||||
from ..pages.ethernet_hub_configuration_page import EthernetHubConfigurationPage
|
||||
@@ -54,6 +55,11 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
self.uiEthernetHubsTreeWidget.itemSelectionChanged.connect(self._ethernetHubChangedSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
Adds a new section to the tree widget.
|
||||
|
||||
:param name: section name
|
||||
"""
|
||||
|
||||
section_item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubInfoTreeWidget)
|
||||
section_item.setText(0, name)
|
||||
@@ -63,15 +69,19 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, ethernet_hub):
|
||||
"""
|
||||
Refreshes the content of the tree widget.
|
||||
"""
|
||||
|
||||
self.uiEthernetHubInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_hub["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template ID:", ethernet_hub.get("template_id", "none")])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_hub["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["server"]).name()])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["compute_id"]).name()])
|
||||
except KeyError:
|
||||
pass
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Number of ports:", str(len(ethernet_hub["ports_mapping"]))])
|
||||
@@ -83,7 +93,7 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
|
||||
def _ethernetHubChangedSlot(self):
|
||||
"""
|
||||
Loads a selected Ethernet hub template from the tree widget.
|
||||
Loads a selected Ethernet hub from the tree widget.
|
||||
"""
|
||||
|
||||
selection = self.uiEthernetHubsTreeWidget.selectedItems()
|
||||
@@ -100,14 +110,14 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
|
||||
def _newEthernetHubSlot(self):
|
||||
"""
|
||||
Creates a new Ethernet hub template.
|
||||
Creates a new Ethernet hub.
|
||||
"""
|
||||
|
||||
wizard = EthernetHubWizard(self._ethernet_hubs, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_ethernet_hub_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_ethernet_hub_settings["server"], name=new_ethernet_hub_settings["name"])
|
||||
key = "{server}:{name}".format(server=new_ethernet_hub_settings["compute_id"], name=new_ethernet_hub_settings["name"])
|
||||
self._ethernet_hubs[key] = ETHERNET_HUB_SETTINGS.copy()
|
||||
self._ethernet_hubs[key].update(new_ethernet_hub_settings)
|
||||
|
||||
@@ -120,7 +130,7 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
|
||||
def _editEthernetHubSlot(self):
|
||||
"""
|
||||
Edits an Ethernet hub template.
|
||||
Edits an Ethernet hub.
|
||||
"""
|
||||
|
||||
item = self.uiEthernetHubsTreeWidget.currentItem()
|
||||
@@ -133,10 +143,10 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(ethernet_hub["symbol"], qpartial(self._setItemIcon, item))
|
||||
if ethernet_hub["name"] != item.text(0):
|
||||
new_key = "{server}:{name}".format(server=ethernet_hub["server"], name=ethernet_hub["name"])
|
||||
new_key = "{server}:{name}".format(server=ethernet_hub["compute_id"], name=ethernet_hub["name"])
|
||||
if new_key in self._ethernet_hubs:
|
||||
QtWidgets.QMessageBox.critical(self, "Ethernet hub", "Ethernet hub name {} already exists for server {}".format(ethernet_hub["name"],
|
||||
ethernet_hub["server"]))
|
||||
ethernet_hub["compute_id"]))
|
||||
ethernet_hub["name"] = item.text(0)
|
||||
return
|
||||
self._ethernet_hubs[new_key] = self._ethernet_hubs[key]
|
||||
@@ -147,7 +157,7 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
|
||||
def _deleteEthernetHubSlot(self):
|
||||
"""
|
||||
Deletes an Ethernet hub template.
|
||||
Deletes an Ethernet hub.
|
||||
"""
|
||||
|
||||
for item in self.uiEthernetHubsTreeWidget.selectedItems():
|
||||
@@ -161,10 +171,17 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
Loads the ethernet hub preferences.
|
||||
"""
|
||||
|
||||
builtin_module = Builtin.instance()
|
||||
self._ethernet_hubs = copy.deepcopy(builtin_module.ethernetHubs())
|
||||
self._items.clear()
|
||||
self._ethernet_hubs = {}
|
||||
templates = TemplateManager.instance().templates()
|
||||
for template_id, template in templates.items():
|
||||
if template.template_type() == "ethernet_hub" and not template.builtin():
|
||||
name = template.name()
|
||||
server = template.compute_id()
|
||||
#TODO: use template id for the key
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
self._ethernet_hubs[key] = copy.deepcopy(template.settings())
|
||||
|
||||
self._items.clear()
|
||||
for key, ethernet_hub in self._ethernet_hubs.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubsTreeWidget)
|
||||
item.setText(0, ethernet_hub["name"])
|
||||
@@ -178,6 +195,10 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
"""
|
||||
Sets an item icon.
|
||||
"""
|
||||
|
||||
item.setIcon(0, icon)
|
||||
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
@@ -186,4 +207,11 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
Saves the Ethernet hub preferences.
|
||||
"""
|
||||
|
||||
Builtin.instance().setEthernetHubs(self._ethernet_hubs)
|
||||
templates = []
|
||||
for template in TemplateManager.instance().templates().values():
|
||||
if template.template_type() != "ethernet_hub":
|
||||
templates.append(template)
|
||||
for template_settings in self._ethernet_hubs.values():
|
||||
templates.append(Template(template_settings))
|
||||
TemplateManager.instance().updateList(templates)
|
||||
|
||||
|
||||
@@ -196,7 +196,6 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# rename the label from "Name" to "Template name"
|
||||
self.uiNameLabel.setText("Template name:")
|
||||
|
||||
# load the default name format
|
||||
@@ -223,12 +222,17 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
for port_info in settings["ports_mapping"]:
|
||||
item = TreeWidgetItem(self.uiPortsTreeWidget)
|
||||
item.setText(0, str(port_info["port_number"]))
|
||||
item.setText(1, str(port_info["vlan"]))
|
||||
item.setText(2, port_info["type"])
|
||||
item.setText(1, str(port_info.get("vlan", 1)))
|
||||
item.setText(2, port_info.get("type", "access"))
|
||||
item.setText(3, port_info.get("ethertype", ""))
|
||||
self.uiPortsTreeWidget.addTopLevelItem(item)
|
||||
self._ports[port_info["port_number"]] = port_info
|
||||
|
||||
# load the console type
|
||||
index = self.uiConsoleTypeComboBox.findText(settings["console_type"])
|
||||
if index != -1:
|
||||
self.uiConsoleTypeComboBox.setCurrentIndex(index)
|
||||
|
||||
self.uiPortsTreeWidget.resizeColumnToContents(0)
|
||||
self.uiPortsTreeWidget.resizeColumnToContents(1)
|
||||
if len(self._ports) > 0:
|
||||
@@ -266,5 +270,8 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
|
||||
# save console type
|
||||
settings["console_type"] = self.uiConsoleTypeComboBox.currentText().lower()
|
||||
|
||||
settings["ports_mapping"] = list(self._ports.values())
|
||||
return settings
|
||||
|
||||
@@ -21,14 +21,15 @@ Configuration page for Ethernet switch preferences.
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from gns3.qt import QtCore, QtWidgets, qpartial
|
||||
from gns3.controller import Controller
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.template_manager import TemplateManager
|
||||
from gns3.template import Template
|
||||
|
||||
from .. import Builtin
|
||||
from ..settings import ETHERNET_SWITCH_SETTINGS
|
||||
from ..ui.ethernet_switch_preferences_page_ui import Ui_EthernetSwitchPreferencesPageWidget
|
||||
from ..pages.ethernet_switch_configuration_page import EthernetSwitchConfigurationPage
|
||||
@@ -54,6 +55,11 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
self.uiEthernetSwitchesTreeWidget.itemSelectionChanged.connect(self._ethernetSwitchChangedSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
Adds a new section to the tree widget.
|
||||
|
||||
:param name: section name
|
||||
"""
|
||||
|
||||
section_item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchInfoTreeWidget)
|
||||
section_item.setText(0, name)
|
||||
@@ -63,18 +69,23 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, ethernet_switch):
|
||||
"""
|
||||
Refreshes the content of the tree widget.
|
||||
"""
|
||||
|
||||
self.uiEthernetSwitchInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_switch["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template ID:", ethernet_switch.get("template_id", "none")])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_switch["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["server"]).name()])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["compute_id"]).name()])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Console type:", ethernet_switch["console_type"]])
|
||||
for port in ethernet_switch["ports_mapping"]:
|
||||
section_item = self._createSectionItem("Port{}".format(port["port_number"]))
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Name:", port["name"]])
|
||||
@@ -88,7 +99,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
|
||||
def _ethernetSwitchChangedSlot(self):
|
||||
"""
|
||||
Loads a selected Ethernet switch template from the tree widget.
|
||||
Loads a selected Ethernet switch from the tree widget.
|
||||
"""
|
||||
|
||||
selection = self.uiEthernetSwitchesTreeWidget.selectedItems()
|
||||
@@ -105,14 +116,14 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
|
||||
def _newEthernetSwitchSlot(self):
|
||||
"""
|
||||
Creates a new Ethernet switch template.
|
||||
Creates a new Ethernet switch.
|
||||
"""
|
||||
|
||||
wizard = EthernetSwitchWizard(self._ethernet_switches, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_ethernet_switch_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_ethernet_switch_settings["server"], name=new_ethernet_switch_settings["name"])
|
||||
key = "{server}:{name}".format(server=new_ethernet_switch_settings["compute_id"], name=new_ethernet_switch_settings["name"])
|
||||
self._ethernet_switches[key] = ETHERNET_SWITCH_SETTINGS.copy()
|
||||
self._ethernet_switches[key].update(new_ethernet_switch_settings)
|
||||
|
||||
@@ -126,7 +137,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
|
||||
def _editEthernetSwitchSlot(self):
|
||||
"""
|
||||
Edits an Ethernet switch template.
|
||||
Edits an Ethernet switch.
|
||||
"""
|
||||
|
||||
item = self.uiEthernetSwitchesTreeWidget.currentItem()
|
||||
@@ -139,10 +150,10 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(ethernet_switch["symbol"], qpartial(self._setItemIcon, item))
|
||||
if ethernet_switch["name"] != item.text(0):
|
||||
new_key = "{server}:{name}".format(server=ethernet_switch["server"], name=ethernet_switch["name"])
|
||||
new_key = "{server}:{name}".format(server=ethernet_switch["compute_id"], name=ethernet_switch["name"])
|
||||
if new_key in self._ethernet_switches:
|
||||
QtWidgets.QMessageBox.critical(self, "Ethernet switch", "Ethernet switch name {} already exists for server {}".format(ethernet_switch["name"],
|
||||
ethernet_switch["server"]))
|
||||
ethernet_switch["compute_id"]))
|
||||
ethernet_switch["name"] = item.text(0)
|
||||
return
|
||||
self._ethernet_switches[new_key] = self._ethernet_switches[key]
|
||||
@@ -153,7 +164,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
|
||||
def _deleteEthernetSwitchSlot(self):
|
||||
"""
|
||||
Deletes an Ethernet switch template.
|
||||
Deletes an Ethernet switch.
|
||||
"""
|
||||
for item in self.uiEthernetSwitchesTreeWidget.selectedItems():
|
||||
if item:
|
||||
@@ -166,10 +177,17 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
Loads the ethernet switch preferences.
|
||||
"""
|
||||
|
||||
builtin_module = Builtin.instance()
|
||||
self._ethernet_switches = copy.deepcopy(builtin_module.ethernetSwitches())
|
||||
self._items.clear()
|
||||
self._ethernet_switches = {}
|
||||
templates = TemplateManager.instance().templates()
|
||||
for template_id, template in templates.items():
|
||||
if template.template_type() == "ethernet_switch" and not template.builtin():
|
||||
name = template.name()
|
||||
server = template.compute_id()
|
||||
#TODO: use template id for the key
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
self._ethernet_switches[key] = copy.deepcopy(template.settings())
|
||||
|
||||
self._items.clear()
|
||||
for key, ethernet_switch in self._ethernet_switches.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchesTreeWidget)
|
||||
item.setText(0, ethernet_switch["name"])
|
||||
@@ -182,13 +200,24 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
self.uiEthernetSwitchesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
"""
|
||||
Sets an item icon.
|
||||
"""
|
||||
|
||||
item.setIcon(0, icon)
|
||||
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def savePreferences(self):
|
||||
"""
|
||||
Saves the Ethernet switch preferences.
|
||||
"""
|
||||
|
||||
Builtin.instance().setEthernetSwitches(self._ethernet_switches)
|
||||
templates = []
|
||||
for template in TemplateManager.instance().templates().values():
|
||||
if template.template_type() != "ethernet_switch":
|
||||
templates.append(template)
|
||||
for template_settings in self._ethernet_switches.values():
|
||||
templates.append(Template(template_settings))
|
||||
TemplateManager.instance().updateList(templates)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
item.setIcon(0, icon)
|
||||
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
@@ -19,26 +19,30 @@
|
||||
Default Built-in settings.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
DEFAULT_NAT_INTERFACE = "virbr0"
|
||||
else:
|
||||
DEFAULT_NAT_INTERFACE = "vmnet8"
|
||||
|
||||
BUILTIN_SETTINGS = {
|
||||
}
|
||||
|
||||
|
||||
NAT_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Nat{0}",
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"category": Node.end_devices,
|
||||
"ports_mapping": [],
|
||||
"default_nat_interface": DEFAULT_NAT_INTERFACE
|
||||
}
|
||||
|
||||
CLOUD_SETTINGS = {
|
||||
"name": "",
|
||||
"remote_console_host": "127.0.0.1",
|
||||
"remote_console_port": 23,
|
||||
"remote_console_type": "none",
|
||||
"remote_console_http_path": "/",
|
||||
"default_name_format": "Cloud{0}",
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"category": Node.end_devices,
|
||||
"ports_mapping": [],
|
||||
"node_type": "cloud"
|
||||
}
|
||||
|
||||
ETHERNET_HUB_SETTINGS = {
|
||||
@@ -47,6 +51,7 @@ ETHERNET_HUB_SETTINGS = {
|
||||
"symbol": ":/symbols/hub.svg",
|
||||
"category": Node.switches,
|
||||
"ports_mapping": [],
|
||||
"node_type": "ethernet_hub"
|
||||
}
|
||||
|
||||
ETHERNET_SWITCH_SETTINGS = {
|
||||
@@ -54,5 +59,7 @@ ETHERNET_SWITCH_SETTINGS = {
|
||||
"default_name_format": "Switch{0}",
|
||||
"symbol": ":/symbols/ethernet_switch.svg",
|
||||
"category": Node.switches,
|
||||
"console_type": "none",
|
||||
"ports_mapping": [],
|
||||
"node_type": "ethernet_switch"
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>330</width>
|
||||
<height>200</height>
|
||||
<width>456</width>
|
||||
<height>385</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -26,8 +26,18 @@
|
||||
<attribute name="title">
|
||||
<string>Local settings</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Default NAT interface:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QComboBox" name="uiNATInterfaceComboBox"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/builtin_preferences_page.ui'
|
||||
#
|
||||
# Created: Wed Dec 7 21:40:18 2016
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
# Created by: PyQt5 UI code generator 5.9.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_BuiltinPreferencesPageWidget(object):
|
||||
def setupUi(self, BuiltinPreferencesPageWidget):
|
||||
BuiltinPreferencesPageWidget.setObjectName("BuiltinPreferencesPageWidget")
|
||||
BuiltinPreferencesPageWidget.resize(330, 200)
|
||||
BuiltinPreferencesPageWidget.resize(456, 385)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(BuiltinPreferencesPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(BuiltinPreferencesPageWidget)
|
||||
@@ -20,10 +19,16 @@ class Ui_BuiltinPreferencesPageWidget(object):
|
||||
self.uiTabWidget.setObjectName("uiTabWidget")
|
||||
self.uiServerSettingsTabWidget = QtWidgets.QWidget()
|
||||
self.uiServerSettingsTabWidget.setObjectName("uiServerSettingsTabWidget")
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.uiServerSettingsTabWidget)
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.uiServerSettingsTabWidget)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.label = QtWidgets.QLabel(self.uiServerSettingsTabWidget)
|
||||
self.label.setObjectName("label")
|
||||
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
|
||||
self.uiNATInterfaceComboBox = QtWidgets.QComboBox(self.uiServerSettingsTabWidget)
|
||||
self.uiNATInterfaceComboBox.setObjectName("uiNATInterfaceComboBox")
|
||||
self.gridLayout.addWidget(self.uiNATInterfaceComboBox, 1, 0, 1, 1)
|
||||
spacerItem = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout_2.addItem(spacerItem)
|
||||
self.gridLayout.addItem(spacerItem, 2, 0, 1, 1)
|
||||
self.uiTabWidget.addTab(self.uiServerSettingsTabWidget, "")
|
||||
self.verticalLayout.addWidget(self.uiTabWidget)
|
||||
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||
@@ -42,6 +47,7 @@ class Ui_BuiltinPreferencesPageWidget(object):
|
||||
def retranslateUi(self, BuiltinPreferencesPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
BuiltinPreferencesPageWidget.setWindowTitle(_translate("BuiltinPreferencesPageWidget", "Built-in"))
|
||||
self.label.setText(_translate("BuiltinPreferencesPageWidget", "Default NAT interface:"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("BuiltinPreferencesPageWidget", "Local settings"))
|
||||
self.uiRestoreDefaultsPushButton.setText(_translate("BuiltinPreferencesPageWidget", "Restore defaults"))
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>821</width>
|
||||
<height>363</height>
|
||||
<width>979</width>
|
||||
<height>564</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Cloud configuration</string>
|
||||
<string>Cloud template configuration</string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p>A cloud node allows you to connect your project to the &quot;real world&quot; (a network or host) using either an Ethernet interface, a TAP interface (Linux only) or even an UDP tunnel. <span style=" font-weight:600;">Please be aware that Wifi interfaces may not work properly.</span></p></body></html></string>
|
||||
@@ -57,7 +57,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<item row="0" column="5">
|
||||
<widget class="QPushButton" name="uiDeleteEthernetPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
@@ -67,7 +67,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="5">
|
||||
<item row="1" column="0" colspan="6">
|
||||
<widget class="QListWidget" name="uiEthernetListWidget">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
@@ -82,9 +82,6 @@
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="dialog-warning"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
@@ -94,6 +91,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QPushButton" name="uiRefreshEthernetPushButton">
|
||||
<property name="text">
|
||||
<string>&Refresh</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<zorder>uiEthernetListWidget</zorder>
|
||||
<zorder>uiEthernetComboBox</zorder>
|
||||
@@ -102,13 +106,14 @@
|
||||
<zorder>uiAddAllEthernetPushButton</zorder>
|
||||
<zorder>uiShowSpecialInterfacesCheckBox</zorder>
|
||||
<zorder>uiEthernetWarningPushButton</zorder>
|
||||
<zorder>uiRefreshEthernetPushButton</zorder>
|
||||
</widget>
|
||||
<widget class="QWidget" name="TAPTab">
|
||||
<attribute name="title">
|
||||
<string>TAP interfaces</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="1" column="4">
|
||||
<item row="1" column="5">
|
||||
<widget class="QPushButton" name="uiDeleteTAPPushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
@@ -118,7 +123,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="5">
|
||||
<item row="2" column="0" colspan="6">
|
||||
<widget class="QListWidget" name="uiTAPListWidget">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
@@ -145,7 +150,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="4">
|
||||
<item row="0" column="1" colspan="5">
|
||||
<widget class="QComboBox" name="uiTAPComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
@@ -168,6 +173,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="4">
|
||||
<widget class="QPushButton" name="uiRefreshTAPPushButton">
|
||||
<property name="text">
|
||||
<string>&Refresh</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="UDPTab">
|
||||
@@ -378,6 +390,19 @@
|
||||
<string>Misc.</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="3" column="2">
|
||||
<widget class="QSpinBox" name="uiConsolePortSpinBox">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>65535</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>23</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiNameLabel">
|
||||
<property name="text">
|
||||
@@ -388,24 +413,64 @@
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="uiNameLineEdit"/>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="uiConsoleHostLabel">
|
||||
<property name="text">
|
||||
<string>Console host:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLineEdit" name="uiConsoleHostLineEdit"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="labeluiConsolePortLabel">
|
||||
<property name="text">
|
||||
<string>Console port:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="uiConsoleHttpPathLabel">
|
||||
<property name="text">
|
||||
<string>Console HTTP path:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<widget class="QLineEdit" name="uiConsoleHttpPathLineEdit"/>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="uiDefaultNameFormatLabel">
|
||||
<property name="text">
|
||||
<string>Default name format:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<item row="6" column="2">
|
||||
<widget class="QLineEdit" name="uiDefaultNameFormatLineEdit"/>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="uiSymbolLabel">
|
||||
<property name="text">
|
||||
<string>Symbol:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<item row="9" column="0" colspan="3">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>399</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="7" column="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="uiSymbolLineEdit"/>
|
||||
@@ -422,30 +487,73 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="uiCategoryLabel">
|
||||
<property name="text">
|
||||
<string>Category:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<item row="8" column="2">
|
||||
<widget class="QComboBox" name="uiCategoryComboBox"/>
|
||||
</item>
|
||||
<item row="4" column="1" colspan="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiConsoleTypeLabel">
|
||||
<property name="text">
|
||||
<string>Console type:</string>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>399</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QComboBox" name="uiConsoleTypeComboBox">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>telnet</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>vnc</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>spice</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>http</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>https</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>none</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<zorder>uiNameLabel</zorder>
|
||||
<zorder>uiNameLineEdit</zorder>
|
||||
<zorder>uiDefaultNameFormatLabel</zorder>
|
||||
<zorder>uiDefaultNameFormatLineEdit</zorder>
|
||||
<zorder>uiSymbolLabel</zorder>
|
||||
<zorder>uiCategoryLabel</zorder>
|
||||
<zorder>uiCategoryComboBox</zorder>
|
||||
<zorder>labeluiConsolePortLabel</zorder>
|
||||
<zorder>uiConsoleHostLabel</zorder>
|
||||
<zorder>uiConsoleHostLineEdit</zorder>
|
||||
<zorder>uiConsolePortSpinBox</zorder>
|
||||
<zorder>uiConsoleHttpPathLabel</zorder>
|
||||
<zorder>uiConsoleHttpPathLineEdit</zorder>
|
||||
<zorder>uiConsoleTypeLabel</zorder>
|
||||
<zorder>uiConsoleTypeComboBox</zorder>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.5.1
|
||||
# Created by: PyQt5 UI code generator 5.11.3
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -11,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_cloudConfigPageWidget(object):
|
||||
def setupUi(self, cloudConfigPageWidget):
|
||||
cloudConfigPageWidget.setObjectName("cloudConfigPageWidget")
|
||||
cloudConfigPageWidget.resize(758, 299)
|
||||
cloudConfigPageWidget.resize(979, 564)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
|
||||
@@ -39,20 +39,21 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiDeleteEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiDeleteEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteEthernetPushButton.setObjectName("uiDeleteEthernetPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiDeleteEthernetPushButton, 0, 4, 1, 1)
|
||||
self.gridLayout_3.addWidget(self.uiDeleteEthernetPushButton, 0, 5, 1, 1)
|
||||
self.uiEthernetListWidget = QtWidgets.QListWidget(self.EthernetTab)
|
||||
self.uiEthernetListWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.uiEthernetListWidget.setObjectName("uiEthernetListWidget")
|
||||
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 5)
|
||||
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 6)
|
||||
self.uiEthernetWarningPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiEthernetWarningPushButton.setText("")
|
||||
icon = QtGui.QIcon.fromTheme("dialog-warning")
|
||||
self.uiEthernetWarningPushButton.setIcon(icon)
|
||||
self.uiEthernetWarningPushButton.setObjectName("uiEthernetWarningPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiEthernetWarningPushButton, 0, 1, 1, 1)
|
||||
self.uiShowSpecialInterfacesCheckBox = QtWidgets.QCheckBox(self.EthernetTab)
|
||||
self.uiShowSpecialInterfacesCheckBox.setObjectName("uiShowSpecialInterfacesCheckBox")
|
||||
self.gridLayout_3.addWidget(self.uiShowSpecialInterfacesCheckBox, 2, 0, 1, 2)
|
||||
self.uiRefreshEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiRefreshEthernetPushButton.setObjectName("uiRefreshEthernetPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiRefreshEthernetPushButton, 0, 4, 1, 1)
|
||||
self.uiEthernetListWidget.raise_()
|
||||
self.uiEthernetComboBox.raise_()
|
||||
self.uiAddEthernetPushButton.raise_()
|
||||
@@ -60,6 +61,7 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiAddAllEthernetPushButton.raise_()
|
||||
self.uiShowSpecialInterfacesCheckBox.raise_()
|
||||
self.uiEthernetWarningPushButton.raise_()
|
||||
self.uiRefreshEthernetPushButton.raise_()
|
||||
self.uiTabWidget.addTab(self.EthernetTab, "")
|
||||
self.TAPTab = QtWidgets.QWidget()
|
||||
self.TAPTab.setObjectName("TAPTab")
|
||||
@@ -68,11 +70,11 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiDeleteTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
|
||||
self.uiDeleteTAPPushButton.setEnabled(False)
|
||||
self.uiDeleteTAPPushButton.setObjectName("uiDeleteTAPPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiDeleteTAPPushButton, 1, 4, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.uiDeleteTAPPushButton, 1, 5, 1, 1)
|
||||
self.uiTAPListWidget = QtWidgets.QListWidget(self.TAPTab)
|
||||
self.uiTAPListWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.uiTAPListWidget.setObjectName("uiTAPListWidget")
|
||||
self.gridLayout_2.addWidget(self.uiTAPListWidget, 2, 0, 1, 5)
|
||||
self.gridLayout_2.addWidget(self.uiTAPListWidget, 2, 0, 1, 6)
|
||||
self.uiTAPLineEdit = QtWidgets.QLineEdit(self.TAPTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
@@ -93,10 +95,13 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiTAPComboBox.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
|
||||
self.uiTAPComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
self.uiTAPComboBox.setObjectName("uiTAPComboBox")
|
||||
self.gridLayout_2.addWidget(self.uiTAPComboBox, 0, 1, 1, 4)
|
||||
self.gridLayout_2.addWidget(self.uiTAPComboBox, 0, 1, 1, 5)
|
||||
self.uiAddAllTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
|
||||
self.uiAddAllTAPPushButton.setObjectName("uiAddAllTAPPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiAddAllTAPPushButton, 1, 3, 1, 1)
|
||||
self.uiRefreshTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
|
||||
self.uiRefreshTAPPushButton.setObjectName("uiRefreshTAPPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiRefreshTAPPushButton, 1, 4, 1, 1)
|
||||
self.uiTabWidget.addTab(self.TAPTab, "")
|
||||
self.UDPTab = QtWidgets.QWidget()
|
||||
self.UDPTab.setObjectName("UDPTab")
|
||||
@@ -194,21 +199,44 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.MiscTab.setObjectName("MiscTab")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.MiscTab)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.uiConsolePortSpinBox = QtWidgets.QSpinBox(self.MiscTab)
|
||||
self.uiConsolePortSpinBox.setMinimum(1)
|
||||
self.uiConsolePortSpinBox.setMaximum(65535)
|
||||
self.uiConsolePortSpinBox.setProperty("value", 23)
|
||||
self.uiConsolePortSpinBox.setObjectName("uiConsolePortSpinBox")
|
||||
self.gridLayout.addWidget(self.uiConsolePortSpinBox, 3, 2, 1, 1)
|
||||
self.uiNameLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.uiNameLabel.setObjectName("uiNameLabel")
|
||||
self.gridLayout.addWidget(self.uiNameLabel, 0, 0, 1, 1)
|
||||
self.uiNameLineEdit = QtWidgets.QLineEdit(self.MiscTab)
|
||||
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
|
||||
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 2, 1, 1)
|
||||
self.uiConsoleHostLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.uiConsoleHostLabel.setObjectName("uiConsoleHostLabel")
|
||||
self.gridLayout.addWidget(self.uiConsoleHostLabel, 2, 0, 1, 1)
|
||||
self.uiConsoleHostLineEdit = QtWidgets.QLineEdit(self.MiscTab)
|
||||
self.uiConsoleHostLineEdit.setObjectName("uiConsoleHostLineEdit")
|
||||
self.gridLayout.addWidget(self.uiConsoleHostLineEdit, 2, 2, 1, 1)
|
||||
self.labeluiConsolePortLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.labeluiConsolePortLabel.setObjectName("labeluiConsolePortLabel")
|
||||
self.gridLayout.addWidget(self.labeluiConsolePortLabel, 3, 0, 1, 1)
|
||||
self.uiConsoleHttpPathLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.uiConsoleHttpPathLabel.setObjectName("uiConsoleHttpPathLabel")
|
||||
self.gridLayout.addWidget(self.uiConsoleHttpPathLabel, 5, 0, 1, 1)
|
||||
self.uiConsoleHttpPathLineEdit = QtWidgets.QLineEdit(self.MiscTab)
|
||||
self.uiConsoleHttpPathLineEdit.setObjectName("uiConsoleHttpPathLineEdit")
|
||||
self.gridLayout.addWidget(self.uiConsoleHttpPathLineEdit, 5, 2, 1, 1)
|
||||
self.uiDefaultNameFormatLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.uiDefaultNameFormatLabel.setObjectName("uiDefaultNameFormatLabel")
|
||||
self.gridLayout.addWidget(self.uiDefaultNameFormatLabel, 1, 0, 1, 2)
|
||||
self.gridLayout.addWidget(self.uiDefaultNameFormatLabel, 6, 0, 1, 1)
|
||||
self.uiDefaultNameFormatLineEdit = QtWidgets.QLineEdit(self.MiscTab)
|
||||
self.uiDefaultNameFormatLineEdit.setObjectName("uiDefaultNameFormatLineEdit")
|
||||
self.gridLayout.addWidget(self.uiDefaultNameFormatLineEdit, 1, 2, 1, 1)
|
||||
self.gridLayout.addWidget(self.uiDefaultNameFormatLineEdit, 6, 2, 1, 1)
|
||||
self.uiSymbolLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.uiSymbolLabel.setObjectName("uiSymbolLabel")
|
||||
self.gridLayout.addWidget(self.uiSymbolLabel, 2, 0, 1, 2)
|
||||
self.gridLayout.addWidget(self.uiSymbolLabel, 7, 0, 1, 1)
|
||||
spacerItem2 = QtWidgets.QSpacerItem(20, 399, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout.addItem(spacerItem2, 9, 0, 1, 3)
|
||||
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
|
||||
self.uiSymbolLineEdit = QtWidgets.QLineEdit(self.MiscTab)
|
||||
@@ -218,15 +246,40 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiSymbolToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
|
||||
self.uiSymbolToolButton.setObjectName("uiSymbolToolButton")
|
||||
self.horizontalLayout_7.addWidget(self.uiSymbolToolButton)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_7, 2, 2, 1, 1)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_7, 7, 2, 1, 1)
|
||||
self.uiCategoryLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.uiCategoryLabel.setObjectName("uiCategoryLabel")
|
||||
self.gridLayout.addWidget(self.uiCategoryLabel, 3, 0, 1, 2)
|
||||
self.gridLayout.addWidget(self.uiCategoryLabel, 8, 0, 1, 1)
|
||||
self.uiCategoryComboBox = QtWidgets.QComboBox(self.MiscTab)
|
||||
self.uiCategoryComboBox.setObjectName("uiCategoryComboBox")
|
||||
self.gridLayout.addWidget(self.uiCategoryComboBox, 3, 2, 1, 1)
|
||||
spacerItem2 = QtWidgets.QSpacerItem(20, 399, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout.addItem(spacerItem2, 4, 1, 1, 2)
|
||||
self.gridLayout.addWidget(self.uiCategoryComboBox, 8, 2, 1, 1)
|
||||
self.uiConsoleTypeLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.uiConsoleTypeLabel.setObjectName("uiConsoleTypeLabel")
|
||||
self.gridLayout.addWidget(self.uiConsoleTypeLabel, 1, 0, 1, 1)
|
||||
self.uiConsoleTypeComboBox = QtWidgets.QComboBox(self.MiscTab)
|
||||
self.uiConsoleTypeComboBox.setObjectName("uiConsoleTypeComboBox")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.gridLayout.addWidget(self.uiConsoleTypeComboBox, 1, 2, 1, 1)
|
||||
self.uiNameLabel.raise_()
|
||||
self.uiNameLineEdit.raise_()
|
||||
self.uiDefaultNameFormatLabel.raise_()
|
||||
self.uiDefaultNameFormatLineEdit.raise_()
|
||||
self.uiSymbolLabel.raise_()
|
||||
self.uiCategoryLabel.raise_()
|
||||
self.uiCategoryComboBox.raise_()
|
||||
self.labeluiConsolePortLabel.raise_()
|
||||
self.uiConsoleHostLabel.raise_()
|
||||
self.uiConsoleHostLineEdit.raise_()
|
||||
self.uiConsolePortSpinBox.raise_()
|
||||
self.uiConsoleHttpPathLabel.raise_()
|
||||
self.uiConsoleHttpPathLineEdit.raise_()
|
||||
self.uiConsoleTypeLabel.raise_()
|
||||
self.uiConsoleTypeComboBox.raise_()
|
||||
self.uiTabWidget.addTab(self.MiscTab, "")
|
||||
self.verticalLayout.addWidget(self.uiTabWidget)
|
||||
|
||||
@@ -236,18 +289,20 @@ class Ui_cloudConfigPageWidget(object):
|
||||
|
||||
def retranslateUi(self, cloudConfigPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
cloudConfigPageWidget.setWindowTitle(_translate("cloudConfigPageWidget", "Cloud configuration"))
|
||||
cloudConfigPageWidget.setWindowTitle(_translate("cloudConfigPageWidget", "Cloud template configuration"))
|
||||
cloudConfigPageWidget.setWhatsThis(_translate("cloudConfigPageWidget", "<html><head/><body><p>A cloud node allows you to connect your project to the "real world" (a network or host) using either an Ethernet interface, a TAP interface (Linux only) or even an UDP tunnel. <span style=\" font-weight:600;\">Please be aware that Wifi interfaces may not work properly.</span></p></body></html>"))
|
||||
self.uiAddEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiAddAllEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
|
||||
self.uiDeleteEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiEthernetListWidget.setSortingEnabled(True)
|
||||
self.uiShowSpecialInterfacesCheckBox.setText(_translate("cloudConfigPageWidget", "&Show special Ethernet interfaces"))
|
||||
self.uiRefreshEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Refresh"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.EthernetTab), _translate("cloudConfigPageWidget", "Ethernet interfaces"))
|
||||
self.uiDeleteTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiTAPListWidget.setSortingEnabled(True)
|
||||
self.uiAddTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiAddAllTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
|
||||
self.uiRefreshTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Refresh"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.TAPTab), _translate("cloudConfigPageWidget", "TAP interfaces"))
|
||||
self.uiUDPTunnelSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "UDP tunnel settings"))
|
||||
self.uiRemoteHostLineEdit.setText(_translate("cloudConfigPageWidget", "127.0.0.1"))
|
||||
@@ -265,9 +320,19 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiUDPTreeWidget.headerItem().setText(3, _translate("cloudConfigPageWidget", "Remote port"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.UDPTab), _translate("cloudConfigPageWidget", "UDP tunnels"))
|
||||
self.uiNameLabel.setText(_translate("cloudConfigPageWidget", "Name:"))
|
||||
self.uiConsoleHostLabel.setText(_translate("cloudConfigPageWidget", "Console host:"))
|
||||
self.labeluiConsolePortLabel.setText(_translate("cloudConfigPageWidget", "Console port:"))
|
||||
self.uiConsoleHttpPathLabel.setText(_translate("cloudConfigPageWidget", "Console HTTP path:"))
|
||||
self.uiDefaultNameFormatLabel.setText(_translate("cloudConfigPageWidget", "Default name format:"))
|
||||
self.uiSymbolLabel.setText(_translate("cloudConfigPageWidget", "Symbol:"))
|
||||
self.uiSymbolToolButton.setText(_translate("cloudConfigPageWidget", "&Browse..."))
|
||||
self.uiCategoryLabel.setText(_translate("cloudConfigPageWidget", "Category:"))
|
||||
self.uiConsoleTypeLabel.setText(_translate("cloudConfigPageWidget", "Console type:"))
|
||||
self.uiConsoleTypeComboBox.setItemText(0, _translate("cloudConfigPageWidget", "telnet"))
|
||||
self.uiConsoleTypeComboBox.setItemText(1, _translate("cloudConfigPageWidget", "vnc"))
|
||||
self.uiConsoleTypeComboBox.setItemText(2, _translate("cloudConfigPageWidget", "spice"))
|
||||
self.uiConsoleTypeComboBox.setItemText(3, _translate("cloudConfigPageWidget", "http"))
|
||||
self.uiConsoleTypeComboBox.setItemText(4, _translate("cloudConfigPageWidget", "https"))
|
||||
self.uiConsoleTypeComboBox.setItemText(5, _translate("cloudConfigPageWidget", "none"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.MiscTab), _translate("cloudConfigPageWidget", "Misc."))
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_preferences_page.ui'
|
||||
#
|
||||
# Created: Fri Jun 10 14:39:19 2016
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
# Created by: PyQt5 UI code generator 5.9
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user