mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-05-17 08:56:06 +03:00
Compare commits
711 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
812aedebe3 | ||
|
|
2f0d2063cf | ||
|
|
ce0515f0ae | ||
|
|
bc10c69a2d | ||
|
|
3707758388 | ||
|
|
8aaefac91b | ||
|
|
19157ab49d | ||
|
|
38b98cd883 | ||
|
|
e9419924c5 | ||
|
|
cf3c5c09fa | ||
|
|
9e89cf5ad5 | ||
|
|
131ef09b55 | ||
|
|
2df18ee04e | ||
|
|
31711a9d4e | ||
|
|
fd2e236927 | ||
|
|
21409a899d | ||
|
|
25be9e7ec7 | ||
|
|
1260c2bc2d | ||
|
|
1441e38876 | ||
|
|
3c8cff20b7 | ||
|
|
f831d71c3f | ||
|
|
f71b6dcda1 | ||
|
|
62289e7be3 | ||
|
|
7447e9b7d4 | ||
|
|
c5961f400e | ||
|
|
6ca61905b2 | ||
|
|
130e91da76 | ||
|
|
bbc5b3e4ac | ||
|
|
2a72ad5e0b | ||
|
|
88ed9407b9 | ||
|
|
18be274fed | ||
|
|
fefda50378 | ||
|
|
3df374e784 | ||
|
|
04fb449b44 | ||
|
|
fc54b76ee1 | ||
|
|
59b284e18b | ||
|
|
1d4492c911 | ||
|
|
9d8b6a172e | ||
|
|
8c3ef7a968 | ||
|
|
947733aada | ||
|
|
a199fef03b | ||
|
|
29b851207b | ||
|
|
ca5557e579 | ||
|
|
7331ae29ef | ||
|
|
3a7e06e14b | ||
|
|
0a81af8248 | ||
|
|
a882956ec9 | ||
|
|
9f4361d66f | ||
|
|
d10c3c7308 | ||
|
|
0cd7d7e4c2 | ||
|
|
8f3f72ff54 | ||
|
|
7fbc0befa1 | ||
|
|
9d9668442e | ||
|
|
932083be88 | ||
|
|
82e7c151c4 | ||
|
|
7e5c363bc3 | ||
|
|
15d029a7fb | ||
|
|
9087ba8f5a | ||
|
|
3d89d6e6cc | ||
|
|
91bae81300 | ||
|
|
7de5bf6bd5 | ||
|
|
9de238619a | ||
|
|
ed88466d63 | ||
|
|
478b793b04 | ||
|
|
841c29e6f6 | ||
|
|
f9d96051f5 | ||
|
|
607e201674 | ||
|
|
18950ca64f | ||
|
|
c0b5f39c4c | ||
|
|
3e717999ca | ||
|
|
800d14363d | ||
|
|
2dd9d61c57 | ||
|
|
10afb5a8de | ||
|
|
3413afe952 | ||
|
|
7222da9512 | ||
|
|
dbe8df5a37 | ||
|
|
a9890265b9 | ||
|
|
97b777ceea | ||
|
|
c06e534935 | ||
|
|
025276f8a7 | ||
|
|
6777961d29 | ||
|
|
7f6cace0d5 | ||
|
|
1a739c0c37 | ||
|
|
6d855045ef | ||
|
|
fef734bbbe | ||
|
|
b079443735 | ||
|
|
4a32ae9736 | ||
|
|
9793d00131 | ||
|
|
2b7840279a | ||
|
|
9243083321 | ||
|
|
3a8b3e5c4a | ||
|
|
e2168a3c81 | ||
|
|
01deb01e6a | ||
|
|
1133ee6e1b | ||
|
|
7512ffec64 | ||
|
|
3527e5551c | ||
|
|
72960f8f2b | ||
|
|
8abb502c72 | ||
|
|
08c729e83a | ||
|
|
aac004bd2f | ||
|
|
70677d8f18 | ||
|
|
fba1ff4208 | ||
|
|
e4edbefc23 | ||
|
|
d93f9afe74 | ||
|
|
162d197e36 | ||
|
|
5c21dd8a2f | ||
|
|
aa9b9d3b0b | ||
|
|
eae9eec15b | ||
|
|
a3bf832721 | ||
|
|
67890d74d9 | ||
|
|
ab4325f951 | ||
|
|
2a947b9cc5 | ||
|
|
601c082288 | ||
|
|
7701d57bd0 | ||
|
|
f0b4148a20 | ||
|
|
2fdcbafbc1 | ||
|
|
5d82cea935 | ||
|
|
b0e3e93c41 | ||
|
|
535f53737d | ||
|
|
354f3eecec | ||
|
|
35a6a5c8c7 | ||
|
|
23cba0a28d | ||
|
|
9c3d7bc95a | ||
|
|
cf2802b15a | ||
|
|
b162c55078 | ||
|
|
bb42b0ed0b | ||
|
|
e108b5194d | ||
|
|
e9ef8735be | ||
|
|
e8e90bb16a | ||
|
|
49f77930f4 | ||
|
|
a58451a552 | ||
|
|
0a43b9e6e9 | ||
|
|
4f32619ed8 | ||
|
|
20740748c1 | ||
|
|
8a5ab6b374 | ||
|
|
e11ce27f7b | ||
|
|
4b7cf4e553 | ||
|
|
b72358461c | ||
|
|
2987bcf91a | ||
|
|
5e97bc0f86 | ||
|
|
e3a3de5df7 | ||
|
|
e1693ce113 | ||
|
|
522091d219 | ||
|
|
1446748934 | ||
|
|
3f0ce380e8 | ||
|
|
bd71383354 | ||
|
|
9649895378 | ||
|
|
8579ffa20a | ||
|
|
3206743329 | ||
|
|
29c87b6e96 | ||
|
|
93b2721d6a | ||
|
|
1ff369683f | ||
|
|
7c56a2467c | ||
|
|
59ef34c17d | ||
|
|
d1fae54049 | ||
|
|
49bd61f769 | ||
|
|
9a4faddd10 | ||
|
|
7654681a94 | ||
|
|
9bfecde957 | ||
|
|
705cbf8bb9 | ||
|
|
ab6e0ce496 | ||
|
|
8042c9eb6f | ||
|
|
ad3c8a09db | ||
|
|
ea4a7f201e | ||
|
|
28c82b8718 | ||
|
|
6a4dd59e81 | ||
|
|
7418c190a8 | ||
|
|
737e32f5c3 | ||
|
|
cfc09d2c14 | ||
|
|
f6ab5cae16 | ||
|
|
39ec7eb8ea | ||
|
|
64c579d43c | ||
|
|
98cc82e6fd | ||
|
|
4b795112b4 | ||
|
|
0e186afaf1 | ||
|
|
b218f7fdf8 | ||
|
|
f1cb6d66f3 | ||
|
|
29758f1b4f | ||
|
|
445dcf3e3b | ||
|
|
f623f28509 | ||
|
|
9d02d57162 | ||
|
|
df03f50e3d | ||
|
|
3b72a66ca5 | ||
|
|
f8aee44442 | ||
|
|
6dd4d1700e | ||
|
|
9b674669db | ||
|
|
53073d458f | ||
|
|
957d89d450 | ||
|
|
db02cbdb2f | ||
|
|
0c9f70152f | ||
|
|
9016975958 | ||
|
|
bf295060fd | ||
|
|
c22ec9f8bd | ||
|
|
bb49cadc6a | ||
|
|
131a49160c | ||
|
|
225b829eae | ||
|
|
e5ef6180b1 | ||
|
|
6e7947eea3 | ||
|
|
3922d370a8 | ||
|
|
833b9d00c9 | ||
|
|
377b8dfcaf | ||
|
|
e68937475f | ||
|
|
6f418f0853 | ||
|
|
8e59927ada | ||
|
|
1012686053 | ||
|
|
672bd850ad | ||
|
|
5db5e1f9fe | ||
|
|
ca94c71bf2 | ||
|
|
76264c55ce | ||
|
|
fd243c42a8 | ||
|
|
a6521ef9e4 | ||
|
|
9fa833762c | ||
|
|
ca0c6468b5 | ||
|
|
15f6945a94 | ||
|
|
645deb8c79 | ||
|
|
428f12a2b3 | ||
|
|
9ad5760ee6 | ||
|
|
82fc4fb3c9 | ||
|
|
df42147d92 | ||
|
|
da5520aa90 | ||
|
|
491c66a315 | ||
|
|
e5c81da700 | ||
|
|
65fad1b4f4 | ||
|
|
34661908d9 | ||
|
|
aee5ffa17f | ||
|
|
e9e8be42b5 | ||
|
|
ae0d928383 | ||
|
|
8db3c1be42 | ||
|
|
f50da3ebd7 | ||
|
|
75b52fc9a4 | ||
|
|
1952da5876 | ||
|
|
1f620026d4 | ||
|
|
1d293618e5 | ||
|
|
2622549ce6 | ||
|
|
900bd1c0b4 | ||
|
|
0b3dbb2843 | ||
|
|
ef4f6b2b27 | ||
|
|
e9806345ca | ||
|
|
ee23e32c75 | ||
|
|
fbeacdcb2a | ||
|
|
b3937c7b94 | ||
|
|
181bf3f360 | ||
|
|
f2711732db | ||
|
|
148ac4b072 | ||
|
|
65eeb79b26 | ||
|
|
537304ce08 | ||
|
|
f22df5f016 | ||
|
|
8dfc8b7714 | ||
|
|
8c6fa9433f | ||
|
|
63837578c5 | ||
|
|
b719703dbe | ||
|
|
084d14c17e | ||
|
|
8c0fca1dd7 | ||
|
|
863d05c923 | ||
|
|
3ebaac8a2c | ||
|
|
16878c9dfa | ||
|
|
45da18bb7c | ||
|
|
7a6d06ea0c | ||
|
|
d371042647 | ||
|
|
0321c11c34 | ||
|
|
522df41a57 | ||
|
|
afccdf5b9e | ||
|
|
b2cd24b511 | ||
|
|
6d131a05f1 | ||
|
|
35e6156c6c | ||
|
|
96d8de4da8 | ||
|
|
6b5a6f3dfe | ||
|
|
8f82eac321 | ||
|
|
e03ed64f59 | ||
|
|
3d702aabd0 | ||
|
|
f5e63c2321 | ||
|
|
1047eb916a | ||
|
|
5dc7d0fbda | ||
|
|
2609be98b6 | ||
|
|
6286e596c0 | ||
|
|
3c546086ed | ||
|
|
f4b2c1c5b9 | ||
|
|
e578ecdd8a | ||
|
|
da8adbaa18 | ||
|
|
6d1333f5fe | ||
|
|
92c858dd07 | ||
|
|
0c7a12f68c | ||
|
|
a4d08cce8c | ||
|
|
e0dd7a66e1 | ||
|
|
23be668c97 | ||
|
|
68d0278140 | ||
|
|
d8e4c1de4d | ||
|
|
a5aa9bfb7a | ||
|
|
3e0273848f | ||
|
|
ec374f173c | ||
|
|
b8abdc79dc | ||
|
|
43744eab7e | ||
|
|
e16f700e49 | ||
|
|
925d57b2f8 | ||
|
|
eceaea1317 | ||
|
|
4326785dfc | ||
|
|
3920c28bde | ||
|
|
b34f51e4b0 | ||
|
|
ef45b2e0f1 | ||
|
|
545a9f53a8 | ||
|
|
83d9367860 | ||
|
|
2131f07e5f | ||
|
|
cf3e716e63 | ||
|
|
c79f14bcab | ||
|
|
acd044a88a | ||
|
|
f26c638350 | ||
|
|
4ea24e622b | ||
|
|
ab854752d9 | ||
|
|
5cee045a65 | ||
|
|
37cd82fb44 | ||
|
|
334eb5175c | ||
|
|
25841ea7db | ||
|
|
3d3b4f92b2 | ||
|
|
82740da89d | ||
|
|
ad19b3dda0 | ||
|
|
bb8fd18f98 | ||
|
|
336eaf443a | ||
|
|
0b94be6805 | ||
|
|
671ced78ff | ||
|
|
c8766ce529 | ||
|
|
bec9512c78 | ||
|
|
b2ad5f4158 | ||
|
|
966873bc6c | ||
|
|
5b9111b55d | ||
|
|
56688f2236 | ||
|
|
2e656a9d53 | ||
|
|
2790f707c3 | ||
|
|
ee9002df61 | ||
|
|
52626e9fe9 | ||
|
|
6619c6af97 | ||
|
|
60e04c7248 | ||
|
|
724858f977 | ||
|
|
5a2e05a4fd | ||
|
|
010888e3ca | ||
|
|
3226921536 | ||
|
|
022e918301 | ||
|
|
846b19a9e7 | ||
|
|
45f5c6e010 | ||
|
|
963bbb7b89 | ||
|
|
016ad7a775 | ||
|
|
e8c82566c6 | ||
|
|
1ed6fceade | ||
|
|
d945fd8b7b | ||
|
|
fd6c7eccd0 | ||
|
|
7a1afe2aec | ||
|
|
6debe56d8e | ||
|
|
a4c7d41c26 | ||
|
|
ea9243dcd9 | ||
|
|
e9d8337bd6 | ||
|
|
3c92e463f8 | ||
|
|
3d07db5c5f | ||
|
|
20cc309ac8 | ||
|
|
262a2839c5 | ||
|
|
ece4d51213 | ||
|
|
0ef39ba129 | ||
|
|
f90267b4f0 | ||
|
|
8f16706a22 | ||
|
|
2d3ee3abf9 | ||
|
|
b8b209fa55 | ||
|
|
18129e3d29 | ||
|
|
7a2b9c024f | ||
|
|
4923a6dc17 | ||
|
|
73dfc047aa | ||
|
|
fe0a70c4be | ||
|
|
67014965be | ||
|
|
f14cb43404 | ||
|
|
f8517ee5ac | ||
|
|
7dc607b4c5 | ||
|
|
882fa76550 | ||
|
|
1490a1ad8f | ||
|
|
aab0c99cc6 | ||
|
|
a6a987d74c | ||
|
|
9c58b18c20 | ||
|
|
8bc499c68f | ||
|
|
bd5eb288b7 | ||
|
|
465a289568 | ||
|
|
d240ba3056 | ||
|
|
3cedfd3649 | ||
|
|
276d7abdd9 | ||
|
|
927e38bd6d | ||
|
|
376cc29995 | ||
|
|
1f8ebeb084 | ||
|
|
0212755c78 | ||
|
|
2f7d75eae9 | ||
|
|
fc1c060922 | ||
|
|
0ea72ce782 | ||
|
|
3de2d2eda2 | ||
|
|
c08262f8af | ||
|
|
9ae70bf2fe | ||
|
|
fa6d250602 | ||
|
|
0668840a2b | ||
|
|
8b25d1b06c | ||
|
|
58c3ba0755 | ||
|
|
5a91c9aaf8 | ||
|
|
0fc3f4ef16 | ||
|
|
f0e5cd2ba2 | ||
|
|
f59ef6378a | ||
|
|
61ef08d1b7 | ||
|
|
e812c000fd | ||
|
|
d3d9e1e8ae | ||
|
|
05f8df345a | ||
|
|
4b0cc11cab | ||
|
|
b5285cd142 | ||
|
|
69482343ba | ||
|
|
d4639c2e61 | ||
|
|
b85ade9dd7 | ||
|
|
e191cb8aa3 | ||
|
|
e6bc75ce26 | ||
|
|
bc1df346f2 | ||
|
|
27c35321f0 | ||
|
|
3e212fc629 | ||
|
|
25e41dc0f1 | ||
|
|
c58c7774c4 | ||
|
|
bd2bc8265c | ||
|
|
f2209a2780 | ||
|
|
7b99ba325b | ||
|
|
74763287fb | ||
|
|
737ff42d64 | ||
|
|
5656bd2d48 | ||
|
|
058c069394 | ||
|
|
926ec48d00 | ||
|
|
410e5353b2 | ||
|
|
bfb90406ed | ||
|
|
439cdce287 | ||
|
|
4e50c2a4b1 | ||
|
|
94c636ae61 | ||
|
|
f53b9a266e | ||
|
|
2476448032 | ||
|
|
a34cd742e3 | ||
|
|
39698196ac | ||
|
|
61432ced4f | ||
|
|
2ddd13c445 | ||
|
|
af6c4c5b3e | ||
|
|
d4012294bf | ||
|
|
4b04b0e855 | ||
|
|
1bec5019bf | ||
|
|
ec6b876baa | ||
|
|
7cee0d01ab | ||
|
|
dd3314d06b | ||
|
|
9c58b26265 | ||
|
|
83c26f47da | ||
|
|
8ed2f55600 | ||
|
|
b435317904 | ||
|
|
acb8aa8ca2 | ||
|
|
c55d6b8a6f | ||
|
|
a4039a254e | ||
|
|
85ed4b3026 | ||
|
|
5207a99692 | ||
|
|
d69527995d | ||
|
|
4b000ba2f7 | ||
|
|
1b302b77a0 | ||
|
|
a5b5c404ec | ||
|
|
6b97b0c6cd | ||
|
|
115dd43eee | ||
|
|
2530bf97a8 | ||
|
|
9892fd0654 | ||
|
|
c71ee73da8 | ||
|
|
0643fd516d | ||
|
|
a25680f2ce | ||
|
|
58bd5be920 | ||
|
|
d95633ba2c | ||
|
|
dfea6d1723 | ||
|
|
ddeb95cb0a | ||
|
|
5f7ff0d70d | ||
|
|
a00e039cec | ||
|
|
a24e9adef1 | ||
|
|
ee5f8e8edd | ||
|
|
f5470130f5 | ||
|
|
1ff405885e | ||
|
|
9fb42ead9f | ||
|
|
2ea1946c0f | ||
|
|
963e054918 | ||
|
|
0f5f6ab645 | ||
|
|
8a905b5c39 | ||
|
|
e917193f06 | ||
|
|
16846ce49c | ||
|
|
624a670ae7 | ||
|
|
406326ccd8 | ||
|
|
24bc15fb73 | ||
|
|
348d8b9438 | ||
|
|
6787982408 | ||
|
|
c2384917fa | ||
|
|
b80178d0cf | ||
|
|
e6084ed834 | ||
|
|
ba924cd0d9 | ||
|
|
0c3d43346f | ||
|
|
fcf6ef3027 | ||
|
|
e0f87e573d | ||
|
|
3ec068f0cb | ||
|
|
37f1fcf6f7 | ||
|
|
c51dd1605d | ||
|
|
4ebf3b4e1c | ||
|
|
b1ec9d535c | ||
|
|
7fc9087cf0 | ||
|
|
5dc2c77806 | ||
|
|
4972d460d2 | ||
|
|
c388836be7 | ||
|
|
18ae4a6ce9 | ||
|
|
3020e1fc9f | ||
|
|
fe2f8424db | ||
|
|
a744f65199 | ||
|
|
d27578f0fc | ||
|
|
b01c11f19b | ||
|
|
fb269da4d3 | ||
|
|
ab15f96bb5 | ||
|
|
5bb8b8e8bd | ||
|
|
3f9632fae0 | ||
|
|
b5f8195abb | ||
|
|
73a293bd17 | ||
|
|
0a1dfb99e9 | ||
|
|
d352919264 | ||
|
|
65f2a1e461 | ||
|
|
71f289721b | ||
|
|
c28089d400 | ||
|
|
64f009bf71 | ||
|
|
edb2fd7fd9 | ||
|
|
62e7ad8c8a | ||
|
|
caeb5d71c3 | ||
|
|
cfe96b2311 | ||
|
|
8955b9ee29 | ||
|
|
e727abf27a | ||
|
|
f209bf7644 | ||
|
|
5860dedc32 | ||
|
|
9e2df17a4e | ||
|
|
a95761437a | ||
|
|
626510865f | ||
|
|
2e248aa340 | ||
|
|
e306f73f01 | ||
|
|
0c4367d77e | ||
|
|
6dda0ff787 | ||
|
|
d7d4b84309 | ||
|
|
7b57983699 | ||
|
|
bb89fe2275 | ||
|
|
7eb2a923b2 | ||
|
|
58052e3cce | ||
|
|
e431104f6b | ||
|
|
a4e9d6b8ce | ||
|
|
f58f5c7b95 | ||
|
|
37faa39309 | ||
|
|
4d64598ed2 | ||
|
|
5132c4e172 | ||
|
|
3c3fdd9ffd | ||
|
|
efa50571c6 | ||
|
|
ca9b10fcca | ||
|
|
8660161b10 | ||
|
|
50ebfb9c06 | ||
|
|
26df59d6b6 | ||
|
|
b903e2ad73 | ||
|
|
b2fe7eb643 | ||
|
|
8095fef228 | ||
|
|
b8da5440f5 | ||
|
|
6cea094e4e | ||
|
|
9ac46c9d50 | ||
|
|
c8a8663ff0 | ||
|
|
d27e5c1795 | ||
|
|
0d2f91709c | ||
|
|
6dc44d5108 | ||
|
|
9c6be0341b | ||
|
|
011a49e998 | ||
|
|
e18c2df5f5 | ||
|
|
1794b8389f | ||
|
|
0379c370eb | ||
|
|
e03550a89b | ||
|
|
c6ea775e81 | ||
|
|
38233ba5e9 | ||
|
|
89c1272bc1 | ||
|
|
8bbb46c599 | ||
|
|
74fca3d736 | ||
|
|
7aeed7aa59 | ||
|
|
aa15ace887 | ||
|
|
2d0a7b5f58 | ||
|
|
20a09b56c1 | ||
|
|
1938cdabae | ||
|
|
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 | ||
|
|
a9a2a541c0 | ||
|
|
8998c07e0e | ||
|
|
ba01a89af1 | ||
|
|
eae07d62ad | ||
|
|
23903cf0c9 | ||
|
|
4d908fd855 | ||
|
|
bb0e67be4f | ||
|
|
0410c446fc | ||
|
|
18486e4772 |
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.
|
||||
16
.github/workflows/add-new-issues-to-project.yml
vendored
Normal file
16
.github/workflows/add-new-issues-to-project.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Add new issues to GNS3 project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.4.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/GNS3/projects/3
|
||||
github-token: ${{ secrets.ADD_NEW_ISSUES_TO_PROJECT }}
|
||||
76
.github/workflows/codeql.yml
vendored
Normal file
76
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '27 6 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
19
.github/workflows/testing.yml
vendored
Normal file
19
.github/workflows/testing.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: testing
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build and run Docker image
|
||||
run: |
|
||||
docker build -t gns3-gui-test .
|
||||
docker run gns3-gui-test
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -63,3 +63,4 @@ __pycache__
|
||||
|
||||
# Virtualenv
|
||||
env
|
||||
venv
|
||||
|
||||
21
.travis.yml
21
.travis.yml
@@ -1,21 +0,0 @@
|
||||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
notifications:
|
||||
email: false
|
||||
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=
|
||||
on:
|
||||
tags: true
|
||||
repo: GNS3/gns3-gui
|
||||
14
.whitesource
Normal file
14
.whitesource
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"scanSettings": {
|
||||
"configMode": "AUTO",
|
||||
"configExternalURL": "",
|
||||
"projectToken" : "",
|
||||
"baseBranches": ["master", "2.2", "3.0"]
|
||||
},
|
||||
"checkRunSettings": {
|
||||
"vulnerableCheckRunConclusionLevel": "failure"
|
||||
},
|
||||
"issueSettings": {
|
||||
"minSeverityLevel": "LOW"
|
||||
}
|
||||
}
|
||||
524
CHANGELOG
524
CHANGELOG
@@ -1,5 +1,529 @@
|
||||
# Change Log
|
||||
|
||||
## 2.2.47 15/05/2024
|
||||
|
||||
* Remove maximum size for capture dialog. Ref #3576
|
||||
* Change sentry-sdk version
|
||||
* Upgrade aiohttp, sentry-sdk and truststore
|
||||
* Upgrade jsonschema and aiohttp
|
||||
* Drop Python 3.7
|
||||
* Remove dev requirements for Python 3.6
|
||||
* Add NAT symbols
|
||||
* Only show log message if event has "message"
|
||||
|
||||
## 2.2.46 26/02/2024
|
||||
|
||||
* Add GNS3 console command "env" to show what environment variables are used. Ref https://github.com/GNS3/gns3-server/issues/2306
|
||||
* Add CTRL+C shortcut to copy status bar message. Ref #3561
|
||||
* Key modifier (ALT) to ignore snap to grid. Fixes #3538
|
||||
* Increase timeout to 5s for status bar messages. The coordinates message has no timeout and can be reset when clicking on the scene. Ref #3561
|
||||
* Add reset GUI state feature. Ref #3549
|
||||
* Fix for hiding Windows terminal. Ref #3290
|
||||
* Drop support for Python 3.6
|
||||
* Upgrade sentry-sdk, psutil and distro dependencies
|
||||
|
||||
## 2.2.45 12/01/2024
|
||||
|
||||
* Add missing console_type values in appliance_v8.json. Ref https://github.com/GNS3/gns3-registry/issues/849
|
||||
* Handle moved project notifications on controller stream
|
||||
* Add debug for PATH env variable
|
||||
* Add custom executable paths on Windows
|
||||
* Add --suppressApplicationTitle for Windows terminal. Fixes https://github.com/GNS3/gns3-gui/issues/3544
|
||||
* Upgrade sentry-sdk and aiohttp
|
||||
|
||||
## 2.2.44.1 07/11/2023
|
||||
|
||||
* No changes
|
||||
|
||||
## 2.2.44 06/11/2023
|
||||
|
||||
* Fix timeout issue when creating Qemu disk image. Fixes https://github.com/GNS3/gns3-server/issues/2313
|
||||
* Refactor command variables support
|
||||
* Add vendor_logo_url in appliance schemas. Ref https://github.com/GNS3/gns3-registry/pull/825
|
||||
* Add Qemu IGB network device
|
||||
* Add the ability to edit width and height in the style edit dialog.
|
||||
|
||||
## 2.2.43 19/09/2023
|
||||
|
||||
* Add KiTTY to preconfigured telnet consoles. Fixes #3507
|
||||
* Fix generic icon in Wayland. Ref #3501
|
||||
* Support for appliance format version 8.
|
||||
* Use importlib instead of pkg_resources
|
||||
* Upgrade to PyQt 5.15.9 and pywin32
|
||||
* Add support for appliance version 8 format
|
||||
|
||||
## 2.2.42 09/08/2023
|
||||
|
||||
* Use the system's certificate store for SSL connections
|
||||
* Give a node some time to start before opening the console (for console auto start). Fixes #3474
|
||||
* Use Mate Terminal by default if installed on Debian, Ubuntu and Linux Mint.
|
||||
* Support for gnome-terminal tabs to be opened in the same window.
|
||||
* Remove import urllib3 and let sentry_sdk import and patch it. Fixes https://github.com/GNS3/gns3-gui/issues/3498
|
||||
* Add import sys in sudo.py
|
||||
* Rounded Rectangle support
|
||||
|
||||
## 2.2.41 12/07/2023
|
||||
|
||||
* Use alternative method to set the correct permissions for uBridge on macOS
|
||||
* Remove sending stats to GA
|
||||
* Catch urllib3 exceptions when sending crash report. Ref https://github.com/GNS3/gns3-gui/issues/3483
|
||||
* Backport UEFI boot mode support for Qemu VMs
|
||||
* Add debug for dropEvent. Ref https://github.com/GNS3/gns3-server/issues/2242
|
||||
|
||||
## 2.2.40.1 10/06/2023
|
||||
|
||||
* No changes
|
||||
|
||||
## 2.2.40 06/06/2023
|
||||
|
||||
* Change log messages for Websocket errors
|
||||
* Do not proceed if an appliance symbol cannot be downloaded. Ref #3466
|
||||
* Delete a node or link from topology summary view using Delete key. Ref #3445
|
||||
* Fix "Start the capture visualization program" checkbox works only one (first) time for a given link. Fixes #3442
|
||||
* Let the selected link style applied when editing a link. Fixes #3460
|
||||
* Fix hovered color shown in style editing dialog. Fixes #3460
|
||||
|
||||
## 2.2.39 08/05/2023
|
||||
|
||||
* Fix nodes are not snapped to the grid at the moment of creation
|
||||
* Upgrade distro and aiohttp dependencies
|
||||
|
||||
## 2.2.38 28/02/2023
|
||||
|
||||
* Add long description content type in setup.py
|
||||
* Automatically add new issues to GNS3 project
|
||||
* Development 2.2.38.dev1
|
||||
|
||||
## 2.2.37 25/01/2023
|
||||
|
||||
* Upgrade to PyQt5 v5.15.7
|
||||
* Changed Windows Terminal telnet console profile from OS X to windows ref: issue #3193
|
||||
|
||||
## 2.2.36 04/01/2023
|
||||
|
||||
* Add Trusted Platform Module (TPM) support for Qemu VMs
|
||||
* Add "on_close" setting to appliance schema. Fixes https://github.com/GNS3/gns3-server/issues/2148
|
||||
* Add default 'ide' disk interface when manually creating Qemu VM template. Fixes #3360
|
||||
* Fix zoom factor is multiplied when loading projects. Fixes #3408
|
||||
* Remove deprecated PuTTY option in preferences. Ref https://github.com/GNS3/gns3-gui/discussions/3415
|
||||
|
||||
|
||||
## 2.2.35.1 10/11/2022
|
||||
|
||||
* Re-release Web-Ui v2.2.35
|
||||
|
||||
## 2.2.35 08/11/2022
|
||||
|
||||
* Fix "variables": [] in project file leads to unlimited increase of empty name/value pairs in GUI. Fixes #3397
|
||||
* Make version PEP 440 compliant
|
||||
* Support for Python 3.11
|
||||
* Upgrade PyQt to 5.15.7 and pywin32 to v305
|
||||
* Allow for more dependency versions at patch level
|
||||
* Replace deprecated distro.linux_distribution() call
|
||||
* Add a fix for the CVE-2007-4559
|
||||
|
||||
## 2.2.34 28/08/2022
|
||||
|
||||
* Upgrade dev dependencies
|
||||
* Implement new option (Delete All) to contextual menu in "Console" dock. Fixes #3325
|
||||
* Fix 2560x1440 resolution for Docker container
|
||||
|
||||
## 2.2.33.1 21/06/2022
|
||||
|
||||
* Match GNS3 server version
|
||||
|
||||
## 2.2.33 20/06/2022
|
||||
|
||||
* Upgrade sentry-sdk and psutil
|
||||
* Check that node names for Qemu and Docker are valid
|
||||
* Backport reset all console connections. Fixes #2072
|
||||
* Add more video resolutions to Docker containers using VNC. Fixes #3329
|
||||
* Add python_requires=">=3.4" in setup.py. Fixes #3326
|
||||
* Only allow post release corrective versions of GUI and server to interact
|
||||
* Allow minor versions of GUI and server to interact
|
||||
* Update VirtViewer path. Fixes #3334
|
||||
|
||||
## 2.2.32 27/04/2022
|
||||
|
||||
* Use public DSNs for Sentry
|
||||
* Fix exception when doubleclick on NAT node. Fixes #3312
|
||||
* Fix "Apply" button in the "Preferences" dialog stays gray when templates/nodes are opened by double-click. Fixes #3307
|
||||
* Add 'reset docks' in the view menu. Ref #3317
|
||||
|
||||
## 2.2.31 26/02/2022
|
||||
|
||||
* Install setuptools v59.6.0 when using Python 3.6
|
||||
|
||||
## 2.2.30 25/02/2022
|
||||
|
||||
* Set setuptools to v60.6.0
|
||||
* Upgrade to pywin32 v303. Ref #3290
|
||||
* Fix int() call. Ref #3283
|
||||
* Fix QPoint() as unexpected type 'float'. Fixes #3283
|
||||
* Fix painter.drawRect() has unexpected type 'float'. Fixes #3282
|
||||
* Fix SpinBox.setValue() requires integer. Fixes #3281
|
||||
|
||||
## 2.2.29 08/01/2022
|
||||
|
||||
* Clear cache when opening symbol selection dialog. Fixes #3256
|
||||
* Fix @ in username issue with HTTP authentication. Fixes #3275
|
||||
* Use '//' operator instead of int()
|
||||
* Fix create drawing item calls since mapToScene() returns a QPointF https://doc.qt.io/qt-5/qgraphicsview.html#mapToScene-4
|
||||
* Fixed QPoint called with floats
|
||||
|
||||
## 2.2.28 15/12/2021
|
||||
|
||||
* Fixed drawLine called with float arguments
|
||||
* Fixed dead VIX API link
|
||||
|
||||
## 2.2.27 12/11/2021
|
||||
|
||||
* Fix symbols in "Symbol selection" dialog are not placed in alphabetical order. Fixes #3245
|
||||
* Fix links duplicates in topology summary. Fixes #3251
|
||||
* chore : use --no-cache-dir flag to pip in dockerfiles to save space
|
||||
|
||||
## 2.2.26 08/10/2021
|
||||
|
||||
* Upgrade embedded Python to version 3.7 in Windows package
|
||||
* Upgrade Visual C++ Redistributable for Visual Studio 2019 in Windows package
|
||||
* Fix SSL support in Windows package
|
||||
* Open "template configuration" dialog with double click on template name in "Preferences". Fixes #3239
|
||||
* Only show "virtio" network adapter when legacy node is enabled. Fixes https://github.com/GNS3/gns3-gui/issues/1969
|
||||
* Double-click on a template opens "template configuration" dialog. Fixes #3236
|
||||
* Fix "Custom symbols" can't be unfolded after using "Filter" field. Fixes #3231
|
||||
|
||||
## 2.2.25 14/09/2021
|
||||
|
||||
* Fix menu disabled for modal dialogs on macOS. Fixes #3007
|
||||
* Change method to display the recent files menu. Fixes #3007
|
||||
* Fix bug when using empty port names for custom adapters. Fixes #3228
|
||||
* Upgrade Qt to version 5.15.4 on macOS
|
||||
* Fix mouse zoom-in/out step value is two times bigger than keyboard one. Fixes #3226
|
||||
* Upgrade to Qt 5.15.4 on Windows. Ref #3210
|
||||
* Fix issue with custom adapters at the node level. Fixes #3223
|
||||
* Explicitly require setuptools, utils/get_resource.py imports pkg_resources
|
||||
|
||||
## 2.2.24 25/08/2021
|
||||
|
||||
* Fix incorrect Qemu binary selected when importing template. Fixes https://github.com/GNS3/gns3-gui/issues/3216
|
||||
* Early support for Python3.10
|
||||
* Bump pywin32 from 300 to 301
|
||||
* Add PyQt5==5.12.3 for macOS build
|
||||
|
||||
## 2.2.23 05/08/2021
|
||||
|
||||
* Handle -no-kvm param deprecated in Qemu >= v5.2
|
||||
* Support for invisible links. Fixes #2461
|
||||
* Add kitty console application command line. Fixes #3203
|
||||
* Add Windows Terminal profile as an option for Console Applications. Fixes #3193
|
||||
|
||||
## 2.2.22 10/06/2021
|
||||
|
||||
* Fix exception shown when GNS3 is started with empty config. Fixes #3188
|
||||
* Add ZOC8 console terminal for macOS command line
|
||||
* Link style support. Fixes https://github.com/GNS3/gns3-gui/issues/2461
|
||||
* Fix charcoal theme. Ref #3137
|
||||
* Fix issue when showing menu to select port. Fixes #3169
|
||||
|
||||
## 2.2.21 10/05/2021
|
||||
|
||||
* Fix issue with empty project variable name. Fixes #3162
|
||||
* Downgrade to PyQt5 5.12.1. Fixes https://github.com/GNS3/gns3-gui/issues/3169
|
||||
|
||||
## 2.2.20 09/04/2021
|
||||
|
||||
* Fix project does not load anymore. Fixes #3140
|
||||
* Do not connect to server while waiting for user to accept/reject SSL certificate. Fixes #3144
|
||||
* Fix invalid server version check request. Fixes #3144
|
||||
* Upgrade dependencies
|
||||
* Add terminator as a predefined custom console option
|
||||
|
||||
## 2.2.19 05/03/2021
|
||||
|
||||
* No changes
|
||||
|
||||
## 2.2.18 16/02/2021
|
||||
|
||||
* SSL support.
|
||||
* Remove the useless file "zoom-in (copy).svg". Fixes #3114
|
||||
* Use HDD disk image as startup QEMU config disk
|
||||
* Edit only text mode config files
|
||||
* Hide config import/export when configFiles attribute is empty
|
||||
* Qemu disk interfaces must be set to "none" by default. Ref #3035
|
||||
* Do not allow image to be configured on Qemu VM secondary slave disk if create config disk option is enabled.
|
||||
* Add explicit option to automatically create or not the config disk. Off by default.
|
||||
* QEMU config disk support
|
||||
|
||||
## 2.2.17 04/12/2020
|
||||
|
||||
* Remove "-nographic" option by default for Qemu VM. Fixes #3094
|
||||
* Fix app cannot start on macOS Big Sur. Ref #3037
|
||||
* Require confirmation before stopping all devices.
|
||||
|
||||
## 2.2.16 05/11/2020
|
||||
|
||||
* Fix packets capture stops after some time. Fixes #3067
|
||||
* Option to allocate or not the vCPUs and RAM settings for the GNS3 VM. Fixes https://github.com/GNS3/gns3-gui/issues/3069
|
||||
|
||||
## 2.2.15 07/10/2020
|
||||
|
||||
* Fix custom symbol not sent to remote controller when installing appliance
|
||||
|
||||
## 2.2.14 14/09/2020
|
||||
|
||||
* Improvements to add a new version of an appliance from wizard. Fixes #3002.
|
||||
|
||||
## 2.2.13 04/09/2020
|
||||
|
||||
* No changes
|
||||
|
||||
## 2.2.12 07/08/2020
|
||||
|
||||
* Downgrade psutil to version 5.6.7
|
||||
* Fix log shows the GUI command line without spaces between its arguments. Fixes #3026
|
||||
* Use server host is console host is equal to "0:0:0:0:0:0:0:0"
|
||||
* Remove VMware promotion.
|
||||
|
||||
## 2.2.11 09/07/2020
|
||||
|
||||
* Try to fix "Recent project" selection not working. Ref #3007
|
||||
* Fix debug entries shown twice in console window and double error messages with remote GNS3VM. Fixes #3010
|
||||
* Fix deprecation warning. Ref #3009
|
||||
* Fix tests on macOS. Ref #3009
|
||||
* Fix sentry SDK is configured twice.
|
||||
|
||||
## 2.2.10 18/06/2020
|
||||
|
||||
* New fix for multi-device selection/deselection not working as expected with right click. Fixes #2986
|
||||
* Optimize snap-to-grid code for drawing items. Fixes #2997
|
||||
* Move jsonschema 2.6.0 requirement in build repository.
|
||||
* Only use jsonschema 2.6.0 on Windows and macOS.
|
||||
* Disable default integrations for sentry sdk.
|
||||
|
||||
## 2.2.9 04/06/2020
|
||||
|
||||
* Fix GUI doesn't detect another GUI on macOS. Fixes #2994
|
||||
* Support to activate/deactive network connection state replication in Qemu.
|
||||
* Option to reset or not all MAC addresses when exporting or duplicating a project.
|
||||
* Fix Multi-device selection/deselection not working as expected with right click. Fixes #2986
|
||||
* Replace Raven by Sentry SDK. Fixes https://github.com/GNS3/gns3-server/issues/1758
|
||||
* Fix online help menu URL. Fixes #2984
|
||||
* Require setuptools>=17.1 in setup.py. Ref https://github.com/GNS3/gns3-server/issues/1751 This is to support environmental markers. https://github.com/pypa/setuptools/blob/master/CHANGES.rst#171
|
||||
* Update README. Ref https://github.com/GNS3/gns3-server/issues/1719
|
||||
* Restore editReadme attribute which was removed in Change 'New export project wizard'
|
||||
* Updated GUI pyqt files from Tab Order 'fixes' in "Tab Order in Preferences and Project Dialog #2872"
|
||||
|
||||
## 2.2.8 07/05/2020
|
||||
|
||||
* Default port set to 80 for server running in the GNS3 VM. Fixes #1737
|
||||
* Make the Web UI the default page. Ref https://github.com/GNS3/gns3-server/issues/1737
|
||||
* Fix "export portable project forgets contents of README". Fixes #1724
|
||||
* Activate unified title and toolbar on MacOS. Fixes #2968
|
||||
* Confirmation dialog for "console connect to all nodes". Fixes #2971
|
||||
* Add "Resume all suspended links". Fixes #2858
|
||||
* Revert "Change default path for SecureCRT. Fixes #2896"
|
||||
* Remove @property from ConfigurationDialog(). Fixes #2819 #2965
|
||||
* Use Environmental Markers to force jsonschema version. Fixes https://github.com/GNS3/gns3-gui/issues/2849 Version 3.2.0 with Python >= 3.8 Version 2.6.0 with Python < 3.8
|
||||
* Use Environmental Markers to force jsonschema version 2.6.0 on Windows/macOS. Ref https://github.com/GNS3/gns3-gui/issues/2849
|
||||
* Remove preferences dialog geometry restoration. Fixes #2807
|
||||
* Fix unable to configure custom adapters for Qemu VMs. Fixes #2961
|
||||
|
||||
## 2.2.7 07/04/2020
|
||||
|
||||
* Fix VNC console template doesn't extract %i (Project UUID). Fixes #2960
|
||||
* Fix contextual menu issues. Ref #2955
|
||||
|
||||
## 2.2.6 26/03/2020
|
||||
|
||||
* Prevent locked drawings to be deleted. Fixes https://github.com/GNS3/gns3-gui/issues/2948
|
||||
* Fix issues with empty project variables. Fixes https://github.com/GNS3/gns3-gui/issues/2941
|
||||
* Upgrade psutil to version 5.6.6 due to CVE-2019-18874 https://github.com/advisories/GHSA-qfc5-mcwq-26q8
|
||||
* Use existing README.txt if existing when exporting portable project. Fixes https://github.com/GNS3/gns3-server/issues/1724
|
||||
* Allow creation of a diskless Qemu VMs. Fixes #2939
|
||||
* Re-enable "create new version" in appliance wizard. Fixes #2837
|
||||
* Fix unable to load project from project library. Fixes #2932
|
||||
* Fix some permission denied errors when loading remote project. Ref #2871 Fixes #2901
|
||||
* Add 'Royal TS V5' to predefined console list
|
||||
* Disallow invalid grid sized. Fixes #2908
|
||||
* Check if hostname is blank. Fixes #2924
|
||||
* Add nvme disk interface and fix scsi disk interface for Qemu VMs.
|
||||
* Add latest Qemu nic models.
|
||||
* Upgrade Qt version to 5.14.1. Ref #2778 #2903
|
||||
|
||||
## 2.2.5 09/01/2020
|
||||
|
||||
* Add gns3-gui.xml and update Linux icons paths & permissions. Ref #2919
|
||||
|
||||
## 2.2.4 08/01/2020
|
||||
|
||||
* Fix "Console to all nodes" doesn't open cloud objects with console configured. Fixes #2902
|
||||
* Change default path for SecureCRT. Fixes #2896
|
||||
* Add icons in setup.py Ref #2898
|
||||
* Add remote viewer as a VNC console for Linux. Fixes #2913
|
||||
|
||||
## 2.2.3 12/11/2019
|
||||
|
||||
* Fix issue when binding on 0.0.0.0. Fixes #2892
|
||||
* Allow double click on cloud with configured console to open session. Fixes #2894
|
||||
* Officially support Python 3.8. Ref https://github.com/GNS3/gns3-gui/issues/2895
|
||||
* Set psutil to version 5.6.3 in requirements.txt
|
||||
|
||||
## 2.2.2 04/11/2019
|
||||
|
||||
* Fix KeyError: 'spice+agent'. Fixes #2890
|
||||
* Fix wrong log.error() call when exporting file.
|
||||
* Revert "Explicitly cleanup the cache directory."
|
||||
* Fix "UnboundLocalError: local variable 'pywintypes' referenced before assignment"
|
||||
* Fix GUI uses only telnet console. Fixes #2885
|
||||
* Fix missing sys module in sudo.py Fixes #2886
|
||||
|
||||
## 2.2.1 01/11/2019
|
||||
|
||||
* Check if console_type is None.
|
||||
* Explicitly cleanup the cache directory.
|
||||
* Get Windows interface from registry if cannot load win32com module.
|
||||
* Ignore OSError returned by psutil when bringing console to front.
|
||||
* Catch error if NPF or NPCAP service cannot be detected. Ref https://github.com/GNS3/gns3-server/issues/1670
|
||||
* Better handling for reading synchronous JSON response from server. Ref #2874
|
||||
* Fix JSONDecodeError when getting server version. Fixes #2874
|
||||
* Fix FileNotFoundError exceptions when launching SPICE or VNC clients.
|
||||
* Fix UnboundLocalError local variable 'win32serviceutil' referenced before assignment
|
||||
* 'Fix' tab order in preferences dialog so it follows the layout
|
||||
* 'Fix' tab order in edit project dialog so it follows the layout
|
||||
* Use compatible shlex_quote to handle case where Windows needs double quotes around file names, not single quotes. Ref https://github.com/GNS3/gns3-gui/issues/2866
|
||||
* Use 0.0.0.0 by default for server host. Fixes https://github.com/GNS3/gns3-server/issues/1663
|
||||
* Catch IndexError when configuring port names. Fixes #2865
|
||||
|
||||
## 2.2.0 30/09/2019
|
||||
|
||||
* No changes
|
||||
|
||||
## 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
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -1,22 +1,16 @@
|
||||
# Run tests inside a container
|
||||
FROM ubuntu:17.10
|
||||
|
||||
FROM ubuntu:18.04
|
||||
MAINTAINER GNS3 Team
|
||||
|
||||
#ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --force-yes python3.6 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3.6-dev xvfb
|
||||
RUN apt-get clean
|
||||
|
||||
|
||||
ADD dev-requirements.txt /dev-requirements.txt
|
||||
ADD requirements.txt /requirements.txt
|
||||
RUN pip3 install -r /dev-requirements.txt
|
||||
|
||||
RUN pip3 install --no-cache-dir -r /dev-requirements.txt
|
||||
|
||||
ADD . /src
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
CMD xvfb-run python3.6 -m pytest -vv
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
include README.rst
|
||||
include README.md
|
||||
include AUTHORS
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
include tox.ini
|
||||
recursive-include tests *
|
||||
recursive-include gns3 *
|
||||
recursive-include resources *
|
||||
|
||||
62
README.md
Normal file
62
README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
GNS3-gui
|
||||
========
|
||||
|
||||
[](https://github.com/GNS3/gns3-gui/actions?query=workflow%3Atesting)
|
||||
|
||||
[](https://pypi.python.org/pypi/gns3-gui)
|
||||
|
||||
[](https://snyk.io/test/github/GNS3/gns3-gui)
|
||||
|
||||
GNS3 GUI repository.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Please see <https://docs.gns3.com/>
|
||||
|
||||
Software dependencies
|
||||
---------------------
|
||||
|
||||
PyQt5 which is either part of the Linux distribution or installable from
|
||||
PyPi. The other Python dependencies are automatically installed during
|
||||
the GNS3 GUI installation and are listed
|
||||
[here](https://github.com/GNS3/gns3-gui/blob/master/requirements.txt)
|
||||
|
||||
For connecting to nodes using Telnet, a Telnet client is required. On
|
||||
Linux that's a terminal emulator like xterm, gnome-terminal, konsole
|
||||
plus the telnet program. For connecting to nodes with a GUI, a VNC
|
||||
client is required, optionally a SPICE client can be used for Qemu
|
||||
nodes.
|
||||
|
||||
For using packet captures within GNS3, Wireshark should be installed.
|
||||
It's recommended, but if you don't need that functionality you can go
|
||||
without it.
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
If you want to update the interface, modify the .ui files using QT
|
||||
tools. And:
|
||||
|
||||
``` {.bash}
|
||||
cd scripts
|
||||
python build_pyqt.py
|
||||
```
|
||||
|
||||
### Debug
|
||||
|
||||
If you want to see the full logs in the internal shell you can type:
|
||||
|
||||
``` {.bash}
|
||||
debug 2
|
||||
```
|
||||
|
||||
Or start the app with --debug flag.
|
||||
|
||||
Due to the fact PyQT intercept you can use a web debugger for inspecting
|
||||
stuff: <https://github.com/Kozea/wdb>
|
||||
|
||||
Security issues
|
||||
---------------
|
||||
|
||||
Please contact us at <security@gns3.net>
|
||||
47
README.rst
47
README.rst
@@ -1,47 +0,0 @@
|
||||
GNS3-gui
|
||||
========
|
||||
|
||||
.. image:: https://travis-ci.org/GNS3/gns3-gui.svg?branch=master
|
||||
:target: https://travis-ci.org/GNS3/gns3-gui
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/gns3-gui.svg
|
||||
:target: https://pypi.python.org/pypi/gns3-gui
|
||||
|
||||
|
||||
GNS3 GUI repository.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Please see https://docs.gns3.com/
|
||||
|
||||
Development
|
||||
-------------
|
||||
|
||||
If you want to update the interface, modify the .ui files using QT tools. And:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd scripts
|
||||
python build_pyqt.py
|
||||
|
||||
Debug
|
||||
"""""
|
||||
|
||||
If you want to see the full logs in the internal shell you can type:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
debug 2
|
||||
|
||||
|
||||
Or start the app with --debug flag.
|
||||
|
||||
Due to the fact PyQT intercept you can use a web debugger for inspecting stuff:
|
||||
https://github.com/Kozea/wdb
|
||||
|
||||
Security issues
|
||||
----------------
|
||||
Please contact us using contact informations available here:
|
||||
http://docs.gns3.com/1ON9JBXSeR7Nt2-Qum2o3ZX0GU86BZwlmNSUgvmqNWGY/index.html
|
||||
|
||||
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please use GitHub's report a vulnerability feature. More information can be found in https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability
|
||||
@@ -1,15 +1,16 @@
|
||||
version: '{build}-{branch}'
|
||||
|
||||
image: Visual Studio 2017
|
||||
image: Visual Studio 2022
|
||||
|
||||
platform: x64
|
||||
|
||||
environment:
|
||||
PYTHON: "C:\\Python36-x64"
|
||||
PYTHON: "C:\\Python38-x64"
|
||||
DISTUTILS_USE_SDK: "1"
|
||||
|
||||
install:
|
||||
- cinst nmap
|
||||
- "%PYTHON%\\python.exe -m pip install -U pip setuptools" # upgrade pip & setuptools first
|
||||
- "%PYTHON%\\python.exe -m pip install -r dev-requirements.txt"
|
||||
- "%PYTHON%\\python.exe -m pip install -r win-requirements.txt"
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
-rrequirements.txt
|
||||
|
||||
pep8==1.7.0
|
||||
pytest==4.3.1
|
||||
pytest-pythonpath==0.7.3 # useful for running tests outside tox
|
||||
pytest-timeout==1.3.3
|
||||
pytest==7.2.0
|
||||
flake8==5.0.4
|
||||
pytest-timeout==2.1.0
|
||||
|
||||
@@ -46,6 +46,9 @@ class Application(QtWidgets.QApplication):
|
||||
|
||||
super().__init__(argv)
|
||||
|
||||
# this is tell Wayland what is the name of the desktop file (gns3.desktop)
|
||||
self.setDesktopFileName("gns3")
|
||||
|
||||
# this info is necessary for QSettings
|
||||
self.setOrganizationName("GNS3")
|
||||
self.setOrganizationDomain("gns3.net")
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
Handles commands typed in the GNS3 console.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import cmd
|
||||
import struct
|
||||
@@ -34,6 +35,14 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
def do_env(self, args):
|
||||
"""
|
||||
Show the environment variables used by GNS3.
|
||||
"""
|
||||
|
||||
for key, val in os.environ.items():
|
||||
print("{}={}".format(key, val))
|
||||
|
||||
def do_version(self, args):
|
||||
"""
|
||||
Show the version of GNS3 and its dependencies.
|
||||
@@ -223,17 +232,10 @@ class ConsoleCmd(cmd.Cmd):
|
||||
level = int(args[0])
|
||||
if level == 0:
|
||||
print("Deactivating debugging")
|
||||
for handler in root.handlers:
|
||||
if isinstance(handler, logging.StreamHandler):
|
||||
root.removeHandler(handler)
|
||||
root.setLevel(logging.INFO)
|
||||
else:
|
||||
root.addHandler(logging.StreamHandler(sys.stdout))
|
||||
if level == 1:
|
||||
print("Activating debugging")
|
||||
else:
|
||||
print("Activating full debugging")
|
||||
root.setLevel(logging.DEBUG)
|
||||
print("Activating debugging")
|
||||
root.setLevel(logging.DEBUG)
|
||||
from .main_window import MainWindow
|
||||
MainWindow.instance().setSettings({"debug_level": level})
|
||||
else:
|
||||
|
||||
@@ -22,7 +22,7 @@ import inspect
|
||||
import datetime
|
||||
import platform
|
||||
|
||||
from .qt import QtCore
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .topology import Topology
|
||||
from .version import __version__
|
||||
from .console_cmd import ConsoleCmd
|
||||
@@ -109,6 +109,29 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
self.stdout = sys.stdout
|
||||
self._topology = Topology.instance()
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
"""
|
||||
Handles all context menu events.
|
||||
|
||||
:param event: QContextMenuEvent instance
|
||||
"""
|
||||
|
||||
menu = self.createStandardContextMenu()
|
||||
delete_all_action = QtWidgets.QAction("Delete All", menu)
|
||||
delete_all_action.triggered.connect(self._deleteAllActionSlot)
|
||||
menu.addAction(delete_all_action)
|
||||
menu.exec_(event.globalPos());
|
||||
|
||||
def _deleteAllActionSlot(self):
|
||||
"""
|
||||
Delete all action slot
|
||||
"""
|
||||
|
||||
self.clear()
|
||||
self.write(self.prompt)
|
||||
self.lines = []
|
||||
self._clearLine()
|
||||
|
||||
def _writeMessageSlot(self, message, level):
|
||||
"""
|
||||
Write a message in the console.
|
||||
|
||||
@@ -46,6 +46,7 @@ class Controller(QtCore.QObject):
|
||||
super().__init__()
|
||||
self._connected = False
|
||||
self._connecting = False
|
||||
self._notification_stream = None
|
||||
self._version = None
|
||||
self._cache_directory = tempfile.TemporaryDirectory(suffix="-gns3")
|
||||
self._http_client = None
|
||||
@@ -53,8 +54,7 @@ class Controller(QtCore.QObject):
|
||||
self._error_dialog = None
|
||||
self._display_error = True
|
||||
self._projects = []
|
||||
self._controller_websocket = QtWebSockets.QWebSocket()
|
||||
self._project_websocket = QtWebSockets.QWebSocket()
|
||||
self._websocket = QtWebSockets.QWebSocket()
|
||||
|
||||
# If we do multiple call in order to download the same symbol we queue them
|
||||
self._static_asset_download_queue = {}
|
||||
@@ -130,7 +130,8 @@ class Controller(QtCore.QObject):
|
||||
|
||||
self._connected = False
|
||||
self._connecting = True
|
||||
self.get('/version', self._versionGetSlot)
|
||||
status, json_data = self.httpClient().getSynchronous('GET', '/version', timeout=60)
|
||||
self._versionGetSlot(json_data, status is None or status >= 300)
|
||||
|
||||
def _httpClientDisconnectedSlot(self):
|
||||
if self._connected:
|
||||
@@ -148,11 +149,14 @@ class Controller(QtCore.QObject):
|
||||
if self._first_error:
|
||||
self._connecting = False
|
||||
self.connection_failed_signal.emit()
|
||||
if "message" in result and self._display_error:
|
||||
if self._display_error:
|
||||
self._error_dialog = QtWidgets.QMessageBox(self.parent())
|
||||
self._error_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
self._error_dialog.setWindowTitle("Connection to server")
|
||||
self._error_dialog.setText("Error when connecting to the GNS3 server:\n{}".format(result["message"]))
|
||||
if result and "message" in result:
|
||||
self._error_dialog.setText("Error when connecting to the GNS3 server:\n{}".format(result["message"]))
|
||||
else:
|
||||
self._error_dialog.setText("Cannot connect to the GNS3 server")
|
||||
self._error_dialog.setIcon(QtWidgets.QMessageBox.Critical)
|
||||
self._error_dialog.show()
|
||||
# Try to connect again in 5 seconds
|
||||
@@ -164,6 +168,7 @@ class Controller(QtCore.QObject):
|
||||
self._error_dialog.reject()
|
||||
self._error_dialog = None
|
||||
self._version = result.get("version")
|
||||
self._http_client.connection_connected_signal.emit()
|
||||
|
||||
def _httpClientConnectedSlot(self):
|
||||
|
||||
@@ -246,12 +251,6 @@ class Controller(QtCore.QObject):
|
||||
if self._http_client:
|
||||
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
|
||||
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
return self._http_client.getSynchronous(endpoint, timeout)
|
||||
|
||||
def connectProjectWebSocket(self, path, *args):
|
||||
return self._http_client.connectWebSocket(self._project_websocket, path)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
@@ -426,9 +425,10 @@ class Controller(QtCore.QObject):
|
||||
ignoreErrors=True)
|
||||
|
||||
else:
|
||||
self._notification_stream = self._http_client.connectWebSocket(self._controller_websocket, "/notifications/ws")
|
||||
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)
|
||||
self._notification_stream.sslErrors.connect(self._sslErrorsSlot)
|
||||
|
||||
def stopListenNotifications(self):
|
||||
if self._notification_stream:
|
||||
@@ -449,10 +449,15 @@ class Controller(QtCore.QObject):
|
||||
@qslot
|
||||
def _websocket_error(self, error):
|
||||
if self._notification_stream:
|
||||
log.error(self._notification_stream.errorString())
|
||||
log.error("Websocket controller notification stream error: {}".format(self._notification_stream.errorString()))
|
||||
self._notification_stream = None
|
||||
self._startListenNotifications()
|
||||
|
||||
@qslot
|
||||
def _sslErrorsSlot(self, ssl_errors):
|
||||
|
||||
self._http_client.handleSslError(self._notification_stream, ssl_errors)
|
||||
|
||||
@qslot
|
||||
def _websocket_event_received(self, event):
|
||||
try:
|
||||
@@ -474,11 +479,21 @@ class Controller(QtCore.QObject):
|
||||
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"] == "project.closed":
|
||||
from .topology import Topology
|
||||
project = Topology.instance().project()
|
||||
if project and project.id() == result["event"]["project_id"]:
|
||||
Topology.instance().setProject(None)
|
||||
elif result["action"] == "project.updated":
|
||||
from .topology import Topology
|
||||
project = Topology.instance().project()
|
||||
if project and project.id() == result["event"]["project_id"]:
|
||||
project.projectUpdatedCallback(result["event"])
|
||||
elif result["action"] == "log.error" and result["event"].get("message"):
|
||||
log.error(result["event"].get("message"))
|
||||
elif result["action"] == "log.warning" and result["event"].get("message"):
|
||||
log.warning(result["event"].get("message"))
|
||||
elif result["action"] == "log.info" and result["event"].get("message"):
|
||||
log.info(result["event"].get("message"), extra={"show": True})
|
||||
elif result["action"] == "ping":
|
||||
pass
|
||||
|
||||
@@ -15,22 +15,20 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.logging import LoggingIntegration
|
||||
SENTRY_SDK_AVAILABLE = True
|
||||
except ImportError:
|
||||
# Sentry SDK is not installed with deb package in order to simplify packaging
|
||||
SENTRY_SDK_AVAILABLE = False
|
||||
|
||||
import sys
|
||||
import psutil
|
||||
import os
|
||||
import platform
|
||||
import struct
|
||||
import distro
|
||||
|
||||
try:
|
||||
import raven
|
||||
from raven.transport.http import HTTPTransport
|
||||
RAVEN_AVAILABLE = True
|
||||
except ImportError:
|
||||
# raven is not installed with deb package in order to simplify packaging
|
||||
RAVEN_AVAILABLE = False
|
||||
|
||||
from .utils.get_resource import get_resource
|
||||
from .version import __version__, __version_info__
|
||||
|
||||
import logging
|
||||
@@ -52,66 +50,61 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "https://cc555ea871c3443b8ef31f6bf2bb0d7a:5b6b0f2388bd4838a5c9187aae03fcad@sentry.io/38506"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = get_resource("cacert.pem")
|
||||
if cacert is not None and os.path.isfile(cacert):
|
||||
DSN += "?ca_certs={}".format(cacert)
|
||||
else:
|
||||
log.warning("The SSL certificate bundle file '{}' could not be found".format(cacert))
|
||||
DSN = "https://235ecbc961abe34327a4a397d8ce427a@o19455.ingest.us.sentry.io/38506"
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
# We don't want sentry making noise if an error is catched when you don't have internet
|
||||
# We don't want sentry making noise if an error is caught when we don't have internet
|
||||
sentry_errors = logging.getLogger('sentry.errors')
|
||||
sentry_errors.disabled = True
|
||||
|
||||
sentry_uncaught = logging.getLogger('sentry.errors.uncaught')
|
||||
sentry_uncaught.disabled = True
|
||||
self._sentry_initialized = False
|
||||
|
||||
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:
|
||||
if SENTRY_SDK_AVAILABLE:
|
||||
# Don't send log records as events.
|
||||
sentry_logging = LoggingIntegration(level=logging.INFO, event_level=None)
|
||||
try:
|
||||
sentry_sdk.init(dsn=CrashReport.DSN,
|
||||
release=__version__,
|
||||
default_integrations=False,
|
||||
integrations=[sentry_logging])
|
||||
except Exception as e:
|
||||
log.error("Crash report could not be sent: {}".format(e))
|
||||
return
|
||||
|
||||
if os.path.exists(LocalConfig.instance().runAsRootPath()):
|
||||
log.warning("User has run application as root. Crash reports are disabled.")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
if os.path.exists(".git"):
|
||||
log.warning("A .git directory exist crash report is turn off for developers. Instant exit")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
if hasattr(exception, "fingerprint"):
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, fingerprint=['{{ default }}', exception.fingerprint], transport=HTTPTransport)
|
||||
else:
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, transport=HTTPTransport)
|
||||
context = {
|
||||
tags = {
|
||||
"os:name": platform.system(),
|
||||
"os:release": platform.release(),
|
||||
"os:win_32": " ".join(platform.win32_ver()),
|
||||
"os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]),
|
||||
"os:linux": " ".join(distro.linux_distribution()),
|
||||
"os:linux": distro.name(pretty=True),
|
||||
|
||||
}
|
||||
|
||||
self._add_qt_information(tags)
|
||||
|
||||
with sentry_sdk.configure_scope() as scope:
|
||||
for key, value in tags.items():
|
||||
scope.set_tag(key, value)
|
||||
|
||||
extra_context = {
|
||||
"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()}
|
||||
from .controller import Controller
|
||||
from .compute_manager import ComputeManager
|
||||
extra_context["controller:version"] = Controller.instance().version()
|
||||
extra_context["controller:host"] = Controller.instance().host()
|
||||
extra_context["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(),
|
||||
@@ -120,27 +113,48 @@ class CrashReport:
|
||||
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.debug("Crash report sent with event ID: {}".format(client.get_ident(report)))
|
||||
with sentry_sdk.configure_scope() as scope:
|
||||
for key, value in extra_context.items():
|
||||
scope.set_extra(key, value)
|
||||
|
||||
def captureException(self, exception, value, tb):
|
||||
|
||||
from .local_server import LocalServer
|
||||
from .local_config import LocalConfig
|
||||
|
||||
local_server = LocalServer.instance().localServerSettings()
|
||||
if local_server["report_errors"]:
|
||||
|
||||
if not SENTRY_SDK_AVAILABLE:
|
||||
log.warning("Cannot capture exception: Sentry SDK is not available")
|
||||
return
|
||||
|
||||
if os.path.exists(LocalConfig.instance().runAsRootPath()):
|
||||
log.warning("User is running application as root. Crash reports disabled.")
|
||||
return
|
||||
|
||||
if not hasattr(sys, "frozen") and os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")):
|
||||
log.warning(".git directory detected, crash reporting is turned off for developers.")
|
||||
return
|
||||
|
||||
try:
|
||||
error = (exception, value, tb)
|
||||
sentry_sdk.capture_exception(error=error)
|
||||
log.info("Crash report sent with event ID: {}".format(sentry_sdk.last_event_id()))
|
||||
except Exception as e:
|
||||
log.warning("Can't send crash report to Sentry: {}".format(e))
|
||||
|
||||
def _add_qt_information(self, tags):
|
||||
|
||||
def _add_qt_information(self, context):
|
||||
try:
|
||||
from .qt import QtCore
|
||||
from .qt import sip
|
||||
except ImportError:
|
||||
return context
|
||||
context["psutil:version"] = psutil.__version__
|
||||
context["pyqt:version"] = QtCore.PYQT_VERSION_STR
|
||||
context["qt:version"] = QtCore.QT_VERSION_STR
|
||||
context["sip:version"] = sip.SIP_VERSION_STR
|
||||
return context
|
||||
return tags
|
||||
tags["pyqt:version"] = QtCore.PYQT_VERSION_STR
|
||||
tags["qt:version"] = QtCore.QT_VERSION_STR
|
||||
tags["sip:version"] = sip.SIP_VERSION_STR
|
||||
return tags
|
||||
|
||||
@classmethod
|
||||
def instance(cls):
|
||||
|
||||
@@ -36,6 +36,7 @@ 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__)
|
||||
@@ -71,11 +72,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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):
|
||||
@@ -90,9 +94,11 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.setWindowTitle("Install {} appliance".format(self._appliance["name"]))
|
||||
|
||||
# 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)
|
||||
if self._appliance["registry_version"] < 8:
|
||||
# FIXME: show appliance info for v8
|
||||
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)
|
||||
@@ -140,18 +146,9 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
if self.page(page_id) == self.uiServerWizardPage:
|
||||
|
||||
Controller.instance().getSymbols(self._getSymbolsCallback)
|
||||
|
||||
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")
|
||||
|
||||
template_type = self._appliance.template_type()
|
||||
if not template_type:
|
||||
raise ApplianceError("No template type found for appliance {}".format(self._appliance["name"]))
|
||||
is_mac = ComputeManager.instance().localPlatform().startswith("darwin")
|
||||
is_win = ComputeManager.instance().localPlatform().startswith("win")
|
||||
|
||||
@@ -169,11 +166,11 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
if ComputeManager.instance().localPlatform() is None:
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
elif is_mac or is_win:
|
||||
if emulator_type == "qemu":
|
||||
if template_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 emulator_type != "dynamips":
|
||||
elif template_type != "dynamips":
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
|
||||
if ComputeManager.instance().vmCompute():
|
||||
@@ -190,25 +187,56 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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.template_type(), self._compute_id)
|
||||
else:
|
||||
self.images_changed_signal.emit()
|
||||
|
||||
elif self.page(page_id) == self.uiQemuWizardPage:
|
||||
if self._appliance['qemu'].get('kvm', 'require') == 'require':
|
||||
if self._appliance.template_properties().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"]])
|
||||
if self._appliance["registry_version"] >= 8:
|
||||
qemu_platform = self._appliance.template_properties()["platform"]
|
||||
else:
|
||||
qemu_platform = self._appliance.template_properties()["arch"]
|
||||
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [qemu_platform])
|
||||
|
||||
elif self.page(page_id) == self.uiInstructionsPage:
|
||||
|
||||
installation_instructions = self._appliance.get("installation_instructions", "No installation instructions available")
|
||||
self.uiInstructionsTextEdit.setText(installation_instructions.strip())
|
||||
|
||||
elif self.page(page_id) == self.uiUsageWizardPage:
|
||||
self.uiUsageTextEdit.setText("The template will be available in the {} category.\n\n{}".format(self._appliance["category"].replace("_", " "), self._appliance.get("usage", "")))
|
||||
# TODO: allow taking these info fields at the version level in v8
|
||||
category = self._appliance["category"].replace("_", " ")
|
||||
usage = self._appliance.get("usage", "No usage information available")
|
||||
if self._appliance["registry_version"] >= 8:
|
||||
default_username = self._appliance.get("default_username")
|
||||
default_password = self._appliance.get("default_password")
|
||||
if default_username and default_password:
|
||||
usage += "\n\nDefault username: {}\nDefault password: {}".format(default_username, default_password)
|
||||
|
||||
usage_info = """
|
||||
The template will be available in the {} category.
|
||||
|
||||
Usage: {}
|
||||
""".format(category, usage)
|
||||
|
||||
self.uiUsageTextEdit.setText(usage_info.strip())
|
||||
|
||||
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
|
||||
"""
|
||||
Check if the server supports KVM or not
|
||||
"""
|
||||
|
||||
if error is None and "kvm" in result and self._appliance["qemu"]["arch"] in result["kvm"]:
|
||||
if self._appliance["registry_version"] >= 8:
|
||||
qemu_platform = self._appliance.template_properties()["platform"]
|
||||
else:
|
||||
qemu_platform = self._appliance.template_properties()["arch"]
|
||||
if error is None and "kvm" in result and qemu_platform in result["kvm"]:
|
||||
self._server_check = True
|
||||
else:
|
||||
if error:
|
||||
@@ -221,8 +249,15 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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.template_type(), self._compute_id)
|
||||
|
||||
def _showApplianceInfoSlot(self):
|
||||
"""
|
||||
@@ -339,6 +374,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("red")))
|
||||
else:
|
||||
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)
|
||||
@@ -392,15 +428,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(),
|
||||
img = self._registry.search_image_file(self._appliance.template_type(),
|
||||
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
|
||||
@@ -455,8 +496,24 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
Allow user to create a new version of an appliance
|
||||
"""
|
||||
|
||||
new_version, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Creating a new version allows to import unknown files to use with this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.Normal)
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
if current is None:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Base version", "Please select a base version")
|
||||
return
|
||||
base_version = current.data(0, QtCore.Qt.UserRole)
|
||||
|
||||
new_version_name, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Create a new version for this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.Normal, base_version.get("name"))
|
||||
if ok:
|
||||
new_version = {"name": new_version_name}
|
||||
new_version["images"] = {}
|
||||
|
||||
for disk_type in base_version["images"]:
|
||||
base_filename = base_version["images"][disk_type]["filename"]
|
||||
filename, ok = QtWidgets.QInputDialog.getText(self, "Image", "Disk image filename for {}".format(disk_type), QtWidgets.QLineEdit.Normal, base_filename)
|
||||
if not ok:
|
||||
filename = base_filename
|
||||
new_version["images"][disk_type] = {"filename": filename, "version": new_version_name}
|
||||
|
||||
try:
|
||||
self._appliance.create_new_version(new_version)
|
||||
except ApplianceError as e:
|
||||
@@ -483,7 +540,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
if len(path) == 0:
|
||||
return
|
||||
|
||||
image = Image(self._appliance.emulator(), path, filename=disk["filename"])
|
||||
image = Image(self._appliance.template_type(), path, filename=disk["filename"])
|
||||
try:
|
||||
if "md5sum" in disk and image.md5sum != disk["md5sum"]:
|
||||
reply = QtWidgets.QMessageBox.question(self, "Add appliance",
|
||||
@@ -495,9 +552,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
|
||||
return
|
||||
|
||||
image_upload_manger = ImageUploadManager(
|
||||
image, Controller.instance(), self._compute_id,
|
||||
self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
|
||||
image_upload_manger = ImageUploadManager(image, Controller.instance(), self._compute_id, self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
|
||||
image_upload_manger.upload()
|
||||
|
||||
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
|
||||
@@ -520,7 +575,11 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
if self.uiQemuListComboBox.count() == 1:
|
||||
self.next()
|
||||
else:
|
||||
i = self.uiQemuListComboBox.findText(self._appliance["qemu"]["arch"], QtCore.Qt.MatchContains)
|
||||
if self._appliance["registry_version"] >= 8:
|
||||
qemu_platform = self._appliance.template_properties()["platform"]
|
||||
else:
|
||||
qemu_platform = self._appliance.template_properties()["arch"]
|
||||
i = self.uiQemuListComboBox.findData(qemu_platform, flags=QtCore.Qt.MatchEndsWith)
|
||||
if i != -1:
|
||||
self.uiQemuListComboBox.setCurrentIndex(i)
|
||||
|
||||
@@ -533,8 +592,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
if version is None:
|
||||
appliance_configuration = self._appliance.copy()
|
||||
if not "docker" in appliance_configuration:
|
||||
# only Docker do not have version
|
||||
if self._appliance.template_type() != "docker":
|
||||
# only Docker do not have versions
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
@@ -551,10 +610,15 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
return False
|
||||
appliance_configuration["name"] = appliance_configuration["name"].strip()
|
||||
|
||||
if "qemu" in appliance_configuration:
|
||||
if self._appliance["registry_version"] >= 8:
|
||||
if "settings" in appliance_configuration:
|
||||
for settings in appliance_configuration["settings"]:
|
||||
if settings["template_type"] == "qemu":
|
||||
settings["template_properties"]["path"] = self.uiQemuListComboBox.currentData()
|
||||
elif "qemu" in appliance_configuration:
|
||||
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
|
||||
|
||||
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, self._symbols)
|
||||
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, version, self._symbols, parent=self)
|
||||
TemplateManager.instance().createTemplate(Template(new_template), callback=self._templateCreatedCallback)
|
||||
return False
|
||||
|
||||
@@ -583,33 +647,48 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self._template_created = True
|
||||
self.done(True)
|
||||
|
||||
def _uploadImages(self, version):
|
||||
def _uploadImages(self, name, version):
|
||||
"""
|
||||
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":
|
||||
image = Image(self._appliance.emulator(), image["path"], filename=image["filename"])
|
||||
|
||||
image_upload_manger = ImageUploadManager(
|
||||
image, Controller.instance(), self._compute_id,
|
||||
self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
|
||||
image_upload_manger.upload()
|
||||
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.template_type(), image["path"], filename=image["filename"])
|
||||
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:
|
||||
if self._appliance.template_type() == "docker":
|
||||
# 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() + 3
|
||||
elif self._appliance.template_type() != "qemu":
|
||||
# skip the Qemu binary selection page if not a Qemu appliance
|
||||
return super().nextId() + 1
|
||||
if self.currentPage() == self.uiQemuWizardPage:
|
||||
if not self._appliance.get("installation_instructions"):
|
||||
# skip the installation instructions page if there are no instructions
|
||||
return super().nextId() + 1
|
||||
return super().nextId()
|
||||
|
||||
def validateCurrentPage(self):
|
||||
@@ -638,7 +717,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
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:
|
||||
# validate the usage page
|
||||
@@ -676,7 +756,6 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
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
|
||||
|
||||
@@ -49,7 +49,6 @@ class ConfigurationDialog(QtWidgets.QDialog, Ui_configurationDialog):
|
||||
self._settings = settings
|
||||
self._configuration_page = configuration_page
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return self._settings
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
if console_type == "spice+agent":
|
||||
# special case for spice+agent, use the spice console type
|
||||
console_type = "spice"
|
||||
self._console_type = console_type
|
||||
self._current = current
|
||||
|
||||
@@ -63,7 +66,7 @@ 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"):
|
||||
elif self._console_type == "spice":
|
||||
self._consoles = copy.copy(PRECONFIGURED_SPICE_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
|
||||
@@ -121,8 +124,8 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
|
||||
dialog = ConsoleCommandDialog(parent, console_type=console_type, current=current)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
return (True, dialog.uiCommandPlainTextEdit.toPlainText().replace("\n", " "))
|
||||
return (False, None)
|
||||
return True, dialog.uiCommandPlainTextEdit.toPlainText().replace("\n", " ")
|
||||
return False, None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -169,6 +169,9 @@ class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConf
|
||||
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 not port_name:
|
||||
QtWidgets.QMessageBox.critical(self, "Port name", "Port name cannot be empty for adapter {}".format(adapter_number))
|
||||
return False
|
||||
if original_port_name != port_name:
|
||||
custom_adapter_settings["port_name"] = port_name
|
||||
if self._default_adapter_type and self._adapter_types:
|
||||
@@ -180,13 +183,14 @@ class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConf
|
||||
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
|
||||
return False
|
||||
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())
|
||||
return True
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
@@ -196,5 +200,6 @@ class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConf
|
||||
"""
|
||||
|
||||
if result:
|
||||
self._updateCustomAdapters()
|
||||
if not self._updateCustomAdapters():
|
||||
return
|
||||
super().done(result)
|
||||
|
||||
@@ -41,6 +41,9 @@ 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())
|
||||
@@ -78,7 +81,7 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
|
||||
host = self.uiServerHostLineEdit.text().strip()
|
||||
name = self.uiServerNameLineEdit.text().strip()
|
||||
protocol = "http"
|
||||
protocol = self.uiServerProtocolComboBox.currentText().lower()
|
||||
port = self.uiServerPortSpinBox.value()
|
||||
user = self.uiServerUserLineEdit.text().strip()
|
||||
password = self.uiServerPasswordLineEdit.text().strip()
|
||||
|
||||
@@ -46,19 +46,11 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
self.uiNewVarButton.clicked.connect(self.onAddNewVariable)
|
||||
self.uiGlobalVariablesGrid.addWidget(self.uiNewVarButton, 0, 3, QtCore.Qt.AlignRight)
|
||||
|
||||
self._variables = self.setUpVariables()
|
||||
self._variables = self._project.variables()
|
||||
if not self._variables:
|
||||
self._variables = [{"name": "", "value": ""}]
|
||||
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)
|
||||
@@ -98,7 +90,7 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
variable["value"] = text
|
||||
|
||||
def _cleanVariables(self):
|
||||
return [v for v in self._variables if v.get("name", "").strip() != ""]
|
||||
return [v for v in self._variables if v.get("name").strip() != ""]
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
@@ -108,14 +100,19 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
"""
|
||||
|
||||
if result:
|
||||
self._project.setName(self.uiProjectNameLineEdit.text())
|
||||
self._project.setAutoOpen(self.uiProjectAutoOpenCheckBox.isChecked())
|
||||
self._project.setAutoClose(not self.uiProjectAutoCloseCheckBox.isChecked())
|
||||
self._project.setAutoStart(self.uiProjectAutoStartCheckBox.isChecked())
|
||||
self._project.setSceneHeight(self.uiSceneHeightSpinBox.value())
|
||||
self._project.setSceneWidth(self.uiSceneWidthSpinBox.value())
|
||||
self._project.setNodeGridSize(self.uiNodeGridSizeSpinBox.value())
|
||||
self._project.setDrawingGridSize(self.uiDrawingGridSizeSpinBox.value())
|
||||
self._project.setVariables(self._cleanVariables())
|
||||
self._project.update()
|
||||
node_grid_size = self.uiNodeGridSizeSpinBox.value()
|
||||
drawing_grid_size = self.uiDrawingGridSizeSpinBox.value()
|
||||
if node_grid_size % drawing_grid_size != 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Grid sizes", "Invalid grid sizes which will create overlapping lines")
|
||||
else:
|
||||
self._project.setNodeGridSize(node_grid_size)
|
||||
self._project.setDrawingGridSize(drawing_grid_size)
|
||||
self._project.setName(self.uiProjectNameLineEdit.text())
|
||||
self._project.setAutoOpen(self.uiProjectAutoOpenCheckBox.isChecked())
|
||||
self._project.setAutoClose(not self.uiProjectAutoCloseCheckBox.isChecked())
|
||||
self._project.setAutoStart(self.uiProjectAutoStartCheckBox.isChecked())
|
||||
self._project.setSceneHeight(self.uiSceneHeightSpinBox.value())
|
||||
self._project.setSceneWidth(self.uiSceneWidthSpinBox.value())
|
||||
self._project.setVariables(self._cleanVariables())
|
||||
self._project.update()
|
||||
super().done(result)
|
||||
|
||||
@@ -152,18 +152,18 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
|
||||
"""
|
||||
|
||||
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_guests.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
parent_switches = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
|
||||
parent_switches.setText(0, "Switches")
|
||||
parent_switches.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
parent_routers = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
|
||||
parent_routers.setText(0, "Routers")
|
||||
parent_routers.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
parent_firewalls.setFlags(parent_firewalls.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
self.uiAppliancesTreeWidget.expandAll()
|
||||
|
||||
for appliance in ApplianceManager.instance().appliances():
|
||||
@@ -268,7 +268,7 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
|
||||
|
||||
super().done(result)
|
||||
if result:
|
||||
ApplianceManager.instance().appliances_changed_signal.disconnect(self._appliancesChangedSlot)
|
||||
#ApplianceManager.instance().appliances_changed_signal.disconnect(self._appliancesChangedSlot)
|
||||
from gns3.main_window import MainWindow
|
||||
if self.currentPage() == self.uiApplianceFromServerWizardPage:
|
||||
items = self.uiAppliancesTreeWidget.selectedItems()
|
||||
|
||||
@@ -128,9 +128,10 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
|
||||
# Class name, changed signal
|
||||
widget_to_watch = {
|
||||
#QtWidgets.QLineEdit: "textChanged",
|
||||
QtWidgets.QLineEdit: "textChanged",
|
||||
QtWidgets.QPlainTextEdit: "textChanged",
|
||||
# QtWidgets.QTreeWidget: "itemChanged",
|
||||
QtWidgets.QTreeWidget: "itemDoubleClicked",
|
||||
QtWidgets.QComboBox: "currentIndexChanged",
|
||||
QtWidgets.QSpinBox: "valueChanged",
|
||||
QtWidgets.QAbstractButton: "pressed"
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -53,7 +53,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
|
||||
if show_open_options:
|
||||
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
|
||||
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
|
||||
self._addRecentFilesMenu()
|
||||
else:
|
||||
self.uiOpenProjectGroupBox.hide()
|
||||
self.uiProjectTabWidget.removeTab(1)
|
||||
@@ -135,17 +135,20 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
new_project_name)
|
||||
name = name.strip()
|
||||
if reply and len(name) > 0:
|
||||
|
||||
reset_mac_addresses = self.uiResetMacAddressesCheckBox.isChecked()
|
||||
|
||||
if Controller.instance().isRemote():
|
||||
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
|
||||
self._duplicateCallback,
|
||||
body={"name": name},
|
||||
body={"name": name, "reset_mac_addresses": reset_mac_addresses},
|
||||
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},
|
||||
body={"name": name, "path": project_location, "reset_mac_addresses": reset_mac_addresses},
|
||||
progressText="Duplicating project '{}'...".format(name),
|
||||
timeout=None)
|
||||
|
||||
@@ -228,20 +231,20 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
self._main_window.openProjectActionSlot()
|
||||
self.reject()
|
||||
|
||||
def _showRecentProjectsSlot(self):
|
||||
def _addRecentFilesMenu(self):
|
||||
"""
|
||||
lot to show all the recent projects in a menu.
|
||||
Add recent projects in a menu.
|
||||
"""
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.triggered.connect(self._menuTriggeredSlot)
|
||||
menu = QtWidgets.QMenu(parent=self)
|
||||
if Controller.instance().isRemote():
|
||||
for action in self._main_window.recent_project_actions:
|
||||
menu.addAction(action)
|
||||
else:
|
||||
for action in self._main_window.recent_file_actions:
|
||||
menu.addAction(action)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
menu.triggered.connect(self._menuTriggeredSlot)
|
||||
self.uiRecentProjectsPushButton.setMenu(menu)
|
||||
|
||||
def _overwriteProjectCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import datetime
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
@@ -54,9 +55,19 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
|
||||
self.uiCompressionComboBox.setCurrentIndex(1)
|
||||
self.helpRequested.connect(self._showHelpSlot)
|
||||
self.uiPathBrowserToolButton.clicked.connect(self._pathBrowserSlot)
|
||||
self._loadReadme()
|
||||
|
||||
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 _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.uiReadmeTextEdit.setPlainText(raw_body.decode("utf-8", errors="replace"))
|
||||
else:
|
||||
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):
|
||||
|
||||
@@ -119,8 +130,16 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
|
||||
include_images = "yes"
|
||||
else:
|
||||
include_images = "no"
|
||||
if self.uiIncludeSnapshotsCheckBox.isChecked():
|
||||
include_snapshots = "yes"
|
||||
else:
|
||||
include_snapshots = "no"
|
||||
if self.uiResetMacAddressesCheckBox.isChecked():
|
||||
reset_mac_addresses = "yes"
|
||||
else:
|
||||
reset_mac_addresses = "no"
|
||||
compression = self.uiCompressionComboBox.currentData()
|
||||
export_worker = ExportProjectWorker(self._project, self._path, include_images, compression)
|
||||
export_worker = ExportProjectWorker(self._project, self._path, include_images, include_snapshots, reset_mac_addresses, compression)
|
||||
progress_dialog = ProgressDialog(export_worker, "Exporting project", "Exporting portable project files...", "Cancel", parent=self, create_thread=False)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
@@ -37,9 +37,7 @@ class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
|
||||
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()
|
||||
|
||||
@@ -50,10 +48,11 @@ class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
|
||||
return variables
|
||||
|
||||
def _addMisingVariablesEdits(self):
|
||||
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
|
||||
#TODO: refactor this to use a QListWidget
|
||||
missing = [v for v in self._variables if v.get("name") and v.get("value", "").strip() == ""]
|
||||
for i, variable in enumerate(missing, start=0):
|
||||
nameLabel = QtWidgets.QLabel()
|
||||
nameLabel.setText(variable.get("name", ""))
|
||||
nameLabel.setText(variable.get("name") + ":")
|
||||
self.gridLayout.addWidget(nameLabel, i, 0)
|
||||
|
||||
valueEdit = QtWidgets.QLineEdit()
|
||||
@@ -72,13 +71,13 @@ class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
|
||||
variable["value"] = text
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
|
||||
missing = [v for v in self._variables if v.get("name") and 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)
|
||||
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
|
||||
|
||||
|
||||
@@ -43,15 +43,18 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.adjustSize()
|
||||
|
||||
self._gns3_vm_settings = {
|
||||
"enable": True,
|
||||
"headless": False,
|
||||
"when_exit": "stop",
|
||||
"engine": "vmware",
|
||||
"allocate_vcpus_ram": True,
|
||||
"vcpus": 1,
|
||||
"ram": 2048,
|
||||
"vmname": "GNS3 VM"
|
||||
"vmname": "GNS3 VM",
|
||||
"port": 80
|
||||
}
|
||||
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
@@ -65,7 +68,6 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
|
||||
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
|
||||
self.uiVirtualBoxRadioButton.clicked.connect(self._listVirtualBoxVMsSlot)
|
||||
self.uiVMwareBannerButton.clicked.connect(self._VMwareBannerButtonClickedSlot)
|
||||
settings = parent.settings()
|
||||
self.uiShowCheckBox.setChecked(settings["hide_setup_wizard"])
|
||||
|
||||
@@ -83,14 +85,14 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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
|
||||
# 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.png"))
|
||||
else:
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.png"))
|
||||
self.uiLocalServerHostComboBox.addItem("localhost", "localhost") # local host
|
||||
self.uiLocalServerHostComboBox.addItem("::", "::") # all IPv6 addresses
|
||||
self.uiLocalServerHostComboBox.addItem("0.0.0.0", "0.0.0.0") # all IPv4 addresses
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
@@ -114,13 +116,6 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
self.uiLocalServerPathLineEdit.setText(path)
|
||||
|
||||
def _VMwareBannerButtonClickedSlot(self):
|
||||
if sys.platform.startswith("darwin"):
|
||||
url = "http://send.onenetworkdirect.net/z/621395/CD225091/"
|
||||
else:
|
||||
url = "http://send.onenetworkdirect.net/z/616207/CD225091/"
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
|
||||
|
||||
def _listVMwareVMsSlot(self):
|
||||
"""
|
||||
Slot to refresh the VMware VMs list.
|
||||
@@ -132,7 +127,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
from gns3.modules import VMware
|
||||
settings = VMware.instance().settings()
|
||||
if not os.path.exists(settings["vmrun_path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed. You can download it from https://www.vmware.com/support/developer/vix-api/. After installation you need to restart GNS3.")
|
||||
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed. You can download it from https://customerconnect.vmware.com/downloads/details?downloadGroup=PLAYER-1400-VIX1170&productId=687. After installation you need to restart GNS3.")
|
||||
return
|
||||
self._refreshVMListSlot()
|
||||
|
||||
@@ -141,7 +136,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)
|
||||
@@ -414,11 +408,8 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
settings["hide_setup_wizard"] = True
|
||||
else:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
if local_server_settings["host"] is None:
|
||||
local_server_settings["host"] = DEFAULT_LOCAL_SERVER_HOST
|
||||
LocalServer.instance().updateLocalServerSettings(local_server_settings)
|
||||
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
|
||||
|
||||
LocalServer.instance().updateLocalServerSettings(local_server_settings)
|
||||
settings["hide_setup_wizard"] = not self.uiShowCheckBox.isChecked()
|
||||
self.parentWidget().setSettings(settings)
|
||||
super().done(result)
|
||||
|
||||
|
||||
@@ -22,6 +22,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
|
||||
from ..items.rectangle_item import RectangleItem
|
||||
|
||||
|
||||
class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
@@ -70,8 +71,27 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
self._border_color.green(),
|
||||
self._border_color.blue(),
|
||||
self._border_color.alpha()))
|
||||
self.uiRotationSpinBox.setValue(first_item.rotation())
|
||||
if isinstance(first_item, RectangleItem):
|
||||
# use the horizontal corner radius first and then the vertical one if it's not set
|
||||
# maybe we allow configuring them separately in the future
|
||||
corner_radius = first_item.horizontalCornerRadius()
|
||||
if not corner_radius:
|
||||
corner_radius = first_item.verticalCornerRadius()
|
||||
self.uiCornerRadiusSpinBox.setValue(corner_radius)
|
||||
else:
|
||||
self.uiCornerRadiusLabel.hide()
|
||||
self.uiCornerRadiusSpinBox.hide()
|
||||
self.uiRotationSpinBox.setValue(int(first_item.rotation()))
|
||||
self.uiBorderWidthSpinBox.setValue(pen.width())
|
||||
if isinstance(first_item, ShapeItem):
|
||||
rect = first_item.rect()
|
||||
self.uiWidthSpinBox.setValue(int(rect.width()))
|
||||
self.uiHeightSpinBox.setValue(int(rect.height()))
|
||||
else:
|
||||
self.uiWidthSpinBox.hide()
|
||||
self.uiWidthLabel.hide()
|
||||
self.uiHeightSpinBox.hide()
|
||||
self.uiHeightLabel.hide()
|
||||
index = self.uiBorderStyleComboBox.findData(pen.style())
|
||||
if index != -1:
|
||||
self.uiBorderStyleComboBox.setCurrentIndex(index)
|
||||
@@ -116,10 +136,18 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
for item in self._items:
|
||||
item.setPen(pen)
|
||||
# on multiselection it's possible to select many type of items
|
||||
# on multi-selection 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)
|
||||
if isinstance(item, RectangleItem):
|
||||
corner_radius = self.uiCornerRadiusSpinBox.value()
|
||||
# use the corner radius for both horizontal (rx) and vertical (ry)
|
||||
# maybe we support setting them separately in the future
|
||||
item.setHorizontalCornerRadius(corner_radius)
|
||||
item.setVerticalCornerRadius(corner_radius)
|
||||
if isinstance(item, ShapeItem):
|
||||
item.setWidthAndHeight(self.uiWidthSpinBox.value(), self.uiHeightSpinBox.value())
|
||||
item.setRotation(self.uiRotationSpinBox.value())
|
||||
|
||||
def done(self, result):
|
||||
|
||||
120
gns3/dialogs/style_editor_dialog_link.py
Normal file
120
gns3/dialogs/style_editor_dialog_link.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Pekka Helenius
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Style editor to edit Link items.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
|
||||
|
||||
|
||||
class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
"""
|
||||
Style editor dialog.
|
||||
|
||||
:param parent: parent widget
|
||||
:param link: selected link
|
||||
"""
|
||||
|
||||
def __init__(self, link, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._link = link
|
||||
self._link_style = {}
|
||||
|
||||
self.uiBorderColorPushButton.clicked.connect(self._setBorderColorSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
|
||||
self.uiBorderStyleComboBox.addItem("Solid", QtCore.Qt.SolidLine)
|
||||
self.uiBorderStyleComboBox.addItem("Dash", QtCore.Qt.DashLine)
|
||||
self.uiBorderStyleComboBox.addItem("Dot", QtCore.Qt.DotLine)
|
||||
self.uiBorderStyleComboBox.addItem("Dash Dot", QtCore.Qt.DashDotLine)
|
||||
self.uiBorderStyleComboBox.addItem("Dash Dot Dot", QtCore.Qt.DashDotDotLine)
|
||||
self.uiBorderStyleComboBox.addItem("Invisible", QtCore.Qt.NoPen)
|
||||
|
||||
self.uiColorLabel.hide()
|
||||
self.uiColorPushButton.hide()
|
||||
self._color = None
|
||||
|
||||
self.uiCornerRadiusLabel.hide()
|
||||
self.uiCornerRadiusSpinBox.hide()
|
||||
self.uiRotationLabel.hide()
|
||||
self.uiRotationSpinBox.hide()
|
||||
|
||||
link.setHovered(False) # make sure we use the right style
|
||||
pen = link.pen()
|
||||
link.setHovered(True)
|
||||
|
||||
self._border_color = pen.color()
|
||||
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
|
||||
self._border_color.green(),
|
||||
self._border_color.blue(),
|
||||
self._border_color.alpha()))
|
||||
self.uiBorderWidthSpinBox.setValue(pen.width())
|
||||
index = self.uiBorderStyleComboBox.findData(pen.style())
|
||||
if index != -1:
|
||||
self.uiBorderStyleComboBox.setCurrentIndex(index)
|
||||
|
||||
self.adjustSize()
|
||||
|
||||
def _setBorderColorSlot(self):
|
||||
"""
|
||||
Slot to select the border color.
|
||||
"""
|
||||
|
||||
color = QtWidgets.QColorDialog.getColor(self._border_color, self, "Select Color", QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self._border_color = color
|
||||
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
|
||||
self._border_color.green(),
|
||||
self._border_color.blue(),
|
||||
self._border_color.alpha()))
|
||||
|
||||
def _applyPreferencesSlot(self):
|
||||
"""
|
||||
Applies the new style settings.
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
self._link.setPen(pen)
|
||||
|
||||
new_link_style = {}
|
||||
new_link_style["color"] = self._border_color.name()
|
||||
new_link_style["width"] = self.uiBorderWidthSpinBox.value()
|
||||
new_link_style["type"] = border_style
|
||||
|
||||
# Store values
|
||||
self._link.setLinkStyle(new_link_style)
|
||||
self._link.setHovered(False) # allow to see the new style
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
Called when the dialog is closed.
|
||||
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
|
||||
if result:
|
||||
self._applyPreferencesSlot()
|
||||
super().done(result)
|
||||
@@ -67,6 +67,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
self._symbol_items = []
|
||||
self._parents = {}
|
||||
|
||||
Controller.instance().clearStaticCache() # TODO: use etag to know when to refresh the cache
|
||||
Controller.instance().get("/symbols", self._listSymbolsCallback)
|
||||
|
||||
def _listSymbolsCallback(self, result, error=False, **kwargs):
|
||||
@@ -108,6 +109,9 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
item.setIcon(0, icon)
|
||||
|
||||
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
|
||||
|
||||
for parent in self._parents.values():
|
||||
parent.sortChildren(0, QtCore.Qt.AscendingOrder)
|
||||
self.adjustSize()
|
||||
|
||||
def _searchTextChangedSlot(self, text):
|
||||
@@ -119,13 +123,13 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
"""
|
||||
text = self.uiSearchLineEdit.text()
|
||||
for item in self._symbol_items:
|
||||
if not item.data(0, QtCore.Qt.UserRole).builtin():
|
||||
item.setHidden(True)
|
||||
# if not item.data(0, QtCore.Qt.UserRole).builtin():
|
||||
# item.setHidden(True)
|
||||
# else:
|
||||
if not text.strip() or text.strip().lower() in item.text(0).lower():
|
||||
item.setHidden(False)
|
||||
else:
|
||||
if len(text.strip()) == 0 or text.strip().lower() in item.text(0).lower():
|
||||
item.setHidden(False)
|
||||
else:
|
||||
item.setHidden(True)
|
||||
item.setHidden(True)
|
||||
|
||||
def _customSymbolToggledSlot(self, checked):
|
||||
"""
|
||||
@@ -161,6 +165,8 @@ 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
|
||||
@@ -169,7 +175,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
|
||||
if self.uiSymbolTreeWidget.isEnabled():
|
||||
current = self.uiSymbolTreeWidget.currentItem()
|
||||
if current:
|
||||
if current and current.parent():
|
||||
return current.data(0, QtCore.Qt.UserRole).id()
|
||||
else:
|
||||
return os.path.basename(self.uiSymbolLineEdit.text())
|
||||
|
||||
@@ -44,7 +44,7 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
|
||||
# use the first item in the list as the model
|
||||
first_item = items[0]
|
||||
self._setColor(first_item.defaultTextColor())
|
||||
self.uiRotationSpinBox.setValue(first_item.rotation())
|
||||
self.uiRotationSpinBox.setValue(int(first_item.rotation()))
|
||||
self.uiPlainTextEdit.setPlainText(first_item.toPlainText())
|
||||
self.uiPlainTextEdit.setFont(first_item.font())
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ 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
|
||||
|
||||
@@ -121,8 +121,6 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
def setZoom(self, zoom):
|
||||
"""
|
||||
Sets zoom of the Graphics View
|
||||
:param zoom:
|
||||
:return:
|
||||
"""
|
||||
if zoom:
|
||||
factor = zoom / 100.
|
||||
@@ -193,6 +191,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# clear all objects on the scene
|
||||
self.scene().clear()
|
||||
|
||||
# reset zoom / scale
|
||||
self.resetTransform()
|
||||
|
||||
|
||||
def _loadSettings(self):
|
||||
"""
|
||||
@@ -326,6 +327,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# connect the signals that let the graphics view knows about events such as
|
||||
# a new link creation or deletion.
|
||||
if self._topology.addLink(link):
|
||||
source_node.addLink(link)
|
||||
destination_node.addLink(link)
|
||||
link.add_link_signal.connect(self.addLinkSlot)
|
||||
link.delete_link_signal.connect(self.deleteLinkSlot)
|
||||
|
||||
@@ -392,7 +395,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# link addition code
|
||||
if not self._newlink:
|
||||
source_item = item
|
||||
source_port = source_item.connectToPort()
|
||||
source_port = source_item.connectToPort(event.globalPos())
|
||||
if not source_port:
|
||||
return
|
||||
|
||||
@@ -409,7 +412,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
source_item = self._newlink.sourceItem()
|
||||
source_port = self._newlink.sourcePort()
|
||||
destination_item = item
|
||||
destination_port = destination_item.connectToPort()
|
||||
destination_port = destination_item.connectToPort(event.globalPos())
|
||||
if not destination_port:
|
||||
return
|
||||
|
||||
@@ -435,6 +438,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
item = self.itemAt(event.pos())
|
||||
if item and sip.isdeleted(item):
|
||||
return
|
||||
elif not item:
|
||||
self._main_window.uiStatusBar.clearMessage() # reset the status bar message when clicking on the scene
|
||||
|
||||
if item and (isinstance(item, LinkItem) or isinstance(item.parentItem(), LinkItem)):
|
||||
is_not_link = False
|
||||
@@ -460,28 +465,18 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
item.setSelected(True)
|
||||
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)
|
||||
item.setSelected(True)
|
||||
self._showDeviceContextualMenu(QtGui.QCursor.pos())
|
||||
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
|
||||
elif len(self.scene().selectedItems()) > 1:
|
||||
self._showDeviceContextualMenu(QtGui.QCursor.pos())
|
||||
pass #TODO: remove this without creating a bug...
|
||||
elif is_not_link and self._adding_link and event.button() == QtCore.Qt.RightButton:
|
||||
# send a escape key to the main window to cancel the link addition
|
||||
key = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Escape, QtCore.Qt.NoModifier)
|
||||
QtWidgets.QApplication.sendEvent(self._main_window, key)
|
||||
elif item and isinstance(item, NodeItem) and self._adding_link and event.button() == QtCore.Qt.LeftButton:
|
||||
self._userNodeLinking(event, item)
|
||||
#context_event = QtGui.QContextMenuEvent(QtGui.QContextMenuEvent.Mouse, event.pos())
|
||||
#QtWidgets.QApplication.sendEvent(self, context_event)
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_note:
|
||||
pos = self.mapToScene(event.pos())
|
||||
note = self.createDrawingItem("text", pos.x(), pos.y(), 2)
|
||||
note = self.createDrawingItem("text", int(pos.x()), int(pos.y()), 2)
|
||||
pos_x = note.pos().x()
|
||||
pos_y = note.pos().y() - (note.boundingRect().height() / 2)
|
||||
note.setPos(pos_x, pos_y)
|
||||
@@ -491,19 +486,19 @@ 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(), 1)
|
||||
self.createDrawingItem("rect", int(pos.x()), int(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(), 1)
|
||||
self.createDrawingItem("ellipse", int(pos.x()), int(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.createDrawingItem("line", int(pos.x()), int(pos.y()), 1)
|
||||
self._main_window.uiDrawLineAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_line = False
|
||||
@@ -512,6 +507,46 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
self.toggleUiDeviceMenu()
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
"""
|
||||
Handles all context menu events.
|
||||
|
||||
:param event: QContextMenuEvent instance
|
||||
"""
|
||||
|
||||
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):
|
||||
it.setHovered(False)
|
||||
|
||||
if is_not_link and is_not_logo 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)
|
||||
item.setSelected(True)
|
||||
self._showDeviceContextualMenu(event.globalPos())
|
||||
# when more than one item is selected display the contextual menu even if mouse is not above an item
|
||||
elif len(self.scene().selectedItems()) > 1:
|
||||
self._showDeviceContextualMenu(event.globalPos())
|
||||
#elif item and isinstance(item, NodeItem) and self._adding_link:
|
||||
# self._userNodeLinking(event, item)
|
||||
else:
|
||||
super().contextMenuEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""
|
||||
Handles all mouse release events.
|
||||
@@ -547,7 +582,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
delta = event.angleDelta()
|
||||
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.scaleView(pow(2.0, (delta.y()/2) / 240.0))
|
||||
self._topology.project().setZoom(round(self.transform().m11() * 100))
|
||||
self._topology.project().update()
|
||||
else:
|
||||
@@ -562,7 +597,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)
|
||||
self._main_window.uiStatusBar.showMessage("Zoom: {}%".format(round(self.transform().m11() * 100)), 5000)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
@@ -607,7 +642,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if item:
|
||||
# show item coords in the status bar
|
||||
coords = "X: {} Y: {} Z: {}".format(item.x(), item.y(), item.zValue())
|
||||
self._main_window.uiStatusBar.showMessage(coords, 2000)
|
||||
self._main_window.uiStatusBar.showMessage(coords)
|
||||
|
||||
# force the children to redraw because of a problem with QGraphicsEffect
|
||||
for item in self.scene().selectedItems():
|
||||
@@ -627,7 +662,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if not self._adding_link:
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
item.setSelected(True)
|
||||
if item.node().status() == Node.stopped or item.node().isAlwaysOn():
|
||||
if item.node().status() == Node.stopped or item.node().consoleType() == "none" or item.node().consoleType() is None:
|
||||
self.configureSlot()
|
||||
return
|
||||
else:
|
||||
@@ -680,6 +715,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
:param event: QDropEvent instance
|
||||
"""
|
||||
|
||||
log.debug("Drop event received with mime data: {}".format(event.mimeData().formats()))
|
||||
# check if what has been dropped is handled by this view
|
||||
if event.mimeData().hasFormat("application/x-gns3-template"):
|
||||
template_id = event.mimeData().data("application/x-gns3-template").data().decode()
|
||||
@@ -691,8 +727,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
integer, ok = QtWidgets.QInputDialog.getInt(self, "Nodes", "Number of nodes:", 2, 1, 100, 1)
|
||||
if ok:
|
||||
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
|
||||
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
|
||||
if self.createNodeFromTemplateId(template_id, QtCore.QPoint(x, y)) is False:
|
||||
event.ignore()
|
||||
break
|
||||
@@ -724,6 +760,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self.populateDeviceContextualMenu(menu)
|
||||
menu.exec_(pos)
|
||||
menu.clear()
|
||||
# Make sure to deselect all items.
|
||||
# This is to prevent a bug on Windows
|
||||
# see https://github.com/GNS3/gns3-gui/issues/2986
|
||||
for it in self.scene().items():
|
||||
it.setSelected(False)
|
||||
|
||||
def populateDeviceContextualMenu(self, menu):
|
||||
"""
|
||||
@@ -826,19 +867,19 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
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)):
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and bool(item.node().configFiles()), items)):
|
||||
import_config_action = QtWidgets.QAction("Import config", menu)
|
||||
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)):
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and bool(item.node().configFiles()), items)):
|
||||
export_config_action = QtWidgets.QAction("Export config", menu)
|
||||
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)):
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and bool(item.node().configTextFiles()), items)):
|
||||
export_config_action = QtWidgets.QAction("Edit config", menu)
|
||||
export_config_action.setIcon(get_icon("edit.svg"))
|
||||
export_config_action.triggered.connect(self.editConfigActionSlot)
|
||||
@@ -988,6 +1029,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
new_hostname, ok = QtWidgets.QInputDialog.getText(self, "Change hostname", "Hostname:", QtWidgets.QLineEdit.Normal, item.node().name())
|
||||
if ok:
|
||||
if not new_hostname.strip():
|
||||
QtWidgets.QMessageBox.critical(self, "Change hostname", "Hostname cannot be blank")
|
||||
continue
|
||||
if hasattr(item.node(), "validateHostname"):
|
||||
if not item.node().validateHostname(new_hostname):
|
||||
QtWidgets.QMessageBox.critical(self, "Change hostname", "Invalid name detected for this node: {}".format(new_hostname))
|
||||
@@ -1054,8 +1098,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
# 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.")
|
||||
QtWidgets.QMessageBox.warning(self, "TightVNC", "TightVNC (vncviewer) may not start because of lack of IPv6 support.")
|
||||
|
||||
try:
|
||||
node.openConsole(aux=aux)
|
||||
@@ -1096,11 +1139,21 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
def consoleFromAllItems(self):
|
||||
"""
|
||||
Console from all scene items, except builtin devices.
|
||||
Console from all scene items with console type different than "none"
|
||||
"""
|
||||
|
||||
items = [item for item in self.scene().items()
|
||||
if not (isinstance(item, NodeItem) and isinstance(item.node().module(), Builtin))]
|
||||
if isinstance(item, NodeItem) and item.node().consoleType() != "none"]
|
||||
nb_items = len(items)
|
||||
if nb_items > 10:
|
||||
proceed = QtWidgets.QMessageBox.question(self,
|
||||
"Console to all nodes",
|
||||
"You are about to open console windows to {} nodes. Are you sure?".format(nb_items),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
if proceed == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
self.consoleFromItems(items)
|
||||
|
||||
def consoleActionSlot(self):
|
||||
@@ -1116,24 +1169,20 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
Allow user to use a custom console for this VM
|
||||
"""
|
||||
|
||||
current_cmd = None
|
||||
console_type = "telnet"
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and item.node().console() is not None 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():
|
||||
if item.node().consoleType() not in ("telnet", "serial", "vnc", "spice", "spice+agent"):
|
||||
continue
|
||||
current_cmd = item.node().consoleCommand()
|
||||
console_type = item.node().consoleType()
|
||||
|
||||
(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 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", "spice", "spice+agent"):
|
||||
continue
|
||||
(ok, cmd) = ConsoleCommandDialog.getCommand(self, console_type=console_type, current=current_cmd)
|
||||
if ok:
|
||||
try:
|
||||
node.openConsole(command=cmd)
|
||||
if item.node().status() != Node.started:
|
||||
QtWidgets.QMessageBox.warning(self, "Console", "This node must be started before a console can be opened")
|
||||
continue
|
||||
item.node().openConsole(command=cmd)
|
||||
except (OSError, ValueError) as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
|
||||
|
||||
@@ -1183,7 +1232,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "configFiles") and item.node().initialized():
|
||||
if isinstance(item, NodeItem) and item.node().configFiles() and item.node().initialized():
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
@@ -1199,12 +1248,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
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)")
|
||||
if not path:
|
||||
continue
|
||||
self._import_config_dir = os.path.dirname(path)
|
||||
self._import_config_directory = os.path.dirname(path)
|
||||
item.node().importFile(config_file, path)
|
||||
|
||||
def editConfigActionSlot(self):
|
||||
@@ -1214,17 +1263,17 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "configFiles") and item.node().initialized():
|
||||
if isinstance(item, NodeItem) and item.node().configTextFiles() and item.node().initialized():
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
return
|
||||
|
||||
for item in items:
|
||||
if len(item.node().configFiles()) == 1:
|
||||
config_file = item.node().configFiles()[0]
|
||||
if len(item.node().configTextFiles()) == 1:
|
||||
config_file = item.node().configTextFiles()[0]
|
||||
else:
|
||||
config_file, ok = QtWidgets.QInputDialog.getItem(self, "Edit file", "File to edit?", item.node().configFiles(), 0, False)
|
||||
config_file, ok = QtWidgets.QInputDialog.getItem(self, "Edit file", "File to edit?", item.node().configTextFiles(), 0, False)
|
||||
if not ok:
|
||||
continue
|
||||
dialog = FileEditorDialog(item.node(), config_file, parent=self)
|
||||
@@ -1239,7 +1288,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "configFiles") and item.node().initialized():
|
||||
if isinstance(item, NodeItem) and item.node().configFiles() and item.node().initialized():
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
@@ -1366,7 +1415,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
type = "rect"
|
||||
else:
|
||||
type = "image"
|
||||
self.createDrawingItem(type, item.pos().x() + 20, item.pos().y() + 20, item.zValue(), rotation=item.rotation(), svg=item.toSvg())
|
||||
|
||||
self.createDrawingItem(
|
||||
type,
|
||||
int(item.pos().x()) + 20,
|
||||
int(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())
|
||||
|
||||
@@ -1485,7 +1542,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if not isinstance(item, LinkItem) and not isinstance(item, LabelItem):
|
||||
if not isinstance(item, LinkItem) and not isinstance(item, LabelItem) and not isinstance(item, SvgIconItem):
|
||||
if item.locked() is True:
|
||||
item.setLocked(False)
|
||||
else:
|
||||
@@ -1503,7 +1560,14 @@ 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 isinstance(item, DrawingItem) and item.locked():
|
||||
QtWidgets.QMessageBox.critical(self, "Delete", "Cannot delete drawing because it is locked")
|
||||
return
|
||||
if selected_nodes:
|
||||
if len(selected_nodes) > 1:
|
||||
question = "Do you want to permanently delete these {} nodes?".format(len(selected_nodes))
|
||||
@@ -1598,10 +1662,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
def drawBackground(self, painter, rect):
|
||||
super().drawBackground(painter, rect)
|
||||
if self._main_window.uiShowGridAction.isChecked():
|
||||
grids = [(self.drawingGridSize(),QtGui.QColor(208, 208, 208)),
|
||||
(self.nodeGridSize(),QtGui.QColor(190, 190, 190))]
|
||||
grids = [(self.drawingGridSize(), QtGui.QColor(208, 208, 208)),
|
||||
(self.nodeGridSize(), QtGui.QColor(190, 190, 190))]
|
||||
painter.save()
|
||||
for (grid,colour) in grids:
|
||||
for (grid, colour) in grids:
|
||||
if not grid:
|
||||
continue
|
||||
painter.setPen(QtGui.QPen(colour))
|
||||
|
||||
left = int(rect.left()) - (int(rect.left()) % grid)
|
||||
@@ -1609,11 +1675,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
x = left
|
||||
while x < rect.right():
|
||||
painter.drawLine(x, rect.top(), x, rect.bottom())
|
||||
painter.drawLine(x, int(rect.top()), x, int(rect.bottom()))
|
||||
x += grid
|
||||
y = top
|
||||
while y < rect.bottom():
|
||||
painter.drawLine(rect.left(), y, rect.right(), y)
|
||||
painter.drawLine(int(rect.left()), y, int(rect.right()), y)
|
||||
y += grid
|
||||
painter.restore()
|
||||
|
||||
|
||||
@@ -18,18 +18,15 @@
|
||||
from .qt import sip
|
||||
import json
|
||||
import copy
|
||||
import http
|
||||
import uuid
|
||||
import pathlib
|
||||
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
|
||||
from .qt import QtCore, QtNetwork, QtWidgets, qpartial, sip_is_deleted
|
||||
from .utils import parse_version
|
||||
|
||||
import logging
|
||||
@@ -79,6 +76,18 @@ class HTTPClient(QtCore.QObject):
|
||||
self._shutdown = False # Shutdown in progress
|
||||
self._accept_insecure_certificate = settings.get("accept_insecure_certificate", None)
|
||||
|
||||
# Add custom CA
|
||||
# ssl_config = QtNetwork.QSslConfiguration.defaultConfiguration()
|
||||
# if ssl_config.addCaCertificates("/path/to/rootCA.crt"):
|
||||
# log.debug("CA certificate added")
|
||||
# QtNetwork.QSslConfiguration.setDefaultConfiguration(ssl_config)
|
||||
|
||||
if self._protocol == "https":
|
||||
if not QtNetwork.QSslSocket.supportsSsl():
|
||||
log.error("SSL is not supported")
|
||||
else:
|
||||
log.debug(f"SSL is supported, version: {QtNetwork.QSslSocket().sslLibraryBuildVersionString()}")
|
||||
|
||||
# In order to detect computer hibernation we detect the date of the last
|
||||
# query and disconnect if time is too long between two query
|
||||
self._last_query_timestamp = None
|
||||
@@ -93,6 +102,12 @@ class HTTPClient(QtCore.QObject):
|
||||
# List of query waiting for the connection
|
||||
self._query_waiting_connections = []
|
||||
|
||||
# To catch SSL errors
|
||||
self._network_manager.sslErrors.connect(self._sslErrorsSlot)
|
||||
|
||||
# Store SSL error exceptions
|
||||
self._ssl_exceptions = {}
|
||||
|
||||
def setMaxTimeDifferenceBetweenQueries(self, value):
|
||||
self._max_time_difference_between_queries = value
|
||||
|
||||
@@ -207,16 +222,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, str(sent), str(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, str(sent), str(abs(total)))
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
|
||||
|
||||
@classmethod
|
||||
def setProgressCallback(cls, progress_callback):
|
||||
@@ -389,17 +404,12 @@ class HTTPClient(QtCore.QObject):
|
||||
self._query_waiting_connections = []
|
||||
return
|
||||
|
||||
if params["version"].split("-")[0] != __version__.split("-")[0]:
|
||||
if params["version"].split("+")[0] != __version__.split("+")[0]:
|
||||
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]:
|
||||
# We don't allow different versions to interact even with dev build
|
||||
# (excepting post release corrections e.g 2.2.32.1, occassionally done when fixing a packaging problem)
|
||||
# TODO: we should probably follow this standard starting with v3.0: https://semver.org/
|
||||
if parse_version(__version__)[:3] != parse_version(params["version"])[:3]:
|
||||
log.error(msg)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
@@ -470,7 +480,16 @@ class HTTPClient(QtCore.QObject):
|
||||
"""
|
||||
host = self._getHostForQuery()
|
||||
request = websocket.request()
|
||||
request.setUrl(QtCore.QUrl("ws://{host}:{port}{prefix}{path}".format(host=host, port=self._port, path=path, prefix=prefix)))
|
||||
ws_protocol = "ws"
|
||||
if self._protocol == "https":
|
||||
ws_protocol = "wss"
|
||||
ws_url = "{protocol}://{host}:{port}{prefix}{path}".format(protocol=ws_protocol,
|
||||
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
|
||||
@@ -530,14 +549,13 @@ class HTTPClient(QtCore.QObject):
|
||||
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))
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{path}{query_string}".format(protocol=self._protocol, host=host, port=self._port, path=path, prefix=prefix, query_string=query_string))
|
||||
|
||||
if self._user:
|
||||
url = QtCore.QUrl("{protocol}://{user}@{host}:{port}{prefix}{path}{query_string}".format(protocol=self._protocol, user=self._user, host=host, port=self._port, path=path, prefix=prefix, query_string=query_string))
|
||||
else:
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{path}{query_string}".format(protocol=self._protocol, host=host, port=self._port, path=path, prefix=prefix, query_string=query_string))
|
||||
url.setUserName(self._user)
|
||||
|
||||
request = self._request(url)
|
||||
|
||||
request = self._addAuth(request)
|
||||
|
||||
request.setRawHeader(b"User-Agent", "GNS3 QT Client v{version}".format(version=__version__).encode())
|
||||
|
||||
# By default QT doesn't support GET with body even if it's in the RFC that's why we need to use sendCustomRequest
|
||||
@@ -725,47 +743,103 @@ 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:
|
||||
if content_type == "application/json":
|
||||
content = response.read()
|
||||
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))
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
|
||||
|
||||
if self._user:
|
||||
url.setUserName(self._user)
|
||||
|
||||
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()))
|
||||
else:
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
if status == 200 and content_type == "application/json":
|
||||
content = bytes(response.readAll())
|
||||
try:
|
||||
json_data = json.loads(content.decode("utf-8"))
|
||||
return response.status, json_data
|
||||
else:
|
||||
return response.status, None
|
||||
except http.client.InvalidURL as e:
|
||||
log.warning("Invalid local server url: {}".format(e))
|
||||
return 0, None
|
||||
except urllib.error.URLError:
|
||||
# Connection refused. It's a normal behavior if server is not started
|
||||
return 0, None
|
||||
except urllib.error.HTTPError as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return e.code, None
|
||||
except (OSError, http.client.BadStatusLine, ValueError) as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return 0, None
|
||||
except (UnicodeEncodeError, ValueError) as e:
|
||||
log.warning("Could not read JSON data returned from {}: {}".format(url, e))
|
||||
else:
|
||||
return status, json_data
|
||||
return status, None
|
||||
|
||||
def _sslErrorsSlot(self, response, ssl_errors):
|
||||
|
||||
self.handleSslError(response, ssl_errors)
|
||||
|
||||
def handleSslError(self, response, ssl_errors):
|
||||
|
||||
if self._accept_insecure_certificate:
|
||||
response.ignoreSslErrors()
|
||||
return
|
||||
|
||||
url = response.request().url()
|
||||
host_port_key = f"{url.host()}:{url.port()}"
|
||||
|
||||
# get the certificate digest
|
||||
ssl_config = response.sslConfiguration()
|
||||
peer_cert = ssl_config.peerCertificate()
|
||||
digest = peer_cert.digest()
|
||||
|
||||
if host_port_key in self._ssl_exceptions:
|
||||
|
||||
if self._ssl_exceptions[host_port_key] == digest:
|
||||
response.ignoreSslErrors()
|
||||
return
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
main_window = MainWindow.instance()
|
||||
|
||||
msgbox = QtWidgets.QMessageBox(main_window)
|
||||
msgbox.setWindowTitle("SSL error detected")
|
||||
msgbox.setText(f"This server could not prove that it is {url.host()}:{url.port()}. Please carefully examine the certificate to make sure the server can be trusted.")
|
||||
msgbox.setInformativeText(f"{ssl_errors[0].errorString()}")
|
||||
msgbox.setDetailedText(peer_cert.toText())
|
||||
msgbox.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
connect_button = QtWidgets.QPushButton(f"&Connect to {url.host()}:{url.port()}", msgbox)
|
||||
msgbox.addButton(connect_button, QtWidgets.QMessageBox.YesRole)
|
||||
abort_button = QtWidgets.QPushButton("&Abort", msgbox)
|
||||
msgbox.addButton(abort_button, QtWidgets.QMessageBox.RejectRole)
|
||||
msgbox.setDefaultButton(abort_button)
|
||||
msgbox.setIcon(QtWidgets.QMessageBox.Critical)
|
||||
msgbox.exec_()
|
||||
|
||||
if msgbox.clickedButton() == connect_button:
|
||||
self._ssl_exceptions[host_port_key] = digest
|
||||
response.ignoreSslErrors()
|
||||
else:
|
||||
for error in ssl_errors:
|
||||
log.error(f"SSL error detected: {error.errorString()}")
|
||||
main_window.close()
|
||||
|
||||
@classmethod
|
||||
def fromUrl(cls, url, network_manager=None, base_settings=None):
|
||||
|
||||
@@ -176,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():
|
||||
|
||||
@@ -15,6 +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 os
|
||||
import pathlib
|
||||
import urllib.parse
|
||||
|
||||
@@ -37,6 +38,9 @@ class ImageUploadManager(object):
|
||||
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)
|
||||
@@ -71,19 +75,16 @@ class ImageUploadManager(object):
|
||||
self._callback(result, error, **kwargs)
|
||||
|
||||
def _fileUploadToCompute(self, endpoint):
|
||||
log.info("Uploading file to compute: {}".format(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),
|
||||
progressText="Uploading {}".format(self._image.filename), timeout=None, prefix="")
|
||||
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.info("Uploading file to controller: {}".format(self._getComputePath()))
|
||||
self._controller.postCompute(
|
||||
self._getComputePath(), self._compute_id, self._callback, body=pathlib.Path(self._image.path),
|
||||
progressText="Uploading {}".format(self._image.filename), timeout=None)
|
||||
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)
|
||||
|
||||
@@ -44,6 +44,7 @@ class DrawingItem:
|
||||
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._allow_snap_to_grid = True
|
||||
self._locked = locked
|
||||
if self._id is None:
|
||||
self._id = str(uuid.uuid4())
|
||||
@@ -135,6 +136,9 @@ class DrawingItem:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
return True
|
||||
elif modifiers & QtCore.Qt.AltModifier:
|
||||
self._allow_snap_to_grid = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
@@ -147,6 +151,15 @@ class DrawingItem:
|
||||
if not self.handleKeyPressEvent(event):
|
||||
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
|
||||
|
||||
def keyReleaseEvent(self, event):
|
||||
"""
|
||||
Handles all key release events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
self._allow_snap_to_grid = True
|
||||
|
||||
def __json__(self):
|
||||
data = {
|
||||
"drawing_id": self._id,
|
||||
@@ -212,14 +225,14 @@ class DrawingItem:
|
||||
self._project.delete("/drawings/" + self._id, None, body=self.__json__())
|
||||
|
||||
def itemChange(self, change, value):
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self._main_window.uiSnapToGridAction.isChecked() \
|
||||
and self._allow_snap_to_grid:
|
||||
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
|
||||
value.setX((grid_size * round((value.x() + mid_x) / grid_size)) - mid_x)
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
tmp_y = (grid_size * round((self.y() + mid_y) / grid_size)) - mid_y
|
||||
if tmp_x != self.x() and tmp_y != self.y():
|
||||
self.setPos(tmp_x, tmp_y)
|
||||
value.setY((grid_size * round((value.y()+mid_y)/grid_size)) - mid_y)
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if not value:
|
||||
@@ -247,7 +260,7 @@ class DrawingItem:
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Pekka Helenius
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -51,10 +52,16 @@ class EthernetLinkItem(LinkItem):
|
||||
|
||||
LinkItem.adjust(self)
|
||||
|
||||
if self._hovered:
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.red, self._pen_width + 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
else:
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.black, self._pen_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
try:
|
||||
if self._hovered:
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.red, self._link._link_style["width"] + 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
else:
|
||||
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], self._link._link_style["type"], QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
except:
|
||||
if self._hovered:
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.red, self._pen_width + 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
else:
|
||||
self.setPen(QtGui.QPen(QtGui.QColor("#000000"), self._pen_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
|
||||
# draw a line between nodes
|
||||
path = QtGui.QPainterPath(self.source)
|
||||
@@ -147,11 +154,12 @@ class EthernetLinkItem(LinkItem):
|
||||
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()
|
||||
|
||||
if self._settings["draw_link_status_points"]:
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.NoPen:
|
||||
painter.drawPoint(point1)
|
||||
|
||||
if self._link.suspended() or self._destination_port.status() == Port.suspended:
|
||||
@@ -189,11 +197,12 @@ class EthernetLinkItem(LinkItem):
|
||||
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()
|
||||
|
||||
if self._settings["draw_link_status_points"]:
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.NoPen:
|
||||
painter.drawPoint(point2)
|
||||
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -165,7 +165,7 @@ class LabelItem(QtWidgets.QGraphicsTextItem):
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x(), center.y()), zval)
|
||||
|
||||
@@ -25,6 +25,7 @@ from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot, sip_is_deleted
|
||||
|
||||
from ..packet_capture import PacketCapture
|
||||
from ..dialogs.filter_dialog import FilterDialog
|
||||
from ..dialogs.style_editor_dialog_link import StyleEditorDialogLink
|
||||
from ..utils.get_icon import get_icon
|
||||
|
||||
|
||||
@@ -137,6 +138,21 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
def _suspendActionSlot(self, *args):
|
||||
self._link.toggleSuspend()
|
||||
|
||||
@qslot
|
||||
def _styleActionSlot(self, *args):
|
||||
style_dialog = StyleEditorDialogLink(self, self._main_window)
|
||||
style_dialog.show()
|
||||
style_dialog.exec_()
|
||||
|
||||
def setLinkStyle(self, link_style):
|
||||
self._link._link_style["color"] = link_style["color"]
|
||||
self._link._link_style["width"] = link_style["width"]
|
||||
self._link._link_style["type"] = link_style["type"]
|
||||
|
||||
# This refers to functions in link.py!
|
||||
self._link.setLinkStyle(link_style)
|
||||
self._link.update()
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete this link
|
||||
@@ -266,6 +282,12 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
resume_action.triggered.connect(self._suspendActionSlot)
|
||||
menu.addAction(resume_action)
|
||||
|
||||
# style
|
||||
style_action = QtWidgets.QAction("Style", menu)
|
||||
style_action.setIcon(get_icon("node_conception.svg"))
|
||||
style_action.triggered.connect(self._styleActionSlot)
|
||||
menu.addAction(style_action)
|
||||
|
||||
# delete
|
||||
delete_action = QtWidgets.QAction("Delete", menu)
|
||||
delete_action.setIcon(get_icon('delete.svg'))
|
||||
@@ -280,23 +302,31 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
:param: QGraphicsSceneMouseEvent instance
|
||||
"""
|
||||
|
||||
if event.button() == QtCore.Qt.RightButton:
|
||||
if self._adding_flag:
|
||||
# send a escape key to the main window to cancel the link addition
|
||||
from ..main_window import MainWindow
|
||||
key = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Escape, QtCore.Qt.NoModifier)
|
||||
QtWidgets.QApplication.sendEvent(MainWindow.instance(), key)
|
||||
return
|
||||
if event.button() == QtCore.Qt.RightButton and self._adding_flag:
|
||||
# send a escape key to the main window to cancel the link addition
|
||||
from ..main_window import MainWindow
|
||||
key = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Escape, QtCore.Qt.NoModifier)
|
||||
QtWidgets.QApplication.sendEvent(MainWindow.instance(), key)
|
||||
return
|
||||
else:
|
||||
super().mousePressEvent(event)
|
||||
|
||||
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 contextMenuEvent(self, event):
|
||||
"""
|
||||
Handles all context menu events.
|
||||
|
||||
:param event: QContextMenuEvent instance
|
||||
"""
|
||||
|
||||
if not sip_is_deleted(self):
|
||||
# create the contextual menu
|
||||
self.setHovered(True)
|
||||
self.setAcceptHoverEvents(False)
|
||||
menu = QtWidgets.QMenu()
|
||||
self.populateLinkContextualMenu(menu)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setHovered(False)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
|
||||
@@ -51,6 +51,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
self._links = []
|
||||
self._symbol = None
|
||||
self._locked = False
|
||||
self._allow_snap_to_grid = True
|
||||
|
||||
# says if the attached node has been initialized
|
||||
# by the server.
|
||||
@@ -103,21 +104,13 @@ 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):
|
||||
|
||||
grid_size = self._main_window.uiGraphicsView.nodeGridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
x = (grid_size * round((self.x() + mid_x) / grid_size)) - mid_x
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
y = (grid_size * round((self.y() + mid_y) / grid_size)) - mid_y
|
||||
self.setPos(x, y)
|
||||
if self._main_window.uiSnapToGridAction.isChecked():
|
||||
self.setPos(QtCore.QPointF(self._node.x() + 0.1, self._node.y()))
|
||||
|
||||
def updateNode(self):
|
||||
"""
|
||||
@@ -399,7 +392,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
else:
|
||||
self._node_label.setPos(label_data["x"], label_data["y"])
|
||||
|
||||
def connectToPort(self, unavailable_ports=[]):
|
||||
def connectToPort(self, pos, unavailable_ports=[]):
|
||||
"""
|
||||
Shows a contextual menu for the user to choose port or auto-select one.
|
||||
|
||||
@@ -447,7 +440,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port_object.name())
|
||||
|
||||
menu.triggered.connect(self.selectedPortSlot)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
# add some delay before showing the menu
|
||||
# https://github.com/GNS3/gns3-gui/issues/3169
|
||||
QtCore.QThread.msleep(100)
|
||||
menu.exec_(pos)
|
||||
return self._selected_port
|
||||
|
||||
def selectedPortSlot(self, action):
|
||||
@@ -474,7 +470,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param value: value of the change
|
||||
"""
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self._main_window.uiSnapToGridAction.isChecked() \
|
||||
and self._allow_snap_to_grid:
|
||||
grid_size = self._main_window.uiGraphicsView.nodeGridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
value.setX((grid_size * round((value.x() + mid_x) / grid_size)) - mid_x)
|
||||
@@ -515,7 +512,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
if self.show_layer:
|
||||
text = str(int(self.zValue())) # Z value
|
||||
@@ -536,6 +533,27 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
for link in self._links:
|
||||
link.adjust()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
if event.modifiers() & QtCore.Qt.AltModifier:
|
||||
self._allow_snap_to_grid = False
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def keyReleaseEvent(self, event):
|
||||
"""
|
||||
Handles all key release events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
self._allow_snap_to_grid = True
|
||||
|
||||
def locked(self):
|
||||
|
||||
return self._locked
|
||||
|
||||
@@ -32,8 +32,22 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
"""
|
||||
|
||||
def __init__(self, width=200, height=100, **kws):
|
||||
self._rx = 0
|
||||
self._ry = 0
|
||||
super().__init__(width=width, height=height, **kws)
|
||||
|
||||
def setHorizontalCornerRadius(self, radius: int):
|
||||
self._rx = radius
|
||||
|
||||
def horizontalCornerRadius(self):
|
||||
return self._rx
|
||||
|
||||
def setVerticalCornerRadius(self, radius: int):
|
||||
self._ry = radius
|
||||
|
||||
def verticalCornerRadius(self):
|
||||
return self._ry
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
Paints the contents of an item in local coordinates.
|
||||
@@ -43,7 +57,9 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
painter.setPen(self.pen())
|
||||
painter.setBrush(self.brush())
|
||||
painter.drawRoundedRect(self.rect(), self._rx, self._ry)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def toSvg(self):
|
||||
@@ -57,7 +73,27 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
rect = ET.SubElement(svg, "rect")
|
||||
rect.set("width", str(int(self.rect().width())))
|
||||
rect.set("height", str(int(self.rect().height())))
|
||||
if self._rx:
|
||||
rect.set("rx", str(self._rx))
|
||||
if self._ry:
|
||||
rect.set("ry", str(self._ry))
|
||||
|
||||
rect = self._styleSvg(rect)
|
||||
|
||||
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
|
||||
def fromSvg(self, svg):
|
||||
svg_elem = ET.fromstring(svg)
|
||||
if len(svg_elem):
|
||||
# handle horizontal corner radius and vertical corner radius (specific to rectangles)
|
||||
rx = svg_elem[0].get("rx")
|
||||
ry = svg_elem[0].get("ry")
|
||||
if rx:
|
||||
self._rx = int(rx)
|
||||
elif ry:
|
||||
self._rx = int(ry) # defaults to ry if it is specified
|
||||
if ry:
|
||||
self._ry = int(ry)
|
||||
elif rx:
|
||||
self._ry = int(rx) # defaults to rx if it is specified
|
||||
super().fromSvg(svg)
|
||||
|
||||
@@ -50,10 +50,16 @@ class SerialLinkItem(LinkItem):
|
||||
|
||||
LinkItem.adjust(self)
|
||||
|
||||
if self._hovered:
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.red, self._pen_width + 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
else:
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.darkRed, self._pen_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
try:
|
||||
if self._hovered:
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.red, self._link._link_style["width"] + 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
else:
|
||||
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], self._link._link_style["type"], QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
except:
|
||||
if self._hovered:
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.red, self._pen_width + 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
else:
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.darkRed, self._pen_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
|
||||
# get source to destination angle
|
||||
vector_angle = math.atan2(self.dy, self.dx)
|
||||
@@ -136,11 +142,12 @@ class SerialLinkItem(LinkItem):
|
||||
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()
|
||||
|
||||
if self._settings["draw_link_status_points"]:
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.NoPen:
|
||||
painter.drawPoint(self.source_point)
|
||||
|
||||
# destination point color
|
||||
@@ -167,11 +174,12 @@ class SerialLinkItem(LinkItem):
|
||||
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()
|
||||
|
||||
if self._settings["draw_link_status_points"]:
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.NoPen:
|
||||
painter.drawPoint(self.destination_point)
|
||||
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -171,9 +171,12 @@ class ShapeItem(DrawingItem):
|
||||
if not self.locked():
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def setWidthAndHeight(self, width, height):
|
||||
self.setRect(0, 0, width, height)
|
||||
|
||||
def fromSvg(self, svg):
|
||||
"""
|
||||
Import element informations from an SVG
|
||||
Import element information from SVG
|
||||
"""
|
||||
svg = ET.fromstring(svg)
|
||||
width = float(svg.get("width", self.rect().width()))
|
||||
|
||||
67
gns3/link.py
67
gns3/link.py
@@ -19,7 +19,6 @@
|
||||
Manages and stores everything needed for a connection between 2 devices.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from .qt import sip
|
||||
import uuid
|
||||
@@ -79,6 +78,8 @@ class Link(QtCore.QObject):
|
||||
self._deleting = False
|
||||
self._capture_file_path = None
|
||||
self._capture_file = None
|
||||
self._response_stream = None
|
||||
self._capture_compute_id = None
|
||||
self._initialized = False
|
||||
self._filters = {}
|
||||
self._suspend = False
|
||||
@@ -89,9 +90,7 @@ class Link(QtCore.QObject):
|
||||
self._creator = False
|
||||
|
||||
self._nodes = []
|
||||
|
||||
self._source_node.addLink(self)
|
||||
self._destination_node.addLink(self)
|
||||
self._link_style = {}
|
||||
|
||||
body = self._prepareParams()
|
||||
if self._link_id:
|
||||
@@ -103,30 +102,38 @@ 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
|
||||
self._capturing = result.get("capturing", False)
|
||||
if self._capturing:
|
||||
if Controller.instance().isRemote():
|
||||
if self._capture_file_path is None and result.get("capture_file_path", None) is not None:
|
||||
self._capture_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()
|
||||
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"]
|
||||
else:
|
||||
self._capture_file = QtCore.QFile(self._capture_file_path)
|
||||
self._capture_file.open(QtCore.QFile.WriteOnly)
|
||||
self._response_stream = 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("Has successfully started capturing packets on link {} to '{}'".format(self._link_id, self._capture_file_path))
|
||||
else:
|
||||
self._response_stream = None
|
||||
|
||||
if "nodes" in result:
|
||||
self._nodes = result["nodes"]
|
||||
self._updateLabels()
|
||||
if "filters" in result:
|
||||
self._filters = result["filters"]
|
||||
if "link_style" in result:
|
||||
self._link_style = result["link_style"]
|
||||
if "suspend" in result:
|
||||
self._suspend = result["suspend"]
|
||||
self.updated_link_signal.emit(self._id)
|
||||
@@ -209,6 +216,7 @@ class Link(QtCore.QObject):
|
||||
}
|
||||
],
|
||||
"filters": self._filters,
|
||||
"link_style": self._link_style,
|
||||
"suspend": self._suspend
|
||||
}
|
||||
if self._source_port.label():
|
||||
@@ -351,9 +359,8 @@ class Link(QtCore.QObject):
|
||||
|
||||
def _startCaptureCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while starting capture on link: {}".format(result["message"]))
|
||||
log.error("Error while starting capture on link {}: {}".format(self._link_id, result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
def _downloadPcapProgress(self, content, server=None, context={}, **kwargs):
|
||||
"""
|
||||
@@ -366,25 +373,27 @@ class Link(QtCore.QObject):
|
||||
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 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))
|
||||
# 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)
|
||||
|
||||
|
||||
def _stopCaptureCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while stopping capture on link: {}".format(result["message"]))
|
||||
log.error("Error while stopping capture on link {}: {}".format(self._link_id, result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
log.debug("Has successfully stopped capturing packets on link {}".format(self._link_id))
|
||||
|
||||
def get(self, path, callback, **kwargs):
|
||||
"""
|
||||
@@ -462,3 +471,9 @@ class Link(QtCore.QObject):
|
||||
:params filters: List of filters
|
||||
"""
|
||||
self._filters = filters
|
||||
|
||||
def setLinkStyle(self, link_style):
|
||||
"""
|
||||
:params _link_style: Set link style attributes
|
||||
"""
|
||||
self._link_style = link_style
|
||||
|
||||
@@ -24,8 +24,10 @@ import copy
|
||||
import psutil
|
||||
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .version import __version__
|
||||
from .version import __version__, __version_info__
|
||||
from .utils import parse_version
|
||||
from .local_server_config import LocalServerConfig
|
||||
from .settings import LOCAL_SERVER_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -87,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))
|
||||
|
||||
@@ -114,17 +133,18 @@ class LocalConfig(QtCore.QObject):
|
||||
self._config_file = None
|
||||
self._resetLoadConfig()
|
||||
|
||||
|
||||
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)
|
||||
@@ -146,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)
|
||||
@@ -156,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
|
||||
@@ -165,7 +186,7 @@ class LocalConfig(QtCore.QObject):
|
||||
if "version" in self._settings:
|
||||
if parse_version(self._settings["version"])[:2] > parse_version(__version__)[:2]:
|
||||
app = QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
|
||||
error_message = "Your settings are for version {} of GNS3. You cannot use a previous version of GNS3 without risking losing data. If you want to reset delete the settings in {}".format(self._settings["version"], self.configDirectory())
|
||||
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)
|
||||
@@ -174,21 +195,21 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
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
|
||||
@@ -201,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
|
||||
@@ -212,7 +233,7 @@ 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:
|
||||
@@ -467,7 +488,7 @@ class LocalConfig(QtCore.QObject):
|
||||
if pid != my_pid:
|
||||
try:
|
||||
process = psutil.Process(pid=pid)
|
||||
ps_name = process.name()
|
||||
ps_name = process.name().lower()
|
||||
except (OSError, psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
else:
|
||||
|
||||
@@ -31,12 +31,11 @@ import subprocess
|
||||
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore, qslot
|
||||
from gns3.settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.settings import LOCAL_SERVER_SETTINGS, DEFAULT_LOCAL_SERVER_HOST
|
||||
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
|
||||
@@ -131,6 +130,7 @@ class LocalServer(QtCore.QObject):
|
||||
import win32serviceutil
|
||||
except ImportError as e:
|
||||
log.error("Could not check if the {} service is running: {}".format(service_name, e))
|
||||
return
|
||||
|
||||
try:
|
||||
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
|
||||
@@ -193,7 +193,11 @@ class LocalServer(QtCore.QObject):
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
|
||||
from gns3.utils.macos_ubridge_setuid import macos_ubridge_setuid
|
||||
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
|
||||
macos_ubridge_setuid()
|
||||
else:
|
||||
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set root permissions to uBridge {}: {}".format(path, str(e)))
|
||||
return False
|
||||
@@ -246,6 +250,9 @@ class LocalServer(QtCore.QObject):
|
||||
"""
|
||||
Update the local server settings. Keep the key not in new_settings
|
||||
"""
|
||||
|
||||
if "host" in new_settings and new_settings["host"] is None:
|
||||
new_settings["host"] = DEFAULT_LOCAL_SERVER_HOST
|
||||
old_settings = copy.copy(self._settings)
|
||||
if not self._settings:
|
||||
self._settings = new_settings
|
||||
@@ -369,13 +376,15 @@ class LocalServer(QtCore.QObject):
|
||||
|
||||
self._checkUbridgePermissions()
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Error", "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
|
||||
return False
|
||||
if sys.platform.startswith("win"):
|
||||
import pywintypes
|
||||
try:
|
||||
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
|
||||
log.warning("The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
|
||||
except pywintypes.error as e:
|
||||
log.warning("Could not check if the NPF or Npcap service is running: {}".format(e.strerror))
|
||||
|
||||
self._port = self._settings["port"]
|
||||
|
||||
# check the local server path
|
||||
local_server_path = self.localServerPath()
|
||||
if not local_server_path:
|
||||
@@ -473,11 +482,15 @@ class LocalServer(QtCore.QObject):
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
# use the string on Windows
|
||||
self._local_server_process = subprocess.Popen(command, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, stderr=subprocess.PIPE)
|
||||
self._local_server_process = subprocess.Popen(
|
||||
command,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||
stderr=subprocess.PIPE,
|
||||
env=os.environ)
|
||||
else:
|
||||
# use arguments on other platforms
|
||||
args = shlex.split(command)
|
||||
self._local_server_process = subprocess.Popen(args, stderr=subprocess.PIPE)
|
||||
self._local_server_process = subprocess.Popen(args, stderr=subprocess.PIPE, env=os.environ)
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.warning('Could not start local server "{}": {}'.format(command, e))
|
||||
return False
|
||||
@@ -516,9 +529,7 @@ 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"])
|
||||
|
||||
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:
|
||||
|
||||
47
gns3/main.py
47
gns3/main.py
@@ -30,16 +30,6 @@ try:
|
||||
except Exception as e:
|
||||
print("Fail update installation: {}".format(str(e)))
|
||||
|
||||
|
||||
# WARNING
|
||||
# Due to buggy user machines we choose to put this as the first loading modules
|
||||
# otherwise the egg cache is initialized in his standard location and
|
||||
# if is not writetable the application crash. It's the user fault
|
||||
# because one day the user as used sudo to run an egg and break his
|
||||
# filesystem permissions, but it's a common mistake.
|
||||
from gns3.utils.get_resource import get_resource
|
||||
|
||||
|
||||
import datetime
|
||||
import traceback
|
||||
import time
|
||||
@@ -60,12 +50,12 @@ from gns3.local_config import LocalConfig
|
||||
from gns3.application import Application
|
||||
from gns3.utils import parse_version
|
||||
from gns3.dialogs.profile_select import ProfileSelectDialog
|
||||
from gns3.version import __version__
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from gns3.version import __version__
|
||||
|
||||
|
||||
def locale_check():
|
||||
"""
|
||||
@@ -118,6 +108,9 @@ def main():
|
||||
# an extra argument starting with -psn_. We filter it
|
||||
if sys.platform.startswith("darwin"):
|
||||
sys.argv = [a for a in sys.argv if not a.startswith("-psn_")]
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.15.2"):
|
||||
# Fixes issue on macOS Big Sur: https://github.com/GNS3/gns3-gui/issues/3037
|
||||
os.environ["QT_MAC_WANTS_LAYER"] = "1"
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("project", help="load a GNS3 project (.gns3)", metavar="path", nargs="?")
|
||||
@@ -132,6 +125,13 @@ def main():
|
||||
if options.project:
|
||||
options.project = os.path.abspath(options.project)
|
||||
|
||||
try:
|
||||
import truststore
|
||||
truststore.inject_into_ssl()
|
||||
log.info("Using system certificate store for SSL connections")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
# We add to the path where the OS search executable our binary location starting by GNS3
|
||||
# packaged binary
|
||||
@@ -142,6 +142,7 @@ def main():
|
||||
frozen_dirs = [
|
||||
frozen_dir,
|
||||
os.path.normpath(os.path.join(frozen_dir, 'dynamips')),
|
||||
os.path.normpath(os.path.join(frozen_dir, 'ubridge')),
|
||||
os.path.normpath(os.path.join(frozen_dir, 'vpcs')),
|
||||
os.path.normpath(os.path.join(frozen_dir, 'traceng'))
|
||||
]
|
||||
@@ -151,6 +152,7 @@ def main():
|
||||
if options.project:
|
||||
os.chdir(frozen_dir)
|
||||
|
||||
|
||||
def exceptionHook(exception, value, tb):
|
||||
|
||||
if exception == KeyboardInterrupt:
|
||||
@@ -182,9 +184,9 @@ def main():
|
||||
# catch exceptions to write them in a file
|
||||
sys.excepthook = exceptionHook
|
||||
|
||||
# we only support Python 3 version >= 3.4
|
||||
if sys.version_info < (3, 4):
|
||||
raise SystemExit("Python 3.4 or higher is required")
|
||||
# we only support Python 3 version >= 3.8
|
||||
if sys.version_info < (3, 8):
|
||||
raise SystemExit("Python 3.8 or higher is required")
|
||||
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.5.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.5.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
@@ -217,10 +219,17 @@ def main():
|
||||
if not options.debug:
|
||||
try:
|
||||
# hide the console
|
||||
# win32console.AllocConsole()
|
||||
console_window = win32console.GetConsoleWindow()
|
||||
win32gui.ShowWindow(console_window, win32con.SW_HIDE)
|
||||
parent_window = win32gui.GetParent(console_window)
|
||||
if not parent_window and console_window:
|
||||
win32gui.ShowWindow(console_window, win32con.SW_HIDE)
|
||||
elif parent_window:
|
||||
win32gui.ShowWindow(parent_window, win32con.SW_HIDE)
|
||||
else:
|
||||
log.warning("Could not get the console window")
|
||||
except win32console.error as e:
|
||||
print("warning: could not allocate console: {}".format(e))
|
||||
log.warning("Could not allocate console: {}".format(e))
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
|
||||
@@ -255,8 +264,8 @@ def main():
|
||||
current_year = datetime.date.today().year
|
||||
log.info("GNS3 GUI version {}".format(__version__))
|
||||
log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
|
||||
|
||||
log.info("Application started with {}".format("".join(sys.argv)))
|
||||
log.info("Application started with {}".format(" ".join(sys.argv)))
|
||||
log.debug("PATH={}".format(os.environ["PATH"]))
|
||||
|
||||
# update the exception file path to have it in the same directory as the settings file.
|
||||
exception_file_path = os.path.join(LocalConfig.instance().configDirectory(), exception_file_path)
|
||||
|
||||
@@ -42,14 +42,13 @@ 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_template_wizard import NewTemplateWizard
|
||||
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
|
||||
@@ -84,6 +83,24 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._settings = {}
|
||||
|
||||
self.setupUi(self)
|
||||
self.setUnifiedTitleAndToolBarOnMac(True)
|
||||
|
||||
# These widgets will be disabled when no project is loaded
|
||||
self.disableWhenNoProjectWidgets = [
|
||||
self.uiGraphicsView,
|
||||
self.uiAnnotateMenu,
|
||||
self.uiAnnotationToolBar,
|
||||
self.uiControlToolBar,
|
||||
self.uiControlMenu,
|
||||
self.uiSaveProjectAsAction,
|
||||
self.uiExportProjectAction,
|
||||
self.uiScreenshotAction,
|
||||
self.uiSnapshotAction,
|
||||
self.uiEditProjectAction,
|
||||
self.uiDeleteProjectAction,
|
||||
self.uiImportExportConfigsAction,
|
||||
self.uiLockAllAction
|
||||
]
|
||||
|
||||
self._notif_dialog = NotifDialog(self)
|
||||
# Setup logger
|
||||
@@ -115,11 +132,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
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._save_gui_state_geometry = True
|
||||
self.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["geometry"].encode()))
|
||||
self.restoreState(QtCore.QByteArray().fromBase64(self._settings["state"].encode()))
|
||||
|
||||
@@ -177,27 +194,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
self.setWindowTitle("[*] GNS3")
|
||||
|
||||
# This widgets will be disable when you have no project loaded
|
||||
self.disableWhenNoProjectWidgets = [
|
||||
self.uiGraphicsView,
|
||||
self.uiAnnotateMenu,
|
||||
self.uiAnnotationToolBar,
|
||||
self.uiControlToolBar,
|
||||
self.uiControlMenu,
|
||||
self.uiSaveProjectAsAction,
|
||||
self.uiExportProjectAction,
|
||||
self.uiScreenshotAction,
|
||||
self.uiSnapshotAction,
|
||||
self.uiEditProjectAction,
|
||||
self.uiDeleteProjectAction,
|
||||
self.uiImportExportConfigsAction
|
||||
]
|
||||
|
||||
# This widgets are not enabled if it's a remote controller (no access to the local file system)
|
||||
self.disableWhenRemoteContollerWidgets = [
|
||||
# self.uiImportExportConfigsAction
|
||||
]
|
||||
|
||||
# load initial stuff once the event loop isn't busy
|
||||
self.run_later(0, self.startupLoading)
|
||||
|
||||
@@ -237,9 +233,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiShowGridAction.triggered.connect(self._showGridActionSlot)
|
||||
self.uiSnapToGridAction.triggered.connect(self._snapToGridActionSlot)
|
||||
self.uiLockAllAction.triggered.connect(self._lockActionSlot)
|
||||
self.uiResetGUIStateAction.triggered.connect(self._resetGUIState)
|
||||
self.uiResetDocksAction.triggered.connect(self._resetDocksSlot)
|
||||
|
||||
# tool menu connections
|
||||
self.uiWebInterfaceAction.triggered.connect(self._openLightWebInterfaceActionSlot)
|
||||
self.uiWebUIAction.triggered.connect(self._openWebInterfaceActionSlot)
|
||||
|
||||
# control menu connections
|
||||
@@ -249,6 +246,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiReloadAllAction.triggered.connect(self._reloadAllActionSlot)
|
||||
self.uiAuxConsoleAllAction.triggered.connect(self._auxConsoleAllActionSlot)
|
||||
self.uiConsoleAllAction.triggered.connect(self._consoleAllActionSlot)
|
||||
self.uiResetConsoleAllAction.triggered.connect(self._consoleResetAllActionSlot)
|
||||
|
||||
# device menu is contextual and is build on-the-fly
|
||||
self.uiDeviceMenu.aboutToShow.connect(self._deviceMenuActionSlot)
|
||||
@@ -322,14 +320,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
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():
|
||||
base_url = Controller.instance().httpClient().fullUrl()
|
||||
webui_url = "{}/static/web-ui/local".format(base_url)
|
||||
webui_url = "{}/static/web-ui/bundled".format(base_url)
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(webui_url))
|
||||
|
||||
def _showGridActionSlot(self):
|
||||
@@ -363,22 +357,38 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
:return: None
|
||||
"""
|
||||
|
||||
for item in self.uiGraphicsView.items():
|
||||
if not isinstance(item, LinkItem) and not isinstance(item, LabelItem):
|
||||
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()
|
||||
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):
|
||||
def _resetGUIState(self):
|
||||
"""
|
||||
Return the analytics client
|
||||
Reset the GUI state.
|
||||
"""
|
||||
|
||||
return self._analytics_client
|
||||
self._save_gui_state_geometry = False
|
||||
self.close()
|
||||
if hasattr(sys, "frozen"):
|
||||
QtCore.QProcess.startDetached(os.path.abspath(sys.executable), sys.argv)
|
||||
else:
|
||||
QtWidgets.QMessageBox.information(self, "GUI state","The GUI state has been reset, please restart the application")
|
||||
|
||||
def _resetDocksSlot(self):
|
||||
"""
|
||||
Reset the dock widgets.
|
||||
"""
|
||||
|
||||
self.uiTopologySummaryDockWidget.setFloating(False)
|
||||
self.uiComputeSummaryDockWidget.setFloating(False)
|
||||
self.uiConsoleDockWidget.setFloating(False)
|
||||
self.uiNodesDockWidget.setFloating(False)
|
||||
|
||||
def _newProjectActionSlot(self):
|
||||
"""
|
||||
@@ -392,9 +402,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
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())
|
||||
@@ -436,7 +443,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._newProjectActionSlot()
|
||||
else:
|
||||
directory = self._project_dir
|
||||
if self._project_dir and not os.path.exists(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)",
|
||||
@@ -545,8 +552,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Refresh widgets that should be visible or not
|
||||
"""
|
||||
for widget in self.disableWhenRemoteContollerWidgets:
|
||||
widget.setVisible(not Controller.instance().isRemote())
|
||||
|
||||
# No projects
|
||||
if Topology.instance().project() is None:
|
||||
@@ -815,6 +820,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Slot called when starting all the nodes.
|
||||
"""
|
||||
|
||||
reply = QtWidgets.QMessageBox.question(self, "Confirm Start All", "Are you sure you want to start all devices?",
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.start_all_nodes()
|
||||
@@ -824,6 +836,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Slot called when suspending all the nodes.
|
||||
"""
|
||||
|
||||
reply = QtWidgets.QMessageBox.question(self, "Confirm Suspend All", "Are you sure you want to suspend all devices?",
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.suspend_all_nodes()
|
||||
@@ -833,6 +851,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Slot called when stopping all the nodes.
|
||||
"""
|
||||
|
||||
reply = QtWidgets.QMessageBox.question(self, "Confirm Stop All", "Are you sure you want to stop all devices?",
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.stop_all_nodes()
|
||||
@@ -842,10 +866,25 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Slot called when reloading all the nodes.
|
||||
"""
|
||||
|
||||
reply = QtWidgets.QMessageBox.question(self, "Confirm Reload All", "Are you sure you want to reload all devices?",
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.reload_all_nodes()
|
||||
|
||||
def _consoleResetAllActionSlot(self):
|
||||
"""
|
||||
Slot called when reset all console connections.
|
||||
"""
|
||||
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.reset_console_all_nodes()
|
||||
|
||||
def _deviceMenuActionSlot(self):
|
||||
"""
|
||||
Slot to contextually show the device menu.
|
||||
@@ -916,7 +955,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Slot to launch a browser pointing to the documentation page.
|
||||
"""
|
||||
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://gns3.com/support/docs"))
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://docs.gns3.com/"))
|
||||
|
||||
def _checkForUpdateActionSlot(self, silent=False):
|
||||
"""
|
||||
@@ -1060,11 +1099,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
with Progress.instance().context(min_duration=0):
|
||||
dialog = PreferencesDialog(self)
|
||||
dialog.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["preferences_dialog_geometry"].encode()))
|
||||
#dialog.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["preferences_dialog_geometry"].encode()))
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
self._settings["preferences_dialog_geometry"] = bytes(dialog.saveGeometry().toBase64()).decode()
|
||||
self.setSettings(self._settings)
|
||||
#self._settings["preferences_dialog_geometry"] = bytes(dialog.saveGeometry().toBase64()).decode()
|
||||
#self.setSettings(self._settings)
|
||||
|
||||
def _editReadmeActionSlot(self):
|
||||
"""
|
||||
@@ -1088,6 +1127,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
if self.uiAddLinkAction.isChecked() and key == QtCore.Qt.Key_Escape:
|
||||
self.uiAddLinkAction.setChecked(False)
|
||||
self._addLinkActionSlot()
|
||||
elif key == QtCore.Qt.Key_C and event.modifiers() & QtCore.Qt.ControlModifier:
|
||||
status_bar_message = self.uiStatusBar.currentMessage()
|
||||
if status_bar_message:
|
||||
QtWidgets.QApplication.clipboard().setText(status_bar_message)
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
@@ -1110,8 +1153,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
progress.setCancelButtonText("Force quit")
|
||||
|
||||
log.debug("Close the Main Window")
|
||||
self._analytics_client.sendScreenView("Main Window", session_start=False)
|
||||
|
||||
self._finish_application_closing(close_windows=False)
|
||||
event.accept()
|
||||
self.uiConsoleTextEdit.closeIO()
|
||||
@@ -1126,10 +1167,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
log.debug("_finish_application_closing")
|
||||
|
||||
self._settings["geometry"] = bytes(self.saveGeometry().toBase64()).decode()
|
||||
self._settings["state"] = bytes(self.saveState().toBase64()).decode()
|
||||
if self._save_gui_state_geometry:
|
||||
self._settings["geometry"] = bytes(self.saveGeometry().toBase64()).decode()
|
||||
self._settings["state"] = bytes(self.saveState().toBase64()).decode()
|
||||
else:
|
||||
self._settings["geometry"] = ""
|
||||
self._settings["state"] = ""
|
||||
self.setSettings(self._settings)
|
||||
|
||||
Controller.instance().stopListenNotifications()
|
||||
server = LocalServer.instance()
|
||||
server.stopLocalServer(wait=True)
|
||||
|
||||
@@ -1189,8 +1235,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
# restore debug level
|
||||
if self._settings["debug_level"]:
|
||||
print("Activating debugging (use command 'debug 0' to deactivate)")
|
||||
root = logging.getLogger()
|
||||
root.addHandler(logging.StreamHandler(sys.stdout))
|
||||
root.setLevel(logging.DEBUG)
|
||||
|
||||
# restore the style
|
||||
self._setStyle(self._settings.get("style"))
|
||||
@@ -1198,7 +1245,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Controller.instance().connected_signal.connect(self._controllerConnectedSlot)
|
||||
Controller.instance().project_list_updated_signal.connect(self.updateRecentProjectActions)
|
||||
|
||||
self._analytics_client.sendScreenView("Main Window")
|
||||
self.uiGraphicsView.setEnabled(False)
|
||||
|
||||
# show the setup wizard
|
||||
|
||||
@@ -38,7 +38,7 @@ class EthernetSwitch(Node):
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"ports_mapping": [], "console_type": "telnet"})
|
||||
self.settings().update({"ports_mapping": [], "console_type": "none"})
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
|
||||
@@ -73,7 +73,7 @@ class Nat(Node):
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/cloud.svg"
|
||||
return ":/symbols/nat.svg"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
|
||||
@@ -52,6 +52,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
self.uiEditCloudNodePushButton.clicked.connect(self._editCloudNodeSlot)
|
||||
self.uiDeleteCloudNodePushButton.clicked.connect(self._deleteCloudNodeSlot)
|
||||
self.uiCloudNodesTreeWidget.itemSelectionChanged.connect(self._cloudNodeChangedSlot)
|
||||
self.uiCloudNodesTreeWidget.itemDoubleClicked.connect(self._editCloudNodeSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -53,6 +53,8 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
self.uiEditEthernetHubPushButton.clicked.connect(self._editEthernetHubSlot)
|
||||
self.uiDeleteEthernetHubPushButton.clicked.connect(self._deleteEthernetHubSlot)
|
||||
self.uiEthernetHubsTreeWidget.itemSelectionChanged.connect(self._ethernetHubChangedSlot)
|
||||
self.uiEthernetHubsTreeWidget.itemDoubleClicked.connect(self._editEthernetHubSlot)
|
||||
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -53,6 +53,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
self.uiEditEthernetSwitchPushButton.clicked.connect(self._editEthernetSwitchSlot)
|
||||
self.uiDeleteEthernetSwitchPushButton.clicked.connect(self._deleteEthernetSwitchSlot)
|
||||
self.uiEthernetSwitchesTreeWidget.itemSelectionChanged.connect(self._ethernetSwitchChangedSlot)
|
||||
self.uiEthernetSwitchesTreeWidget.itemDoubleClicked.connect(self._editEthernetSwitchSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -59,7 +59,7 @@ ETHERNET_SWITCH_SETTINGS = {
|
||||
"default_name_format": "Switch{0}",
|
||||
"symbol": ":/symbols/ethernet_switch.svg",
|
||||
"category": Node.switches,
|
||||
"console_type": "telnet",
|
||||
"console_type": "none",
|
||||
"ports_mapping": [],
|
||||
"node_type": "ethernet_switch"
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ class DockerVM(Node):
|
||||
"console_resolution": DOCKER_CONTAINER_SETTINGS["console_resolution"],
|
||||
"console_http_port": DOCKER_CONTAINER_SETTINGS["console_http_port"],
|
||||
"console_http_path": DOCKER_CONTAINER_SETTINGS["console_http_path"],
|
||||
"extra_hosts": DOCKER_CONTAINER_SETTINGS["extra_hosts"]}
|
||||
"extra_hosts": DOCKER_CONTAINER_SETTINGS["extra_hosts"],
|
||||
"extra_volumes": DOCKER_CONTAINER_SETTINGS["extra_volumes"]}
|
||||
|
||||
self.settings().update(docker_vm_settings)
|
||||
|
||||
@@ -115,6 +116,18 @@ class DockerVM(Node):
|
||||
from .pages.docker_vm_configuration_page import DockerVMConfigurationPage
|
||||
return DockerVMConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def validateHostname(hostname):
|
||||
"""
|
||||
Checks if the hostname is valid.
|
||||
|
||||
:param hostname: hostname to check
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return DockerVM.isValidRfc1123Hostname(hostname)
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
|
||||
@@ -103,7 +103,8 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
|
||||
self.uiConsoleResolutionComboBox.setCurrentIndex(self.uiConsoleResolutionComboBox.findText(settings["console_resolution"]))
|
||||
self.uiConsoleHttpPortSpinBox.setValue(settings["console_http_port"])
|
||||
self.uiHttpConsolePathLineEdit.setText(settings["console_http_path"])
|
||||
self.uiExtraHostsTextEdit.setText(settings["extra_hosts"])
|
||||
self.uiExtraHostsTextEdit.setPlainText(settings["extra_hosts"])
|
||||
self.uiExtraVolumeTextEdit.setPlainText("\n".join(settings["extra_volumes"]))
|
||||
|
||||
if not group:
|
||||
self.uiNameLineEdit.setText(settings["name"])
|
||||
@@ -175,6 +176,8 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
|
||||
settings["console_http_port"] = self.uiConsoleHttpPortSpinBox.value()
|
||||
settings["console_http_path"] = self.uiHttpConsolePathLineEdit.text()
|
||||
settings["extra_hosts"] = self.uiExtraHostsTextEdit.toPlainText()
|
||||
# only tidy input here, validation is performed server side
|
||||
settings["extra_volumes"] = [ y for x in self.uiExtraVolumeTextEdit.toPlainText().split("\n") for y in [ x.strip() ] if y ]
|
||||
|
||||
if not group:
|
||||
adapters = self.uiAdapterSpinBox.value()
|
||||
|
||||
@@ -52,6 +52,7 @@ class DockerVMPreferencesPage(QtWidgets.QWidget, Ui_DockerVMPreferencesPageWidge
|
||||
self.uiEditDockerVMPushButton.clicked.connect(self._dockerImageEditSlot)
|
||||
self.uiDeleteDockerVMPushButton.clicked.connect(self._dockerImageDeleteSlot)
|
||||
self.uiDockerVMsTreeWidget.itemSelectionChanged.connect(self._dockerImageChangedSlot)
|
||||
self.uiDockerVMsTreeWidget.itemDoubleClicked.connect(self._dockerImageEditSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
@@ -95,6 +96,9 @@ class DockerVMPreferencesPage(QtWidgets.QWidget, Ui_DockerVMPreferencesPageWidge
|
||||
if docker_container["extra_hosts"]:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Extra hosts:", str(docker_container["extra_hosts"])])
|
||||
|
||||
if docker_container["extra_volumes"]:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Extra volumes:", "\n".join(docker_container["extra_volumes"])])
|
||||
|
||||
self.uiDockerVMInfoTreeWidget.expandAll()
|
||||
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(0)
|
||||
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(1)
|
||||
|
||||
@@ -43,5 +43,6 @@ DOCKER_CONTAINER_SETTINGS = {
|
||||
"console_http_port": 80,
|
||||
"console_http_path": "/",
|
||||
"extra_hosts": "",
|
||||
"extra_volumes": [],
|
||||
"node_type": "docker"
|
||||
}
|
||||
|
||||
@@ -175,11 +175,26 @@
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QComboBox" name="uiConsoleResolutionComboBox">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>2560x1440</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1920x1080</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1680x1050</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1440x900</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1366x768</string>
|
||||
@@ -297,9 +312,34 @@ one per line)</string>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QTextEdit" name="uiExtraHostsTextEdit"/>
|
||||
<widget class="QPlainTextEdit" name="uiExtraHostsTextEdit">
|
||||
<property name="placeholderText">
|
||||
<string>e.g. router:192.168.0.1</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiExtraVolumeLabel">
|
||||
<property name="text">
|
||||
<string>Additional directories to
|
||||
make persistent that are
|
||||
not included in the image
|
||||
VOLUMES config. One
|
||||
directory per line.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPlainTextEdit" name="uiExtraVolumeTextEdit">
|
||||
<property name="plainText">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>e.g. /etc/sysctl.d</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/docker/ui/docker_vm_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.11.3
|
||||
# Created by: PyQt5 UI code generator 5.15.7
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_dockerVMConfigPageWidget(object):
|
||||
def setupUi(self, dockerVMConfigPageWidget):
|
||||
dockerVMConfigPageWidget.setObjectName("dockerVMConfigPageWidget")
|
||||
@@ -100,6 +103,9 @@ class Ui_dockerVMConfigPageWidget(object):
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.gridLayout.addWidget(self.uiConsoleResolutionComboBox, 8, 1, 1, 1)
|
||||
self.label = QtWidgets.QLabel(self.tab)
|
||||
self.label.setObjectName("label")
|
||||
@@ -143,11 +149,18 @@ class Ui_dockerVMConfigPageWidget(object):
|
||||
self.uiExtraHostsLabel.setWordWrap(True)
|
||||
self.uiExtraHostsLabel.setObjectName("uiExtraHostsLabel")
|
||||
self.gridLayout_2.addWidget(self.uiExtraHostsLabel, 0, 0, 1, 1)
|
||||
self.uiExtraHostsTextEdit = QtWidgets.QTextEdit(self.tab_2)
|
||||
self.uiExtraHostsTextEdit = QtWidgets.QPlainTextEdit(self.tab_2)
|
||||
self.uiExtraHostsTextEdit.setObjectName("uiExtraHostsTextEdit")
|
||||
self.gridLayout_2.addWidget(self.uiExtraHostsTextEdit, 0, 1, 1, 1)
|
||||
self.uiExtraVolumeLabel = QtWidgets.QLabel(self.tab_2)
|
||||
self.uiExtraVolumeLabel.setObjectName("uiExtraVolumeLabel")
|
||||
self.gridLayout_2.addWidget(self.uiExtraVolumeLabel, 1, 0, 1, 1)
|
||||
self.uiExtraVolumeTextEdit = QtWidgets.QPlainTextEdit(self.tab_2)
|
||||
self.uiExtraVolumeTextEdit.setPlainText("")
|
||||
self.uiExtraVolumeTextEdit.setObjectName("uiExtraVolumeTextEdit")
|
||||
self.gridLayout_2.addWidget(self.uiExtraVolumeTextEdit, 1, 1, 1, 1)
|
||||
spacerItem = QtWidgets.QSpacerItem(20, 388, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout_2.addItem(spacerItem, 1, 1, 1, 1)
|
||||
self.gridLayout_2.addItem(spacerItem, 2, 1, 1, 1)
|
||||
self.uiTabWidget.addTab(self.tab_2, "")
|
||||
self.tab_3 = QtWidgets.QWidget()
|
||||
self.tab_3.setObjectName("tab_3")
|
||||
@@ -183,13 +196,16 @@ class Ui_dockerVMConfigPageWidget(object):
|
||||
self.uiConsoleTypeComboBox.setItemText(4, _translate("dockerVMConfigPageWidget", "none"))
|
||||
self.uiConsoleAutoStartCheckBox.setText(_translate("dockerVMConfigPageWidget", "Auto start console"))
|
||||
self.uiConsoleResolutionLabel.setText(_translate("dockerVMConfigPageWidget", "VNC console resolution:"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(0, _translate("dockerVMConfigPageWidget", "1920x1080"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(1, _translate("dockerVMConfigPageWidget", "1366x768"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(2, _translate("dockerVMConfigPageWidget", "1280x1024"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(3, _translate("dockerVMConfigPageWidget", "1280x800"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(4, _translate("dockerVMConfigPageWidget", "1024x768"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(5, _translate("dockerVMConfigPageWidget", "800x600"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(6, _translate("dockerVMConfigPageWidget", "640x480"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(0, _translate("dockerVMConfigPageWidget", "2560x1440"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(1, _translate("dockerVMConfigPageWidget", "1920x1080"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(2, _translate("dockerVMConfigPageWidget", "1680x1050"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(3, _translate("dockerVMConfigPageWidget", "1440x900"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(4, _translate("dockerVMConfigPageWidget", "1366x768"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(5, _translate("dockerVMConfigPageWidget", "1280x1024"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(6, _translate("dockerVMConfigPageWidget", "1280x800"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(7, _translate("dockerVMConfigPageWidget", "1024x768"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(8, _translate("dockerVMConfigPageWidget", "800x600"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(9, _translate("dockerVMConfigPageWidget", "640x480"))
|
||||
self.label.setText(_translate("dockerVMConfigPageWidget", "HTTP port in the container:"))
|
||||
self.label_2.setText(_translate("dockerVMConfigPageWidget", "HTTP path:"))
|
||||
self.uiEnvironmentLabel.setText(_translate("dockerVMConfigPageWidget", "Environment variables:\n"
|
||||
@@ -201,6 +217,12 @@ class Ui_dockerVMConfigPageWidget(object):
|
||||
"to the /etc/hosts file.\n"
|
||||
"(hostname:IP\n"
|
||||
"one per line)"))
|
||||
self.uiExtraHostsTextEdit.setPlaceholderText(_translate("dockerVMConfigPageWidget", "e.g. router:192.168.0.1"))
|
||||
self.uiExtraVolumeLabel.setText(_translate("dockerVMConfigPageWidget", "Additional directories to\n"
|
||||
"make persistent that are\n"
|
||||
"not included in the image\n"
|
||||
"VOLUMES config. One\n"
|
||||
"directory per line."))
|
||||
self.uiExtraVolumeTextEdit.setPlaceholderText(_translate("dockerVMConfigPageWidget", "e.g. /etc/sysctl.d"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab_2), _translate("dockerVMConfigPageWidget", "Advanced"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab_3), _translate("dockerVMConfigPageWidget", "Usage"))
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class Dynamips(Module):
|
||||
for router in self._settings.get("routers"):
|
||||
router_settings = IOS_ROUTER_SETTINGS.copy()
|
||||
router_settings.update(router)
|
||||
if not router_settings.get("chassis"):
|
||||
if router_settings.get("chassis"):
|
||||
del router_settings["chassis"]
|
||||
templates.append(Template(router_settings))
|
||||
TemplateManager.instance().updateList(templates)
|
||||
|
||||
@@ -123,7 +123,7 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
|
||||
|
||||
# try to guess the platform
|
||||
image = os.path.basename(self.uiIOSImageLineEdit.text())
|
||||
match = re.match(r"^(c[0-9]+)p?\\-\w+", image.lower())
|
||||
match = re.match(r"^(c[0-9]+)p?-\w+", image.lower())
|
||||
if not match:
|
||||
QtWidgets.QMessageBox.warning(self, "IOS image", "Could not detect the platform, make sure this is a valid IOS image!")
|
||||
return
|
||||
|
||||
@@ -310,8 +310,8 @@ class Router(Node):
|
||||
|
||||
# IOS names must start with a letter, end with a letter or digit, and
|
||||
# have as interior characters only letters, digits, and hyphens.
|
||||
# They must be 63 characters or fewer.
|
||||
if re.search(r"""^[\-\w]+$""", hostname) and len(hostname) <= 63:
|
||||
# They must be 63 characters or fewer (ARPANET rules).
|
||||
if re.search(r"""^(?!-|[0-9])[a-zA-Z0-9-]{1,63}(?<!-)$""", hostname):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
|
||||
|
||||
# try to guess the platform
|
||||
image = os.path.basename(path)
|
||||
match = re.match(r"^(c[0-9]+)\\-\w+", image)
|
||||
match = re.match(r"^(c[0-9]+)p?-\w+", image)
|
||||
if not match:
|
||||
QtWidgets.QMessageBox.warning(self, "IOS image", "Could not detect the platform, make sure this is a valid IOS image!")
|
||||
return
|
||||
|
||||
@@ -70,6 +70,7 @@ class IOSRouterPreferencesPage(QtWidgets.QWidget, Ui_IOSRouterPreferencesPageWid
|
||||
self.uiDeleteIOSRouterPushButton.clicked.connect(self._iosRouterDeleteSlot)
|
||||
self.uiIOSRoutersTreeWidget.itemSelectionChanged.connect(self._iosRouterChangedSlot)
|
||||
self.uiDecompressIOSPushButton.clicked.connect(self._decompressIOSSlot)
|
||||
self.uiIOSRoutersTreeWidget.itemDoubleClicked.connect(self._iosRouterEditSlot)
|
||||
|
||||
def _iosRouterChangedSlot(self):
|
||||
"""
|
||||
|
||||
@@ -131,8 +131,8 @@ class IOUDevice(Node):
|
||||
|
||||
# IOS names must start with a letter, end with a letter or digit, and
|
||||
# have as interior characters only letters, digits, and hyphens.
|
||||
# They must be 63 characters or fewer.
|
||||
if re.search(r"""^[\-\w]+$""", hostname) and len(hostname) <= 63:
|
||||
# They must be 63 characters or fewer (ARPANET rules).
|
||||
if re.search(r"""^(?!-|[0-9])[a-zA-Z0-9-]{1,63}(?<!-)$""", hostname):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
|
||||
self.uiPrivateConfigToolButton.hide()
|
||||
|
||||
# location of the base config templates
|
||||
# FIXME: this does not work
|
||||
self._base_iou_l2_config_template = get_resource(os.path.join("configs", "iou_l2_base_startup-config.txt"))
|
||||
self._base_iou_l3_config_template = get_resource(os.path.join("configs", "iou_l3_base_startup-config.txt"))
|
||||
self._default_configs_dir = LocalServer.instance().localServerSettings()["configs_path"]
|
||||
|
||||
@@ -59,6 +59,7 @@ class IOUDevicePreferencesPage(QtWidgets.QWidget, Ui_IOUDevicePreferencesPageWid
|
||||
self.uiEditIOUDevicePushButton.clicked.connect(self._iouDeviceEditSlot)
|
||||
self.uiDeleteIOUDevicePushButton.clicked.connect(self._iouDeviceDeleteSlot)
|
||||
self.uiIOUDevicesTreeWidget.itemSelectionChanged.connect(self._iouDeviceChangedSlot)
|
||||
self.uiIOUDevicesTreeWidget.itemDoubleClicked.connect(self._iouDeviceEditSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
@@ -210,7 +211,7 @@ class IOUDevicePreferencesPage(QtWidgets.QWidget, Ui_IOUDevicePreferencesPageWid
|
||||
del self._iou_devices[key]
|
||||
item.setText(0, iou_device["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(dialog.settings)
|
||||
self._refreshInfo(dialog.settings())
|
||||
|
||||
def _iouDeviceDeleteSlot(self):
|
||||
"""
|
||||
|
||||
@@ -98,7 +98,10 @@ class IOUPreferencesPage(QtWidgets.QWidget, Ui_IOUPreferencesPageWidget):
|
||||
:param settings: IOU settings
|
||||
"""
|
||||
|
||||
self.IOULicenceTextEdit.setPlainText(settings["iourc_content"])
|
||||
if settings["iourc_content"]:
|
||||
self.IOULicenceTextEdit.blockSignals(True)
|
||||
self.IOULicenceTextEdit.setPlainText(settings["iourc_content"])
|
||||
self.IOULicenceTextEdit.blockSignals(False)
|
||||
self.uiLicensecheckBox.setChecked(settings["license_check"])
|
||||
|
||||
def loadPreferences(self):
|
||||
@@ -106,7 +109,10 @@ class IOUPreferencesPage(QtWidgets.QWidget, Ui_IOUPreferencesPageWidget):
|
||||
Loads IOU preferences.
|
||||
"""
|
||||
|
||||
Controller.instance().get("/iou_license", self._getSettingsCallback)
|
||||
if Controller.instance().connected():
|
||||
Controller.instance().get("/iou_license", self._getSettingsCallback)
|
||||
else:
|
||||
log.error("Cannot load the IOU license in the preferences dialog: not connected to the controller")
|
||||
|
||||
@qslot
|
||||
def _getSettingsCallback(self, result, error=False, **kwargs):
|
||||
@@ -115,7 +121,7 @@ class IOUPreferencesPage(QtWidgets.QWidget, Ui_IOUPreferencesPageWidget):
|
||||
return
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while getting settings : {}".format(result["message"]))
|
||||
log.error("Error while getting the IOU license information: {}".format(result["message"]))
|
||||
return
|
||||
self._old_settings = copy.copy(result)
|
||||
self._populateWidgets(result)
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
<string>IOS on UNIX</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>IOU licence (iourc file):</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/iou/ui/iou_preferences_page.ui'
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/iou/ui/iou_preferences_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.7.1
|
||||
# Created by: PyQt5 UI code generator 5.9
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_IOUPreferencesPageWidget(object):
|
||||
|
||||
def setupUi(self, IOUPreferencesPageWidget):
|
||||
IOUPreferencesPageWidget.setObjectName("IOUPreferencesPageWidget")
|
||||
IOUPreferencesPageWidget.resize(490, 532)
|
||||
self.vboxlayout = QtWidgets.QVBoxLayout(IOUPreferencesPageWidget)
|
||||
self.vboxlayout.setObjectName("vboxlayout")
|
||||
self.label = QtWidgets.QLabel(IOUPreferencesPageWidget)
|
||||
self.label.setObjectName("label")
|
||||
self.vboxlayout.addWidget(self.label)
|
||||
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
|
||||
self.IOULicenceTextEdit = QtWidgets.QPlainTextEdit(IOUPreferencesPageWidget)
|
||||
@@ -47,7 +48,9 @@ class Ui_IOUPreferencesPageWidget(object):
|
||||
def retranslateUi(self, IOUPreferencesPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
IOUPreferencesPageWidget.setWindowTitle(_translate("IOUPreferencesPageWidget", "IOS on UNIX"))
|
||||
self.label.setText(_translate("IOUPreferencesPageWidget", "IOU licence (iourc file):"))
|
||||
self.IOULicenceTextEdit.setToolTip(_translate("IOUPreferencesPageWidget", "A license is required to run IOU. Copy & paste the content of your iourc file here or use the browse button to select a file. The license will be pushed to remote servers."))
|
||||
self.uiIOURCPathToolButton.setText(_translate("IOUPreferencesPageWidget", "&Browse..."))
|
||||
self.uiLicensecheckBox.setText(_translate("IOUPreferencesPageWidget", "Check for a valid IOU license key"))
|
||||
self.uiRestoreDefaultsPushButton.setText(_translate("IOUPreferencesPageWidget", "Restore defaults"))
|
||||
|
||||
|
||||
@@ -152,9 +152,14 @@ class Module(QtCore.QObject):
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
node_names_cannot_export = []
|
||||
for node in self._nodes:
|
||||
if hasattr(node, "initialized") and node.initialized():
|
||||
node.exportConfigsToDirectory(directory)
|
||||
if not node.exportConfigsToDirectory(directory):
|
||||
node_names_cannot_export.append(node.name())
|
||||
|
||||
if node_names_cannot_export:
|
||||
log.warning("Config export is not supported by the following nodes: {}".format(" ".join(node_names_cannot_export)))
|
||||
|
||||
def importConfigs(self, directory):
|
||||
"""
|
||||
|
||||
@@ -121,7 +121,7 @@ class Qemu(Module):
|
||||
:param options: Options for the image creation
|
||||
"""
|
||||
|
||||
Controller.instance().postCompute("/qemu/img", compute_id, callback, body=options)
|
||||
Controller.instance().postCompute("/qemu/img", compute_id, callback, timeout=None, body=options)
|
||||
|
||||
def updateDiskImage(self, compute_id, callback, options):
|
||||
"""
|
||||
|
||||
@@ -47,10 +47,13 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
|
||||
|
||||
# Mandatory fields
|
||||
self.uiNameWizardPage.registerField("vm_name*", self.uiNameLineEdit)
|
||||
self.uiDiskWizardPage.registerField("hda_disk_image*", self.uiHdaDiskImageLineEdit)
|
||||
self.uiInitrdKernelImageWizardPage.registerField("initrd*", self.uiInitrdImageLineEdit)
|
||||
self.uiInitrdKernelImageWizardPage.registerField("kernel_image*", self.uiKernelImageLineEdit)
|
||||
|
||||
# Fill image combo boxes
|
||||
self.addImageSelector(self.uiHdaDiskExistingImageRadioButton, self.uiHdaDiskImageListComboBox, self.uiHdaDiskImageLineEdit, self.uiHdaDiskImageToolButton, QemuVMConfigurationPage.getDiskImage, create_image_wizard=QemuImageWizard, create_button=self.uiHdaDiskImageCreateToolButton, image_suffix="-hda")
|
||||
self.addImageSelector(self.uiLinuxExistingImageRadioButton, self.uiInitrdImageListComboBox, self.uiInitrdImageLineEdit, self.uiInitrdImageToolButton, QemuVMConfigurationPage.getDiskImage)
|
||||
self.addImageSelector(self.uiLinuxExistingImageRadioButton, self.uiKernelImageListComboBox, self.uiKernelImageLineEdit, self.uiKernelImageToolButton, QemuVMConfigurationPage.getDiskImage)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
@@ -61,7 +64,11 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
|
||||
return False
|
||||
|
||||
if self.currentPage() == self.uiNameWizardPage:
|
||||
self.uiRamSpinBox.setValue(512)
|
||||
if self.uiLegacyASACheckBox.isChecked():
|
||||
QtWidgets.QMessageBox.warning(self, "Legacy ASA VM", "Running ASA (with initrd/kernel) is not recommended and will not work on Windows 10, please use ASAv instead")
|
||||
self.uiRamSpinBox.setValue(1024)
|
||||
else:
|
||||
self.uiRamSpinBox.setValue(256)
|
||||
|
||||
if self.currentPage() == self.uiBinaryMemoryWizardPage:
|
||||
if not self.uiQemuListComboBox.count():
|
||||
@@ -81,7 +88,7 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
|
||||
if self.uiLocalRadioButton.isChecked() and not ComputeManager.instance().localPlatform().startswith("linux"):
|
||||
QtWidgets.QMessageBox.warning(self, "QEMU on Windows or Mac", "The recommended way to run QEMU on Windows and OSX is to use the GNS3 VM")
|
||||
|
||||
if self.page(page_id) == self.uiDiskWizardPage:
|
||||
if self.page(page_id) in [self.uiDiskWizardPage, self.uiInitrdKernelImageWizardPage]:
|
||||
self.loadImagesList("/qemu/images")
|
||||
elif self.page(page_id) == self.uiBinaryMemoryWizardPage:
|
||||
try:
|
||||
@@ -109,7 +116,9 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
|
||||
|
||||
is_64bit = sys.maxsize > 2 ** 32
|
||||
if ComputeManager.instance().localPlatform().startswith("win") and self.uiLocalRadioButton.isChecked():
|
||||
if is_64bit:
|
||||
if self.uiLegacyASACheckBox.isChecked():
|
||||
search_string = r"qemu-0.13.0\qemu-system-i386w.exe"
|
||||
elif is_64bit:
|
||||
# default is qemu-system-x86_64w.exe on Windows 64-bit with a remote server
|
||||
search_string = "x86_64w.exe"
|
||||
else:
|
||||
@@ -143,9 +152,46 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
|
||||
"qemu_path": qemu_path,
|
||||
"compute_id": self._compute_id,
|
||||
"category": Node.end_devices,
|
||||
"hda_disk_image": self.uiHdaDiskImageLineEdit.text(),
|
||||
"console_type": console_type,
|
||||
"options": ""
|
||||
"console_type": console_type
|
||||
}
|
||||
|
||||
if self.uiHdaDiskImageLineEdit.text().strip():
|
||||
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text().strip()
|
||||
settings["hda_disk_interface"] = "ide"
|
||||
|
||||
if self.uiLegacyASACheckBox.isChecked():
|
||||
# special settings for legacy ASA VM
|
||||
settings["adapters"] = 4
|
||||
settings["initrd"] = self.uiInitrdImageLineEdit.text()
|
||||
settings["kernel_image"] = self.uiKernelImageLineEdit.text()
|
||||
settings["kernel_command_line"] = "ide_generic.probe_mask=0x01 ide_core.chs=0.0:980,16,32 auto nousb console=ttyS0,9600 bigphysarea=65536 ide1=noprobe no-hlt -net nic"
|
||||
settings["options"] = "-machine accel=tcg -icount auto"
|
||||
if not sys.platform.startswith("darwin"):
|
||||
settings["cpu_throttling"] = 80 # limit to 80% CPU usage
|
||||
settings["process_priority"] = "low"
|
||||
settings["symbol"] = ":/symbols/asa.svg"
|
||||
settings["category"] = Node.security_devices
|
||||
|
||||
if "options" not in settings:
|
||||
settings["options"] = ""
|
||||
if self._compute_id == "local" and (sys.platform.startswith("win") and qemu_path.endswith(r"qemu-0.11.0\qemu.exe")) or \
|
||||
(sys.platform.startswith("darwin") and "GNS3.app" in qemu_path):
|
||||
settings["options"] += " -vga none -vnc none"
|
||||
settings["legacy_networking"] = True
|
||||
settings["options"] = settings["options"].strip()
|
||||
|
||||
return settings
|
||||
|
||||
def nextId(self):
|
||||
"""
|
||||
Wizard rules!
|
||||
"""
|
||||
|
||||
current_id = self.currentId()
|
||||
if self.page(current_id) == self.uiDiskWizardPage:
|
||||
if self.uiLegacyASACheckBox.isChecked():
|
||||
return self.uiDiskWizardPage.nextId()
|
||||
return -1
|
||||
elif self.page(current_id) == self.uiInitrdKernelImageWizardPage:
|
||||
return -1
|
||||
return QtWidgets.QWizard.nextId(self)
|
||||
|
||||
@@ -78,7 +78,7 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
self.uiHdcDiskImageResizeToolButton.clicked.connect(self._hdcDiskImageResizeSlot)
|
||||
self.uiHddDiskImageResizeToolButton.clicked.connect(self._hddDiskImageResizeSlot)
|
||||
|
||||
disk_interfaces = ["ide", "sata", "scsi", "sd", "mtd", "floppy", "pflash", "virtio", "none"]
|
||||
disk_interfaces = ["ide", "sata", "nvme", "scsi", "sd", "mtd", "floppy", "pflash", "virtio", "none"]
|
||||
self.uiHdaDiskInterfaceComboBox.addItems(disk_interfaces)
|
||||
self.uiHdbDiskInterfaceComboBox.addItems(disk_interfaces)
|
||||
self.uiHdcDiskInterfaceComboBox.addItems(disk_interfaces)
|
||||
@@ -90,6 +90,7 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
self.uiActivateCPUThrottlingCheckBox.stateChanged.connect(self._cpuThrottlingChangedSlot)
|
||||
self.uiLegacyNetworkingCheckBox.stateChanged.connect(self._legacyNetworkingChangedSlot)
|
||||
self.uiCustomAdaptersConfigurationPushButton.clicked.connect(self._customAdaptersConfigurationSlot)
|
||||
self.uiCreateConfigDiskCheckBox.stateChanged.connect(self._createConfigDiskChangedSlot)
|
||||
|
||||
# add the categories
|
||||
for name, category in Node.defaultCategories().items():
|
||||
@@ -99,8 +100,14 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
for name, option_name in Node.onCloseOptions().items():
|
||||
self.uiOnCloseComboBox.addItem(name, option_name)
|
||||
|
||||
# Supported NIC models: e1000, e1000-82544gc, e1000-82545em, e1000e, i82550, i82551, i82557a, i82557b, i82557c, i82558a
|
||||
# i82558b, i82559a, i82559b, i82559c, i82559er, i82562, i82801, igb, ne2k_pci, pcnet, rocker, rtl8139, virtio-net-pci, vmxnet3
|
||||
# This list can be retrieved using "qemu-system-x86_64 -nic model=?" or "qemu-system-x86_64 -device help"
|
||||
self._legacy_devices = ("e1000", "i82551", "i82557b", "i82559er", "ne2k_pci", "pcnet", "rtl8139", "virtio")
|
||||
self._qemu_network_devices = OrderedDict([("e1000", "Intel Gigabit Ethernet"),
|
||||
("e1000-82544gc", "Intel 82544GC Gigabit Ethernet"),
|
||||
("e1000-82545em", "Intel 82545EM Gigabit Ethernet"),
|
||||
("e1000e", "Intel PCIe Gigabit Ethernet"),
|
||||
("i82550", "Intel i82550 Ethernet"),
|
||||
("i82551", "Intel i82551 Ethernet"),
|
||||
("i82557a", "Intel i82557A Ethernet"),
|
||||
@@ -114,8 +121,10 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
("i82559er", "Intel i82559ER Ethernet"),
|
||||
("i82562", "Intel i82562 Ethernet"),
|
||||
("i82801", "Intel i82801 Ethernet"),
|
||||
("igb", "Intel 82576 Gigabit Ethernet"),
|
||||
("ne2k_pci", "NE2000 Ethernet"),
|
||||
("pcnet", "AMD PCNet Ethernet"),
|
||||
("rocker", "Rocker L2 switch device"),
|
||||
("rtl8139", "Realtek 8139 Ethernet"),
|
||||
("virtio", "Legacy paravirtualized Network I/O"),
|
||||
("virtio-net-pci", "Paravirtualized Network I/O"),
|
||||
@@ -147,6 +156,9 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
for device_name, device_description in self._qemu_network_devices.items():
|
||||
if legacy_networking and device_name not in self._legacy_devices:
|
||||
continue
|
||||
# special case for virtio legacy networking
|
||||
if not legacy_networking and device_name == "virtio":
|
||||
continue
|
||||
self.uiAdapterTypesComboBox.addItem("{} ({})".format(device_description, device_name), device_name)
|
||||
|
||||
@staticmethod
|
||||
@@ -360,6 +372,19 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
else:
|
||||
self._refreshQemuNetworkDevices()
|
||||
|
||||
def _createConfigDiskChangedSlot(self, state):
|
||||
"""
|
||||
Slot to allow or not HDD disk to be configured based on the state of the config disk option.
|
||||
"""
|
||||
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
if state:
|
||||
self.uiHddDiskImageLabel.setText(_translate("QemuVMConfigPageWidget", "Startup-cfg:"))
|
||||
else:
|
||||
self.uiHddDiskImageLabel.setText(_translate("QemuVMConfigPageWidget", "Disk image:"))
|
||||
self.uiHddDiskImageCreateToolButton.setEnabled(not state)
|
||||
self.uiHddDiskImageResizeToolButton.setEnabled(not state)
|
||||
|
||||
def _customAdaptersConfigurationSlot(self):
|
||||
"""
|
||||
Slot to open the custom adapters configuration dialog
|
||||
@@ -391,11 +416,21 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
|
||||
try:
|
||||
ports = StandardPortNameFactory(adapters, first_port_name, port_name_format, port_segment_size)
|
||||
except (ValueError, KeyError):
|
||||
except (IndexError, ValueError, KeyError):
|
||||
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
|
||||
return
|
||||
|
||||
dialog = CustomAdaptersConfigurationDialog(ports, self._custom_adapters, default_adapter, self._qemu_network_devices, base_mac_address, parent=self)
|
||||
if self.uiLegacyNetworkingCheckBox.isChecked():
|
||||
network_devices = {}
|
||||
for nic, desc in self._qemu_network_devices.items():
|
||||
if nic in self._legacy_devices:
|
||||
network_devices[nic] = desc
|
||||
else:
|
||||
network_devices = self._qemu_network_devices.copy()
|
||||
# special case for virtio legacy networking
|
||||
network_devices.pop("virtio")
|
||||
|
||||
dialog = CustomAdaptersConfigurationDialog(ports, self._custom_adapters, default_adapter, network_devices, base_mac_address, parent=self)
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
@@ -439,6 +474,7 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
self.uiHdbDiskInterfaceComboBox.setCurrentIndex(self.uiHdbDiskInterfaceComboBox.findText(settings["hdb_disk_interface"]))
|
||||
self.uiHdcDiskInterfaceComboBox.setCurrentIndex(self.uiHdcDiskInterfaceComboBox.findText(settings["hdc_disk_interface"]))
|
||||
self.uiHddDiskInterfaceComboBox.setCurrentIndex(self.uiHddDiskInterfaceComboBox.findText(settings["hdd_disk_interface"]))
|
||||
self.uiCreateConfigDiskCheckBox.setChecked(settings["create_config_disk"])
|
||||
self.uiCdromImageLineEdit.setText(settings["cdrom_image"])
|
||||
self.uiBiosImageLineEdit.setText(settings["bios_image"])
|
||||
self.uiInitrdLineEdit.setText(settings["initrd"])
|
||||
@@ -511,6 +547,7 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
self._custom_adapters = settings["custom_adapters"].copy()
|
||||
|
||||
self.uiLegacyNetworkingCheckBox.setChecked(settings["legacy_networking"])
|
||||
self.uiReplicateNetworkConnectionStateCheckBox.setChecked(settings["replicate_network_connection_state"])
|
||||
|
||||
# load the MAC address setting
|
||||
self.uiMacAddrLineEdit.setInputMask("HH:HH:HH:HH:HH:HH;_")
|
||||
@@ -542,6 +579,8 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
self.uiProcessPriorityComboBox.setCurrentIndex(index)
|
||||
self.uiQemuOptionsLineEdit.setText(settings["options"])
|
||||
self.uiUsageTextEdit.setPlainText(settings["usage"])
|
||||
self.uiTPMCheckBox.setChecked(settings["tpm"])
|
||||
self.uiUEFICheckBox.setChecked(settings["uefi"])
|
||||
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
@@ -573,6 +612,7 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
settings["hdb_disk_interface"] = self.uiHdbDiskInterfaceComboBox.currentText()
|
||||
settings["hdc_disk_interface"] = self.uiHdcDiskInterfaceComboBox.currentText()
|
||||
settings["hdd_disk_interface"] = self.uiHddDiskInterfaceComboBox.currentText()
|
||||
settings["create_config_disk"] = self.uiCreateConfigDiskCheckBox.isChecked()
|
||||
settings["cdrom_image"] = self.uiCdromImageLineEdit.text().strip()
|
||||
settings["bios_image"] = self.uiBiosImageLineEdit.text().strip()
|
||||
settings["initrd"] = self.uiInitrdLineEdit.text().strip()
|
||||
@@ -642,6 +682,8 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
raise ConfigurationError()
|
||||
|
||||
settings["adapters"] = adapters
|
||||
settings["legacy_networking"] = self.uiLegacyNetworkingCheckBox.isChecked()
|
||||
settings["replicate_network_connection_state"] = self.uiReplicateNetworkConnectionStateCheckBox.isChecked()
|
||||
settings["custom_adapters"] = self._custom_adapters.copy()
|
||||
settings["on_close"] = self.uiOnCloseComboBox.itemData(self.uiOnCloseComboBox.currentIndex())
|
||||
settings["cpus"] = self.uiCPUSpinBox.value()
|
||||
@@ -653,4 +695,6 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
settings["process_priority"] = self.uiProcessPriorityComboBox.currentText().lower()
|
||||
settings["options"] = self.uiQemuOptionsLineEdit.text()
|
||||
settings["usage"] = self.uiUsageTextEdit.toPlainText()
|
||||
settings["tpm"] = self.uiTPMCheckBox.isChecked()
|
||||
settings["uefi"] = self.uiUEFICheckBox.isChecked()
|
||||
return settings
|
||||
|
||||
@@ -54,6 +54,7 @@ class QemuVMPreferencesPage(QtWidgets.QWidget, Ui_QemuVMPreferencesPageWidget):
|
||||
self.uiEditQemuVMPushButton.clicked.connect(self._qemuVMEditSlot)
|
||||
self.uiDeleteQemuVMPushButton.clicked.connect(self._qemuVMDeleteSlot)
|
||||
self.uiQemuVMsTreeWidget.itemSelectionChanged.connect(self._qemuVMChangedSlot)
|
||||
self.uiQemuVMsTreeWidget.itemDoubleClicked.connect(self._qemuVMEditSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
QEMU VM implementation.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from gns3.node import Node
|
||||
from .settings import QEMU_VM_SETTINGS
|
||||
|
||||
@@ -71,6 +73,10 @@ class QemuVM(Node):
|
||||
"adapter_type": QEMU_VM_SETTINGS["adapter_type"],
|
||||
"mac_address": QEMU_VM_SETTINGS["mac_address"],
|
||||
"legacy_networking": QEMU_VM_SETTINGS["legacy_networking"],
|
||||
"replicate_network_connection_state": QEMU_VM_SETTINGS["replicate_network_connection_state"],
|
||||
"tpm": QEMU_VM_SETTINGS["tpm"],
|
||||
"uefi": QEMU_VM_SETTINGS["uefi"],
|
||||
"create_config_disk": QEMU_VM_SETTINGS["create_config_disk"],
|
||||
"platform": QEMU_VM_SETTINGS["platform"],
|
||||
"on_close": QEMU_VM_SETTINGS["on_close"],
|
||||
"cpu_throttling": QEMU_VM_SETTINGS["cpu_throttling"],
|
||||
@@ -133,6 +139,24 @@ class QemuVM(Node):
|
||||
usage = "\n" + self._settings.get("usage")
|
||||
return info + port_info + usage
|
||||
|
||||
def configFiles(self):
|
||||
"""
|
||||
Name of the configuration files
|
||||
"""
|
||||
|
||||
if self._settings.get("create_config_disk"):
|
||||
return ["config.zip"]
|
||||
return None
|
||||
|
||||
def configTextFiles(self):
|
||||
"""
|
||||
Name of the configuration files, which are plain text files
|
||||
|
||||
:returns: List of configuration files, False if no files
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
@@ -143,6 +167,18 @@ class QemuVM(Node):
|
||||
from .pages.qemu_vm_configuration_page import QemuVMConfigurationPage
|
||||
return QemuVMConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def validateHostname(hostname):
|
||||
"""
|
||||
Checks if the hostname is valid.
|
||||
|
||||
:param hostname: hostname to check
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return QemuVM.isValidRfc1123Hostname(hostname)
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
|
||||
@@ -41,10 +41,10 @@ QEMU_VM_SETTINGS = {
|
||||
"hdb_disk_image": "",
|
||||
"hdc_disk_image": "",
|
||||
"hdd_disk_image": "",
|
||||
"hda_disk_interface": "ide",
|
||||
"hdb_disk_interface": "ide",
|
||||
"hdc_disk_interface": "ide",
|
||||
"hdd_disk_interface": "ide",
|
||||
"hda_disk_interface": "none",
|
||||
"hdb_disk_interface": "none",
|
||||
"hdc_disk_interface": "none",
|
||||
"hdd_disk_interface": "none",
|
||||
"cdrom_image": "",
|
||||
"bios_image": "",
|
||||
"boot_priority": "c",
|
||||
@@ -56,6 +56,10 @@ QEMU_VM_SETTINGS = {
|
||||
"adapter_type": "e1000",
|
||||
"mac_address": "",
|
||||
"legacy_networking": False,
|
||||
"replicate_network_connection_state": True,
|
||||
"tpm": False,
|
||||
"uefi": False,
|
||||
"create_config_disk": False,
|
||||
"on_close": "power_off",
|
||||
"platform": "",
|
||||
"cpu_throttling": 0,
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>941</width>
|
||||
<height>877</height>
|
||||
<width>478</width>
|
||||
<height>579</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -399,14 +399,21 @@
|
||||
<string>HDD (Secondary Slave)</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_9">
|
||||
<item row="0" column="0">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiCreateConfigDiskCheckBox">
|
||||
<property name="text">
|
||||
<string>Automatically create a config disk on HDD</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiHddDiskImageLabel">
|
||||
<property name="text">
|
||||
<string>Disk image:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" rowspan="2">
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="uiHddDiskImageLineEdit"/>
|
||||
@@ -437,7 +444,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0" rowspan="2">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="uiHddDiskInterfaceLabel">
|
||||
<property name="text">
|
||||
<string>Disk interface:</string>
|
||||
@@ -470,7 +477,16 @@
|
||||
<string>CD/DVD</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -526,6 +542,23 @@
|
||||
<string>Network</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="uiPortSegmentSizeLabel">
|
||||
<property name="text">
|
||||
<string>Segment size:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1" colspan="2">
|
||||
<widget class="QSpinBox" name="uiPortSegmentSizeSpinBox">
|
||||
<property name="maximum">
|
||||
<number>128</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiAdaptersLabel">
|
||||
<property name="text">
|
||||
@@ -560,13 +593,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="uiPortSegmentSizeLabel">
|
||||
<property name="text">
|
||||
<string>Segment size:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="uiMacAddrLabel">
|
||||
<property name="text">
|
||||
@@ -598,14 +624,14 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="3">
|
||||
<item row="8" column="0" colspan="3">
|
||||
<widget class="QCheckBox" name="uiLegacyNetworkingCheckBox">
|
||||
<property name="text">
|
||||
<string>Use the legacy networking mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="2">
|
||||
<item row="9" column="2">
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@@ -628,16 +654,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1" colspan="2">
|
||||
<widget class="QSpinBox" name="uiPortSegmentSizeSpinBox">
|
||||
<property name="maximum">
|
||||
<number>128</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QSpinBox" name="uiAdaptersSpinBox">
|
||||
<property name="sizePolicy">
|
||||
@@ -654,6 +670,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="3">
|
||||
<widget class="QCheckBox" name="uiReplicateNetworkConnectionStateCheckBox">
|
||||
<property name="text">
|
||||
<string>Replicate network connection states in Qemu</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="uiAdvancedSettingsTab">
|
||||
@@ -844,13 +867,6 @@
|
||||
<string>Additional settings</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiQemuOptionsLabel">
|
||||
<property name="text">
|
||||
<string>Options:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="uiQemuOptionsLineEdit">
|
||||
<property name="toolTip">
|
||||
@@ -860,12 +876,14 @@
|
||||
<li>%vm-id% =VM ID</li>
|
||||
<li>%project-id% = project ID</li>
|
||||
<li>%project-path% = project path</li>
|
||||
<li>%console-port% = console port number</li>
|
||||
<li>%guest-cid% = unique ID from 3 to 65535</li>
|
||||
</ul>
|
||||
</body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiBaseVMCheckBox">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
@@ -875,10 +893,33 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiTPMCheckBox">
|
||||
<property name="text">
|
||||
<string>Enable Trusted Platform Module (TPM)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiQemuOptionsLabel">
|
||||
<property name="text">
|
||||
<string>Options:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiUEFICheckBox">
|
||||
<property name="text">
|
||||
<string>Enable UEFI boot mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<zorder>uiQemuOptionsLineEdit</zorder>
|
||||
<zorder>uiQemuOptionsLabel</zorder>
|
||||
<zorder>uiBaseVMCheckBox</zorder>
|
||||
<zorder>uiTPMCheckBox</zorder>
|
||||
<zorder>uiUEFICheckBox</zorder>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.11.3
|
||||
# Created by: PyQt5 UI code generator 5.15.9
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_QemuVMConfigPageWidget(object):
|
||||
def setupUi(self, QemuVMConfigPageWidget):
|
||||
QemuVMConfigPageWidget.setObjectName("QemuVMConfigPageWidget")
|
||||
QemuVMConfigPageWidget.resize(941, 877)
|
||||
QemuVMConfigPageWidget.resize(478, 579)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(QemuVMConfigPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiQemutabWidget = QtWidgets.QTabWidget(QemuVMConfigPageWidget)
|
||||
@@ -209,9 +212,12 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiHddGroupBox.setObjectName("uiHddGroupBox")
|
||||
self.gridLayout_9 = QtWidgets.QGridLayout(self.uiHddGroupBox)
|
||||
self.gridLayout_9.setObjectName("gridLayout_9")
|
||||
self.uiCreateConfigDiskCheckBox = QtWidgets.QCheckBox(self.uiHddGroupBox)
|
||||
self.uiCreateConfigDiskCheckBox.setObjectName("uiCreateConfigDiskCheckBox")
|
||||
self.gridLayout_9.addWidget(self.uiCreateConfigDiskCheckBox, 0, 0, 1, 2)
|
||||
self.uiHddDiskImageLabel = QtWidgets.QLabel(self.uiHddGroupBox)
|
||||
self.uiHddDiskImageLabel.setObjectName("uiHddDiskImageLabel")
|
||||
self.gridLayout_9.addWidget(self.uiHddDiskImageLabel, 0, 0, 1, 1)
|
||||
self.gridLayout_9.addWidget(self.uiHddDiskImageLabel, 1, 0, 1, 1)
|
||||
self.horizontalLayout_10 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_10.setObjectName("horizontalLayout_10")
|
||||
self.uiHddDiskImageLineEdit = QtWidgets.QLineEdit(self.uiHddGroupBox)
|
||||
@@ -227,10 +233,10 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiHddDiskImageResizeToolButton = QtWidgets.QToolButton(self.uiHddGroupBox)
|
||||
self.uiHddDiskImageResizeToolButton.setObjectName("uiHddDiskImageResizeToolButton")
|
||||
self.horizontalLayout_10.addWidget(self.uiHddDiskImageResizeToolButton)
|
||||
self.gridLayout_9.addLayout(self.horizontalLayout_10, 0, 1, 2, 1)
|
||||
self.gridLayout_9.addLayout(self.horizontalLayout_10, 1, 1, 1, 1)
|
||||
self.uiHddDiskInterfaceLabel = QtWidgets.QLabel(self.uiHddGroupBox)
|
||||
self.uiHddDiskInterfaceLabel.setObjectName("uiHddDiskInterfaceLabel")
|
||||
self.gridLayout_9.addWidget(self.uiHddDiskInterfaceLabel, 1, 0, 2, 1)
|
||||
self.gridLayout_9.addWidget(self.uiHddDiskInterfaceLabel, 2, 0, 1, 1)
|
||||
self.uiHddDiskInterfaceComboBox = QtWidgets.QComboBox(self.uiHddGroupBox)
|
||||
self.uiHddDiskInterfaceComboBox.setObjectName("uiHddDiskInterfaceComboBox")
|
||||
self.gridLayout_9.addWidget(self.uiHddDiskInterfaceComboBox, 2, 1, 1, 1)
|
||||
@@ -268,6 +274,14 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiNetworkTab.setObjectName("uiNetworkTab")
|
||||
self.gridLayout_5 = QtWidgets.QGridLayout(self.uiNetworkTab)
|
||||
self.gridLayout_5.setObjectName("gridLayout_5")
|
||||
self.uiPortSegmentSizeLabel = QtWidgets.QLabel(self.uiNetworkTab)
|
||||
self.uiPortSegmentSizeLabel.setObjectName("uiPortSegmentSizeLabel")
|
||||
self.gridLayout_5.addWidget(self.uiPortSegmentSizeLabel, 3, 0, 1, 1)
|
||||
self.uiPortSegmentSizeSpinBox = QtWidgets.QSpinBox(self.uiNetworkTab)
|
||||
self.uiPortSegmentSizeSpinBox.setMaximum(128)
|
||||
self.uiPortSegmentSizeSpinBox.setSingleStep(4)
|
||||
self.uiPortSegmentSizeSpinBox.setObjectName("uiPortSegmentSizeSpinBox")
|
||||
self.gridLayout_5.addWidget(self.uiPortSegmentSizeSpinBox, 3, 1, 1, 2)
|
||||
self.uiAdaptersLabel = QtWidgets.QLabel(self.uiNetworkTab)
|
||||
self.uiAdaptersLabel.setObjectName("uiAdaptersLabel")
|
||||
self.gridLayout_5.addWidget(self.uiAdaptersLabel, 0, 0, 1, 1)
|
||||
@@ -284,9 +298,6 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiPortNameFormatLineEdit.setText("")
|
||||
self.uiPortNameFormatLineEdit.setObjectName("uiPortNameFormatLineEdit")
|
||||
self.gridLayout_5.addWidget(self.uiPortNameFormatLineEdit, 2, 1, 1, 2)
|
||||
self.uiPortSegmentSizeLabel = QtWidgets.QLabel(self.uiNetworkTab)
|
||||
self.uiPortSegmentSizeLabel.setObjectName("uiPortSegmentSizeLabel")
|
||||
self.gridLayout_5.addWidget(self.uiPortSegmentSizeLabel, 3, 0, 1, 1)
|
||||
self.uiMacAddrLabel = QtWidgets.QLabel(self.uiNetworkTab)
|
||||
self.uiMacAddrLabel.setObjectName("uiMacAddrLabel")
|
||||
self.gridLayout_5.addWidget(self.uiMacAddrLabel, 4, 0, 1, 1)
|
||||
@@ -304,9 +315,9 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.gridLayout_5.addWidget(self.uiCustomAdaptersConfigurationPushButton, 6, 1, 1, 2)
|
||||
self.uiLegacyNetworkingCheckBox = QtWidgets.QCheckBox(self.uiNetworkTab)
|
||||
self.uiLegacyNetworkingCheckBox.setObjectName("uiLegacyNetworkingCheckBox")
|
||||
self.gridLayout_5.addWidget(self.uiLegacyNetworkingCheckBox, 7, 0, 1, 3)
|
||||
self.gridLayout_5.addWidget(self.uiLegacyNetworkingCheckBox, 8, 0, 1, 3)
|
||||
spacerItem3 = QtWidgets.QSpacerItem(20, 261, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout_5.addItem(spacerItem3, 8, 2, 1, 1)
|
||||
self.gridLayout_5.addItem(spacerItem3, 9, 2, 1, 1)
|
||||
self.uiAdapterTypesComboBox = QtWidgets.QComboBox(self.uiNetworkTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
@@ -315,11 +326,6 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiAdapterTypesComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiAdapterTypesComboBox.setObjectName("uiAdapterTypesComboBox")
|
||||
self.gridLayout_5.addWidget(self.uiAdapterTypesComboBox, 5, 1, 1, 2)
|
||||
self.uiPortSegmentSizeSpinBox = QtWidgets.QSpinBox(self.uiNetworkTab)
|
||||
self.uiPortSegmentSizeSpinBox.setMaximum(128)
|
||||
self.uiPortSegmentSizeSpinBox.setSingleStep(4)
|
||||
self.uiPortSegmentSizeSpinBox.setObjectName("uiPortSegmentSizeSpinBox")
|
||||
self.gridLayout_5.addWidget(self.uiPortSegmentSizeSpinBox, 3, 1, 1, 2)
|
||||
self.uiAdaptersSpinBox = QtWidgets.QSpinBox(self.uiNetworkTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
@@ -330,6 +336,9 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiAdaptersSpinBox.setMaximum(275)
|
||||
self.uiAdaptersSpinBox.setObjectName("uiAdaptersSpinBox")
|
||||
self.gridLayout_5.addWidget(self.uiAdaptersSpinBox, 0, 1, 1, 2)
|
||||
self.uiReplicateNetworkConnectionStateCheckBox = QtWidgets.QCheckBox(self.uiNetworkTab)
|
||||
self.uiReplicateNetworkConnectionStateCheckBox.setObjectName("uiReplicateNetworkConnectionStateCheckBox")
|
||||
self.gridLayout_5.addWidget(self.uiReplicateNetworkConnectionStateCheckBox, 7, 0, 1, 3)
|
||||
self.uiQemutabWidget.addTab(self.uiNetworkTab, "")
|
||||
self.uiAdvancedSettingsTab = QtWidgets.QWidget()
|
||||
self.uiAdvancedSettingsTab.setObjectName("uiAdvancedSettingsTab")
|
||||
@@ -419,19 +428,27 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.groupBox.setObjectName("groupBox")
|
||||
self.gridLayout_3 = QtWidgets.QGridLayout(self.groupBox)
|
||||
self.gridLayout_3.setObjectName("gridLayout_3")
|
||||
self.uiQemuOptionsLabel = QtWidgets.QLabel(self.groupBox)
|
||||
self.uiQemuOptionsLabel.setObjectName("uiQemuOptionsLabel")
|
||||
self.gridLayout_3.addWidget(self.uiQemuOptionsLabel, 0, 0, 1, 1)
|
||||
self.uiQemuOptionsLineEdit = QtWidgets.QLineEdit(self.groupBox)
|
||||
self.uiQemuOptionsLineEdit.setObjectName("uiQemuOptionsLineEdit")
|
||||
self.gridLayout_3.addWidget(self.uiQemuOptionsLineEdit, 0, 1, 1, 1)
|
||||
self.uiBaseVMCheckBox = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.uiBaseVMCheckBox.setEnabled(True)
|
||||
self.uiBaseVMCheckBox.setObjectName("uiBaseVMCheckBox")
|
||||
self.gridLayout_3.addWidget(self.uiBaseVMCheckBox, 1, 0, 1, 2)
|
||||
self.gridLayout_3.addWidget(self.uiBaseVMCheckBox, 3, 0, 1, 2)
|
||||
self.uiTPMCheckBox = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.uiTPMCheckBox.setObjectName("uiTPMCheckBox")
|
||||
self.gridLayout_3.addWidget(self.uiTPMCheckBox, 1, 0, 1, 2)
|
||||
self.uiQemuOptionsLabel = QtWidgets.QLabel(self.groupBox)
|
||||
self.uiQemuOptionsLabel.setObjectName("uiQemuOptionsLabel")
|
||||
self.gridLayout_3.addWidget(self.uiQemuOptionsLabel, 0, 0, 1, 1)
|
||||
self.uiUEFICheckBox = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.uiUEFICheckBox.setObjectName("uiUEFICheckBox")
|
||||
self.gridLayout_3.addWidget(self.uiUEFICheckBox, 2, 0, 1, 2)
|
||||
self.uiQemuOptionsLineEdit.raise_()
|
||||
self.uiQemuOptionsLabel.raise_()
|
||||
self.uiBaseVMCheckBox.raise_()
|
||||
self.uiTPMCheckBox.raise_()
|
||||
self.uiUEFICheckBox.raise_()
|
||||
self.verticalLayout_2.addWidget(self.groupBox)
|
||||
spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout_2.addItem(spacerItem4)
|
||||
@@ -492,6 +509,7 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiHdcDiskImageResizeToolButton.setText(_translate("QemuVMConfigPageWidget", "Resize..."))
|
||||
self.uiHdcDiskInterfaceLabel.setText(_translate("QemuVMConfigPageWidget", "Disk interface:"))
|
||||
self.uiHddGroupBox.setTitle(_translate("QemuVMConfigPageWidget", "HDD (Secondary Slave)"))
|
||||
self.uiCreateConfigDiskCheckBox.setText(_translate("QemuVMConfigPageWidget", "Automatically create a config disk on HDD"))
|
||||
self.uiHddDiskImageLabel.setText(_translate("QemuVMConfigPageWidget", "Disk image:"))
|
||||
self.uiHddDiskImageToolButton.setText(_translate("QemuVMConfigPageWidget", "&Browse..."))
|
||||
self.uiHddDiskImageCreateToolButton.setText(_translate("QemuVMConfigPageWidget", "Create..."))
|
||||
@@ -502,16 +520,17 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiCdromImageLabel.setText(_translate("QemuVMConfigPageWidget", "Image:"))
|
||||
self.uiCdromImageToolButton.setText(_translate("QemuVMConfigPageWidget", "&Browse..."))
|
||||
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.uiCdromTab), _translate("QemuVMConfigPageWidget", "CD/DVD"))
|
||||
self.uiPortSegmentSizeLabel.setText(_translate("QemuVMConfigPageWidget", "Segment size:"))
|
||||
self.uiAdaptersLabel.setText(_translate("QemuVMConfigPageWidget", "Adapters:"))
|
||||
self.uiFirstPortNameLabel.setText(_translate("QemuVMConfigPageWidget", "First port name:"))
|
||||
self.uiPortNameFormatLabel.setToolTip(_translate("QemuVMConfigPageWidget", "<html><head/><body><p>{0} - the port number, from 0 to the number of adapters-1.</p><p>{1} - the segment number, from 0 to the number of segments-1.</p><p>{port0} - named alias for {0}.</p><p>{port1} - the port number, from 1 to the number of adapters.</p><p>{segment0} - named alias for {1}.</p><p>{segment1} - the segment number, from 1 to the number of segments.</p></body></html>"))
|
||||
self.uiPortNameFormatLabel.setText(_translate("QemuVMConfigPageWidget", "Name format:"))
|
||||
self.uiPortSegmentSizeLabel.setText(_translate("QemuVMConfigPageWidget", "Segment size:"))
|
||||
self.uiMacAddrLabel.setText(_translate("QemuVMConfigPageWidget", "Base MAC:"))
|
||||
self.uiAdapterTypesLabel.setText(_translate("QemuVMConfigPageWidget", "Type:"))
|
||||
self.uiCustomAdaptersLabel.setText(_translate("QemuVMConfigPageWidget", "Custom adapters:"))
|
||||
self.uiCustomAdaptersConfigurationPushButton.setText(_translate("QemuVMConfigPageWidget", "&Configure custom adapters"))
|
||||
self.uiLegacyNetworkingCheckBox.setText(_translate("QemuVMConfigPageWidget", "Use the legacy networking mode"))
|
||||
self.uiReplicateNetworkConnectionStateCheckBox.setText(_translate("QemuVMConfigPageWidget", "Replicate network connection states in Qemu"))
|
||||
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.uiNetworkTab), _translate("QemuVMConfigPageWidget", "Network"))
|
||||
self.uiLinuxBootGroupBox.setTitle(_translate("QemuVMConfigPageWidget", "Linux boot specific settings"))
|
||||
self.uiKernelCommandLineLabel.setText(_translate("QemuVMConfigPageWidget", "Kernel command line:"))
|
||||
@@ -534,16 +553,19 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiProcessPriorityComboBox.setItemText(4, _translate("QemuVMConfigPageWidget", "Low"))
|
||||
self.uiProcessPriorityComboBox.setItemText(5, _translate("QemuVMConfigPageWidget", "Very low"))
|
||||
self.groupBox.setTitle(_translate("QemuVMConfigPageWidget", "Additional settings"))
|
||||
self.uiQemuOptionsLabel.setText(_translate("QemuVMConfigPageWidget", "Options:"))
|
||||
self.uiQemuOptionsLineEdit.setToolTip(_translate("QemuVMConfigPageWidget", "<html><head/><body><p>Variable replacements:</p>\n"
|
||||
"<ul>\n"
|
||||
"<li>%vm-name% =VM name</li>\n"
|
||||
"<li>%vm-id% =VM ID</li>\n"
|
||||
"<li>%project-id% = project ID</li>\n"
|
||||
"<li>%project-path% = project path</li>\n"
|
||||
"<li>%console-port% = console port number</li>\n"
|
||||
"<li>%guest-cid% = unique ID from 3 to 65535</li>\n"
|
||||
"</ul>\n"
|
||||
"</body></html>"))
|
||||
self.uiBaseVMCheckBox.setText(_translate("QemuVMConfigPageWidget", "Use as a linked base VM"))
|
||||
self.uiTPMCheckBox.setText(_translate("QemuVMConfigPageWidget", "Enable Trusted Platform Module (TPM)"))
|
||||
self.uiQemuOptionsLabel.setText(_translate("QemuVMConfigPageWidget", "Options:"))
|
||||
self.uiUEFICheckBox.setText(_translate("QemuVMConfigPageWidget", "Enable UEFI boot mode"))
|
||||
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.uiAdvancedSettingsTab), _translate("QemuVMConfigPageWidget", "Advanced"))
|
||||
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.uiUsageTab), _translate("QemuVMConfigPageWidget", "Usage"))
|
||||
|
||||
|
||||
@@ -106,6 +106,13 @@
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="uiNameLineEdit"/>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiLegacyASACheckBox">
|
||||
<property name="text">
|
||||
<string>This is a legacy ASA VM</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWizardPage" name="uiBinaryMemoryWizardPage">
|
||||
@@ -305,6 +312,114 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWizardPage" name="uiInitrdKernelImageWizardPage">
|
||||
<property name="title">
|
||||
<string>Linux boot specific settings</string>
|
||||
</property>
|
||||
<property name="subTitle">
|
||||
<string>Please choose a initrd and a kernel image.</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="0" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="uiLinuxExistingImageRadioButton">
|
||||
<property name="text">
|
||||
<string>Existing image</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="uiNewImageRadioButton_4">
|
||||
<property name="text">
|
||||
<string>New Image</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<layout class="QFormLayout" name="formLayout_2">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_11">
|
||||
<item>
|
||||
<widget class="QComboBox" name="uiInitrdImageListComboBox"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="uiInitrdImageLineEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="uiInitrdImageToolButton">
|
||||
<property name="text">
|
||||
<string>&Browse...</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextOnly</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiKernelImageLabel">
|
||||
<property name="text">
|
||||
<string>Kernel image (vmlinuz):</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_14">
|
||||
<item>
|
||||
<widget class="QComboBox" name="uiKernelImageListComboBox"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="uiKernelImageLineEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="uiKernelImageToolButton">
|
||||
<property name="text">
|
||||
<string>&Browse...</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextOnly</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiInitrdLabel">
|
||||
<property name="text">
|
||||
<string>Initial RAM disk (initrd):</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>uiNameLineEdit</tabstop>
|
||||
|
||||
@@ -60,6 +60,9 @@ class Ui_QemuVMWizard(object):
|
||||
self.uiNameLineEdit = QtWidgets.QLineEdit(self.uiNameWizardPage)
|
||||
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
|
||||
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 1, 1, 1)
|
||||
self.uiLegacyASACheckBox = QtWidgets.QCheckBox(self.uiNameWizardPage)
|
||||
self.uiLegacyASACheckBox.setObjectName("uiLegacyASACheckBox")
|
||||
self.gridLayout.addWidget(self.uiLegacyASACheckBox, 1, 0, 1, 2)
|
||||
QemuVMWizard.addPage(self.uiNameWizardPage)
|
||||
self.uiBinaryMemoryWizardPage = QtWidgets.QWizardPage()
|
||||
self.uiBinaryMemoryWizardPage.setObjectName("uiBinaryMemoryWizardPage")
|
||||
@@ -146,6 +149,60 @@ class Ui_QemuVMWizard(object):
|
||||
self.horizontalLayout_8.addWidget(self.uiHdaDiskImageCreateToolButton)
|
||||
self.gridLayout_3.addLayout(self.horizontalLayout_8, 1, 0, 1, 1)
|
||||
QemuVMWizard.addPage(self.uiDiskWizardPage)
|
||||
self.uiInitrdKernelImageWizardPage = QtWidgets.QWizardPage()
|
||||
self.uiInitrdKernelImageWizardPage.setObjectName("uiInitrdKernelImageWizardPage")
|
||||
self.gridLayout_4 = QtWidgets.QGridLayout(self.uiInitrdKernelImageWizardPage)
|
||||
self.gridLayout_4.setObjectName("gridLayout_4")
|
||||
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
|
||||
self.uiLinuxExistingImageRadioButton = QtWidgets.QRadioButton(self.uiInitrdKernelImageWizardPage)
|
||||
self.uiLinuxExistingImageRadioButton.setChecked(True)
|
||||
self.uiLinuxExistingImageRadioButton.setObjectName("uiLinuxExistingImageRadioButton")
|
||||
self.horizontalLayout_7.addWidget(self.uiLinuxExistingImageRadioButton)
|
||||
self.uiNewImageRadioButton_4 = QtWidgets.QRadioButton(self.uiInitrdKernelImageWizardPage)
|
||||
self.uiNewImageRadioButton_4.setChecked(False)
|
||||
self.uiNewImageRadioButton_4.setObjectName("uiNewImageRadioButton_4")
|
||||
self.horizontalLayout_7.addWidget(self.uiNewImageRadioButton_4)
|
||||
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_7.addItem(spacerItem2)
|
||||
self.gridLayout_4.addLayout(self.horizontalLayout_7, 0, 0, 1, 1)
|
||||
self.formLayout_2 = QtWidgets.QFormLayout()
|
||||
self.formLayout_2.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)
|
||||
self.formLayout_2.setObjectName("formLayout_2")
|
||||
self.horizontalLayout_11 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_11.setObjectName("horizontalLayout_11")
|
||||
self.uiInitrdImageListComboBox = QtWidgets.QComboBox(self.uiInitrdKernelImageWizardPage)
|
||||
self.uiInitrdImageListComboBox.setObjectName("uiInitrdImageListComboBox")
|
||||
self.horizontalLayout_11.addWidget(self.uiInitrdImageListComboBox)
|
||||
self.uiInitrdImageLineEdit = QtWidgets.QLineEdit(self.uiInitrdKernelImageWizardPage)
|
||||
self.uiInitrdImageLineEdit.setObjectName("uiInitrdImageLineEdit")
|
||||
self.horizontalLayout_11.addWidget(self.uiInitrdImageLineEdit)
|
||||
self.uiInitrdImageToolButton = QtWidgets.QToolButton(self.uiInitrdKernelImageWizardPage)
|
||||
self.uiInitrdImageToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
|
||||
self.uiInitrdImageToolButton.setObjectName("uiInitrdImageToolButton")
|
||||
self.horizontalLayout_11.addWidget(self.uiInitrdImageToolButton)
|
||||
self.formLayout_2.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_11)
|
||||
self.uiKernelImageLabel = QtWidgets.QLabel(self.uiInitrdKernelImageWizardPage)
|
||||
self.uiKernelImageLabel.setObjectName("uiKernelImageLabel")
|
||||
self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.uiKernelImageLabel)
|
||||
self.horizontalLayout_14 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_14.setObjectName("horizontalLayout_14")
|
||||
self.uiKernelImageListComboBox = QtWidgets.QComboBox(self.uiInitrdKernelImageWizardPage)
|
||||
self.uiKernelImageListComboBox.setObjectName("uiKernelImageListComboBox")
|
||||
self.horizontalLayout_14.addWidget(self.uiKernelImageListComboBox)
|
||||
self.uiKernelImageLineEdit = QtWidgets.QLineEdit(self.uiInitrdKernelImageWizardPage)
|
||||
self.uiKernelImageLineEdit.setObjectName("uiKernelImageLineEdit")
|
||||
self.horizontalLayout_14.addWidget(self.uiKernelImageLineEdit)
|
||||
self.uiKernelImageToolButton = QtWidgets.QToolButton(self.uiInitrdKernelImageWizardPage)
|
||||
self.uiKernelImageToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
|
||||
self.uiKernelImageToolButton.setObjectName("uiKernelImageToolButton")
|
||||
self.horizontalLayout_14.addWidget(self.uiKernelImageToolButton)
|
||||
self.formLayout_2.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_14)
|
||||
self.uiInitrdLabel = QtWidgets.QLabel(self.uiInitrdKernelImageWizardPage)
|
||||
self.uiInitrdLabel.setObjectName("uiInitrdLabel")
|
||||
self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.uiInitrdLabel)
|
||||
self.gridLayout_4.addLayout(self.formLayout_2, 1, 0, 1, 1)
|
||||
QemuVMWizard.addPage(self.uiInitrdKernelImageWizardPage)
|
||||
|
||||
self.retranslateUi(QemuVMWizard)
|
||||
QtCore.QMetaObject.connectSlotsByName(QemuVMWizard)
|
||||
@@ -168,6 +225,7 @@ class Ui_QemuVMWizard(object):
|
||||
self.uiNameWizardPage.setTitle(_translate("QemuVMWizard", "QEMU VM name"))
|
||||
self.uiNameWizardPage.setSubTitle(_translate("QemuVMWizard", "Please choose a descriptive name for your new QEMU virtual machine."))
|
||||
self.uiNameLabel.setText(_translate("QemuVMWizard", "Name:"))
|
||||
self.uiLegacyASACheckBox.setText(_translate("QemuVMWizard", "This is a legacy ASA VM"))
|
||||
self.uiBinaryMemoryWizardPage.setTitle(_translate("QemuVMWizard", "QEMU binary and memory"))
|
||||
self.uiBinaryMemoryWizardPage.setSubTitle(_translate("QemuVMWizard", "Please check the Qemu binary is correctly set and the virtual machine has enough memory to work."))
|
||||
self.uiQemuListLabel.setText(_translate("QemuVMWizard", "Qemu binary:"))
|
||||
@@ -188,4 +246,12 @@ class Ui_QemuVMWizard(object):
|
||||
self.uiHdaDiskImageLabel.setText(_translate("QemuVMWizard", "Disk image (hda):"))
|
||||
self.uiHdaDiskImageToolButton.setText(_translate("QemuVMWizard", "&Browse..."))
|
||||
self.uiHdaDiskImageCreateToolButton.setText(_translate("QemuVMWizard", "&Create"))
|
||||
self.uiInitrdKernelImageWizardPage.setTitle(_translate("QemuVMWizard", "Linux boot specific settings"))
|
||||
self.uiInitrdKernelImageWizardPage.setSubTitle(_translate("QemuVMWizard", "Please choose a initrd and a kernel image."))
|
||||
self.uiLinuxExistingImageRadioButton.setText(_translate("QemuVMWizard", "Existing image"))
|
||||
self.uiNewImageRadioButton_4.setText(_translate("QemuVMWizard", "New Image"))
|
||||
self.uiInitrdImageToolButton.setText(_translate("QemuVMWizard", "&Browse..."))
|
||||
self.uiKernelImageLabel.setText(_translate("QemuVMWizard", "Kernel image (vmlinuz):"))
|
||||
self.uiKernelImageToolButton.setText(_translate("QemuVMWizard", "&Browse..."))
|
||||
self.uiInitrdLabel.setText(_translate("QemuVMWizard", "Initial RAM disk (initrd):"))
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class TraceNGNode(Node):
|
||||
self._last_destination = destination
|
||||
params = {"destination": destination}
|
||||
log.debug("{} is starting".format(self.name()))
|
||||
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, body=params, timeout=None, progressText="{} is starting".format(self.name()))
|
||||
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, body=params, timeout=None, showProgress=False)
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
|
||||
@@ -99,7 +99,7 @@ class VirtualBoxVMConfigurationPage(QtWidgets.QWidget, Ui_virtualBoxVMConfigPage
|
||||
|
||||
try:
|
||||
ports = StandardPortNameFactory(adapters, first_port_name, port_name_format, port_segment_size)
|
||||
except (ValueError, KeyError):
|
||||
except (IndexError, ValueError, KeyError):
|
||||
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
|
||||
return
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ class VirtualBoxVMPreferencesPage(QtWidgets.QWidget, Ui_VirtualBoxVMPreferencesP
|
||||
self.uiEditVirtualBoxVMPushButton.clicked.connect(self._vboxVMEditSlot)
|
||||
self.uiDeleteVirtualBoxVMPushButton.clicked.connect(self._vboxVMDeleteSlot)
|
||||
self.uiVirtualBoxVMsTreeWidget.itemSelectionChanged.connect(self._vboxVMChangedSlot)
|
||||
self.uiVirtualBoxVMsTreeWidget.itemDoubleClicked.connect(self._vboxVMEditSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -99,7 +99,7 @@ class VMwareVMConfigurationPage(QtWidgets.QWidget, Ui_VMwareVMConfigPageWidget):
|
||||
|
||||
try:
|
||||
ports = StandardPortNameFactory(adapters, first_port_name, port_name_format, port_segment_size)
|
||||
except (ValueError, KeyError):
|
||||
except (IndexError, ValueError, KeyError):
|
||||
QtWidgets.QMessageBox.critical(self, "Invalid format", "Invalid port name format")
|
||||
return
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ class VMwareVMPreferencesPage(QtWidgets.QWidget, Ui_VMwareVMPreferencesPageWidge
|
||||
self.uiEditVMwareVMPushButton.clicked.connect(self._vmwareVMEditSlot)
|
||||
self.uiDeleteVMwareVMPushButton.clicked.connect(self._vmwareVMDeleteSlot)
|
||||
self.uiVMwareVMsTreeWidget.itemSelectionChanged.connect(self._vmwareVMChangedSlot)
|
||||
self.uiVMwareVMsTreeWidget.itemDoubleClicked.connect(self._vmwareVMEditSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -53,6 +53,7 @@ class VPCSNodePreferencesPage(QtWidgets.QWidget, Ui_VPCSNodePageWidget):
|
||||
self.uiEditVPCSPushButton.clicked.connect(self._editVPCSSlot)
|
||||
self.uiDeleteVPCSPushButton.clicked.connect(self._deleteVPCSSlot)
|
||||
self.uiVPCSTreeWidget.itemSelectionChanged.connect(self._vpcsChangedSlot)
|
||||
self.uiVPCSTreeWidget.itemDoubleClicked.connect(self._editVPCSSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
87
gns3/node.py
87
gns3/node.py
@@ -17,6 +17,7 @@
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
from gns3.controller import Controller
|
||||
from gns3.ports.ethernet_port import EthernetPort
|
||||
@@ -199,6 +200,26 @@ class Node(BaseNode):
|
||||
|
||||
return self._always_on
|
||||
|
||||
def configFiles(self):
|
||||
"""
|
||||
Name of the configuration files
|
||||
|
||||
This method should be overridden in derived classes
|
||||
|
||||
:returns: List of configuration files, False if no files
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
def configTextFiles(self):
|
||||
"""
|
||||
Name of the configuration files, which are plain text files
|
||||
|
||||
:returns: List of configuration files, False if no files
|
||||
"""
|
||||
|
||||
return self.configFiles()
|
||||
|
||||
def get(self, path, *args, **kwargs):
|
||||
"""
|
||||
GET on current server / project
|
||||
@@ -223,7 +244,7 @@ class Node(BaseNode):
|
||||
return
|
||||
|
||||
log.debug("{} is starting".format(self.name()))
|
||||
self.post("/start", self._startCallback, timeout=None, progressText="{} is starting".format(self.name()))
|
||||
self.post("/start", self._startCallback, timeout=None, showProgress=False)
|
||||
|
||||
def _startCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -249,7 +270,7 @@ class Node(BaseNode):
|
||||
return
|
||||
|
||||
log.debug("{} is stopping".format(self.name()))
|
||||
self.post("/stop", self._stopCallback, timeout=None, progressText="{} is stopping".format(self.name()))
|
||||
self.post("/stop", self._stopCallback, timeout=None, showProgress=False)
|
||||
|
||||
def _stopCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -279,7 +300,7 @@ class Node(BaseNode):
|
||||
return
|
||||
|
||||
log.debug("{} is being suspended".format(self.name()))
|
||||
self.post("/suspend", self._suspendCallback, timeout=None, progressText="{} is suspending".format(self.name()))
|
||||
self.post("/suspend", self._suspendCallback, timeout=None, showProgress=False)
|
||||
|
||||
def _suspendCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -301,7 +322,7 @@ class Node(BaseNode):
|
||||
"""
|
||||
|
||||
log.debug("{} is being reloaded".format(self.name()))
|
||||
self.post("/reload", self._reloadCallback, timeout=None, progressText="{} is reloading".format(self.name()))
|
||||
self.post("/reload", self._reloadCallback, timeout=None, showProgress=False)
|
||||
|
||||
def _reloadCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -455,7 +476,7 @@ class Node(BaseNode):
|
||||
if not skip_controller:
|
||||
for link in self.links():
|
||||
link.setDeleting()
|
||||
self.controllerHttpDelete("/nodes/{node_id}".format(node_id=self._node_id), self._deleteCallback)
|
||||
self.controllerHttpDelete("/nodes/{node_id}".format(node_id=self._node_id), self._deleteCallback, showProgress=False)
|
||||
else:
|
||||
self.deleted_signal.emit()
|
||||
self._module.removeNode(self)
|
||||
@@ -485,7 +506,7 @@ class Node(BaseNode):
|
||||
"y": int(y),
|
||||
"z": int(z)}
|
||||
|
||||
self.post("/duplicate", self._duplicateCallback, body=params, timeout=None, progressText="{} is being duplicated".format(self.name()))
|
||||
self.post("/duplicate", self._duplicateCallback, body=params, timeout=None, showProgress=False)
|
||||
|
||||
def _duplicateCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -538,7 +559,7 @@ class Node(BaseNode):
|
||||
del result["properties"]
|
||||
|
||||
# Update common element of all nodes
|
||||
for key in ["x", "y", "z", "locked", "symbol", "label", "console_host", "console", "console_type", "console_auto_start", "custom_adapters"]:
|
||||
for key in ["x", "y", "z", "locked", "symbol", "label", "console_host", "console", "console_type", "console_auto_start", "custom_adapters", "first_port_name", "port_name_format", "port_segment_size"]:
|
||||
if key in result:
|
||||
self._settings[key] = result[key]
|
||||
|
||||
@@ -628,11 +649,12 @@ class Node(BaseNode):
|
||||
from .main_window import MainWindow
|
||||
general_settings = MainWindow.instance().settings()
|
||||
|
||||
if console_type != "telnet":
|
||||
if not console_type:
|
||||
console_type = self.consoleType()
|
||||
if console_type:
|
||||
if console_type == "vnc":
|
||||
return general_settings["vnc_console_command"]
|
||||
if console_type.startswith("spice"):
|
||||
elif console_type.startswith("spice"):
|
||||
return general_settings["spice_console_command"]
|
||||
return general_settings["telnet_console_command"]
|
||||
|
||||
@@ -641,7 +663,7 @@ class Node(BaseNode):
|
||||
Get the console type (serial, telnet or VNC)
|
||||
"""
|
||||
|
||||
console_type = "telnet"
|
||||
console_type = "none"
|
||||
if "console_type" in self.settings():
|
||||
return self.settings()["console_type"]
|
||||
return console_type
|
||||
@@ -654,7 +676,7 @@ class Node(BaseNode):
|
||||
"""
|
||||
|
||||
host = self.settings()["console_host"]
|
||||
if host is None or host == "::" or host == "0.0.0.0":
|
||||
if host is None or host == "::" or host == "0.0.0.0" or host == "0:0:0:0:0:0:0:0":
|
||||
host = Controller.instance().host()
|
||||
return host
|
||||
|
||||
@@ -667,7 +689,8 @@ class Node(BaseNode):
|
||||
return
|
||||
super().setStatus(status)
|
||||
if status == self.started and "console_auto_start" in self.settings() and self.settings()["console_auto_start"]:
|
||||
self.openConsole()
|
||||
# give the node some time to start before opening the console
|
||||
QtCore.QTimer.singleShot(1000, self.openConsole)
|
||||
|
||||
def openConsole(self, command=None, aux=False):
|
||||
"""
|
||||
@@ -704,10 +727,10 @@ class Node(BaseNode):
|
||||
nodeTelnetConsole(self, console_port, command)
|
||||
elif console_type == "vnc":
|
||||
from .vnc_console import vncConsole
|
||||
vncConsole(self.consoleHost(), console_port, command)
|
||||
vncConsole(self, console_port, command)
|
||||
elif console_type.startswith("spice"):
|
||||
from .spice_console import spiceConsole
|
||||
spiceConsole(self.consoleHost(), console_port, command)
|
||||
spiceConsole(self, console_port, command)
|
||||
elif console_type == "http" or console_type == "https":
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl("{console_type}://{host}:{port}{path}".format(console_type=console_type, host=self.consoleHost(), port=console_port, path=self.consoleHttpPath())))
|
||||
|
||||
@@ -765,7 +788,7 @@ class Node(BaseNode):
|
||||
with open(context["path"], "wb+") as f:
|
||||
f.write(raw_body)
|
||||
except OSError as e:
|
||||
log.erro("Can't write %s: %s", context["path"], str(e))
|
||||
log.error("Cannot export file '{}': {}".format(context["path"], e))
|
||||
|
||||
def exportConfigsToDirectory(self, directory):
|
||||
"""
|
||||
@@ -774,13 +797,14 @@ class Node(BaseNode):
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
if not hasattr(self, "configFiles"):
|
||||
return
|
||||
if not self.configFiles():
|
||||
return False
|
||||
for file in self.configFiles():
|
||||
self.get("/files/{file}".format(file=file),
|
||||
self._exportConfigsToDirectoryCallback,
|
||||
context={"directory": directory, "file": file},
|
||||
raw=True)
|
||||
return True
|
||||
|
||||
def _exportConfigsToDirectoryCallback(self, result, error=False, raw_body=None, context={}, **kwargs):
|
||||
"""
|
||||
@@ -812,7 +836,7 @@ class Node(BaseNode):
|
||||
:param directory: source directory path
|
||||
"""
|
||||
|
||||
if not hasattr(self, "configFiles"):
|
||||
if not self.configFiles():
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -842,6 +866,33 @@ class Node(BaseNode):
|
||||
if error and "message" in result:
|
||||
log.error("Error while import config: {}".format(result["message"]))
|
||||
|
||||
@staticmethod
|
||||
def isValidRfc1123Hostname(hostname):
|
||||
"""
|
||||
Validate a hostname according to RFC 1123
|
||||
|
||||
Each element of the hostname must be from 1 to 63 characters long
|
||||
and the entire hostname, including the dots, can be at most 253
|
||||
characters long. Valid characters for hostnames are ASCII
|
||||
letters from a to z, the digits from 0 to 9, and the hyphen (-).
|
||||
A hostname may not start with a hyphen.
|
||||
"""
|
||||
|
||||
if hostname[-1] == ".":
|
||||
hostname = hostname[:-1] # strip exactly one dot from the right, if present
|
||||
|
||||
if len(hostname) > 253:
|
||||
return False
|
||||
|
||||
labels = hostname.split(".")
|
||||
|
||||
# the TLD must be not all-numeric
|
||||
if re.match(r"[0-9]+$", labels[-1]):
|
||||
return False
|
||||
|
||||
allowed = re.compile(r"(?!-)[a-zA-Z0-9-]{1,63}(?<!-)$")
|
||||
return all(allowed.match(label) for label in labels)
|
||||
|
||||
@staticmethod
|
||||
def onCloseOptions():
|
||||
"""
|
||||
|
||||
@@ -142,19 +142,30 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
# when we're in docked mode, outside main window
|
||||
return self.window().parent()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
def contextMenuEvent(self, event):
|
||||
"""
|
||||
Handles all mouse press events.
|
||||
Handles all context menu events.
|
||||
|
||||
:param: QMouseEvent instance
|
||||
:param event: QContextMenuEvent instance
|
||||
"""
|
||||
|
||||
# Check that an item has been selected and right click
|
||||
if event.button() == QtCore.Qt.RightButton:
|
||||
self._showContextualMenu()
|
||||
event.accept()
|
||||
return
|
||||
super().mousePressEvent(event)
|
||||
self._showContextualMenu(event.globalPos())
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
"""
|
||||
Handles all mouse double click events.
|
||||
|
||||
:param event: QMouseEvent instance
|
||||
"""
|
||||
|
||||
item = self.itemAt(event.pos())
|
||||
if item:
|
||||
template = TemplateManager.instance().getTemplate(item.data(0, QtCore.Qt.UserRole))
|
||||
if template:
|
||||
configuration_page = TEMPLATE_TYPE_TO_CONFIGURATION_PAGE.get(template.template_type())
|
||||
if not template.builtin() and configuration_page:
|
||||
self._configurationSlot(template, configuration_page)
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""
|
||||
@@ -181,7 +192,7 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
drag.exec_(QtCore.Qt.CopyAction)
|
||||
event.accept()
|
||||
|
||||
def _showContextualMenu(self):
|
||||
def _showContextualMenu(self, pos):
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
refresh_action = QtWidgets.QAction("Refresh templates", menu)
|
||||
@@ -207,16 +218,16 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
delete_action.triggered.connect(qpartial(self._deleteSlot, template))
|
||||
menu.addAction(delete_action)
|
||||
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
menu.exec_(pos)
|
||||
|
||||
def _configurationSlot(self, template, configuration_page, source):
|
||||
def _configurationSlot(self, template, configuration_page, source=None):
|
||||
|
||||
dialog = ConfigurationDialog(template.name(), template.settings(), configuration_page(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
TemplateManager.instance().updateTemplate(template)
|
||||
|
||||
def _deleteSlot(self, template, source):
|
||||
def _deleteSlot(self, template, source=None):
|
||||
|
||||
reply = QtWidgets.QMessageBox.question(self, "Template", "Delete {} template?".format(template.name()),
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
|
||||
@@ -38,7 +38,7 @@ class PacketCapture:
|
||||
def __init__(self):
|
||||
self._tail_process = {}
|
||||
self._capture_reader_process = {}
|
||||
# Auto start the capture program for th link
|
||||
# Auto start the capture program for this link
|
||||
self._autostart = {}
|
||||
|
||||
Topology.instance().project_changed_signal.connect(self.killAllCapture)
|
||||
@@ -47,6 +47,7 @@ class PacketCapture:
|
||||
"""
|
||||
Kill all running captures (for example when change project)
|
||||
"""
|
||||
|
||||
for process in list(self._tail_process.values()):
|
||||
try:
|
||||
process.kill()
|
||||
@@ -89,12 +90,11 @@ class PacketCapture:
|
||||
|
||||
def _updatedLinkSlot(self, link_id):
|
||||
link = self.topology().getLink(link_id)
|
||||
|
||||
if link:
|
||||
if link.capturing():
|
||||
if self._autostart.get(link) and link not in self._tail_process:
|
||||
log.debug("Starting packet capture reader for link {}".format(link.link_id()))
|
||||
self.startPacketCaptureReader(link)
|
||||
log.debug("Has successfully started capturing packets on {} to {}".format(link.id(), link.capture_file_path()))
|
||||
else:
|
||||
self.stopPacketCaptureReader(link)
|
||||
|
||||
@@ -107,20 +107,25 @@ class PacketCapture:
|
||||
"""
|
||||
|
||||
link.stopCapture()
|
||||
log.debug("Has successfully stopped capturing packets on {}".format(link.id()))
|
||||
|
||||
def startPacketCaptureReader(self, link):
|
||||
"""
|
||||
Starts the packet capture reader.
|
||||
"""
|
||||
|
||||
self._startPacketCommand(link, self.settings()["packet_capture_reader_command"])
|
||||
|
||||
def stopPacketCaptureReader(self, link):
|
||||
"""
|
||||
Stop the packet capture reader
|
||||
"""
|
||||
if link in self._tail_process and self._tail_process[link].poll() is None:
|
||||
self._tail_process[link].kill()
|
||||
|
||||
if link in self._tail_process:
|
||||
log.debug("Stopping packet capture reader for link {}".format(link.link_id()))
|
||||
try:
|
||||
self._tail_process[link].kill()
|
||||
except (PermissionError, OSError):
|
||||
pass
|
||||
del self._tail_process[link]
|
||||
|
||||
def startPacketCaptureAnalyzer(self, link):
|
||||
@@ -141,13 +146,19 @@ class PacketCapture:
|
||||
|
||||
# PCAP capture file path
|
||||
command = command.replace("%c", '"' + capture_file_path + '"')
|
||||
command = command.replace("{pcap_file}", '"' + capture_file_path + '"')
|
||||
|
||||
# Add description
|
||||
# Add project name
|
||||
command = command.replace("%P", link.project().name())
|
||||
command = command.replace("{project}", link.project().name().replace('"', '\\"'))
|
||||
|
||||
# Add link description
|
||||
description = "{}[{}]->{}[{}]".format(link.sourceNode().name(),
|
||||
link.sourcePort().name(),
|
||||
link.destinationNode().name(),
|
||||
link.destinationPort().name())
|
||||
command = command.replace("%d", description)
|
||||
command = command.replace("{link_description}", description)
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
command = shlex.split(command)
|
||||
@@ -155,7 +166,7 @@ class PacketCapture:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Packet Capture Analyzer", "No packet capture analyzer program configured")
|
||||
return
|
||||
try:
|
||||
subprocess.Popen(command)
|
||||
subprocess.Popen(command, env=os.environ)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Packet Capture Analyzer", "Can't start packet capture analyzer program {}".format(str(e)))
|
||||
return
|
||||
@@ -183,14 +194,14 @@ class PacketCapture:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Packet capture", "Can't create packet capture file {}: {}".format(capture_file_path, str(e)))
|
||||
return
|
||||
|
||||
if link in self._tail_process and self._tail_process[link].poll() is None:
|
||||
if link in self._tail_process:
|
||||
try:
|
||||
self._tail_process[link].kill()
|
||||
except (PermissionError, OSError):
|
||||
# Sometimes we have condition on windows where the process is in the process to quit
|
||||
pass
|
||||
del self._tail_process[link]
|
||||
if link in self._capture_reader_process and self._capture_reader_process[link].poll() is None:
|
||||
if link in self._capture_reader_process:
|
||||
try:
|
||||
self._capture_reader_process[link].kill()
|
||||
except (PermissionError, OSError):
|
||||
@@ -199,13 +210,19 @@ class PacketCapture:
|
||||
|
||||
# PCAP capture file path
|
||||
command = command.replace("%c", '"' + capture_file_path + '"')
|
||||
command = command.replace("{pcap_file}", '"' + capture_file_path + '"')
|
||||
|
||||
# Add description
|
||||
# Add project name
|
||||
command = command.replace("%P", link.project().name())
|
||||
command = command.replace("{project}", link.project().name().replace('"', '\\"'))
|
||||
|
||||
# Add link description
|
||||
description = "{} {} to {} {}".format(link.sourceNode().name(),
|
||||
link.sourcePort().name(),
|
||||
link.destinationNode().name(),
|
||||
link.destinationPort().name())
|
||||
command = command.replace("%d", description)
|
||||
command = command.replace("{link_description}", description)
|
||||
|
||||
if "|" in command:
|
||||
# live traffic capture (using tail)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user