mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-06-07 19:16:26 +03:00
Compare commits
1015 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
535f3193db | ||
|
|
7fae1eac48 | ||
|
|
4cc2975e97 | ||
|
|
149d1ad590 | ||
|
|
abc900a764 | ||
|
|
14084192da | ||
|
|
2f6603070b | ||
|
|
c3aac9f0a6 | ||
|
|
afde80dab5 | ||
|
|
b7f68afbf1 | ||
|
|
08634f330c | ||
|
|
83e35e6aa0 | ||
|
|
dc3bfef038 | ||
|
|
c13bb77b08 | ||
|
|
fab621bb40 | ||
|
|
bb760cd861 | ||
|
|
59e7b3fd93 | ||
|
|
315870aa09 | ||
|
|
5ed17acd6b | ||
|
|
d0dde822dd | ||
|
|
e3023532ed | ||
|
|
2a4c229c9d | ||
|
|
7b77627db7 | ||
|
|
fd5f32bcc4 | ||
|
|
7e7eecfa3e | ||
|
|
8325339b58 | ||
|
|
b39185ceb3 | ||
|
|
d93100f3c3 | ||
|
|
1691558989 | ||
|
|
17cabba48f | ||
|
|
c9a2cc05ea | ||
|
|
f9ac5302ca | ||
|
|
7c8c8dfc20 | ||
|
|
6622bc7560 | ||
|
|
3df7bd99eb | ||
|
|
da1636af60 | ||
|
|
2378d7ff78 | ||
|
|
c2f357cc0e | ||
|
|
f2bd85d803 | ||
|
|
ec3a856c59 | ||
|
|
1a55487e7b | ||
|
|
0f0ac33345 | ||
|
|
d0b4e5045b | ||
|
|
393cff0343 | ||
|
|
8e56b59bb8 | ||
|
|
a939dfe37f | ||
|
|
9b7e06bd54 | ||
|
|
cc2f7920b8 | ||
|
|
66a69dd22d | ||
|
|
d3f7eaee1c | ||
|
|
3200f4cd28 | ||
|
|
fc04b30b7e | ||
|
|
716f65786d | ||
|
|
0c397ba0a8 | ||
|
|
bff8a09147 | ||
|
|
f65ea13c6f | ||
|
|
3975b09898 | ||
|
|
6525d3130c | ||
|
|
5d8ad83cbe | ||
|
|
13a819ee87 | ||
|
|
cd1beb5191 | ||
|
|
483259ba2c | ||
|
|
3d828c42de | ||
|
|
d70dbe82d7 | ||
|
|
243c5a0f82 | ||
|
|
f4470d8190 | ||
|
|
b435163a3c | ||
|
|
26f6315b69 | ||
|
|
b81a02fbb4 | ||
|
|
b19988784f | ||
|
|
5caf576f83 | ||
|
|
e61b132c93 | ||
|
|
41b826e9ac | ||
|
|
4079f19e25 | ||
|
|
04f108cbdf | ||
|
|
e222e3e7c2 | ||
|
|
cb687205a4 | ||
|
|
4ef19056bb | ||
|
|
8a4fb55cdf | ||
|
|
805b573370 | ||
|
|
3f87719d02 | ||
|
|
2a27bb560c | ||
|
|
73dbe68301 | ||
|
|
c2bd4c8984 | ||
|
|
3021cfb164 | ||
|
|
794f317f83 | ||
|
|
e357581587 | ||
|
|
e46b699fcb | ||
|
|
c426ea6e03 | ||
|
|
b0a1fdb65a | ||
|
|
4515620259 | ||
|
|
090caa967b | ||
|
|
e9ac774464 | ||
|
|
de2de45196 | ||
|
|
64dd0e0be3 | ||
|
|
6cd5438c89 | ||
|
|
df5a09b46d | ||
|
|
cba03a7539 | ||
|
|
069d02d908 | ||
|
|
a207ba61de | ||
|
|
496db6427d | ||
|
|
1bcf6699f0 | ||
|
|
fa12721c3c | ||
|
|
65ad43431c | ||
|
|
081544e778 | ||
|
|
a30daf03d4 | ||
|
|
f80af230af | ||
|
|
71125a4b58 | ||
|
|
c1dfaf13fb | ||
|
|
03a3081361 | ||
|
|
b5715e46d2 | ||
|
|
3aa9ed61c6 | ||
|
|
0b6cc588b6 | ||
|
|
ed4af4a8e7 | ||
|
|
c1f6c3ddeb | ||
|
|
d57652815e | ||
|
|
ea3191739b | ||
|
|
378b4f973b | ||
|
|
6ecbe59011 | ||
|
|
2fcaa1f6cc | ||
|
|
f0a7582fd2 | ||
|
|
2da076a501 | ||
|
|
f31cc4806f | ||
|
|
ae228988a5 | ||
|
|
185f0463d4 | ||
|
|
cf668e774b | ||
|
|
b933cdd950 | ||
|
|
f38d3bdb8e | ||
|
|
2c5ed9c884 | ||
|
|
f2e457de6c | ||
|
|
6b146fc7a7 | ||
|
|
25b44a2070 | ||
|
|
ac8ef05b06 | ||
|
|
663185d93e | ||
|
|
61ac6ed6e0 | ||
|
|
0fa51bcd36 | ||
|
|
f000425350 | ||
|
|
8df07808a9 | ||
|
|
7f169261d4 | ||
|
|
a86c728a99 | ||
|
|
11d56d1ea7 | ||
|
|
9c52cf0b0b | ||
|
|
cd96492dff | ||
|
|
43ab1deb24 | ||
|
|
646bf10017 | ||
|
|
6a23874054 | ||
|
|
b6fe18b975 | ||
|
|
84125fe463 | ||
|
|
12732715bd | ||
|
|
ece0b94ae8 | ||
|
|
84d0532039 | ||
|
|
ab68c1f1ab | ||
|
|
b360d8d931 | ||
|
|
f9681f2766 | ||
|
|
e8a09eef72 | ||
|
|
3290639e54 | ||
|
|
02d3275475 | ||
|
|
fd92049cda | ||
|
|
f48a5655ed | ||
|
|
22267b4123 | ||
|
|
b06230ef3d | ||
|
|
87b37e2839 | ||
|
|
0f3dd2e05d | ||
|
|
0495429df8 | ||
|
|
2e97f1e037 | ||
|
|
8133ba61a5 | ||
|
|
4cce5cd4ff | ||
|
|
1f1f95e3da | ||
|
|
7c9c66470d | ||
|
|
da5bead39c | ||
|
|
68a0c74a1f | ||
|
|
949d1a900a | ||
|
|
88c7a472b1 | ||
|
|
4b49501630 | ||
|
|
9eb71b870a | ||
|
|
5d46560427 | ||
|
|
d7856af6db | ||
|
|
c0a093d044 | ||
|
|
03c7df9dad | ||
|
|
d847316914 | ||
|
|
190e17b445 | ||
|
|
ce5d1b56f0 | ||
|
|
cb7cbc15b3 | ||
|
|
dc6756da04 | ||
|
|
e4eeb437eb | ||
|
|
81b94070ac | ||
|
|
de36a04d88 | ||
|
|
1287b77dfe | ||
|
|
146bb004c0 | ||
|
|
d26abecea7 | ||
|
|
d2da14a951 | ||
|
|
4ae17a1f66 | ||
|
|
473f60167d | ||
|
|
5377590520 | ||
|
|
58fe1f6e2b | ||
|
|
c0564c89c8 | ||
|
|
34d27dc120 | ||
|
|
d907af46bc | ||
|
|
a8055471cd | ||
|
|
317c28eaf2 | ||
|
|
d2b6aeddbd | ||
|
|
4285d01a19 | ||
|
|
802c59a1d1 | ||
|
|
f1879c8d4b | ||
|
|
97ad294623 | ||
|
|
6147dcd304 | ||
|
|
d900842363 | ||
|
|
e6ac92abb4 | ||
|
|
944b5fc6c9 | ||
|
|
bf6c645281 | ||
|
|
14f04e5f88 | ||
|
|
ecbf7bd661 | ||
|
|
de15edcd0a | ||
|
|
a5d619d6a8 | ||
|
|
271811d376 | ||
|
|
6ed9652a2a | ||
|
|
0d62811a17 | ||
|
|
615f1f2b5d | ||
|
|
721bff01b0 | ||
|
|
69f671106c | ||
|
|
0c59970974 | ||
|
|
d161a75ab2 | ||
|
|
b87f2b2952 | ||
|
|
a0b4c38a44 | ||
|
|
c9cc98ae39 | ||
|
|
ad492e2b90 | ||
|
|
a977042017 | ||
|
|
9e4b5ad02b | ||
|
|
90b9b2d29c | ||
|
|
54b6efce59 | ||
|
|
1f77a825b3 | ||
|
|
283d787c8d | ||
|
|
47be4d39c2 | ||
|
|
8a4fab9528 | ||
|
|
a31586fac9 | ||
|
|
3b88c72778 | ||
|
|
0e4a5da71a | ||
|
|
2a6327b2f2 | ||
|
|
d4d8adf4ac | ||
|
|
8492a31dd7 | ||
|
|
bb91402d2c | ||
|
|
35d8b4f848 | ||
|
|
073dc1afe7 | ||
|
|
b61e6dabd4 | ||
|
|
316fa688c3 | ||
|
|
0724387257 | ||
|
|
8e87c8cdbe | ||
|
|
9553d3ba25 | ||
|
|
4bccd6de25 | ||
|
|
dd5bafca0a | ||
|
|
6d3e28226a | ||
|
|
0fc040773a | ||
|
|
1f2294f9bf | ||
|
|
bed3f1d8fa | ||
|
|
b1da0b8279 | ||
|
|
32f3137f4d | ||
|
|
df10bca2c0 | ||
|
|
21fba1c4f7 | ||
|
|
f56e7e8dd8 | ||
|
|
41f6119118 | ||
|
|
7732f2a27e | ||
|
|
0be4f31162 | ||
|
|
6bc8428dd0 | ||
|
|
fd92e92a4f | ||
|
|
9dc7a4447b | ||
|
|
c2a597ffcf | ||
|
|
fc4850afab | ||
|
|
6c31de36ac | ||
|
|
4082aa8d77 | ||
|
|
71a835ff5f | ||
|
|
b156df6fc2 | ||
|
|
d077621ee9 | ||
|
|
3624502a23 | ||
|
|
c9f12fece7 | ||
|
|
0f43fd4560 | ||
|
|
1c85f980d3 | ||
|
|
49428d3ead | ||
|
|
2f48752ff2 | ||
|
|
48ac89abc9 | ||
|
|
bd8bad5e4c | ||
|
|
13c189fb00 | ||
|
|
eaab3c3f5e | ||
|
|
61fb8246f0 | ||
|
|
b09249b384 | ||
|
|
7d6b98766c | ||
|
|
96bcf55942 | ||
|
|
7bb6078b13 | ||
|
|
c8d6a4640a | ||
|
|
27be2b7a1d | ||
|
|
b05d682aa3 | ||
|
|
636b26b0e8 | ||
|
|
ae2a111536 | ||
|
|
33796a8bd3 | ||
|
|
695e5d3daa | ||
|
|
9d805d5d42 | ||
|
|
ec3fd63138 | ||
|
|
8e5e2d4a0c | ||
|
|
ee6e2b41f7 | ||
|
|
42c54ef02f | ||
|
|
227cbfc79a | ||
|
|
0fd5a1a91d | ||
|
|
4406c940b5 | ||
|
|
8eab44349f | ||
|
|
f50f7153dc | ||
|
|
a994f65d79 | ||
|
|
1a3a17e480 | ||
|
|
840e4aec54 | ||
|
|
74b660af61 | ||
|
|
cc0c56087a | ||
|
|
990e6c0eed | ||
|
|
3bc6cd8b4d | ||
|
|
515119e1fa | ||
|
|
570303273c | ||
|
|
1739cc58d4 | ||
|
|
b3a7d42f9d | ||
|
|
17ed1f9806 | ||
|
|
1e3883674e | ||
|
|
bead888c67 | ||
|
|
07fcd66d8d | ||
|
|
0f4cac1b76 | ||
|
|
89fbc537bf | ||
|
|
f0ebdf295f | ||
|
|
d396cb911a | ||
|
|
9064487a3e | ||
|
|
8b03f32f95 | ||
|
|
3295cc514e | ||
|
|
c6df492852 | ||
|
|
565c71cb80 | ||
|
|
30bd710650 | ||
|
|
c0dbf95b94 | ||
|
|
7f58837111 | ||
|
|
53f609c4d7 | ||
|
|
26790fd80d | ||
|
|
cfcb24a732 | ||
|
|
e31746b676 | ||
|
|
154435d5a5 | ||
|
|
d24a0312d8 | ||
|
|
aa5d8b9377 | ||
|
|
e9703e03cd | ||
|
|
13a8d27349 | ||
|
|
939f8f52c1 | ||
|
|
cb1e062f9b | ||
|
|
a1d1bc5aea | ||
|
|
1d81c0521f | ||
|
|
d3ef916b23 | ||
|
|
c9b7259cd7 | ||
|
|
fa8c135b22 | ||
|
|
9d53d806fd | ||
|
|
6f499e6c56 | ||
|
|
e66bdc936a | ||
|
|
be34e062e7 | ||
|
|
5664b32cc5 | ||
|
|
6bf0ea63d4 | ||
|
|
628970e588 | ||
|
|
fb68ccad15 | ||
|
|
d6b394500f | ||
|
|
32508d60b1 | ||
|
|
0ae23c30c4 | ||
|
|
e5f18c5e22 | ||
|
|
df0f25b234 | ||
|
|
dc1d9e59b0 | ||
|
|
266eb77eb5 | ||
|
|
c1cac82081 | ||
|
|
323c787d91 | ||
|
|
41070495ba | ||
|
|
037e531b22 | ||
|
|
54713b5d68 | ||
|
|
d94f9a91db | ||
|
|
516b8e848f | ||
|
|
6d1d1705b2 | ||
|
|
666a527aa3 | ||
|
|
f296c7fdad | ||
|
|
58e62da913 | ||
|
|
ca364d4d56 | ||
|
|
2232680ded | ||
|
|
b662c54a07 | ||
|
|
cd2f897ff2 | ||
|
|
c877d4b1d7 | ||
|
|
dc6032aa43 | ||
|
|
fec9431ae5 | ||
|
|
bf1b7e640b | ||
|
|
780ab5b14f | ||
|
|
7371aebb76 | ||
|
|
c227f39a03 | ||
|
|
ff794f1578 | ||
|
|
f26c342e82 | ||
|
|
edacb88ff5 | ||
|
|
4854eac2da | ||
|
|
60cd105b82 | ||
|
|
184db222c5 | ||
|
|
abcfb9ee12 | ||
|
|
5f3ba669eb | ||
|
|
942d4756c7 | ||
|
|
d9b6dfd8d0 | ||
|
|
ff79e7ad36 | ||
|
|
ebfdac96ae | ||
|
|
e2a85885be | ||
|
|
c6b0fb4d65 | ||
|
|
fb5c4df4db | ||
|
|
9cb4eb775b | ||
|
|
8c349e4669 | ||
|
|
b5d879139a | ||
|
|
0e05918631 | ||
|
|
c3fee8d323 | ||
|
|
b5743d9902 | ||
|
|
44974c04ad | ||
|
|
336f8d525b | ||
|
|
f76b6afe6a | ||
|
|
602b58d1df | ||
|
|
030edccc90 | ||
|
|
dd3317f4f6 | ||
|
|
f29d0e45b7 | ||
|
|
c038ed3db4 | ||
|
|
afcf2a9400 | ||
|
|
6bcc4c86e6 | ||
|
|
ded32730bf | ||
|
|
2e1b6aef9f | ||
|
|
26d918e218 | ||
|
|
23e1097f89 | ||
|
|
6d1e2d9fab | ||
|
|
7c37284901 | ||
|
|
e9384676e1 | ||
|
|
88bf51c066 | ||
|
|
fbbe8aff54 | ||
|
|
e3d441d19f | ||
|
|
4f3d20a7c4 | ||
|
|
222ea18bcd | ||
|
|
df7c91f17f | ||
|
|
99331fcc54 | ||
|
|
b59d31855e | ||
|
|
a2211cfa46 | ||
|
|
e9ec42be02 | ||
|
|
0801d9bf65 | ||
|
|
379b7a56ef | ||
|
|
8e9062c812 | ||
|
|
21e03e8318 | ||
|
|
fa1b53682c | ||
|
|
ec68deb7e4 | ||
|
|
1d51f3eed5 | ||
|
|
e8e189d5f3 | ||
|
|
443e338cc3 | ||
|
|
9cab049696 | ||
|
|
e30e869025 | ||
|
|
b106be2ed5 | ||
|
|
842519d7d0 | ||
|
|
d2ff73b579 | ||
|
|
c31d9dfbb2 | ||
|
|
d7ed734ffb | ||
|
|
b5a04bfe63 | ||
|
|
077e6a110e | ||
|
|
66d87e8b12 | ||
|
|
6bf5e7abcc | ||
|
|
39979a411d | ||
|
|
fdd5c71711 | ||
|
|
d6e20fe166 | ||
|
|
7988b13281 | ||
|
|
8395865b75 | ||
|
|
c3f33acdb3 | ||
|
|
6d9167c30f | ||
|
|
f8d698aea9 | ||
|
|
0cbde5046e | ||
|
|
7a137a68ae | ||
|
|
09f7e6ce99 | ||
|
|
305cc72485 | ||
|
|
9b04901754 | ||
|
|
d262f429c4 | ||
|
|
6bb1223614 | ||
|
|
1d97b217cd | ||
|
|
dfe48466e0 | ||
|
|
6fed45e7a8 | ||
|
|
d97b75a3e1 | ||
|
|
87f2e08b3a | ||
|
|
f2c517a4a4 | ||
|
|
b8b810cdb1 | ||
|
|
c21900100e | ||
|
|
50222f5083 | ||
|
|
594b596cf9 | ||
|
|
3494a4875c | ||
|
|
a5d880e411 | ||
|
|
e9f445380b | ||
|
|
ea51f15253 | ||
|
|
58501c205a | ||
|
|
47f23884b4 | ||
|
|
35ff7fd83e | ||
|
|
1375d7922c | ||
|
|
acc0a2ec67 | ||
|
|
eecf1f4a54 | ||
|
|
088d022d5e | ||
|
|
22fcb14f9a | ||
|
|
6f2294f9b9 | ||
|
|
d7190b0602 | ||
|
|
4cf769e7b6 | ||
|
|
3889c8c1fa | ||
|
|
d122e10703 | ||
|
|
05f1fa0ecb | ||
|
|
3974629e34 | ||
|
|
dfd8147873 | ||
|
|
bb503d9cc7 | ||
|
|
913cb1a3cd | ||
|
|
7125fb285e | ||
|
|
0454868958 | ||
|
|
8523e3d1a4 | ||
|
|
e2415b68d3 | ||
|
|
0850a3428e | ||
|
|
6f44a8b6ee | ||
|
|
0cc16c232b | ||
|
|
57bd21d346 | ||
|
|
f18e7295bd | ||
|
|
d6a6343aa8 | ||
|
|
7397d2da50 | ||
|
|
f70c457e88 | ||
|
|
7750720f4d | ||
|
|
950281caa6 | ||
|
|
b5202b5591 | ||
|
|
4aa01acce4 | ||
|
|
c58e788eba | ||
|
|
e7b60a1f27 | ||
|
|
1e4bbc4ecf | ||
|
|
e599da7033 | ||
|
|
341b5cd947 | ||
|
|
fa35f3f9e4 | ||
|
|
1375578b52 | ||
|
|
1e2326913b | ||
|
|
82c41e09b5 | ||
|
|
16f3b71af4 | ||
|
|
03373f3cda | ||
|
|
2c9c01b991 | ||
|
|
8a44b6fdb7 | ||
|
|
3ceb886ca9 | ||
|
|
fb3df39263 | ||
|
|
829e8ed745 | ||
|
|
ed5c52a807 | ||
|
|
8afc5afadf | ||
|
|
b26401203f | ||
|
|
c127548dd1 | ||
|
|
f8c1a48350 | ||
|
|
55f634bec3 | ||
|
|
8c14e42a09 | ||
|
|
2e30a96389 | ||
|
|
3fc4898904 | ||
|
|
3561c55174 | ||
|
|
5195c647f6 | ||
|
|
f3a0d1daac | ||
|
|
dcad6e2d23 | ||
|
|
6a402fe544 | ||
|
|
310ae5905f | ||
|
|
23aa820cdf | ||
|
|
74f702cea6 | ||
|
|
52335bddbc | ||
|
|
05acf724a8 | ||
|
|
71319a0a7c | ||
|
|
c341c55258 | ||
|
|
cf40e641a6 | ||
|
|
7cb6af85a8 | ||
|
|
358ef34918 | ||
|
|
a66d194e12 | ||
|
|
57b3ce4666 | ||
|
|
aaa2b6f817 | ||
|
|
97b56e5620 | ||
|
|
79850176c3 | ||
|
|
8e1896ef5b | ||
|
|
8cf911bb15 | ||
|
|
ad0af16fa3 | ||
|
|
2aada61af3 | ||
|
|
66b9b4c68c | ||
|
|
60d6151ce9 | ||
|
|
bfb4b0b9da | ||
|
|
0f00e206bf | ||
|
|
71536ef9d3 | ||
|
|
354e73b4e7 | ||
|
|
30121e3617 | ||
|
|
4d422e716b | ||
|
|
8aa0f8d070 | ||
|
|
7c03c0cbcf | ||
|
|
c13a4835b2 | ||
|
|
412d9b7645 | ||
|
|
27cdaf1ed5 | ||
|
|
32e8a45e4e | ||
|
|
34f35aff27 | ||
|
|
9b0101321a | ||
|
|
a4c9487192 | ||
|
|
6449973ddc | ||
|
|
bd9a168667 | ||
|
|
5c0b03f133 | ||
|
|
5708f039c0 | ||
|
|
ba0809159c | ||
|
|
4aeb4238b2 | ||
|
|
119bc8207f | ||
|
|
5e7dc27e1f | ||
|
|
6d3b4db760 | ||
|
|
ccba9aa4d5 | ||
|
|
47cbc91b02 | ||
|
|
7a10fa157d | ||
|
|
a27ed4051c | ||
|
|
dbbde4b098 | ||
|
|
2f71480849 | ||
|
|
7e2284e094 | ||
|
|
0ce5c198aa | ||
|
|
c8519188a1 | ||
|
|
bf9f782970 | ||
|
|
72f580efb8 | ||
|
|
a443e3dcde | ||
|
|
5496c6c8af | ||
|
|
b96d5e765e | ||
|
|
cee5fb915a | ||
|
|
54888ff278 | ||
|
|
ab3f3d72ab | ||
|
|
b0eb0d74fb | ||
|
|
8451b4b14e | ||
|
|
ca85d5e8c0 | ||
|
|
9f7cf16335 | ||
|
|
e09353b0fe | ||
|
|
56ace4dd31 | ||
|
|
3cfd1a0957 | ||
|
|
3bd91dc9cb | ||
|
|
aa805a611a | ||
|
|
b46109a086 | ||
|
|
141b102129 | ||
|
|
2a03953f6c | ||
|
|
0ff3bb1a34 | ||
|
|
45d4c26972 | ||
|
|
c05aeffbbb | ||
|
|
b37b07bb06 | ||
|
|
83bb38b857 | ||
|
|
6ac398f11d | ||
|
|
774c210097 | ||
|
|
173aa53cbe | ||
|
|
be128bc12a | ||
|
|
305975bb3b | ||
|
|
e6726eb69d | ||
|
|
2988bae855 | ||
|
|
d65e1087f9 | ||
|
|
03744a7606 | ||
|
|
65e2a1c8aa | ||
|
|
1e8ef4b208 | ||
|
|
2a636481e8 | ||
|
|
9efc424462 | ||
|
|
ad9db64e8b | ||
|
|
0cf04e34c7 | ||
|
|
f932f96097 | ||
|
|
4c5dac5e13 | ||
|
|
abd838de00 | ||
|
|
cd92f69804 | ||
|
|
9d4cddb4a0 | ||
|
|
4f105ced0e | ||
|
|
983a69ed5d | ||
|
|
e17b6aa5c0 | ||
|
|
c73c302d77 | ||
|
|
bdd40ec59d | ||
|
|
d78064daa6 | ||
|
|
7683f7820f | ||
|
|
c6b88d1fcd | ||
|
|
dfaae1df1a | ||
|
|
58efa8411b | ||
|
|
95f000252b | ||
|
|
2cf5880940 | ||
|
|
88c948f117 | ||
|
|
89a369165e | ||
|
|
9fc53329b5 | ||
|
|
8765b7b3bd | ||
|
|
c4710b4bd2 | ||
|
|
43bd08a58f | ||
|
|
8a78cc2f5e | ||
|
|
186429890e | ||
|
|
85d9988d79 | ||
|
|
25ed2b794d | ||
|
|
1e3d216961 | ||
|
|
b43a94b3c7 | ||
|
|
fc5cb3f0ad | ||
|
|
fc83a9e905 | ||
|
|
6635f2f9c1 | ||
|
|
dc054d7e6b | ||
|
|
555d464f8f | ||
|
|
c8a8336dc7 | ||
|
|
228c39719d | ||
|
|
5207fedd61 | ||
|
|
ae9c082cb7 | ||
|
|
b302f16f65 | ||
|
|
bf0bb0519a | ||
|
|
c9db57fb7f | ||
|
|
ca0ace0832 | ||
|
|
32217db357 | ||
|
|
a2ddfc5674 | ||
|
|
1ae7be4f6a | ||
|
|
a418af0aad | ||
|
|
50a92e9ea0 | ||
|
|
1e63fc14cb | ||
|
|
6c00ef65af | ||
|
|
5c29d42d8c | ||
|
|
19055ba004 | ||
|
|
0825ae8cb5 | ||
|
|
c032c9f458 | ||
|
|
fa5a9621e0 | ||
|
|
b2db2cc719 | ||
|
|
0f76819936 | ||
|
|
66d1597312 | ||
|
|
74f4ae03f3 | ||
|
|
5894cec3e4 | ||
|
|
e66c411989 | ||
|
|
f74920fd1b | ||
|
|
c1c98cc7b6 | ||
|
|
d217d9a291 | ||
|
|
29a73b183c | ||
|
|
79c64f0e38 | ||
|
|
b8a3deeb02 | ||
|
|
108c774c0f | ||
|
|
830c7556b8 | ||
|
|
5470add29a | ||
|
|
f6c9ab0068 | ||
|
|
e44b34062c | ||
|
|
320ae611a1 | ||
|
|
e54a87c436 | ||
|
|
608cc363a2 | ||
|
|
f9609c5871 | ||
|
|
cc422a6b1d | ||
|
|
638d75c388 | ||
|
|
b02495dd3d | ||
|
|
90ee8033b0 | ||
|
|
3f5d8fe2a1 | ||
|
|
32176d3e2f | ||
|
|
c65f55b22a | ||
|
|
c9d221404b | ||
|
|
ee73961832 | ||
|
|
ef39c174ed | ||
|
|
962d8f77dd | ||
|
|
bbc7abc50d | ||
|
|
00f1258032 | ||
|
|
beb297967f | ||
|
|
00c913fd19 | ||
|
|
a38a8c4ba4 | ||
|
|
56fafba8e9 | ||
|
|
d0396b3da9 | ||
|
|
180eaa2ce5 | ||
|
|
d8de60afb9 | ||
|
|
d5248e8472 | ||
|
|
a7c199b195 | ||
|
|
97a5351a52 | ||
|
|
e0b4452007 | ||
|
|
2e4a532b3c | ||
|
|
e54266d3a5 | ||
|
|
422ed0a5e2 | ||
|
|
59e17738cc | ||
|
|
50644cf3c4 | ||
|
|
4a3ceb710d | ||
|
|
5ab640c380 | ||
|
|
ba1afca4dd | ||
|
|
4d4ffdb86c | ||
|
|
0c6002a861 | ||
|
|
3ebdd8da14 | ||
|
|
31eb689635 | ||
|
|
c3d66f243a | ||
|
|
5c108635d0 | ||
|
|
365808eff2 | ||
|
|
5c654e99e4 | ||
|
|
709c47d40d | ||
|
|
8ff8fb9c92 | ||
|
|
e3cdc5d3ff | ||
|
|
69851d1596 | ||
|
|
c5330246b1 | ||
|
|
0f2b46b56a | ||
|
|
6580ea5891 | ||
|
|
1cfd5ae4f0 | ||
|
|
339beeabaf | ||
|
|
3a4b9e2e31 | ||
|
|
3055eeaa4f | ||
|
|
e517fa6000 | ||
|
|
3946ebcb92 | ||
|
|
a34fa04e4f | ||
|
|
2108f3209d | ||
|
|
8d2ae5e254 | ||
|
|
5092bc571d | ||
|
|
b013e8af50 | ||
|
|
3af8c4d28f | ||
|
|
1f1860e53c | ||
|
|
21015bccb5 | ||
|
|
b27fabea12 | ||
|
|
932c708538 | ||
|
|
adf241c146 | ||
|
|
b27a62c625 | ||
|
|
132596a17e | ||
|
|
f0359dcde9 | ||
|
|
08a005b271 | ||
|
|
1b873acd72 | ||
|
|
a76ac9b5e3 | ||
|
|
f7fa47026c | ||
|
|
36c3fe6a27 | ||
|
|
7c67f08362 | ||
|
|
5d71a828c4 | ||
|
|
3e9392b4b7 | ||
|
|
02d9d7c22c | ||
|
|
8d60c65e5b | ||
|
|
0418cd0a95 | ||
|
|
a464295e5b | ||
|
|
95ec16fa92 | ||
|
|
2bd43cdc62 | ||
|
|
37e7222371 | ||
|
|
254c766883 | ||
|
|
e82a8ad63e | ||
|
|
39b4b233c9 | ||
|
|
3e8208a117 | ||
|
|
fbc7fd1de3 | ||
|
|
d533733d4b | ||
|
|
082716e21a | ||
|
|
5ffdecab9e | ||
|
|
62c87b4f87 | ||
|
|
ab3a50f22f | ||
|
|
71d3e8dd04 | ||
|
|
aa8bbc32c5 | ||
|
|
05646c03cc | ||
|
|
1453b30e41 | ||
|
|
4f575fda73 | ||
|
|
14b6c70f47 | ||
|
|
07144659b1 | ||
|
|
69b7eb43f6 | ||
|
|
2a8b59b79a | ||
|
|
8f4e9ac48f | ||
|
|
30069e719b | ||
|
|
71fa0dff4b | ||
|
|
40f3a78795 | ||
|
|
0d11c71bb7 | ||
|
|
b58abf2a5c | ||
|
|
f7911701b1 | ||
|
|
f9d4b58588 | ||
|
|
1269aa273b | ||
|
|
758480dd5f | ||
|
|
2ca84501ba | ||
|
|
f9756e0977 | ||
|
|
8894c26748 | ||
|
|
9bb7e3a541 | ||
|
|
235cba5ba5 | ||
|
|
6c04b3936a | ||
|
|
31ba460553 | ||
|
|
57f519db65 | ||
|
|
edf6c65e38 | ||
|
|
349cf1981a | ||
|
|
a15635d953 | ||
|
|
04d9f3808b | ||
|
|
494724c795 | ||
|
|
71cadad05a | ||
|
|
30d204dddc | ||
|
|
cc19748fd2 | ||
|
|
48f197b7ea | ||
|
|
99b0ab5f50 | ||
|
|
db02b4443b | ||
|
|
8e35500269 | ||
|
|
74628642ad | ||
|
|
f41edf284c | ||
|
|
f740fde834 | ||
|
|
db35c28607 | ||
|
|
b1aae4a85a | ||
|
|
f4435c255c | ||
|
|
5aaec02af0 | ||
|
|
7f4b3edd84 | ||
|
|
14cc7fcfeb | ||
|
|
95c0afd5dd | ||
|
|
306ea31f0b | ||
|
|
18b7989e03 | ||
|
|
559eef594e | ||
|
|
7e6d2c6586 | ||
|
|
9fd22a92ac | ||
|
|
e9470f4c94 | ||
|
|
cceb4bb324 | ||
|
|
f14b4f4429 | ||
|
|
23db719c36 | ||
|
|
8b88a17836 | ||
|
|
f1599d6e69 | ||
|
|
df0c72ab0a | ||
|
|
c68395549f | ||
|
|
d69addc3af | ||
|
|
d96e67e850 | ||
|
|
321d9f376e | ||
|
|
e3450352c5 | ||
|
|
4ba8be67fe | ||
|
|
abff997179 | ||
|
|
55216e1de7 | ||
|
|
194f3352a1 | ||
|
|
9e0ae5dc96 | ||
|
|
ba88fd5306 | ||
|
|
179529214b | ||
|
|
b27a7a1c31 | ||
|
|
1a5b5327dc | ||
|
|
d30ac79a77 | ||
|
|
a1461d9ea6 | ||
|
|
783248d58b | ||
|
|
d13d77e39a | ||
|
|
bf333e1964 | ||
|
|
1833e8683b | ||
|
|
e4a336cd67 | ||
|
|
1810956185 | ||
|
|
af7f0f49d1 | ||
|
|
d6ebf8ea04 | ||
|
|
13721c9811 | ||
|
|
eafde17259 | ||
|
|
02810e84a9 | ||
|
|
ac3214fedc | ||
|
|
c376689ad4 | ||
|
|
a12a686a68 | ||
|
|
16ec69bb8c | ||
|
|
82cb95aff7 | ||
|
|
1eb3693f0a | ||
|
|
50925536d1 | ||
|
|
bfd9c654aa | ||
|
|
1104ce3176 | ||
|
|
19ef5b7e1d | ||
|
|
b1477f5fb5 | ||
|
|
8613a89264 | ||
|
|
6a90bac196 | ||
|
|
cda6398010 | ||
|
|
e502f1dcc4 | ||
|
|
13b699a183 | ||
|
|
0cd1f314f6 | ||
|
|
0c894ee48a | ||
|
|
f6f1d4a97c | ||
|
|
c85820e685 | ||
|
|
445f721ceb | ||
|
|
5fa24247c6 | ||
|
|
7ee76fdd2b | ||
|
|
2acfe7f1bf | ||
|
|
b33f660f90 | ||
|
|
91509888af | ||
|
|
fa3657f736 | ||
|
|
09638633c1 | ||
|
|
11a6f1124f | ||
|
|
98ba58f39b | ||
|
|
b6fa4f3242 | ||
|
|
baa7436e43 | ||
|
|
4a9b649e9f | ||
|
|
42143f77c5 | ||
|
|
e2bdd216bb | ||
|
|
856fde79ec | ||
|
|
91987b2e06 | ||
|
|
36c6eeabc9 | ||
|
|
549e364da3 | ||
|
|
1cc2d6c6b7 | ||
|
|
d0321ce0fa | ||
|
|
0d4cca3aa7 | ||
|
|
d77177a98f | ||
|
|
13bcac7e22 | ||
|
|
0046d8ba90 | ||
|
|
4475a93fc5 | ||
|
|
041b609f99 | ||
|
|
bb6653cecb | ||
|
|
d84ce07038 | ||
|
|
c9fec9ad51 | ||
|
|
e953ea7212 | ||
|
|
d1d7cd8186 | ||
|
|
d3e4f17dab | ||
|
|
344fde0d6f | ||
|
|
e27c8955c5 | ||
|
|
27fa234888 | ||
|
|
59ae047359 | ||
|
|
a847a2ff91 | ||
|
|
8445637dd1 | ||
|
|
7f339ede4e | ||
|
|
e54faa3079 | ||
|
|
4d2a4f3433 | ||
|
|
ddd6de24ce | ||
|
|
19f1b969ea | ||
|
|
48beb69103 | ||
|
|
a0be08d62a | ||
|
|
a750c7df2a | ||
|
|
950986c69e | ||
|
|
cc7bddefa1 | ||
|
|
1c4b735880 | ||
|
|
0862c135d0 | ||
|
|
01e3cf1b1c | ||
|
|
519bd389f6 | ||
|
|
ee6aac2614 | ||
|
|
fa319b6529 | ||
|
|
905be4130e | ||
|
|
814e973f9a | ||
|
|
d84596860c | ||
|
|
5d9414e728 | ||
|
|
016e279d43 | ||
|
|
d432047ac1 | ||
|
|
9938e60e05 | ||
|
|
6e751bbfaf | ||
|
|
8bdc07185b | ||
|
|
8872aceb0b | ||
|
|
f7b14c7da7 | ||
|
|
86cd732453 | ||
|
|
ae6571b18b | ||
|
|
0faf773f71 | ||
|
|
3178fb1a46 | ||
|
|
a917e80774 | ||
|
|
9bde5fcad6 | ||
|
|
e33f423733 | ||
|
|
cc35408607 | ||
|
|
10d91b50ec | ||
|
|
7dd06b5659 | ||
|
|
86087bf505 | ||
|
|
cb4923815a | ||
|
|
43258d64d2 | ||
|
|
6d3109be67 | ||
|
|
a25e0cc23f | ||
|
|
026a78b1b6 | ||
|
|
380a4a0395 | ||
|
|
f6c2a6387f | ||
|
|
858f33f782 | ||
|
|
fa072bf387 | ||
|
|
154ea7354d | ||
|
|
06ed266278 | ||
|
|
5df16db823 | ||
|
|
4f23706b19 | ||
|
|
b9601cb54a | ||
|
|
3d12f85f66 | ||
|
|
79b8baac9f | ||
|
|
8a6df8abc7 | ||
|
|
8b8d763fb7 | ||
|
|
ac8d2beb80 | ||
|
|
5e6384074e | ||
|
|
2522bd44d6 | ||
|
|
76131f1cc7 | ||
|
|
54fb5dc765 | ||
|
|
4110af56e7 |
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
gns3/version.py merge=ours
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -59,3 +59,4 @@ keys
|
||||
/gns3_server.ini
|
||||
updates
|
||||
.cache
|
||||
__pycache__
|
||||
|
||||
361
CHANGELOG
361
CHANGELOG
@@ -1,6 +1,325 @@
|
||||
# Change Log
|
||||
# Change Log
|
||||
|
||||
## 1.4.1 01/02/2016
|
||||
## 2.0.0 alpha 2 20/10/2016
|
||||
|
||||
* Support pure remote server for importing appliance
|
||||
* Dissallow binding GNS3 server to an IPV6 (not supported by some emulators)
|
||||
* Drop vmware host type choice in client
|
||||
* Ask user to restart GNS3 after VMware installation
|
||||
* Improve duplicate prevention in topology summary
|
||||
* Add a duplicate button in the project library dialog
|
||||
* Fix error introduce in previous commits
|
||||
* Fix duplicates in recent project list
|
||||
* Fix a project override error
|
||||
* Fix Duplicated node in node summary when restoring a snapshot
|
||||
* Fix a crash in the VMware / VirtualBox wizard
|
||||
* If console host is 0.0.0.0 use controller address
|
||||
* Fix save issue when importing an appliance
|
||||
* Strip HTML in console view logs and log files
|
||||
* Fix TypeError: _expandAllSlot() takes 1 positional argument but 2 were given
|
||||
* Fix Cannot open created project by using Recents projects
|
||||
* Update edit project Ui.
|
||||
* Update crash report key
|
||||
* Fix a crash when exporting debug without project open
|
||||
* Fix a crash in rare condition when logging informations to the console
|
||||
* Fix a crash in compute summary view
|
||||
* Add a text about how to change the topology size in 2.0 in general preferences
|
||||
* Improve warning when connection issue to GNS3 VM
|
||||
* Fix crash in setup wizard
|
||||
* Fix the wizard for creating appliance template doesn't support remote main server
|
||||
* Appliance wizard support remote controller
|
||||
* Fix Browse button is not working in the local server page in the setup wizard
|
||||
* Check if local server is running in the setup wizard
|
||||
* Hide setup wizard after first successful run
|
||||
* Import appliance and New project are display at the same time
|
||||
* Support remote controller in the setup wizard
|
||||
* Fix When importing a gns3a the correct qemu binary is not selected
|
||||
* Increase creation timeout for docker container
|
||||
* Make WaitForLambdaWorker more crash proof
|
||||
* Fix a crash when importing appliance
|
||||
* Fix error in import appliances
|
||||
* Try to fix the a segfault when importing appliance
|
||||
* Fix crash in upload images
|
||||
* Trust the server for link creation error (avoid sync issue)
|
||||
* Fix an Error in server preference page
|
||||
* Fix compatibility with remote server of 1.X
|
||||
* New appliance dialog should not be display if you cancel the setup wizard
|
||||
|
||||
## 2.0.0 alpha 1 29/09/2016
|
||||
* Save as you go
|
||||
* Smart packet capture
|
||||
* Capture on any link between any node
|
||||
* Select where to run a VPCS node
|
||||
* Delete a project from the GUI
|
||||
* Project options
|
||||
* The cloud is a real node
|
||||
* Cloud templates
|
||||
* New cloud interface
|
||||
* VPCS / Ethernet Switch / Ethernet Hub templates
|
||||
* Search OS images in multiple locations
|
||||
* Periodic extraction of startup configs for Dynamips and IOU
|
||||
* Custom cloud, Ethernet hub and Ethernet switch templates
|
||||
* Snap to grid for all objects
|
||||
* Synchronize the node templates when using multiple GUI
|
||||
* Link label style
|
||||
* New place holders in command line for opening consoles
|
||||
* %i will be replaced by the project UUID
|
||||
* %c will be replaced by the connection string
|
||||
* Export a portable project from multiple remote servers
|
||||
* New save as
|
||||
* Snapshots with remote servers
|
||||
* Better start / stop / suspend all nodes
|
||||
* Edit config
|
||||
* NAT node
|
||||
* Support for colorblind users
|
||||
* Support for non local server
|
||||
* Support for profiles
|
||||
* Suspend the GNS3VM when closing GNS3
|
||||
* Edit the scene size
|
||||
* New API
|
||||
|
||||
## 1.5.2 18/08/2016
|
||||
|
||||
* Make more clear that VMware VM are not ESXi
|
||||
* Add AppData and Desktop files
|
||||
* Fix you can not select the server for VPCS
|
||||
* Fix error when removing an interface from a cloud
|
||||
* Fix crash when scanning a directory for image and you don't have permission on a file
|
||||
* Bring back the warning dialog when no router is configured
|
||||
* Fix rare crash in server summary
|
||||
* Fix crash during export
|
||||
|
||||
## 1.5.1 06/07/2016
|
||||
|
||||
* Try to fix a crash when reseting interface label
|
||||
* Fix a crash with broken file system
|
||||
* Fix EtherSwitch default name format
|
||||
* Fix crash when you have utf-8 char in the README
|
||||
* Fix rare crash when creating a link
|
||||
* Stop node before hot unlink
|
||||
* Prevent a crash due to issue in Qt
|
||||
* Add another security to prevent client to send empty hostname
|
||||
* Fix rare crash when deleting interface from the cloud
|
||||
* Fix rare crash in topology summary view
|
||||
* Ask user to send explanation if they cross a rare error
|
||||
* Fix rare crash when deleting a node
|
||||
* Hotlink support for Docker
|
||||
* Fix typo in the a warning dialog
|
||||
* Fix Remote GNS3 VM requires local server
|
||||
* Fix AttributeError: 'NoneType' object has no attribute '_server'
|
||||
* No timeout when importing a .gns3project
|
||||
|
||||
## 1.5.0 27/06/2016
|
||||
|
||||
* Fix double extension of portable project
|
||||
* Disallow export of project with a cloud
|
||||
* Change view grid -> show the grid.
|
||||
* Check if a link can be removed from a running node. Fixes #1320.
|
||||
* Hide non implemented console options in general preferences. Ref #1315.
|
||||
* Improve snap to grid
|
||||
* Change grid color
|
||||
* Avoid a crash with snap to grid and ostinato logo
|
||||
* Add a view grid
|
||||
* Fix you can no longer capture if you start stop capture multiple time
|
||||
* A button to open the file browser with the configuration file location
|
||||
* Add snap to grid feature
|
||||
|
||||
## 1.5.0rc2 15/06/2016
|
||||
|
||||
* Ethernet0 => eth0 for docker
|
||||
* Validate appliance schema before loading it
|
||||
* Fix a rare crash when loading images
|
||||
* Fixes doctor failure with 1.5rc1. Fixes #1290.
|
||||
* Check for template name collisions.
|
||||
* Log GNS3 doctor exceptions.
|
||||
* Option to hide the new appliance template button. Fixes #1277.
|
||||
|
||||
## 1.5.0rc1 01/06/2016
|
||||
|
||||
* Avoid a segfault when exiting with debug enabled
|
||||
* Fix the GNS3 VM is visible even if deactivated
|
||||
* Do not automatically stop the GNS3 VM by default.
|
||||
* Block VMnet host traffic by default. Solves the traffic loop issue on Windows.
|
||||
* Remove tooltip for Qemu VM base mac address.
|
||||
* Fix you cannot select the remote server of your choice in qemu wizard
|
||||
* Fix issue when deleting a running container
|
||||
* Allow to block network traffic originating from the host OS for vmnet interfaces (Windows only).
|
||||
* Change tooltip for Qemu VM base MAC address.
|
||||
* Improve image import
|
||||
* Support dragging an image in the GNS3 topology from the system file browser
|
||||
* Fix an issue with import with no GNS3 VM
|
||||
* Fix error when using {} in the node name
|
||||
* Display the progress dialog after 250ms
|
||||
* Fix a crash when exporting a project with virtualbox or VMware VM
|
||||
* Set default VMware VM adapter type to e1000.
|
||||
|
||||
|
||||
## 1.5.0b1 23/05/2016
|
||||
|
||||
* Remote server selector not enabled in import appliance wizard
|
||||
* New server dialog is now windows modal
|
||||
* Fixes issue when UDPPortAllocatedSlot() is called multiple times.
|
||||
* Private-config is optional.
|
||||
* Fixes alternative IOS image selection when loading a project.
|
||||
* Accept fill_color property for rectangle/ellipse objects. Compatibility for old 1.0 projects.
|
||||
* Fixes check for NPF service and add check for NPCAP service on Windows.
|
||||
* :latest for docker image is managed server side
|
||||
* Remove unbreakable space
|
||||
* Fix Checkbox and radio button are not readable with charcoal style
|
||||
* Fix existing remotez server is not recognised
|
||||
* Fix Cannot change docker image adapter number from docker image configuration
|
||||
* Fix got an unexpected keyword argument 'ram_limit'
|
||||
* Check that both Qt and PyQt version >= 5.6 to enable high DPI scaling.
|
||||
* Check Qt version, not PyQt. Fixes #1232.
|
||||
* Fix you can not turn off the GNS3VM with remote server
|
||||
|
||||
## 1.5.0a2 10/05/2016
|
||||
|
||||
* Fix issue with PyPi
|
||||
|
||||
## 1.5.0a1 10/05/2016
|
||||
|
||||
* Rebase Qcow2 disks when starting a VM if needed
|
||||
* Docker support
|
||||
* import / export portable projects (.gns3project)
|
||||
|
||||
## 1.4.6 28/04/2016
|
||||
|
||||
* Fix a typo in qemu preferences
|
||||
* Fix upload of large image to the VM
|
||||
* Reduce the number of connection tries from 120 to 40 when connecting the GNS3 server running inside the GNS3 VM.
|
||||
* Include link to the GNS3 academy. Fixes #1178.
|
||||
* Snapback feature for port labels. Fixes #1182.
|
||||
* Prevent users to select VirtualBox.exe instead of VBoxManage.exe. Fixes #1195.
|
||||
* Improve the vmrun error message
|
||||
* If we can not read the registry try to guess vmware type from vmrun path
|
||||
* Ensure that you can not duplicate an interface in a cloud
|
||||
* Disallow removal of link of running emulator without support of hotlink
|
||||
* Check PyQT version support dev version
|
||||
* Show server CPU usage if it's 0
|
||||
* Clear warnings about using linked clones with VMware Player.
|
||||
* Double click center on link
|
||||
* Double click on an element in topology summary center the view on it
|
||||
* Fix a very very rare crash when closing a project
|
||||
* Avoid a small blink of the waiting text
|
||||
* Fix a crash with image item
|
||||
* Show a symbol in the middle of the link when packet capturing is activated. Ref #789.
|
||||
* GNS3 doctor: check if the NPF service is running. Fixes #1124.
|
||||
* Fixes progress dialog is None in accept()
|
||||
* Fix another race conditions in progress dialog
|
||||
* Replace the installation instructions by a link to the doc
|
||||
|
||||
## 1.4.5 23/03/2016
|
||||
|
||||
* Change some sentences.
|
||||
* Sort snapshots by date
|
||||
* Block save as and snapshot when a device is running
|
||||
* If you hit enter in the new project dialog it's work
|
||||
* Display upload size during progress
|
||||
* This should avoid blinking dialog. And display better progress
|
||||
* SetupWizard: limit the number of vCPUs for the GNS3 VM to the number of physical cores.
|
||||
* Remove blocking code. Ref #1109.
|
||||
* Fixes "QThread: Destroyed while thread is still running",
|
||||
* Add a timeout when you are not able to join the remote server
|
||||
* Remove bad smell from progress dialog and handle ESC key
|
||||
* Remove root required messages in cloud node. Ref #608.
|
||||
* Show a warning when the GUI is run with root rights. Fixes #608.
|
||||
* Change message when closing GNS3 with running device.
|
||||
* Ask the user to stop device before closing
|
||||
* At startup display a warning if another GUI is already running
|
||||
* Fix a crash if you delete a file while refreshing the list of appliances
|
||||
* Fix double opening of serial console
|
||||
* Always ask the server for builtin
|
||||
* Improve detection of vmrun on OSX
|
||||
* Delete image from images dir when no longer need
|
||||
* Sort node name in topology summary
|
||||
* Allow to show a message box for test without starting GNS3
|
||||
* Drop licence for paramiko since we no longer use it
|
||||
|
||||
## 1.4.4 23/02/2016
|
||||
|
||||
* Fix crash when selecting no image in GNS3A but clicking on Download
|
||||
* Fix crash when you have a file size None (testing a new gns3a)
|
||||
* Prevent the progress dialog to cancel the GNS3VM when it's finish
|
||||
* Add a command show gns3vm to get the GNS3 VM statusM
|
||||
* Prevent setup wizard to appear if VM is running
|
||||
* Display error dialog if a custom console is invalid
|
||||
* Crash when you import GNS3A just after installing gns3 Fix #1063
|
||||
* Change the way we check is setup wizard has been turned off Fix #1071
|
||||
* Do not failed if GNS3 VM server has an incorrect version
|
||||
* Include the output from vmrun or VBoxManage when they return an error code.
|
||||
* Fixes bug that forced the GNS3 VM running in VirtualBox to restart even if no preferences had been changed.
|
||||
* Allows to cancel the progress dialog when GNS3 tries to contact the server running in the GNS3 VM.
|
||||
* Ask user to upgrade via the VM menu
|
||||
|
||||
## 1.4.3 19/02/2016
|
||||
|
||||
* Allow idlepc 0x0 in topology
|
||||
* Show an explicit error message when status code 0 is returned. Fixes #1034.
|
||||
* Fixes minor bug when dropping a VirtualBox VM on the scene. Fixes #748.
|
||||
* Correctly check local server if only local is available in vm wizard
|
||||
* Make the GNS3 VM server running value more reliable
|
||||
* Make VM configuration dialog modal
|
||||
* Cannot take GIF screenshots (write is not supported by Qt).
|
||||
|
||||
## 1.4.2 17/02/2016
|
||||
|
||||
* Allow gif image (not animated) since patent expire in 2004
|
||||
* Countdown before starting the GNS3 VM
|
||||
* Prevent IOU GNS3A install on Windows
|
||||
* Set timeout from 1 to 3 seconds when waiting for GNS3 VM server. Ref #1034.
|
||||
* Redirect stderr to stdout when executing VBoxManage or vmrun. Ref #1027.
|
||||
* Update VMware banners
|
||||
* Prevent a crash in progress dialog
|
||||
* Update for 4K monitor
|
||||
* Allow to cancel the start of the GNS3 VM
|
||||
* Update the .net converter
|
||||
* Detect and fix duplicate port name in topology
|
||||
* Allow to open a custom console on any node
|
||||
* All node are now SVG items
|
||||
* Move Qt code to a module so we can add new code in differents files
|
||||
* Fix rare crash when updating node
|
||||
* Prevent duplicate server in server summary
|
||||
* Fix a regression in Host and Cloud
|
||||
* Check if GNS3 is not installed twice in doctor
|
||||
* Allow to add custom command to the list
|
||||
* Update Readme Python 3.4 is require
|
||||
* Allow to import unknown files via GNS3A
|
||||
* Fix a problem with gns3 running in background after exit
|
||||
* Move common code for ports dump to vm.py
|
||||
* Move common code _updatePortSettings to vm.py
|
||||
* Fix a crash when searching for alternative images
|
||||
* Fix a crash with corrupted topology from 1.0
|
||||
* Remove all the docker code from 1.4 gui to avoid confusion
|
||||
* Create a dialog for choosing the console command.
|
||||
* Catch error if Dynamips is disabled for local and no remote available
|
||||
* Put a link for the GNS3 VM in the setup wizard
|
||||
* When importing appliance explain why options is gray
|
||||
* User configurable default name format for VMware and VirtualBox. Ref #748
|
||||
* Changes "base name prefix" to "default name format". Ref #748.
|
||||
* User configurable base name support for Dynamips, IOU and VPCS. Ref #748.
|
||||
* Refactor "Import config" router dialog. Fixes #752.
|
||||
* Fixes ValueError: cannot mmap an empty file. Fixes #723.
|
||||
* Saves the "show port names" state in topology files. Fixes #778.
|
||||
* Fix KeyError: 'midplane' when loading 7200 in some cases
|
||||
* Hide the server select box for builtin switch if Dynamips local is off
|
||||
* Fix an issue where the Existing image button can disappear from wizard
|
||||
* Fix a race condition when you ask for image list but close the windows
|
||||
* Fix alignments of VMware and VirtualBox in VM choice type
|
||||
* Better explanation during server choice
|
||||
* Disabled remote button when we have no remote in server wizard
|
||||
* Improved lookup for VMware host type. Fixes #970.
|
||||
* Change some text in nodes view.
|
||||
* Allow to edit a node via a right click in the node
|
||||
* Show error if a problem occur when getting remote server KVM status
|
||||
* Display a clean error when an appliance has an invalid JSON
|
||||
* MobaXterm integration
|
||||
* Allow to show the command line used to start a VM
|
||||
* Add - GNS3 at the end of the windows name
|
||||
* Fix a crash with doctor on windows
|
||||
* Fix crash in doctor if ubridge path is empty
|
||||
|
||||
## 1.4.1 01/02/2016
|
||||
|
||||
* Improvement to detect VMware Player on Linux. Ref #970.
|
||||
* You can move Dock widgets everywhere
|
||||
@@ -43,7 +362,7 @@
|
||||
* Fix crash on Windows when a gui is already running
|
||||
* Add default idle-pc value for c7200-adventerprisek9-mz.155-2.XB. Fixes #389.
|
||||
|
||||
## 1.4.0rc3 05/01/2016
|
||||
## 1.4.0rc3 05/01/2016
|
||||
|
||||
* Add information about antivirus and firewall in case of connection fail
|
||||
* Change link to doc for missing router image
|
||||
@@ -135,7 +454,7 @@
|
||||
* Change text for export debug information.
|
||||
* Add informations about GNS3 VM
|
||||
|
||||
## 1.4.0rc1 12/15/2015
|
||||
## 1.4.0rc1 12/15/2015
|
||||
|
||||
* Rename an appliance if the default name is already taken
|
||||
* Existing image option should be hidden when none is available
|
||||
@@ -147,7 +466,7 @@
|
||||
* Log to console the Qt Message Boxes
|
||||
* Drops securecrt.vbs
|
||||
|
||||
## 1.4.0b5 02/11/2015
|
||||
## 1.4.0b5 02/11/2015
|
||||
|
||||
* Fix crash when loading invalid appliance file
|
||||
* Show a message is starting or is stopping in progress dialog
|
||||
@@ -169,7 +488,7 @@
|
||||
* Fix crash when using an old version of 1.4 server
|
||||
* Ensure default settings are saved when starting the app
|
||||
|
||||
## 1.4.0b4 19/10/2015
|
||||
## 1.4.0b4 19/10/2015
|
||||
|
||||
* Mockup of appliances wizard
|
||||
* Fix tests
|
||||
@@ -249,7 +568,7 @@
|
||||
* Search image by default also in the download directory
|
||||
* Fixes issue when Telnet doesn't let you to login to an appliance on Linux.
|
||||
|
||||
## 1.3.11 07/10/2015
|
||||
## 1.3.11 07/10/2015
|
||||
|
||||
* Display the version of Qt in the console
|
||||
* Catch errors when we have an infinite recursion when copying a folder
|
||||
@@ -384,7 +703,7 @@
|
||||
* Fix issue with file upload and Qt 5.5
|
||||
* Improves the symbol dialog. Implements #514.
|
||||
|
||||
## 1.3.9 03/08/2015
|
||||
## 1.3.9 03/08/2015
|
||||
|
||||
* Catch exception when trying to launch Wireshark.
|
||||
* Backport: fixes migration of cloud interfaces.
|
||||
@@ -402,7 +721,7 @@
|
||||
* Write GNS3 upgrade to appdata
|
||||
* Fix windows asking for upgrade to the wrong version
|
||||
|
||||
## 1.3.8 27/07/2015
|
||||
## 1.3.8 27/07/2015
|
||||
|
||||
* Fixes rare issue when adding a link. Fixes #573.
|
||||
* Backport: option to drop nvram & disk files for IOS routers in order to save disk space.
|
||||
@@ -429,7 +748,7 @@
|
||||
* Remove ram as a mandatory dynamips settings
|
||||
* Force UTF-8 when reading server configuration file
|
||||
|
||||
## 1.4.0alpha1 09/07/2015
|
||||
## 1.4.0alpha1 09/07/2015
|
||||
|
||||
* Remove unused cloud code from the 1.4
|
||||
* Setup Wizard (to be tweaked). Implements #402.
|
||||
@@ -506,7 +825,7 @@
|
||||
* Fixes WICs are not displayed correctly. Fixes #434.
|
||||
* Do not load settings that the GUI doesn't use.
|
||||
|
||||
## 1.3.6 16/06/2015
|
||||
## 1.3.6 16/06/2015
|
||||
|
||||
* Fix an issue with 1.4dev compatibility
|
||||
|
||||
@@ -527,7 +846,7 @@
|
||||
* Raise error if we pass non string to Port name
|
||||
* Add basic auth support for local server
|
||||
|
||||
## 1.3.4 02/06/2015
|
||||
## 1.3.4 02/06/2015
|
||||
|
||||
* Check if an IOS image is set in the IOS router template
|
||||
* Ensure the version number is written in configuration file
|
||||
@@ -544,7 +863,7 @@
|
||||
* Fix a rare crash in completion
|
||||
* Fix crash when loading topology in rare conditions
|
||||
|
||||
## 1.3.3 14/05/2015
|
||||
## 1.3.3 14/05/2015
|
||||
|
||||
* New inline help text for the idle-pc dialog.
|
||||
* Reactivate auto idle-pc in device contextual menu + save a chosen idle-pc value in template.
|
||||
@@ -689,7 +1008,7 @@
|
||||
* Fix issues with progress dialog
|
||||
* Fix save as
|
||||
|
||||
## 1.3.0rc2 23/03/2015
|
||||
## 1.3.0rc2 23/03/2015
|
||||
|
||||
* Fix crash when in same occasion the project name is missing
|
||||
* Update sentry key
|
||||
@@ -706,7 +1025,7 @@
|
||||
* Del key deletes selected link
|
||||
* Fix crash is no remote servers is available
|
||||
|
||||
## 1.3.0rc1 19/03/2015
|
||||
## 1.3.0rc1 19/03/2015
|
||||
|
||||
* Handle legacy snapshots
|
||||
* Add server informations for Qemu, VirtualBox and VPCS info boxes
|
||||
@@ -719,7 +1038,7 @@
|
||||
* Display a warning on console if server port is already in used
|
||||
* Display an error if server version is incorrect
|
||||
|
||||
## 1.3.0beta2 13/03/2015
|
||||
## 1.3.0beta2 13/03/2015
|
||||
|
||||
* Alternative local server shutdown (faster GUI closing on Windows).
|
||||
* Grey out local server preferences if the local server is not activated.
|
||||
@@ -728,7 +1047,7 @@
|
||||
* Support RAM setting for VirtualBox VMs.
|
||||
* Fixed duplicate VM template entries for Qemu, VirtualBox and IOU.
|
||||
|
||||
## 1.3.0beta1 11/03/2015
|
||||
## 1.3.0beta1 11/03/2015
|
||||
|
||||
* New title for VMs/Devices/routers preference pages.
|
||||
* Deactivate auto idle-pc in contextual menu while we think about a better implementation.
|
||||
@@ -757,7 +1076,7 @@
|
||||
* Fixed adapter bug with VirtualBox.
|
||||
* Fixed various errors when a project was not initialized.
|
||||
|
||||
## 1.3.0alpha1 03/03/2015
|
||||
## 1.3.0alpha1 03/03/2015
|
||||
|
||||
* No more console port and UDP tunneling settings by type of module
|
||||
* Fixe save
|
||||
@@ -778,7 +1097,7 @@
|
||||
|
||||
## 1.2.2 2015/01/16
|
||||
|
||||
### Small improvements / new features
|
||||
### Small improvements / new features
|
||||
|
||||
* EtherSwitch routers can be added and configured like other IOS routers.
|
||||
* Change hostname option in the contextual device menu.
|
||||
@@ -799,7 +1118,7 @@
|
||||
* Console switching from local/remote to remote/local while a VirtualBox VM is running.
|
||||
* Default Jungle dock location is now bottom right corner.
|
||||
|
||||
### Bug fixes
|
||||
### Bug fixes
|
||||
|
||||
* Fixed the default jungle news loading on Windows.
|
||||
* Fixed SuperPutty integration (not the default, still have to select it in the preferences).
|
||||
@@ -846,7 +1165,7 @@ Prevent GNS3 to crash on Windows when importing GNS3 config file.
|
||||
* Fix SecureCRT issue when disconnecting from an IOU device on Windows.
|
||||
* Update VPCS to version 0.6 in the all-in-one installer.
|
||||
|
||||
## 1.1 2014/11/20
|
||||
## 1.1 2014/11/20
|
||||
|
||||
* Fixed broken cloud.
|
||||
* Fixed broken remote server.
|
||||
|
||||
@@ -18,13 +18,17 @@ it on https://github.com/GNS3/gns3-gui we will take care of the triage.
|
||||
|
||||
For bugs specific to the GNS3 VM, please report on https://github.com/GNS3/gns3-vm
|
||||
|
||||
## Asking for new features
|
||||
## Security issues
|
||||
|
||||
For security issues please keep it private and send an email to developers@gns3.net
|
||||
|
||||
## Asking for new features
|
||||
|
||||
The best is to start a discussion on the community website in order to get feedback
|
||||
from the whole community.
|
||||
|
||||
|
||||
## Contributing code
|
||||
## Contributing code
|
||||
|
||||
We welcome code contribution from everyone including beginners.
|
||||
Don't be afraid to submit a half finished or mediocre contribution and we will help you.
|
||||
@@ -45,6 +49,6 @@ The reason we do this is to ensure, to the extent possible, that we don’t “t
|
||||
|
||||
More information there: https://github.com/GNS3/cla
|
||||
|
||||
### Pull requests
|
||||
### Pull requests
|
||||
|
||||
Creating a pull request is the easiest way to contribute code. Do not hesitate to create one early when contributing for new feature in order to get our feedback.
|
||||
|
||||
70
COPYING
70
COPYING
@@ -272,50 +272,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License notice for Apache Libcloud
|
||||
----------------------------------
|
||||
https://github.com/apache/libcloud/blob/trunk/LICENSE
|
||||
|
||||
Copyright (c) 2010-2015 The Apache Software Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
License notice for requests
|
||||
---------------------------
|
||||
https://github.com/kennethreitz/requests/blob/master/LICENSE
|
||||
|
||||
Copyright (c) 2015 Kenneth Reitz
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
License notice for gns3-converter
|
||||
---------------------------------
|
||||
https://github.com/dlintott/gns3-converter/blob/master/COPYING
|
||||
|
||||
License notice for paramiko
|
||||
---------------------------
|
||||
https://github.com/paramiko/paramiko/blob/master/LICENSE
|
||||
|
||||
License notice for pywin32
|
||||
--------------------------
|
||||
https://github.com/SublimeText/Pywin32/blob/master/License.txt
|
||||
@@ -520,3 +476,29 @@ THE POSSIBILITY OF SUCH DAMAGE.
|
||||
License notice for Python
|
||||
-------------------------
|
||||
https://www.python.org/download/releases/3.4.2/license/
|
||||
|
||||
License notice for BusyBox
|
||||
---------------------------
|
||||
BusyBox is distributed under version 2 of the General Public License
|
||||
https://busybox.net/license.html
|
||||
|
||||
Source code is available here:
|
||||
https://github.com/GNS3/busybox
|
||||
|
||||
|
||||
Licence notice for zipstream
|
||||
-----------------------------
|
||||
zipstream is distributed under version 3 of the General Public License
|
||||
https://github.com/allanlei/python-zipstream/blob/master/LICENSE
|
||||
|
||||
Source code is available here:
|
||||
https://pypi.python.org/pypi/zipstream
|
||||
|
||||
|
||||
Licence notice for aiohttp_cors
|
||||
-------------------------------
|
||||
Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>.
|
||||
|
||||
Licensed under the Apache License, Version 2.0, see LICENSE file for details.
|
||||
|
||||
https://github.com/aio-libs/aiohttp_cors
|
||||
|
||||
@@ -3,6 +3,7 @@ include AUTHORS
|
||||
include INSTALL
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
include tox.ini
|
||||
recursive-include tests *
|
||||
recursive-include gns3 *
|
||||
|
||||
106
README.rst
106
README.rst
@@ -10,98 +10,10 @@ GNS3-gui
|
||||
|
||||
GNS3 GUI repository.
|
||||
|
||||
Linux (Debian based)
|
||||
--------------------
|
||||
Installation
|
||||
------------
|
||||
|
||||
The following instructions have been tested with Ubuntu and Mint.
|
||||
You must be connected to the Internet in order to install the dependencies.
|
||||
|
||||
Dependencies:
|
||||
|
||||
- Python 3.3 or above
|
||||
- Setuptools
|
||||
- PyQt 5 libraries
|
||||
- Apache Libcloud library
|
||||
- Requests library
|
||||
- Paramiko library
|
||||
|
||||
The following commands will install some of these dependencies:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
sudo apt-get install python3-setuptools
|
||||
sudo apt-get install python3-pyqt5
|
||||
sudo apt-get install python3-pyqt5.qtsvg
|
||||
sudo apt-get install python3-pyqt5.qtwebkit
|
||||
|
||||
If you want to test using PyQt4
|
||||
|
||||
.. code:: bash
|
||||
sudo apt-get install python3-pyqt4
|
||||
|
||||
Finally these commands will install the GUI as well as the rest of the dependencies:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd gns3-gui-master
|
||||
sudo python3 setup.py install
|
||||
gns3
|
||||
|
||||
Windows
|
||||
-------
|
||||
|
||||
Please use our `all-in-one installer <https://gns3.com/software/download>`_ to install the stable build.
|
||||
|
||||
If you install via source you need to first install:
|
||||
|
||||
- Python (3.3 or above) - https://www.python.org/downloads/windows/
|
||||
- Pywin32 - https://sourceforge.net/projects/pywin32/
|
||||
- Qt5 - http://www.qt.io/download-open-source/
|
||||
- PyQt5 - http://www.riverbankcomputing.com/software/pyqt/download5
|
||||
- PyCrypto (which if you compile from source, requires Visual Studio 2010 with GMP or MPIR libraries)
|
||||
|
||||
And finally, call
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python setup.py install
|
||||
|
||||
to install the remaining dependencies.
|
||||
|
||||
Mac OS X
|
||||
--------
|
||||
|
||||
Please use our DMG package or you can manually install using the following steps (experimental):
|
||||
|
||||
`First install homebrew <http://brew.sh/>`_.
|
||||
|
||||
Then install the GNS3 dependencies.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
brew install python3
|
||||
brew install qt
|
||||
brew install sip --without-python --with-python3
|
||||
brew install pyqt5 --without-python --with-python3
|
||||
|
||||
If you want to test using PyQt4
|
||||
|
||||
.. code:: bash
|
||||
brew install pyqt --without-python --with-python3
|
||||
|
||||
Finally, install both the GUI & server from the source.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd gns3-gui-master
|
||||
python3 setup.py install
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd gns3-server-master
|
||||
python3 setup.py install
|
||||
|
||||
Or follow this `HOWTO that uses MacPorts <http://binarynature.blogspot.ca/2014/05/install-gns3-early-release-on-mac-os-x.html>`_.
|
||||
https://gns3.com/support/docs
|
||||
|
||||
Development
|
||||
-------------
|
||||
@@ -128,15 +40,3 @@ Or start the app with --debug flag.
|
||||
Due to the fact PyQT intercept you can use a web debugger for inspecting stuff:
|
||||
https://github.com/Kozea/wdb
|
||||
|
||||
|
||||
Test with PyQT4
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to simulate a user with PyQT4:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
export GNS3_QT4=1
|
||||
python gns3/main.py
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-rrequirements.txt
|
||||
|
||||
pep8
|
||||
pytest
|
||||
pytest-pythonpath # useful for running tests outside tox
|
||||
pytest-timeout
|
||||
pytest-capturelog
|
||||
pep8==1.7.0
|
||||
pytest==3.0.3
|
||||
pytest-pythonpath==0.7.1 # useful for running tests outside tox
|
||||
pytest-timeout==1.0.0
|
||||
pytest-capturelog==0.7
|
||||
|
||||
@@ -33,6 +33,8 @@ sys.path.insert(0, os.path.dirname(sys.executable))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(sys.executable), 'site-packages'))
|
||||
|
||||
sys.frozen = True
|
||||
sys.executable = "/Applications/GNS3.app/Contents/MacOS/gns3"
|
||||
os.environ["_"] = "/Applications/GNS3.app/Contents/MacOS/gns3"
|
||||
|
||||
module = importlib.import_module("gns3.main")
|
||||
module.main()
|
||||
|
||||
29
gns3-gui.appdata.xml
Normal file
29
gns3-gui.appdata.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright 2016 Athmane Madjoudj <athmane@fedoraproject.org> -->
|
||||
<component type="desktop">
|
||||
<id>gns3-gui.desktop</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<name>GNS3</name>
|
||||
<summary>Graphical Network Simulator 3</summary>
|
||||
<description>
|
||||
<p>
|
||||
GNS3 is a graphical network simulator that allows you to design complex network
|
||||
topologies. You may run simulations or configure devices ranging from simple
|
||||
workstations to powerful routers.
|
||||
</p>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://a.fsdn.com/con/app/proj/gns-3/screenshots/127765.jpg</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://a.fsdn.com/con/app/proj/gns-3/screenshots/127755.jpg</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://a.fsdn.com/con/app/proj/gns-3/screenshots/127755.jpg</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<url type="homepage">http://gns3.com/</url>
|
||||
<update_contact>athmane_at_fedoraproject.org</update_contact>
|
||||
</component>
|
||||
9
gns3-gui.desktop
Normal file
9
gns3-gui.desktop
Normal file
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Name=GNS3
|
||||
GenericName=Graphical Network Simulator 3
|
||||
Comment=Graphical Network Simulator 3
|
||||
Exec=gns3
|
||||
Icon=gns3
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Application;Network;Qt;
|
||||
@@ -19,6 +19,7 @@
|
||||
import sys
|
||||
|
||||
from .qt import QtWidgets, QtGui, QtCore
|
||||
from gns3.utils import parse_version
|
||||
from .version import __version__
|
||||
|
||||
import logging
|
||||
@@ -29,7 +30,16 @@ class Application(QtWidgets.QApplication):
|
||||
file_open_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, argv):
|
||||
|
||||
self.setStyle(QtWidgets.QStyleFactory.create("Fusion"))
|
||||
# both Qt and PyQt must be version >= 5.6 in order to enable high DPI scaling
|
||||
if parse_version(QtCore.QT_VERSION_STR) >= parse_version("5.6") and parse_version(QtCore.PYQT_VERSION_STR) >= parse_version("5.6"):
|
||||
# only available starting Qt version 5.6
|
||||
self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
|
||||
self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
|
||||
|
||||
super().__init__(argv)
|
||||
|
||||
# this info is necessary for QSettings
|
||||
self.setOrganizationName("GNS3")
|
||||
self.setOrganizationDomain("gns3.net")
|
||||
@@ -41,7 +51,7 @@ class Application(QtWidgets.QApplication):
|
||||
self.open_file_at_startup = None
|
||||
|
||||
def event(self, event):
|
||||
# When you double click file you receive an event
|
||||
# When you double click file you receive an event
|
||||
# and not the file as command line parameter
|
||||
if sys.platform.startswith("darwin"):
|
||||
if isinstance(event, QtGui.QFileOpenEvent):
|
||||
|
||||
327
gns3/base_node.py
Normal file
327
gns3/base_node.py
Normal file
@@ -0,0 +1,327 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Base class for node classes.
|
||||
"""
|
||||
|
||||
from .qt import QtCore
|
||||
from .ports.port import Port
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseNode(QtCore.QObject):
|
||||
|
||||
"""
|
||||
BaseNode implementation.
|
||||
|
||||
:param module: Module instance
|
||||
:param server: client connection to a server
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
# signals used to let the GUI know about some events.
|
||||
created_signal = QtCore.Signal(int)
|
||||
started_signal = QtCore.Signal()
|
||||
stopped_signal = QtCore.Signal()
|
||||
suspended_signal = QtCore.Signal()
|
||||
updated_signal = QtCore.Signal()
|
||||
loaded_signal = QtCore.Signal()
|
||||
deleted_signal = QtCore.Signal()
|
||||
error_signal = QtCore.Signal(int, str)
|
||||
warning_signal = QtCore.Signal(int, str)
|
||||
server_error_signal = QtCore.Signal(int, str)
|
||||
|
||||
_instance_count = 1
|
||||
_allocated_names = set()
|
||||
|
||||
# node statuses
|
||||
stopped = 0
|
||||
started = 1
|
||||
suspended = 2
|
||||
|
||||
# node categories
|
||||
routers = 0
|
||||
switches = 1
|
||||
end_devices = 2
|
||||
security_devices = 3
|
||||
|
||||
def __init__(self, module, compute, project):
|
||||
|
||||
super().__init__()
|
||||
# create an unique ID
|
||||
self._id = BaseNode._instance_count
|
||||
BaseNode._instance_count += 1
|
||||
|
||||
self._module = module
|
||||
self._compute = compute
|
||||
assert project is not None
|
||||
self._project = project
|
||||
self._initialized = False
|
||||
self._loading = False
|
||||
self._status = BaseNode.stopped
|
||||
self._ports = []
|
||||
self._links = set()
|
||||
|
||||
def links(self):
|
||||
"""
|
||||
Links connected to the node
|
||||
"""
|
||||
return self._links
|
||||
|
||||
def addLink(self, link):
|
||||
self._links.add(link)
|
||||
|
||||
def deleteLink(self, link):
|
||||
self._links.remove(link)
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
"""
|
||||
Reset the instance count.
|
||||
"""
|
||||
|
||||
cls._instance_count = 1
|
||||
|
||||
def module(self):
|
||||
"""
|
||||
Returns this node module.
|
||||
|
||||
:returns: Module instance
|
||||
"""
|
||||
|
||||
return self._module
|
||||
|
||||
def compute(self):
|
||||
"""
|
||||
Returns this node compute.
|
||||
|
||||
:returns: Compute instance
|
||||
"""
|
||||
return self._compute
|
||||
|
||||
def project(self):
|
||||
"""
|
||||
Returns this node project.
|
||||
|
||||
:returns: Project instance
|
||||
"""
|
||||
|
||||
return self._project
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
Returns this node identifier.
|
||||
|
||||
:returns: node identifier (integer)
|
||||
"""
|
||||
|
||||
return self._id
|
||||
|
||||
def setId(self, new_id):
|
||||
"""
|
||||
Sets an identifier for this node.
|
||||
|
||||
:param new_id: node identifier (integer)
|
||||
"""
|
||||
|
||||
self._id = new_id
|
||||
|
||||
# update the instance count to avoid conflicts
|
||||
if new_id >= BaseNode._instance_count:
|
||||
BaseNode._instance_count = new_id + 1
|
||||
|
||||
def status(self):
|
||||
"""
|
||||
Returns the status of this node.
|
||||
0 = stopped, 1 = started, 2 = suspended.
|
||||
|
||||
:returns: node status (integer)
|
||||
"""
|
||||
|
||||
return self._status
|
||||
|
||||
def setStatus(self, status):
|
||||
"""
|
||||
Sets a status for this node.
|
||||
0 = stopped, 1 = started, 2 = suspended.
|
||||
|
||||
:param status: node status (integer)
|
||||
"""
|
||||
|
||||
if status == self._status:
|
||||
return
|
||||
self._status = status
|
||||
if status == self.started:
|
||||
for port in self._ports:
|
||||
# set ports as started
|
||||
port.setStatus(Port.started)
|
||||
self.started_signal.emit()
|
||||
log.info("{} has started".format(self.name()))
|
||||
elif status == self.stopped:
|
||||
for port in self._ports:
|
||||
# set ports as stopped
|
||||
port.setStatus(Port.stopped)
|
||||
self.stopped_signal.emit()
|
||||
log.info("{} has stopped".format(self.name()))
|
||||
elif status == self.suspended:
|
||||
for port in self._ports:
|
||||
# set ports as suspended
|
||||
port.setStatus(Port.suspended)
|
||||
self.suspended_signal.emit()
|
||||
log.info("{} has suspended".format(self.name()))
|
||||
|
||||
def initialized(self):
|
||||
"""
|
||||
Returns if the node has been initialized
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._initialized
|
||||
|
||||
def setInitialized(self, initialized):
|
||||
"""
|
||||
Sets if the node has been initialized
|
||||
|
||||
:param initialized: boolean
|
||||
"""
|
||||
|
||||
self._initialized = initialized
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this node.
|
||||
Must be overloaded.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def ports(self):
|
||||
"""
|
||||
Returns all the ports for this node.
|
||||
|
||||
:returns: list of Port instances
|
||||
"""
|
||||
|
||||
return self._ports
|
||||
|
||||
@staticmethod
|
||||
def defaultCategories():
|
||||
"""
|
||||
Returns the default categories.
|
||||
|
||||
:returns: dict
|
||||
"""
|
||||
|
||||
categories = {"Routers": BaseNode.routers,
|
||||
"Switches": BaseNode.switches,
|
||||
"End devices": BaseNode.end_devices,
|
||||
"Security devices": BaseNode.security_devices}
|
||||
|
||||
return categories
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
Must be overloaded.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
"""
|
||||
Returns the symbol name (for the nodes view).
|
||||
|
||||
:returns: name (string)
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def categories(self):
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node category (integer)
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Must be overloaded.
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def controllerHttpPost(self, path, callback, body={}, context={}, **kwargs):
|
||||
"""
|
||||
POST on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param body: params to send (dictionary)
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.post(path, callback, body=body, context=context, **kwargs)
|
||||
|
||||
def controllerHttpPut(self, path, callback, body={}, context={}, **kwargs):
|
||||
"""
|
||||
PUT on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param body: params to send (dictionary)
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.put(path, callback, body=body, context=context, **kwargs)
|
||||
|
||||
def controllerHttpGet(self, path, callback, context={}, **kwargs):
|
||||
"""
|
||||
Get on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param body: params to send (dictionary)
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.get(path, callback, context=context, **kwargs)
|
||||
|
||||
|
||||
def controllerHttpDelete(self, path, callback, context={}, **kwargs):
|
||||
"""
|
||||
Delete on current server / project
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
:param context: Pass a context to the response callback
|
||||
"""
|
||||
|
||||
self._project.delete(path, callback, context=context, **kwargs)
|
||||
120
gns3/compute.py
Normal file
120
gns3/compute.py
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
|
||||
|
||||
class Compute:
|
||||
"""
|
||||
A compute node on the remote server
|
||||
"""
|
||||
|
||||
def __init__(self, compute_id=None):
|
||||
if compute_id is None:
|
||||
compute_id = str(uuid.uuid4())
|
||||
self._compute_id = compute_id
|
||||
self._name = compute_id
|
||||
self._connected = False
|
||||
self._protocol = None
|
||||
self._host = None
|
||||
self._port = 3080
|
||||
self._user = None
|
||||
self._password = None
|
||||
self._cpu_usage_percent = None
|
||||
self._memory_usage_percent = None
|
||||
self._capabilities = {
|
||||
"node_types": []
|
||||
}
|
||||
|
||||
def id(self):
|
||||
return self._compute_id
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def setName(self, name):
|
||||
self._name = name
|
||||
|
||||
def connected(self):
|
||||
return self._connected
|
||||
|
||||
def setConnected(self, value):
|
||||
self._connected = value
|
||||
|
||||
def port(self):
|
||||
return self._port
|
||||
|
||||
def setPort(self, port):
|
||||
self._port = port
|
||||
|
||||
def user(self):
|
||||
return self._user
|
||||
|
||||
def setUser(self, user):
|
||||
self._user = user
|
||||
|
||||
def setPassword(self, password):
|
||||
self._password = password
|
||||
|
||||
def protocol(self):
|
||||
return self._protocol
|
||||
|
||||
def setProtocol(self, protocol):
|
||||
self._protocol = protocol
|
||||
|
||||
def host(self):
|
||||
return self._host
|
||||
|
||||
def setHost(self, host):
|
||||
self._host = host
|
||||
|
||||
def setCpuUsagePercent(self, usage):
|
||||
self._cpu_usage_percent = usage
|
||||
|
||||
def cpuUsagePercent(self):
|
||||
return self._cpu_usage_percent
|
||||
|
||||
def setMemoryUsagePercent(self, usage):
|
||||
self._memory_usage_percent = usage
|
||||
|
||||
def memoryUsagePercent(self):
|
||||
return self._memory_usage_percent
|
||||
|
||||
def capabilities(self):
|
||||
return self._capabilities
|
||||
|
||||
def setCapabilities(self, val):
|
||||
self._capabilities = val
|
||||
|
||||
def __str__(self):
|
||||
return self._compute_id
|
||||
|
||||
def __json__(self):
|
||||
return {
|
||||
"host": self._host,
|
||||
"port": self._port,
|
||||
"protocol": self._protocol,
|
||||
"user": self._user,
|
||||
"password": self._password,
|
||||
"name": self._name,
|
||||
"compute_id": self._compute_id
|
||||
}
|
||||
|
||||
def __eq__(self, v):
|
||||
if isinstance(v, Compute):
|
||||
return self.__json__() == v.__json__()
|
||||
return False
|
||||
194
gns3/compute_manager.py
Normal file
194
gns3/compute_manager.py
Normal file
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .qt import QtCore
|
||||
|
||||
from .compute import Compute
|
||||
from .controller import Controller
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import urllib
|
||||
import datetime
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComputeManager(QtCore.QObject):
|
||||
created_signal = QtCore.Signal(str)
|
||||
updated_signal = QtCore.Signal(str)
|
||||
deleted_signal = QtCore.Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._computes = {}
|
||||
self._controller = Controller.instance()
|
||||
self._controller.connected_signal.connect(self._controllerConnectedSlot)
|
||||
self._controllerConnectedSlot()
|
||||
|
||||
# If we receive fresh data from the notification feed no need to refresh via an API call
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
|
||||
self._timer = QtCore.QTimer()
|
||||
self._timer.setInterval(1000)
|
||||
self._timer.timeout.connect(self._refreshComputesSlot)
|
||||
self._timer.start()
|
||||
|
||||
def _refreshComputesSlot(self):
|
||||
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 5:
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
self._controller.get("/computes", self._listComputesCallback, showProgress=True)
|
||||
|
||||
def _controllerConnectedSlot(self):
|
||||
if self._controller.connected():
|
||||
self._controller.get("/computes", self._listComputesCallback)
|
||||
|
||||
def _listComputesCallback(self, result, error=False, **kwargs):
|
||||
if error is True:
|
||||
log.error("Error while getting compute list: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
for compute in result:
|
||||
self.computeDataReceivedCallback(compute)
|
||||
|
||||
def computeDataReceivedCallback(self, compute):
|
||||
"""
|
||||
Called when we received data from a compute
|
||||
node.
|
||||
"""
|
||||
self._last_computes_refresh = datetime.datetime.now().timestamp()
|
||||
|
||||
new_node = False
|
||||
compute_id = compute["compute_id"]
|
||||
if compute_id not in self._computes:
|
||||
new_node = True
|
||||
self._computes[compute_id] = Compute(compute_id)
|
||||
|
||||
self._computes[compute_id].setName(compute["name"])
|
||||
self._computes[compute_id].setConnected(compute["connected"])
|
||||
self._computes[compute_id].setProtocol(compute["protocol"])
|
||||
self._computes[compute_id].setHost(compute["host"])
|
||||
self._computes[compute_id].setPort(compute["port"])
|
||||
self._computes[compute_id].setUser(compute["user"])
|
||||
self._computes[compute_id].setCpuUsagePercent(compute["cpu_usage_percent"])
|
||||
self._computes[compute_id].setMemoryUsagePercent(compute["memory_usage_percent"])
|
||||
self._computes[compute_id].setCapabilities(compute["capabilities"])
|
||||
|
||||
if new_node:
|
||||
self.created_signal.emit(compute_id)
|
||||
else:
|
||||
self.updated_signal.emit(compute_id)
|
||||
|
||||
def computes(self):
|
||||
"""
|
||||
:returns: List of computes nodes
|
||||
"""
|
||||
return list(self._computes.values())
|
||||
|
||||
def vmCompute(self):
|
||||
"""
|
||||
:returns: The GNS3 VM compute node or None
|
||||
"""
|
||||
try:
|
||||
return self._computes["vm"]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def localCompute(self):
|
||||
"""
|
||||
:returns: The local compute node or None
|
||||
"""
|
||||
try:
|
||||
return self._computes["local"]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def localPlatform(self):
|
||||
"""
|
||||
Return the platform of the local compute.
|
||||
|
||||
With a remote controller it could be different of our local platform
|
||||
"""
|
||||
return self.localCompute().capabilities().get("platform", sys.platform)
|
||||
|
||||
def remoteComputes(self):
|
||||
"""
|
||||
:returns: List of non local and non VM computes
|
||||
"""
|
||||
return [c for c in self._computes.values() if c.id() != "local" and c.id() != "vm"]
|
||||
|
||||
def getCompute(self, compute_id):
|
||||
if compute_id.startswith("http:") or compute_id.startswith("https:"):
|
||||
u = urllib.parse.urlsplit(compute_id)
|
||||
for compute in self._computes.values():
|
||||
if "{}:{}".format(compute.host(), compute.port()) == u.netloc:
|
||||
return compute
|
||||
raise KeyError("Compute {} is missing.".format(compute_id))
|
||||
if compute_id not in self._computes:
|
||||
self._computes[compute_id] = Compute(compute_id)
|
||||
self.created_signal.emit(compute_id)
|
||||
return self._computes[compute_id]
|
||||
|
||||
def deleteCompute(self, compute_id):
|
||||
if compute_id in self._computes:
|
||||
compute = self._computes[compute_id]
|
||||
del self._computes[compute_id]
|
||||
self._controller.delete("/computes/" + compute_id, None)
|
||||
self.deleted_signal.emit(compute_id)
|
||||
|
||||
def updateList(self, computes):
|
||||
"""
|
||||
Sync an array of compute server with remote
|
||||
"""
|
||||
for compute_id in copy.copy(self._computes):
|
||||
# Delete compute on controller not in the new computes
|
||||
if compute_id in ["local", "vm"]:
|
||||
continue
|
||||
|
||||
if compute_id not in [c.id() for c in computes]:
|
||||
log.debug("Delete compute %s", compute_id)
|
||||
self.deleteCompute(compute_id)
|
||||
else:
|
||||
# Update the changed nodes
|
||||
for c in computes:
|
||||
if c.id() == compute_id and c != self._computes[compute_id]:
|
||||
log.debug("Update compute %s", compute_id)
|
||||
self._controller.put("/computes/" + compute_id, None, body=c.__json__())
|
||||
self._computes[compute_id] = c
|
||||
# Create the new nodes
|
||||
for compute in computes:
|
||||
if compute.id() not in self._computes:
|
||||
log.debug("Create compute %s", compute.id())
|
||||
self._controller.post("/computes", None, body=compute.__json__())
|
||||
self._computes[compute.id()] = compute
|
||||
|
||||
@staticmethod
|
||||
def reset():
|
||||
ComputeManager._instance = None
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of ComputeManager.
|
||||
:returns: instance of ComputeManager
|
||||
"""
|
||||
|
||||
if not hasattr(ComputeManager, '_instance') or ComputeManager._instance is None:
|
||||
ComputeManager._instance = ComputeManager()
|
||||
return ComputeManager._instance
|
||||
130
gns3/compute_summary_view.py
Normal file
130
gns3/compute_summary_view.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Compute summary view that list all the compute, their status.
|
||||
"""
|
||||
|
||||
import sip
|
||||
|
||||
from .qt import QtGui, QtCore, QtWidgets
|
||||
from .compute_manager import ComputeManager
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComputeItem(QtWidgets.QTreeWidgetItem):
|
||||
|
||||
"""
|
||||
Custom item for the QTreeWidget instance
|
||||
(topology summary view).
|
||||
|
||||
:param parent: parent widget
|
||||
:param compute: Compute instance
|
||||
"""
|
||||
|
||||
def __init__(self, parent, compute):
|
||||
|
||||
super().__init__(parent)
|
||||
self._compute = compute
|
||||
self._parent = parent
|
||||
self._status = "unknown"
|
||||
|
||||
self._refreshStatusSlot()
|
||||
|
||||
def _refreshStatusSlot(self):
|
||||
"""
|
||||
Changes the icon to show the node status (started, stopped etc.)
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
|
||||
usage = None
|
||||
text = self._compute.name()
|
||||
|
||||
if self._compute.cpuUsagePercent() is not None:
|
||||
text = "{} CPU {}%, RAM {}%".format(text, self._compute.cpuUsagePercent(), self._compute.memoryUsagePercent())
|
||||
|
||||
self.setText(0, text)
|
||||
self.setToolTip(0, text + " on " + self._compute.capabilities().get("platform", ""))
|
||||
|
||||
if self._compute.connected():
|
||||
self._status = "connected"
|
||||
if usage is None or (self._compute.cpuUsagePercent() < 90 and self._compute.memoryUsagePercent() < 90):
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_green.svg'))
|
||||
else:
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_yellow.svg'))
|
||||
else:
|
||||
if self._status == "unknown":
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_gray.svg'))
|
||||
else:
|
||||
self._status = "stopped"
|
||||
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
|
||||
|
||||
|
||||
class ComputeSummaryView(QtWidgets.QTreeWidget):
|
||||
|
||||
"""
|
||||
Compute summary view implementation.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self._computes = {}
|
||||
|
||||
ComputeManager.instance().created_signal.connect(self._computeAddedSlot)
|
||||
ComputeManager.instance().updated_signal.connect(self._computeUpdatedSlot)
|
||||
ComputeManager.instance().deleted_signal.connect(self._computeRemovedSlot)
|
||||
for compute in ComputeManager.instance().computes():
|
||||
self._computeAddedSlot(compute.id())
|
||||
|
||||
def _computeAddedSlot(self, compute_id):
|
||||
"""
|
||||
Called when a compute is added to the list of computes
|
||||
|
||||
:params url: URL of the compute
|
||||
"""
|
||||
|
||||
compute = ComputeManager.instance().getCompute(compute_id)
|
||||
self._computes[compute_id] = ComputeItem(self, compute)
|
||||
|
||||
def _computeUpdatedSlot(self, compute_id):
|
||||
"""
|
||||
Called when a compute is removed to the list of computes
|
||||
|
||||
:params url: URL of the compute
|
||||
"""
|
||||
|
||||
if compute_id in self._computes:
|
||||
self._computes[compute_id]._refreshStatusSlot()
|
||||
|
||||
def _computeRemovedSlot(self, compute_id):
|
||||
"""
|
||||
Called when a compute is removed to the list of computes
|
||||
|
||||
:params url: URL of the compute
|
||||
"""
|
||||
|
||||
if compute_id in self._computes:
|
||||
self.takeTopLevelItem(self.indexOfTopLevelItem(self._computes[compute_id]))
|
||||
del self._computes[compute_id]
|
||||
@@ -1,21 +0,0 @@
|
||||
!
|
||||
|
||||
kerberos password
|
||||
crypto RSA-key-pair %h.mydomain.com 0 1014940935
|
||||
30820155 02010030 0D06092A 864886F7 0D010101 05000482 013F3082 013B0201
|
||||
00024100 A7EA2920 73033037 689F8166 B6AEA7FF 91015466 7379FA4F D7B175C3
|
||||
8D5D1E56 89B00E73 D5553491 06D651DA 71213D18 3E4EAF44 8C5F05F1 E8C1FE47
|
||||
B07D5A1B 02030100 01024049 FE964106 6DD14199 8930ACE2 B3F4B45A 620B9F5A
|
||||
23D67A78 C26AF2D1 C8C72504 987ADD3E 2755DCC4 70AADB86 679171D7 54A9038F
|
||||
0EB080E7 8B514EB8 8A038102 2100D588 DF0A6D31 AEF5C231 5A4A3459 5D3FD973
|
||||
F1A13EA8 2C25D210 6ACD4733 39AF0221 00C94EC2 9428B371 2599E7EA 8C89E86C
|
||||
E188F689 3AFCFE7A 59B42810 E83DABBD 55022100 944FB792 D75ACDC9 96328F22
|
||||
C10F5CAC 2F4DCF83 0E30E250 F6813E9D 0B99F1B3 02204863 D126D428 0B05197E
|
||||
4362FC68 9F56CF18 D0AA6CB5 DA2B8DD4 66980D2D 47ED0221 00991914 B6CDC66E
|
||||
60AF0332 D5FB2771 B9F0317B 886E6E48 B86CDFDF 3FC1D48E CA
|
||||
quit
|
||||
305C300D 06092A86 4886F70D 01010105 00034B00 30480241 00A7EA29 20730330
|
||||
37689F81 66B6AEA7 FF910154 667379FA 4FD7B175 C38D5D1E 5689B00E 73D55534
|
||||
9106D651 DA71213D 183E4EAF 448C5F05 F1E8C1FE 47B07D5A 1B020301 0001
|
||||
quit
|
||||
end
|
||||
@@ -25,13 +25,10 @@ import logging
|
||||
import struct
|
||||
import sip
|
||||
import json
|
||||
from .qt import QtCore
|
||||
|
||||
from .node import Node
|
||||
from .qt import QtCore
|
||||
from .version import __version__
|
||||
try:
|
||||
from gns3converter import __version__ as gns3converter_version
|
||||
except ImportError:
|
||||
gns3converter_version = "Not installed"
|
||||
|
||||
|
||||
class ConsoleCmd(cmd.Cmd):
|
||||
@@ -45,7 +42,6 @@ class ConsoleCmd(cmd.Cmd):
|
||||
if hasattr(sys, "frozen"):
|
||||
compiled = "(compiled)"
|
||||
print("GNS3 version is {} {}".format(__version__, compiled))
|
||||
print("GNS3 Converter version is {}".format(gns3converter_version))
|
||||
print("Python version is {}.{}.{} ({}-bit) with {} encoding".format(sys.version_info[0],
|
||||
sys.version_info[1],
|
||||
sys.version_info[2],
|
||||
@@ -188,14 +184,9 @@ class ConsoleCmd(cmd.Cmd):
|
||||
:param node: Node instance
|
||||
"""
|
||||
|
||||
name = node.name()
|
||||
console_port = node.console()
|
||||
console_host = node.server().host()
|
||||
try:
|
||||
from .telnet_console import telnetConsole
|
||||
telnetConsole(name, console_host, console_port)
|
||||
except (OSError, ValueError) as e:
|
||||
print("Cannot start console application: {}".format(e))
|
||||
from .telnet_console import nodeTelnetConsole
|
||||
nodeTelnetConsole(node, console_port)
|
||||
|
||||
def do_debug(self, args):
|
||||
"""
|
||||
@@ -278,21 +269,21 @@ class ConsoleCmd(cmd.Cmd):
|
||||
params.pop(0)
|
||||
for param in params:
|
||||
node_name = param
|
||||
node_id = None
|
||||
base_node_id = None
|
||||
|
||||
# get the node ID
|
||||
for node in self._topology.nodes():
|
||||
if node.name() == node_name:
|
||||
node_id = node.id()
|
||||
base_node_id = node.id()
|
||||
break
|
||||
|
||||
if node_id is None:
|
||||
if base_node_id is None:
|
||||
print("{}: no such device".format(node_name))
|
||||
continue
|
||||
|
||||
if "nodes" in topology["topology"]:
|
||||
for node in topology["topology"]["nodes"]:
|
||||
if node["id"] == node_id:
|
||||
if node["id"] == base_node_id:
|
||||
print(json.dumps(node, sort_keys=True, indent=4))
|
||||
break
|
||||
|
||||
@@ -309,6 +300,9 @@ class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
Show topology info of a device:
|
||||
show run <device_name>
|
||||
|
||||
Show the GNS3 VM status
|
||||
show gns3vm
|
||||
"""
|
||||
|
||||
if '?' in args or args.strip() == "":
|
||||
@@ -320,6 +314,8 @@ class ConsoleCmd(cmd.Cmd):
|
||||
self._show_device(params)
|
||||
elif params[0] == "run":
|
||||
self._show_run(params)
|
||||
elif params[0] == "gns3vm":
|
||||
self._show_gnsvm(params)
|
||||
else:
|
||||
print(self.do_show.__doc__)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import struct
|
||||
import inspect
|
||||
import datetime
|
||||
|
||||
from .qt import QtCore
|
||||
from .qt import QtCore, Qt
|
||||
from .topology import Topology
|
||||
from .version import __version__
|
||||
from .console_cmd import ConsoleCmd
|
||||
@@ -29,9 +29,36 @@ from .pycutext import PyCutExt
|
||||
from .modules import MODULES
|
||||
from .local_config import LocalConfig
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleLogHandler(logging.StreamHandler):
|
||||
"""
|
||||
Display log event to the console
|
||||
"""
|
||||
|
||||
def emit(self, record):
|
||||
message = self.format(record)
|
||||
level_no = record.levelno
|
||||
if level_no >= logging.ERROR:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "error")
|
||||
elif level_no >= logging.WARNING:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "warning")
|
||||
elif level_no >= logging.INFO:
|
||||
# To avoid noise on console we display all event only if log level is debug
|
||||
# or if we force the display in the log record
|
||||
if "show" in record.__dict__ or logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "debug")
|
||||
elif level_no >= logging.DEBUG:
|
||||
self._console_view.write_message_signal.emit("{}\n".format(message), "debug")
|
||||
|
||||
|
||||
class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
|
||||
# Emit this signal to write a message on console
|
||||
write_message_signal = QtCore.Signal(str, str)
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
# Set the prompt PyCutExt
|
||||
@@ -41,13 +68,10 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
# Set introduction message
|
||||
bitness = struct.calcsize("P") * 8
|
||||
current_year = datetime.date.today().year
|
||||
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {}.\n" \
|
||||
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {} and PyQt {}.\n" \
|
||||
"Copyright (c) 2006-{} GNS3 Technologies.\n" \
|
||||
"Use Help -> GNS3 Doctor to detect common issues." \
|
||||
"".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, current_year)
|
||||
|
||||
if LocalConfig.instance().experimental():
|
||||
self.intro += "\nWARNING: Experimental features enable. You can use some unfinished features and lost data."
|
||||
"".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, Qt.PYQT_VERSION_STR, current_year)
|
||||
|
||||
# Parent class initialization
|
||||
try:
|
||||
@@ -66,14 +90,39 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
except Exception as e:
|
||||
sys.stderr.write(e)
|
||||
|
||||
self._handleLogs()
|
||||
|
||||
if LocalConfig.instance().experimental():
|
||||
log.warning("WARNING: Experimental features enable. You can use some unfinished features and lost data.")
|
||||
|
||||
for module in MODULES:
|
||||
instance = module.instance()
|
||||
instance.notification_signal.connect(self.writeNotification)
|
||||
|
||||
self.write_message_signal.connect(self._writeMessageSlot)
|
||||
|
||||
# required for Cmd module (do_help etc.)
|
||||
self.stdout = sys.stdout
|
||||
self._topology = Topology.instance()
|
||||
|
||||
def _writeMessageSlot(self, message, level):
|
||||
if level == "error":
|
||||
self.write(message, error=True)
|
||||
elif level == "warning":
|
||||
self.write(message, warning=True)
|
||||
else:
|
||||
self.write(message)
|
||||
|
||||
def _handleLogs(self):
|
||||
"""
|
||||
Catch log message and display them
|
||||
"""
|
||||
|
||||
log = logging.getLogger()
|
||||
log_handler = ConsoleLogHandler()
|
||||
log_handler._console_view = self
|
||||
log.addHandler(log_handler)
|
||||
|
||||
def isatty(self):
|
||||
"""
|
||||
For exception handling purposes
|
||||
@@ -144,15 +193,15 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
self.write(details)
|
||||
self.write("\n")
|
||||
|
||||
def writeError(self, node_id, message):
|
||||
def writeError(self, base_node_id, message):
|
||||
"""
|
||||
Write error messages.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: base node identifier
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
node = Topology.instance().getNode(node_id)
|
||||
node = Topology.instance().getNode(base_node_id)
|
||||
name = ""
|
||||
if node and node.name():
|
||||
name = " {}:".format(node.name())
|
||||
@@ -162,15 +211,15 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
self.write(text, error=True)
|
||||
self.write("\n")
|
||||
|
||||
def writeWarning(self, node_id, message):
|
||||
def writeWarning(self, base_node_id, message):
|
||||
"""
|
||||
Write warning messages.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: base node identifier
|
||||
:param message: warning message
|
||||
"""
|
||||
|
||||
node = Topology.instance().getNode(node_id)
|
||||
node = Topology.instance().getNode(base_node_id)
|
||||
name = ""
|
||||
if node and node.name():
|
||||
name = " {}:".format(node.name())
|
||||
@@ -180,26 +229,26 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
self.write(text, warning=True)
|
||||
self.write("\n")
|
||||
|
||||
def writeServerError(self, node_id, message):
|
||||
def writeServerError(self, base_node_id, message):
|
||||
"""
|
||||
Write server error messages coming from the server.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: Base node identifier
|
||||
:param code: error code
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
node = Topology.instance().getNode(node_id)
|
||||
node = Topology.instance().getNode(base_node_id)
|
||||
server = name = ""
|
||||
if node:
|
||||
if node.name():
|
||||
name = " {}:".format(node.name())
|
||||
server = "from {}".format(node.server().url())
|
||||
server = "from {}".format(node.compute().name())
|
||||
|
||||
text = "Server error {server}:{name} {message}".format(server=server,
|
||||
name=name,
|
||||
message=message)
|
||||
self.write(text, error=True)
|
||||
self.write(text.strip(), error=True)
|
||||
self.write("\n")
|
||||
|
||||
def _run(self):
|
||||
|
||||
213
gns3/controller.py
Normal file
213
gns3/controller.py
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import tempfile
|
||||
|
||||
from .qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from .symbol import Symbol
|
||||
from .local_server_config import LocalServerConfig
|
||||
from .settings import LOCAL_SERVER_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Controller(QtCore.QObject):
|
||||
"""
|
||||
An instance of the GNS3 server controller
|
||||
"""
|
||||
connected_signal = QtCore.Signal()
|
||||
connection_failed_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__()
|
||||
self._connected = False
|
||||
self._connecting = False
|
||||
self._cache_directory = tempfile.TemporaryDirectory()
|
||||
self._http_client = None
|
||||
# If it's the first error we display an alert box to the user
|
||||
self._first_error = True
|
||||
|
||||
def host(self):
|
||||
return self._http_client.host()
|
||||
|
||||
def isRemote(self):
|
||||
"""
|
||||
:returns Boolean: True if the controller is remote
|
||||
"""
|
||||
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
|
||||
return not settings["auto_start"]
|
||||
|
||||
def connecting(self):
|
||||
"""
|
||||
:returns: True if connection is in progress
|
||||
"""
|
||||
return self._connecting
|
||||
|
||||
def connected(self):
|
||||
"""
|
||||
Is the controller connected
|
||||
"""
|
||||
return self._connected
|
||||
|
||||
def httpClient(self):
|
||||
"""
|
||||
:returns: HTTP client for connected to the controller
|
||||
"""
|
||||
return self._http_client
|
||||
|
||||
def setHttpClient(self, http_client):
|
||||
"""
|
||||
:param http_client: Instance of HTTP client to communicate with the server
|
||||
"""
|
||||
self._http_client = http_client
|
||||
if self._http_client:
|
||||
self._http_client.connection_connected_signal.connect(self._httpClientConnectedSlot)
|
||||
self._connected = False
|
||||
self._connecting = True
|
||||
self.get('/version', self._versionGetSlot)
|
||||
|
||||
def _versionGetSlot(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Called after the inital version get
|
||||
"""
|
||||
if error:
|
||||
if self._first_error:
|
||||
self._connecting = False
|
||||
self.connection_failed_signal.emit()
|
||||
if "message" in result:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Connection", result["message"])
|
||||
# Try to connect again in 1 seconds
|
||||
QtCore.QTimer.singleShot(1000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_error))
|
||||
self._first_error = False
|
||||
else:
|
||||
self._first_error = True
|
||||
|
||||
def _httpClientConnectedSlot(self):
|
||||
if not self._connected:
|
||||
self._connected = True
|
||||
self._connecting = False
|
||||
self.connected_signal.emit()
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("GET", *args, **kwargs)
|
||||
|
||||
def getCompute(self, path, compute_id, *args, **kwargs):
|
||||
"""
|
||||
API get on a specific compute
|
||||
"""
|
||||
compute_id = self.__fix_compute_id(compute_id)
|
||||
path = "/computes/{}{}".format(compute_id, path)
|
||||
return self.get(path, *args, **kwargs)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("POST", *args, **kwargs)
|
||||
|
||||
def postCompute(self, path, compute_id, *args, **kwargs):
|
||||
"""
|
||||
API post on a specific compute
|
||||
"""
|
||||
compute_id = self.__fix_compute_id(compute_id)
|
||||
path = "/computes/{}{}".format(compute_id, path)
|
||||
return self.post(path, *args, **kwargs)
|
||||
|
||||
def __fix_compute_id(self, compute_id):
|
||||
"""
|
||||
Support for remote server <= 1.5
|
||||
This fix should be not require after the 2.1
|
||||
when all the appliance template will be managed
|
||||
on server
|
||||
"""
|
||||
if compute_id.startswith("http:") or compute_id.startswith("https:"):
|
||||
from .compute_manager import ComputeManager
|
||||
return ComputeManager.instance().getCompute(compute_id).id()
|
||||
return compute_id
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("PUT", *args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("DELETE", *args, **kwargs)
|
||||
|
||||
def createHTTPQuery(self, method, path, *args, **kwargs):
|
||||
"""
|
||||
Forward the query to the HTTP client or controller depending of the path
|
||||
"""
|
||||
if self._http_client:
|
||||
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
|
||||
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
return self._http_client.getSynchronous(endpoint, timeout)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of Controller.
|
||||
:returns: instance of Controller
|
||||
"""
|
||||
|
||||
if not hasattr(Controller, '_instance') or Controller._instance is None:
|
||||
Controller._instance = Controller()
|
||||
return Controller._instance
|
||||
|
||||
def getStatic(self, url, callback):
|
||||
"""
|
||||
Get a URL from the /static on controller and cache it on disk
|
||||
|
||||
:param url: URL without the protocol and host part
|
||||
:param callback: Callback to call when file is ready
|
||||
"""
|
||||
|
||||
if not self._http_client:
|
||||
return
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(url.encode())
|
||||
if ".svg" in url:
|
||||
extension = ".svg"
|
||||
else:
|
||||
extension = ".png"
|
||||
path = os.path.join(self._cache_directory.name, m.hexdigest() + extension)
|
||||
if os.path.exists(path):
|
||||
callback(path)
|
||||
else:
|
||||
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, callback, url, path))
|
||||
|
||||
def _getStaticCallback(self, callback, url, path, result, error=False, raw_body=None, **kwargs):
|
||||
if error:
|
||||
log.error("Error while downloading file: {}".format(url))
|
||||
return
|
||||
with open(path, "wb+") as f:
|
||||
f.write(raw_body)
|
||||
log.debug("File stored {} for {}".format(path, url))
|
||||
callback(path)
|
||||
|
||||
def getSymbolIcon(self, symbol_id, callback):
|
||||
"""
|
||||
Get a QIcon for a symbol from the controller
|
||||
|
||||
:param url: URL without the protocol and host part
|
||||
:param callback: Callback to call when file is ready
|
||||
"""
|
||||
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback))
|
||||
|
||||
def _getIconCallback(self, callback, path):
|
||||
icon = QtGui.QIcon()
|
||||
icon.addFile(path)
|
||||
callback(icon)
|
||||
@@ -23,6 +23,7 @@ import struct
|
||||
|
||||
try:
|
||||
import raven
|
||||
from raven.transport.http import HTTPTransport
|
||||
RAVEN_AVAILABLE = True
|
||||
except ImportError:
|
||||
# raven is not installed with deb package in order to simplify packaging
|
||||
@@ -35,7 +36,7 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Dev build
|
||||
# Dev build
|
||||
if __version_info__[3] != 0:
|
||||
import faulthandler
|
||||
# Display a traceback in case of segfault crash. Usefull when frozen
|
||||
@@ -50,7 +51,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "sync+https://3d44e34021504514a5fb0539ae6f8f92:af41562761754b4c9beca492d1b9115d@app.getsentry.com/38506"
|
||||
DSN = "sync+https://5a35a0b636ef492082c9877fdd94d071:df8a73ef344643cf9100ac70e77fd143@sentry.io/38506"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = get_resource("cacert.pem")
|
||||
if cacert is not None and os.path.isfile(cacert):
|
||||
@@ -68,20 +69,21 @@ class CrashReport:
|
||||
sentry_uncaught.disabled = True
|
||||
|
||||
def captureException(self, exception, value, tb):
|
||||
from .servers import Servers
|
||||
from .local_server import LocalServer
|
||||
|
||||
local_server = Servers.instance().localServerSettings()
|
||||
local_server = LocalServer.instance().localServerSettings()
|
||||
if local_server["report_errors"]:
|
||||
if not RAVEN_AVAILABLE:
|
||||
return
|
||||
if os.path.exists(".git"):
|
||||
log.warning("A .git directory exist crash report is turn off for developers")
|
||||
log.warning("A .git directory exist crash report is turn off for developers. Instant exit")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
if hasattr(exception, "fingerprint"):
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, fingerprint=['{{ default }}', exception.fingerprint])
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, fingerprint=['{{ default }}', exception.fingerprint], transport=HTTPTransport)
|
||||
else:
|
||||
client = raven.Client(CrashReport.DSN, release=__version__)
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, transport=HTTPTransport)
|
||||
context = {
|
||||
"os:name": platform.system(),
|
||||
"os:release": platform.release(),
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sip
|
||||
|
||||
from ..qt import QtWidgets, QtCore, QtGui, qpartial
|
||||
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
|
||||
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
|
||||
from ..image_manager import ImageManager
|
||||
from ..modules import Qemu
|
||||
@@ -29,39 +30,48 @@ from ..registry.image import Image
|
||||
from ..utils import human_filesize
|
||||
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..servers import Servers
|
||||
from ..gns3_vm import GNS3VM
|
||||
from ..compute_manager import ComputeManager
|
||||
from ..controller import Controller
|
||||
from ..local_config import LocalConfig
|
||||
|
||||
|
||||
class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
images_changed_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent, path):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.images_changed_signal.connect(self._refreshVersions, QtCore.Qt.QueuedConnection)
|
||||
|
||||
self._refreshing = False
|
||||
|
||||
self._path = path
|
||||
self.setupUi(self)
|
||||
# Count how many images are curently uploading
|
||||
self._image_uploading_count = 0
|
||||
|
||||
images_directories = list()
|
||||
images_directories.append(os.path.dirname(self._path))
|
||||
download_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
|
||||
if download_directory != "" and download_directory != os.path.dirname(self._path):
|
||||
images_directories.append(download_directory)
|
||||
self._registry = Registry(images_directories)
|
||||
self._registry.image_list_changed_signal.connect(self.images_changed_signal.emit)
|
||||
|
||||
self._appliance = Appliance(self._registry, self._path)
|
||||
self._registry.appendImageDirectory(os.path.join(ImageManager.instance().getDirectory(), self._appliance.image_dir_name()))
|
||||
|
||||
self.uiApplianceVersionTreeWidget.currentItemChanged.connect(self._applianceVersionCurrentItemChangedSlot)
|
||||
self.uiRefreshPushButton.clicked.connect(self._refreshVersions)
|
||||
self.uiRefreshPushButton.clicked.connect(self.images_changed_signal.emit)
|
||||
self.uiDownloadPushButton.clicked.connect(self._downloadPushButtonClickedSlot)
|
||||
self.uiImportPushButton.clicked.connect(self._importPushButtonClickedSlot)
|
||||
self.uiCreateVersionPushButton.clicked.connect(self._createVersionPushButtonClickedSlot)
|
||||
|
||||
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
if hasattr(self, "uiLoadBalanceCheckBox"):
|
||||
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
|
||||
if Controller.instance().isRemote():
|
||||
self.uiLocalRadioButton.setText("Run the appliance on the main server")
|
||||
|
||||
self.uiServerWizardPage.isComplete = self._uiServerWizardPage_isComplete
|
||||
|
||||
@@ -83,6 +93,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
type = "qemu"
|
||||
elif "iou" in self._appliance:
|
||||
type = "iou"
|
||||
elif "docker" in self._appliance:
|
||||
type = "docker"
|
||||
elif "dynamips" in self._appliance:
|
||||
type = "dynamips"
|
||||
|
||||
@@ -117,30 +129,38 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
elif self.page(page_id) == self.uiServerWizardPage:
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
for server in Servers.instance().remoteServers().values():
|
||||
self.uiRemoteServersComboBox.addItem(server.url(), server)
|
||||
if len(ComputeManager.instance().remoteComputes()) == 0:
|
||||
self.uiRemoteRadioButton.setEnabled(False)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setEnabled(True)
|
||||
for compute in ComputeManager.instance().remoteComputes():
|
||||
self.uiRemoteServersComboBox.addItem(compute.name(), compute)
|
||||
|
||||
if not GNS3VM.instance().isRunning():
|
||||
if not ComputeManager.instance().vmCompute():
|
||||
self.uiVMRadioButton.setEnabled(False)
|
||||
|
||||
# Qemu has issues on OSX and Windows we disallow usage of the local server
|
||||
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")) and not LocalConfig.instance().experimental():
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
if (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
|
||||
if type == "qemu":
|
||||
# Qemu has issues on OSX and Windows we disallow usage of the local server
|
||||
if not LocalConfig.instance().experimental():
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
elif type != "dynamips":
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
|
||||
if GNS3VM.instance().isRunning():
|
||||
if ComputeManager.instance().vmCompute():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
elif Servers.instance().localServer().isLocalServerRunning():
|
||||
elif ComputeManager.instance().localCompute() and self.uiLocalRadioButton.isEnabled():
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
elif len(Servers.instance().remoteServers().values()) > 0:
|
||||
elif self.uiRemoteRadioButton.isEnabled():
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setChecked(False)
|
||||
|
||||
elif self.page(page_id) == self.uiFilesWizardPage:
|
||||
self._refreshVersions()
|
||||
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
|
||||
|
||||
elif self.page(page_id) == self.uiQemuWizardPage:
|
||||
Qemu.instance().getQemuBinariesFromServer(self._server, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
|
||||
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
|
||||
|
||||
elif self.page(page_id) == self.uiSummaryWizardPage:
|
||||
self.uiSummaryTreeWidget.clear()
|
||||
@@ -163,12 +183,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiCheckServerLabel.setText("Please wait while checking server capacities...")
|
||||
if 'qemu' in self._appliance:
|
||||
if self._appliance['qemu'].get('kvm', 'require') == 'require':
|
||||
self._server_check = False # If the server as the capacities for running the appliance
|
||||
Qemu.instance().getQemuCapabilitiesFromServer(self._server, qpartial(self._qemuServerCapabilitiesCallback))
|
||||
self._server_check = False # If the server as the capacities for running the appliance
|
||||
self.uiCheckServerLabel.setText("")
|
||||
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
|
||||
return
|
||||
self.uiCheckServerLabel.setText("")
|
||||
self.uiCheckServerLabel.setText("GNS3 server requirements is OK you can continue the installation")
|
||||
self._server_check = True
|
||||
self.next()
|
||||
|
||||
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
|
||||
"""
|
||||
@@ -179,7 +199,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiCheckServerLabel.setText("GNS3 server requirements is OK you can continue the installation")
|
||||
else:
|
||||
if error:
|
||||
msg = error
|
||||
msg = result["message"]
|
||||
else:
|
||||
msg = "The remote server doesn't support KVM. You need a Linux server or the GNS3 VM with VMware and CPU virtualization instructions."
|
||||
self.uiCheckServerLabel.setText(msg)
|
||||
@@ -189,36 +209,44 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
def _uiServerWizardPage_isComplete(self):
|
||||
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
|
||||
|
||||
def _refreshVersions(self):
|
||||
def _imageUploadedCallback(self, result, error=False, **kwargs):
|
||||
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
|
||||
|
||||
@qslot
|
||||
def _refreshVersions(self, *args):
|
||||
"""
|
||||
Refresh the list of files for different version of the appliance
|
||||
"""
|
||||
self.uiFilesWizardPage.setSubTitle("The following versions are available for " + self._appliance["product_name"] + ". Check the status of files required to install.")
|
||||
self.uiApplianceVersionTreeWidget.clear()
|
||||
|
||||
worker = WaitForLambdaWorker(lambda: self._resfreshDialogWorker())
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Scanning directories for images...", None, busy=True, parent=self)
|
||||
if self._refreshing:
|
||||
return
|
||||
self._refreshing = True
|
||||
|
||||
self.uiFilesWizardPage.setSubTitle("The following versions are available for " + self._appliance["product_name"] + ". Check the status of files required to install.")
|
||||
|
||||
self.uiApplianceVersionTreeWidget.clear()
|
||||
worker = WaitForLambdaWorker(lambda: self._refreshDialogWorker())
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Scanning directories for files...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if progress_dialog.exec_():
|
||||
for version in self._appliance["versions"]:
|
||||
top = QtWidgets.QTreeWidgetItem(["{} {}".format(self._appliance["product_name"], version["name"])])
|
||||
|
||||
top = QtWidgets.QTreeWidgetItem(self.uiApplianceVersionTreeWidget, ["{} {}".format(self._appliance["product_name"], version["name"])])
|
||||
size = 0
|
||||
status = "Ready to install"
|
||||
for image in version["images"].values():
|
||||
if image["status"] == "Missing":
|
||||
status = "Missing files"
|
||||
|
||||
size += image["filesize"]
|
||||
size += image.get("filesize", 0)
|
||||
image_widget = QtWidgets.QTreeWidgetItem(
|
||||
[
|
||||
"",
|
||||
image["filename"],
|
||||
human_filesize(image["filesize"]),
|
||||
human_filesize(image.get("filesize", 0)),
|
||||
image["status"],
|
||||
image["version"]
|
||||
image["version"],
|
||||
image.get("md5sum", "")
|
||||
])
|
||||
|
||||
if image["status"] == "Missing":
|
||||
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
|
||||
else:
|
||||
@@ -246,32 +274,44 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
top.setData(2, QtCore.Qt.UserRole, self._appliance)
|
||||
top.setData(0, QtCore.Qt.UserRole, version)
|
||||
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
|
||||
# self.uiApplianceVersionTreeWidget.setCurrentItem(top)
|
||||
if expand:
|
||||
top.setExpanded(True)
|
||||
|
||||
if len(self._appliance["versions"]) > 0:
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(0)
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(1)
|
||||
self.uiApplianceVersionTreeWidget.setCurrentItem(self.uiApplianceVersionTreeWidget.topLevelItem(0))
|
||||
self._refreshing = False
|
||||
|
||||
def _resfreshDialogWorker(self):
|
||||
def _refreshDialogWorker(self):
|
||||
"""
|
||||
Scan local directory in order to found the images on disk
|
||||
"""
|
||||
|
||||
# Docker do not have versions
|
||||
if "versions" not in self._appliance:
|
||||
return
|
||||
|
||||
for version in self._appliance["versions"]:
|
||||
for image in version["images"].values():
|
||||
if self._registry.search_image_file(image["filename"], image["md5sum"], image["filesize"]):
|
||||
img = self._registry.search_image_file(self._appliance.emulator(), image["filename"], image.get("md5sum"), image.get("filesize"))
|
||||
if img:
|
||||
image["status"] = "Found"
|
||||
image["md5sum"] = img.md5sum
|
||||
image["filesize"] = img.filesize
|
||||
else:
|
||||
image["status"] = "Missing"
|
||||
|
||||
@qslot
|
||||
def _applianceVersionCurrentItemChangedSlot(self, current, previous):
|
||||
"""
|
||||
Called when user select a different item in the list of appliance files
|
||||
"""
|
||||
self.uiDownloadPushButton.hide()
|
||||
self.uiImportPushButton.hide()
|
||||
self.uiExplainDownloadLabel.hide()
|
||||
|
||||
if current is None:
|
||||
if current is None or sip.isdeleted(current):
|
||||
return
|
||||
|
||||
image = current.data(1, QtCore.Qt.UserRole)
|
||||
@@ -280,26 +320,49 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiDownloadPushButton.show()
|
||||
self.uiImportPushButton.show()
|
||||
|
||||
def _downloadPushButtonClickedSlot(self):
|
||||
@qslot
|
||||
def _downloadPushButtonClickedSlot(self, *args):
|
||||
"""
|
||||
Called when user want to download an appliance images.
|
||||
He should have selected the file before.
|
||||
"""
|
||||
if self._refreshing:
|
||||
return False
|
||||
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
|
||||
if current is None or sip.isdeleted(current):
|
||||
return
|
||||
|
||||
data = current.data(1, QtCore.Qt.UserRole)
|
||||
if data is not None:
|
||||
if "direct_download_url" in data:
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["direct_download_url"]))
|
||||
if "compression" in data:
|
||||
QtWidgets.QMessageBox.warning(self, "Add appliance", "The image is compressed with {} you need to uncompress it before using it.".format(data["compression"]))
|
||||
QtWidgets.QMessageBox.warning(self, "Add appliance", "The file is compressed with {} you need to uncompress it before using it.".format(data["compression"]))
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "Add appliance", "Download will redirect you where the required file can be downloaded, you may have to be registered with the vendor in order to download the file.")
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["download_url"]))
|
||||
|
||||
def _importPushButtonClickedSlot(self):
|
||||
@qslot
|
||||
def _createVersionPushButtonClickedSlot(self, *args):
|
||||
"""
|
||||
Allow user to create a new version of an appliance
|
||||
"""
|
||||
|
||||
new_version, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Creating a new version allows to import unknown files to use with this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.Normal)
|
||||
if ok:
|
||||
self._appliance.create_new_version(new_version)
|
||||
self.images_changed_signal.emit()
|
||||
|
||||
@qslot
|
||||
def _importPushButtonClickedSlot(self, *args):
|
||||
"""
|
||||
Called when user want to import an appliance images.
|
||||
He should have selected the file before.
|
||||
"""
|
||||
if self._refreshing:
|
||||
return False
|
||||
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
disk = current.data(1, QtCore.Qt.UserRole)
|
||||
@@ -308,17 +371,13 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
if len(path) == 0:
|
||||
return
|
||||
|
||||
image = Image(path)
|
||||
if image.md5sum != disk["md5sum"]:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "This is not the correct image file. The MD5 sum is {} and should be {}. For OVA you need to import the OVA/OVF not the file inside the archive.".format(image.md5sum, disk["md5sum"]))
|
||||
image = Image(self._appliance.emulator(), path)
|
||||
if "md5sum" in disk and image.md5sum != disk["md5sum"]:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "This is not the correct file. The MD5 sum is {} and should be {}.".format(image.md5sum, disk["md5sum"]))
|
||||
return
|
||||
|
||||
config = Config()
|
||||
worker = WaitForLambdaWorker(lambda: image.copy(os.path.join(config.images_dir, self._appliance.image_dir_name()), disk["filename"]), allowed_exceptions=[OSError])
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Import the appliance...", None, busy=True, parent=self)
|
||||
if not progress_dialog.exec_():
|
||||
return
|
||||
self._refreshVersions()
|
||||
image.upload(self._compute_id, callback=self._imageUploadedCallback)
|
||||
|
||||
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -339,6 +398,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiQemuListComboBox.addItem("{path}".format(path=qemu["path"]), qemu["path"])
|
||||
if self.uiQemuListComboBox.count() == 1:
|
||||
self.next()
|
||||
else:
|
||||
i = self.uiQemuListComboBox.findText(self._appliance["qemu"]["arch"], QtCore.Qt.MatchContains)
|
||||
if i != -1:
|
||||
self.uiQemuListComboBox.setCurrentIndex(i)
|
||||
|
||||
def _install(self, version):
|
||||
"""
|
||||
@@ -353,14 +416,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
||||
return False
|
||||
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
|
||||
if self._server.isLocal():
|
||||
server_string = "local"
|
||||
elif self._server.isGNS3VM():
|
||||
server_string = "vm"
|
||||
if version is None:
|
||||
appliance_configuration = self._appliance.copy()
|
||||
else:
|
||||
server_string = self._server.url()
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
|
||||
while len(appliance_configuration["name"]) == 0 or not config.is_name_available(appliance_configuration["name"]):
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "The name \"{}\" is already used by another appliance".format(appliance_configuration["name"]))
|
||||
@@ -370,7 +429,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
if "qemu" in appliance_configuration:
|
||||
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
|
||||
|
||||
worker = WaitForLambdaWorker(lambda: config.add_appliance(appliance_configuration, server_string), allowed_exceptions=[ConfigException, OSError])
|
||||
worker = WaitForLambdaWorker(lambda: config.add_appliance(appliance_configuration, self._compute_id), allowed_exceptions=[ConfigException, OSError])
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if not progress_dialog.exec_():
|
||||
@@ -383,9 +442,26 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
|
||||
return True
|
||||
|
||||
def _uploadImages(self, version):
|
||||
"""
|
||||
Upload an image to the compute
|
||||
"""
|
||||
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
for image in appliance_configuration["images"]:
|
||||
if image["location"] == "local":
|
||||
image = Image(self._appliance.emulator(), image["path"])
|
||||
image.upload(self._compute_id, self._applianceImageUploadedCallback)
|
||||
self._image_uploading_count += 1
|
||||
|
||||
def _applianceImageUploadedCallback(self, result, error=False, **kwargs):
|
||||
self._image_uploading_count -= 1
|
||||
|
||||
def nextId(self):
|
||||
if self.currentPage() == self.uiServerWizardPage:
|
||||
if "qemu" not in self._appliance:
|
||||
if "docker" in self._appliance:
|
||||
return super().nextId() + 3
|
||||
elif "qemu" not in self._appliance:
|
||||
return super().nextId() + 1
|
||||
elif self.currentPage() == self.uiFilesWizardPage:
|
||||
if "qemu" not in self._appliance:
|
||||
@@ -398,41 +474,50 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiFilesWizardPage:
|
||||
if self._refreshing:
|
||||
return False
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
if current is None or sip.isdeleted(current):
|
||||
return False
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
appliance = current.data(2, QtCore.Qt.UserRole)
|
||||
name = "{} {}".format(appliance["name"], version["name"])
|
||||
if not self._appliance.is_version_installable(version["name"]):
|
||||
QtWidgets.QMessageBox.warning(self, "Appliance", "Sorry, you cannot install {} with missing files".format(name))
|
||||
QtWidgets.QMessageBox.warning(self, "Appliance", "Sorry, you cannot install {} with missing files".format(appliance["name"]))
|
||||
return False
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Would you like to install {}?".format(name), QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Would you like to install {} version {}?".format(appliance["name"], version["name"]),
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
self._uploadImages(version["name"])
|
||||
|
||||
elif self.currentPage() == self.uiUsageWizardPage:
|
||||
if self._image_uploading_count > 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for image uploading")
|
||||
return False
|
||||
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
return self._install(version["name"])
|
||||
if current:
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
return self._install(version["name"])
|
||||
else:
|
||||
return self._install(None)
|
||||
|
||||
elif self.currentPage() == self.uiServerWizardPage:
|
||||
if self.uiRemoteRadioButton.isChecked():
|
||||
if not Servers.instance().remoteServers():
|
||||
if len(ComputeManager.instance().remoteComputes()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote server registered in your preferences")
|
||||
return False
|
||||
self._server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
|
||||
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
|
||||
gns3_vm_server = Servers.instance().vmServer()
|
||||
if gns3_vm_server is None:
|
||||
QtWidgets.QMessageBox.critical(self, "GNS3 VM", "The GNS3 VM is not running")
|
||||
return False
|
||||
self._server = gns3_vm_server
|
||||
self._compute_id = "vm"
|
||||
else:
|
||||
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")):
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and MacOSX is not supported by the GNS3 team. Are you sur to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
if (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
|
||||
if "qemu" in self._appliance:
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and MacOSX is not supported by the GNS3 team. Are you sur to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
|
||||
self._server = Servers.instance().localServer()
|
||||
self._compute_id = "local"
|
||||
|
||||
elif self.currentPage() == self.uiQemuWizardPage:
|
||||
if self.uiQemuListComboBox.currentIndex() == -1:
|
||||
@@ -444,6 +529,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
return True
|
||||
|
||||
@qslot
|
||||
def _vmToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the VM radio button is toggled.
|
||||
@@ -454,6 +540,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
@qslot
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the remote server radio button is toggled.
|
||||
@@ -465,6 +552,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiRemoteServersGroupBox.setEnabled(True)
|
||||
self.uiRemoteServersGroupBox.show()
|
||||
|
||||
@qslot
|
||||
def _localToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the local server radio button is toggled.
|
||||
@@ -474,15 +562,3 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
def _loadBalanceToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the load balance checkbox is toggled.
|
||||
|
||||
:param checked: either the box is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersComboBox.setEnabled(False)
|
||||
else:
|
||||
self.uiRemoteServersComboBox.setEnabled(True)
|
||||
|
||||
75
gns3/dialogs/capture_dialog.py
Normal file
75
gns3/dialogs/capture_dialog.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.capture_dialog_ui import Ui_CaptureDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CaptureDialog(QtWidgets.QDialog, Ui_CaptureDialog):
|
||||
"""
|
||||
This dialog allow configure the packet capture
|
||||
"""
|
||||
|
||||
def __init__(self, parent, file_name, auto_start, ethernet_link=True):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
||||
|
||||
if ethernet_link:
|
||||
self.uiDataLinkTypeComboBox.addItem("Ethernet", "DLT_EN10MB")
|
||||
else:
|
||||
self.uiDataLinkTypeComboBox.addItem("Cisco HDLC", "DLT_C_HDLC")
|
||||
self.uiDataLinkTypeComboBox.addItem("Cisco PPP", "DLT_PPP_SERIAL")
|
||||
self.uiDataLinkTypeComboBox.addItem("Frame Relay", "DLT_FRELAY")
|
||||
self.uiDataLinkTypeComboBox.addItem("ATM", "DLT_ATM_RFC1483")
|
||||
|
||||
self.uiCaptureFileNameLineEdit.setText(file_name)
|
||||
self.uiStartCommandCheckBox.setChecked(auto_start)
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
if len(self.fileName()) == 0:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Packet capture", "Please provide a file name for the capture")
|
||||
return
|
||||
self.accept()
|
||||
|
||||
def fileName(self):
|
||||
return self.uiCaptureFileNameLineEdit.text()
|
||||
|
||||
def dataLink(self):
|
||||
"""
|
||||
Type of link for capture
|
||||
"""
|
||||
return self.uiDataLinkTypeComboBox.currentData()
|
||||
|
||||
def commandAutoStart(self):
|
||||
return self.uiStartCommandCheckBox.isChecked()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = CaptureDialog(main, "test.pcap")
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
print(dialog.dataLink())
|
||||
print(dialog.fileName())
|
||||
@@ -44,6 +44,7 @@ class ConfigurationDialog(QtWidgets.QDialog, Ui_configurationDialog):
|
||||
self.setWindowTitle(configuration_page.windowTitle())
|
||||
self.uiConfigStackedWidget.addWidget(configuration_page)
|
||||
self.uiConfigStackedWidget.setCurrentWidget(configuration_page)
|
||||
self.setModal(True)
|
||||
configuration_page.loadSettings(settings)
|
||||
self._settings = settings
|
||||
self._configuration_page = configuration_page
|
||||
|
||||
135
gns3/dialogs/console_command_dialog.py
Normal file
135
gns3/dialogs/console_command_dialog.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.ui.console_command_dialog_ui import Ui_uiConsoleCommandDialog
|
||||
from gns3.settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, \
|
||||
PRECONFIGURED_SERIAL_CONSOLE_COMMANDS, \
|
||||
PRECONFIGURED_VNC_CONSOLE_COMMANDS, \
|
||||
CUSTOM_CONSOLE_COMMANDS_SETTINGS
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
|
||||
"""
|
||||
This dialog allow user to select the command used to start a
|
||||
console.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, console_type="telnet", current=None):
|
||||
"""
|
||||
:params console_type: telnet, serial or vnc
|
||||
:params current: Current console command
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self._console_type = console_type
|
||||
self._current = current
|
||||
|
||||
self._settings = LocalConfig.instance().loadSectionSettings("CustomConsoleCommands", CUSTOM_CONSOLE_COMMANDS_SETTINGS)
|
||||
|
||||
self.uiCommandComboBox.currentIndexChanged.connect(self.commandComboBoxCurrentIndexChangedSlot)
|
||||
self.uiCommandPlainTextEdit.textChanged.connect(self.textChangedSlot)
|
||||
self.uiSavePushButton.clicked.connect(self.savePushButtonClickedSlot)
|
||||
self.uiRemovePushButton.clicked.connect(self.removePushButtonClickedSlot)
|
||||
|
||||
self._refreshList()
|
||||
|
||||
def _refreshList(self):
|
||||
if self._console_type == "telnet":
|
||||
self._consoles = copy.copy(PRECONFIGURED_TELNET_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
elif self._console_type == "vnc":
|
||||
self._consoles = copy.copy(PRECONFIGURED_VNC_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
else:
|
||||
self._consoles = copy.copy(PRECONFIGURED_SERIAL_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
|
||||
self.uiCommandComboBox.clear()
|
||||
self.uiCommandComboBox.addItem("Custom", "")
|
||||
for name, cmd in sorted(self._consoles.items(), key=(lambda item: item[0].lower())):
|
||||
self.uiCommandComboBox.addItem(name, cmd)
|
||||
|
||||
if self._current:
|
||||
self.uiCommandPlainTextEdit.setPlainText(self._current)
|
||||
else:
|
||||
self.uiCommandComboBox.setCurrentIndex(1)
|
||||
|
||||
def removePushButtonClickedSlot(self):
|
||||
"""
|
||||
Remove the custom command from the custom list
|
||||
"""
|
||||
self._settings[self._console_type].pop(self.uiCommandComboBox.currentText())
|
||||
LocalConfig.instance().saveSectionSettings("CustomConsoleCommands", self._settings)
|
||||
self._current = None
|
||||
self._refreshList()
|
||||
|
||||
def savePushButtonClickedSlot(self):
|
||||
"""
|
||||
Save a custom command to the list
|
||||
"""
|
||||
name, ok = QtWidgets.QInputDialog.getText(self, "Add a command", "Command name:", QtWidgets.QLineEdit.Normal)
|
||||
command = self.uiCommandPlainTextEdit.toPlainText().strip()
|
||||
if ok and len(command) > 0:
|
||||
if command not in self._consoles.values():
|
||||
self._settings[self._console_type][name] = command
|
||||
self._current = command
|
||||
LocalConfig.instance().saveSectionSettings("CustomConsoleCommands", self._settings)
|
||||
self._refreshList()
|
||||
|
||||
def textChangedSlot(self):
|
||||
index = self.uiCommandComboBox.findData(self.uiCommandPlainTextEdit.toPlainText())
|
||||
if index == -1:
|
||||
index = 0
|
||||
self.uiCommandComboBox.setCurrentIndex(index)
|
||||
|
||||
def commandComboBoxCurrentIndexChangedSlot(self, index):
|
||||
self.uiRemovePushButton.hide()
|
||||
# Ignore custom command
|
||||
if index != 0:
|
||||
self.uiCommandPlainTextEdit.setPlainText(self.uiCommandComboBox.currentData())
|
||||
self.uiSavePushButton.hide()
|
||||
if self.uiCommandComboBox.currentText() in self._settings[self._console_type].keys():
|
||||
self.uiRemovePushButton.show()
|
||||
else:
|
||||
self.uiSavePushButton.show()
|
||||
|
||||
@staticmethod
|
||||
def getCommand(parent, console_type="telnet", current=None):
|
||||
dialog = ConsoleCommandDialog(parent, console_type=console_type, current=current)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
return (True, dialog.uiCommandPlainTextEdit.toPlainText().replace("\n", " "))
|
||||
return (False, None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
(ok, command) = ConsoleCommandDialog.getCommand(main, console_type="telnet", current=list(PRECONFIGURED_TELNET_CONSOLE_COMMANDS.items())[0][1])
|
||||
print(ok)
|
||||
print(command)
|
||||
|
||||
@@ -24,7 +24,7 @@ import struct
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.doctor_dialog_ui import Ui_DoctorDialog
|
||||
from gns3.servers import Servers
|
||||
from gns3.local_server import LocalServer
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3 import version
|
||||
from gns3.modules.vmware import VMware
|
||||
@@ -49,14 +49,18 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
|
||||
for method in sorted(dir(self)):
|
||||
if method.startswith('check'):
|
||||
self.write(getattr(self, method).__doc__ + "...")
|
||||
(res, msg) = getattr(self, method)()
|
||||
if res == 0:
|
||||
self.write('<span style="color: green"><strong>OK</strong></span>')
|
||||
elif res == 1:
|
||||
self.write('<span style="color: orange"><strong>WARNING</strong> {}</span>'.format(msg))
|
||||
elif res == 2:
|
||||
self.write('<span style="color: red"><strong>ERROR</strong> {}</span>'.format(msg))
|
||||
try:
|
||||
self.write(getattr(self, method).__doc__ + "...")
|
||||
(res, msg) = getattr(self, method)()
|
||||
if res == 0:
|
||||
self.write('<span style="color: green"><strong>OK</strong></span>')
|
||||
elif res == 1:
|
||||
self.write('<span style="color: orange"><strong>WARNING</strong> {}</span>'.format(msg))
|
||||
elif res == 2:
|
||||
self.write('<span style="color: red"><strong>ERROR</strong> {}</span>'.format(msg))
|
||||
except Exception as e:
|
||||
log.error("GNS3 doctor exception detected: {}".format(e), exc_info=1)
|
||||
self.write('<span style="color: red"><strong>FAIL</strong> The doctor failed during this test with error: {} Please check on the forum.</span>'.format(str(e)))
|
||||
self.write("<br/>")
|
||||
|
||||
def write(self, text):
|
||||
@@ -72,7 +76,7 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
|
||||
def checkLocalServerEnabled(self):
|
||||
"""Checking if the local server is enabled"""
|
||||
if Servers.instance().shouldLocalServerAutoStart() is False:
|
||||
if LocalServer.instance().shouldLocalServerAutoStart() is False:
|
||||
return (2, "The local server is disabled. Go to Preferences -> Server -> Local Server and enable the local server.")
|
||||
return (0, None)
|
||||
|
||||
@@ -122,13 +126,15 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
|
||||
def checkUbridgePermission(self):
|
||||
"""Check if ubridge has the correct permission"""
|
||||
if os.geteuid() == 0:
|
||||
if not sys.platform.startswith("win") and os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return (0, None)
|
||||
|
||||
path = Servers.instance().localServerSettings().get("ubridge_path")
|
||||
path = LocalServer.instance().localServerSettings().get("ubridge_path")
|
||||
if path is None:
|
||||
return (0, None)
|
||||
if not os.path.exists(path):
|
||||
return (2, "Ubridge path {path} doesn't exists".format(path=path))
|
||||
|
||||
request_setuid = False
|
||||
if sys.platform.startswith("linux"):
|
||||
@@ -148,21 +154,66 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
|
||||
def checkDynamipsPermission(self):
|
||||
"""Check if dynamips has the correct permission"""
|
||||
if os.geteuid() == 0:
|
||||
if not sys.platform.startswith("win") and os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return (0, None)
|
||||
|
||||
path = Servers.instance().localServerSettings().get("dynamips_path")
|
||||
path = LocalServer.instance().localServerSettings().get("dynamips_path")
|
||||
if path is None:
|
||||
return (0, None)
|
||||
if not os.path.exists(path):
|
||||
return (2, "Dynamips path {path} doesn't exists".format(path=path))
|
||||
|
||||
if sys.platform.startswith("linux") and "security.capability" in os.listxattr(path):
|
||||
caps = os.getxattr(path, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
return(2, "Dynamips require CAP_NET_RAW. Run sudo setcap cap_net_raw,cap_net_admin+eip {path}".format(path=path))
|
||||
return (2, "Dynamips require CAP_NET_RAW. Run sudo setcap cap_net_raw,cap_net_admin+eip {path}".format(path=path))
|
||||
return (0, None)
|
||||
|
||||
def checkGNS3InstalledTwice(self):
|
||||
"""Check if gns3 is not installed twice"""
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
return (0, None)
|
||||
|
||||
try:
|
||||
if os.path.exists("/usr/local/bin/gns3server") and os.path.exists("/usr/bin/gns3server"):
|
||||
return (2, "GNS3 is installed twice please remove it from /usr/local/bin")
|
||||
except OSError:
|
||||
pass
|
||||
return (0, None)
|
||||
|
||||
def _checkWindowsService(self, service_name):
|
||||
|
||||
import pywintypes
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
try:
|
||||
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
|
||||
return False
|
||||
except pywintypes.error as e:
|
||||
if e.winerror == 1060:
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
return True
|
||||
|
||||
def checkRPFServiceIsRunning(self):
|
||||
"""Check if the RPF service is running (required to use Ethernet NIOs)"""
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
return (0, None)
|
||||
|
||||
import pywintypes
|
||||
try:
|
||||
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
|
||||
return (2, "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot")
|
||||
except pywintypes.error as e:
|
||||
return (2, "Could not check if the NPF or Npcap service is running: {}".format(e.strerror))
|
||||
|
||||
return (0, None)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
121
gns3/dialogs/edit_compute_dialog.py
Normal file
121
gns3/dialogs/edit_compute_dialog.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.compute import Compute
|
||||
from gns3.ui.edit_compute_dialog_ui import Ui_EditComputeDialog
|
||||
|
||||
|
||||
class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
|
||||
"""
|
||||
New compute dialog.
|
||||
|
||||
:param parent: parent widget.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, compute=None):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiEnableAuthenticationCheckBox.toggled.connect(self._enableAuthenticationSlot)
|
||||
self._compute = compute
|
||||
if self._compute:
|
||||
self.uiServerNameLineEdit.setText(self._compute.name())
|
||||
self.uiServerHostLineEdit.setText(self._compute.host())
|
||||
self.uiServerPortSpinBox.setValue(self._compute.port())
|
||||
|
||||
index = self.uiServerProtocolComboBox.findText(self._compute.protocol().upper())
|
||||
self.uiServerProtocolComboBox.setCurrentIndex(index)
|
||||
|
||||
if self._compute.user():
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(True)
|
||||
self.uiServerUserLineEdit.setText(self._compute.user())
|
||||
else:
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(False)
|
||||
self.uiWarningLabel.setVisible(False)
|
||||
else:
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(False)
|
||||
self.uiWarningLabel.setVisible(False)
|
||||
self._enableAuthenticationSlot(self.uiEnableAuthenticationCheckBox.isChecked())
|
||||
|
||||
def _enableAuthenticationSlot(self, state):
|
||||
"""
|
||||
Slot to enable or not the authentication.
|
||||
"""
|
||||
|
||||
if self.uiEnableAuthenticationCheckBox.isChecked():
|
||||
self.uiServerUserLineEdit.setVisible(True)
|
||||
self.uiServerPasswordLineEdit.setVisible(True)
|
||||
self.uiServerUserLabel.setVisible(True)
|
||||
self.uiServerPasswordLabel.setVisible(True)
|
||||
else:
|
||||
self.uiServerUserLineEdit.setVisible(False)
|
||||
self.uiServerPasswordLineEdit.setVisible(False)
|
||||
self.uiServerUserLabel.setVisible(False)
|
||||
self.uiServerPasswordLabel.setVisible(False)
|
||||
|
||||
def compute(self):
|
||||
return self._compute
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
Adds a new remote compute.
|
||||
"""
|
||||
|
||||
host = self.uiServerHostLineEdit.text().strip()
|
||||
name = self.uiServerNameLineEdit.text().strip()
|
||||
protocol = self.uiServerProtocolComboBox.currentText().lower()
|
||||
port = self.uiServerPortSpinBox.value()
|
||||
user = self.uiServerUserLineEdit.text().strip()
|
||||
password = self.uiServerPasswordLineEdit.text().strip()
|
||||
|
||||
if not re.match(r"^[a-zA-Z0-9\.{}-]+$".format("\u0370-\u1CDF\u2C00-\u30FF\u4E00-\u9FBF"), host):
|
||||
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server hostname {}".format(host))
|
||||
return
|
||||
if len(name) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server name {}".format(name))
|
||||
return
|
||||
if port is None or port < 1:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server port {}".format(port))
|
||||
return
|
||||
|
||||
if not self._compute:
|
||||
self._compute = Compute()
|
||||
self._compute.setName(name)
|
||||
self._compute.setProtocol(protocol)
|
||||
self._compute.setHost(host)
|
||||
self._compute.setPort(port)
|
||||
if self.uiEnableAuthenticationCheckBox.isChecked():
|
||||
self._compute.setUser(user)
|
||||
self._compute.setPassword(password)
|
||||
else:
|
||||
self._compute.setUser(None)
|
||||
self._compute.setPassword(None)
|
||||
|
||||
super().accept()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = EditComputeDialog(main)
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
58
gns3/dialogs/edit_project_dialog.py
Normal file
58
gns3/dialogs/edit_project_dialog.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from ..qt import QtWidgets
|
||||
from ..topology import Topology
|
||||
from ..ui.edit_project_dialog_ui import Ui_EditProjectDialog
|
||||
|
||||
|
||||
class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
"""
|
||||
Edit current project settings
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self._project = Topology.instance().project()
|
||||
self.uiProjectNameLineEdit.setText(self._project.name())
|
||||
self.uiProjectAutoOpenCheckBox.setChecked(self._project.autoOpen())
|
||||
self.uiProjectAutoCloseCheckBox.setChecked(not self._project.autoClose())
|
||||
self.uiProjectAutoStartCheckBox.setChecked(self._project.autoStart())
|
||||
self.uiSceneWidthSpinBox.setValue(self._project.sceneWidth())
|
||||
self.uiSceneHeightSpinBox.setValue(self._project.sceneHeight())
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
Called when the dialog is closed.
|
||||
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
|
||||
if result:
|
||||
self._project.setName(self.uiProjectNameLineEdit.text())
|
||||
self._project.setAutoOpen(self.uiProjectAutoOpenCheckBox.isChecked())
|
||||
self._project.setAutoClose(not self.uiProjectAutoCloseCheckBox.isChecked())
|
||||
self._project.setAutoStart(self.uiProjectAutoStartCheckBox.isChecked())
|
||||
self._project.setSceneHeight(self.uiSceneHeightSpinBox.value())
|
||||
self._project.setSceneWidth(self.uiSceneWidthSpinBox.value())
|
||||
self._project.update()
|
||||
super().done(result)
|
||||
@@ -29,6 +29,7 @@ from gns3.local_config import LocalConfig
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
"""
|
||||
This dialog allow user to export useful information
|
||||
@@ -54,18 +55,19 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
try:
|
||||
with ZipFile(path, 'w') as zip:
|
||||
zip.writestr("debug.txt", self._getDebugData())
|
||||
dir = LocalConfig.configDirectory()
|
||||
dir = LocalConfig.instance().configDirectory()
|
||||
for filename in os.listdir(dir):
|
||||
path = os.path.join(dir, filename)
|
||||
if os.path.isfile(path):
|
||||
zip.write(path, filename)
|
||||
|
||||
dir = self._project.filesDir()
|
||||
if dir:
|
||||
for filename in os.listdir(dir):
|
||||
path = os.path.join(dir, filename)
|
||||
if os.path.isfile(path):
|
||||
zip.write(path, filename)
|
||||
if self._project:
|
||||
dir = self._project.filesDir()
|
||||
if dir:
|
||||
for filename in os.listdir(dir):
|
||||
path = os.path.join(dir, filename)
|
||||
if os.path.isfile(path):
|
||||
zip.write(path, filename)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Debug", "Can't export debug information: {}".format(str(e)))
|
||||
self.accept()
|
||||
@@ -86,6 +88,7 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
OS: {os}
|
||||
Python: {python}
|
||||
Qt: {qt}
|
||||
PyQt: {pyqt}
|
||||
CPU: {cpu}
|
||||
Memory: {memory}
|
||||
|
||||
@@ -98,7 +101,8 @@ Open connections:
|
||||
Processus:
|
||||
""".format(
|
||||
version=__version__,
|
||||
qt=QtCore.BINDING_VERSION_STR,
|
||||
qt=QtCore.QT_VERSION_STR,
|
||||
pyqt=QtCore.PYQT_VERSION_STR,
|
||||
os=platform.platform(),
|
||||
python=platform.python_version(),
|
||||
memory=psutil.virtual_memory(),
|
||||
|
||||
72
gns3/dialogs/file_editor_dialog.py
Normal file
72
gns3/dialogs/file_editor_dialog.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.file_editor_dialog_ui import Ui_FileEditorDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
|
||||
"""
|
||||
This dialog allow user to detect error in his GNS3 installation.
|
||||
|
||||
If you want to add a test add a method starting by check. The
|
||||
check return a tuple result and a message in case of failure.
|
||||
"""
|
||||
|
||||
def __init__(self, target, path, parent=None, default=""):
|
||||
|
||||
if parent is None:
|
||||
from gns3.main_window import MainWindow
|
||||
parent = MainWindow.instance()
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._target = target
|
||||
self._path = path
|
||||
self._default = default
|
||||
|
||||
self.setWindowTitle(target.name() + " " + os.path.basename(path))
|
||||
|
||||
self.uiRefreshButton.pressed.connect(self._refreshSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Save).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
||||
|
||||
self._refreshSlot()
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
text = self.uiFileTextEdit.toPlainText()
|
||||
self._target.post("/files/" + self._path, self._saveCallback, body=text)
|
||||
|
||||
def _saveCallback(self, result, error=False, **kwargs):
|
||||
if not error:
|
||||
self.accept()
|
||||
|
||||
def _refreshSlot(self):
|
||||
self._target.get("/files/" + self._path, self._getCallback)
|
||||
|
||||
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
|
||||
if not error:
|
||||
self.uiFileTextEdit.setText(raw_body.decode("utf-8"))
|
||||
elif result["status"] == 404:
|
||||
if self._default:
|
||||
self.uiFileTextEdit.setText(self._default)
|
||||
121
gns3/dialogs/new_appliance_dialog.py
Normal file
121
gns3/dialogs/new_appliance_dialog.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore
|
||||
from gns3.ui.new_appliance_dialog_ui import Ui_NewApplianceDialog
|
||||
from gns3.dialogs.preferences_dialog import PreferencesDialog
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NewApplianceDialog(QtWidgets.QDialog, Ui_NewApplianceDialog):
|
||||
"""
|
||||
This dialog allow user to create a new appliance by opening
|
||||
the correct creation dialog
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiImportApplianceTemplatePushButton.clicked.connect(self._importApplianceTemplatePushButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpButtonClickedSlot)
|
||||
|
||||
def _importApplianceTemplatePushButtonClickedSlot(self):
|
||||
|
||||
self.accept()
|
||||
from gns3.main_window import MainWindow
|
||||
MainWindow.instance().openApplianceActionSlot()
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
|
||||
self.accept()
|
||||
dialog = PreferencesDialog(self.parent())
|
||||
if self.uiAddIOSRouterRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Dynamips").uiNewIOSRouterPushButton.clicked.emit(False)
|
||||
elif self.uiAddIOUDeviceRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "IOS on UNIX").uiNewIOUDevicePushButton.clicked.emit(False)
|
||||
elif self.uiAddQemuVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "QEMU").uiNewQemuVMPushButton.clicked.emit(False)
|
||||
elif self.uiAddVirtualBoxVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "VirtualBox").uiNewVirtualBoxVMPushButton.clicked.emit(False)
|
||||
elif self.uiAddVMwareVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "VMware").uiNewVMwareVMPushButton.clicked.emit(False)
|
||||
elif self.uiAddDockerVMRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Docker").uiNewDockerVMPushButton.clicked.emit(False)
|
||||
elif self.uiAddVPCSRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "VPCS").uiNewVPCSPushButton.clicked.emit(False)
|
||||
elif self.uiAddCloudRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Cloud nodes").uiNewCloudNodePushButton.clicked.emit(False)
|
||||
elif self.uiAddEthernetHubRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Ethernet hubs").uiNewEthernetHubPushButton.clicked.emit(False)
|
||||
elif self.uiAddEthernetSwitchRadioButton.isChecked():
|
||||
self._setPreferencesPane(dialog, "Ethernet switches").uiNewEthernetSwitchPushButton.clicked.emit(False)
|
||||
else:
|
||||
return
|
||||
dialog.exec_()
|
||||
|
||||
def _helpButtonClickedSlot(self):
|
||||
|
||||
help_text = """<html><p>This dialog helps you to add an appliance template in GNS3. In all cases you must provide your own images.</p>
|
||||
<p>You can download appliance template files (.gns3appliance) from <a href="https://gns3.com/marketplace/appliances">the GNS3 website</a></p>
|
||||
<p>A template file provides community tested settings to run a specific appliance in GNS3.</p></html>
|
||||
"""
|
||||
QtWidgets.QMessageBox.information(self, "Help for adding a new appliance template", help_text)
|
||||
|
||||
def _setPreferencesPane(self, dialog, name):
|
||||
"""
|
||||
Finds the first child of the QTreeWidgetItem name.
|
||||
|
||||
:param dialog: PreferencesDialog instance
|
||||
:param name: QTreeWidgetItem name
|
||||
|
||||
:returns: current QWidget
|
||||
"""
|
||||
|
||||
panes = dialog.uiTreeWidget.findItems(name, QtCore.Qt.MatchFixedString)
|
||||
if len(panes) > 0:
|
||||
child_pane = panes[0].child(0)
|
||||
dialog.uiTreeWidget.setCurrentItem(child_pane)
|
||||
else:
|
||||
i = 0
|
||||
root = dialog.uiTreeWidget.invisibleRootItem()
|
||||
while i < root.childCount():
|
||||
root_item = root.child(i)
|
||||
x = 0
|
||||
while x < root_item.childCount():
|
||||
item = root_item.child(x)
|
||||
x += 1
|
||||
if item.text(0) == name:
|
||||
dialog.uiTreeWidget.setCurrentItem(item)
|
||||
i += 1
|
||||
dialog.addModifiedPage(dialog.uiStackedWidget.currentWidget())
|
||||
return dialog.uiStackedWidget.currentWidget()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = NewApplianceDialog(main)
|
||||
dialog._setPreferencesPane(PreferencesDialog(main), "Ethernet hubs").uiNewEthernetHubPushButton.clicked.emit(False)
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
@@ -1,140 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..ui.new_project_dialog_ui import Ui_NewProjectDialog
|
||||
|
||||
|
||||
class NewProjectDialog(QtWidgets.QDialog, Ui_NewProjectDialog):
|
||||
|
||||
"""
|
||||
New project dialog.
|
||||
|
||||
:param parent: parent widget.
|
||||
:param showed_from_startup: boolean to indicate if this dialog
|
||||
has been opened automatically when GNS3 started.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, showed_from_startup=False):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = parent
|
||||
self._project_settings = {}
|
||||
default_project_name = "untitled"
|
||||
self.uiNameLineEdit.setText(default_project_name)
|
||||
self.uiLocationLineEdit.setText(os.path.join(self._main_window.projectsDirPath(), default_project_name))
|
||||
|
||||
self.uiNameLineEdit.textEdited.connect(self._projectNameSlot)
|
||||
self.uiLocationBrowserToolButton.clicked.connect(self._projectPathSlot)
|
||||
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
|
||||
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
|
||||
|
||||
if not showed_from_startup:
|
||||
self.uiOpenProjectPushButton.hide()
|
||||
self.uiRecentProjectsPushButton.hide()
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
"""
|
||||
Event handler in order to properly handle escape.
|
||||
"""
|
||||
|
||||
if e.key() == QtCore.Qt.Key_Escape:
|
||||
self.close()
|
||||
|
||||
def _projectNameSlot(self, text):
|
||||
|
||||
project_dir = self._main_window.projectsDirPath()
|
||||
if os.path.dirname(self.uiLocationLineEdit.text()) == project_dir:
|
||||
self.uiLocationLineEdit.setText(os.path.join(project_dir, text))
|
||||
|
||||
def _projectPathSlot(self):
|
||||
"""
|
||||
Slot to select the a new project location.
|
||||
"""
|
||||
|
||||
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Project location", os.path.join(self._main_window.projectsDirPath(),
|
||||
self.uiNameLineEdit.text()))
|
||||
|
||||
if path:
|
||||
self.uiLocationLineEdit.setText(path)
|
||||
|
||||
def getNewProjectSettings(self):
|
||||
|
||||
return self._project_settings
|
||||
|
||||
def _menuTriggeredSlot(self, action):
|
||||
"""
|
||||
Closes this dialog when a recent project
|
||||
has been opened.
|
||||
|
||||
:param action: ignored.
|
||||
"""
|
||||
|
||||
self.reject()
|
||||
|
||||
def _openProjectActionSlot(self):
|
||||
"""
|
||||
Opens a project and closes this dialog.
|
||||
"""
|
||||
|
||||
self._main_window.openProjectActionSlot()
|
||||
self.reject()
|
||||
|
||||
def _showRecentProjectsSlot(self):
|
||||
"""
|
||||
lot to show all the recent projects in a menu.
|
||||
"""
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.triggered.connect(self._menuTriggeredSlot)
|
||||
for action in self._main_window._recent_file_actions:
|
||||
menu.addAction(action)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
def done(self, result):
|
||||
|
||||
if result:
|
||||
project_name = self.uiNameLineEdit.text()
|
||||
project_location = self.uiLocationLineEdit.text()
|
||||
project_type = "local"
|
||||
|
||||
if not project_name:
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project name is empty")
|
||||
return
|
||||
|
||||
if not project_location:
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project location is empty")
|
||||
return
|
||||
|
||||
if os.path.isdir(project_location):
|
||||
reply = QtWidgets.QMessageBox.question(self,
|
||||
"New project",
|
||||
"Location {} already exists, overwrite it?".format(project_location),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
self._project_settings["project_name"] = project_name
|
||||
self._project_settings["project_path"] = os.path.join(project_location, project_name + ".gns3")
|
||||
self._project_settings["project_files_dir"] = project_location
|
||||
self._project_settings["project_type"] = project_type
|
||||
|
||||
super().done(result)
|
||||
@@ -93,6 +93,11 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
|
||||
self.uiNodesTreeWidget.setCurrentItem(item)
|
||||
self.showConfigurationPageSlot(item, 0)
|
||||
self.splitter.setSizes([0, 600])
|
||||
elif len(self._parent_items) > 0:
|
||||
# We have multiple node we select the first group
|
||||
item = next(iter(self._parent_items.values()))
|
||||
self.uiNodesTreeWidget.setCurrentItem(item)
|
||||
self.showConfigurationPageSlot(item, 0)
|
||||
|
||||
def showConfigurationPageSlot(self, item, column):
|
||||
"""
|
||||
@@ -173,12 +178,10 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
|
||||
# all children for that group
|
||||
self.previousItem = None
|
||||
self.previousNode = None
|
||||
settings = item.child(0).settings().copy()
|
||||
node = item.child(0).node()
|
||||
page.saveSettings(settings, node, group=True)
|
||||
settings = page.saveSettings({}, node, group=True)
|
||||
for index in range(0, item.childCount()):
|
||||
child = item.child(index)
|
||||
# child.node().update(settings) #TODO: delete
|
||||
child.settings().update(settings)
|
||||
|
||||
# update the nodes with the settings
|
||||
|
||||
@@ -24,6 +24,7 @@ from ..ui.preferences_dialog_ui import Ui_PreferencesDialog
|
||||
from ..pages.server_preferences_page import ServerPreferencesPage
|
||||
from ..pages.general_preferences_page import GeneralPreferencesPage
|
||||
from ..pages.packet_capture_preferences_page import PacketCapturePreferencesPage
|
||||
from ..pages.gns3_vm_preferences_page import GNS3VMPreferencesPage
|
||||
from ..modules import MODULES
|
||||
|
||||
|
||||
@@ -42,11 +43,17 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
self.setupUi(self)
|
||||
|
||||
# We adapt the max size to the screen resolution
|
||||
# We need to manually do that otherwise on small screen the windows
|
||||
# could be bigger than the screen instead of displaying scrollbars
|
||||
height = QtWidgets.QDesktopWidget().screenGeometry().height() - 100
|
||||
if height > 800:
|
||||
height = 800
|
||||
self.setMaximumSize(QtCore.QSize(900, height))
|
||||
self.resize(900, height)
|
||||
width = QtWidgets.QDesktopWidget().screenGeometry().width() - 100
|
||||
|
||||
# 980 is the default width
|
||||
if self.width() > width:
|
||||
self.resize(width, self.height())
|
||||
# 680 is the default height
|
||||
if self.height() > height:
|
||||
self.resize(self.width(), height)
|
||||
|
||||
self.uiTreeWidget.currentItemChanged.connect(self._showPreferencesPageSlot)
|
||||
self._applyButton = self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply)
|
||||
@@ -59,10 +66,11 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
# select the first available page
|
||||
self.uiTreeWidget.setCurrentItem(self._items[0])
|
||||
|
||||
# set the maximum width based on the content of column 0
|
||||
self.uiTreeWidget.setMaximumWidth(self.uiTreeWidget.sizeHintForColumn(0) + 10)
|
||||
|
||||
# Something has change?
|
||||
self._modified = False
|
||||
|
||||
|
||||
self._modified_pages = set()
|
||||
|
||||
def _loadPreferencePages(self):
|
||||
"""
|
||||
@@ -73,6 +81,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
pages = [
|
||||
GeneralPreferencesPage,
|
||||
ServerPreferencesPage,
|
||||
GNS3VMPreferencesPage,
|
||||
PacketCapturePreferencesPage,
|
||||
]
|
||||
|
||||
@@ -115,7 +124,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
# Class name, changed signal
|
||||
widget_to_watch = {
|
||||
QtWidgets.QLineEdit: "textChanged",
|
||||
QtWidgets.QTreeWidget: "itemChanged",
|
||||
# QtWidgets.QTreeWidget: "itemChanged",
|
||||
QtWidgets.QComboBox: "currentIndexChanged",
|
||||
QtWidgets.QSpinBox: "valueChanged",
|
||||
QtWidgets.QAbstractButton: "pressed"
|
||||
@@ -127,10 +136,21 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
|
||||
def _preferenceChangeSlot(self, *args):
|
||||
"""
|
||||
Called when somthing change in the preference dialog
|
||||
Called when something change in the preference dialog
|
||||
"""
|
||||
self._applyButton.setEnabled(True)
|
||||
self._modified = True
|
||||
|
||||
# Found the page with the change
|
||||
widget = self.sender()
|
||||
while widget.parent() != self.uiStackedWidget:
|
||||
widget = widget.parent()
|
||||
|
||||
self.addModifiedPage(widget)
|
||||
|
||||
def addModifiedPage(self, widget):
|
||||
# The widget can trigger signal before the end of init due to async api call
|
||||
if not hasattr(widget, 'pageInitialized') or widget.pageInitialized():
|
||||
self._applyButton.setEnabled(True)
|
||||
self._modified_pages.add(widget)
|
||||
|
||||
def _showPreferencesPageSlot(self, current, previous):
|
||||
"""
|
||||
@@ -152,7 +172,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
self.uiTitleLabel.setText("{} preferences".format(name))
|
||||
index = self.uiStackedWidget.indexOf(preferences_page)
|
||||
widget = self.uiStackedWidget.widget(index)
|
||||
self.uiStackedWidget.setMinimumSize(widget.size())
|
||||
# self.uiStackedWidget.setMinimumSize(widget.size())
|
||||
self.uiStackedWidget.resize(widget.size())
|
||||
self.uiStackedWidget.setCurrentIndex(index)
|
||||
|
||||
@@ -162,15 +182,14 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
"""
|
||||
|
||||
success = True
|
||||
for item in self._items:
|
||||
preferences_page = item.data(0, QtCore.Qt.UserRole)
|
||||
for preferences_page in self._modified_pages:
|
||||
ok = preferences_page.savePreferences()
|
||||
# if page.savePreferences() returns None, assume success
|
||||
if ok is not None and not ok:
|
||||
success = False
|
||||
if success:
|
||||
self._applyButton.setEnabled(False)
|
||||
self._modified = False
|
||||
self._modified_pages = set()
|
||||
return success
|
||||
|
||||
def reject(self):
|
||||
@@ -178,10 +197,12 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
Closes this dialog.
|
||||
"""
|
||||
|
||||
if self._modified:
|
||||
if len(self._modified_pages) > 0:
|
||||
# Get the title of pages with modifications
|
||||
pages_title = ', '.join([page.windowTitle() for page in self._modified_pages])
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"Preferences",
|
||||
"You have unsaved preferences.\n\nContinue without saving?",
|
||||
"You have unsaved preferences in {}.\n\nContinue without saving?".format(pages_title),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
|
||||
86
gns3/dialogs/profile_select.py
Normal file
86
gns3/dialogs/profile_select.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.ui.profile_select_dialog_ui import Ui_ProfileSelectDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
"""
|
||||
This dialog allow user to choose a profile of settings
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
if parent is None:
|
||||
self._main = QtWidgets.QMainWindow()
|
||||
self._main.hide()
|
||||
parent = self._main
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.uiNewPushButton.clicked.connect(self._newPushButtonSlot)
|
||||
|
||||
# Center on screen
|
||||
screen = QtWidgets.QApplication.desktop().screenGeometry()
|
||||
self.move(screen.center() - self.rect().center())
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
path = os.path.join(appdata, "GNS3")
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
path = os.path.join(home, ".config", "GNS3")
|
||||
profiles_path = os.path.join(path, "profiles")
|
||||
|
||||
self.uiShowAtStartupCheckBox.setChecked(LocalConfig.instance().multiProfiles())
|
||||
self.uiProfileSelectComboBox.addItem("default")
|
||||
|
||||
try:
|
||||
if os.path.exists(profiles_path):
|
||||
for profil in sorted(os.listdir(os.path.join(path, "profiles"))):
|
||||
self.uiProfileSelectComboBox.addItem(profil)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def profile(self):
|
||||
return self.uiProfileSelectComboBox.currentText()
|
||||
|
||||
def accept(self):
|
||||
LocalConfig.instance().setMultiProfiles(self.uiShowAtStartupCheckBox.isChecked())
|
||||
super().accept()
|
||||
|
||||
def _newPushButtonSlot(self):
|
||||
profile, ok = QtWidgets.QInputDialog.getText(self.parent(), "New profile", "Profile name:")
|
||||
if ok:
|
||||
self.uiProfileSelectComboBox.addItem(profile)
|
||||
self.uiProfileSelectComboBox.setCurrentText(profile)
|
||||
self.accept()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
dialog = ProfileSelectDialog()
|
||||
dialog.show()
|
||||
exit_code = app.exec_()
|
||||
298
gns3/dialogs/project_dialog.py
Normal file
298
gns3/dialogs/project_dialog.py
Normal file
@@ -0,0 +1,298 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..ui.project_dialog_ui import Ui_ProjectDialog
|
||||
from ..controller import Controller
|
||||
from ..topology import Topology
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
|
||||
"""
|
||||
New project dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, default_project_name="untitled", show_open_options=True):
|
||||
"""
|
||||
:param parent: parent widget.
|
||||
:param default_project_name: Project name by default
|
||||
:param show_open_options: If true allow to open a project from the dialog
|
||||
otherwise it's just for create a project
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = parent
|
||||
self._projects = []
|
||||
self._project_settings = {}
|
||||
self.uiNameLineEdit.setText(default_project_name)
|
||||
self.uiLocationLineEdit.setText(os.path.join(Topology.instance().projectsDirPath(), default_project_name))
|
||||
|
||||
self.uiNameLineEdit.textEdited.connect(self._projectNameSlot)
|
||||
self.uiLocationBrowserToolButton.clicked.connect(self._projectPathSlot)
|
||||
self.uiSettingsPushButton.clicked.connect(self._settingsClickedSlot)
|
||||
|
||||
if show_open_options:
|
||||
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
|
||||
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
|
||||
else:
|
||||
self.uiOpenProjectGroupBox.hide()
|
||||
self.uiProjectTabWidget.removeTab(1)
|
||||
|
||||
# If the controller is remote we hide option for local file system
|
||||
if Controller.instance().isRemote():
|
||||
self.uiLocationLabel.setVisible(False)
|
||||
self.uiLocationLineEdit.setVisible(False)
|
||||
self.uiLocationBrowserToolButton.setVisible(False)
|
||||
self.uiOpenProjectPushButton.setVisible(False)
|
||||
Controller.instance().connected_signal.connect(self._refreshProjects)
|
||||
|
||||
self.uiProjectsTreeWidget.itemDoubleClicked.connect(self._projectsTreeWidgetDoubleClickedSlot)
|
||||
self.uiDeleteProjectButton.clicked.connect(self._deleteProjectSlot)
|
||||
self.uiDuplicateProjectPushButton.clicked.connect(self._duplicateProjectSlot)
|
||||
self.uiRefreshProjectsPushButton.clicked.connect(self._refreshProjects)
|
||||
self._refreshProjects()
|
||||
|
||||
def _refreshProjects(self):
|
||||
Controller.instance().get("/projects", self._projectListCallback)
|
||||
|
||||
def _settingsClickedSlot(self):
|
||||
"""
|
||||
When the user click on the settings button
|
||||
"""
|
||||
self.reject()
|
||||
self._main_window.preferencesActionSlot()
|
||||
|
||||
def _projectsTreeWidgetDoubleClickedSlot(self, item, column):
|
||||
self.done(True)
|
||||
|
||||
def _deleteProjectSlot(self):
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Delete project", "No project selected")
|
||||
return
|
||||
|
||||
projects_to_delete = set()
|
||||
for project in self.uiProjectsTreeWidget.selectedItems():
|
||||
project_id = project.data(0, QtCore.Qt.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"Delete project",
|
||||
'Delete project "{}"?\nThis cannot be reverted.'.format(project_name),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
projects_to_delete.add(project_id)
|
||||
|
||||
for project_id in projects_to_delete:
|
||||
Controller.instance().delete("/projects/{}".format(project_id), self._deleteProjectCallback)
|
||||
|
||||
def _deleteProjectCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while deleting project: {}".format(result["message"]))
|
||||
return
|
||||
Controller.instance().get("/projects", self._projectListCallback)
|
||||
|
||||
def _duplicateProjectSlot(self):
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Duplicate project", "No project selected")
|
||||
return
|
||||
|
||||
if len(self.uiProjectsTreeWidget.selectedItems()) > 1:
|
||||
QtWidgets.QMessageBox.critical(self, "Duplicate project", "Please select only one project to duplicate")
|
||||
return
|
||||
|
||||
for project in self.uiProjectsTreeWidget.selectedItems():
|
||||
project_id = project.data(0, QtCore.Qt.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
new_project_name = project_name + "-1"
|
||||
existing_project_name = [p["name"] for p in self._projects]
|
||||
i = 1
|
||||
while new_project_name in existing_project_name:
|
||||
new_project_name = "{}-{}".format(project_name, i)
|
||||
i += 1
|
||||
|
||||
name, reply = QtWidgets.QInputDialog.getText(self,
|
||||
"Duplicate project",
|
||||
'Duplicate project "{}"?.'.format(project_name),
|
||||
QtWidgets.QLineEdit.Normal,
|
||||
new_project_name)
|
||||
name = name.strip()
|
||||
if reply and len(name) > 0:
|
||||
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id), self._duplicateCallback, body={"name": name})
|
||||
|
||||
def _duplicateCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while duplicate project: {}".format(result["message"]))
|
||||
return
|
||||
Controller.instance().get("/projects", self._projectListCallback)
|
||||
|
||||
def _projectListCallback(self, result, error=False, **kwargs):
|
||||
self.uiProjectsTreeWidget.clear()
|
||||
self.uiDeleteProjectButton.setEnabled(False)
|
||||
if not error:
|
||||
self._projects = result
|
||||
self.uiProjectsTreeWidget.setUpdatesEnabled(False)
|
||||
items = []
|
||||
for project in result:
|
||||
path = os.path.join(project["path"], project["filename"])
|
||||
item = QtWidgets.QTreeWidgetItem([project["name"], project["status"], path])
|
||||
item.setData(0, QtCore.Qt.UserRole, project["project_id"])
|
||||
item.setData(1, QtCore.Qt.UserRole, project["name"])
|
||||
item.setData(2, QtCore.Qt.UserRole, path)
|
||||
items.append(item)
|
||||
self.uiProjectsTreeWidget.addTopLevelItems(items)
|
||||
|
||||
if len(result):
|
||||
self.uiDeleteProjectButton.setEnabled(True)
|
||||
|
||||
self.uiProjectsTreeWidget.header().setResizeContentsPrecision(100) # How many row is checked for the resize for performance reason
|
||||
self.uiProjectsTreeWidget.resizeColumnToContents(0)
|
||||
self.uiProjectsTreeWidget.resizeColumnToContents(1)
|
||||
self.uiProjectsTreeWidget.resizeColumnToContents(2)
|
||||
self.uiProjectsTreeWidget.sortItems(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiProjectsTreeWidget.setUpdatesEnabled(True)
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
"""
|
||||
Event handler in order to properly handle escape.
|
||||
"""
|
||||
|
||||
if e.key() == QtCore.Qt.Key_Escape:
|
||||
self.close()
|
||||
|
||||
def _projectNameSlot(self, text):
|
||||
|
||||
project_dir = Topology.instance().projectsDirPath()
|
||||
if os.path.dirname(self.uiLocationLineEdit.text()) == project_dir:
|
||||
self.uiLocationLineEdit.setText(os.path.join(project_dir, text))
|
||||
|
||||
def _projectPathSlot(self):
|
||||
"""
|
||||
Slot to select the a new project location.
|
||||
"""
|
||||
|
||||
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Project location", os.path.join(Topology.instance().projectsDirPath(),
|
||||
self.uiNameLineEdit.text()))
|
||||
|
||||
if path:
|
||||
self.uiNameLineEdit.setText(os.path.basename(path))
|
||||
self.uiLocationLineEdit.setText(path)
|
||||
|
||||
def getProjectSettings(self):
|
||||
|
||||
return self._project_settings
|
||||
|
||||
def _menuTriggeredSlot(self, action):
|
||||
"""
|
||||
Closes this dialog when a recent project
|
||||
has been opened.
|
||||
|
||||
:param action: ignored.
|
||||
"""
|
||||
|
||||
self.reject()
|
||||
|
||||
def _openProjectActionSlot(self):
|
||||
"""
|
||||
Opens a project and closes this dialog.
|
||||
"""
|
||||
|
||||
self._main_window.openProjectActionSlot()
|
||||
self.reject()
|
||||
|
||||
def _showRecentProjectsSlot(self):
|
||||
"""
|
||||
lot to show all the recent projects in a menu.
|
||||
"""
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.triggered.connect(self._menuTriggeredSlot)
|
||||
for action in self._main_window._recent_project_actions:
|
||||
menu.addAction(action)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
def _overwriteProjectCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
# A 404 could arrive if someone else as deleted the project
|
||||
if "status" not in result or result["status"] != 404:
|
||||
return
|
||||
elif "message" in result:
|
||||
QtWidgets.QMessageBox.critical(self,
|
||||
"New Project",
|
||||
"Error while overwrite project: {}".format(result["message"]))
|
||||
self._projects = []
|
||||
self._refreshProjects()
|
||||
self.done(True)
|
||||
|
||||
def _newProject(self):
|
||||
project_name = self.uiNameLineEdit.text()
|
||||
|
||||
if not project_name:
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project name is empty")
|
||||
return False
|
||||
|
||||
for existing_project in self._projects:
|
||||
if project_name == existing_project["name"]:
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"New project",
|
||||
"Project {} already exists, overwrite it?".format(project_name),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
Controller.instance().delete("/projects/{}".format(existing_project["project_id"]), self._overwriteProjectCallback)
|
||||
|
||||
# In all cases we cancel the new project and if project success to delete
|
||||
# we will call done again
|
||||
return False
|
||||
|
||||
self._project_settings["project_name"] = project_name
|
||||
|
||||
if not Controller.instance().isRemote():
|
||||
project_location = self.uiLocationLineEdit.text()
|
||||
if not project_location:
|
||||
QtWidgets.QMessageBox.critical(self, "New project", "Project location is empty")
|
||||
return False
|
||||
|
||||
self._project_settings["project_path"] = os.path.join(project_location, project_name + ".gns3")
|
||||
self._project_settings["project_files_dir"] = project_location
|
||||
return True
|
||||
|
||||
def done(self, result):
|
||||
|
||||
if result:
|
||||
if self.uiProjectTabWidget.currentIndex() == 0:
|
||||
if not self._newProject():
|
||||
return
|
||||
else:
|
||||
current = self.uiProjectsTreeWidget.currentItem()
|
||||
if current is None:
|
||||
QtWidgets.QMessageBox.critical(self, "Open project", "No project selected")
|
||||
return
|
||||
|
||||
self._project_settings["project_id"] = current.data(0, QtCore.Qt.UserRole)
|
||||
self._project_settings["project_name"] = current.data(1, QtCore.Qt.UserRole)
|
||||
self._project_settings["project_path"] = current.data(2, QtCore.Qt.UserRole)
|
||||
super().done(result)
|
||||
@@ -17,16 +17,19 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import psutil
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets, QtGui, QtNetwork, qslot
|
||||
from gns3.controller import Controller
|
||||
from gns3.local_server import LocalServer
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets, QtGui
|
||||
from gns3.servers import Servers
|
||||
from ..gns3_vm import GNS3VM
|
||||
from ..dialogs.preferences_dialog import PreferencesDialog
|
||||
from ..ui.setup_wizard_ui import Ui_SetupWizard
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..utils.wait_for_vm_worker import WaitForVMWorker
|
||||
from ..utils.wait_for_connection_worker import WaitForConnectionWorker
|
||||
from ..version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
@@ -40,12 +43,24 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._gns3_vm_settings = {
|
||||
"enable": True,
|
||||
"headless": False,
|
||||
"when_exit": "stop",
|
||||
"engine": "vmware",
|
||||
"vcpus": 1,
|
||||
"ram": 2048,
|
||||
"vmname": "GNS3 VM"
|
||||
}
|
||||
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
|
||||
|
||||
self._server = Servers.instance().localServer()
|
||||
self.uiLocalServerToolButton.clicked.connect(self._localServerBrowserSlot)
|
||||
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('')
|
||||
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
|
||||
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
|
||||
self.uiVirtualBoxRadioButton.clicked.connect(self._listVirtualBoxVMsSlot)
|
||||
@@ -59,11 +74,46 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
self.uiVmwareRadioButton.setChecked(False)
|
||||
self.uiVirtualBoxRadioButton.setChecked(False)
|
||||
|
||||
# Mandatory fields
|
||||
self.uiLocalServerWizardPage.registerField("path*", self.uiLocalServerPathLineEdit)
|
||||
|
||||
# load all available addresses
|
||||
for address in QtNetwork.QNetworkInterface.allAddresses():
|
||||
address_string = address.toString()
|
||||
# if address.protocol() == QtNetwork.QAbstractSocket.IPv6Protocol:
|
||||
# we do not want the scope id when using an IPv6 address...
|
||||
# address.setScopeId("")
|
||||
self.uiLocalServerHostComboBox.addItem(address_string, address.toString())
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.jpg"))
|
||||
else:
|
||||
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.jpg"))
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
self.uiVMRadioButton.setText("Run the topologies in an isolated and standard VM")
|
||||
self.uiLocalRadioButton.setText("Run the topologies on my computer")
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
self.uiLocalLabel.setVisible(False)
|
||||
|
||||
Controller.instance().connected_signal.connect(self._refreshLocalServerStatusSlot)
|
||||
Controller.instance().connection_failed_signal.connect(self._refreshLocalServerStatusSlot)
|
||||
|
||||
def _localServerBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select a local server.
|
||||
"""
|
||||
|
||||
filter = ""
|
||||
if sys.platform.startswith("win"):
|
||||
filter = "Executable (*.exe);;All files (*.*)"
|
||||
server_path = shutil.which("gns3server")
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select the local server", server_path, filter)
|
||||
if not path:
|
||||
return
|
||||
|
||||
self.uiLocalServerPathLineEdit.setText(path)
|
||||
|
||||
def _VMwareBannerButtonClickedSlot(self):
|
||||
if sys.platform.startswith("darwin"):
|
||||
url = "http://send.onenetworkdirect.net/z/616461/CD225091/"
|
||||
@@ -76,11 +126,13 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
Slot to refresh the VMware VMs list.
|
||||
"""
|
||||
|
||||
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{version}.zip".format(version=__version__)
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('The GNS3 VM can <a href="{download_url}">downloaded here</a>.<br>Import the VM in your virtualization software and hit refresh.'.format(download_url=download_url))
|
||||
self.uiVirtualBoxRadioButton.setChecked(False)
|
||||
from gns3.modules import VMware
|
||||
settings = VMware.instance().settings()
|
||||
if not os.path.exists(settings["vmrun_path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed. You can download it from https://www.vmware.com/support/developer/vix-api/")
|
||||
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed. You can download it from https://www.vmware.com/support/developer/vix-api/. After installation you need to restart GNS3.")
|
||||
return
|
||||
self._refreshVMListSlot()
|
||||
|
||||
@@ -89,23 +141,17 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
Slot to refresh the VirtualBox VMs list.
|
||||
"""
|
||||
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM on VirtualBox", "VirtualBox doesn't support nested virtualization, this means running Qemu based VM could be very slow")
|
||||
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format(version=__version__)
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('If you don\'t have the GNS3 Virtual Machine you can <a href="{download_url}">download it here</a>.<br>And import the VM in the virtualization software and hit refresh.'.format(download_url=download_url))
|
||||
self.uiVmwareRadioButton.setChecked(False)
|
||||
from gns3.modules import VirtualBox
|
||||
settings = VirtualBox.instance().settings()
|
||||
if not os.path.exists(settings["vboxmanage_path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "VirtualBox", "VBoxManage could not be found, VirtualBox is probably not installed")
|
||||
QtWidgets.QMessageBox.critical(self, "VirtualBox", "VBoxManage could not be found, VirtualBox is probably not installed. After installation you need to restart GNS3.")
|
||||
return
|
||||
self._refreshVMListSlot()
|
||||
|
||||
def showit(self):
|
||||
"""
|
||||
Either this dialog should be automatically showed at startup.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return not self.uiShowCheckBox.isChecked()
|
||||
|
||||
def _setPreferencesPane(self, dialog, name):
|
||||
"""
|
||||
Finds the first child of the QTreeWidgetItem name.
|
||||
@@ -121,6 +167,13 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
dialog.uiTreeWidget.setCurrentItem(child_pane)
|
||||
return dialog.uiStackedWidget.currentWidget()
|
||||
|
||||
def _getSettingsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while get gettings: {}".format(result["message"]))
|
||||
return
|
||||
self._gns3_vm_settings = result
|
||||
|
||||
def initializePage(self, page_id):
|
||||
"""
|
||||
Initialize Wizard pages.
|
||||
@@ -129,103 +182,174 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
"""
|
||||
|
||||
super().initializePage(page_id)
|
||||
if self.page(page_id) == self.uiVMWizardPage:
|
||||
cpu_count = psutil.cpu_count()
|
||||
self.uiCPUSpinBox.setValue(cpu_count)
|
||||
# we want to allocate half of the available physical memory
|
||||
ram = int(psutil.virtual_memory().total / (1024 * 1024) / 2)
|
||||
# value must be a multiple of 4 (VMware requirement)
|
||||
ram -= ram % 4
|
||||
self.uiRAMSpinBox.setValue(ram)
|
||||
if self.page(page_id) == self.uiServerWizardPage:
|
||||
Controller.instance().get("/gns3vm", self._getSettingsCallback)
|
||||
elif self.page(page_id) == self.uiVMWizardPage:
|
||||
if self._GNS3VMSettings()["engine"] == "vmware":
|
||||
self.uiVmwareRadioButton.setChecked(True)
|
||||
self._listVMwareVMsSlot()
|
||||
elif self._GNS3VMSettings()["engine"] == "virtualbox":
|
||||
self.uiVirtualBoxRadioButton.setChecked(True)
|
||||
self._listVirtualBoxVMsSlot()
|
||||
self.uiCPUSpinBox.setValue(self._GNS3VMSettings()["vcpus"])
|
||||
self.uiRAMSpinBox.setValue(self._GNS3VMSettings()["ram"])
|
||||
|
||||
elif self.page(page_id) == self.uiLocalServerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self.uiLocalServerPathLineEdit.setText(local_server_settings["path"])
|
||||
index = self.uiLocalServerHostComboBox.findData(local_server_settings["host"])
|
||||
if index != -1:
|
||||
self.uiLocalServerHostComboBox.setCurrentIndex(index)
|
||||
self.uiLocalServerPortSpinBox.setValue(local_server_settings["port"])
|
||||
|
||||
elif self.page(page_id) == self.uiRemoteControllerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self.uiRemoteMainServerHostLineEdit.setText(local_server_settings["host"])
|
||||
self.uiRemoteMainServerPortSpinBox.setValue(local_server_settings["port"])
|
||||
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["user"])
|
||||
self.uiRemoteMainServerPasswordLineEdit.setText(local_server_settings["password"])
|
||||
self.uiRemoteMainServerProtocolComboBox.setCurrentText(local_server_settings["protocol"])
|
||||
self.uiRemoteMainServerAuthCheckBox.setChecked(local_server_settings["auth"])
|
||||
elif self.page(page_id) == self.uiLocalServerStatusWizardPage:
|
||||
self._refreshLocalServerStatusSlot()
|
||||
|
||||
elif self.page(page_id) == self.uiSummaryWizardPage:
|
||||
self.uiSummaryTreeWidget.clear()
|
||||
if self.uiLocalRadioButton.isChecked():
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self._addSummaryEntry("Server type:", "Local")
|
||||
self._addSummaryEntry("Path:", local_server_settings["path"])
|
||||
self._addSummaryEntry("Host:", local_server_settings["host"])
|
||||
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
|
||||
elif self.uiRemoteControllerRadioButton.isChecked():
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self._addSummaryEntry("Server type:", "Remote")
|
||||
self._addSummaryEntry("Host:", local_server_settings["host"])
|
||||
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
|
||||
self._addSummaryEntry("User:", local_server_settings["user"])
|
||||
else:
|
||||
self._addSummaryEntry("Server type:", "GNS3 Virtual Machine")
|
||||
self._addSummaryEntry("VM engine:", self._GNS3VMSettings()["engine"].capitalize())
|
||||
self._addSummaryEntry("VM name:", self._GNS3VMSettings()["vmname"])
|
||||
self._addSummaryEntry("VM vCPUs:", str(self._GNS3VMSettings()["vcpus"]))
|
||||
self._addSummaryEntry("VM RAM:", str(self._GNS3VMSettings()["ram"]) + " MB")
|
||||
|
||||
@qslot
|
||||
def _refreshLocalServerStatusSlot(self):
|
||||
"""
|
||||
Refresh the local server status page
|
||||
"""
|
||||
if Controller.instance().connected():
|
||||
self.uiLocalServerStatusLabel.setText("Connection to local server successfull")
|
||||
elif Controller.instance().connecting():
|
||||
self.uiLocalServerStatusLabel.setText("Please wait connection to the GNS3 server")
|
||||
else:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
self.uiLocalServerStatusLabel.setText("Connection to local server failed.\n* Make sure GNS3 is authorized in your firewall.\n* Go back and try to change server port\n* Please check in a browser if you can connect to {protocol}://{host}:{port}.\n* If it's not working try to run {path} in a terminal to see if you have an error.".format(protocol=local_server_settings["protocol"], host=local_server_settings["host"], port=local_server_settings["port"], path=local_server_settings["path"]))
|
||||
|
||||
def _GNS3VMSettings(self):
|
||||
return self._gns3_vm_settings
|
||||
|
||||
def _setGNS3VMSettings(self, settings):
|
||||
Controller.instance().put("/gns3vm", self._saveSettingsCallback, settings, timeout=60 * 5)
|
||||
|
||||
def _saveSettingsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
QtWidgets.QMessageBox.critical(self, "Save settings", "Error while save settings: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def _addSummaryEntry(self, name, value):
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiSummaryTreeWidget, [name, value])
|
||||
item.setText(0, name)
|
||||
font = item.font(0)
|
||||
font.setBold(True)
|
||||
item.setFont(0, font)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the settings.
|
||||
"""
|
||||
|
||||
gns3_vm = GNS3VM.instance()
|
||||
servers = Servers.instance()
|
||||
if self.currentPage() == self.uiVMWizardPage:
|
||||
vmname = self.uiVMListComboBox.currentText()
|
||||
if vmname:
|
||||
# save the GNS3 VM settings
|
||||
vm_settings = {"auto_start": True,
|
||||
"vmname": vmname,
|
||||
"vmx_path": self.uiVMListComboBox.currentData()}
|
||||
vm_settings = self._GNS3VMSettings()
|
||||
vm_settings["enable"] = True
|
||||
vm_settings["vmname"] = vmname
|
||||
|
||||
if self.uiVmwareRadioButton.isChecked():
|
||||
vm_settings["virtualization"] = "VMware"
|
||||
vm_settings["engine"] = "vmware"
|
||||
elif self.uiVirtualBoxRadioButton.isChecked():
|
||||
vm_settings["virtualization"] = "VirtualBox"
|
||||
gns3_vm.setSettings(vm_settings)
|
||||
servers.save()
|
||||
vm_settings["engine"] = "virtualbox"
|
||||
|
||||
# set the vCPU count and RAM
|
||||
vpcus = self.uiCPUSpinBox.value()
|
||||
ram = self.uiRAMSpinBox.value()
|
||||
if ram < 1024:
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM memory", "It is recommended to allocate a minimum of 1024 MB of RAM to the GNS3 VM")
|
||||
available_ram = int(psutil.virtual_memory().available / (1024 * 1024))
|
||||
if ram > available_ram:
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM memory", "You have probably allocated too much memory for the GNS3 VM! (available memory is {} MB)".format(available_ram))
|
||||
if gns3_vm.setvCPUandRAM(vpcus, ram) is False:
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "Could not configure vCPUs and RAM amounts for the GNS3 VM")
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM memory", "It is recommended to allocate a minimum of 1024 MB of memory to the GNS3 VM")
|
||||
vm_settings["vcpus"] = vpcus
|
||||
vm_settings["ram"] = ram
|
||||
|
||||
# start the GNS3 VM
|
||||
servers.initVMServer()
|
||||
worker = WaitForVMWorker()
|
||||
progress_dialog = ProgressDialog(worker, "GNS3 VM", "Starting the GNS3 VM...", "Cancel", busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if progress_dialog.exec_():
|
||||
previous_local_server_ip = servers.localServer().host()
|
||||
new_local_server_ip = gns3_vm.adjustLocalServerIP()
|
||||
self.uiShowCheckBox.setChecked(True)
|
||||
# restart the local server if necessary
|
||||
if new_local_server_ip != previous_local_server_ip:
|
||||
servers.stopLocalServer(wait=True)
|
||||
if servers.startLocalServer():
|
||||
worker = WaitForConnectionWorker(new_local_server_ip, servers.localServer().port())
|
||||
dialog = ProgressDialog(worker, "Local server", "Connecting...", "Cancel", busy=True, parent=self)
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
self._setGNS3VMSettings(vm_settings)
|
||||
else:
|
||||
if not self.uiVmwareRadioButton.isChecked() and not self.uiVirtualBoxRadioButton.isChecked():
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "Please select VMware or VirtualBox")
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "Please select a VM. If no VM is listed, check if the GNS3 VM is correctly imported and press refresh.")
|
||||
return False
|
||||
elif self.currentPage() == self.uiAddVMsWizardPage:
|
||||
elif self.currentPage() == self.uiLocalServerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
local_server_settings["auto_start"] = True
|
||||
local_server_settings["path"] = self.uiLocalServerPathLineEdit.text().strip()
|
||||
local_server_settings["host"] = self.uiLocalServerHostComboBox.itemData(self.uiLocalServerHostComboBox.currentIndex())
|
||||
local_server_settings["port"] = self.uiLocalServerPortSpinBox.value()
|
||||
|
||||
use_local_server = self.uiLocalRadioButton.isChecked()
|
||||
if use_local_server:
|
||||
if not os.path.isfile(local_server_settings["path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "Local server", "Could not find local server {}".format(local_server_settings["path"]))
|
||||
return False
|
||||
if not os.access(local_server_settings["path"], os.X_OK):
|
||||
QtWidgets.QMessageBox.critical(self, "Local server", "{} is not an executable".format(local_server_settings["path"]))
|
||||
return False
|
||||
|
||||
LocalServer.instance().updateLocalServerSettings(local_server_settings)
|
||||
|
||||
elif self.currentPage() == self.uiRemoteControllerWizardPage:
|
||||
local_server_settings = LocalServer.instance().localServerSettings()
|
||||
local_server_settings["auto_start"] = False
|
||||
local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
|
||||
local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
|
||||
local_server_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText()
|
||||
local_server_settings["user"] = self.uiRemoteMainServerUserLineEdit.text()
|
||||
local_server_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
|
||||
local_server_settings["auth"] = self.uiRemoteMainServerAuthCheckBox.isChecked()
|
||||
LocalServer.instance().updateLocalServerSettings(local_server_settings)
|
||||
|
||||
elif self.currentPage() == self.uiSummaryWizardPage:
|
||||
if self.uiLocalRadioButton.isChecked():
|
||||
# deactivate the GNS3 VM if using the local server
|
||||
vm_settings = {"auto_start": False}
|
||||
gns3_vm.setSettings(vm_settings)
|
||||
servers.save()
|
||||
self.uiShowCheckBox.setChecked(True)
|
||||
vm_settings = self._GNS3VMSettings()
|
||||
vm_settings["enable"] = False
|
||||
self._setGNS3VMSettings(vm_settings)
|
||||
|
||||
from gns3.modules import Dynamips
|
||||
Dynamips.instance().setSettings({"use_local_server": use_local_server})
|
||||
if sys.platform.startswith("linux"):
|
||||
# IOU only works on Linux
|
||||
from gns3.modules import IOU
|
||||
IOU.instance().setSettings({"use_local_server": use_local_server})
|
||||
from gns3.modules import Qemu
|
||||
Qemu.instance().setSettings({"use_local_server": use_local_server})
|
||||
from gns3.modules import VPCS
|
||||
VPCS.instance().setSettings({"use_local_server": use_local_server})
|
||||
# update the modules so they use the local server
|
||||
from gns3.modules import Dynamips
|
||||
Dynamips.instance().setSettings({"use_local_server": True})
|
||||
if sys.platform.startswith("linux"):
|
||||
# IOU only works on Linux
|
||||
from gns3.modules import IOU
|
||||
IOU.instance().setSettings({"use_local_server": True})
|
||||
from gns3.modules import Qemu
|
||||
Qemu.instance().setSettings({"use_local_server": True})
|
||||
from gns3.modules import VPCS
|
||||
VPCS.instance().setSettings({"use_local_server": True})
|
||||
|
||||
elif self.currentPage() == self.uiLocalServerStatusWizardPage:
|
||||
if not Controller.instance().connected():
|
||||
return False
|
||||
|
||||
dialog = PreferencesDialog(self)
|
||||
if self.uiAddIOSRouterCheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "Dynamips").uiNewIOSRouterPushButton.clicked.emit(False)
|
||||
if self.uiAddIOUDeviceCheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "IOS on UNIX").uiNewIOUDevicePushButton.clicked.emit(False)
|
||||
if self.uiAddQemuVMcheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "QEMU").uiNewQemuVMPushButton.clicked.emit(False)
|
||||
if self.uiAddVirtualBoxVMcheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "VirtualBox").uiNewVirtualBoxVMPushButton.clicked.emit(False)
|
||||
if self.uiAddVMwareVMcheckBox.isChecked():
|
||||
self._setPreferencesPane(dialog, "VMware").uiNewVMwareVMPushButton.clicked.emit(False)
|
||||
dialog.exec_()
|
||||
return True
|
||||
|
||||
def _refreshVMListSlot(self):
|
||||
@@ -233,11 +357,10 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
Refresh the list of VM available in VMware or VirtualBox.
|
||||
"""
|
||||
|
||||
server = Servers.instance().localServer()
|
||||
if self.uiVmwareRadioButton.isChecked():
|
||||
server.get("/vmware/vms", self._getVMsFromServerCallback)
|
||||
Controller.instance().get("/gns3vm/engines/vmware/vms", self._getVMsFromServerCallback, progressText="Retrieving VMware VM list from server...")
|
||||
elif self.uiVirtualBoxRadioButton.isChecked():
|
||||
server.get("/virtualbox/vms", self._getVMsFromServerCallback)
|
||||
Controller.instance().get("/gns3vm/engines/virtualbox/vms", self._getVMsFromServerCallback, progressText="Retrieving VirtualBox VM list from server...")
|
||||
|
||||
def _getVMsFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -253,9 +376,9 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
else:
|
||||
self.uiVMListComboBox.clear()
|
||||
for vm in result:
|
||||
self.uiVMListComboBox.addItem(vm["vmname"], vm.get("vmx_path", ""))
|
||||
gns3_vm = Servers.instance().vmSettings()
|
||||
index = self.uiVMListComboBox.findText(gns3_vm["vmname"])
|
||||
self.uiVMListComboBox.addItem(vm["vmname"])
|
||||
|
||||
index = self.uiVMListComboBox.findText(self._GNS3VMSettings()["vmname"])
|
||||
if index != -1:
|
||||
self.uiVMListComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
@@ -273,7 +396,11 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
"""
|
||||
|
||||
settings = self.parentWidget().settings()
|
||||
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
|
||||
if result:
|
||||
settings["hide_setup_wizard"] = True
|
||||
else:
|
||||
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
|
||||
|
||||
self.parentWidget().setSettings(settings)
|
||||
super().done(result)
|
||||
|
||||
@@ -283,7 +410,21 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
"""
|
||||
|
||||
current_id = self.currentId()
|
||||
if self.page(current_id) == self.uiServerWizardPage and not self.uiVMRadioButton.isChecked():
|
||||
# skip the GNS3 VM page if using the local server.
|
||||
return self.uiServerWizardPage.nextId() + 1
|
||||
if self.page(current_id) == self.uiLocalServerStatusWizardPage and not self.uiVMRadioButton.isChecked():
|
||||
return self._pageId(self.uiSummaryWizardPage)
|
||||
|
||||
if self.page(current_id) == self.uiServerWizardPage and self.uiRemoteControllerRadioButton.isChecked():
|
||||
return self._pageId(self.uiRemoteControllerWizardPage)
|
||||
|
||||
if self.page(current_id) == self.uiVMWizardPage:
|
||||
return self._pageId(self.uiSummaryWizardPage)
|
||||
return QtWidgets.QWizard.nextId(self)
|
||||
|
||||
def _pageId(self, page):
|
||||
"""
|
||||
Return id of the page
|
||||
"""
|
||||
for id in self.pageIds():
|
||||
if self.page(id) == page:
|
||||
return id
|
||||
raise KeyError
|
||||
|
||||
@@ -28,8 +28,13 @@ from ..qt import QtCore, QtWidgets
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..utils.process_files_worker import ProcessFilesWorker
|
||||
from ..ui.snapshots_dialog_ui import Ui_SnapshotsDialog
|
||||
from ..topology import Topology
|
||||
from ..node import Node
|
||||
from ..controller import Controller
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
@@ -40,41 +45,37 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, parent, project_path, project_files_dir):
|
||||
def __init__(self, parent, project):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._project_path = project_path
|
||||
self._project_files_dir = os.path.join(project_files_dir, "project-files")
|
||||
self._project = project
|
||||
|
||||
self.uiCreatePushButton.clicked.connect(self._createSnapshotSlot)
|
||||
self.uiDeletePushButton.clicked.connect(self._deleteSnapshotSlot)
|
||||
self.uiRestorePushButton.clicked.connect(self._restoreSnapshotSlot)
|
||||
self.uiSnapshotsList.itemDoubleClicked.connect(self._snapshotDoubleClickedSlot)
|
||||
self._listSnaphosts()
|
||||
self._listSnapshots()
|
||||
|
||||
def _listSnaphosts(self):
|
||||
def _listSnapshots(self):
|
||||
"""
|
||||
Lists all available snapshots.
|
||||
"""
|
||||
|
||||
self.uiSnapshotsList.clear()
|
||||
snapshot_dir = os.path.join(self._project_files_dir, "snapshots")
|
||||
if not os.path.isdir(snapshot_dir):
|
||||
Controller.instance().get("/projects/{}/snapshots".format(self._project.id()), self._listSnapshotsCallback)
|
||||
|
||||
def _listSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
return
|
||||
|
||||
for snapshot in os.listdir(snapshot_dir):
|
||||
match = re.search(r"^(.*)_([0-9]+)_([0-9]+)", snapshot)
|
||||
if match:
|
||||
snapshot_name = match.group(1)
|
||||
snapshot_date = match.group(2)[:2] + '/' + match.group(2)[2:4] + '/' + match.group(2)[4:]
|
||||
snapshot_time = match.group(3)[:2] + ':' + match.group(3)[2:4] + ':' + match.group(3)[4:]
|
||||
item = QtWidgets.QListWidgetItem(self.uiSnapshotsList)
|
||||
item.setText("{} on {} at {}".format(snapshot_name, snapshot_date, snapshot_time))
|
||||
item.setData(QtCore.Qt.UserRole, os.path.join(snapshot_dir, snapshot))
|
||||
|
||||
self.uiSnapshotsList.sortItems(QtCore.Qt.AscendingOrder)
|
||||
for snapshot in result:
|
||||
item = QtWidgets.QListWidgetItem(self.uiSnapshotsList)
|
||||
item.setText("{} on {}".format(snapshot["name"], datetime.fromtimestamp(snapshot["created_at"]).strftime("%d/%m/%y at %H:%M:%S")))
|
||||
item.setData(QtCore.Qt.UserRole, snapshot["snapshot_id"])
|
||||
|
||||
if self.uiSnapshotsList.count():
|
||||
self.uiSnapshotsList.setCurrentRow(0)
|
||||
@@ -91,15 +92,14 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.Normal, "Unnamed")
|
||||
if ok and snapshot_name:
|
||||
from ..main_window import MainWindow
|
||||
MainWindow.instance().saveProject(self._project_path)
|
||||
snapshot_name = "{name}_{date}".format(name=snapshot_name, date=time.strftime("%d%m%y_%H%M%S"))
|
||||
snapshot_dir = os.path.join(self._project_files_dir, "snapshots", snapshot_name)
|
||||
worker = ProcessFilesWorker(os.path.dirname(self._project_path), snapshot_dir, skip_dirs=["snapshots"])
|
||||
progress_dialog = ProgressDialog(worker, "Creating snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
self._listSnaphosts()
|
||||
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()), self._createSnapshotsCallback, {"name": snapshot_name})
|
||||
|
||||
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
return
|
||||
self._listSnapshots()
|
||||
|
||||
def _deleteSnapshotSlot(self):
|
||||
"""
|
||||
@@ -108,9 +108,15 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
item = self.uiSnapshotsList.currentItem()
|
||||
if item:
|
||||
snapshot_path = item.data(QtCore.Qt.UserRole)
|
||||
shutil.rmtree(snapshot_path, ignore_errors=True)
|
||||
self._listSnaphosts()
|
||||
snapshot_id = item.data(QtCore.Qt.UserRole)
|
||||
Controller.instance().delete("/projects/{}/snapshots/{}".format(self._project.id(), snapshot_id), self._deleteSnapshotsCallback)
|
||||
|
||||
def _deleteSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
return
|
||||
self._listSnapshots()
|
||||
|
||||
def _restoreSnapshotSlot(self):
|
||||
"""
|
||||
@@ -119,63 +125,26 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
item = self.uiSnapshotsList.currentItem()
|
||||
if item:
|
||||
snapshot_path = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_path)
|
||||
snapshot_id = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_id)
|
||||
|
||||
def _restoreSnapshot(self, snapshot_path):
|
||||
def _restoreSnapshot(self, snapshot_id):
|
||||
"""
|
||||
Restores a snapshot.
|
||||
|
||||
:param snapshot_path: path to the snapshot
|
||||
:param snapshot_id: id of the snapshot
|
||||
"""
|
||||
|
||||
match = re.search(r"^(.*)_([0-9]+)_([0-9]+)", os.path.basename(snapshot_path))
|
||||
if match:
|
||||
snapshot_name = match.group(1)
|
||||
else:
|
||||
snapshot_name = "Unknown"
|
||||
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot \"{}\" was taken?".format(snapshot_name),
|
||||
QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
|
||||
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
|
||||
if reply == QtWidgets.QMessageBox.Cancel:
|
||||
return
|
||||
|
||||
# stop all the nodes
|
||||
topology = Topology.instance()
|
||||
for node in topology.nodes():
|
||||
if hasattr(node, "start") and node.status() == Node.started:
|
||||
node.stop()
|
||||
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id), self._restoreSnapshotsCallback)
|
||||
|
||||
project_name, _ = os.path.splitext(os.path.basename(self._project_path))
|
||||
legacy_project_files_dir = os.path.join(snapshot_path, "{}-files".format(project_name))
|
||||
if os.path.exists(legacy_project_files_dir):
|
||||
# support for pre 1.3 snapshots
|
||||
for root, dirs, _ in os.walk(self._project_files_dir):
|
||||
dirs[:] = [d for d in dirs if d not in "snapshots"]
|
||||
for project_subdir in dirs:
|
||||
project_subdir_path = os.path.join(root, project_subdir)
|
||||
shutil.rmtree(project_subdir_path, ignore_errors=True)
|
||||
|
||||
dirs = os.listdir(legacy_project_files_dir)
|
||||
for snapshot_subdir in dirs:
|
||||
snapshot_subdir_path = os.path.join(legacy_project_files_dir, snapshot_subdir)
|
||||
worker = ProcessFilesWorker(snapshot_subdir_path, os.path.join(self._project_files_dir, snapshot_subdir))
|
||||
progress_dialog = ProgressDialog(worker, "Restoring snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
try:
|
||||
os.remove(self._project_path)
|
||||
shutil.copy(os.path.join(snapshot_path, os.path.basename(self._project_path)), self._project_path)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Restore snapshot", "Cannot restore snapshot: {}".format(e))
|
||||
else:
|
||||
worker = ProcessFilesWorker(snapshot_path, os.path.dirname(self._project_path), skip_dirs=["snapshots"])
|
||||
progress_dialog = ProgressDialog(worker, "Restoring snapshot", "Copying project files...", "Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
from ..main_window import MainWindow
|
||||
MainWindow.instance().loadSnapshot(self._project_path)
|
||||
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
if error:
|
||||
if result:
|
||||
log.error(result["message"])
|
||||
return
|
||||
self.accept()
|
||||
|
||||
def _snapshotDoubleClickedSlot(self, item):
|
||||
@@ -183,5 +152,5 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
Slot to restore a snapshot when it is double clicked.
|
||||
"""
|
||||
|
||||
snapshot_path = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_path)
|
||||
snapshot_id = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_id)
|
||||
|
||||
@@ -20,11 +20,15 @@ Dialog to change node symbols.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from ..qt import QtSvg, QtCore, QtGui, QtWidgets
|
||||
from ..items.pixmap_node_item import PixmapNodeItem
|
||||
from ..qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from ..ui.symbol_selection_dialog_ui import Ui_SymbolSelectionDialog
|
||||
from ..servers import Servers
|
||||
from ..local_server import LocalServer
|
||||
from ..controller import Controller
|
||||
from ..symbol import Symbol
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -39,6 +43,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
:param items: list of items
|
||||
"""
|
||||
|
||||
_symbols_dir = None
|
||||
|
||||
def __init__(self, parent, items=None, symbol=None):
|
||||
|
||||
super().__init__(parent)
|
||||
@@ -51,8 +57,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
self.uiBuiltInSymbolRadioButton.toggled.connect(self._builtInSymbolToggledSlot)
|
||||
self.uiSearchLineEdit.textChanged.connect(self._searchTextChangedSlot)
|
||||
self.uiBuiltinSymbolOnlyCheckBox.toggled.connect(self._builtinSymbolOnlyToggledSlot)
|
||||
self._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
|
||||
self._symbols_path = Servers.instance().localServerSettings()["symbols_path"]
|
||||
if not SymbolSelectionDialog._symbols_dir:
|
||||
SymbolSelectionDialog._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
|
||||
|
||||
if not self._items:
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).hide()
|
||||
@@ -60,42 +66,40 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
self.uiBuiltInSymbolRadioButton.setChecked(True)
|
||||
self.uiSymbolListWidget.setFocus()
|
||||
self.uiSymbolListWidget.setIconSize(QtCore.QSize(64, 64))
|
||||
symbol_resources = QtCore.QResource(":/symbols")
|
||||
self._symbol_items = []
|
||||
symbols = symbol_resources.children()
|
||||
|
||||
try:
|
||||
for file in os.listdir(self._symbols_path):
|
||||
symbols.append(file)
|
||||
except OSError:
|
||||
pass
|
||||
Controller.instance().get("/symbols", self._listSymbolsCallback)
|
||||
|
||||
symbols.sort()
|
||||
for symbol in symbols:
|
||||
if symbol.endswith(".svg") or symbol.endswith(".png"):
|
||||
name = os.path.splitext(symbol)[0]
|
||||
item = QtWidgets.QListWidgetItem(self.uiSymbolListWidget)
|
||||
self._symbol_items.append(item)
|
||||
item.setText(name)
|
||||
def _listSymbolsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while listing symbols: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
self._symbol_items = []
|
||||
for symbol in result:
|
||||
symbol = Symbol(**symbol)
|
||||
name = os.path.splitext(symbol.filename())[0]
|
||||
item = QtWidgets.QListWidgetItem(self.uiSymbolListWidget)
|
||||
item.setData(QtCore.Qt.UserRole, symbol)
|
||||
self._symbol_items.append(item)
|
||||
item.setText(name)
|
||||
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
|
||||
item.setIcon(icon)
|
||||
|
||||
def render(item, path):
|
||||
svg_renderer = QImageSvgRenderer(path)
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
|
||||
if os.path.exists(os.path.join(self._symbols_path, symbol)):
|
||||
svg_renderer = QtSvg.QSvgRenderer(os.path.join(self._symbols_path, symbol))
|
||||
if svg_renderer.isValid():
|
||||
svg_renderer.render(QtGui.QPainter(image))
|
||||
else:
|
||||
image.load(os.path.join(self._symbols_path, symbol))
|
||||
else:
|
||||
resource_path = ":/symbols/" + symbol
|
||||
svg_renderer = QtSvg.QSvgRenderer(resource_path)
|
||||
svg_renderer.render(QtGui.QPainter(image))
|
||||
|
||||
svg_renderer.render(QtGui.QPainter(image))
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
|
||||
item.setIcon(icon)
|
||||
|
||||
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
|
||||
self.adjustSize()
|
||||
|
||||
def _builtinSymbolOnlyToggledSlot(self, checked):
|
||||
@@ -110,7 +114,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
"""
|
||||
text = self.uiSearchLineEdit.text()
|
||||
for item in self._symbol_items:
|
||||
if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not QtCore.QResource(":/symbols/{}.svg".format(item.text())).isValid():
|
||||
if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not item.data(QtCore.Qt.UserRole).builtin():
|
||||
item.setHidden(True)
|
||||
else:
|
||||
if len(text.strip()) == 0 or text.strip().lower() in item.text().lower():
|
||||
@@ -152,22 +156,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
"""
|
||||
|
||||
symbol_path = self.getSymbol()
|
||||
|
||||
pixmap = QtGui.QPixmap(symbol_path)
|
||||
if not pixmap.isNull():
|
||||
for item in self._items:
|
||||
if isinstance(item, PixmapNodeItem):
|
||||
item.setPixmap(pixmap)
|
||||
item.setPixmapSymbolPath(symbol_path)
|
||||
else:
|
||||
renderer = QtSvg.QSvgRenderer(symbol_path)
|
||||
renderer.setObjectName(symbol_path)
|
||||
if renderer.isValid():
|
||||
item.setSharedRenderer(renderer)
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Custom pixmap symbol", "Custom pixmap symbol which is not SVG format cannot be applied on SVG node item")
|
||||
return False
|
||||
|
||||
for item in self._items:
|
||||
item.setSymbol(symbol_path)
|
||||
return True
|
||||
|
||||
def getSymbol(self):
|
||||
@@ -175,27 +165,27 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
if self.uiSymbolListWidget.isEnabled():
|
||||
current = self.uiSymbolListWidget.currentItem()
|
||||
if current:
|
||||
name = current.text()
|
||||
if QtCore.QResource(":/symbols/{}.svg".format(name)).isValid():
|
||||
return ":/symbols/{}.svg".format(name)
|
||||
else:
|
||||
symbol_path = os.path.join(self._symbols_path, "{}.svg".format(name))
|
||||
if not os.path.exists(symbol_path):
|
||||
symbol_path = os.path.join(self._symbols_path, "{}.png".format(name))
|
||||
return symbol_path
|
||||
return current.data(QtCore.Qt.UserRole).id()
|
||||
else:
|
||||
return self.uiSymbolLineEdit.text()
|
||||
return os.path.basename(self.uiSymbolLineEdit.text())
|
||||
return None
|
||||
|
||||
def _symbolBrowserSlot(self):
|
||||
|
||||
# supported image file formats
|
||||
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.pbm *.pgm *.png *.ppm *.xbm *.xpm);;All files (*.*)"
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Image", self._symbols_dir, file_formats)
|
||||
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.pbm *.pgm *.png *.ppm *.xbm *.xpm *.gif);;All files (*.*)"
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Image", SymbolSelectionDialog._symbols_dir, file_formats)
|
||||
if not path:
|
||||
return
|
||||
SymbolSelectionDialog._symbols_dir = os.path.dirname(path)
|
||||
|
||||
self._symbols_dir = os.path.dirname(path)
|
||||
symbol_id = os.path.basename(path)
|
||||
Controller.instance().post("/symbols/" + symbol_id + "/raw", qpartial(self._finishSymbolUpload, path), body=pathlib.Path(path), progressText="Uploading {}".format(symbol_id), timeout=None)
|
||||
|
||||
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while uploading symbol: {}".format(path))
|
||||
return
|
||||
self.uiSymbolLineEdit.clear()
|
||||
self.uiSymbolLineEdit.setText(path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(path))
|
||||
@@ -207,10 +197,9 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
|
||||
if result:
|
||||
if not self.uiSymbolListWidget.isEnabled() and not os.path.exists(self.uiSymbolLineEdit.text()):
|
||||
QtWidgets.QMessageBox.critical(self, "Custom symbol", "Invalid path to custom symbol: {}".format(self.uiSymbolLineEdit.text()))
|
||||
result = 0
|
||||
elif result and self._items and not self._applyPreferencesSlot():
|
||||
result = 0
|
||||
if result and self._items and not self._applyPreferencesSlot():
|
||||
result = 0
|
||||
super().done(result)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
from .vm_wizard import VMWizard
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.servers import Servers
|
||||
from gns3.controller import Controller
|
||||
|
||||
|
||||
class VMWithImagesWizard(VMWizard):
|
||||
@@ -31,7 +31,7 @@ class VMWithImagesWizard(VMWizard):
|
||||
"""
|
||||
|
||||
def __init__(self, devices, use_local_server, parent):
|
||||
# The list of images combo box (Qemu support multiple images)
|
||||
# The list of images combo box (Qemu support multiple images)
|
||||
self._images_combo_boxes = set()
|
||||
|
||||
# The list of radio button for existing image or new images
|
||||
@@ -89,9 +89,7 @@ class VMWithImagesWizard(VMWizard):
|
||||
self._radio_existing_images_buttons.add(radio_button)
|
||||
|
||||
def _imageCreateSlot(self, line_edit, create_image_wizard, image_suffix):
|
||||
server = Servers.instance().getServerFromString(self.getSettings()["server"])
|
||||
|
||||
create_dialog = create_image_wizard(self, server, self.uiNameLineEdit.text() + image_suffix)
|
||||
create_dialog = create_image_wizard(self, self.getSettings()["server"], self.uiNameLineEdit.text() + image_suffix)
|
||||
if QtWidgets.QDialog.Accepted == create_dialog.exec_():
|
||||
line_edit.setText(create_dialog.uiLocationLineEdit.text())
|
||||
|
||||
@@ -100,8 +98,7 @@ class VMWithImagesWizard(VMWizard):
|
||||
Slot to open a file browser and select an image.
|
||||
"""
|
||||
|
||||
server = Servers.instance().getServerFromString(self.getSettings()["server"])
|
||||
path = image_selector(self, server)
|
||||
path = image_selector(self, self._compute_id)
|
||||
if not path:
|
||||
return
|
||||
line_edit.clear()
|
||||
@@ -146,7 +143,7 @@ class VMWithImagesWizard(VMWizard):
|
||||
:param endpoint: server endpoint with the list of Images
|
||||
"""
|
||||
|
||||
self._server.get(endpoint, self._getImagesFromServerCallback)
|
||||
Controller.instance().getCompute(endpoint, self._compute_id, self._getImagesFromServerCallback)
|
||||
|
||||
def _getImagesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -160,6 +157,10 @@ class VMWithImagesWizard(VMWizard):
|
||||
QtWidgets.QMessageBox.critical(self, "Images", "Error while getting the VMs: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
# Wizard is closed
|
||||
if self.currentPage() is None:
|
||||
return
|
||||
|
||||
if len(result) == 0:
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
if radio_button.isChecked() and self._widgetOnCurrentPage(radio_button):
|
||||
@@ -167,6 +168,13 @@ class VMWithImagesWizard(VMWizard):
|
||||
if button != radio_button:
|
||||
button.setChecked(True)
|
||||
button.hide()
|
||||
else:
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
if self._widgetOnCurrentPage(radio_button):
|
||||
for button in radio_button.parent().findChildren(QtWidgets.QRadioButton):
|
||||
if button == radio_button:
|
||||
button.setChecked(True)
|
||||
button.show()
|
||||
|
||||
for combo_box in self._images_combo_boxes:
|
||||
if self._widgetOnCurrentPage(combo_box):
|
||||
@@ -174,10 +182,8 @@ class VMWithImagesWizard(VMWizard):
|
||||
for vm in result:
|
||||
combo_box.addItem(vm["path"], vm)
|
||||
|
||||
|
||||
def _widgetOnCurrentPage(self, widget):
|
||||
"""
|
||||
:returns Boolean True if widget is current active Wizard page
|
||||
"""
|
||||
return self.currentPage().findChild(widget.__class__, widget.objectName()) is not None
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
import sys
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.servers import Servers
|
||||
from gns3.gns3_vm import GNS3VM
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.controller import Controller
|
||||
|
||||
|
||||
class VMWizard(QtWidgets.QWizard):
|
||||
@@ -50,16 +50,16 @@ class VMWizard(QtWidgets.QWizard):
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
if hasattr(self, "uiLoadBalanceCheckBox"):
|
||||
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
|
||||
if Controller.instance().isRemote():
|
||||
self.uiLocalRadioButton.setText("Run device on the main server")
|
||||
|
||||
# By default we use the local server
|
||||
self._server = Servers.instance().localServer()
|
||||
self._compute_id = ComputeManager.instance().computes()[0].id()
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
self._localToggledSlot(True)
|
||||
|
||||
if Servers.instance().isNonLocalServerConfigured() is False:
|
||||
# skip the server page if we use the local server
|
||||
if len(ComputeManager.instance().computes()) == 1:
|
||||
# skip the server page if we use the first server
|
||||
self.setStartId(1)
|
||||
|
||||
def _vmToggledSlot(self, checked):
|
||||
@@ -81,6 +81,7 @@ class VMWizard(QtWidgets.QWizard):
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(True)
|
||||
self.uiRemoteServersComboBox.setEnabled(True)
|
||||
self.uiRemoteServersGroupBox.show()
|
||||
|
||||
def _localToggledSlot(self, checked):
|
||||
@@ -93,66 +94,66 @@ class VMWizard(QtWidgets.QWizard):
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
def setStartId(self, index):
|
||||
"""
|
||||
Which page should we use when starting the Wizard
|
||||
"""
|
||||
super().setStartId(index)
|
||||
# If we skip the initial page (choosing a server)
|
||||
# we check the settings
|
||||
if index != 0:
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
|
||||
def initializePage(self, page_id):
|
||||
|
||||
if self.page(page_id) == self.uiServerWizardPage:
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
for server in Servers.instance().remoteServers().values():
|
||||
self.uiRemoteServersComboBox.addItem(server.url(), server)
|
||||
if hasattr(self, "uiVMRadioButton") and not GNS3VM.instance().isRunning():
|
||||
|
||||
self.uiRemoteRadioButton.setEnabled(False)
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.setEnabled(False)
|
||||
if hasattr(self, "uiVMRadioButton") and GNS3VM.instance().isRunning():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
elif self._use_local_server and self.uiLocalRadioButton.isChecked():
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
for compute in ComputeManager.instance().computes():
|
||||
if compute.id() == "local":
|
||||
self.uiLocalRadioButton.setEnabled(True)
|
||||
elif compute.id() == "vm" and hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.setEnabled(True)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setEnabled(True)
|
||||
self.uiRemoteServersComboBox.addItem(compute.name(), compute.id())
|
||||
|
||||
if self._use_local_server and self.uiLocalRadioButton.isEnabled() and self.uiLocalRadioButton.isVisible():
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isEnabled():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
if self.uiRemoteRadioButton.isEnabled():
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
|
||||
def _disableLocalServer(self):
|
||||
"""
|
||||
Turn off the local server
|
||||
"""
|
||||
self.uiLocalRadioButton.hide()
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
self.setStartId(0)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the server.
|
||||
"""
|
||||
|
||||
if hasattr(self, "uiNamePlatformWizardPage") and self.currentPage() == self.uiNamePlatformWizardPage:
|
||||
if hasattr(self, "uiNameWizardPage") and self.currentPage() == self.uiNameWizardPage:
|
||||
name = self.uiNameLineEdit.text()
|
||||
for device in self._devices.values():
|
||||
if device["name"] == name:
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "{} is already used, please choose another name".format(name))
|
||||
return False
|
||||
elif self.currentPage() == self.uiServerWizardPage:
|
||||
# If the local button is not visible it's because it's not supported
|
||||
if self.uiLocalRadioButton.isChecked() and self.uiLocalRadioButton.isHidden():
|
||||
QtWidgets.QMessageBox.critical(self, "New device", "Please configure before the GNS3 VM in order to use this device.")
|
||||
return False
|
||||
|
||||
if self.uiRemoteRadioButton.isChecked():
|
||||
if not Servers.instance().remoteServers():
|
||||
if self.uiRemoteServersComboBox.count() == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote server registered in your preferences")
|
||||
return False
|
||||
self._server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
|
||||
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
|
||||
gns3_vm_server = Servers.instance().vmServer()
|
||||
if gns3_vm_server is None:
|
||||
QtWidgets.QMessageBox.critical(self, "GNS3 VM", "The GNS3 VM is not running")
|
||||
return False
|
||||
self._server = gns3_vm_server
|
||||
self._compute_id = "vm"
|
||||
else:
|
||||
self._server = Servers.instance().localServer()
|
||||
if self.uiLocalRadioButton.isEnabled():
|
||||
self._compute_id = "local"
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Server", "No available server support this type of node. You probably need to setup the GNS3 VM")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _loadBalanceToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the load balance checkbox is toggled.
|
||||
|
||||
:param checked: either the box is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersComboBox.setEnabled(False)
|
||||
else:
|
||||
self.uiRemoteServersComboBox.setEnabled(True)
|
||||
|
||||
279
gns3/gns3_vm.py
279
gns3/gns3_vm.py
@@ -1,279 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Manages the GNS3 VM.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
import codecs
|
||||
import shutil
|
||||
|
||||
from .qt import QtNetwork
|
||||
from collections import OrderedDict
|
||||
from .servers import Servers
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GNS3VM:
|
||||
|
||||
"""
|
||||
GNS3 VM management class.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._is_running = False
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns the GNS3 VM settings.
|
||||
|
||||
:returns: GNS3 VM settings (dict)
|
||||
"""
|
||||
|
||||
return Servers.instance().vmSettings()
|
||||
|
||||
def setSettings(self, settings):
|
||||
"""
|
||||
Set new GNS3 VM settings.
|
||||
|
||||
:param settings: GNS3 VM settings (dict)
|
||||
"""
|
||||
|
||||
Servers.instance().setVMsettings(settings)
|
||||
|
||||
@staticmethod
|
||||
def execute_vmrun(subcommand, args, timeout=60):
|
||||
|
||||
from gns3.modules.vmware import VMware
|
||||
vmware_settings = VMware.instance().settings()
|
||||
vmrun_path = vmware_settings["vmrun_path"]
|
||||
if sys.platform.startswith("darwin"):
|
||||
command = [vmrun_path, "-T", "fusion", subcommand]
|
||||
else:
|
||||
host_type = vmware_settings["host_type"]
|
||||
command = [vmrun_path, "-T", host_type, subcommand]
|
||||
command.extend(args)
|
||||
log.debug("Executing vmrun with command: {}".format(command))
|
||||
output = subprocess.check_output(command, timeout=timeout)
|
||||
return output.decode("utf-8", errors="ignore").strip()
|
||||
|
||||
@staticmethod
|
||||
def execute_vboxmanage(subcommand, args, timeout=60):
|
||||
|
||||
from gns3.modules.virtualbox import VirtualBox
|
||||
virtualbox_settings = VirtualBox.instance().settings()
|
||||
vboxmanage_path = virtualbox_settings["vboxmanage_path"]
|
||||
command = [vboxmanage_path, "--nologo", subcommand]
|
||||
command.extend(args)
|
||||
log.debug("Executing VBoxManage with command: {}".format(command))
|
||||
output = subprocess.check_output(command, timeout=timeout)
|
||||
return output.decode("utf-8", errors="ignore").strip()
|
||||
|
||||
@staticmethod
|
||||
def parse_vmx_file(path):
|
||||
"""
|
||||
Parses a VMX file.
|
||||
|
||||
:param path: path to the VMX file
|
||||
|
||||
:returns: dict
|
||||
"""
|
||||
|
||||
pairs = OrderedDict()
|
||||
encoding = "utf-8"
|
||||
# get the first line to read the .encoding value
|
||||
with open(path, "rb") as f:
|
||||
line = f.readline().decode(encoding, errors="ignore")
|
||||
if line.startswith("#!"):
|
||||
# skip the shebang
|
||||
line = f.readline().decode(encoding, errors="ignore")
|
||||
try:
|
||||
key, value = line.split('=', 1)
|
||||
if key.strip().lower() == ".encoding":
|
||||
file_encoding = value.strip('" ')
|
||||
try:
|
||||
codecs.lookup(file_encoding)
|
||||
encoding = file_encoding
|
||||
except LookupError:
|
||||
log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding))
|
||||
except ValueError:
|
||||
log.warning("Couldn't find file encoding in {}, using {}...".format(path, encoding))
|
||||
|
||||
# read the file with the correct encoding
|
||||
with open(path, encoding=encoding, errors="ignore") as f:
|
||||
for line in f.read().splitlines():
|
||||
try:
|
||||
key, value = line.split('=', 1)
|
||||
pairs[key.strip().lower()] = value.strip('" ')
|
||||
except ValueError:
|
||||
continue
|
||||
return pairs
|
||||
|
||||
@staticmethod
|
||||
def write_vmx_file(path, pairs):
|
||||
"""
|
||||
Write a VMware VMX file.
|
||||
|
||||
:param path: path to the VMX file
|
||||
:param pairs: settings to write
|
||||
"""
|
||||
|
||||
encoding = "utf-8"
|
||||
if ".encoding" in pairs:
|
||||
file_encoding = pairs[".encoding"]
|
||||
try:
|
||||
codecs.lookup(file_encoding)
|
||||
encoding = file_encoding
|
||||
except LookupError:
|
||||
log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding))
|
||||
with open(path, "w", encoding=encoding, errors="ignore") as f:
|
||||
if sys.platform.startswith("linux"):
|
||||
# write the shebang on the first line on Linux
|
||||
vmware_path = shutil.which("vmware")
|
||||
if vmware_path:
|
||||
f.write("#!{}\n".format(vmware_path))
|
||||
for key, value in pairs.items():
|
||||
entry = '{} = "{}"\n'.format(key, value)
|
||||
f.write(entry)
|
||||
|
||||
def autoStart(self):
|
||||
"""
|
||||
Automatically start the GNS3 VM at startup.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
vm_settings = Servers.instance().vmSettings()
|
||||
return vm_settings["auto_start"]
|
||||
|
||||
def adjustLocalServerIP(self):
|
||||
"""
|
||||
Adjust the local server IP address to be in the same subnet as the GNS3 VM.
|
||||
|
||||
:returns: the local server IP/host address
|
||||
"""
|
||||
|
||||
servers = Servers.instance()
|
||||
local_server_settings = servers.localServerSettings()
|
||||
if Servers.instance().vmSettings()["adjust_local_server_ip"]:
|
||||
vm_server = servers.vmServer()
|
||||
vm_ip_address = vm_server.host()
|
||||
log.debug("GNS3 VM IP address is {}".format(vm_ip_address))
|
||||
|
||||
for interface in QtNetwork.QNetworkInterface.allInterfaces():
|
||||
for address in interface.addressEntries():
|
||||
ip = address.ip().toString()
|
||||
prefix_length = address.prefixLength()
|
||||
subnet = QtNetwork.QHostAddress.parseSubnet("{}/{}".format(ip, prefix_length))
|
||||
if QtNetwork.QHostAddress(vm_ip_address).isInSubnet(subnet):
|
||||
if local_server_settings["host"] != ip:
|
||||
log.info("Adjust local server IP address to {}".format(ip))
|
||||
servers.setLocalServerSettings({"host": ip})
|
||||
servers.registerLocalServer()
|
||||
servers.save()
|
||||
return ip
|
||||
return local_server_settings["host"]
|
||||
|
||||
def setRunning(self, value):
|
||||
"""
|
||||
Sets either the GNS3 VM is running or not.
|
||||
|
||||
:param value: boolean
|
||||
"""
|
||||
|
||||
self._is_running = value
|
||||
|
||||
def isRunning(self):
|
||||
"""
|
||||
Returns either the GNS3 VM is running or not.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._is_running
|
||||
|
||||
def setvCPUandRAM(self, vcpus, ram):
|
||||
"""
|
||||
Set the vCPU cores and RAM amount for the GNS3 VM.
|
||||
|
||||
:param vcpus: number of vCPU cores
|
||||
:param ram: amount of memory
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
vm_settings = self.settings()
|
||||
if vm_settings["virtualization"] == "VMware":
|
||||
try:
|
||||
pairs = self.parse_vmx_file(vm_settings["vmx_path"])
|
||||
pairs["numvcpus"] = str(vcpus)
|
||||
pairs["memsize"] = str(ram)
|
||||
self.write_vmx_file(vm_settings["vmx_path"], pairs)
|
||||
except OSError as e:
|
||||
log.error('Could not read/write VMware VMX file "{}": {}'.format(vm_settings["vmx_path"], e))
|
||||
return False
|
||||
|
||||
elif vm_settings["virtualization"] == "VirtualBox":
|
||||
try:
|
||||
self.execute_vboxmanage("modifyvm", [vm_settings["vmname"], "--cpus", str(vcpus)], timeout=3)
|
||||
self.execute_vboxmanage("modifyvm", [vm_settings["vmname"], "--memory", str(ram)], timeout=3)
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.error("Could not execute VBoxManage: {}".format(e), True)
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
log.error("VBoxmanage timeout expired", True)
|
||||
return False
|
||||
log.info("GNS3 VM vCPU count set to {} and RAM to {} MB".format(vcpus, ram))
|
||||
return True
|
||||
|
||||
def shutdown(self, force=False):
|
||||
"""
|
||||
Gracefully shutdowns the GNS3 VM.
|
||||
"""
|
||||
|
||||
vm_settings = self.settings()
|
||||
if self._is_running and (vm_settings["auto_stop"] or force):
|
||||
try:
|
||||
if vm_settings["virtualization"] == "VMware":
|
||||
if vm_settings["vmx_path"] is None:
|
||||
log.error("No vm path configured, can't stop the VM")
|
||||
return
|
||||
self.execute_vmrun("stop", [vm_settings["vmx_path"], "soft"])
|
||||
elif vm_settings["virtualization"] == "VirtualBox":
|
||||
self.execute_vboxmanage("controlvm", [vm_settings["vmname"], "acpipowerbutton"], timeout=3)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
except subprocess.TimeoutExpired:
|
||||
log.warning("Could not ACPI shutdown the VM (timeout expired)")
|
||||
self._is_running = False
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of GNS3VM
|
||||
|
||||
:returns: instance of GNS3VM
|
||||
"""
|
||||
|
||||
if not hasattr(GNS3VM, "_instance") or GNS3VM._instance is None:
|
||||
GNS3VM._instance = GNS3VM()
|
||||
return GNS3VM._instance
|
||||
@@ -21,13 +21,11 @@ Graphical view on the scene where items are drawn.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sip
|
||||
import pickle
|
||||
|
||||
from .qt import QtCore, QtGui, QtSvg, QtNetwork, QtWidgets, qpartial
|
||||
from .servers import Servers
|
||||
from .items.node_item import NodeItem
|
||||
from .items.svg_node_item import SvgNodeItem
|
||||
from .items.pixmap_node_item import PixmapNodeItem
|
||||
from .dialogs.node_properties_dialog import NodePropertiesDialog
|
||||
from .link import Link
|
||||
from .node import Node
|
||||
@@ -41,9 +39,13 @@ from .dialogs.style_editor_dialog import StyleEditorDialog
|
||||
from .dialogs.text_editor_dialog import TextEditorDialog
|
||||
from .dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from .dialogs.idlepc_dialog import IdlePCDialog
|
||||
from .dialogs.console_command_dialog import ConsoleCommandDialog
|
||||
from .dialogs.file_editor_dialog import FileEditorDialog
|
||||
from .local_config import LocalConfig
|
||||
from .progress import Progress
|
||||
from .utils.server_select import server_select
|
||||
from .utils.normalize_filename import normalize_filename
|
||||
from .compute_manager import ComputeManager
|
||||
|
||||
# link items
|
||||
from .items.link_item import LinkItem
|
||||
@@ -52,12 +54,12 @@ from .items.serial_link_item import SerialLinkItem
|
||||
|
||||
# other items
|
||||
from .items.note_item import NoteItem
|
||||
from .items.text_item import TextItem
|
||||
from .items.shape_item import ShapeItem
|
||||
from .items.drawing_item import DrawingItem
|
||||
from .items.rectangle_item import RectangleItem
|
||||
from .items.ellipse_item import EllipseItem
|
||||
from .items.image_item import ImageItem
|
||||
from .items.pixmap_image_item import PixmapImageItem
|
||||
from .items.svg_image_item import SvgImageItem
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -94,12 +96,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
scene = QtWidgets.QGraphicsScene(parent=self)
|
||||
width = self._settings["scene_width"]
|
||||
height = self._settings["scene_height"]
|
||||
scene.setSceneRect(-(width / 2), -(height / 2), width, height)
|
||||
self.setScene(scene)
|
||||
self.setSceneSize(width, height)
|
||||
|
||||
# set the custom flags for this view
|
||||
self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
|
||||
self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground)
|
||||
self.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
|
||||
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter)
|
||||
@@ -112,6 +113,18 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
self._local_addresses = ['0.0.0.0', '127.0.0.1', 'localhost', '::1', '0:0:0:0:0:0:0:1', '::', QtNetwork.QHostInfo.localHostName()]
|
||||
|
||||
def setSceneSize(self, width, height):
|
||||
self.scene().setSceneRect(-(width / 2), -(height / 2), width, height)
|
||||
|
||||
def setEnabled(self, enabled):
|
||||
|
||||
if enabled is False:
|
||||
self.reset()
|
||||
item = QtWidgets.QGraphicsTextItem("Please create a project")
|
||||
item.setPos(0, 0)
|
||||
self.scene().addItem(item)
|
||||
super().setEnabled(enabled)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Remove all the items from the scene and
|
||||
@@ -127,7 +140,6 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# nodes, links and ports
|
||||
Node.reset()
|
||||
Link.reset()
|
||||
Port.reset()
|
||||
|
||||
# reset the topology
|
||||
self._topology.reset()
|
||||
@@ -224,27 +236,19 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_ellipse = False
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addImage(self, image, image_path):
|
||||
def addImage(self, image_path):
|
||||
"""
|
||||
Adds an image.
|
||||
|
||||
:param image: QPixmap or QSvgRenderer instance
|
||||
:param image_path: path to the image
|
||||
"""
|
||||
|
||||
if isinstance(image, QtSvg.QSvgRenderer):
|
||||
# use a SVG image item if this is a valid SVG file
|
||||
image_item = SvgImageItem(image, image_path)
|
||||
else:
|
||||
image_item = PixmapImageItem(image, image_path)
|
||||
# center the image on the scene
|
||||
x = image_item.pos().x() - (image_item.boundingRect().width() / 2)
|
||||
y = image_item.pos().y() - (image_item.boundingRect().height() / 2)
|
||||
image_item.setPos(x, y)
|
||||
image_item = ImageItem(image_path=image_path, project=self._topology.project())
|
||||
image_item.create()
|
||||
self.scene().addItem(image_item)
|
||||
self._topology.addImage(image_item)
|
||||
self._topology.addDrawing(image_item)
|
||||
|
||||
def addLink(self, source_node, source_port, destination_node, destination_port):
|
||||
def addLink(self, source_node, source_port, destination_node, destination_port, **link_data):
|
||||
"""
|
||||
Creates a Link instance representing a connection between 2 devices.
|
||||
|
||||
@@ -252,16 +256,19 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
:param source_port: source Port instance
|
||||
:param destination_node: destination Node instance
|
||||
:param destination_port: destination Port instance
|
||||
:param link_data: information about link from the API
|
||||
:returns: Link
|
||||
"""
|
||||
|
||||
link = Link(source_node, source_port, destination_node, destination_port)
|
||||
|
||||
link = Link(source_node, source_port, destination_node, destination_port, **link_data)
|
||||
# connect the signals that let the graphics view knows about events such as
|
||||
# a new link creation or deletion.
|
||||
if self._topology.addLink(link):
|
||||
link.add_link_signal.connect(self.addLinkSlot)
|
||||
link.delete_link_signal.connect(self.deleteLinkSlot)
|
||||
|
||||
if link.initialized():
|
||||
self.addLinkSlot(link.id())
|
||||
return link
|
||||
|
||||
def addLinkSlot(self, link_id):
|
||||
@@ -289,7 +296,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
break
|
||||
|
||||
if not source_item or not destination_item:
|
||||
print("Could not find a source or destination item for the link!")
|
||||
log.error("Could not find a source or destination item for the link!")
|
||||
self.deleteLinkSlot(link_id)
|
||||
return
|
||||
|
||||
@@ -324,7 +331,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
multi = -multi // 2
|
||||
|
||||
if link.sourcePort().linkType() == "Serial" or (source_port.isStub() and link.destinationPort().linkType() == "Serial"):
|
||||
if link.sourcePort().linkType() == "Serial":
|
||||
link_item = SerialLinkItem(source_item, source_port, destination_item, destination_port, link, multilink=multi)
|
||||
else:
|
||||
link_item = EthernetLinkItem(source_item, source_port, destination_item, destination_port, link, multilink=multi)
|
||||
@@ -339,9 +346,6 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
link = self._topology.getLink(link_id)
|
||||
# disconnect the signals just in case...
|
||||
link.add_link_signal.disconnect()
|
||||
link.delete_link_signal.disconnect()
|
||||
self._topology.removeLink(link)
|
||||
|
||||
def _userNodeLinking(self, event, item):
|
||||
@@ -358,15 +362,6 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
source_port = source_item.connectToPort()
|
||||
if not source_port:
|
||||
return
|
||||
if not source_item.node().initialized():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "This node hasn't been initialized correctly")
|
||||
return
|
||||
if not source_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Port {} isn't free".format(source_port.name()))
|
||||
return
|
||||
if not source_port.isHotPluggable() and source_item.node().status() == Node.started:
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "A new link cannot be added because {} is running".format(source_item.node().name()))
|
||||
return
|
||||
if source_port.linkType() == "Serial":
|
||||
self._newlink = SerialLinkItem(source_item, source_port, self.mapToScene(event.pos()), None, adding_flag=True)
|
||||
else:
|
||||
@@ -376,52 +371,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
source_item = self._newlink.sourceItem()
|
||||
source_port = self._newlink.sourcePort()
|
||||
destination_item = item
|
||||
if source_item == destination_item:
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Cannot connect to itself!")
|
||||
return
|
||||
destination_port = destination_item.connectToPort()
|
||||
if not destination_port:
|
||||
return
|
||||
if not destination_item.node().initialized():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "This node hasn't been initialized correctly")
|
||||
return
|
||||
if not destination_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Port {} isn't free".format(destination_port.name()))
|
||||
return
|
||||
if not destination_port.isHotPluggable() and destination_item.node().status() == Node.started:
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "A new link cannot be added because {} is running".format(destination_item.node().name()))
|
||||
return
|
||||
if source_port.isStub() or destination_port.isStub():
|
||||
pass
|
||||
elif source_port.linkType() != destination_port.linkType():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Cannot connect this port!")
|
||||
return
|
||||
elif source_port.defaultNio() != destination_port.defaultNio():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "These nodes cannot be connected together ({} != {})".format(source_port.defaultNio().__name__,
|
||||
destination_port.defaultNio().__name__))
|
||||
return
|
||||
|
||||
if source_item.node().server().protocol() != destination_item.node().server().protocol():
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Sorry, you cannot connect a device running on an insecure server to a device running on a secure server.")
|
||||
return
|
||||
|
||||
if isinstance(source_item.node(), Cloud) and isinstance(destination_item.node(), Cloud):
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Sorry, you cannot connect a cloud to another cloud!")
|
||||
return
|
||||
|
||||
source_host = source_item.node().server().host()
|
||||
destination_host = destination_item.node().server().host()
|
||||
|
||||
# check that the node can be connected to a cloud
|
||||
if (isinstance(source_item.node(), Cloud) or isinstance(destination_item.node(), Cloud)) and source_host != destination_host:
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "This device can only be connected to a cloud on the same host")
|
||||
return
|
||||
|
||||
# check if the 2 nodes can communicate
|
||||
if (source_host in self._local_addresses and destination_host not in self._local_addresses) or \
|
||||
(destination_host in self._local_addresses and source_host not in self._local_addresses):
|
||||
QtWidgets.QMessageBox.critical(self, "Connection", "Server {} cannot communicate with server {}, most likely because your local server host binding is set to a local address".format(source_host, destination_host))
|
||||
return
|
||||
|
||||
if self._newlink in self.scene().items():
|
||||
self.scene().removeItem(self._newlink)
|
||||
@@ -437,7 +389,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
is_not_link = True
|
||||
item = self.itemAt(event.pos())
|
||||
if item and isinstance(item, LinkItem):
|
||||
if item and sip.isdeleted(item):
|
||||
return
|
||||
|
||||
if item and (isinstance(item, LinkItem) or isinstance(item.parentItem(), LinkItem)):
|
||||
is_not_link = False
|
||||
else:
|
||||
for it in self.scene().items():
|
||||
@@ -484,28 +439,24 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
elif item and isinstance(item, NodeItem) and self._adding_link and event.button() == QtCore.Qt.LeftButton:
|
||||
self._userNodeLinking(event, item)
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_note:
|
||||
note = NoteItem()
|
||||
note.setPos(self.mapToScene(event.pos()))
|
||||
pos = self.mapToScene(event.pos())
|
||||
note = self.createDrawingItem("text", pos.x(), pos.y(), 0)
|
||||
pos_x = note.pos().x()
|
||||
pos_y = note.pos().y() - (note.boundingRect().height() / 2)
|
||||
note.setPos(pos_x, pos_y)
|
||||
self.scene().addItem(note)
|
||||
self._topology.addNote(note)
|
||||
note.editText()
|
||||
self._main_window.uiAddNoteAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_note = False
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_rectangle:
|
||||
rectangle = RectangleItem(self.mapToScene(event.pos()))
|
||||
self.scene().addItem(rectangle)
|
||||
self._topology.addRectangle(rectangle)
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("rect", pos.x(), pos.y(), 0)
|
||||
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:
|
||||
ellipse = EllipseItem(self.mapToScene(event.pos()))
|
||||
self.scene().addItem(ellipse)
|
||||
self._topology.addEllipse(ellipse)
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("ellipse", pos.x(), pos.y(), 0)
|
||||
self._main_window.uiDrawEllipseAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_ellipse = False
|
||||
@@ -519,13 +470,17 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
:param: QMouseEvent instance
|
||||
"""
|
||||
|
||||
item = self.itemAt(event.pos())
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem):
|
||||
item.mouseRelease()
|
||||
|
||||
# If the left mouse button is not still pressed TOGETHER with the SHIFT key and neither is the middle button
|
||||
# this means the user is no longer trying to drag the view
|
||||
if self._dragging and not (event.buttons() == QtCore.Qt.LeftButton and event.modifiers() == QtCore.Qt.ShiftModifier) and not event.buttons() & QtCore.Qt.MidButton:
|
||||
self._dragging = False
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
else:
|
||||
item = self.itemAt(event.pos())
|
||||
if item is not None and not event.modifiers() & QtCore.Qt.ControlModifier:
|
||||
item.setSelected(True)
|
||||
super().mouseReleaseEvent(event)
|
||||
@@ -538,8 +493,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
if event.modifiers() == QtCore.Qt.ControlModifier:
|
||||
# event.delta() added for Qt4 compatibility
|
||||
delta = event.angleDelta() if hasattr(event, 'angleDelta') else event.delta()
|
||||
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))
|
||||
@@ -566,7 +520,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if event.key() == QtCore.Qt.Key_Delete:
|
||||
# check if we are editing an NoteItem instance, then send the delete key event to it
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NoteItem) and item.hasFocus():
|
||||
if (isinstance(item, NoteItem) or isinstance(item, TextItem)) and item.hasFocus():
|
||||
super().keyPressEvent(event)
|
||||
return
|
||||
self.deleteActionSlot()
|
||||
@@ -640,7 +594,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if not items:
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
if isinstance(item, NodeItem) and item.node().initialized() and hasattr(item.node(), "configPage"):
|
||||
items.append(item)
|
||||
with Progress.instance().context(min_duration=0):
|
||||
node_properties = NodePropertiesDialog(items, self._main_window)
|
||||
@@ -682,13 +636,12 @@ 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):
|
||||
node_item = self.createNode(node_data, event.pos())
|
||||
x = event.pos().x() - (150 / 2) + (node_number % max_nodes_per_line) * offset
|
||||
y = event.pos().y() - (70 / 2) + (node_number // max_nodes_per_line) * offset
|
||||
node_item = self.createNode(node_data, QtCore.QPoint(x, y))
|
||||
if node_item is None:
|
||||
# stop if there is any error
|
||||
break
|
||||
x = node_item.pos().x() - (node_item.boundingRect().width() / 2) + (node_number % max_nodes_per_line) * offset
|
||||
y = node_item.pos().y() - (node_item.boundingRect().height() / 2) + (node_number // max_nodes_per_line) * offset
|
||||
node_item.setPos(x, y)
|
||||
else:
|
||||
self.createNode(node_data, event.pos())
|
||||
elif event.mimeData().hasFormat("text/uri-list") and event.mimeData().hasUrls():
|
||||
@@ -728,25 +681,27 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if not items:
|
||||
return
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configPage"), items)):
|
||||
configure_action = QtWidgets.QAction("Configure", menu)
|
||||
configure_action.setIcon(QtGui.QIcon(':/icons/configuration.svg'))
|
||||
configure_action.triggered.connect(self.configureActionSlot)
|
||||
menu.addAction(configure_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
|
||||
# Action: Change hostname
|
||||
change_hostname_action = QtWidgets.QAction("Change hostname", menu)
|
||||
change_hostname_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
|
||||
change_hostname_action.triggered.connect(self.changeHostnameActionSlot)
|
||||
menu.addAction(change_hostname_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
|
||||
# Action: Change symbol
|
||||
change_symbol_action = QtWidgets.QAction("Change symbol", menu)
|
||||
change_symbol_action.setIcon(QtGui.QIcon(':/icons/node_conception.svg'))
|
||||
change_symbol_action.triggered.connect(self.changeSymbolActionSlot)
|
||||
menu.addAction(change_symbol_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "vmDir"), items)):
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "nodeDir"), items)):
|
||||
# Action: Show in file manager
|
||||
show_in_file_manager_action = QtWidgets.QAction("Show in file manager", menu)
|
||||
show_in_file_manager_action.setIcon(QtGui.QIcon(':/icons/open.svg'))
|
||||
@@ -759,35 +714,35 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
console_action.triggered.connect(self.consoleActionSlot)
|
||||
menu.addAction(console_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "console"), items)):
|
||||
console_edit_action = QtWidgets.QAction("Custom console", menu)
|
||||
console_edit_action.setIcon(QtGui.QIcon(':/icons/console_edit.svg'))
|
||||
console_edit_action.triggered.connect(self.customConsoleActionSlot)
|
||||
menu.addAction(console_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole"), items)):
|
||||
aux_console_action = QtWidgets.QAction("Auxiliary console", menu)
|
||||
aux_console_action.setIcon(QtGui.QIcon(':/icons/aux-console.svg'))
|
||||
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
|
||||
menu.addAction(aux_console_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "importConfig"), items)):
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
|
||||
import_config_action = QtWidgets.QAction("Import config", menu)
|
||||
import_config_action.setIcon(QtGui.QIcon(':/icons/import_config.svg'))
|
||||
import_config_action.triggered.connect(self.importConfigActionSlot)
|
||||
menu.addAction(import_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "exportConfig"), items)):
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
|
||||
export_config_action = QtWidgets.QAction("Export config", menu)
|
||||
export_config_action.setIcon(QtGui.QIcon(':/icons/export_config.svg'))
|
||||
export_config_action.triggered.connect(self.exportConfigActionSlot)
|
||||
menu.addAction(export_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "saveConfig"), items)):
|
||||
save_config_action = QtWidgets.QAction("Save config", menu)
|
||||
save_config_action.setIcon(QtGui.QIcon(':/icons/save.svg'))
|
||||
save_config_action.triggered.connect(self.saveConfigActionSlot)
|
||||
menu.addAction(save_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "startPacketCapture"), items)):
|
||||
capture_action = QtWidgets.QAction("Capture", menu)
|
||||
capture_action.setIcon(QtGui.QIcon(':/icons/inspect.svg'))
|
||||
capture_action.triggered.connect(self.captureActionSlot)
|
||||
menu.addAction(capture_action)
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
|
||||
export_config_action = QtWidgets.QAction("Edit config", menu)
|
||||
export_config_action.setIcon(QtGui.QIcon(':/icons/edit.svg'))
|
||||
export_config_action.triggered.connect(self.editConfigActionSlot)
|
||||
menu.addAction(export_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "idlepc"), items)):
|
||||
idlepc_action = QtWidgets.QAction("Idle-PC", menu)
|
||||
@@ -801,31 +756,31 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
auto_idlepc_action.triggered.connect(self.autoIdlepcActionSlot)
|
||||
menu.addAction(auto_idlepc_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "start"), items)):
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
start_action = QtWidgets.QAction("Start", menu)
|
||||
start_action.setIcon(QtGui.QIcon(':/icons/start.svg'))
|
||||
start_action.triggered.connect(self.startActionSlot)
|
||||
menu.addAction(start_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "suspend"), items)):
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
suspend_action = QtWidgets.QAction("Suspend", menu)
|
||||
suspend_action.setIcon(QtGui.QIcon(':/icons/pause.svg'))
|
||||
suspend_action.triggered.connect(self.suspendActionSlot)
|
||||
menu.addAction(suspend_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "stop"), items)):
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
stop_action = QtWidgets.QAction("Stop", menu)
|
||||
stop_action.setIcon(QtGui.QIcon(':/icons/stop.svg'))
|
||||
stop_action.triggered.connect(self.stopActionSlot)
|
||||
menu.addAction(stop_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "reload"), items)):
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
reload_action = QtWidgets.QAction("Reload", menu)
|
||||
reload_action.setIcon(QtGui.QIcon(':/icons/reload.svg'))
|
||||
reload_action.triggered.connect(self.reloadActionSlot)
|
||||
menu.addAction(reload_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NoteItem) or isinstance(item, ShapeItem) or isinstance(item, ImageItem), items)):
|
||||
if True in list(map(lambda item: isinstance(item, DrawingItem), items)):
|
||||
duplicate_action = QtWidgets.QAction("Duplicate", menu)
|
||||
duplicate_action.setIcon(QtGui.QIcon(':/icons/new.svg'))
|
||||
duplicate_action.triggered.connect(self.duplicateActionSlot)
|
||||
@@ -833,7 +788,13 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NoteItem), items)):
|
||||
text_edit_action = QtWidgets.QAction("Text edit", menu)
|
||||
text_edit_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg')) # TODO: change icon for text edit
|
||||
text_edit_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
|
||||
text_edit_action.triggered.connect(self.textEditActionSlot)
|
||||
menu.addAction(text_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, TextItem), items)):
|
||||
text_edit_action = QtWidgets.QAction("Text edit", menu)
|
||||
text_edit_action.setIcon(QtGui.QIcon(':/icons/edit.svg'))
|
||||
text_edit_action.triggered.connect(self.textEditActionSlot)
|
||||
menu.addAction(text_edit_action)
|
||||
|
||||
@@ -843,6 +804,20 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
style_action.triggered.connect(self.styleActionSlot)
|
||||
menu.addAction(style_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "commandLine"), items)):
|
||||
# Action: Get command line
|
||||
show_in_file_manager_action = QtWidgets.QAction("Command line", menu)
|
||||
show_in_file_manager_action.setIcon(QtGui.QIcon(':/icons/console.svg'))
|
||||
show_in_file_manager_action.triggered.connect(self.getCommandLineSlot)
|
||||
menu.addAction(show_in_file_manager_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NoteItem), items)) and False in list(map(lambda item: item.parentItem() is None, items)):
|
||||
# action only for port labels
|
||||
reset_label_position_action = QtWidgets.QAction("Reset position", menu)
|
||||
reset_label_position_action.setIcon(QtGui.QIcon(':/icons/reset.svg'))
|
||||
reset_label_position_action.triggered.connect(self.resetLabelPositionActionSlot)
|
||||
menu.addAction(reset_label_position_action)
|
||||
|
||||
# item must have no parent
|
||||
if True in list(map(lambda item: item.parentItem() is None, items)):
|
||||
|
||||
@@ -964,20 +939,20 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "vmDir") and item.node().initialized():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "nodeDir") and item.node().initialized():
|
||||
node = item.node()
|
||||
vm_dir = node.vmDir()
|
||||
if vm_dir is None:
|
||||
QtWidgets.QMessageBox.critical(self, "Show in file manager", "This VM has no working directory")
|
||||
node_dir = node.nodeDir()
|
||||
if node_dir is None:
|
||||
QtWidgets.QMessageBox.critical(self, "Show in file manager", "This node has no working directory")
|
||||
break
|
||||
|
||||
if os.path.exists(vm_dir):
|
||||
if os.path.exists(node_dir):
|
||||
log.debug("Open %s in file manage")
|
||||
if QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(vm_dir)) is False:
|
||||
QtWidgets.QMessageBox.critical(self, "Show in file manager", "Failed to open {}".format(vm_dir))
|
||||
if QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(node_dir)) is False:
|
||||
QtWidgets.QMessageBox.critical(self, "Show in file manager", "Failed to open {}".format(node_dir))
|
||||
break
|
||||
else:
|
||||
QtWidgets.QMessageBox.information(self, "Show in file manager", "The device directory is located in {} on {}".format(vm_dir, node.server().url()))
|
||||
QtWidgets.QMessageBox.information(self, "Show in file manager", "The device directory is located in {} on {}".format(node_dir, node.compute().name()))
|
||||
break
|
||||
|
||||
def consoleToNode(self, node, aux=False):
|
||||
@@ -998,38 +973,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# returns True to ignore this node.
|
||||
return True
|
||||
|
||||
if hasattr(node, "serialConsole") and node.serialConsole():
|
||||
try:
|
||||
from .serial_console import serialConsole
|
||||
serialConsole(node.name(), node.serialPipe())
|
||||
except (OSError, ValueError) as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Console", "Cannot start serial console application: {}".format(e))
|
||||
return False
|
||||
else:
|
||||
name = node.name()
|
||||
if aux:
|
||||
console_port = node.auxConsole()
|
||||
if console_port is None:
|
||||
QtWidgets.QMessageBox.critical(self, "Console", "AUX console port not allocated for {}".format(name))
|
||||
return False
|
||||
else:
|
||||
console_port = node.console()
|
||||
|
||||
console_type = "telnet"
|
||||
if "console_type" in node.settings():
|
||||
console_type = node.settings()["console_type"]
|
||||
|
||||
try:
|
||||
from .telnet_console import nodeTelnetConsole
|
||||
from .vnc_console import vncConsole
|
||||
|
||||
if console_type == "telnet":
|
||||
nodeTelnetConsole(name, node.server(), console_port)
|
||||
elif console_type == "vnc":
|
||||
vncConsole(node.server().host(), console_port)
|
||||
except (OSError, ValueError) as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
|
||||
return False
|
||||
try:
|
||||
node.openConsole(aux=aux)
|
||||
except (OSError, ValueError) as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def consoleFromItems(self, items):
|
||||
@@ -1045,6 +993,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
node = item.node()
|
||||
nodes[node.name()] = node
|
||||
|
||||
if not nodes:
|
||||
if len(items) > 1:
|
||||
QtWidgets.QMessageBox.warning(self, "Console", "At least one node must be started before a console can be opened")
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "Console", "This node must be started before a console can be opened")
|
||||
|
||||
delay = self._main_window.settings()["delay_console_all"]
|
||||
counter = 0
|
||||
for name in sorted(nodes.keys()):
|
||||
@@ -1061,6 +1015,32 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
self.consoleFromItems(self.scene().selectedItems())
|
||||
|
||||
def customConsoleActionSlot(self):
|
||||
"""
|
||||
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 hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
|
||||
if item.node().consoleType() not in ("telnet", "serial", "vnc"):
|
||||
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 hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
|
||||
node = item.node()
|
||||
if node.consoleType() not in ("telnet", "serial", "vnc"):
|
||||
continue
|
||||
try:
|
||||
node.openConsole(command=cmd)
|
||||
except (OSError, ValueError) as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
|
||||
|
||||
def auxConsoleFromItems(self, items):
|
||||
"""
|
||||
Aux console from scene items.
|
||||
@@ -1074,6 +1054,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
node = item.node()
|
||||
nodes[node.name()] = node
|
||||
|
||||
if not nodes:
|
||||
if len(items) > 1:
|
||||
QtWidgets.QMessageBox.warning(self, "Console", "At least one node must be started before a console can be opened")
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "Console", "This node must be started before a console can be opened")
|
||||
|
||||
delay = self._main_window.settings()["delay_console_all"]
|
||||
counter = 0
|
||||
for name in sorted(nodes.keys()):
|
||||
@@ -1098,40 +1084,54 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "importConfig") and item.node().initialized():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "configFiles") and item.node().initialized():
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
return
|
||||
|
||||
if len(items) > 1:
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self, "Import directory", self._import_configs_from_dir, QtWidgets.QFileDialog.ShowDirsOnly)
|
||||
if path:
|
||||
self._import_configs_from_dir = os.path.dirname(path)
|
||||
for item in items:
|
||||
item.node().importConfigFromDirectory(path)
|
||||
else:
|
||||
if not self._import_config_dir:
|
||||
self._import_config_dir = self._main_window.project().filesDir()
|
||||
for item in items:
|
||||
if len(item.node().configFiles()) == 1:
|
||||
config_file = item.node().configFiles()[0]
|
||||
else:
|
||||
config_file, ok = QtWidgets.QInputDialog.getItem(self, "Import file", "File to import?", item.node().configFiles(), 0, False)
|
||||
if not ok:
|
||||
continue
|
||||
|
||||
if not self._import_config_dir:
|
||||
self._import_config_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
|
||||
|
||||
item = items[0]
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self,
|
||||
"Import config",
|
||||
"Import {}".format(os.path.basename(config_file)),
|
||||
self._import_config_dir,
|
||||
"All files (*.*);;Config files (*.cfg)",
|
||||
"Config files (*.cfg)")
|
||||
self._import_config_dir = os.path.dirname(path)
|
||||
item.node().importFile(config_file, path)
|
||||
|
||||
if path:
|
||||
self._import_config_dir = os.path.dirname(path)
|
||||
item.node().importConfig(path)
|
||||
if hasattr(item.node(), "importPrivateConfig"):
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self,
|
||||
"Import private-config",
|
||||
self._import_config_dir,
|
||||
"All files (*.*);;Config files (*.cfg)",
|
||||
"Config files (*.cfg)")
|
||||
if path:
|
||||
item.node().importPrivateConfig(path)
|
||||
def editConfigActionSlot(self):
|
||||
"""
|
||||
Slot to receive event to edit the configuration file
|
||||
"""
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "configFiles") 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]
|
||||
else:
|
||||
config_file, ok = QtWidgets.QInputDialog.getItem(self, "Edit file", "File to edit?", item.node().configFiles(), 0, False)
|
||||
if not ok:
|
||||
continue
|
||||
dialog = FileEditorDialog(item.node(), config_file, parent=self)
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
def exportConfigActionSlot(self):
|
||||
"""
|
||||
@@ -1141,69 +1141,50 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "exportConfig") and item.node().initialized():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "configFiles") and item.node().initialized():
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
return
|
||||
|
||||
if len(items) > 1:
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self, "Export directory", self._export_configs_to_dir, QtWidgets.QFileDialog.ShowDirsOnly)
|
||||
if path:
|
||||
if not self._export_configs_to_dir:
|
||||
self._export_configs_to_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
|
||||
|
||||
for item in items:
|
||||
for config_file in item.node().configFiles():
|
||||
path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_configs_to_dir, item.node().name() + "_" + os.path.basename(config_file)), "All files (*.*);;Config files (*.cfg)")
|
||||
|
||||
if not path:
|
||||
continue
|
||||
|
||||
self._export_configs_to_dir = os.path.dirname(path)
|
||||
for item in items:
|
||||
item.node().exportConfigToDirectory(path)
|
||||
else:
|
||||
if not self._export_config_dir:
|
||||
self._export_config_dir = self._main_window.project().filesDir()
|
||||
|
||||
item = items[0]
|
||||
if hasattr(item.node(), "importPrivateConfig"):
|
||||
config_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export startup-config", self._export_config_dir)
|
||||
self._export_config_dir = os.path.dirname(config_path)
|
||||
private_config_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export private-config", self._export_config_dir)
|
||||
item.node().exportConfig(config_path, private_config_path)
|
||||
item.node().exportFile(config_file, path)
|
||||
|
||||
def getCommandLineSlot(self):
|
||||
"""
|
||||
Slot to receive events from the get command line action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
items = self.scene().selectedItems()
|
||||
if len(items) != 1:
|
||||
QtWidgets.QMessageBox.critical(self, "Command line", "Please select only one router")
|
||||
return
|
||||
item = items[0]
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "commandLine"):
|
||||
router = item.node()
|
||||
if router.commandLine() is None:
|
||||
QtWidgets.QMessageBox.warning(self, "Command line", "Get command line is not supported for this type of node.")
|
||||
elif router.commandLine() == '':
|
||||
QtWidgets.QMessageBox.warning(self, "Command line", "Please start the node in order to get the command line.")
|
||||
else:
|
||||
config_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export config", self._export_config_dir)
|
||||
self._export_config_dir = os.path.dirname(config_path)
|
||||
item.node().exportConfig(config_path)
|
||||
|
||||
def saveConfigActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the save config action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "saveConfig") and item.node().initialized():
|
||||
item.node().saveConfig()
|
||||
|
||||
def captureActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the capture action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "startPacketCapture") and item.node().initialized():
|
||||
node = item.node()
|
||||
ports = {}
|
||||
for port in node.ports():
|
||||
if not port.isFree() and port.packetCaptureSupported() and not port.capturing():
|
||||
for dlt_name, dlt in port.dataLinkTypes().items():
|
||||
key = "Port {} ({} encapsulation: {})".format(port.name(), dlt_name, dlt)
|
||||
ports[key] = [port, dlt]
|
||||
if ports:
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(self, "Capture on {}".format(node.name()), "Please select a port:", list(ports.keys()), 0, False)
|
||||
if ok:
|
||||
if selection in ports:
|
||||
port, dlt = ports[selection]
|
||||
try:
|
||||
node.startPacketCapture(port, port.captureFileName(node.name()), dlt)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Packet capture", "Cannot start Wireshark: {}".format(e))
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "Capture", "No port available for packet capture on {}".format(node.name()))
|
||||
dialog = QtWidgets.QInputDialog(self)
|
||||
dialog.setOptions(QtWidgets.QInputDialog.NoButtons)
|
||||
dialog.setLabelText("Command used to start the VM:")
|
||||
dialog.setTextValue(router.commandLine())
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
def idlepcActionSlot(self):
|
||||
"""
|
||||
@@ -1283,22 +1264,16 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NoteItem):
|
||||
note_item = item.duplicate()
|
||||
self.scene().addItem(note_item)
|
||||
self._topology.addNote(note_item)
|
||||
elif isinstance(item, RectangleItem):
|
||||
rectangle_item = item.duplicate()
|
||||
self.scene().addItem(rectangle_item)
|
||||
self._topology.addRectangle(rectangle_item)
|
||||
elif isinstance(item, EllipseItem):
|
||||
ellipse_item = item.duplicate()
|
||||
self.scene().addItem(ellipse_item)
|
||||
self._topology.addEllipse(ellipse_item)
|
||||
elif isinstance(item, ImageItem):
|
||||
image_item = item.duplicate()
|
||||
self.scene().addItem(image_item)
|
||||
self._topology.addImage(image_item)
|
||||
if isinstance(item, DrawingItem):
|
||||
if isinstance(item, EllipseItem):
|
||||
type = "ellipse"
|
||||
elif isinstance(item, TextItem):
|
||||
type = "text"
|
||||
elif isinstance(item, RectangleItem):
|
||||
type = "rect"
|
||||
else:
|
||||
type = "image"
|
||||
self.createDrawingItem(type, item.pos().x() + 20, item.pos().y() + 20, item.zValue(), rotation=item.rotation(), svg=item.toSvg())
|
||||
|
||||
def styleActionSlot(self):
|
||||
"""
|
||||
@@ -1323,13 +1298,31 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NoteItem):
|
||||
if isinstance(item, NoteItem) or isinstance(item, TextItem):
|
||||
items.append(item)
|
||||
if items:
|
||||
text_edit_dialog = TextEditorDialog(self._main_window, items)
|
||||
text_edit_dialog.show()
|
||||
text_edit_dialog.exec_()
|
||||
|
||||
def resetLabelPositionActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the reset label position action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NoteItem) and item.parentItem():
|
||||
links = item.parentItem().links()
|
||||
for port in item.parentItem().node().ports():
|
||||
# find the correct port associated with the label
|
||||
if port.label() == item:
|
||||
port.deleteLabel()
|
||||
break
|
||||
# adjust all node links to force to re-display the label
|
||||
for link in links:
|
||||
link.adjust()
|
||||
|
||||
def horizontalAlignmentSlot(self):
|
||||
"""
|
||||
Slot to receive events from the horizontal align action in the
|
||||
@@ -1409,17 +1402,23 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
elif item.parentItem() is None:
|
||||
item.delete()
|
||||
|
||||
@staticmethod
|
||||
def allocateServer():
|
||||
def allocateCompute(self, node_data, module_instance):
|
||||
"""
|
||||
Allocates a server.
|
||||
|
||||
:returns: allocated server (HTTPClient instance)
|
||||
:returns: allocated compute node
|
||||
"""
|
||||
|
||||
from .main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
server = server_select(mainwindow)
|
||||
|
||||
if "server" in node_data:
|
||||
return ComputeManager.instance().getCompute(node_data["server"])
|
||||
|
||||
if "builtin" in node_data:
|
||||
allow_local_server = True
|
||||
else:
|
||||
allow_local_server = module_instance.settings()["use_local_server"]
|
||||
server = server_select(mainwindow, node_data.get("node_type"), allow_local_server=allow_local_server)
|
||||
if server is None:
|
||||
raise ModuleError("Please select a server")
|
||||
return server
|
||||
@@ -1433,7 +1432,6 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
:returns: NodeItem instance
|
||||
"""
|
||||
|
||||
try:
|
||||
node_module = None
|
||||
for module in MODULES:
|
||||
@@ -1446,45 +1444,78 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if not node_module:
|
||||
raise ModuleError("Could not find any module for {}".format(node_class))
|
||||
|
||||
if "server" not in node_data:
|
||||
server = self.allocateServer()
|
||||
elif node_data["server"] == "local":
|
||||
server = Servers.instance().localServer()
|
||||
elif node_data["server"] == "vm":
|
||||
server = Servers.instance().vmServer()
|
||||
if server is None:
|
||||
QtWidgets.QMessageBox.critical(self, "GNS3 VM", "The GNS3 VM is not running")
|
||||
return
|
||||
elif node_data["server"] == "load-balance":
|
||||
ram = node_data.get("ram", 0)
|
||||
server = Servers.instance().anyRemoteServer(ram)
|
||||
if server is None:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "Cannot load balance: no remote server configured")
|
||||
return
|
||||
else:
|
||||
server = Servers.instance().getServerFromString(node_data["server"])
|
||||
|
||||
if server is None:
|
||||
return
|
||||
|
||||
node = node_module.createNode(node_class, server, self._main_window.project())
|
||||
node.error_signal.connect(self._main_window.uiConsoleTextEdit.writeError)
|
||||
node.warning_signal.connect(self._main_window.uiConsoleTextEdit.writeWarning)
|
||||
node.server_error_signal.connect(self._main_window.uiConsoleTextEdit.writeServerError)
|
||||
if QtSvg.QSvgRenderer(node_data["symbol"]).isValid():
|
||||
node_item = SvgNodeItem(node, node_data["symbol"])
|
||||
else:
|
||||
node_item = PixmapNodeItem(node, node_data["symbol"])
|
||||
node_module.setupNode(node, node_data["name"])
|
||||
except ModuleError as e:
|
||||
node = node_module.instantiateNode(node_class, self.allocateCompute(node_data, instance), self._topology.project())
|
||||
# If no server is available a ValueError is raised
|
||||
except (ModuleError, ValueError) as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Node creation", "{}".format(e))
|
||||
return
|
||||
|
||||
node_item.setPos(self.mapToScene(pos))
|
||||
self.scene().addItem(node_item)
|
||||
x = node_item.pos().x() - (node_item.boundingRect().width() / 2)
|
||||
y = node_item.pos().y() - (node_item.boundingRect().height() / 2)
|
||||
node_item.setPos(x, y)
|
||||
self._topology.addNode(node)
|
||||
self._main_window.uiTopologySummaryTreeWidget.addNode(node)
|
||||
pos = self.mapToScene(pos)
|
||||
node_item = self.createNodeItem(node, node_data["symbol"], pos.x(), pos.y())
|
||||
node.setGraphics(node_item)
|
||||
node_module.createNode(node, node_data["name"])
|
||||
return node_item
|
||||
|
||||
def createNodeItem(self, node, symbol, x, y):
|
||||
node.setSymbol(symbol)
|
||||
node.setPos(x, y)
|
||||
node_item = NodeItem(node)
|
||||
self.scene().addItem(node_item)
|
||||
self._topology.addNode(node)
|
||||
|
||||
node.error_signal.connect(self._main_window.uiConsoleTextEdit.writeError)
|
||||
node.error_signal.connect(self._displayNodeErrorSlot)
|
||||
node.warning_signal.connect(self._main_window.uiConsoleTextEdit.writeWarning)
|
||||
node.server_error_signal.connect(self._main_window.uiConsoleTextEdit.writeServerError)
|
||||
node.server_error_signal.connect(self._displayNodeErrorSlot)
|
||||
|
||||
return node_item
|
||||
|
||||
def _displayNodeErrorSlot(self, node_id, message):
|
||||
"""
|
||||
Show error send by a node to the user
|
||||
"""
|
||||
node = Topology.instance().getNode(node_id)
|
||||
name = "Node"
|
||||
if node:
|
||||
if node.name():
|
||||
name = node.name()
|
||||
QtWidgets.QMessageBox.critical(self._main_window, name, message.strip())
|
||||
|
||||
def createDrawingItem(self, type, x, y, z, rotation=0, svg=None, drawing_id=None):
|
||||
|
||||
if type == "ellipse":
|
||||
item = EllipseItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "rect":
|
||||
item = RectangleItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "image":
|
||||
item = ImageItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "text":
|
||||
item = TextItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
|
||||
if drawing_id is None:
|
||||
item.create()
|
||||
|
||||
self.scene().addItem(item)
|
||||
self._topology.addDrawing(item)
|
||||
return item
|
||||
|
||||
def drawBackground(self, painter, rect):
|
||||
super().drawBackground(painter, rect)
|
||||
if self._main_window.uiShowGridAction.isChecked():
|
||||
gridSize = 75
|
||||
painter.save()
|
||||
painter.setPen(QtGui.QPen(QtGui.QColor(190, 190, 190)))
|
||||
|
||||
left = int(rect.left()) - (int(rect.left()) % gridSize)
|
||||
top = int(rect.top()) - (int(rect.top()) % gridSize)
|
||||
|
||||
x = left
|
||||
while x < rect.right():
|
||||
painter.drawLine(x, rect.top(), x, rect.bottom())
|
||||
x += gridSize
|
||||
y = top
|
||||
while y < rect.bottom():
|
||||
painter.drawLine(rect.left(), y, rect.right(), y)
|
||||
y += gridSize
|
||||
painter.restore()
|
||||
|
||||
@@ -17,17 +17,16 @@
|
||||
|
||||
|
||||
import json
|
||||
import http
|
||||
import copy
|
||||
import ipaddress
|
||||
import http
|
||||
import uuid
|
||||
import urllib.request
|
||||
import pathlib
|
||||
import urllib.request
|
||||
import base64
|
||||
|
||||
from .version import __version__, __version_info__
|
||||
from .qt import QtCore, QtNetwork, qpartial
|
||||
from .network_client import getNetworkUrl
|
||||
from .utils import parse_version
|
||||
|
||||
import logging
|
||||
@@ -49,78 +48,43 @@ class HTTPClient(QtCore.QObject):
|
||||
:param network_manager: A QT network manager
|
||||
"""
|
||||
|
||||
_instance_count = 1
|
||||
# How many times we need to retry a connection
|
||||
MAX_RETRY_CONNECTION = 5
|
||||
|
||||
# Callback class used for displaying progress
|
||||
_progress_callback = None
|
||||
|
||||
connection_connected_signal = QtCore.Signal()
|
||||
connection_closed_signal = QtCore.Signal()
|
||||
system_usage_updated_signal = QtCore.Signal()
|
||||
connection_error_signal = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, settings, network_manager):
|
||||
def __init__(self, settings, network_manager=None):
|
||||
|
||||
super().__init__()
|
||||
self._version = ""
|
||||
|
||||
self._scheme = settings.get("protocol", "http")
|
||||
self._protocol = settings.get("protocol", "http")
|
||||
self._host = settings["host"]
|
||||
if "http_host" in settings:
|
||||
self._http_host = settings["http_host"]
|
||||
else:
|
||||
self._http_host = settings["host"]
|
||||
if self._host == "0.0.0.0":
|
||||
self._host = "127.0.0.1"
|
||||
elif ":" in self._host and str(ipaddress.IPv6Address(self._host)) == "::":
|
||||
self._host = "::1"
|
||||
self._port = int(settings["port"])
|
||||
self._http_port = int(settings["port"])
|
||||
self._user = settings.get("user", None)
|
||||
self._password = settings.get("password", None)
|
||||
# How many time we have retry connection
|
||||
self._retry = 0
|
||||
self._connected = False
|
||||
self._local = True
|
||||
self._cloud = False
|
||||
self._gns3_vm = False
|
||||
self._ram_limit = settings.get("ram_limit", 0)
|
||||
self._allocated_ram = 0
|
||||
self._shutdown = False # Shutdown in progress
|
||||
self._accept_insecure_certificate = settings.get("accept_insecure_certificate", None)
|
||||
self._usage = None
|
||||
|
||||
self._network_manager = network_manager
|
||||
if network_manager:
|
||||
self._network_manager = network_manager
|
||||
else:
|
||||
self._network_manager = QtNetwork.QNetworkAccessManager()
|
||||
|
||||
# A buffer used by progress download
|
||||
self._buffer = {}
|
||||
|
||||
# create an unique ID
|
||||
self._id = HTTPClient._instance_count
|
||||
HTTPClient._instance_count += 1
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Return a dictionnary with server settings
|
||||
"""
|
||||
settings = {"protocol": self.protocol(),
|
||||
"ram_limit": self.RAMLimit(),
|
||||
"host": self.host(),
|
||||
"port": self.port(),
|
||||
"user": self.user(),
|
||||
"password": self._password}
|
||||
if self.protocol() == "https":
|
||||
settings["accept_insecure_certificate"] = self.acceptInsecureCertificate()
|
||||
return settings
|
||||
|
||||
def acceptInsecureCertificate(self, certificate=None):
|
||||
"""
|
||||
Does the server accept this insecure SSL certificate digest
|
||||
|
||||
:param: Certificate digest
|
||||
"""
|
||||
return self._accept_insecure_certificate
|
||||
|
||||
def setAcceptInsecureCertificate(self, certificate):
|
||||
"""
|
||||
Does the server accept this insecure SSL certificate digest
|
||||
|
||||
:param: Certificate digest
|
||||
"""
|
||||
self._accept_insecure_certificate = certificate
|
||||
# List of query waiting for the connection
|
||||
self._query_waiting_connections = []
|
||||
|
||||
def host(self):
|
||||
"""
|
||||
@@ -130,7 +94,6 @@ class HTTPClient(QtCore.QObject):
|
||||
|
||||
def setHost(self, host):
|
||||
self._host = host
|
||||
self._http_host = host
|
||||
|
||||
def port(self):
|
||||
"""
|
||||
@@ -140,13 +103,20 @@ class HTTPClient(QtCore.QObject):
|
||||
|
||||
def setPort(self, port):
|
||||
self._port = port
|
||||
self._http_port = port
|
||||
|
||||
def protocol(self):
|
||||
"""
|
||||
Transport protocol
|
||||
"""
|
||||
return self._scheme
|
||||
return self._protocol
|
||||
|
||||
def setAcceptInsecureCertificate(self, certificate):
|
||||
"""
|
||||
Does the server accept this insecure SSL certificate digest
|
||||
|
||||
:param: Certificate digest
|
||||
"""
|
||||
self._accept_insecure_certificate = certificate
|
||||
|
||||
def user(self):
|
||||
"""
|
||||
@@ -154,13 +124,38 @@ class HTTPClient(QtCore.QObject):
|
||||
"""
|
||||
return self._user
|
||||
|
||||
def setUser(self, user):
|
||||
self._user = user
|
||||
def url(self):
|
||||
"""Returns current server url"""
|
||||
|
||||
if ":" in self.host():
|
||||
return "{}://[{}]:{}".format(self.protocol(), self.host(), self.port())
|
||||
return "{}://{}:{}".format(self.protocol(), self.host(), self.port())
|
||||
|
||||
def fullUrl(self):
|
||||
"""Returns current server url including user and password"""
|
||||
host = self.host()
|
||||
if ":" in self.host():
|
||||
host = "[{}]".format(host)
|
||||
|
||||
if self._user:
|
||||
return "{}://{}:{}@{}:{}".format(self.protocol(), self._user, self._password, host, self.port())
|
||||
else:
|
||||
return "{}://{}:{}".format(self.protocol(), host, self.port())
|
||||
|
||||
def password(self):
|
||||
return self._password
|
||||
|
||||
def setPassword(self, password):
|
||||
self._password = password
|
||||
|
||||
def notify_progress_start_query(self, query_id, progress_text, response):
|
||||
def shutdown(self):
|
||||
"""
|
||||
Stop the server and stop to accept queries
|
||||
"""
|
||||
self.createHTTPQuery("POST", "/shutdown", None, showProgress=False)
|
||||
self._shutdown = True
|
||||
|
||||
def _notify_progress_start_query(self, query_id, progress_text, response):
|
||||
"""
|
||||
Called when a query start
|
||||
"""
|
||||
@@ -168,12 +163,9 @@ class HTTPClient(QtCore.QObject):
|
||||
if progress_text:
|
||||
HTTPClient._progress_callback.add_query_signal.emit(query_id, progress_text, response)
|
||||
else:
|
||||
if self._local:
|
||||
HTTPClient._progress_callback.add_query_signal.emit(query_id, "Waiting for local GNS3 server", response)
|
||||
else:
|
||||
HTTPClient._progress_callback.add_query_signal.emit(query_id, "Waiting for {}".format(self.url()), response)
|
||||
HTTPClient._progress_callback.add_query_signal.emit(query_id, "Waiting for {}".format(self.url()), response)
|
||||
|
||||
def notify_progress_end_query(cls, query_id):
|
||||
def _notify_progress_end_query(cls, query_id):
|
||||
"""
|
||||
Called when a query is over
|
||||
"""
|
||||
@@ -181,14 +173,14 @@ class HTTPClient(QtCore.QObject):
|
||||
if HTTPClient._progress_callback:
|
||||
HTTPClient._progress_callback.remove_query_signal.emit(query_id)
|
||||
|
||||
def notify_progress_upload(self, query_id, sent, total):
|
||||
def _notify_progress_upload(self, query_id, sent, total):
|
||||
"""
|
||||
Called when a query upload progress
|
||||
"""
|
||||
if HTTPClient._progress_callback:
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
|
||||
|
||||
def notify_progress_download(self, query_id, sent, total):
|
||||
def _notify_progress_download(self, query_id, sent, total):
|
||||
"""
|
||||
Called when a query download progress
|
||||
"""
|
||||
@@ -203,59 +195,6 @@ class HTTPClient(QtCore.QObject):
|
||||
|
||||
cls._progress_callback = progress_callback
|
||||
|
||||
@staticmethod
|
||||
def reset():
|
||||
"""Reset HTTP client internal variables"""
|
||||
|
||||
HTTPClient._instance_count = 0
|
||||
|
||||
def url(self):
|
||||
"""Returns current server url"""
|
||||
|
||||
return getNetworkUrl(self.protocol(), self.host(), self.port(), self.user(), self.settings())
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
Returns this HTTP Client identifier.
|
||||
:returns: HTTP client identifier (string)
|
||||
"""
|
||||
|
||||
return self._id
|
||||
|
||||
def setLocal(self, value):
|
||||
"""
|
||||
Sets either this is a connection to a local server or not.
|
||||
:param value: boolean
|
||||
"""
|
||||
|
||||
self._local = value
|
||||
|
||||
def isLocal(self):
|
||||
"""
|
||||
Returns either this is a connection to a local server or not.
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._local
|
||||
|
||||
def setGNS3VM(self, value):
|
||||
"""
|
||||
Sets either this is a connection to the GNS3 VM or not.
|
||||
|
||||
:param value: boolean
|
||||
"""
|
||||
|
||||
self._gns3_vm = value
|
||||
|
||||
def isGNS3VM(self):
|
||||
"""
|
||||
Returns either this is a connection to the GNS3 VM or not.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._gns3_vm
|
||||
|
||||
def connected(self):
|
||||
"""
|
||||
Returns if the client is connected.
|
||||
@@ -268,115 +207,7 @@ class HTTPClient(QtCore.QObject):
|
||||
"""
|
||||
Closes the connection with the server.
|
||||
"""
|
||||
log.info("Connection to %s closed", self.url())
|
||||
self._connected = False
|
||||
self.connection_closed_signal.emit()
|
||||
|
||||
def isLocalServerRunning(self):
|
||||
"""
|
||||
Synchronous check if a server is already running on this host.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
status, json_data = self.getSynchronous("version", timeout=2)
|
||||
if json_data is None or status != 200:
|
||||
return False
|
||||
else:
|
||||
version = json_data.get("version", None)
|
||||
if version is None:
|
||||
log.debug("Server is not a GNS3 server")
|
||||
return False
|
||||
return True
|
||||
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
"""
|
||||
Synchronous check if a server is running
|
||||
|
||||
:returns: Tuple (Status code, json of anwser). Status 0 is a non HTTP error
|
||||
"""
|
||||
try:
|
||||
url = "{protocol}://{host}:{port}/v1/{endpoint}".format(protocol=self._scheme, host=self._http_host, port=self._http_port, endpoint=endpoint)
|
||||
|
||||
log.debug("Synchronous get %s with user %s", url, self._user)
|
||||
if self._user is not None and len(self._user) > 0:
|
||||
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)
|
||||
|
||||
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()
|
||||
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.warn("Invalid local server url: {}".format(e))
|
||||
return 0, None
|
||||
except (urllib.error.URLError):
|
||||
# Connection refused. It's a normal behavior if server is not started
|
||||
return 0, None
|
||||
except urllib.error.HTTPError as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return e.code, None
|
||||
except (OSError, http.client.BadStatusLine, ValueError) as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return 0, None
|
||||
|
||||
def get(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP GET on the remote server
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
self.createHTTPQuery("GET", path, callback, **kwargs)
|
||||
|
||||
def put(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP PUT on the remote server
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
self.createHTTPQuery("PUT", path, callback, **kwargs)
|
||||
|
||||
def post(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP POST on the remote server
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
self.createHTTPQuery("POST", path, callback, **kwargs)
|
||||
|
||||
def delete(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP DELETE on the remote server
|
||||
|
||||
:param path: Remote path
|
||||
:param callback: callback method to call when the server replies
|
||||
|
||||
Full arg list in createHTTPQuery
|
||||
"""
|
||||
|
||||
self.createHTTPQuery("DELETE", path, callback, **kwargs)
|
||||
|
||||
def _request(self, url):
|
||||
"""
|
||||
@@ -389,15 +220,15 @@ class HTTPClient(QtCore.QObject):
|
||||
|
||||
return QtNetwork.QNetworkRequest(url)
|
||||
|
||||
def _connect(self, query):
|
||||
def _connect(self, query, server):
|
||||
"""
|
||||
Initialize the connection
|
||||
|
||||
:param query: The query to execute when all network stack is ready
|
||||
:param query: The Server to connect
|
||||
"""
|
||||
self.executeHTTPQuery("GET", "/version", query, {})
|
||||
|
||||
def createHTTPQuery(self, method, path, callback, body={}, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None):
|
||||
def createHTTPQuery(self, method, path, callback, body={}, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, timeout=120, server=None, prefix="/v2", params={}, **kwargs):
|
||||
"""
|
||||
Call the remote server, if not connected, check connection before
|
||||
|
||||
@@ -408,42 +239,54 @@ class HTTPClient(QtCore.QObject):
|
||||
:param context: Pass a context to the response callback
|
||||
:param downloadProgressCallback: Callback called when received something, it can be an incomplete response
|
||||
:param showProgress: Display progress to the user
|
||||
:params progressText: Text display to user in the progress dialog. None for auto generated
|
||||
:param progressText: Text display to user in the progress dialog. None for auto generated
|
||||
:param ignoreErrors: Ignore connection error (usefull to not closing a connection when notification feed is broken)
|
||||
:param server: The server where the query will run
|
||||
:param timeout: Delay in seconds before raising a timeout
|
||||
:param prefix: Prefix to the path
|
||||
:param params: Query arguments parameters
|
||||
:returns: QNetworkReply
|
||||
"""
|
||||
|
||||
if self._connected:
|
||||
return self.executeHTTPQuery(method, path, qpartial(callback), body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText)
|
||||
else:
|
||||
log.info("Connection to {}".format(self.url()))
|
||||
query = qpartial(self._callbackConnect, method, path, qpartial(callback), body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText)
|
||||
self._connect(query)
|
||||
# Shutdown in progress do not execute the query
|
||||
if self._shutdown:
|
||||
return
|
||||
|
||||
def _connectionError(self, callback, msg=""):
|
||||
request = qpartial(self._executeHTTPQuery, method, path, qpartial(callback), body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText, server=server, timeout=timeout, prefix=prefix, params=params)
|
||||
|
||||
if self._connected:
|
||||
return request()
|
||||
else:
|
||||
self._query_waiting_connections.append((request, callback))
|
||||
# If we are not connected and we enqueue the first query we open the conection
|
||||
if len(self._query_waiting_connections) == 1:
|
||||
log.info("Connection to {}".format(self.url()))
|
||||
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=5)
|
||||
|
||||
def _connectionError(self, callback, msg="", server=None):
|
||||
"""
|
||||
Return an error to user if connection failed
|
||||
|
||||
:param callback: User callback
|
||||
:param msg: An optional additional message for the callback
|
||||
:param server: Server where the query is execute
|
||||
"""
|
||||
|
||||
if self.isLocal():
|
||||
server = "local server {}".format(self.url())
|
||||
else:
|
||||
server = "remote server {}".format(self.url())
|
||||
if len(msg) > 0:
|
||||
msg = "Cannot connect to {}: {}".format(server, msg)
|
||||
msg = "Cannot connect to server {}: {}".format(self.url(), msg)
|
||||
else:
|
||||
if self.isLocal():
|
||||
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall.".format(server)
|
||||
else:
|
||||
msg = "Cannot connect to {}".format(server)
|
||||
log.error(msg)
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=self)
|
||||
msg = "Cannot connect to {}. Please check if GNS3 is allowed in your antivirus and firewall. And that server version is {}.".format(self.url(), __version__)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
self._query_waiting_connections = []
|
||||
|
||||
def _callbackConnect(self, method, path, callback, body, original_context, params, error=False, server=None, **kwargs):
|
||||
def _retryConnection(self, server=None):
|
||||
log.debug("Retry connection to {}".format(self.url()))
|
||||
self._retry += 1
|
||||
QtCore.QTimer.singleShot(1000, qpartial(self._executeHTTPQuery, "GET", "/version", self._callbackConnect, {}, server=server, timeout=5))
|
||||
|
||||
def _callbackConnect(self, params, error=False, server=None, **kwargs):
|
||||
"""
|
||||
Callback after /version response. Continue execution of query
|
||||
|
||||
@@ -455,14 +298,24 @@ class HTTPClient(QtCore.QObject):
|
||||
"""
|
||||
|
||||
if error is not False:
|
||||
self._connectionError(callback)
|
||||
if self._retry < self.MAX_RETRY_CONNECTION:
|
||||
self._retryConnection(server=server)
|
||||
return
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
self._connectionError(callback)
|
||||
return
|
||||
|
||||
if "version" not in params or "local" not in params:
|
||||
if self._retry < self.MAX_RETRY_CONNECTION:
|
||||
self._retryConnection(server=server)
|
||||
return
|
||||
msg = "The remote server {} is not a GNS3 server".format(self.url())
|
||||
log.error(msg)
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=self)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
self._query_waiting_connections = []
|
||||
return
|
||||
|
||||
if params["version"] != __version__:
|
||||
@@ -470,32 +323,25 @@ class HTTPClient(QtCore.QObject):
|
||||
log.error(msg)
|
||||
# Stable release
|
||||
if __version_info__[3] == 0:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=self)
|
||||
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
|
||||
# We don't allow different major version to interact even with dev build
|
||||
elif parse_version(__version__)[:2] != parse_version(params["version"])[:2]:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=self)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
return
|
||||
print(msg)
|
||||
print("WARNING: Use a different client and server version can create bugs. Use it at your own risk.")
|
||||
|
||||
if params["local"] != self.isLocal():
|
||||
if self.isLocal():
|
||||
msg = "Running server is not a GNS3 local server (not started with --local)"
|
||||
else:
|
||||
msg = "Remote running server is started with --local. It is forbidden for security reasons"
|
||||
log.error(msg)
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=self)
|
||||
return
|
||||
log.warning("Use a different client and server version can create bugs. Use it at your own risk.")
|
||||
|
||||
self._connected = True
|
||||
self._retry = 0
|
||||
self.connection_connected_signal.emit()
|
||||
kwargs["context"] = original_context
|
||||
self.executeHTTPQuery(method, path, callback, body, **kwargs)
|
||||
self._version = params["version"]
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if request:
|
||||
request()
|
||||
self._query_waiting_connections = []
|
||||
|
||||
def _addBodyToRequest(self, body, request):
|
||||
"""
|
||||
@@ -525,10 +371,17 @@ class HTTPClient(QtCore.QObject):
|
||||
request.setRawHeader(b"Content-Type", b"application/octet-stream")
|
||||
# QT is smart and will compute the Content-Lenght for us
|
||||
return body
|
||||
elif isinstance(body, str):
|
||||
request.setRawHeader(b"Content-Type", b"application/octet-stream")
|
||||
data = QtCore.QByteArray(body.encode())
|
||||
body = QtCore.QBuffer(self)
|
||||
body.setData(data)
|
||||
body.open(QtCore.QIODevice.ReadOnly)
|
||||
return body
|
||||
else:
|
||||
return None
|
||||
|
||||
def addAuth(self, request):
|
||||
def _addAuth(self, request):
|
||||
"""
|
||||
If require add basic auth header
|
||||
"""
|
||||
@@ -539,7 +392,7 @@ class HTTPClient(QtCore.QObject):
|
||||
request.setRawHeader(b"Authorization", auth_string.encode())
|
||||
return request
|
||||
|
||||
def executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None):
|
||||
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, **kwargs):
|
||||
"""
|
||||
Call the remote server
|
||||
|
||||
@@ -552,29 +405,41 @@ class HTTPClient(QtCore.QObject):
|
||||
:param showProgress: Display progress to the user
|
||||
:param progressText: Text display to user in progress dialog. None for auto generated
|
||||
:param ignoreErrors: Ignore connection error (usefull to not closing a connection when notification feed is broken)
|
||||
:param server: The server where the query is executed
|
||||
:param timeout: Delay in seconds before raising a timeout
|
||||
:param params: Query arguments parameters
|
||||
:returns: QNetworkReply
|
||||
"""
|
||||
|
||||
# TODO: remove it when all call are migrated
|
||||
if "compute/" in path:
|
||||
log.warning("Legacy compute direct call %s", path)
|
||||
|
||||
try:
|
||||
ip = self._http_host.rsplit('%', 1)[0]
|
||||
ip = self._host.rsplit('%', 1)[0]
|
||||
ipaddress.IPv6Address(ip) # remove any scope ID
|
||||
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
|
||||
host = "[{}]".format(ip)
|
||||
except ipaddress.AddressValueError:
|
||||
host = self._http_host
|
||||
host = self._host
|
||||
|
||||
log.debug("{method} {protocol}://{host}:{port}/v1{path} {body}".format(method=method, protocol=self._scheme, host=host, port=self._http_port, path=path, body=body))
|
||||
if self._user:
|
||||
url = QtCore.QUrl("{protocol}://{user}@{host}:{port}/v1{path}".format(protocol=self._scheme, user=self._user, host=host, port=self._http_port, path=path))
|
||||
if params == {}:
|
||||
query_string = ""
|
||||
else:
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}/v1{path}".format(protocol=self._scheme, host=host, port=self._http_port, path=path))
|
||||
query_string = "?" + urllib.parse.urlencode(params)
|
||||
|
||||
log.debug("{method} {protocol}://{host}:{port}{prefix}{path} {body}{query_string}".format(method=method, protocol=self._protocol, host=host, port=self._port, path=path, body=body, prefix=prefix, query_string=query_string))
|
||||
if self._user:
|
||||
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))
|
||||
request = self._request(url)
|
||||
|
||||
request = self.addAuth(request)
|
||||
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
|
||||
# By default QT doesn't support GET with body even if it's in the RFC that's why we need to use sendCustomRequest
|
||||
body = self._addBodyToRequest(body, request)
|
||||
|
||||
response = self._network_manager.sendCustomRequest(request, method.encode(), body)
|
||||
@@ -582,31 +447,37 @@ class HTTPClient(QtCore.QObject):
|
||||
context = copy.copy(context)
|
||||
context["query_id"] = str(uuid.uuid4())
|
||||
|
||||
response.finished.connect(qpartial(self._processResponse, response, callback, context, body, ignoreErrors))
|
||||
response.finished.connect(qpartial(self._processResponse, response, server, callback, context, body, ignoreErrors))
|
||||
|
||||
if downloadProgressCallback is not None:
|
||||
response.downloadProgress.connect(qpartial(self._processDownloadProgress, response, downloadProgressCallback, context))
|
||||
response.readyRead.connect(qpartial(self._readyReadySlot, response, downloadProgressCallback, context, server))
|
||||
|
||||
if HTTPClient._progress_callback and HTTPClient._progress_callback.progress_dialog():
|
||||
request_canceled = qpartial(self._requestCanceled, response, context)
|
||||
HTTPClient._progress_callback.progress_dialog().canceled.connect(request_canceled)
|
||||
|
||||
if showProgress:
|
||||
response.uploadProgress.connect(qpartial(self.notify_progress_upload, context["query_id"]))
|
||||
response.downloadProgress.connect(qpartial(self.notify_progress_download, context["query_id"]))
|
||||
response.uploadProgress.connect(qpartial(self._notify_progress_upload, context["query_id"]))
|
||||
response.downloadProgress.connect(qpartial(self._notify_progress_download, context["query_id"]))
|
||||
# Should be the last operation otherwise we have race condition in Qt
|
||||
# where query start before finishing connect to everything
|
||||
self.notify_progress_start_query(context["query_id"], progressText, response)
|
||||
# where query start before finishing connect to everything
|
||||
self._notify_progress_start_query(context["query_id"], progressText, response)
|
||||
|
||||
if timeout is not None:
|
||||
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response))
|
||||
|
||||
return response
|
||||
|
||||
def _processDownloadProgress(self, response, callback, context, bytesReceived, bytesTotal):
|
||||
def _readyReadySlot(self, response, callback, context, server, *args):
|
||||
"""
|
||||
Process a packet receive on the notification feed.
|
||||
The feed can contains qpartial JSON. If we found a
|
||||
part of a JSON we keep it for the next packet
|
||||
"""
|
||||
|
||||
if response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
return
|
||||
|
||||
# HTTP error
|
||||
# HTTP error
|
||||
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status >= 300:
|
||||
return
|
||||
@@ -621,25 +492,30 @@ class HTTPClient(QtCore.QObject):
|
||||
while True:
|
||||
content = content.lstrip(" \r\n\t")
|
||||
answer, index = json.JSONDecoder().raw_decode(content)
|
||||
callback(answer, server=self, context=context)
|
||||
callback(answer, server=server, context=context)
|
||||
content = content[index:]
|
||||
except ValueError: # Partial JSON
|
||||
self._buffer[context["query_id"]] = content
|
||||
else:
|
||||
callback(content, server=self, context=context)
|
||||
callback(content, server=server, context=context)
|
||||
|
||||
if HTTPClient._progress_callback and HTTPClient._progress_callback.progress_dialog():
|
||||
request_canceled = qpartial(self._requestCanceled, response, context)
|
||||
HTTPClient._progress_callback.progress_dialog().canceled.connect(request_canceled)
|
||||
def _timeoutSlot(self, response):
|
||||
"""
|
||||
Beware it's call for all request you need to check the status of the response
|
||||
"""
|
||||
# We check if we received HTTP headers
|
||||
if not len(response.rawHeaderList()) > 0:
|
||||
response.abort()
|
||||
|
||||
def _requestCanceled(self, response, context):
|
||||
|
||||
if response.isRunning():
|
||||
log.warn("Aborting request for {}".format(response.url()))
|
||||
response.abort()
|
||||
if "query_id" in context:
|
||||
self.notify_progress_end_query(context["query_id"])
|
||||
self._notify_progress_end_query(context["query_id"])
|
||||
|
||||
def _processResponse(self, response, callback, context, request_body, ignore_errors):
|
||||
def _processResponse(self, response, server, callback, context, request_body, ignore_errors):
|
||||
|
||||
if request_body is not None:
|
||||
request_body.close()
|
||||
@@ -648,25 +524,25 @@ class HTTPClient(QtCore.QObject):
|
||||
body = None
|
||||
|
||||
if "query_id" in context:
|
||||
self.notify_progress_end_query(context["query_id"])
|
||||
self._notify_progress_end_query(context["query_id"])
|
||||
|
||||
if response.error() != QtNetwork.QNetworkReply.NoError:
|
||||
error_code = response.error()
|
||||
error_message = response.errorString()
|
||||
|
||||
if not ignore_errors:
|
||||
log.info("Response error: %s (error: %d)", error_message, error_code)
|
||||
log.debug("Response error: %s (error: %d)", error_message, error_code)
|
||||
|
||||
if error_code < 200:
|
||||
if not ignore_errors:
|
||||
self.close()
|
||||
if callback is not None:
|
||||
callback({"message": error_message}, error=True, server=self, context=context)
|
||||
callback({"message": error_message}, error=True, server=server, context=context)
|
||||
return
|
||||
else:
|
||||
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status == 401:
|
||||
print(error_message)
|
||||
log.error(error_message)
|
||||
|
||||
try:
|
||||
body = bytes(response.readAll()).decode("utf-8").strip("\0")
|
||||
@@ -676,19 +552,26 @@ class HTTPClient(QtCore.QObject):
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
if callback is not None:
|
||||
if not body or content_type != "application/json":
|
||||
callback({"message": error_message}, error=True, server=self, context=context)
|
||||
callback({"message": error_message}, error=True, server=server, context=context)
|
||||
else:
|
||||
log.debug(body)
|
||||
try:
|
||||
callback(json.loads(body), error=True, server=self, context=context)
|
||||
callback(json.loads(body), error=True, server=server, context=context)
|
||||
except ValueError:
|
||||
# It happens when an antivirus catch the communication and send is error page without changing the Content Type
|
||||
callback({"message": error_message}, error=True, server=self, context=context)
|
||||
# It happens when an antivirus catch the communication and send is error page without changing the Content Type
|
||||
callback({"message": error_message}, error=True, server=server, context=context)
|
||||
else:
|
||||
# Because nothing is configured to handle the error we display it to the user
|
||||
try:
|
||||
log.error(json.loads(body)["message"])
|
||||
except (ValueError, KeyError):
|
||||
log.error(error_message)
|
||||
else:
|
||||
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
log.debug("Decoding response from {} response {}".format(response.url().toString(), status))
|
||||
try:
|
||||
body = bytes(response.readAll()).decode("utf-8").strip("\0")
|
||||
raw_body = bytes(response.readAll())
|
||||
body = raw_body.decode("utf-8").strip("\0")
|
||||
# Some time anti-virus intercept our query and reply with garbage content
|
||||
except UnicodeDecodeError:
|
||||
body = None
|
||||
@@ -700,9 +583,9 @@ class HTTPClient(QtCore.QObject):
|
||||
params = {}
|
||||
if callback is not None:
|
||||
if status >= 400:
|
||||
callback(params, error=True, server=self, context=context)
|
||||
callback(params, error=True, server=server, context=context)
|
||||
else:
|
||||
callback(params, server=self, context=context)
|
||||
callback(params, server=server, context=context, raw_body=raw_body)
|
||||
# response.deleteLater()
|
||||
if status == 400:
|
||||
try:
|
||||
@@ -714,73 +597,44 @@ class HTTPClient(QtCore.QObject):
|
||||
e = HttpBadRequest(body)
|
||||
raise e
|
||||
|
||||
def RAMLimit(self):
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
"""
|
||||
Returns the RAM limit for this server (used for RAM usage load balancing).
|
||||
Synchronous check if a server is running
|
||||
|
||||
:returns: RAM limit (integer)
|
||||
:returns: Tuple (Status code, json of anwser). 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)
|
||||
|
||||
return self._ram_limit
|
||||
|
||||
def allocatedRAM(self):
|
||||
"""
|
||||
Amount of allocated RAM on this server (used for RAM usage load balancing).
|
||||
|
||||
:returns: allocated RAM (integer)
|
||||
"""
|
||||
|
||||
return self._allocated_ram
|
||||
|
||||
def increaseAllocatedRAM(self, ram):
|
||||
"""
|
||||
Increase the amount of allocated RAM on this server (used for RAM usage load balancing).
|
||||
|
||||
:param ram: amount of RAM (integer)
|
||||
"""
|
||||
|
||||
log.info("RAM usage on {} has increased by {} MB (total load is now {} MB)".format(self.url(), ram, self._allocated_ram + ram))
|
||||
self._allocated_ram += ram
|
||||
|
||||
def decreaseAllocatedRAM(self, ram):
|
||||
"""
|
||||
Decrease the amount of allocated RAM on this server (used for RAM usage load balancing).
|
||||
|
||||
:param ram: amount of RAM (integer)
|
||||
"""
|
||||
|
||||
log.info("RAM usage on {} has decreased by {} MB (total load is now {} MB)".format(self.url(), ram, self._allocated_ram - ram))
|
||||
self._allocated_ram -= ram
|
||||
if self._allocated_ram < 0:
|
||||
self._allocated_ram = 0
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this server.
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
server = self.settings()
|
||||
server["id"] = self._id
|
||||
server["local"] = self._local
|
||||
server["vm"] = self._gns3_vm
|
||||
#server["cloud"] = self._cloud
|
||||
if "user" in server and self._local:
|
||||
del server["user"]
|
||||
if "password" in server:
|
||||
del server["password"]
|
||||
if server["protocol"] == "https":
|
||||
server["accept_insecure_certificate"] = self._accept_insecure_certificate
|
||||
return server
|
||||
|
||||
def systemUsage(self):
|
||||
"""
|
||||
Get information about current system usage
|
||||
|
||||
:returns: None or dict
|
||||
"""
|
||||
return self._usage
|
||||
|
||||
def setSystemUsage(self, usage):
|
||||
self._usage = usage
|
||||
self.system_usage_updated_signal.emit()
|
||||
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()
|
||||
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.warn("Invalid local server url: {}".format(e))
|
||||
return 0, None
|
||||
except urllib.error.URLError:
|
||||
# Connection refused. It's a normal behavior if server is not started
|
||||
return 0, None
|
||||
except urllib.error.HTTPError as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return e.code, None
|
||||
except (OSError, http.client.BadStatusLine, ValueError) as e:
|
||||
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
|
||||
return 0, None
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import copy
|
||||
import pathlib
|
||||
import glob
|
||||
|
||||
from gns3.servers import Servers
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
from gns3.settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.controller import Controller
|
||||
from gns3.utils.file_copy_worker import FileCopyWorker
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
|
||||
@@ -31,7 +34,7 @@ class ImageManager:
|
||||
# Remember if we already ask the user about this image for this server
|
||||
self._asked_for_this_image = {}
|
||||
|
||||
def askCopyUploadImage(self, parent, path, server, vm_type):
|
||||
def askCopyUploadImage(self, parent, path, server, node_type):
|
||||
"""
|
||||
Ask user for copying the image to the default directory or upload
|
||||
it to remote server.
|
||||
@@ -39,14 +42,14 @@ class ImageManager:
|
||||
:param parent: Parent window
|
||||
:param path: File path on computer
|
||||
:param server: The server where the images should be located
|
||||
:param vm_type: Remote upload endpoint
|
||||
:param node_type: Remote upload endpoint
|
||||
:returns path: Final path
|
||||
"""
|
||||
|
||||
if server and not server.isLocal():
|
||||
return self._uploadImageToRemoteServer(path, server, vm_type)
|
||||
if server and server != "local":
|
||||
return self._uploadImageToRemoteServer(path, server, node_type)
|
||||
else:
|
||||
destination_directory = self.getDirectoryForType(vm_type)
|
||||
destination_directory = self.getDirectoryForType(node_type)
|
||||
if os.path.normpath(os.path.dirname(path)) != destination_directory:
|
||||
# the IOS image is not in the default images directory
|
||||
reply = QtWidgets.QMessageBox.question(parent,
|
||||
@@ -73,56 +76,30 @@ class ImageManager:
|
||||
path = destination_path
|
||||
return path
|
||||
|
||||
def _uploadImageToRemoteServer(self, path, server, vm_type):
|
||||
def _uploadImageToRemoteServer(self, path, server, node_type):
|
||||
"""
|
||||
Upload image to remote server
|
||||
|
||||
:param path: File path on computer
|
||||
:param server: The server where the images should be located
|
||||
:param vm_type: Image vm_type
|
||||
:param node_type: Image node_type
|
||||
:returns path: Final path
|
||||
"""
|
||||
|
||||
if vm_type == 'QEMU':
|
||||
upload_endpoint = '/qemu/vms'
|
||||
elif vm_type == 'IOU':
|
||||
upload_endpoint = '/iou/vms'
|
||||
elif vm_type == 'DYNAMIPS':
|
||||
upload_endpoint = '/dynamips/vms'
|
||||
if node_type == 'QEMU':
|
||||
upload_endpoint = '/qemu/images'
|
||||
elif node_type == 'IOU':
|
||||
upload_endpoint = '/iou/images'
|
||||
elif node_type == 'DYNAMIPS':
|
||||
upload_endpoint = '/dynamips/images'
|
||||
else:
|
||||
raise Exception('Invalid image vm_type')
|
||||
raise Exception('Invalid node type')
|
||||
|
||||
filename = self._getRelativeImagePath(path, vm_type).replace("\\", "/")
|
||||
server.post('{}/{}'.format(upload_endpoint, filename), None, body=pathlib.Path(path), progressText="Uploading {}".format(filename))
|
||||
filename = self._getRelativeImagePath(path, node_type).replace("\\", "/")
|
||||
|
||||
Controller.instance().postCompute('{}/{}'.format(upload_endpoint, filename), server.id(), None, body=pathlib.Path(path), progressText="Uploading {}".format(filename), timeout=None)
|
||||
return filename
|
||||
|
||||
def addMissingImage(self, filename, server, vm_type):
|
||||
"""
|
||||
Add a missing image to the queue of images require to be upload on remote server
|
||||
:param filename: Filename of the image
|
||||
:param server: Server where image should be uploaded
|
||||
:param vm_type: Type of the image
|
||||
"""
|
||||
|
||||
if self._asked_for_this_image.setdefault(server.id(), {}).setdefault(filename, False):
|
||||
return
|
||||
self._asked_for_this_image[server.id()][filename] = True
|
||||
|
||||
if server.isLocal():
|
||||
return
|
||||
path = os.path.join(self.getDirectoryForType(vm_type), filename)
|
||||
if os.path.exists(path):
|
||||
if self._askForUploadMissingImage(filename, server):
|
||||
|
||||
if filename.endswith(".vmdk"):
|
||||
# A vmdk file could be split in multiple vmdk file
|
||||
search = glob.escape(path).replace(".vmdk", "-*.vmdk")
|
||||
for file in glob.glob(search):
|
||||
self._uploadImageToRemoteServer(file, server, vm_type)
|
||||
|
||||
self._uploadImageToRemoteServer(path, server, vm_type)
|
||||
del self._asked_for_this_image[server.id()][filename]
|
||||
|
||||
def _askForUploadMissingImage(self, filename, server):
|
||||
from gns3.main_window import MainWindow
|
||||
parent = MainWindow.instance()
|
||||
@@ -135,20 +112,20 @@ class ImageManager:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _getRelativeImagePath(self, path, vm_type):
|
||||
def _getRelativeImagePath(self, path, node_type):
|
||||
"""
|
||||
Get a path relative to images directory path
|
||||
or just filename if the path is not located inside
|
||||
image directory
|
||||
|
||||
:param path: file path
|
||||
:param vm_type: Type of vm
|
||||
:param node_type: Type of vm
|
||||
:return: file path
|
||||
"""
|
||||
|
||||
if not path:
|
||||
return ""
|
||||
img_directory = self.getDirectoryForType(vm_type)
|
||||
img_directory = self.getDirectoryForType(node_type)
|
||||
path = os.path.abspath(path)
|
||||
if os.path.commonprefix([img_directory, path]) == img_directory:
|
||||
return os.path.relpath(path, img_directory)
|
||||
@@ -161,19 +138,19 @@ class ImageManager:
|
||||
:returns: path to the default images directory
|
||||
"""
|
||||
|
||||
return Servers.instance().localServerSettings()['images_path']
|
||||
return copy.copy(LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)['images_path'])
|
||||
|
||||
def getDirectoryForType(self, vm_type):
|
||||
def getDirectoryForType(self, node_type):
|
||||
"""
|
||||
Return the path of local directory of the images
|
||||
of a specific vm_type
|
||||
of a specific node_type
|
||||
|
||||
:param vm_type: Type of vm
|
||||
:param node_type: Type of vm
|
||||
"""
|
||||
if vm_type == 'DYNAMIPS':
|
||||
if node_type == 'DYNAMIPS':
|
||||
return os.path.join(self.getDirectory(), 'IOS')
|
||||
else:
|
||||
return os.path.join(self.getDirectory(), vm_type)
|
||||
return os.path.join(self.getDirectory(), node_type)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import sys
|
||||
import shutil
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
try:
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
except ImportError:
|
||||
raise SystemExit("Can't import Qt modules: Qt and/or PyQt is probably not installed correctly...")
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from gns3.version import __version__
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.ui.iouvm_converter_wizard_ui import Ui_IOUVMConverterWizard
|
||||
|
||||
|
||||
class IOUVMConverterWizard(QtWidgets.QWizard, Ui_IOUVMConverterWizard):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
|
||||
|
||||
# set the window icon
|
||||
self.setWindowIcon(QtGui.QIcon(":/images/gns3.ico")) # this info is necessary for QSettings
|
||||
|
||||
config = self._loadConfig()
|
||||
self.uiPushButtonBrowse.clicked.connect(self._browseTopologiesSlot)
|
||||
self.uiLineEditTopologiesPath.setText(config['Servers']['local_server']['projects_path'])
|
||||
|
||||
def _browseTopologiesSlot(self):
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select a directory')
|
||||
self.uiLineEditTopologiesPath.setText(path)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the settings.
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiWizardPageIOURCCheck:
|
||||
return self._checkIOURC()
|
||||
elif self.currentPage() == self.uiWizardUpdateConfiguration:
|
||||
return self._updateConfig()
|
||||
elif self.currentPage() == self.uiWizardPagePatchTopologies:
|
||||
return self._patchTopologies()
|
||||
return True
|
||||
|
||||
def _checkIOURC(self):
|
||||
"""
|
||||
Validate if the IOURC contain an entry for the IOUVM
|
||||
"""
|
||||
config = self._loadConfig()
|
||||
iourc_path = config.get("IOU", {}).get("iourc_path", "")
|
||||
if len(iourc_path) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Error", "The IOURC is not configured")
|
||||
return False
|
||||
try:
|
||||
with open(iourc_path) as f:
|
||||
if 'gns3vm' not in f.read():
|
||||
QtWidgets.QMessageBox.critical(self, "Error", "The gns3vm doesn't exist in your iourc file".format(iourc_path))
|
||||
except OSError:
|
||||
QtWidgets.QMessageBox.critical(self, "Error", "IOURC file {} doesn't exist or not accessible".format(iourc_path))
|
||||
return True
|
||||
|
||||
def _updateConfig(self):
|
||||
"""
|
||||
Update the config file to use the GNS3 VM instead of IOU VM
|
||||
"""
|
||||
config = self._loadConfig()
|
||||
if "devices" in config["IOU"]:
|
||||
for device in config["IOU"]["devices"]:
|
||||
device["path"] = os.path.basename(device["path"])
|
||||
device["server"] = "vm"
|
||||
config["Servers"]["remote_servers"] = []
|
||||
self._writeConfig(config)
|
||||
return True
|
||||
|
||||
def _patchTopologies(self):
|
||||
"""
|
||||
Patch topologies to use the GNS3 VM
|
||||
"""
|
||||
|
||||
path = self.uiLineEditTopologiesPath.text()
|
||||
try:
|
||||
for (dirpath, dirnames, filenames) in os.walk(path):
|
||||
for filename in filenames:
|
||||
if filename.endswith(".gns3"):
|
||||
self._patchTopology(os.path.join(dirpath, filename))
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Error", "Can't open {}: {}".format(path, str(e)))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _patchTopology(self, path):
|
||||
"""
|
||||
Path a specific topology
|
||||
"""
|
||||
try:
|
||||
shutil.copy(path, "{}.{}.backup".format(path, datetime.now().isoformat()))
|
||||
with open(path) as f:
|
||||
topo = json.load(f)
|
||||
if "topology" in topo and "servers" in topo["topology"]:
|
||||
for server in topo["topology"]["servers"]:
|
||||
if server["local"] is False:
|
||||
server["vm"] = True
|
||||
with open(path, 'w+') as f:
|
||||
topo = json.dump(topo, f)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Error", "Can't open {}: {}".format(path, str(e)))
|
||||
|
||||
def _loadConfig(self):
|
||||
with open(self._configurationFile()) as f:
|
||||
return json.load(f)
|
||||
|
||||
def _writeConfig(self, config):
|
||||
shutil.copy(self._configurationFile(), "{}.{}.backup".format(self._configurationFile(), datetime.now().isoformat()))
|
||||
with open(self._configurationFile(), 'w+') as f:
|
||||
json.dump(config, f, indent=4)
|
||||
|
||||
def _configurationFile(self):
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_gui.ini"
|
||||
else:
|
||||
filename = "gns3_gui.conf"
|
||||
directory = LocalConfig.configDirectory()
|
||||
return os.path.join(directory, filename)
|
||||
|
||||
|
||||
def main():
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
app.setOrganizationName("GNS3")
|
||||
app.setOrganizationDomain("gns3.net")
|
||||
app.setApplicationName("GNS3")
|
||||
app.setApplicationVersion(__version__)
|
||||
|
||||
# We force a full garbage collect before exit
|
||||
# for unknow reason otherwise Qt Segfault on OSX in some
|
||||
# conditions
|
||||
import gc
|
||||
gc.collect()
|
||||
|
||||
# Manage Ctrl + C or kill command
|
||||
def sigint_handler(*args):
|
||||
log.info("Signal received exiting the application")
|
||||
app.closeAllWindows()
|
||||
# signal.signal(signal.SIGINT, sigint_handler)
|
||||
# signal.signal(signal.SIGTERM, sigint_handler)
|
||||
|
||||
mainwindow = IOUVMConverterWizard()
|
||||
mainwindow.show()
|
||||
exit_code = mainwindow.exec_()
|
||||
|
||||
# We force a full garbage collect before exit
|
||||
# for unknow reason otherwise Qt Segfault on OSX in some
|
||||
# conditions
|
||||
import gc
|
||||
gc.collect()
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
212
gns3/items/drawing_item.py
Normal file
212
gns3/items/drawing_item.py
Normal file
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
import binascii
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DrawingItem:
|
||||
|
||||
show_layer = False
|
||||
|
||||
"""
|
||||
Base class for non emulation item
|
||||
"""
|
||||
|
||||
def __init__(self, project=None, pos=None, drawing_id=None, svg=None, z=0, rotation=0, **kws):
|
||||
self._id = drawing_id
|
||||
if self._id is None:
|
||||
self._id = str(uuid.uuid4())
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._graphics_view = MainWindow.instance().uiGraphicsView
|
||||
self._main_window = MainWindow.instance()
|
||||
|
||||
self._project = project
|
||||
|
||||
# Store a hash of the SVG to avoid him
|
||||
# to be send if he doesn't change
|
||||
self._hash_svg = None
|
||||
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
if z:
|
||||
self.setZValue(z)
|
||||
if rotation:
|
||||
self.setRotation(rotation)
|
||||
|
||||
def drawing_id(self):
|
||||
return self._id
|
||||
|
||||
def create(self):
|
||||
self._project.post("/drawings", self._createDrawingCallback, body=self.__json__())
|
||||
|
||||
def _createDrawingCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up drawing: {}".format(result["message"]))
|
||||
return False
|
||||
self._id = result["drawing_id"]
|
||||
self.updateDrawingCallback(result)
|
||||
|
||||
def updateDrawing(self):
|
||||
if self._id:
|
||||
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__())
|
||||
|
||||
def updateDrawingCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while setting up drawing: {}".format(result["message"]))
|
||||
return False
|
||||
self.setPos(QtCore.QPoint(result["x"], result["y"]))
|
||||
self.setZValue(result["z"])
|
||||
self.setRotation(result["rotation"])
|
||||
if "svg" in result:
|
||||
self.fromSvg(result["svg"])
|
||||
|
||||
def handleKeyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
:return: Boolean True the event has been captured
|
||||
"""
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
if key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() == 0:
|
||||
self.setRotation(359)
|
||||
else:
|
||||
self.setRotation(self.rotation() - 1)
|
||||
return True
|
||||
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
return True
|
||||
return False
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
if not self.handleKeyPressEvent(event):
|
||||
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
|
||||
|
||||
def __json__(self):
|
||||
data = {
|
||||
"drawing_id": self._id,
|
||||
"x": int(self.pos().x()),
|
||||
"y": int(self.pos().y()),
|
||||
"z": int(self.zValue()),
|
||||
"rotation": int(self.rotation())
|
||||
}
|
||||
svg = self.toSvg()
|
||||
hash_svg = binascii.crc32(svg.encode())
|
||||
if hash_svg != self._hash_svg:
|
||||
data["svg"] = svg
|
||||
self._hash_svg = hash_svg
|
||||
return data
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtWidgets.QGraphicsItem.setZValue(self, value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def delete(self, skip_controller=False):
|
||||
"""
|
||||
Deletes this drawing.
|
||||
|
||||
:param skip_controller: Do not replicate change on the controller (usefull when it's already deleted on controller)
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeDrawing(self)
|
||||
if self._id and not skip_controller:
|
||||
self._project.delete("/drawings/" + self._id, None, body=self.__json__())
|
||||
|
||||
def itemChange(self, change, value):
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
GRID_SIZE = 75
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
tmp_x = (GRID_SIZE * round((self.x() + mid_x) / GRID_SIZE)) - mid_x
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
tmp_y = (GRID_SIZE * round((self.y() + mid_y) / GRID_SIZE)) - mid_y
|
||||
if tmp_x != self.x() and tmp_y != self.y():
|
||||
self.setPos(tmp_x, tmp_y)
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if not value:
|
||||
self.updateDrawing()
|
||||
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
|
||||
|
||||
def drawLayerInfo(self, painter):
|
||||
"""
|
||||
Draws the layer position.
|
||||
|
||||
:param painter: QPainter instance
|
||||
"""
|
||||
|
||||
if self.show_layer is False:
|
||||
return
|
||||
|
||||
brect = self.boundingRect()
|
||||
# don't draw anything if the object is too small
|
||||
if brect.width() < 20 or brect.height() < 20:
|
||||
return
|
||||
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
@@ -19,6 +19,9 @@
|
||||
Graphical representation of an ellipse on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import math
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .shape_item import ShapeItem
|
||||
|
||||
@@ -29,25 +32,9 @@ class EllipseItem(QtWidgets.QGraphicsEllipseItem, ShapeItem):
|
||||
Class to draw an ellipse on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, pos=None, width=200, height=200):
|
||||
def __init__(self, width=200, height=200, **kws):
|
||||
super().__init__(width=width, height=height, **kws)
|
||||
|
||||
super().__init__()
|
||||
self.setRect(0, 0, width, height)
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.DashLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.setPen(pen)
|
||||
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
|
||||
self.setBrush(brush)
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this ellipse.
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeEllipse(self)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -61,16 +48,21 @@ class EllipseItem(QtWidgets.QGraphicsEllipseItem, ShapeItem):
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def duplicate(self):
|
||||
def toSvg(self):
|
||||
"""
|
||||
Duplicates this ellipse item.
|
||||
|
||||
:return: EllipseItem instance
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
svg = ET.Element("svg")
|
||||
svg.set("width", str(self.rect().width()))
|
||||
svg.set("height", str(self.rect().height()))
|
||||
|
||||
ellipse = ET.SubElement(svg, "ellipse")
|
||||
ellipse.set("cx", str(math.floor(self.rect().width() / 2)))
|
||||
ellipse.set("rx", str(math.ceil(self.rect().width() / 2)))
|
||||
ellipse.set("cy", str(math.floor(self.rect().height() / 2)))
|
||||
ellipse.set("ry", str(math.ceil(self.rect().height() / 2)))
|
||||
|
||||
ellipse = self._styleSvg(ellipse)
|
||||
|
||||
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
|
||||
ellipse_item = EllipseItem(QtCore.QPointF(self.x() + 20, self.y() + 20), self.rect().width(), self.rect().height())
|
||||
ellipse_item.setPen(self.pen())
|
||||
ellipse_item.setBrush(self.brush())
|
||||
ellipse_item.setZValue(self.zValue())
|
||||
ellipse_item.setRotation(self.rotation())
|
||||
return ellipse_item
|
||||
|
||||
@@ -116,13 +116,16 @@ class EthernetLinkItem(LinkItem):
|
||||
if self._source_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._source_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
point1 = QtCore.QPointF(self.source + self.edge_offset) + QtCore.QPointF((self.dx * self._source_collision_offset) / self.length, (self.dy * self._source_collision_offset) / self.length)
|
||||
|
||||
# avoid any collision of the status point with the source node
|
||||
@@ -137,22 +140,17 @@ class EthernetLinkItem(LinkItem):
|
||||
self._source_collision_offset -= 10
|
||||
|
||||
source_port_label = self._source_port.label()
|
||||
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
source_port_name = self._source_port.name()
|
||||
source_port_label.setPlainText(source_port_name)
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, point1))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
if not self._source_port.isStub():
|
||||
source_port_name = self._source_port.name().replace(self._source_port.longNameType(),
|
||||
self._source_port.shortNameType())
|
||||
else:
|
||||
source_port_name = self._source_port.name()
|
||||
source_port_label.setPlainText(source_port_name)
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, point1))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
elif source_port_label and not source_port_label.isVisible():
|
||||
source_port_label.show()
|
||||
|
||||
elif source_port_label:
|
||||
source_port_label.show()
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
painter.drawPoint(point1)
|
||||
@@ -160,13 +158,16 @@ class EthernetLinkItem(LinkItem):
|
||||
if self._destination_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
point2 = QtCore.QPointF(self.destination - self.edge_offset) - QtCore.QPointF((self.dx * self._destination_collision_offset) / self.length, (self.dy * self._destination_collision_offset) / self.length)
|
||||
|
||||
# avoid any collision of the status point with the destination node
|
||||
@@ -181,22 +182,19 @@ class EthernetLinkItem(LinkItem):
|
||||
self._destination_collision_offset -= 10
|
||||
|
||||
destination_port_label = self._destination_port.label()
|
||||
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
destination_port_name = self._destination_port.name()
|
||||
destination_port_label.setPlainText(destination_port_name)
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, point2))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
if not self._destination_port.isStub():
|
||||
destination_port_name = self._destination_port.name().replace(self._destination_port.longNameType(),
|
||||
self._destination_port.shortNameType())
|
||||
else:
|
||||
destination_port_name = self._destination_port.name()
|
||||
destination_port_label.setPlainText(destination_port_name)
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, point2))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
elif destination_port_label and not destination_port_label.isVisible():
|
||||
destination_port_label.show()
|
||||
|
||||
elif destination_port_label:
|
||||
destination_port_label.show()
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(point2)
|
||||
|
||||
self._drawCaptureSymbol()
|
||||
|
||||
@@ -19,32 +19,42 @@
|
||||
Graphical representation of an image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtWidgets, QtCore, QtSvg
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from .drawing_item import DrawingItem
|
||||
|
||||
|
||||
class ImageItem():
|
||||
class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
|
||||
|
||||
"""
|
||||
Class to insert an image on the scene.
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
|
||||
def __init__(self, image_path, pos=None):
|
||||
def __init__(self, image_path=None, pos=None, svg=None, **kws):
|
||||
|
||||
self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)
|
||||
self._image_path = image_path
|
||||
# Because we call the Qt C++ code we need to handle the case of pos is None otherwise we will get a conversion error
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
super().__init__(pos=pos, **kws)
|
||||
else:
|
||||
super().__init__(**kws)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this image item.
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeImage(self)
|
||||
if self._image_path:
|
||||
renderer = QImageSvgRenderer(image_path)
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
# By default center the image
|
||||
if pos is None:
|
||||
x = self.pos().x() - (self.boundingRect().width() / 2)
|
||||
y = self.pos().y() - (self.boundingRect().height() / 2)
|
||||
self.setPos(x, y)
|
||||
|
||||
if svg:
|
||||
svg = self.fromSvg(svg)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -56,68 +66,15 @@ class ImageItem():
|
||||
"""
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
if self.show_layer is False:
|
||||
return
|
||||
def fromSvg(self, svg):
|
||||
renderer = QImageSvgRenderer(svg)
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
brect = self.boundingRect()
|
||||
# don't draw anything if the object is too small
|
||||
if brect.width() < 20 or brect.height() < 20:
|
||||
return
|
||||
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
def setZValue(self, value):
|
||||
def toSvg(self):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
return self.renderer().svg()
|
||||
|
||||
super().setZValue(value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this image item.
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
image_info = {"path": self._image_path,
|
||||
"x": self.x(),
|
||||
"y": self.y()}
|
||||
|
||||
if self.zValue() != 0:
|
||||
image_info["z"] = self.zValue()
|
||||
|
||||
return image_info
|
||||
|
||||
def load(self, image_info):
|
||||
"""
|
||||
Loads an image representation
|
||||
(from a topology file).
|
||||
|
||||
:param image_info: representation of the image item (dictionary)
|
||||
"""
|
||||
|
||||
# load mandatory properties
|
||||
x = image_info["x"]
|
||||
y = image_info["y"]
|
||||
self.setPos(x, y)
|
||||
|
||||
# load optional properties
|
||||
z = image_info.get("z")
|
||||
if z is not None:
|
||||
self.setZValue(z)
|
||||
|
||||
@@ -21,9 +21,22 @@ Link items are graphical representation of a link on the QGraphicsScene
|
||||
"""
|
||||
|
||||
import math
|
||||
import struct
|
||||
import sys
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
|
||||
from ..node import Node
|
||||
from ..packet_capture import PacketCapture
|
||||
|
||||
|
||||
class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
def __init__(self, symbol, parent):
|
||||
|
||||
QtSvg.QGraphicsSvgItem.__init__(self, symbol, parent)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
|
||||
self.parentItem().mousePressEvent(event)
|
||||
event.accept()
|
||||
|
||||
|
||||
class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
@@ -46,7 +59,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
super().__init__()
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setZValue(-1)
|
||||
self.setZValue(-0.5)
|
||||
self._link = None
|
||||
|
||||
from ..main_window import MainWindow
|
||||
@@ -76,9 +89,14 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
# indicates if the link is being hovered
|
||||
self._hovered = False
|
||||
|
||||
# QGraphicsSvgItem to indicate a capture
|
||||
self._capturing_item = None
|
||||
|
||||
if not self._adding_flag:
|
||||
# there is a destination
|
||||
self._link = link
|
||||
self._link.updated_link_signal.connect(self._drawCaptureSymbol)
|
||||
self._link.delete_link_signal.connect(self._linkDeletedSlot)
|
||||
self.setFlag(self.ItemIsFocusable)
|
||||
source_item.addLink(self)
|
||||
destination_item.addLink(self)
|
||||
@@ -90,11 +108,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
self.adjust()
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete this link
|
||||
"""
|
||||
|
||||
def _linkDeletedSlot(self, link_id):
|
||||
# first delete the port labels if any
|
||||
if self._source_port.label():
|
||||
self._source_port.label().setParentItem(None)
|
||||
@@ -105,10 +119,16 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
self._source_item.removeLink(self)
|
||||
self._destination_item.removeLink(self)
|
||||
self._link.deleteLink()
|
||||
|
||||
if self in self.scene().items():
|
||||
self.scene().removeItem(self)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete this link
|
||||
"""
|
||||
self._link.deleteLink()
|
||||
|
||||
def link(self):
|
||||
"""
|
||||
Returns the link attached to this link item.
|
||||
@@ -177,14 +197,8 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
Resets the port label positions.
|
||||
"""
|
||||
|
||||
source_port_label = self._source_port.label()
|
||||
destination_port_label = self._destination_port.label()
|
||||
if source_port_label is not None:
|
||||
source_port_label.delete()
|
||||
self._source_port.setLabel(None)
|
||||
if destination_port_label is not None:
|
||||
destination_port_label.delete()
|
||||
self._destination_port.setLabel(None)
|
||||
self._source_port.deleteLabel()
|
||||
self._destination_port.deleteLabel()
|
||||
|
||||
def populateLinkContextualMenu(self, menu):
|
||||
"""
|
||||
@@ -193,14 +207,14 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
:param menu: QMenu instance
|
||||
"""
|
||||
|
||||
if not self._source_port.capturing() or not self._destination_port.capturing():
|
||||
if not self._link.capturing():
|
||||
# start capture
|
||||
start_capture_action = QtWidgets.QAction("Start capture", menu)
|
||||
start_capture_action.setIcon(QtGui.QIcon(':/icons/capture-start.svg'))
|
||||
start_capture_action.triggered.connect(self._startCaptureActionSlot)
|
||||
menu.addAction(start_capture_action)
|
||||
|
||||
if self._source_port.capturing() or self._destination_port.capturing():
|
||||
if self._link.capturing():
|
||||
# stop capture
|
||||
stop_capture_action = QtWidgets.QAction("Stop capture", menu)
|
||||
stop_capture_action.setIcon(QtGui.QIcon(':/icons/capture-stop.svg'))
|
||||
@@ -213,8 +227,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
start_wireshark_action.triggered.connect(self._startWiresharkActionSlot)
|
||||
menu.addAction(start_wireshark_action)
|
||||
|
||||
if sys.platform.startswith("win") and struct.calcsize("P") * 8 == 64:
|
||||
# Windows 64-bit only (Solarwinds RTV limitation).
|
||||
if PacketCapture.instance().packetAnalyzerAvailable():
|
||||
analyze_action = QtWidgets.QAction("Analyze capture", menu)
|
||||
analyze_action.setIcon(QtGui.QIcon(':/icons/rtv.png'))
|
||||
analyze_action.triggered.connect(self._analyzeCaptureActionSlot)
|
||||
@@ -276,26 +289,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
ports = {}
|
||||
if self._source_port.packetCaptureSupported() and not self._source_port.capturing():
|
||||
for dlt_name, dlt in self._source_port.dataLinkTypes().items():
|
||||
port = "{} port {} ({} encapsulation: {})".format(self._source_item.node().name(), self._source_port.name(), dlt_name, dlt)
|
||||
ports[port] = [self._source_item.node(), self._source_port, dlt]
|
||||
|
||||
if self._destination_port.packetCaptureSupported() and not self._destination_port.capturing():
|
||||
for dlt_name, dlt in self._destination_port.dataLinkTypes().items():
|
||||
port = "{} port {} ({} encapsulation: {})".format(self._destination_item.node().name(), self._destination_port.name(), dlt_name, dlt)
|
||||
ports[port] = [self._destination_item.node(), self._destination_port, dlt]
|
||||
|
||||
if not ports:
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Packet capture", "Packet capture is not supported on this link")
|
||||
return
|
||||
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", list(ports.keys()), 0, False)
|
||||
if ok:
|
||||
if selection in ports:
|
||||
node, port, dlt = ports[selection]
|
||||
node.startPacketCapture(port, port.captureFileName(node.name()), dlt)
|
||||
PacketCapture.instance().startCapture(self._link)
|
||||
|
||||
def _stopCaptureActionSlot(self):
|
||||
"""
|
||||
@@ -303,21 +297,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
if self._source_port.capturing() and self._destination_port.capturing():
|
||||
ports = {}
|
||||
source_port = "{} port {}".format(self._source_item.node().name(), self._source_port.name())
|
||||
ports[source_port] = [self._source_item.node(), self._source_port]
|
||||
destination_port = "{} port {}".format(self._destination_item.node().name(), self._destination_port.name())
|
||||
ports[destination_port] = [self._destination_item.node(), self._destination_port]
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", list(ports.keys()), 0, False)
|
||||
if ok:
|
||||
if selection in ports:
|
||||
node, port = ports[selection]
|
||||
node.stopPacketCapture(port)
|
||||
elif self._source_port.capturing():
|
||||
self._source_item.node().stopPacketCapture(self._source_port)
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_item.node().stopPacketCapture(self._destination_port)
|
||||
PacketCapture.instance().stopCapture(self._link)
|
||||
|
||||
def _startWiresharkActionSlot(self):
|
||||
"""
|
||||
@@ -325,22 +305,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
try:
|
||||
if self._source_port.capturing() and self._destination_port.capturing():
|
||||
ports = ["{} port {}".format(self._source_item.node().name(), self._source_port.name()),
|
||||
"{} port {}".format(self._destination_item.node().name(), self._destination_port.name())]
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Packet capture", "Please select a port:", ports, 0, False)
|
||||
if ok:
|
||||
if selection.endswith(self._source_port.name()):
|
||||
self._source_port.startPacketCaptureReader(self._source_item.node().name())
|
||||
else:
|
||||
self._destination_port.startPacketCaptureReader(self._destination_item.node().name())
|
||||
elif self._source_port.capturing():
|
||||
self._source_port.startPacketCaptureReader(self._source_item.node().name())
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_port.startPacketCaptureReader(self._destination_item.node().name())
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Packet capture", "Cannot start Wireshark: {}".format(e))
|
||||
PacketCapture.instance().startPacketCaptureReader(self._link)
|
||||
|
||||
def _analyzeCaptureActionSlot(self):
|
||||
"""
|
||||
@@ -349,19 +314,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
"""
|
||||
|
||||
try:
|
||||
if self._source_port.capturing() and self._destination_port.capturing():
|
||||
ports = ["{} port {}".format(self._source_item.node().name(), self._source_port.name()),
|
||||
"{} port {}".format(self._destination_item.node().name(), self._destination_port.name())]
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(self._main_window, "Capture analyzer", "Please select a port:", ports, 0, False)
|
||||
if ok:
|
||||
if selection.endswith(self._source_port.name()):
|
||||
self._source_port.startPacketCaptureAnalyzer()
|
||||
else:
|
||||
self._destination_port.startPacketCaptureAnalyzer()
|
||||
elif self._source_port.capturing():
|
||||
self._source_port.startPacketCaptureAnalyzer()
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_port.startPacketCaptureAnalyzer()
|
||||
PacketCapture.instance().startPacketCaptureAnalyzer(self._link)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Capture analyzer", "Cannot start the packet capture analyzer program: {}".format(e))
|
||||
|
||||
@@ -405,7 +358,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
# links must always be below node items on the scene
|
||||
if not self._adding_flag:
|
||||
min_zvalue = min([self._source_item.zValue(), self._destination_item.zValue()])
|
||||
self.setZValue(min_zvalue - 1)
|
||||
self.setZValue(min_zvalue - 0.5)
|
||||
|
||||
self.prepareGeometryChange()
|
||||
source_rect = self._source_item.boundingRect()
|
||||
@@ -443,3 +396,20 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
self.destination = scene_point
|
||||
self.adjust()
|
||||
self.update()
|
||||
|
||||
def _drawCaptureSymbol(self):
|
||||
"""
|
||||
Draws a capture symbol in the middle of the link to indicate a capture is active.
|
||||
"""
|
||||
|
||||
if not self._adding_flag:
|
||||
if self._link.capturing() and self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._capturing_item is None:
|
||||
self._capturing_item = SvgCaptureItem(':/icons/inspect.svg', self)
|
||||
self._capturing_item.setScale(0.6)
|
||||
self._capturing_item.setPos(link_center)
|
||||
if not self._capturing_item.isVisible():
|
||||
self._capturing_item.show()
|
||||
elif self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
|
||||
@@ -19,14 +19,18 @@
|
||||
Graphical representation of a node on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from .note_item import NoteItem
|
||||
from ..symbol import Symbol
|
||||
from ..controller import Controller
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NodeItem():
|
||||
class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
"""
|
||||
Node for the scene.
|
||||
@@ -37,23 +41,31 @@ class NodeItem():
|
||||
show_layer = False
|
||||
|
||||
def __init__(self, node):
|
||||
super().__init__()
|
||||
|
||||
# attached node
|
||||
self._node = node
|
||||
# link items connected to this node item.
|
||||
self._links = []
|
||||
self._symbol = None
|
||||
|
||||
# says if the attached node has been initialized
|
||||
# by the server.
|
||||
self._initialized = False
|
||||
|
||||
# node label
|
||||
self._node_label = None
|
||||
|
||||
# link items connected to this node item.
|
||||
self._links = []
|
||||
# Temporary symbol during loading
|
||||
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
|
||||
self.setZValue(self._node.z())
|
||||
renderer = QImageSvgRenderer(":/icons/reload.svg")
|
||||
renderer.setObjectName("symbol_loading")
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
effect = QtWidgets.QGraphicsColorizeEffect()
|
||||
effect.setColor(QtGui.QColor("black"))
|
||||
effect.setStrength(0.8)
|
||||
#effect = QtWidgets.QGraphicsDropShadowEffect()
|
||||
# effect.setColor(QtGui.QColor("darkGray"))
|
||||
# effect.setBlurRadius(0)
|
||||
#effect.setOffset(3, 3)
|
||||
self.setGraphicsEffect(effect)
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
@@ -63,7 +75,6 @@ class NodeItem():
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setZValue(1)
|
||||
|
||||
# connect signals to know about some events
|
||||
# e.g. when the node has been started, stopped or suspended etc.
|
||||
@@ -73,17 +84,12 @@ class NodeItem():
|
||||
node.suspended_signal.connect(self.suspendedSlot)
|
||||
node.updated_signal.connect(self.updatedSlot)
|
||||
node.deleted_signal.connect(self.deletedSlot)
|
||||
node.delete_links_signal.connect(self.deleteLinksSlot)
|
||||
node.error_signal.connect(self.errorSlot)
|
||||
node.server_error_signal.connect(self.serverErrorSlot)
|
||||
|
||||
# used when a port has been selected from the contextual menu
|
||||
self._selected_port = None
|
||||
|
||||
# says if the attached node has been initialized
|
||||
# by the server.
|
||||
self._initialized = False
|
||||
|
||||
# contains the last error message received
|
||||
# from the server.
|
||||
self._last_error = None
|
||||
@@ -92,14 +98,46 @@ class NodeItem():
|
||||
self._main_window = MainWindow.instance()
|
||||
self._settings = self._main_window.uiGraphicsView.settings()
|
||||
|
||||
def setUnsavedState(self):
|
||||
"""
|
||||
Indicates the project is in a unsaved state.
|
||||
"""
|
||||
if node.initialized():
|
||||
self.createdSlot(node.id())
|
||||
|
||||
from ..main_window import MainWindow
|
||||
main_window = MainWindow.instance()
|
||||
main_window.setUnsavedState()
|
||||
def _updateNode(self):
|
||||
"""
|
||||
Sync change to the node
|
||||
"""
|
||||
if self._initialized:
|
||||
self._node.setGraphics(self)
|
||||
|
||||
def setSymbol(self, symbol):
|
||||
"""
|
||||
:param symbol: Change the symbol path
|
||||
"""
|
||||
# create renderer using symbols path/resource
|
||||
if symbol is None:
|
||||
symbol = self._node.defaultSymbol()
|
||||
if self._symbol != symbol:
|
||||
self._symbol = symbol
|
||||
|
||||
# Temporary symbol during loading
|
||||
renderer = QImageSvgRenderer(":/icons/reload.svg")
|
||||
renderer.setObjectName("symbol_loading")
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
Controller.instance().getStatic(Symbol(symbol_id=symbol).url(), self._symbolLoadedCallback)
|
||||
|
||||
def symbol(self):
|
||||
return self._symbol
|
||||
|
||||
def _symbolLoadedCallback(self, path):
|
||||
renderer = QImageSvgRenderer(path)
|
||||
renderer.setObjectName(path)
|
||||
self.setSharedRenderer(renderer)
|
||||
if self._node.settings().get("symbol") != self._symbol:
|
||||
self._updateNode()
|
||||
if not self._initialized:
|
||||
self._showLabel()
|
||||
self._initialized = True
|
||||
self._updateNode()
|
||||
|
||||
def node(self):
|
||||
"""
|
||||
@@ -110,6 +148,11 @@ class NodeItem():
|
||||
|
||||
return self._node
|
||||
|
||||
def setPos(self, *args):
|
||||
super().setPos(*args)
|
||||
self._node.setSettingValue("x", int(self.x()))
|
||||
self._node.setSettingValue("y", int(self.y()))
|
||||
|
||||
def addLink(self, link):
|
||||
"""
|
||||
Adds a link items to this node item.
|
||||
@@ -119,7 +162,6 @@ class NodeItem():
|
||||
|
||||
self._links.append(link)
|
||||
self._node.updated_signal.emit()
|
||||
self.setUnsavedState()
|
||||
|
||||
def removeLink(self, link):
|
||||
"""
|
||||
@@ -130,7 +172,6 @@ class NodeItem():
|
||||
|
||||
if link in self._links:
|
||||
self._links.remove(link)
|
||||
self.setUnsavedState()
|
||||
|
||||
def links(self):
|
||||
"""
|
||||
@@ -141,17 +182,19 @@ class NodeItem():
|
||||
|
||||
return self._links
|
||||
|
||||
def createdSlot(self, node_id):
|
||||
def createdSlot(self, base_node_id):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a the node has been created/initialized.
|
||||
|
||||
:param node_id: node identifier (integer)
|
||||
:param base_node_id: base node identifier (integer)
|
||||
"""
|
||||
|
||||
self._initialized = True
|
||||
if self is None:
|
||||
return
|
||||
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
|
||||
self.setSymbol(self._node.symbol())
|
||||
self.update()
|
||||
self._showLabel()
|
||||
|
||||
def startedSlot(self):
|
||||
"""
|
||||
@@ -159,6 +202,8 @@ class NodeItem():
|
||||
when a the node has started.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
for link in self._links:
|
||||
link.update()
|
||||
|
||||
@@ -168,6 +213,8 @@ class NodeItem():
|
||||
when a the node has stopped.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
for link in self._links:
|
||||
link.update()
|
||||
|
||||
@@ -177,6 +224,8 @@ class NodeItem():
|
||||
when a the node has suspended.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
return
|
||||
for link in self._links:
|
||||
link.update()
|
||||
|
||||
@@ -186,57 +235,49 @@ class NodeItem():
|
||||
when a the node has been updated.
|
||||
"""
|
||||
|
||||
if self._node_label:
|
||||
if self._node_label.toPlainText() != self._node.name():
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
self._centerLabel()
|
||||
self.setUnsavedState()
|
||||
if self is None:
|
||||
return
|
||||
|
||||
self.setSymbol(self._node.settings().get("symbol"))
|
||||
self.setPos(self._node.settings().get("x", 0), self._node.settings().get("y", 0))
|
||||
self.setZValue(self._node.settings().get("z", 0))
|
||||
|
||||
self._updateLabel()
|
||||
|
||||
# update the link tooltips in case the
|
||||
# node name has changed
|
||||
for link in self._links:
|
||||
link.setCustomToolTip()
|
||||
|
||||
def deleteLinksSlot(self):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when a all the links must be deleted.
|
||||
"""
|
||||
|
||||
for link in self._links.copy():
|
||||
link.delete()
|
||||
|
||||
def deletedSlot(self):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when the node has been deleted.
|
||||
"""
|
||||
|
||||
if self is None:
|
||||
if self is None or not self.scene():
|
||||
return
|
||||
self._node.removeAllocatedName()
|
||||
if self in self.scene().items():
|
||||
self.scene().removeItem(self)
|
||||
self.setUnsavedState()
|
||||
|
||||
def serverErrorSlot(self, node_id, message):
|
||||
def serverErrorSlot(self, base_node_id, message):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when the node has received an error from the server.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: base node identifier
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
if self:
|
||||
self._last_error = "{message}".format(message=message)
|
||||
|
||||
def errorSlot(self, node_id, message):
|
||||
def errorSlot(self, base_node_id, message):
|
||||
"""
|
||||
Slot to receive events from the attached Node instance
|
||||
when the node wants to report an error.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param base_node_id: base node identifier
|
||||
:param message: error message
|
||||
"""
|
||||
|
||||
@@ -266,14 +307,11 @@ class NodeItem():
|
||||
|
||||
return self._node_label
|
||||
|
||||
def setLabel(self, label):
|
||||
def _labelUnselectedSlot(self):
|
||||
"""
|
||||
Sets the node label.
|
||||
|
||||
:param label: NoteItem instance.
|
||||
Called when user unselect the label
|
||||
"""
|
||||
|
||||
self._node_label = label
|
||||
self._updateNode()
|
||||
|
||||
def _centerLabel(self):
|
||||
"""
|
||||
@@ -287,6 +325,7 @@ class NodeItem():
|
||||
label_x_pos = node_middle.x() - text_middle.x()
|
||||
label_y_pos = -25
|
||||
self._node_label.setPos(label_x_pos, label_y_pos)
|
||||
return
|
||||
|
||||
def _showLabel(self):
|
||||
"""
|
||||
@@ -295,9 +334,29 @@ class NodeItem():
|
||||
|
||||
if not self._node_label:
|
||||
self._node_label = NoteItem(self)
|
||||
self._node_label.item_unselected_signal.connect(self._labelUnselectedSlot)
|
||||
self._node_label.setEditable(False)
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
self._updateLabel()
|
||||
self._node.setSettingValue("label", self._node_label.dump())
|
||||
|
||||
def _updateLabel(self):
|
||||
"""
|
||||
Update the label using the informations stored in the node
|
||||
"""
|
||||
if not self._node_label:
|
||||
return
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
label_data = self._node.settings().get("label")
|
||||
|
||||
if self._node_label.toPlainText() != label_data["text"]:
|
||||
self._node_label.setPlainText(label_data["text"])
|
||||
self._node_label.setStyle(label_data["style"])
|
||||
self._node_label.setRotation(label_data["rotation"])
|
||||
if label_data["x"] is None:
|
||||
self._centerLabel()
|
||||
self._updateNode()
|
||||
else:
|
||||
self._node_label.setPos(label_data["x"], label_data["y"])
|
||||
|
||||
def connectToPort(self, unavailable_ports=[]):
|
||||
"""
|
||||
@@ -328,7 +387,6 @@ class NodeItem():
|
||||
ports_dict[port.portNumber()] = port
|
||||
else:
|
||||
ports_dict[port.name()] = port
|
||||
|
||||
try:
|
||||
ports = sorted(ports_dict.keys(), key=int)
|
||||
except ValueError:
|
||||
@@ -375,20 +433,29 @@ class NodeItem():
|
||||
:param value: value of the change
|
||||
"""
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
GRID_SIZE = 75
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
tmp_x = (GRID_SIZE * round((self.x() + mid_x) / GRID_SIZE)) - mid_x
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
tmp_y = (GRID_SIZE * round((self.y() + mid_y) / GRID_SIZE)) - mid_y
|
||||
if tmp_x != self.x() and tmp_y != self.y():
|
||||
self.setPos(tmp_x, tmp_y)
|
||||
|
||||
# dynamically change the renderer when this node item is selected/unselected.
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if value:
|
||||
self.graphicsEffect().setEnabled(True)
|
||||
else:
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
self._updateNode()
|
||||
|
||||
# adjust link item positions when this node is moving or has changed.
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange or change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
|
||||
self.setUnsavedState()
|
||||
for link in self._links:
|
||||
link.adjust()
|
||||
|
||||
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -441,6 +508,7 @@ class NodeItem():
|
||||
self._node_label.setFlag(self.ItemIsMovable, True)
|
||||
for link in self._links:
|
||||
link.adjust()
|
||||
self._node.setSettingValue("z", int(value))
|
||||
|
||||
def hoverEnterEvent(self, event):
|
||||
"""
|
||||
@@ -462,3 +530,11 @@ class NodeItem():
|
||||
|
||||
if not self.isSelected():
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
def mouseRelease(self):
|
||||
"""
|
||||
Handle all mouse release for this item.
|
||||
It the item is select but mouse is not on it the event
|
||||
is send also
|
||||
"""
|
||||
self._updateNode()
|
||||
|
||||
@@ -20,6 +20,7 @@ Graphical representation of a note on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from .utils import colorFromSvg
|
||||
|
||||
|
||||
class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
@@ -28,6 +29,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
|
||||
:param parent: optional parent
|
||||
"""
|
||||
item_unselected_signal = QtCore.Signal()
|
||||
|
||||
show_layer = False
|
||||
|
||||
@@ -170,7 +172,7 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
painter.drawText(QtCore.QPointF(center.x(), center.y()), zval)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
@@ -187,6 +189,50 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def setStyle(self, styles):
|
||||
"""
|
||||
Set text style using a SVG style
|
||||
"""
|
||||
font = QtGui.QFont()
|
||||
for style in styles.split(";"):
|
||||
if ":" in style:
|
||||
key, val = style.split(":")
|
||||
key = key.strip()
|
||||
val = val.strip()
|
||||
|
||||
if key == "font-size":
|
||||
font.setPointSize(int(val))
|
||||
elif key == "font-family":
|
||||
font.setFamily(val)
|
||||
elif key == "font-style" and val == "italic":
|
||||
font.setItalic(True)
|
||||
elif key == "font-weight" and val == "bold":
|
||||
font.setBold(True)
|
||||
elif key == "fill":
|
||||
new_color = colorFromSvg(val)
|
||||
color = self.defaultTextColor()
|
||||
color.setBlue(new_color.blue())
|
||||
color.setRed(new_color.red())
|
||||
color.setGreen(new_color.green())
|
||||
self.setDefaultTextColor(color)
|
||||
elif key == "fill-opacity":
|
||||
color = self.defaultTextColor()
|
||||
color.setAlphaF(float(val))
|
||||
self.setDefaultTextColor(color)
|
||||
self.setFont(font)
|
||||
|
||||
def itemChange(self, change, value):
|
||||
"""
|
||||
Notifies this node item that some part of the item's state changes.
|
||||
|
||||
:param change: GraphicsItemChange type
|
||||
:param value: value of the change
|
||||
"""
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if value == 0:
|
||||
self.item_unselected_signal.emit()
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this note.
|
||||
@@ -195,63 +241,24 @@ class NoteItem(QtWidgets.QGraphicsTextItem):
|
||||
"""
|
||||
|
||||
note_info = {"text": self.toPlainText(),
|
||||
"x": self.x(),
|
||||
"y": self.y()}
|
||||
"x": int(self.x()),
|
||||
"y": int(self.y()),
|
||||
"rotation": int(self.rotation())}
|
||||
|
||||
note_info["font"] = self.font().toString()
|
||||
note_info["color"] = self.defaultTextColor().name(QtGui.QColor.HexArgb)
|
||||
if self.rotation() != 0:
|
||||
note_info["rotation"] = self.rotation()
|
||||
if self.zValue() != 2:
|
||||
note_info["z"] = self.zValue()
|
||||
style = ""
|
||||
|
||||
style += "font-family: {};".format(self.font().family())
|
||||
style += "font-size: {};".format(self.font().pointSize())
|
||||
|
||||
if self.font().italic():
|
||||
style += "font-style: italic;"
|
||||
|
||||
if self.font().bold():
|
||||
style += "font-weight: bold;"
|
||||
|
||||
style += "fill: {};".format("#" + hex(self.defaultTextColor().rgba())[4:])
|
||||
style += "fill-opacity: {};".format(self.defaultTextColor().alphaF())
|
||||
|
||||
note_info["style"] = style
|
||||
|
||||
return note_info
|
||||
|
||||
def load(self, note_info):
|
||||
"""
|
||||
Loads a note representation
|
||||
(from a topology file).
|
||||
|
||||
:param note_info: representation of the note (dictionary)
|
||||
"""
|
||||
|
||||
# load mandatory properties
|
||||
text = note_info["text"]
|
||||
x = note_info["x"]
|
||||
y = note_info["y"]
|
||||
|
||||
self.setPlainText(text)
|
||||
self.setPos(x, y)
|
||||
|
||||
# load optional properties
|
||||
font = note_info.get("font")
|
||||
color = note_info.get("color")
|
||||
rotation = note_info.get("rotation")
|
||||
z = note_info.get("z")
|
||||
|
||||
if font:
|
||||
qt_font = QtGui.QFont()
|
||||
if qt_font.fromString(font):
|
||||
self.setFont(qt_font)
|
||||
if color:
|
||||
self.setDefaultTextColor(QtGui.QColor(color))
|
||||
if rotation is not None:
|
||||
self.setRotation(float(rotation))
|
||||
if z is not None:
|
||||
self.setZValue(z)
|
||||
|
||||
def duplicate(self):
|
||||
"""
|
||||
Duplicates this node item.
|
||||
|
||||
:return: NoteItem instance
|
||||
"""
|
||||
|
||||
note_item = NoteItem(self.parent())
|
||||
note_item.setPlainText(self.toPlainText())
|
||||
note_item.setPos(self.x() + 20, self.y() + 20)
|
||||
note_item.setZValue(self.zValue())
|
||||
note_item.setFont(self.font())
|
||||
note_item.setDefaultTextColor(self.defaultTextColor())
|
||||
note_item.setRotation(self.rotation())
|
||||
return note_item
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a Pixmap image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtWidgets
|
||||
from .image_item import ImageItem
|
||||
|
||||
|
||||
class PixmapImageItem(ImageItem, QtWidgets.QGraphicsPixmapItem):
|
||||
|
||||
"""
|
||||
Class to insert an pixmap image on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, pixmap, image_path, pos=None):
|
||||
|
||||
QtWidgets.QGraphicsPixmapItem.__init__(self, pixmap)
|
||||
ImageItem.__init__(self, image_path, pos)
|
||||
self.setTransformationMode(QtCore.Qt.SmoothTransformation)
|
||||
|
||||
def duplicate(self):
|
||||
"""
|
||||
Duplicates this image item.
|
||||
|
||||
:return: PixmapImageItem instance
|
||||
"""
|
||||
|
||||
image_item = PixmapImageItem(self.pixmap(), self._image_path, QtCore.QPointF(self.x() + 20, self.y() + 20))
|
||||
image_item.setZValue(self.zValue())
|
||||
return image_item
|
||||
@@ -1,63 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a pixmap node on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtGui, QtWidgets
|
||||
from .node_item import NodeItem
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PixmapNodeItem(NodeItem, QtWidgets.QGraphicsPixmapItem):
|
||||
|
||||
"""
|
||||
Pixmap node for the scene.
|
||||
|
||||
:param node: Node instance
|
||||
:param pixmap_symbol: symbol for the node representation on the scene
|
||||
"""
|
||||
|
||||
def __init__(self, node, pixmap_symbol_path):
|
||||
|
||||
QtWidgets.QGraphicsPixmapItem.__init__(self)
|
||||
NodeItem.__init__(self, node)
|
||||
|
||||
self._pixmap_symbol_path = pixmap_symbol_path
|
||||
pixmap = QtGui.QPixmap(pixmap_symbol_path)
|
||||
self.setPixmap(pixmap)
|
||||
|
||||
def setPixmapSymbolPath(self, path):
|
||||
"""
|
||||
Sets the pixmap path
|
||||
|
||||
:param path: path to the Pixmap file.
|
||||
"""
|
||||
|
||||
self._pixmap_symbol_path = path
|
||||
|
||||
def pixmapSymbolPath(self):
|
||||
"""
|
||||
Returns the pixmap path
|
||||
|
||||
:returns: path to the Pixmap file.
|
||||
"""
|
||||
|
||||
return self._pixmap_symbol_path
|
||||
@@ -19,6 +19,8 @@
|
||||
Graphical representation of a rectangle on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .shape_item import ShapeItem
|
||||
|
||||
@@ -29,25 +31,8 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
Class to draw a rectangle on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, pos=None, width=200, height=100):
|
||||
|
||||
super().__init__()
|
||||
self.setRect(0, 0, width, height)
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.setPen(pen)
|
||||
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
|
||||
self.setBrush(brush)
|
||||
if pos:
|
||||
self.setPos(pos)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this rectangle.
|
||||
"""
|
||||
|
||||
self.scene().removeItem(self)
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeRectangle(self)
|
||||
def __init__(self, width=200, height=100, **kws):
|
||||
super().__init__(width=width, height=height, **kws)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
@@ -61,16 +46,19 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def duplicate(self):
|
||||
def toSvg(self):
|
||||
"""
|
||||
Duplicates this rectangle item.
|
||||
|
||||
:return: RectangleItem instance
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
svg = ET.Element("svg")
|
||||
svg.set("width", str(int(self.rect().width())))
|
||||
svg.set("height", str(int(self.rect().height())))
|
||||
|
||||
rect = ET.SubElement(svg, "rect")
|
||||
rect.set("width", str(int(self.rect().width())))
|
||||
rect.set("height", str(int(self.rect().height())))
|
||||
|
||||
rect = self._styleSvg(rect)
|
||||
|
||||
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
|
||||
rectangle_item = RectangleItem(QtCore.QPointF(self.x() + 20, self.y() + 20), self.rect().width(), self.rect().height())
|
||||
rectangle_item.setPen(self.pen())
|
||||
rectangle_item.setBrush(self.brush())
|
||||
rectangle_item.setZValue(self.zValue())
|
||||
rectangle_item.setRotation(self.rotation())
|
||||
return rectangle_item
|
||||
|
||||
@@ -79,8 +79,8 @@ class SerialLinkItem(LinkItem):
|
||||
scale_vect_diag = math.sqrt(scale_vect.x() ** 2 + scale_vect.y() ** 2)
|
||||
scale_coef = scale_vect_diag / 40.0
|
||||
|
||||
self.source = QtCore.QPointF(self.source.x() + scale_vect.x() / scale_coef, self.source.y() + scale_vect.y() / scale_coef)
|
||||
self.destination = QtCore.QPointF(self.destination.x() - scale_vect.x() / scale_coef, self.destination.y() - scale_vect.y() / scale_coef)
|
||||
self.source_point = QtCore.QPointF(self.source.x() + scale_vect.x() / scale_coef, self.source.y() + scale_vect.y() / scale_coef)
|
||||
self.destination_point = QtCore.QPointF(self.destination.x() - scale_vect.x() / scale_coef, self.destination.y() - scale_vect.y() / scale_coef)
|
||||
|
||||
def shape(self):
|
||||
"""
|
||||
@@ -91,9 +91,9 @@ class SerialLinkItem(LinkItem):
|
||||
|
||||
path = QtWidgets.QGraphicsPathItem.shape(self)
|
||||
offset = self._point_size / 2
|
||||
point = self.source
|
||||
point = self.source_point
|
||||
path.addEllipse(point.x() - offset, point.y() - offset, self._point_size, self._point_size)
|
||||
point = self.destination
|
||||
point = self.destination_point
|
||||
path.addEllipse(point.x() - offset, point.y() - offset, self._point_size, self._point_size)
|
||||
return path
|
||||
|
||||
@@ -117,65 +117,62 @@ class SerialLinkItem(LinkItem):
|
||||
# source point color
|
||||
if self._source_port.status() == Port.started:
|
||||
# port is active
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.green
|
||||
elif self._source_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.yellow
|
||||
else:
|
||||
shape = QtCore.Qt.SquareCap
|
||||
color = QtCore.Qt.red
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
|
||||
source_port_label = self._source_port.label()
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
source_port_name = self._source_port.name()
|
||||
source_port_label.setPlainText(source_port_name)
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, self.source))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if source_port_label is None:
|
||||
source_port_label = NoteItem(self._source_item)
|
||||
if not self._source_port.isStub():
|
||||
source_port_name = self._source_port.name().replace(self._source_port.longNameType(),
|
||||
self._source_port.shortNameType())
|
||||
else:
|
||||
source_port_name = self._source_port.name()
|
||||
source_port_label.setPlainText(source_port_name)
|
||||
source_port_label.setPos(self.mapToItem(self._source_item, self.source))
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
elif source_port_label and not source_port_label.isVisible():
|
||||
source_port_label.show()
|
||||
|
||||
elif source_port_label:
|
||||
source_port_label.show()
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.source)
|
||||
painter.drawPoint(self.source_point)
|
||||
|
||||
# destination point color
|
||||
if self._destination_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
|
||||
destination_port_label = self._destination_port.label()
|
||||
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
destination_port_name = self._destination_port.name()
|
||||
destination_port_label.setPlainText(destination_port_name)
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, self.destination))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
if destination_port_label is None:
|
||||
destination_port_label = NoteItem(self._destination_item)
|
||||
if not self._destination_port.isStub():
|
||||
destination_port_name = self._destination_port.name().replace(self._destination_port.longNameType(),
|
||||
self._destination_port.shortNameType())
|
||||
else:
|
||||
destination_port_name = self._destination_port.name()
|
||||
destination_port_label.setPlainText(destination_port_name)
|
||||
destination_port_label.setPos(self.mapToItem(self._destination_item, self.destination))
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
elif destination_port_label and not destination_port_label.isVisible():
|
||||
destination_port_label.show()
|
||||
|
||||
elif destination_port_label:
|
||||
destination_port_label.show()
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
painter.drawPoint(self.destination)
|
||||
painter.drawPoint(self.destination_point)
|
||||
|
||||
self._drawCaptureSymbol()
|
||||
|
||||
@@ -19,46 +19,49 @@
|
||||
Base class for shape items (Rectangle, ellipse etc.).
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
import xml.etree.ElementTree as ET
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from .drawing_item import DrawingItem
|
||||
from .utils import colorFromSvg
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShapeItem:
|
||||
class ShapeItem(DrawingItem):
|
||||
|
||||
# Map QT stroke to SVG style
|
||||
QT_DASH_TO_SVG = {
|
||||
QtCore.Qt.SolidLine: "",
|
||||
QtCore.Qt.NoPen: None,
|
||||
QtCore.Qt.DashLine: "25, 25",
|
||||
QtCore.Qt.DotLine: "5, 25",
|
||||
QtCore.Qt.DashDotLine: "5, 25, 25",
|
||||
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
Base class to draw shapes on the scene.
|
||||
"""
|
||||
|
||||
show_layer = False
|
||||
def __init__(self, width=200, height=200, svg=None, **kws):
|
||||
|
||||
def __init__(self, **kws):
|
||||
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable)
|
||||
super().__init__(svg=svg, **kws)
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._border = 5
|
||||
self._edge = None
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._graphics_view = MainWindow.instance().uiGraphicsView
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
if key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() > -360.0:
|
||||
self.setRotation(self.rotation() - 1)
|
||||
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
if svg is None:
|
||||
self.setRect(0, 0, width, height)
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.setPen(pen)
|
||||
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
|
||||
self.setBrush(brush)
|
||||
else:
|
||||
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
|
||||
self.fromSvg(svg)
|
||||
if self._id is None:
|
||||
self.create()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""
|
||||
@@ -178,128 +181,67 @@ class ShapeItem:
|
||||
if self.zValue() >= 0:
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def drawLayerInfo(self, painter):
|
||||
def _styleSvg(self, element):
|
||||
"""
|
||||
Draws the layer position.
|
||||
|
||||
:param painter: QPainter instance
|
||||
Add style from the shape item to the SVG element that we will
|
||||
export
|
||||
"""
|
||||
|
||||
if self.show_layer is False:
|
||||
return
|
||||
|
||||
brect = self.boundingRect()
|
||||
# don't draw anything if the object is too small
|
||||
if brect.width() < 20 or brect.height() < 20:
|
||||
return
|
||||
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
def setZValue(self, value):
|
||||
"""
|
||||
Sets a new Z value.
|
||||
|
||||
:param value: Z value
|
||||
"""
|
||||
|
||||
QtWidgets.QGraphicsItem.setZValue(self, value)
|
||||
if self.zValue() < 0:
|
||||
self.setFlag(self.ItemIsSelectable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(self.ItemIsSelectable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this shape item.
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
shape_info = {"width": self.rect().width(),
|
||||
"height": self.rect().height(),
|
||||
"x": self.x(),
|
||||
"y": self.y()}
|
||||
|
||||
brush = self.brush()
|
||||
if brush.color() != QtCore.Qt.white:
|
||||
shape_info["color"] = brush.color().name()
|
||||
if brush.color().alpha() != 255:
|
||||
shape_info["transparency"] = brush.color().alpha()
|
||||
|
||||
style = ""
|
||||
pen = self.pen()
|
||||
if pen.color() != QtCore.Qt.black:
|
||||
shape_info["border_color"] = pen.color().name()
|
||||
if pen.color().alpha() != 255:
|
||||
shape_info["border_transparency"] = pen.color().alpha()
|
||||
if pen.width() != 2:
|
||||
shape_info["border_width"] = pen.width()
|
||||
if pen.style() != QtCore.Qt.SolidLine:
|
||||
shape_info["border_style"] = pen.style()
|
||||
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
|
||||
element.set("fill-opacity", str(self.brush().color().alphaF()))
|
||||
|
||||
if self.rotation() != 0:
|
||||
shape_info["rotation"] = self.rotation()
|
||||
if self.zValue() != 0:
|
||||
shape_info["z"] = self.zValue()
|
||||
dasharray = self.QT_DASH_TO_SVG[pen.style()]
|
||||
if dasharray is None: # No border to the element
|
||||
return element
|
||||
elif dasharray == "":
|
||||
pass # Solid line
|
||||
else:
|
||||
element.set("stroke-dasharray", dasharray)
|
||||
element.set("stroke-width", str(pen.width()))
|
||||
element.set("stroke", "#" + hex(pen.color().rgba())[4:])
|
||||
return element
|
||||
|
||||
return shape_info
|
||||
|
||||
def load(self, shape_info):
|
||||
def fromSvg(self, svg):
|
||||
"""
|
||||
Loads a representation of this shape item.
|
||||
(from a topology file).
|
||||
|
||||
:param shape_info: representation of the shape item (dictionary)
|
||||
Import element informations from an SVG
|
||||
"""
|
||||
|
||||
# load mandatory properties
|
||||
width = shape_info["width"]
|
||||
height = shape_info["height"]
|
||||
x = shape_info["x"]
|
||||
y = shape_info["y"]
|
||||
svg = ET.fromstring(svg)
|
||||
width = float(svg.get("width", self.rect().width()))
|
||||
height = float(svg.get("height", self.rect().height()))
|
||||
self.setRect(0, 0, width, height)
|
||||
self.setPos(x, y)
|
||||
|
||||
# load optional properties
|
||||
z = shape_info.get("z")
|
||||
color = shape_info.get("color")
|
||||
transparency = shape_info.get("transparency")
|
||||
border_color = shape_info.get("border_color")
|
||||
border_transparency = shape_info.get("border_transparency")
|
||||
border_width = shape_info.get("border_width")
|
||||
border_style = shape_info.get("border_style")
|
||||
rotation = shape_info.get("rotation")
|
||||
pen = QtGui.QPen()
|
||||
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
|
||||
|
||||
if color:
|
||||
color = QtGui.QColor(color)
|
||||
else:
|
||||
color = QtGui.QColor(255, 255, 255)
|
||||
if transparency is not None:
|
||||
color.setAlpha(transparency)
|
||||
self.setBrush(QtGui.QBrush(color))
|
||||
if len(svg):
|
||||
if svg[0].get("stroke-width"):
|
||||
pen.setWidth(int(svg[0].get("stroke-width")))
|
||||
if svg[0].get("stroke"):
|
||||
pen.setColor(colorFromSvg(svg[0].get("stroke")))
|
||||
if svg[0].get("fill"):
|
||||
new_color = colorFromSvg(svg[0].get("fill"))
|
||||
color = brush.color()
|
||||
color.setBlue(new_color.blue())
|
||||
color.setRed(new_color.red())
|
||||
color.setGreen(new_color.green())
|
||||
brush.setColor(color)
|
||||
if svg[0].get("fill-opacity"):
|
||||
color = brush.color()
|
||||
color.setAlphaF(float(svg[0].get("fill-opacity")))
|
||||
brush.setColor(color)
|
||||
|
||||
# Map SVG stroke style (border of the element to the Qt version)
|
||||
if not svg[0].get("stroke"):
|
||||
pen.setStyle(QtCore.Qt.NoPen)
|
||||
else:
|
||||
pen.setStyle(QtCore.Qt.SolidLine)
|
||||
stroke = svg[0].get("stroke-dasharray")
|
||||
if stroke:
|
||||
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
|
||||
if svg_stroke == stroke:
|
||||
pen.setStyle(qt_stroke)
|
||||
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
if border_color:
|
||||
border_color = QtGui.QColor(border_color)
|
||||
else:
|
||||
border_color = pen.color()
|
||||
if border_transparency:
|
||||
border_color.setAlpha(border_transparency)
|
||||
pen.setColor(border_color)
|
||||
if border_width is not None:
|
||||
pen.setWidth(int(border_width))
|
||||
if border_style is not None:
|
||||
pen.setStyle(QtCore.Qt.PenStyle(border_style))
|
||||
self.setPen(pen)
|
||||
|
||||
if rotation is not None:
|
||||
self.setRotation(rotation)
|
||||
if z is not None:
|
||||
self.setZValue(z)
|
||||
self.setBrush(brush)
|
||||
self.update()
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a SVG image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtCore, QtSvg
|
||||
from .image_item import ImageItem
|
||||
|
||||
|
||||
class SvgImageItem(ImageItem, QtSvg.QGraphicsSvgItem):
|
||||
|
||||
"""
|
||||
Class to insert a SVG image on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, renderer, image_path, pos=None):
|
||||
|
||||
QtSvg.QGraphicsSvgItem.__init__(self)
|
||||
ImageItem.__init__(self, image_path, pos)
|
||||
self.setSharedRenderer(renderer)
|
||||
|
||||
def duplicate(self):
|
||||
"""
|
||||
Duplicates this image item.
|
||||
|
||||
:return: SvgImageItem instance
|
||||
"""
|
||||
|
||||
image_item = SvgImageItem(self.renderer(), self._image_path, QtCore.QPointF(self.x() + 20, self.y() + 20))
|
||||
image_item.setZValue(self.zValue())
|
||||
return image_item
|
||||
@@ -1,50 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a SVG node on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtSvg
|
||||
from .node_item import NodeItem
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SvgNodeItem(NodeItem, QtSvg.QGraphicsSvgItem):
|
||||
|
||||
"""
|
||||
SVG node for the scene.
|
||||
|
||||
:param node: Node instance
|
||||
:param symbol: symbol for the node representation on the scene
|
||||
"""
|
||||
|
||||
def __init__(self, node, symbol=None):
|
||||
|
||||
QtSvg.QGraphicsSvgItem.__init__(self)
|
||||
NodeItem.__init__(self, node)
|
||||
|
||||
# create renderer using symbols path/resource
|
||||
if symbol:
|
||||
renderer = QtSvg.QSvgRenderer(symbol)
|
||||
if symbol != node.defaultSymbol():
|
||||
renderer.setObjectName(symbol)
|
||||
else:
|
||||
renderer = QtSvg.QSvgRenderer(node.defaultSymbol())
|
||||
self.setSharedRenderer(renderer)
|
||||
175
gns3/items/text_item.py
Normal file
175
gns3/items/text_item.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a note on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from .drawing_item import DrawingItem
|
||||
from .utils import colorFromSvg
|
||||
|
||||
|
||||
class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
"""
|
||||
Text item for the QGraphicsView.
|
||||
"""
|
||||
def __init__(self, svg=None, **kws):
|
||||
|
||||
super().__init__(**kws)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
|
||||
main_window = MainWindow.instance()
|
||||
view_settings = main_window.uiGraphicsView.settings()
|
||||
qt_font = QtGui.QFont()
|
||||
qt_font.fromString(view_settings["default_label_font"])
|
||||
self.setDefaultTextColor(QtGui.QColor(view_settings["default_label_color"]))
|
||||
self.setFont(qt_font)
|
||||
|
||||
if svg:
|
||||
svg = self.fromSvg(svg)
|
||||
|
||||
if self._id is None:
|
||||
self.create()
|
||||
|
||||
def editText(self):
|
||||
"""
|
||||
Edit mode for this note.
|
||||
"""
|
||||
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
|
||||
self.setSelected(True)
|
||||
self.setFocus()
|
||||
cursor = self.textCursor()
|
||||
cursor.select(QtGui.QTextCursor.Document)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
"""
|
||||
Handles all mouse double click events.
|
||||
|
||||
:param event: QMouseEvent instance
|
||||
"""
|
||||
|
||||
self.editText()
|
||||
|
||||
def focusOutEvent(self, event):
|
||||
"""
|
||||
Handles all focus out events.
|
||||
|
||||
:param event: QFocusEvent instance
|
||||
"""
|
||||
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False)
|
||||
cursor = self.textCursor()
|
||||
if cursor.hasSelection():
|
||||
cursor.clearSelection()
|
||||
self.setTextCursor(cursor)
|
||||
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
|
||||
if not self.toPlainText():
|
||||
# delete the note if empty
|
||||
self.delete()
|
||||
return
|
||||
else:
|
||||
self.updateDrawing()
|
||||
return super().focusOutEvent(event)
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
Paints the contents of an item in local coordinates.
|
||||
|
||||
:param painter: QPainter instance
|
||||
:param option: QStyleOptionGraphicsItem instance
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def toSvg(self):
|
||||
"""
|
||||
Return an SVG version of the text
|
||||
"""
|
||||
svg = ET.Element("svg")
|
||||
svg.set("width", str(int(self.boundingRect().width())))
|
||||
svg.set("height", str(int(self.boundingRect().height())))
|
||||
|
||||
text = ET.SubElement(svg, "text")
|
||||
text.set("font-family", self.font().family())
|
||||
text.set("font-size", str(self.font().pointSize()))
|
||||
if self.font().italic():
|
||||
text.set("font-style", "italic")
|
||||
if self.font().bold():
|
||||
text.set("font-weight", "bold")
|
||||
text.set("fill", "#" + hex(self.defaultTextColor().rgba())[4:])
|
||||
text.set("fill-opacity", str(self.defaultTextColor().alphaF()))
|
||||
text.text = self.toPlainText()
|
||||
|
||||
svg = ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
return svg
|
||||
|
||||
def fromSvg(self, svg):
|
||||
svg = ET.fromstring(svg)
|
||||
text = svg[0]
|
||||
|
||||
font = QtGui.QFont()
|
||||
color = text.get("fill")
|
||||
if color:
|
||||
new_color = colorFromSvg(color)
|
||||
color = self.defaultTextColor()
|
||||
color.setBlue(new_color.blue())
|
||||
color.setRed(new_color.red())
|
||||
color.setGreen(new_color.green())
|
||||
self.setDefaultTextColor(color)
|
||||
|
||||
opacity = text.get("fill-opacity")
|
||||
if opacity:
|
||||
color = self.defaultTextColor()
|
||||
color.setAlphaF(float(opacity))
|
||||
self.setDefaultTextColor(color)
|
||||
|
||||
font.setPointSize(int(text.get("font-size", self.font().pointSize())))
|
||||
font.setFamily(text.get("font-family", self.font().family()))
|
||||
if text.get("font-style") == "italic":
|
||||
font.setItalic(True)
|
||||
if text.get("font-weight") == "bold":
|
||||
font.setBold(True)
|
||||
|
||||
self.setFont(font)
|
||||
self.setPlainText(text.text)
|
||||
|
||||
def editable(self):
|
||||
"""
|
||||
Returns either the note is editable or not.
|
||||
|
||||
:return: boolean
|
||||
"""
|
||||
|
||||
return True
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events
|
||||
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
if not self.handleKeyPressEvent(event):
|
||||
super().keyPressEvent(event)
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -15,13 +15,15 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Base class for NIOs (Network Input/Output).
|
||||
"""
|
||||
from ..qt import QtGui
|
||||
|
||||
|
||||
class NIO:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
pass
|
||||
def colorFromSvg(value):
|
||||
"""
|
||||
Transform a color coming from a SVG file to a Qcolor
|
||||
"""
|
||||
value = value.strip('#')
|
||||
if len(value) == 6: # If alpha channel is missing
|
||||
value = "ff" + value
|
||||
value = int(value, base=16)
|
||||
return QtGui.QColor.fromRgba(value)
|
||||
521
gns3/link.py
521
gns3/link.py
@@ -19,10 +19,14 @@
|
||||
Manages and stores everything needed for a connection between 2 devices.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
import tempfile
|
||||
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .controller import Controller
|
||||
|
||||
from .qt import QtCore
|
||||
from .nios.nio_udp import NIOUDP
|
||||
from .nios.nio_vmnet import NIOVMNET
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -37,17 +41,21 @@ class Link(QtCore.QObject):
|
||||
:param source_port: source Port instance
|
||||
:param destination_node: destination Node instance
|
||||
:param destination_port: destination Port instance
|
||||
:param stub: indicates if the link is connected to a stub device like a Cloud
|
||||
"""
|
||||
|
||||
# signals used to let the GUI view know about link
|
||||
# additions and deletions.
|
||||
add_link_signal = QtCore.Signal(int)
|
||||
delete_link_signal = QtCore.Signal(int)
|
||||
updated_link_signal = QtCore.Signal(int)
|
||||
error_link_signal = QtCore.Signal(int)
|
||||
|
||||
_instance_count = 1
|
||||
|
||||
def __init__(self, source_node, source_port, destination_node, destination_port):
|
||||
def __init__(self, source_node, source_port, destination_node, destination_port, link_id=None, **link_data):
|
||||
"""
|
||||
:param link_data: Link information from the API
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
@@ -64,56 +72,160 @@ class Link(QtCore.QObject):
|
||||
self._source_port = source_port
|
||||
self._destination_node = destination_node
|
||||
self._destination_port = destination_port
|
||||
self._source_nio = None
|
||||
self._destination_nio = None
|
||||
self._source_nio_active = False
|
||||
self._destination_nio_active = False
|
||||
self._source_label = None
|
||||
self._destination_label = None
|
||||
self._link_id = link_id
|
||||
self._capturing = False
|
||||
self._capture_file_path = None
|
||||
self._initialized = False
|
||||
|
||||
if source_port.isStub() or destination_port.isStub():
|
||||
self._stub = True
|
||||
# Boolean if True we are creatin the first instance of this node
|
||||
# if false the node already exist in the topology
|
||||
# use to avoid erasing informations when reloading
|
||||
self._creator = False
|
||||
|
||||
self._nodes = []
|
||||
|
||||
self._source_node.addLink(self)
|
||||
self._destination_node.addLink(self)
|
||||
|
||||
body = self._prepareParams()
|
||||
if self._link_id:
|
||||
link_data["link_id"] = self._link_id
|
||||
self._linkCreatedCallback(link_data)
|
||||
else:
|
||||
self._stub = False
|
||||
# we must request UDP information if the NIO is a NIO UDP and before
|
||||
# it can be created.
|
||||
if not self._stub:
|
||||
# connect signals used when a NIO has been created by a node
|
||||
# and this NIO need to be attached to a port connected to this link
|
||||
source_node.nio_signal.connect(self.newNIOSlot)
|
||||
destination_node.nio_signal.connect(self.newNIOSlot)
|
||||
self._link_id = str(uuid.uuid4())
|
||||
self._creator = True
|
||||
Controller.instance().post("/projects/{project_id}/links".format(project_id=source_node.project().id()), self._linkCreatedCallback, body=body)
|
||||
|
||||
# currently, we support only NIO_UDP and NIO_VMNET for normal connections (non-stub).
|
||||
if source_port.defaultNio() == NIOUDP:
|
||||
assert destination_port.defaultNio() == NIOUDP
|
||||
self._source_udp = None
|
||||
self._destination_udp = None
|
||||
def _parseResponse(self, result):
|
||||
self._capturing = result.get("capturing", False)
|
||||
|
||||
# connect signals used to receive a UDP port and host allocated by a node
|
||||
source_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
|
||||
destination_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
|
||||
|
||||
# request the UDP info for each node
|
||||
source_node.allocateUDPPort(self._source_port.id())
|
||||
destination_node.allocateUDPPort(self._destination_port.id())
|
||||
elif source_port.defaultNio() == NIOVMNET:
|
||||
assert destination_port.defaultNio() == NIOVMNET
|
||||
source_node.allocate_vmnet_nio_signal.connect(self.VMnetInterfaceAllocatedSlot)
|
||||
source_node.allocateVMnetInterface(self._source_port.id())
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
# If the controller is remote the capture path should be rewrite to something local
|
||||
if Controller.instance().isRemote():
|
||||
if self._capture_file_path is None and result.get("capture_file_path", None) is not None:
|
||||
(handle, self._capture_file_path) = tempfile.mkstemp()
|
||||
Controller.instance().get(
|
||||
"/projects/{project_id}/links/{link_id}/pcap".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id),
|
||||
None,
|
||||
showProgress=False,
|
||||
downloadProgressCallback=self._downloadPcapProgress,
|
||||
timeout=None)
|
||||
else:
|
||||
# handle stub connections (to a cloud for instance).
|
||||
if not source_port.isStub() and destination_port.isStub():
|
||||
source_node.nio_signal.connect(self.newNIOSlot)
|
||||
self._source_nio = self._destination_port.defaultNio()
|
||||
self._source_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._source_node.addNIO(self._source_port, self._source_nio)
|
||||
elif not destination_port.isStub() and source_port.isStub():
|
||||
destination_node.nio_signal.connect(self.newNIOSlot)
|
||||
self._destination_nio = self._source_port.defaultNio()
|
||||
self._destination_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._destination_node.addNIO(self._destination_port, self._destination_nio)
|
||||
self._capture_file_path = result["capture_file_path"]
|
||||
|
||||
if "nodes" in result:
|
||||
self._nodes = result["nodes"]
|
||||
self._updateLabels()
|
||||
self.updated_link_signal.emit(self._id)
|
||||
|
||||
def creator(self):
|
||||
return self._creator
|
||||
|
||||
def initialized(self):
|
||||
return self._initialized
|
||||
|
||||
def addPortLabel(self, port, label):
|
||||
if port.adapterNumber() == self._source_port.adapterNumber() and port.portNumber() == self._source_port.portNumber() and port.destinationNode() == self._destination_node:
|
||||
self._source_label = label
|
||||
else:
|
||||
self._destination_label = label
|
||||
label.item_unselected_signal.connect(self.update)
|
||||
if self.creator():
|
||||
self.update()
|
||||
else:
|
||||
self._updateLabels()
|
||||
|
||||
def update(self):
|
||||
if not self._link_id:
|
||||
return
|
||||
body = self._prepareParams()
|
||||
Controller.instance().put("/projects/{project_id}/links/{link_id}".format(project_id=self._source_node.project().id(), link_id=self._link_id), self.updateLinkCallback, body=body)
|
||||
|
||||
def updateLinkCallback(self, result, error=False, *args, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Update link", "Error while updating link: {}".format(result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
def _updateLabels(self):
|
||||
for node in self._nodes:
|
||||
if node["node_id"] == self._source_node.node_id() and node["adapter_number"] == self._source_port.adapterNumber() and node["port_number"] == self._source_port.portNumber():
|
||||
self._updateLabel(self._source_label, node["label"])
|
||||
elif node["node_id"] == self._destination_node.node_id() and node["adapter_number"] == self._destination_port.adapterNumber() and node["port_number"] == self._destination_port.portNumber():
|
||||
self._updateLabel(self._destination_label, node["label"])
|
||||
else:
|
||||
log.error("both ports are stub!")
|
||||
raise NotImplementedError
|
||||
|
||||
def _updateLabel(self, label, label_data):
|
||||
if not label:
|
||||
return
|
||||
label.setPlainText(label_data["text"])
|
||||
label.setPos(label_data["x"], label_data["y"])
|
||||
label.setStyle(label_data["style"])
|
||||
label.setRotation(label_data["rotation"])
|
||||
|
||||
def _prepareParams(self):
|
||||
body = {
|
||||
"nodes": [
|
||||
{
|
||||
"node_id": self._source_node.node_id(),
|
||||
"adapter_number": self._source_port.adapterNumber(),
|
||||
"port_number": self._source_port.portNumber(),
|
||||
},
|
||||
{
|
||||
"node_id": self._destination_node.node_id(),
|
||||
"adapter_number": self._destination_port.adapterNumber(),
|
||||
"port_number": self._destination_port.portNumber()
|
||||
}
|
||||
]
|
||||
}
|
||||
if self._source_port.label():
|
||||
body["nodes"][0]["label"] = self._source_port.label().dump()
|
||||
if self._destination_port.label():
|
||||
body["nodes"][1]["label"] = self._destination_port.label().dump()
|
||||
return body
|
||||
|
||||
def _linkCreatedCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Create link", "Error while creating link: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
self._initialized = True
|
||||
|
||||
# let the GUI know about this link has been created
|
||||
self.add_link_signal.emit(self._id)
|
||||
self._source_port.setLinkId(self._id)
|
||||
self._source_port.setLink(self)
|
||||
self._source_port.setDestinationNode(self._destination_node)
|
||||
self._source_port.setDestinationPort(self._destination_port)
|
||||
self._destination_port.setLinkId(self._id)
|
||||
self._destination_port.setLink(self)
|
||||
self._destination_port.setDestinationNode(self._source_node)
|
||||
self._destination_port.setDestinationPort(self._source_port)
|
||||
|
||||
self._link_id = result["link_id"]
|
||||
self._parseResponse(result)
|
||||
|
||||
def link_id(self):
|
||||
return self._link_id
|
||||
|
||||
def capturing(self):
|
||||
"""
|
||||
Is a capture running on the link?
|
||||
"""
|
||||
return self._capturing
|
||||
|
||||
def capture_file_path(self):
|
||||
"""
|
||||
Path of the capture file
|
||||
"""
|
||||
return self._capture_file_path
|
||||
|
||||
def project(self):
|
||||
return self._source_node.project()
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
@@ -130,7 +242,18 @@ class Link(QtCore.QObject):
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
|
||||
def deleteLink(self):
|
||||
def capture_file_name(self):
|
||||
"""
|
||||
:returns: File name for a capture on this link
|
||||
"""
|
||||
capture_file_name = "{}_{}_to_{}_{}".format(
|
||||
self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
return re.sub("[^0-9A-Za-z_-]", "", capture_file_name)
|
||||
|
||||
def deleteLink(self, skip_controller=False):
|
||||
"""
|
||||
Deletes this link.
|
||||
"""
|
||||
@@ -140,19 +263,93 @@ class Link(QtCore.QObject):
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name()))
|
||||
|
||||
# delete the NIOs on both source and destination nodes
|
||||
if self._source_port.nio():
|
||||
self._source_node.deleteNIO(self._source_port)
|
||||
if skip_controller:
|
||||
self._linkDeletedCallback({})
|
||||
else:
|
||||
Controller.instance().delete("/projects/{project_id}/links/{link_id}".format(project_id=self.project().id(),
|
||||
link_id=self._link_id), self._linkDeletedCallback)
|
||||
|
||||
def _linkDeletedCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Called after the link is remove from the topology
|
||||
"""
|
||||
if error:
|
||||
log.error("Error while deleting link: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
self._source_port.setFree()
|
||||
self._source_node.deleteLink(self)
|
||||
self._source_node.updated_signal.emit()
|
||||
if self._destination_port.nio():
|
||||
self._destination_node.deleteNIO(self._destination_port)
|
||||
self._destination_port.setFree()
|
||||
self._destination_node.deleteLink(self)
|
||||
self._destination_node.updated_signal.emit()
|
||||
|
||||
# let the GUI know about this link has been deleted
|
||||
self.delete_link_signal.emit(self._id)
|
||||
|
||||
def startCapture(self, data_link_type, capture_file_name):
|
||||
data = {
|
||||
"capture_file_name": capture_file_name,
|
||||
"data_link_type": data_link_type
|
||||
}
|
||||
Controller.instance().post(
|
||||
"/projects/{project_id}/links/{link_id}/start_capture".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id),
|
||||
self._startCaptureCallback,
|
||||
body=data)
|
||||
|
||||
def _startCaptureCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while starting capture on link: {}".format(result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
def _downloadPcapProgress(self, content, server=None, context={}, **kwargs):
|
||||
"""
|
||||
Called for each part of the file of the PCAP
|
||||
"""
|
||||
if not self._capture_file_path:
|
||||
return
|
||||
try:
|
||||
with open(self._capture_file_path, 'ab') as f:
|
||||
f.write(content)
|
||||
except OSError as e:
|
||||
log.error("Can't write file {}: {}".format(self._capture_file_path, e), True)
|
||||
return
|
||||
|
||||
def stopCapture(self):
|
||||
if Controller.instance().isRemote():
|
||||
if self._capture_file_path:
|
||||
try:
|
||||
os.remove(self._capture_file_path)
|
||||
except OSError as e:
|
||||
log.error("Can't remove file {}".format(self._capture_file_path))
|
||||
self._capture_file_path = None
|
||||
Controller.instance().post(
|
||||
"/projects/{project_id}/links/{link_id}/stop_capture".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id),
|
||||
self._stopCaptureCallback)
|
||||
|
||||
def _stopCaptureCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while stopping capture on link: {}".format(result["message"]))
|
||||
return
|
||||
self._parseResponse(result)
|
||||
|
||||
def get(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP Get from a link
|
||||
"""
|
||||
Controller.instance().get(
|
||||
"/projects/{project_id}/links/{link_id}{path}".format(
|
||||
project_id=self.project().id(),
|
||||
link_id=self._link_id,
|
||||
path=path),
|
||||
callback,
|
||||
**kwargs)
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
Returns this link identifier.
|
||||
@@ -198,214 +395,12 @@ class Link(QtCore.QObject):
|
||||
|
||||
return self._destination_port
|
||||
|
||||
def UDPPortAllocatedSlot(self, node_id, port_id, lport):
|
||||
def getNodePort(self, node):
|
||||
"""
|
||||
Slot to receive events from Node instances
|
||||
when a UDP port has been allocated in order to create a NIO UDP.
|
||||
Search the port in the link corresponding to this node
|
||||
|
||||
:param node_id: node identifier
|
||||
:param port_id: port identifier
|
||||
:param lport: local UDP port
|
||||
:returns: Node instance
|
||||
"""
|
||||
# check that the node is connected to this link as a source
|
||||
if node_id == self._source_node.id() and port_id == self._source_port.id():
|
||||
laddr = self._source_node.server().host()
|
||||
self._source_udp = (lport, laddr)
|
||||
# disconnect the signal has we don't expect new source UDP info for this link.
|
||||
self._source_node.allocate_udp_nio_signal.disconnect(self.UDPPortAllocatedSlot)
|
||||
|
||||
log.debug("{} has allocated UDP port {} for host {}".format(self._source_node.name(),
|
||||
lport,
|
||||
laddr))
|
||||
|
||||
# check that the node is connected to this link as a destination
|
||||
elif node_id == self._destination_node.id() and port_id == self._destination_port.id():
|
||||
laddr = self._destination_node.server().host()
|
||||
self._destination_udp = (lport, laddr)
|
||||
# disconnect the signal has we don't expect new source UDP info for this link.
|
||||
self._destination_node.allocate_udp_nio_signal.disconnect(self.UDPPortAllocatedSlot)
|
||||
|
||||
log.debug("{} has allocated UDP port {} for host {}".format(self._destination_node.name(),
|
||||
lport,
|
||||
laddr))
|
||||
|
||||
if self._source_udp and self._destination_udp:
|
||||
# we got UDP info from both source and destination nodes
|
||||
# meaning we can proceed with the creation of UDP NIOs
|
||||
lport, laddr = self._source_udp
|
||||
rport, raddr = self._destination_udp
|
||||
|
||||
self._source_nio = NIOUDP(lport, raddr, rport)
|
||||
self._destination_nio = NIOUDP(rport, laddr, lport)
|
||||
|
||||
self._source_udp = None
|
||||
self._destination_udp = None
|
||||
|
||||
log.debug("creating UDP tunnel from {}:{} to {}:{} ".format(laddr, lport, raddr, rport))
|
||||
|
||||
# add the UDP NIOs to the nodes
|
||||
self._source_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._source_node.addNIO(self._source_port, self._source_nio)
|
||||
self._destination_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._destination_node.addNIO(self._destination_port, self._destination_nio)
|
||||
|
||||
def VMnetInterfaceAllocatedSlot(self, node_id, port_id, vmnet):
|
||||
"""
|
||||
Slot to receive events from Node instances
|
||||
when a VMnet interface has been allocated in order to create a NIO VMNET.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param port_id: port identifier
|
||||
:param vmnet: vmnet interface name
|
||||
"""
|
||||
|
||||
# check that the node is connected to this link as a source
|
||||
# only the source is used to request the server for a vmnet interface
|
||||
# and then allocate a NIO VMNET to both the source and destination
|
||||
if node_id == self._source_node.id() and port_id == self._source_port.id():
|
||||
self._source_node.allocate_vmnet_nio_signal.disconnect(self.VMnetInterfaceAllocatedSlot)
|
||||
self._source_nio = NIOVMNET(vmnet)
|
||||
self._destination_nio = NIOVMNET(vmnet)
|
||||
|
||||
# add the VMnet NIOs to the nodes
|
||||
self._source_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._source_node.addNIO(self._source_port, self._source_nio)
|
||||
self._destination_node.nio_cancel_signal.connect(self.cancelNIOSlot)
|
||||
self._destination_node.addNIO(self._destination_port, self._destination_nio)
|
||||
|
||||
def newNIOSlot(self, node_id, port_id):
|
||||
"""
|
||||
Slot to receive events from Node instances
|
||||
when a NIO has been created on the server
|
||||
and are active.
|
||||
|
||||
:param node_id: node identifier
|
||||
:param port_id: port identifier
|
||||
"""
|
||||
|
||||
# in very rare cases link is already deleted
|
||||
if self is None:
|
||||
return
|
||||
|
||||
# check that the node is connected to this link as a source
|
||||
if node_id == self._source_node.id() and port_id == self._source_port.id():
|
||||
self._source_nio_active = True
|
||||
# disconnect the signal has we don't expect new source NIO for this link.
|
||||
self._source_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
|
||||
# check that the node is connected to this link as a destination
|
||||
elif node_id == self._destination_node.id() and port_id == self._destination_port.id():
|
||||
self._destination_nio_active = True
|
||||
# disconnect the signal has we don't expect new destination NIO for this link.
|
||||
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
|
||||
if not self._stub and self._source_nio_active and self._destination_nio_active:
|
||||
# both NIOs are active now.
|
||||
self._addToSourcePort(self._source_nio)
|
||||
self._addToDestinationPort(self._destination_nio)
|
||||
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self._source_nio_active = False
|
||||
self._destination_nio_active = False
|
||||
|
||||
# let the GUI know about this link has been created
|
||||
self.add_link_signal.emit(self._id)
|
||||
elif self._stub and self._source_nio_active:
|
||||
self._addToSourcePort(self._source_nio)
|
||||
# add the NIO to destination to show the port is not free.
|
||||
self._addToDestinationPort(self._source_nio)
|
||||
self._source_nio_active = False
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self.add_link_signal.emit(self._id)
|
||||
elif self._stub and self._destination_nio_active:
|
||||
# add the NIO to source to show the port is not free.
|
||||
self._addToSourcePort(self._destination_nio)
|
||||
self._addToDestinationPort(self._destination_nio)
|
||||
self._destination_nio_active = False
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self.add_link_signal.emit(self._id)
|
||||
|
||||
def _addToSourcePort(self, nio):
|
||||
"""
|
||||
Adds a NIO, a link id and a description to the source port.
|
||||
|
||||
:param nio: NIO instance
|
||||
"""
|
||||
|
||||
self._source_port.setNio(nio)
|
||||
self._source_port.setLinkId(self._id)
|
||||
self._source_port.setDestinationNode(self._destination_node)
|
||||
self._source_port.setDestinationPort(self._destination_port)
|
||||
|
||||
log.debug("{} attached to {} on port {}".format(nio,
|
||||
self._source_node.name(),
|
||||
self._source_port.name()))
|
||||
|
||||
def _addToDestinationPort(self, nio):
|
||||
"""
|
||||
Adds a NIO, a link id and a description to the destination port.
|
||||
|
||||
:param nio: NIO instance
|
||||
"""
|
||||
|
||||
self._destination_port.setNio(nio)
|
||||
self._destination_port.setLinkId(self._id)
|
||||
self._destination_port.setDestinationNode(self._source_node)
|
||||
self._destination_port.setDestinationPort(self._source_port)
|
||||
|
||||
log.debug("{} attached to {} on port {}".format(nio,
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name()))
|
||||
|
||||
def cancelNIOSlot(self, node_id):
|
||||
"""
|
||||
Slot to receive events from Node instances
|
||||
when a NIO has been canceled because of an
|
||||
error returned by the server.
|
||||
|
||||
:param node_id: node identifier
|
||||
"""
|
||||
|
||||
if not self._stub:
|
||||
try:
|
||||
# the destination node has canceled its NIO allocation
|
||||
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
except TypeError:
|
||||
# ignore TypeError: 'method' object is not connected
|
||||
pass
|
||||
|
||||
try:
|
||||
# the source node has canceled its NIO allocation
|
||||
self._source_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
except TypeError:
|
||||
# ignore TypeError: 'method' object is not connected
|
||||
pass
|
||||
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
else:
|
||||
if self._source_node.id() == node_id:
|
||||
self._source_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
self._source_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
else:
|
||||
self._destination_node.nio_signal.disconnect(self.newNIOSlot)
|
||||
self._destination_node.nio_cancel_signal.disconnect(self.cancelNIOSlot)
|
||||
|
||||
self._source_nio_active = False
|
||||
self._destination_nio_active = False
|
||||
self.deleteLink()
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this link.
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
return {"id": self.id(),
|
||||
"description": str(self),
|
||||
"source_node_id": self._source_node.id(),
|
||||
"source_port_id": self._source_port.id(),
|
||||
"destination_node_id": self._destination_node.id(),
|
||||
"destination_port_id": self._destination_port.id()}
|
||||
if self._destination_node == node:
|
||||
return self._destination_port
|
||||
return self._source_port
|
||||
|
||||
@@ -23,9 +23,10 @@ import copy
|
||||
|
||||
import psutil
|
||||
|
||||
from .qt import QtCore
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .version import __version__
|
||||
from .utils import parse_version
|
||||
from .controller import Controller
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -40,22 +41,31 @@ class LocalConfig(QtCore.QObject):
|
||||
config_changed_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, config_file=None):
|
||||
"""
|
||||
:param config_file: Path to the config file (override all other config, usefull for tests)
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self._profile = None
|
||||
self._config_file = config_file
|
||||
self._migrateOldConfigPath()
|
||||
self._resetLoadConfig()
|
||||
|
||||
def _resetLoadConfig(self):
|
||||
"""
|
||||
Reload the config from scratch everything is clean
|
||||
|
||||
"""
|
||||
self._settings = {}
|
||||
self._last_config_changed = None
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_gui.ini"
|
||||
else:
|
||||
filename = "gns3_gui.conf"
|
||||
|
||||
self._migrateOldConfigPath()
|
||||
|
||||
appname = "GNS3"
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
|
||||
# On windows, the system wide configuration file location is %COMMON_APPDATA%/GNS3/gns3_gui.conf
|
||||
common_appdata = os.path.expandvars("%COMMON_APPDATA%")
|
||||
system_wide_config_file = os.path.join(common_appdata, appname, filename)
|
||||
@@ -63,10 +73,8 @@ class LocalConfig(QtCore.QObject):
|
||||
# On UNIX-like platforms, the system wide configuration file location is /etc/xdg/GNS3/gns3_gui.conf
|
||||
system_wide_config_file = os.path.join("/etc/xdg", appname, filename)
|
||||
|
||||
if config_file:
|
||||
self._config_file = config_file
|
||||
else:
|
||||
self._config_file = os.path.join(LocalConfig.configDirectory(), filename)
|
||||
if not self._config_file:
|
||||
self._config_file = os.path.join(self.configDirectory(), filename)
|
||||
|
||||
# First load system wide settings
|
||||
if os.path.exists(system_wide_config_file):
|
||||
@@ -90,9 +98,48 @@ class LocalConfig(QtCore.QObject):
|
||||
self._settings.update(user_settings)
|
||||
self._migrateOldConfig()
|
||||
self._writeConfig()
|
||||
Controller.instance().connected_signal.connect(self.refreshConfigFromController)
|
||||
|
||||
@staticmethod
|
||||
def configDirectory():
|
||||
def profile(self):
|
||||
"""
|
||||
:returns: Current settings profile
|
||||
"""
|
||||
return self._profile
|
||||
|
||||
def setProfile(self, profile):
|
||||
previous_profile = self._profile
|
||||
if profile == "default":
|
||||
self._profile = None
|
||||
else:
|
||||
self._profile = profile
|
||||
if previous_profile != self._profile:
|
||||
self._config_file = None
|
||||
self._resetLoadConfig()
|
||||
|
||||
def refreshConfigFromController(self):
|
||||
"""
|
||||
Refresh the configuration from the controller
|
||||
"""
|
||||
controller = Controller.instance()
|
||||
if controller.connected():
|
||||
controller.get("/settings", self._getSettingsCallback)
|
||||
|
||||
def _getSettingsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Can't get settings from controller")
|
||||
return
|
||||
if result == {} and self._settings != {}:
|
||||
self._saveOnController()
|
||||
return
|
||||
|
||||
self._settings.update(result)
|
||||
# Update already loaded section
|
||||
for section in self._settings.keys():
|
||||
if isinstance(self._settings[section], dict):
|
||||
self.loadSectionSettings(section, self._settings[section])
|
||||
self.config_changed_signal.emit()
|
||||
|
||||
def configDirectory(self):
|
||||
"""
|
||||
Get the configuration directory
|
||||
"""
|
||||
@@ -102,6 +149,10 @@ class LocalConfig(QtCore.QObject):
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
path = os.path.join(home, ".config", "GNS3")
|
||||
|
||||
if self._profile is not None:
|
||||
path = os.path.join(path, "profiles", self._profile)
|
||||
|
||||
return os.path.normpath(path)
|
||||
|
||||
def _migrateOldConfigPath(self):
|
||||
@@ -109,7 +160,7 @@ class LocalConfig(QtCore.QObject):
|
||||
Migrate pre 1.4 config path
|
||||
"""
|
||||
|
||||
# In < 1.4 on Mac the config was in a gns3.net directory
|
||||
# In < 1.4 on Mac the config was in a gns3.net directory
|
||||
# We have move to same location as Linux
|
||||
if sys.platform.startswith("darwin"):
|
||||
old_path = os.path.join(os.path.expanduser("~"), ".config", "gns3.net")
|
||||
@@ -118,13 +169,23 @@ class LocalConfig(QtCore.QObject):
|
||||
try:
|
||||
shutil.copytree(old_path, new_path)
|
||||
except OSError as e:
|
||||
print("Can't copy the old config: %s", str(e))
|
||||
log.error("Can't copy the old config: %s", str(e))
|
||||
|
||||
def _migrateOldConfig(self):
|
||||
"""
|
||||
Migrate pre 1.4 config
|
||||
"""
|
||||
|
||||
# Display an error if settings come from a more recent version of GNS3
|
||||
# patch level version are compatible (ex 1.5.3 and 1.5.2). But if you open
|
||||
# settings from 1.6.1 with 1.5.1 you will have an error
|
||||
if "version" in self._settings:
|
||||
if parse_version(self._settings["version"])[:2] > parse_version(__version__)[:2]:
|
||||
app = QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
|
||||
QtWidgets.QMessageBox.critical(None, "Version error", "Your settings are for version {} of GNS3. You cannot use a previous version of GNS3 without risking losing data.".format(self._settings["version"]))
|
||||
# Exit immediately not clean but we want to avoid any side effect that could corrupt the file
|
||||
sys.exit(1)
|
||||
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.0alpha1"):
|
||||
|
||||
servers = self._settings.get("Servers", {})
|
||||
@@ -134,7 +195,7 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
# We migrate the server binary for OSX due to the change from py2app to CX freeze
|
||||
if servers["local_server"]["path"] == "/Applications/GNS3.app/Contents/Resources/server/Contents/MacOS/gns3server":
|
||||
servers["local_server"]["path"] = "/Applications/GNS3.app/Contents/MacOS/gns3server"
|
||||
servers["local_server"]["path"] = "gns3server"
|
||||
|
||||
if "RemoteServers" in self._settings:
|
||||
servers["remote_servers"] = copy.copy(self._settings["RemoteServers"])
|
||||
@@ -154,6 +215,16 @@ class LocalConfig(QtCore.QObject):
|
||||
if self._settings["MainWindow"]["telnet_console_command"] not in PRECONFIGURED_TELNET_CONSOLE_COMMANDS.values():
|
||||
self._settings["MainWindow"]["telnet_console_command"] = DEFAULT_TELNET_CONSOLE_COMMAND
|
||||
|
||||
# Migrate 1.X to 2.0
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
|
||||
if "Qemu" in self._settings:
|
||||
# The internet VM is replaced by the nat Node
|
||||
# we remove it from the list of available VM
|
||||
vms = []
|
||||
for vm in self._settings["Qemu"].get("vms", []):
|
||||
if vm.get("hda_disk_image") != "core-linux-6.4-internet-0.1.img":
|
||||
vms.append(vm)
|
||||
self._settings["Qemu"]["vms"] = vms
|
||||
|
||||
def _readConfig(self, config_path):
|
||||
"""
|
||||
@@ -191,6 +262,24 @@ class LocalConfig(QtCore.QObject):
|
||||
self._last_config_changed = os.stat(self._config_file).st_mtime
|
||||
except (ValueError, OSError) as e:
|
||||
log.error("Could not write the config file {}: {}".format(self._config_file, e))
|
||||
self._saveOnController()
|
||||
|
||||
def _saveOnController(self):
|
||||
"""
|
||||
Save some settings on controller for the transition from
|
||||
GUI to a central controller. Will be removed later
|
||||
"""
|
||||
if Controller.instance().connected():
|
||||
# We save only non user specific sections
|
||||
section_to_save_on_controller = ["Builtin", "Docker", "IOU", "Qemu", "VMware", "VPCS", "VirtualBox", "GraphicsView"]
|
||||
controller_settings = {}
|
||||
for key, val in self._settings.items():
|
||||
if key in section_to_save_on_controller:
|
||||
controller_settings[key] = val
|
||||
# We want only the VM settings on the server
|
||||
elif key == "Server":
|
||||
controller_settings["Server"]["vm"] = self._settings["Server"]["vm"]
|
||||
Controller.instance().post("/settings", None, body=controller_settings)
|
||||
|
||||
def checkConfigChanged(self):
|
||||
|
||||
@@ -219,7 +308,7 @@ class LocalConfig(QtCore.QObject):
|
||||
"""
|
||||
|
||||
self._config_file = config_file
|
||||
self._readConfig(self._config_file)
|
||||
self._resetLoadConfig()
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
@@ -240,6 +329,7 @@ class LocalConfig(QtCore.QObject):
|
||||
if self._settings != settings:
|
||||
self._settings.update(settings)
|
||||
self._writeConfig()
|
||||
self.config_changed_signal.emit()
|
||||
|
||||
def loadSectionSettings(self, section, default_settings):
|
||||
"""
|
||||
@@ -304,8 +394,22 @@ class LocalConfig(QtCore.QObject):
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["experimental_features"]
|
||||
|
||||
def multiProfiles(self):
|
||||
"""
|
||||
:returns: Boolean. True if multi_profiles is enabled
|
||||
"""
|
||||
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["multi_profiles"]
|
||||
|
||||
def setMultiProfiles(self, value):
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
settings = self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)
|
||||
settings["multi_profiles"] = value
|
||||
self.saveSectionSettings("MainWindow", settings)
|
||||
|
||||
@staticmethod
|
||||
def instance(config_file=None):
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of LocalConfig.
|
||||
|
||||
@@ -313,7 +417,7 @@ class LocalConfig(QtCore.QObject):
|
||||
"""
|
||||
|
||||
if not hasattr(LocalConfig, "_instance") or LocalConfig._instance is None:
|
||||
LocalConfig._instance = LocalConfig(config_file=config_file)
|
||||
LocalConfig._instance = LocalConfig()
|
||||
return LocalConfig._instance
|
||||
|
||||
@staticmethod
|
||||
@@ -323,7 +427,7 @@ class LocalConfig(QtCore.QObject):
|
||||
"""
|
||||
|
||||
my_pid = os.getpid()
|
||||
pid_path = os.path.join(LocalConfig.configDirectory(), "gns3_gui.pid")
|
||||
pid_path = os.path.join(LocalConfig.instance().configDirectory(), "gns3_gui.pid")
|
||||
|
||||
if os.path.exists(pid_path):
|
||||
try:
|
||||
|
||||
557
gns3/local_server.py
Normal file
557
gns3/local_server.py
Normal file
@@ -0,0 +1,557 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import copy
|
||||
import stat
|
||||
import shlex
|
||||
import socket
|
||||
import shutil
|
||||
import random
|
||||
import string
|
||||
import struct
|
||||
import psutil
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore
|
||||
from gns3.settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.utils.http import getSynchronous
|
||||
from gns3.utils.sudo import sudo
|
||||
from gns3.http_client import HTTPClient
|
||||
from gns3.controller import Controller
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StopLocalServerWorker(QtCore.QObject):
|
||||
"""
|
||||
Worker for displaying a progress dialog when closing
|
||||
the server
|
||||
"""
|
||||
# signals to update the progress dialog.
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
finished = QtCore.pyqtSignal()
|
||||
updated = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, local_server_process):
|
||||
super().__init__()
|
||||
self._local_server_process = local_server_process
|
||||
|
||||
def run(self):
|
||||
precision = 1
|
||||
remaining_trial = 4 / precision # 4 Seconds
|
||||
while remaining_trial > 0:
|
||||
if self._local_server_process.returncode is None:
|
||||
remaining_trial -= 1
|
||||
self.thread().sleep(precision)
|
||||
else:
|
||||
break
|
||||
self.finished.emit()
|
||||
|
||||
def cancel(self):
|
||||
return
|
||||
|
||||
|
||||
class LocalServer(QtCore.QObject):
|
||||
"""
|
||||
Manage the local server process
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
super().__init__()
|
||||
self._parent = parent
|
||||
self._local_server_path = ""
|
||||
self._local_server_process = None
|
||||
self._config_directory = LocalConfig.instance().configDirectory()
|
||||
self._settings = {}
|
||||
self.localServerSettings()
|
||||
self._port = self._settings.get("port", 3080)
|
||||
|
||||
if not self._settings.get("auto_start", True):
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
else:
|
||||
self._http_client = None
|
||||
|
||||
def _pid_path(self):
|
||||
"""
|
||||
:returns: Path of the PID file
|
||||
"""
|
||||
return os.path.join(self._config_directory, "gns3_server.pid")
|
||||
|
||||
def parent(self):
|
||||
"""
|
||||
Parent window
|
||||
"""
|
||||
if self._parent is None:
|
||||
from gns3.main_window import MainWindow
|
||||
return MainWindow.instance()
|
||||
return self._parent
|
||||
|
||||
def _checkWindowsService(self, service_name):
|
||||
import pywintypes
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
try:
|
||||
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
|
||||
return False
|
||||
except pywintypes.error as e:
|
||||
if e.winerror == 1060:
|
||||
return False
|
||||
else:
|
||||
log.error("Could not check if the {} service is running: {}".format(service_name, e.strerror))
|
||||
return True
|
||||
|
||||
def _checkUbridgePermissions(self):
|
||||
"""
|
||||
Checks that uBridge can interact with network interfaces.
|
||||
"""
|
||||
|
||||
path = os.path.abspath(self._settings["ubridge_path"])
|
||||
|
||||
if not path or len(path) == 0 or not os.path.exists(path):
|
||||
return False
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
# do not check anything on Windows
|
||||
return True
|
||||
|
||||
if os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return True
|
||||
|
||||
request_setuid = False
|
||||
if sys.platform.startswith("linux"):
|
||||
# test if the executable has the CAP_NET_RAW capability (Linux only)
|
||||
try:
|
||||
if "security.capability" in os.listxattr(path):
|
||||
caps = os.getxattr(path, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge?",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
sudo(["setcap", "cap_net_admin,cap_net_raw=ep"])
|
||||
else:
|
||||
# capabilities not supported
|
||||
request_setuid = True
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set CAP_NET_RAW capability to uBridge {}: {}".format(path, str(e)))
|
||||
return False
|
||||
|
||||
if sys.platform.startswith("darwin") or request_setuid:
|
||||
try:
|
||||
if os.stat(path).st_uid != 0 or not os.stat(path).st_mode & stat.S_ISUID:
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge?",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
sudo(["chmod", "4755", path])
|
||||
sudo(["chown", "root", path])
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set root permissions to uBridge {}: {}".format(path, str(e)))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _passwordGenerate(self):
|
||||
"""
|
||||
Generate a random password
|
||||
"""
|
||||
return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||
|
||||
def localServerSettings(self):
|
||||
"""
|
||||
Returns the local server settings.
|
||||
|
||||
:returns: local server settings (dict)
|
||||
"""
|
||||
|
||||
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
|
||||
self._settings = copy.copy(settings)
|
||||
|
||||
# user & password
|
||||
if settings["auth"] is True and not settings["user"].strip():
|
||||
settings["user"] = "admin"
|
||||
settings["password"] = self._passwordGenerate()
|
||||
|
||||
# local GNS3 server path
|
||||
local_server_path = shutil.which(settings["path"].strip())
|
||||
if local_server_path is None:
|
||||
default_server_path = shutil.which("gns3server")
|
||||
if default_server_path is not None:
|
||||
settings["path"] = os.path.abspath(default_server_path)
|
||||
else:
|
||||
settings["path"] = os.path.abspath(local_server_path)
|
||||
|
||||
# uBridge path
|
||||
ubridge_path = shutil.which(settings["ubridge_path"].strip())
|
||||
if ubridge_path is None:
|
||||
default_ubridge_path = shutil.which("ubridge")
|
||||
if default_ubridge_path is not None:
|
||||
settings["ubridge_path"] = os.path.abspath(default_ubridge_path)
|
||||
else:
|
||||
settings["ubridge_path"] = os.path.abspath(ubridge_path)
|
||||
|
||||
if self._settings != settings:
|
||||
self.updateLocalServerSettings(settings)
|
||||
return settings
|
||||
|
||||
def updateLocalServerSettings(self, new_settings):
|
||||
"""
|
||||
Update the local server settings. Keep the key not in new_settings
|
||||
"""
|
||||
old_settings = copy.copy(self._settings)
|
||||
if not self._settings:
|
||||
self._settings = new_settings
|
||||
else:
|
||||
self._settings.update(new_settings)
|
||||
self._port = self._settings["port"]
|
||||
LocalServerConfig.instance().saveSettings("Server", self._settings)
|
||||
|
||||
# Settings have changed we need to restart the server
|
||||
if old_settings != self._settings:
|
||||
if self._settings["auto_start"]:
|
||||
self.stopLocalServer(wait=True)
|
||||
self.localServerAutoStartIfRequire()
|
||||
# If the controller is remote:
|
||||
else:
|
||||
self.stopLocalServer(wait=True)
|
||||
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
|
||||
def shouldLocalServerAutoStart(self):
|
||||
"""
|
||||
Returns either the local server
|
||||
is automatically started on startup.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._settings["auto_start"]
|
||||
|
||||
def localServerPath(self):
|
||||
"""
|
||||
Returns the local server path.
|
||||
|
||||
:returns: path to local server program.
|
||||
"""
|
||||
|
||||
return self._settings["path"]
|
||||
|
||||
def _killAlreadyRunningServer(self):
|
||||
"""
|
||||
Kill a running zombie server (started by a gui that no longer exists)
|
||||
This will not kill server started by hand.
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(self._pid_path()):
|
||||
with open(self._pid_path()) as f:
|
||||
pid = int(f.read())
|
||||
process = psutil.Process(pid=pid)
|
||||
log.info("Kill already running server with PID %d", pid)
|
||||
process.kill()
|
||||
except (OSError, ValueError, psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
# Permission issue, or process no longer exists, or file is empty
|
||||
return
|
||||
|
||||
def localServerAutoStartIfRequire(self):
|
||||
"""
|
||||
Try to start the embed gns3 server.
|
||||
"""
|
||||
|
||||
if not self.shouldLocalServerAutoStart():
|
||||
return
|
||||
|
||||
# We check if two gui are not launched at the same time
|
||||
# to avoid killing the server of the other GUI
|
||||
if not LocalConfig.isMainGui():
|
||||
log.info("Not the main GUI, will not auto start the server")
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
return True
|
||||
|
||||
if self.isLocalServerRunning():
|
||||
log.info("A local server already running on this host")
|
||||
# Try to kill the server. The server can be still running after
|
||||
# if the server was started by hand
|
||||
self._killAlreadyRunningServer()
|
||||
|
||||
if not self.isLocalServerRunning():
|
||||
if not self.initLocalServer():
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not start the local server process: {}".format(self._settings["path"]))
|
||||
return False
|
||||
if not self.startLocalServer():
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not start the local server process: {}".format(self._settings["path"]))
|
||||
return False
|
||||
|
||||
if self.parent():
|
||||
worker = WaitForConnectionWorker(self._settings["host"], self._port)
|
||||
progress_dialog = ProgressDialog(worker,
|
||||
"Local server",
|
||||
"Connecting to server {} on port {}...".format(self._settings["host"], self._port),
|
||||
"Cancel", busy=True, parent=self.parent())
|
||||
progress_dialog.show()
|
||||
if not progress_dialog.exec_():
|
||||
return False
|
||||
|
||||
self._http_client = HTTPClient(self._settings)
|
||||
Controller.instance().setHttpClient(self._http_client)
|
||||
|
||||
return True
|
||||
|
||||
def initLocalServer(self):
|
||||
"""
|
||||
Initialize the local server.
|
||||
"""
|
||||
|
||||
self._checkUbridgePermissions()
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Error", "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
|
||||
return
|
||||
|
||||
self._port = self._settings["port"]
|
||||
|
||||
# check the local server path
|
||||
local_server_path = self.localServerPath()
|
||||
if not local_server_path:
|
||||
log.warn("No local server is configured")
|
||||
return
|
||||
if not os.path.isfile(local_server_path):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not find local server {}".format(local_server_path))
|
||||
return
|
||||
elif not os.access(local_server_path, os.X_OK):
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "{} is not an executable".format(local_server_path))
|
||||
return
|
||||
|
||||
try:
|
||||
# check if the local address still exists
|
||||
for res in socket.getaddrinfo(self._settings["host"], 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
|
||||
af, socktype, proto, _, sa = res
|
||||
with socket.socket(af, socktype, proto) as sock:
|
||||
sock.bind(sa)
|
||||
break
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not bind with {}: {} (please check your host binding setting in the preferences)".format(self._settings["host"], e))
|
||||
return False
|
||||
|
||||
try:
|
||||
# check if the port is already taken
|
||||
find_unused_port = False
|
||||
for res in socket.getaddrinfo(self._settings["host"], self._port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
|
||||
af, socktype, proto, _, sa = res
|
||||
with socket.socket(af, socktype, proto) as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(sa)
|
||||
break
|
||||
except OSError as e:
|
||||
log.warning("Could not use socket {}:{} {}".format(self._settings["host"], self._port, e))
|
||||
find_unused_port = True
|
||||
|
||||
if find_unused_port:
|
||||
# find an alternate port for the local server
|
||||
old_port = self._port
|
||||
try:
|
||||
self._port = self._findUnusedLocalPort(self._settings["host"])
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not find an unused port for the local server: {}".format(e))
|
||||
return False
|
||||
log.warning("The server port {} is already in use, fallback to port {}".format(old_port, self._port))
|
||||
return True
|
||||
|
||||
def _findUnusedLocalPort(self, host):
|
||||
"""
|
||||
Find an unused port.
|
||||
|
||||
:param host: server hosts
|
||||
|
||||
:returns: port number
|
||||
"""
|
||||
|
||||
with socket.socket() as s:
|
||||
s.bind((host, 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
def startLocalServer(self):
|
||||
"""
|
||||
Starts the local server process.
|
||||
"""
|
||||
|
||||
path = self.localServerPath()
|
||||
command = '"{executable}" --local'.format(executable=path)
|
||||
|
||||
if LocalConfig.instance().profile():
|
||||
command += " --profile {}".format(LocalConfig.instance().profile())
|
||||
|
||||
if self._settings["allow_console_from_anywhere"]:
|
||||
# allow connections to console from remote addresses
|
||||
command += " --allow"
|
||||
|
||||
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
||||
command += " --debug"
|
||||
|
||||
settings_dir = self._config_directory
|
||||
if os.path.isdir(settings_dir):
|
||||
# save server logging info to a file in the settings directory
|
||||
logpath = os.path.join(settings_dir, "gns3_server.log")
|
||||
if os.path.isfile(logpath):
|
||||
# delete the previous log file
|
||||
try:
|
||||
os.remove(logpath)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as e:
|
||||
log.warn("could not delete server log file {}: {}".format(logpath, e))
|
||||
command += ' --log="{}" --pid="{}"'.format(logpath, self._pid_path())
|
||||
|
||||
log.info("Starting local server process with {}".format(command))
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
# use the string on Windows
|
||||
self._local_server_process = subprocess.Popen(command, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
|
||||
else:
|
||||
# use arguments on other platforms
|
||||
args = shlex.split(command)
|
||||
self._local_server_process = subprocess.Popen(args)
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.warning('Could not start local server "{}": {}'.format(command, e))
|
||||
return False
|
||||
|
||||
log.info("Local server process has started (PID={})".format(self._local_server_process.pid))
|
||||
return True
|
||||
|
||||
def localServerProcessIsRunning(self):
|
||||
"""
|
||||
Returns either the local server is running.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
try:
|
||||
if self._local_server_process and self._local_server_process.poll() is None:
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def isLocalServerRunning(self):
|
||||
"""
|
||||
Synchronous check if a server is already running on this host.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
status, json_data = getSynchronous(self._settings["host"], self._port, "version",
|
||||
timeout=2, user=self._settings["user"], password=self._settings["password"])
|
||||
|
||||
if json_data is None or status != 200:
|
||||
return False
|
||||
else:
|
||||
version = json_data.get("version", None)
|
||||
if version is None:
|
||||
log.debug("Server is not a GNS3 server")
|
||||
return False
|
||||
return True
|
||||
|
||||
def stopLocalServer(self, wait=False):
|
||||
"""
|
||||
Stops the local server.
|
||||
|
||||
:param wait: wait for the server to stop
|
||||
"""
|
||||
|
||||
if self.localServerProcessIsRunning():
|
||||
log.info("Stopping local server (PID={})".format(self._local_server_process.pid))
|
||||
# local server is running, let's stop it
|
||||
if self._http_client:
|
||||
self._http_client.shutdown()
|
||||
if wait:
|
||||
worker = StopLocalServerWorker(self._local_server_process)
|
||||
progress_dialog = ProgressDialog(worker, "Local server", "Waiting for the local server to stop...", None, busy=True, parent=self.parent())
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
if self._local_server_process.returncode is None:
|
||||
self._killLocalServer()
|
||||
|
||||
def _killLocalServer(self):
|
||||
# the local server couldn't be stopped with the normal procedure
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
self._local_server_process.send_signal(signal.CTRL_BREAK_EVENT)
|
||||
else:
|
||||
self._local_server_process.send_signal(signal.SIGINT)
|
||||
# If the process is already dead we received a permission error
|
||||
# it's a race condition between the timeout and send signal
|
||||
except PermissionError:
|
||||
pass
|
||||
try:
|
||||
# wait for the server to stop for maximum 2 seconds
|
||||
self._local_server_process.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
proceed = QtWidgets.QMessageBox.question(self.parent(),
|
||||
"Local server",
|
||||
"The Local server cannot be stopped, would you like to kill it?",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
self._local_server_process.kill()
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of LocalServer.
|
||||
:returns: instance of LocalServer
|
||||
"""
|
||||
|
||||
if not hasattr(LocalServer, '_instance') or LocalServer._instance is None:
|
||||
LocalServer._instance = LocalServer()
|
||||
return LocalServer._instance
|
||||
|
||||
|
||||
def main():
|
||||
import pprint
|
||||
|
||||
pp = pprint.PrettyPrinter(indent=4)
|
||||
print("Local server config")
|
||||
local_server = LocalServer(False)
|
||||
pp.pprint(local_server.localServerSettings())
|
||||
local_server.localServerAutoStart()
|
||||
local_server.stopLocalServer()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -30,22 +30,25 @@ class LocalServerConfig:
|
||||
Local server configuration.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, config_file=None):
|
||||
|
||||
appname = "GNS3"
|
||||
|
||||
self._config = configparser.RawConfigParser()
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_server.ini"
|
||||
else:
|
||||
filename = "gns3_server.conf"
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
self._config_file = os.path.join(appdata, appname, filename)
|
||||
if config_file:
|
||||
self._config_file = config_file
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
self._config_file = os.path.join(home, ".config", appname, filename)
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_server.ini"
|
||||
else:
|
||||
filename = "gns3_server.conf"
|
||||
|
||||
from .local_config import LocalConfig
|
||||
if sys.platform.startswith("win"):
|
||||
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
|
||||
else:
|
||||
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
|
||||
|
||||
try:
|
||||
# create the config file if it doesn't exist
|
||||
@@ -54,6 +57,14 @@ class LocalServerConfig:
|
||||
log.error("Could not create the local server configuration {}: {}".format(self._config_file, e))
|
||||
self.readConfig()
|
||||
|
||||
def setConfigFile(self, path):
|
||||
"""
|
||||
Change the location of the server config (use for test)
|
||||
"""
|
||||
self._config = configparser.RawConfigParser()
|
||||
self._config_file = path
|
||||
self.readConfig()
|
||||
|
||||
def readConfig(self):
|
||||
"""
|
||||
Read the configuration file.
|
||||
@@ -76,13 +87,12 @@ class LocalServerConfig:
|
||||
except (OSError, configparser.Error) as e:
|
||||
log.error("Could not write the local server configuration {}: {}".format(self._config_file, e))
|
||||
|
||||
def loadSettings(self, section, default_settings, types):
|
||||
def loadSettings(self, section, default_settings):
|
||||
"""
|
||||
Get all the settings from a given section.
|
||||
|
||||
:param section: section name
|
||||
:param default_settings: setting names and default values (dict)
|
||||
:param types: setting types (dict)
|
||||
|
||||
:returns: settings (dict)
|
||||
"""
|
||||
@@ -92,11 +102,11 @@ class LocalServerConfig:
|
||||
|
||||
settings = {}
|
||||
for name, default in default_settings.items():
|
||||
if types[name] is int:
|
||||
settings[name] = self._config[section].getint(name, default)
|
||||
elif types[name] is bool:
|
||||
if isinstance(default, bool):
|
||||
settings[name] = self._config[section].getboolean(name, default)
|
||||
elif types[name] is float:
|
||||
elif isinstance(default, int):
|
||||
settings[name] = self._config[section].getint(name, default)
|
||||
elif isinstance(default, float):
|
||||
settings[name] = self._config[section].getfloat(name, default)
|
||||
else:
|
||||
settings[name] = self._config[section].get(name, default)
|
||||
|
||||
@@ -75,7 +75,7 @@ class ColouredStreamHandler(logging.StreamHandler):
|
||||
stream.write(msg)
|
||||
stream.write(self.terminator)
|
||||
self.flush()
|
||||
# On OSX when frozen flush raise a BrokenPipeError
|
||||
# On OSX when frozen flush raise a BrokenPipeError
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
except Exception:
|
||||
|
||||
76
gns3/main.py
76
gns3/main.py
@@ -45,10 +45,10 @@ import time
|
||||
import locale
|
||||
import argparse
|
||||
import signal
|
||||
import re
|
||||
import psutil
|
||||
|
||||
try:
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, DEFAULT_BINDING
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets
|
||||
except ImportError:
|
||||
raise SystemExit("Can't import Qt modules: Qt and/or PyQt is probably not installed correctly...")
|
||||
from gns3.main_window import MainWindow
|
||||
@@ -57,6 +57,8 @@ from gns3.logger import init_logger
|
||||
from gns3.crash_report import CrashReport
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.application import Application
|
||||
from gns3.utils import parse_version
|
||||
from gns3.dialogs.profile_select import ProfileSelectDialog
|
||||
|
||||
|
||||
import logging
|
||||
@@ -119,16 +121,12 @@ def main():
|
||||
parser.add_argument("--version", help="show the version", action="version", version=__version__)
|
||||
parser.add_argument("--debug", help="print out debug messages", action="store_true", default=False)
|
||||
parser.add_argument("--config", help="Configuration file")
|
||||
parser.add_argument("--profile", help="Settings profile (blank will use default settings files)")
|
||||
options = parser.parse_args()
|
||||
exception_file_path = "exceptions.log"
|
||||
|
||||
if options.config:
|
||||
LocalConfig.instance(config_file=options.config)
|
||||
else:
|
||||
LocalConfig.instance()
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
# We add to the path where the OS search executable our binary location starting by GNS3
|
||||
# We add to the path where the OS search executable our binary location starting by GNS3
|
||||
# packaged binary
|
||||
frozen_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||
if sys.platform.startswith("darwin"):
|
||||
@@ -188,18 +186,10 @@ def main():
|
||||
if sys.version_info < (3, 4):
|
||||
raise SystemExit("Python 3.4 or higher is required")
|
||||
|
||||
def version(version_string):
|
||||
return [int(i) for i in re.split(r'[^0-9]', version_string)]
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.0.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.0.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
|
||||
# 4.8.3 because of QSettings (http://pyqt.sourceforge.net/Docs/PyQt4/pyqt_qsettings.html)
|
||||
if DEFAULT_BINDING == "PyQt4" and version(QtCore.BINDING_VERSION_STR) < version("4.8.3"):
|
||||
raise SystemExit("Requirement is PyQt version 4.8.3 or higher, got version {}".format(QtCore.BINDING_VERSION_STR))
|
||||
|
||||
if DEFAULT_BINDING == "PyQt5" and version(QtCore.BINDING_VERSION_STR) < version("5.0.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.0.0 or higher, got version {}".format(QtCore.BINDING_VERSION_STR))
|
||||
|
||||
import psutil
|
||||
if version(psutil.__version__) < version("2.2.1"):
|
||||
if parse_version(psutil.__version__) < parse_version("2.2.1"):
|
||||
raise SystemExit("Requirement is psutil version 2.2.1 or higher, got version {}".format(psutil.__version__))
|
||||
|
||||
# check for the correct locale
|
||||
@@ -235,8 +225,22 @@ def main():
|
||||
global app
|
||||
app = Application(sys.argv)
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
if local_config.multiProfiles():
|
||||
profile_select = ProfileSelectDialog()
|
||||
profile_select.show()
|
||||
profile_select.exec_()
|
||||
options.profile = profile_select.profile()
|
||||
|
||||
# Init the config
|
||||
if options.config:
|
||||
local_config.setConfigFilePath(options.config)
|
||||
elif options.profile:
|
||||
local_config.setProfile(options.profile)
|
||||
profile = options.profile
|
||||
|
||||
# save client logging info to a file
|
||||
logfile = os.path.join(LocalConfig.configDirectory(), "gns3_gui.log")
|
||||
logfile = os.path.join(LocalConfig.instance().configDirectory(), "gns3_gui.log")
|
||||
|
||||
# on debug enable logging to stdout
|
||||
if options.debug:
|
||||
@@ -245,33 +249,47 @@ def main():
|
||||
root_logger = init_logger(logging.INFO, logfile)
|
||||
|
||||
# update the exception file path to have it in the same directory as the settings file.
|
||||
exception_file_path = os.path.join(LocalConfig.configDirectory(), exception_file_path)
|
||||
exception_file_path = os.path.join(LocalConfig.instance().configDirectory(), exception_file_path)
|
||||
|
||||
# We disallow to run GNS3 from outside the /Applications folder to avoid
|
||||
# issue when people run GNS3 from the .dmg
|
||||
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
|
||||
if not os.path.realpath(sys.executable).startswith("/Applications"):
|
||||
QtWidgets.QMessageBox.critical(None, "Error", "You need to copy GNS3 in your /Applications folder before using it.")
|
||||
sys.exit(1)
|
||||
|
||||
global mainwindow
|
||||
mainwindow = MainWindow()
|
||||
startup_file = app.open_file_at_startup
|
||||
if not startup_file:
|
||||
startup_file = options.project
|
||||
|
||||
mainwindow = MainWindow(open_file=startup_file)
|
||||
|
||||
# On OSX we can receive the file to open from a system event
|
||||
# loadPath is smart and will load only if a path is present
|
||||
mainwindow.ready_signal.connect(lambda: mainwindow.loadPath(app.open_file_at_startup))
|
||||
mainwindow.ready_signal.connect(lambda: mainwindow.loadPath(options.project))
|
||||
# loadPath is smart and will load only if a path is present
|
||||
app.file_open_signal.connect(lambda path: mainwindow.loadPath(path))
|
||||
|
||||
# Manage Ctrl + C or kill command
|
||||
def sigint_handler(*args):
|
||||
log.info("Signal received exiting the application")
|
||||
mainwindow.setSoftExit(False)
|
||||
app.closeAllWindows()
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
signal.signal(signal.SIGTERM, sigint_handler)
|
||||
orig_sigint = signal.signal(signal.SIGINT, sigint_handler)
|
||||
orig_sigterm = signal.signal(signal.SIGTERM, sigint_handler)
|
||||
|
||||
mainwindow.show()
|
||||
|
||||
exit_code = app.exec_()
|
||||
|
||||
signal.signal(signal.SIGINT, orig_sigint)
|
||||
signal.signal(signal.SIGTERM, orig_sigterm)
|
||||
|
||||
delattr(MainWindow, "_instance")
|
||||
|
||||
# We force deleting the app object otherwise it's segfault on Fedora
|
||||
del app
|
||||
# We force a full garbage collect before exit
|
||||
# for unknow reason otherwise Qt Segfault on OSX in some
|
||||
# conditions
|
||||
# conditions
|
||||
import gc
|
||||
gc.collect()
|
||||
|
||||
|
||||
1416
gns3/main_window.py
1416
gns3/main_window.py
File diff suppressed because it is too large
Load Diff
@@ -22,5 +22,6 @@ from gns3.modules.vpcs import VPCS
|
||||
from gns3.modules.virtualbox import VirtualBox
|
||||
from gns3.modules.qemu import Qemu
|
||||
from gns3.modules.vmware import VMware
|
||||
from gns3.modules.docker import Docker
|
||||
|
||||
MODULES = [VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Builtin]
|
||||
MODULES = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker]
|
||||
|
||||
@@ -20,10 +20,23 @@ Built-in module implementation.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
|
||||
from ..module import Module
|
||||
from .cloud import Cloud
|
||||
from .host import Host
|
||||
from .nat import Nat
|
||||
from .ethernet_hub import EthernetHub
|
||||
from .ethernet_switch import EthernetSwitch
|
||||
from .frame_relay_switch import FrameRelaySwitch
|
||||
from .atm_switch import ATMSwitch
|
||||
|
||||
from .settings import (
|
||||
BUILTIN_SETTINGS,
|
||||
CLOUD_SETTINGS,
|
||||
NAT_SETTINGS,
|
||||
ETHERNET_HUB_SETTINGS,
|
||||
ETHERNET_SWITCH_SETTINGS
|
||||
)
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -38,11 +51,144 @@ class Builtin(Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._settings = {}
|
||||
self._nodes = []
|
||||
self._cloud_nodes = {}
|
||||
self._nat_nodes = {}
|
||||
self._ethernet_hubs = {}
|
||||
self._ethernet_switches = {}
|
||||
|
||||
# load the settings
|
||||
self._loadSettings()
|
||||
|
||||
def configChangedSlot(self):
|
||||
|
||||
pass
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns the module settings
|
||||
|
||||
:returns: module settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._settings
|
||||
|
||||
def setSettings(self, settings):
|
||||
"""Sets the module settings
|
||||
|
||||
:param settings: module settings (dictionary)
|
||||
"""
|
||||
|
||||
self._settings.update(settings)
|
||||
self._saveSettings()
|
||||
|
||||
def _saveSettings(self):
|
||||
"""
|
||||
Saves the settings to the persistent settings file.
|
||||
"""
|
||||
|
||||
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
|
||||
|
||||
def _loadSettings(self):
|
||||
"""
|
||||
Loads the settings from the persistent settings file.
|
||||
"""
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
self._settings = local_config.loadSectionSettings(self.__class__.__name__, BUILTIN_SETTINGS)
|
||||
self._loadNodes()
|
||||
|
||||
def _loadBuilinNodesPerType(self, node_dict, node_type, default_settings):
|
||||
|
||||
settings = LocalConfig.instance().settings()
|
||||
if node_type in settings.get(self.__class__.__name__, {}):
|
||||
for device in settings[self.__class__.__name__][node_type]:
|
||||
name = device.get("name")
|
||||
server = device.get("server")
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
if key in node_dict or not name or not server:
|
||||
continue
|
||||
node_settings = default_settings.copy()
|
||||
node_settings.update(device)
|
||||
node_dict[key] = node_settings
|
||||
|
||||
def _loadNodes(self):
|
||||
"""
|
||||
Load the built-in nodes from the persistent settings file.
|
||||
"""
|
||||
|
||||
self._loadBuilinNodesPerType(self._cloud_nodes, "cloud_nodes", CLOUD_SETTINGS)
|
||||
self._loadBuilinNodesPerType(self._ethernet_hubs, "ethernet_hubs", ETHERNET_HUB_SETTINGS)
|
||||
self._loadBuilinNodesPerType(self._ethernet_switches, "ethernet_switches", ETHERNET_SWITCH_SETTINGS)
|
||||
|
||||
def _saveNodes(self):
|
||||
"""
|
||||
Saves the built-in nodes to the persistent settings file.
|
||||
"""
|
||||
|
||||
self._settings["cloud_nodes"] = list(self._cloud_nodes.values())
|
||||
self._settings["ethernet_hubs"] = list(self._ethernet_hubs.values())
|
||||
self._settings["ethernet_switches"] = list(self._ethernet_switches.values())
|
||||
self._saveSettings()
|
||||
|
||||
def cloudNodes(self):
|
||||
"""
|
||||
Returns cloud nodes settings.
|
||||
|
||||
:returns: Cloud nodes settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._cloud_nodes
|
||||
|
||||
def setCloudNodes(self, new_cloud_nodes):
|
||||
"""
|
||||
Sets cloud nodes settings.
|
||||
|
||||
:param new_cloud_nodes: cloud nodes settings (dictionary)
|
||||
"""
|
||||
|
||||
self._cloud_nodes = new_cloud_nodes.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def ethernetHubs(self):
|
||||
"""
|
||||
Returns Ethernet hubs settings.
|
||||
|
||||
:returns: Ethernet hubs settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._ethernet_hubs
|
||||
|
||||
def setEthernetHubs(self, new_ethernet_hubs):
|
||||
"""
|
||||
Sets Ethernet hubs settings.
|
||||
|
||||
:param new_ethernet_hubs: Ethernet hubs settings (dictionary)
|
||||
"""
|
||||
|
||||
self._ethernet_hubs = new_ethernet_hubs.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def ethernetSwitches(self):
|
||||
"""
|
||||
Returns Ethernet switches settings.
|
||||
|
||||
:returns: Ethernet switches settings (dictionary)
|
||||
"""
|
||||
|
||||
return self._ethernet_switches
|
||||
|
||||
def setEthernetSwitches(self, new_ethernet_switches):
|
||||
"""
|
||||
Sets Ethernet switches settings.
|
||||
|
||||
:param new_ethernet_switches: Ethernet switches settings (dictionary)
|
||||
"""
|
||||
|
||||
self._ethernet_switches = new_ethernet_switches.copy()
|
||||
self._saveNodes()
|
||||
|
||||
def addNode(self, node):
|
||||
"""
|
||||
Adds a node to this module.
|
||||
@@ -67,33 +213,51 @@ class Builtin(Module):
|
||||
Resets the module.
|
||||
"""
|
||||
|
||||
log.info("Built-in module reset")
|
||||
self._nodes.clear()
|
||||
|
||||
def createNode(self, node_class, server, project):
|
||||
def instantiateNode(self, node_class, server, project):
|
||||
"""
|
||||
Creates a new node.
|
||||
Instantiate a new node.
|
||||
|
||||
:param node_class: Node object
|
||||
:param server: HTTPClient instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("creating node {}".format(node_class))
|
||||
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def setupNode(self, node, node_name):
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Setups a node.
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("configuring node {}".format(node))
|
||||
node.setup()
|
||||
log.info("creating node {}".format(node))
|
||||
if isinstance(node, Cloud):
|
||||
for key, info in self._cloud_nodes.items():
|
||||
if node_name == info["name"]:
|
||||
node.create(ports=info["ports_mapping"], default_name_format=info["default_name_format"])
|
||||
return
|
||||
elif isinstance(node, Nat):
|
||||
for key, info in self._nat_nodes.items():
|
||||
if node_name == info["name"]:
|
||||
node.create(default_name_format=info["default_name_format"])
|
||||
return
|
||||
elif isinstance(node, EthernetHub):
|
||||
for key, info in self._ethernet_hubs.items():
|
||||
if node_name == info["name"]:
|
||||
node.create(ports=info["ports_mapping"], default_name_format=info["default_name_format"])
|
||||
return
|
||||
elif isinstance(node, EthernetSwitch):
|
||||
for key, info in self._ethernet_switches.items():
|
||||
if node_name == info["name"]:
|
||||
node.create(ports=info["ports_mapping"], default_name_format=info["default_name_format"])
|
||||
return
|
||||
node.create()
|
||||
|
||||
@staticmethod
|
||||
def findAlternativeInterface(node, missing_interface):
|
||||
@@ -129,6 +293,22 @@ class Builtin(Module):
|
||||
return globals()[name]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def getNodeType(name, platform=None):
|
||||
if name == "cloud":
|
||||
return Cloud
|
||||
elif name == "nat":
|
||||
return Nat
|
||||
elif name == "ethernet_hub":
|
||||
return EthernetHub
|
||||
elif name == "ethernet_switch":
|
||||
return EthernetSwitch
|
||||
elif name == "frame_relay_switch":
|
||||
return FrameRelaySwitch
|
||||
elif name == "atm_switch":
|
||||
return ATMSwitch
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def classes():
|
||||
"""
|
||||
@@ -137,7 +317,7 @@ class Builtin(Module):
|
||||
:returns: list of classes
|
||||
"""
|
||||
|
||||
return [Cloud, Host]
|
||||
return [Nat, Cloud, EthernetHub, EthernetSwitch, FrameRelaySwitch, ATMSwitch]
|
||||
|
||||
def nodes(self):
|
||||
"""
|
||||
@@ -151,8 +331,45 @@ class Builtin(Module):
|
||||
{"class": node_class.__name__,
|
||||
"name": node_class.symbolName(),
|
||||
"categories": node_class.categories(),
|
||||
"symbol": node_class.defaultSymbol()}
|
||||
"symbol": node_class.defaultSymbol(),
|
||||
"builtin": True,
|
||||
"node_type": node_class.URL_PREFIX
|
||||
}
|
||||
)
|
||||
|
||||
# add custom cloud node templates
|
||||
for cloud_node in self._cloud_nodes.values():
|
||||
nodes.append(
|
||||
{"class": Cloud.__name__,
|
||||
"name": cloud_node["name"],
|
||||
"server": cloud_node["server"],
|
||||
"symbol": cloud_node["symbol"],
|
||||
"categories": [cloud_node["category"]]
|
||||
}
|
||||
)
|
||||
|
||||
# add custom Ethernet hub templates
|
||||
for hub in self._ethernet_hubs.values():
|
||||
nodes.append(
|
||||
{"class": EthernetHub.__name__,
|
||||
"name": hub["name"],
|
||||
"server": hub["server"],
|
||||
"symbol": hub["symbol"],
|
||||
"categories": [hub["category"]]
|
||||
}
|
||||
)
|
||||
|
||||
# add custom Ethernet switch templates
|
||||
for switch in self._ethernet_switches.values():
|
||||
nodes.append(
|
||||
{"class": EthernetSwitch.__name__,
|
||||
"name": switch["name"],
|
||||
"server": switch["server"],
|
||||
"symbol": switch["symbol"],
|
||||
"categories": [switch["category"]]
|
||||
}
|
||||
)
|
||||
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
@@ -161,7 +378,12 @@ class Builtin(Module):
|
||||
:returns: QWidget object list
|
||||
"""
|
||||
|
||||
return []
|
||||
from .pages.builtin_preferences_page import BuiltinPreferencesPage
|
||||
from .pages.cloud_preferences_page import CloudPreferencesPage
|
||||
from .pages.ethernet_hub_preferences_page import EthernetHubPreferencesPage
|
||||
from .pages.ethernet_switch_preferences_page import EthernetSwitchPreferencesPage
|
||||
|
||||
return [BuiltinPreferencesPage, EthernetHubPreferencesPage, EthernetSwitchPreferencesPage, CloudPreferencesPage]
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
|
||||
229
gns3/modules/builtin/atm_switch.py
Normal file
229
gns3/modules/builtin/atm_switch.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ATMSwitch(Node):
|
||||
|
||||
"""
|
||||
ATM switch.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
URL_PREFIX = "atm_switch"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"mappings": {}})
|
||||
|
||||
def create(self, name=None, node_id=None, mappings=None, default_name_format="ATM{0}"):
|
||||
"""
|
||||
Creates this ATM switch.
|
||||
|
||||
:param name: optional name for this switch.
|
||||
:param node_id: Node identifier on the server
|
||||
:param mappings: mappings to be automatically added when creating this ATM switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if mappings:
|
||||
params["mappings"] = mappings
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this ATM switch.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this ATM switch.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """ATM switch {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's Node ID is {node_id}
|
||||
Hardware is Dynamips emulated simple ATM switch
|
||||
Switch's server runs on {host}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self._compute.name())
|
||||
|
||||
port_info = ""
|
||||
mapping = re.compile(r"""^([0-9]*):([0-9]*):([0-9]*)$""")
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " Port {} is empty\n".format(port.name())
|
||||
else:
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
for source, destination in self._settings["mappings"].items():
|
||||
match_source_mapping = mapping.search(source)
|
||||
match_destination_mapping = mapping.search(destination)
|
||||
if match_source_mapping and match_destination_mapping:
|
||||
source_port, source_vpi, source_vci = match_source_mapping.group(1, 2, 3)
|
||||
destination_port, destination_vpi, destination_vci = match_destination_mapping.group(1, 2, 3)
|
||||
else:
|
||||
source_port, source_vpi = source.split(":")
|
||||
destination_port, destination_vpi = destination.split(":")
|
||||
source_vci = destination_vci = 0
|
||||
|
||||
if port.name() == source_port or port.name() == destination_port:
|
||||
if port.name() == source_port:
|
||||
vpi1 = source_vpi
|
||||
vci1 = source_vci
|
||||
port = destination_port
|
||||
vci2 = destination_vci
|
||||
vpi2 = destination_vpi
|
||||
else:
|
||||
vpi1 = destination_vpi
|
||||
vci1 = destination_vci
|
||||
port = source_port
|
||||
vci2 = source_vci
|
||||
vpi2 = source_vpi
|
||||
|
||||
if vci1 and vci2:
|
||||
port_info += " incoming VPI {vpi1} and VCI {vci1} is switched to port {port} outgoing VPI {vpi2} and VCI {vci2}\n".format(vpi1=vpi1,
|
||||
vci1=vci1,
|
||||
port=port,
|
||||
vpi2=vpi2,
|
||||
vci2=vci2)
|
||||
else:
|
||||
port_info += " incoming VPI {vpi1} is switched to port {port} outgoing VPI {vpi2}\n".format(vpi1=vpi1,
|
||||
port=port,
|
||||
vpi2=vpi2)
|
||||
|
||||
break
|
||||
|
||||
return info + port_info
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this ATM switch
|
||||
(to be saved in a topology file).
|
||||
|
||||
:returns: dictionary
|
||||
"""
|
||||
|
||||
atmsw = super().dump()
|
||||
if self._settings["mappings"]:
|
||||
atmsw["properties"]["mappings"] = self._settings["mappings"]
|
||||
return atmsw
|
||||
|
||||
def load(self, node_info):
|
||||
"""
|
||||
Loads an ATM switch representation
|
||||
(from a topology file).
|
||||
|
||||
:param node_info: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
super().load(node_info)
|
||||
properties = node_info["properties"]
|
||||
name = properties.pop("name")
|
||||
|
||||
# ATM switches do not have an UUID before version 2.0
|
||||
node_id = properties.get("node_id", str(uuid.uuid4()))
|
||||
|
||||
mappings = {}
|
||||
if "mappings" in properties:
|
||||
mappings = properties["mappings"]
|
||||
|
||||
log.info("ATM switch {} is loading".format(name))
|
||||
self.create(name, node_id, mappings)
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
|
||||
from .pages.atm_switch_configuration_page import ATMSwitchConfigurationPage
|
||||
return ATMSwitchConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/atm_switch.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "ATM switch"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.switches]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "ATM switch"
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -15,21 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
NIO implementation on the client side (in the form of a pseudo node represented as a cloud).
|
||||
"""
|
||||
|
||||
import re
|
||||
from gns3.node import Node
|
||||
from gns3.ports.port import Port
|
||||
from gns3.nios.nio_generic_ethernet import NIOGenericEthernet
|
||||
from gns3.nios.nio_linux_ethernet import NIOLinuxEthernet
|
||||
from gns3.nios.nio_nat import NIONAT
|
||||
from gns3.nios.nio_udp import NIOUDP
|
||||
from gns3.nios.nio_tap import NIOTAP
|
||||
from gns3.nios.nio_unix import NIOUNIX
|
||||
from gns3.nios.nio_vde import NIOVDE
|
||||
from gns3.nios.nio_null import NIONull
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -38,238 +24,53 @@ log = logging.getLogger(__name__)
|
||||
class Cloud(Node):
|
||||
|
||||
"""
|
||||
Dynamips cloud.
|
||||
Cloud node
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
_name_instance_count = 1
|
||||
URL_PREFIX = "cloud"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self._interfaces = {}
|
||||
self._cloud_settings = {"ports_mapping": []}
|
||||
self.settings().update(self._cloud_settings)
|
||||
|
||||
log.info("cloud is being created")
|
||||
# create an unique id and name
|
||||
self._name_id = Cloud._name_instance_count
|
||||
Cloud._name_instance_count += 1
|
||||
def interfaces(self):
|
||||
|
||||
name = "Cloud {}".format(self._name_id)
|
||||
self.setStatus(Node.started) # this is an always-on node
|
||||
self._initial_settings = None
|
||||
self._settings = {"name": name,
|
||||
"interfaces": {},
|
||||
"nios": []}
|
||||
return self._interfaces
|
||||
|
||||
def delete(self):
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="Cloud{0}"):
|
||||
"""
|
||||
Deletes this cloud.
|
||||
"""
|
||||
|
||||
# first delete all the links attached to this node
|
||||
self.delete_links_signal.emit()
|
||||
self.deleted_signal.emit()
|
||||
|
||||
def setup(self, name=None, additional_settings={}):
|
||||
"""
|
||||
Setups this cloud.
|
||||
Creates this cloud.
|
||||
|
||||
:param name: optional name for this cloud
|
||||
:param node_id: Node identifier on the server
|
||||
:param ports: ports to be automatically added when creating this cloud
|
||||
"""
|
||||
|
||||
if name:
|
||||
self._settings["name"] = name
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
if additional_settings and "nios" in additional_settings:
|
||||
self._settings["nios"] = additional_settings["nios"]
|
||||
|
||||
self._server.get("/interfaces", self._setupCallback)
|
||||
|
||||
def _setupCallback(self, result, error=False, **kwargs):
|
||||
def _createCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for setup.
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
if "ports_mapping" in result:
|
||||
self._settings["ports_mapping"] = result["ports_mapping"].copy()
|
||||
|
||||
if error:
|
||||
log.error("error while setting up {}: {}".format(self.name(), result["message"]))
|
||||
# a warning message instead of a error is more appropriate here
|
||||
self.warning_signal.emit(self.id(), result["message"])
|
||||
else:
|
||||
self._settings["interfaces"] = result.copy()
|
||||
|
||||
if self._settings["nios"]:
|
||||
self._addPorts(self._settings["nios"])
|
||||
|
||||
if self._loading:
|
||||
self.loaded_signal.emit()
|
||||
else:
|
||||
self.setInitialized(True)
|
||||
log.info("cloud {} has been created".format(self.name()))
|
||||
self.created_signal.emit(self.id())
|
||||
self._module.addNode(self)
|
||||
|
||||
def _createNIOUDP(self, nio):
|
||||
"""
|
||||
Creates a NIO UDP.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_udp:(\d+):(.+):(\d+)$""", nio)
|
||||
if match:
|
||||
lport = int(match.group(1))
|
||||
rhost = match.group(2)
|
||||
rport = int(match.group(3))
|
||||
return NIOUDP(lport, rhost, rport)
|
||||
return None
|
||||
|
||||
def _createNIOGenericEthernet(self, nio):
|
||||
"""
|
||||
Creates a NIO Generic Ethernet.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_gen_eth:(.+)$""", nio)
|
||||
if match:
|
||||
ethernet_device = match.group(1)
|
||||
return NIOGenericEthernet(ethernet_device)
|
||||
return None
|
||||
|
||||
def _createNIOLinuxEthernet(self, nio):
|
||||
"""
|
||||
Creates a NIO Linux Ethernet.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_gen_linux:(.+)$""", nio)
|
||||
if match:
|
||||
linux_device = match.group(1)
|
||||
return NIOLinuxEthernet(linux_device)
|
||||
return None
|
||||
|
||||
def _createNIONAT(self, nio):
|
||||
"""
|
||||
Creates a NIO NAT.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_nat:(.+)$""", nio)
|
||||
if match:
|
||||
identifier = match.group(1)
|
||||
return NIONAT(identifier)
|
||||
return None
|
||||
|
||||
def _createNIOTAP(self, nio):
|
||||
"""
|
||||
Creates a NIO TAP.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_tap:(.+)$""", nio)
|
||||
if match:
|
||||
tap_device = match.group(1)
|
||||
return NIOTAP(tap_device)
|
||||
return None
|
||||
|
||||
def _createNIOUNIX(self, nio):
|
||||
"""
|
||||
Creates a NIO UNIX.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_unix:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
local_file = match.group(1)
|
||||
remote_file = match.group(2)
|
||||
return NIOUNIX(local_file, remote_file)
|
||||
return None
|
||||
|
||||
def _createNIOVDE(self, nio):
|
||||
"""
|
||||
Creates a NIO VDE.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_vde:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
control_file = match.group(1)
|
||||
local_file = match.group(2)
|
||||
return NIOVDE(control_file, local_file)
|
||||
return None
|
||||
|
||||
def _createNIONull(self, nio):
|
||||
"""
|
||||
Creates a NIO Null.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_null:(.+)$""", nio)
|
||||
if match:
|
||||
identifier = match.group(1)
|
||||
return NIONull(identifier)
|
||||
return None
|
||||
|
||||
def _allocateNIO(self, nio):
|
||||
"""
|
||||
Allocate a new NIO object.
|
||||
|
||||
:param nio: NIO description
|
||||
|
||||
:returns: NIO instance
|
||||
"""
|
||||
|
||||
nio_object = None
|
||||
if nio.lower().startswith("nio_udp"):
|
||||
nio_object = self._createNIOUDP(nio)
|
||||
if nio.lower().startswith("nio_gen_eth"):
|
||||
nio_object = self._createNIOGenericEthernet(nio)
|
||||
if nio.lower().startswith("nio_gen_linux"):
|
||||
nio_object = self._createNIOLinuxEthernet(nio)
|
||||
if nio.lower().startswith("nio_nat"):
|
||||
nio_object = self._createNIONAT(nio)
|
||||
if nio.lower().startswith("nio_tap"):
|
||||
nio_object = self._createNIOTAP(nio)
|
||||
if nio.lower().startswith("nio_unix"):
|
||||
nio_object = self._createNIOUNIX(nio)
|
||||
if nio.lower().startswith("nio_vde"):
|
||||
nio_object = self._createNIOVDE(nio)
|
||||
if nio.lower().startswith("nio_null"):
|
||||
nio_object = self._createNIONull(nio)
|
||||
if nio_object is None:
|
||||
log.error("Could not create NIO object from {}".format(nio))
|
||||
return nio_object
|
||||
|
||||
def _addPorts(self, nios, ignore_existing_nio=False):
|
||||
"""
|
||||
Adds adapters.
|
||||
|
||||
:param adapters: number of adapters
|
||||
"""
|
||||
|
||||
# add ports
|
||||
for nio in nios:
|
||||
if ignore_existing_nio and nio in self._settings["nios"]:
|
||||
# port already created for this NIO
|
||||
continue
|
||||
nio_object = self._allocateNIO(nio)
|
||||
if nio_object is None:
|
||||
continue
|
||||
port = Port(nio, nio_object, stub=True)
|
||||
port.setStatus(Port.started)
|
||||
self._ports.append(port)
|
||||
log.debug("port {} has been added".format(nio))
|
||||
if "interfaces" in result:
|
||||
self._interfaces = result["interfaces"].copy()
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
@@ -278,45 +79,35 @@ class Cloud(Node):
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
updated = False
|
||||
if "nios" in new_settings:
|
||||
nios = new_settings["nios"]
|
||||
self._addPorts(nios, ignore_existing_nio=True)
|
||||
updated = True
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
# delete ports
|
||||
for nio in self._settings["nios"]:
|
||||
if nio not in nios:
|
||||
for port in self._ports.copy():
|
||||
if port.name() == nio:
|
||||
self._ports.remove(port)
|
||||
updated = True
|
||||
log.debug("port {} has been deleted".format(nio))
|
||||
break
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
self._settings["nios"] = new_settings["nios"].copy()
|
||||
:param result: server response
|
||||
"""
|
||||
|
||||
if "name" in new_settings and new_settings["name"] != self.name():
|
||||
self._settings["name"] = new_settings["name"]
|
||||
updated = True
|
||||
if "ports_mapping" in result:
|
||||
self._settings["ports_mapping"] = result["ports_mapping"].copy()
|
||||
|
||||
if updated:
|
||||
log.info("cloud {} has been updated".format(self.name()))
|
||||
self.updated_signal.emit()
|
||||
|
||||
def deleteNIO(self, port):
|
||||
|
||||
pass
|
||||
if "interfaces" in result:
|
||||
self._interfaces = result["interfaces"].copy()
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this cloud.
|
||||
|
||||
:returns: formated string
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Cloud device {name} is always-on
|
||||
This is a pseudo-device for external connections
|
||||
This is a node for external connections
|
||||
""".format(name=self.name())
|
||||
|
||||
port_info = ""
|
||||
@@ -327,124 +118,8 @@ This is a pseudo-device for external connections
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
# add the Windows interface name
|
||||
match = re.search(r"""^nio_gen_eth:(\\device\\npf_.+)$""", port.name())
|
||||
if match:
|
||||
for interface in self._settings["interfaces"]:
|
||||
if interface["name"].lower() == match.group(1):
|
||||
port_info += " Windows name: {}\n".format(interface["description"])
|
||||
break
|
||||
|
||||
return info + port_info
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Returns a representation of this cloud
|
||||
(to be saved in a topology file).
|
||||
|
||||
:returns: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
cloud = {"id": self.id(),
|
||||
"type": self.__class__.__name__,
|
||||
"description": str(self),
|
||||
"properties": {"name": self.name(),
|
||||
"nios": self._settings["nios"]},
|
||||
"server_id": self._server.id()}
|
||||
|
||||
# add the ports
|
||||
if self._ports:
|
||||
ports = cloud["ports"] = []
|
||||
for port in self._ports:
|
||||
ports.append(port.dump())
|
||||
|
||||
return cloud
|
||||
|
||||
def load(self, node_info):
|
||||
"""
|
||||
Loads a cloud representation
|
||||
(from a topology file).
|
||||
|
||||
:param node_info: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
settings = node_info["properties"]
|
||||
name = settings.pop("name")
|
||||
log.info("cloud {} is loading".format(name))
|
||||
self.setName(name)
|
||||
self._loading = True
|
||||
self._node_info = node_info
|
||||
self.loaded_signal.connect(self._updatePortSettings)
|
||||
self.setup(name, additional_settings=settings)
|
||||
|
||||
def _updatePortSettings(self):
|
||||
"""
|
||||
Updates port settings when loading a topology.
|
||||
"""
|
||||
|
||||
self.loaded_signal.disconnect(self._updatePortSettings)
|
||||
|
||||
# update the port with the correct IDs
|
||||
if "ports" in self._node_info:
|
||||
ports = self._node_info["ports"]
|
||||
for topology_port in ports:
|
||||
for port in self._ports:
|
||||
if topology_port["name"] == port.name():
|
||||
port.setId(topology_port["id"])
|
||||
if topology_port["name"].startswith("nio_gen_eth") or topology_port["name"].startswith("nio_linux_eth"):
|
||||
# lookup if the interface exists
|
||||
available_interface = False
|
||||
topology_port_name = topology_port["name"].split(':', 1)[1]
|
||||
for interface in self._settings["interfaces"]:
|
||||
if interface["name"] == topology_port_name:
|
||||
available_interface = True
|
||||
break
|
||||
if not available_interface:
|
||||
alternative_interface = self._module.findAlternativeInterface(self, topology_port_name)
|
||||
if alternative_interface:
|
||||
if topology_port["name"] in self._settings["nios"]:
|
||||
self._settings["nios"].remove(topology_port["name"])
|
||||
topology_port["name"] = topology_port["name"].replace(topology_port_name, alternative_interface)
|
||||
nio = self._allocateNIO(topology_port["name"])
|
||||
port.setDefaultNio(nio)
|
||||
port.setName(topology_port["name"])
|
||||
self._settings["nios"].append(topology_port["name"])
|
||||
|
||||
# now we can set the node as initialized and trigger the created signal
|
||||
self.setInitialized(True)
|
||||
log.info("cloud {} has been loaded".format(self.name()))
|
||||
self.created_signal.emit(self.id())
|
||||
self._module.addNode(self)
|
||||
self._loading = False
|
||||
self._node_info = None
|
||||
|
||||
def name(self):
|
||||
"""
|
||||
Returns the name of this cloud.
|
||||
|
||||
:returns: name (string)
|
||||
"""
|
||||
|
||||
return self._settings["name"]
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns all this cloud settings.
|
||||
|
||||
:returns: settings dictionary
|
||||
"""
|
||||
|
||||
return self._settings
|
||||
|
||||
def ports(self):
|
||||
"""
|
||||
Returns all the ports for this cloud.
|
||||
|
||||
:returns: list of Port instances
|
||||
"""
|
||||
|
||||
return self._ports
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
@@ -475,7 +150,7 @@ This is a pseudo-device for external connections
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node category (integer)
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.end_devices]
|
||||
|
||||
57
gns3/modules/builtin/dialogs/cloud_wizard.py
Normal file
57
gns3/modules/builtin/dialogs/cloud_wizard.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Wizard for cloud nodes.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.node import Node
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.cloud_wizard_ui import Ui_CloudNodeWizard
|
||||
from .. import Builtin
|
||||
|
||||
|
||||
class CloudWizard(VMWizard, Ui_CloudNodeWizard):
|
||||
|
||||
"""
|
||||
Wizard to create a cloud node template.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, cloud_nodes, parent):
|
||||
|
||||
super().__init__(cloud_nodes, Builtin.instance().settings()["use_local_server"], parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/cloud.svg"))
|
||||
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
|
||||
def getSettings(self):
|
||||
"""
|
||||
Returns the settings set in this Wizard.
|
||||
|
||||
:return: settings dict
|
||||
"""
|
||||
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"category": Node.end_devices,
|
||||
"server": self._compute_id}
|
||||
|
||||
return settings
|
||||
63
gns3/modules/builtin/dialogs/ethernet_hub_wizard.py
Normal file
63
gns3/modules/builtin/dialogs/ethernet_hub_wizard.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Wizard for Ethernet hubs.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.node import Node
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.ethernet_hub_wizard_ui import Ui_EthernetHubWizard
|
||||
from .. import Builtin
|
||||
|
||||
|
||||
class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
|
||||
|
||||
"""
|
||||
Wizard to create an Ethernet hub template.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, ethernet_hubs, parent):
|
||||
|
||||
super().__init__(ethernet_hubs, Builtin.instance().settings()["use_local_server"], parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/hub.svg"))
|
||||
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
|
||||
def getSettings(self):
|
||||
"""
|
||||
Returns the settings set in this Wizard.
|
||||
|
||||
:return: settings dict
|
||||
"""
|
||||
|
||||
ports = []
|
||||
for port_number in range(1, self.uiPortsSpinBox.value() + 1):
|
||||
ports.append({"port_number": int(port_number),
|
||||
"name": "Ethernet{}".format(port_number)})
|
||||
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"symbol": ":/symbols/hub.svg",
|
||||
"category": Node.switches,
|
||||
"server": self._compute_id,
|
||||
"ports_mapping": ports}
|
||||
|
||||
return settings
|
||||
66
gns3/modules/builtin/dialogs/ethernet_switch_wizard.py
Normal file
66
gns3/modules/builtin/dialogs/ethernet_switch_wizard.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Wizard for Ethernet switches.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.node import Node
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.ethernet_switch_wizard_ui import Ui_EthernetSwitchWizard
|
||||
from .. import Builtin
|
||||
|
||||
|
||||
class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
|
||||
|
||||
"""
|
||||
Wizard to create an Ethernet switch template.
|
||||
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, ethernet_switches, parent):
|
||||
|
||||
super().__init__(ethernet_switches, Builtin.instance().settings()["use_local_server"], parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/ethernet_switch.svg"))
|
||||
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
|
||||
def getSettings(self):
|
||||
"""
|
||||
Returns the settings set in this Wizard.
|
||||
|
||||
:return: settings dict
|
||||
"""
|
||||
|
||||
ports = []
|
||||
for port_number in range(1, self.uiPortsSpinBox.value() + 1):
|
||||
ports.append({"port_number": int(port_number),
|
||||
"name": "Ethernet{}".format(port_number),
|
||||
"type": "access",
|
||||
"vlan": 1,
|
||||
"ethertype": ""})
|
||||
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"symbol": ":/symbols/ethernet_switch.svg",
|
||||
"category": Node.switches,
|
||||
"server": self._compute_id,
|
||||
"ports_mapping": ports}
|
||||
|
||||
return settings
|
||||
150
gns3/modules/builtin/ethernet_hub.py
Normal file
150
gns3/modules/builtin/ethernet_hub.py
Normal file
@@ -0,0 +1,150 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EthernetHub(Node):
|
||||
"""
|
||||
Ethernet hub.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
URL_PREFIX = "ethernet_hub"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"ports_mapping": []})
|
||||
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="Hub{0}"):
|
||||
"""
|
||||
Creates this hub.
|
||||
|
||||
:param name: optional name for this hub
|
||||
:param node_id: node identifier on the server
|
||||
:param ports: ports to automatically be added when creating this hub
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this Ethernet hub.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if "name" in new_settings:
|
||||
params["name"] = new_settings["name"]
|
||||
if "ports_mapping" in new_settings:
|
||||
params["ports_mapping"] = new_settings["ports_mapping"]
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this Ethernet hub.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Ethernet hub {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's node ID is {node_id}
|
||||
Hub's server runs on {host}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self.compute().id())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " Port {} is empty\n".format(port.name())
|
||||
else:
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
return info + port_info
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
|
||||
from .pages.ethernet_hub_configuration_page import EthernetHubConfigurationPage
|
||||
return EthernetHubConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/hub.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Ethernet hub"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.switches]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Ethernet hub"
|
||||
170
gns3/modules/builtin/ethernet_switch.py
Normal file
170
gns3/modules/builtin/ethernet_switch.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EthernetSwitch(Node):
|
||||
|
||||
"""
|
||||
Ethernet switch.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
URL_PREFIX = "ethernet_switch"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"ports_mapping": []})
|
||||
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="SW{0}"):
|
||||
"""
|
||||
Creates this Ethernet switch.
|
||||
|
||||
:param name: optional name for this switch
|
||||
:param node_id: node identifier on the server
|
||||
:param ports: ports to be automatically added when creating this switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this Ethernet switch.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this Ethernet switch.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Ethernet switch {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's Node ID is {node_id}
|
||||
Switch's server runs on {host}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self.compute().id())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " Port {} is empty\n".format(port.name())
|
||||
else:
|
||||
for port_settings in self._settings["ports_mapping"]:
|
||||
if port_settings["port_number"] == port.portNumber():
|
||||
|
||||
port_type = port_settings["type"]
|
||||
port_ethertype = port_settings.get("ethertype", "")
|
||||
port_vlan = port_settings["vlan"]
|
||||
port_ethertype_info = ""
|
||||
|
||||
if port_type == "access":
|
||||
port_vlan_info = "VLAN ID {}".format(port_vlan)
|
||||
elif port_type == "dot1q":
|
||||
port_vlan_info = "native VLAN {}".format(port_vlan)
|
||||
elif port_type == "qinq":
|
||||
port_vlan_info = "outer VLAN {}".format(port_vlan)
|
||||
port_ethertype_info = "({})".format(port_ethertype)
|
||||
|
||||
port_info += " Port {name} is in {port_type} {port_ethertype_info} mode, with {port_vlan_info},\n".format(name=port.name(),
|
||||
port_type=port_type,
|
||||
port_ethertype_info=port_ethertype_info,
|
||||
port_vlan_info=port_vlan_info)
|
||||
port_info += " {port_description}\n".format(port_description=port.description())
|
||||
break
|
||||
|
||||
return info + port_info
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
|
||||
from .pages.ethernet_switch_configuration_page import EthernetSwitchConfigurationPage
|
||||
return EthernetSwitchConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/ethernet_switch.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Ethernet switch"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.switches]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Ethernet switch"
|
||||
171
gns3/modules/builtin/frame_relay_switch.py
Normal file
171
gns3/modules/builtin/frame_relay_switch.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FrameRelaySwitch(Node):
|
||||
|
||||
"""
|
||||
Frame-Relay switch.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
URL_PREFIX = "frame_relay_switch"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"mappings": {}})
|
||||
|
||||
def create(self, name=None, node_id=None, mappings={}, default_name_format="FR{0}"):
|
||||
"""
|
||||
Creates this Frame Relay switch.
|
||||
|
||||
:param name: name for this switch.
|
||||
:param node_id: node identifier on the server
|
||||
:param mappings: mappings to be automatically added when creating this Frame relay switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if mappings:
|
||||
params["mappings"] = mappings
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this Frame Relay switch.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
self.settings()["mappings"] = result["mappings"]
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this Frame Relay switch.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Frame relay switch {name} is always-on
|
||||
Local node ID is {id}
|
||||
Server's Node ID is {node_id}
|
||||
Hardware is Dynamips emulated simple Frame relay switch
|
||||
Switch's server runs on {host}:{port}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
node_id=self._node_id,
|
||||
host=self._compute.host(),
|
||||
port=self._compute.port())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " Port {} is empty\n".format(port.name())
|
||||
else:
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
for source, destination in self._settings["mappings"].items():
|
||||
source_port, source_dlci = source.split(":")
|
||||
destination_port, destination_dlci = destination.split(":")
|
||||
|
||||
if port.name() == source_port or port.name() == destination_port:
|
||||
if port.name() == source_port:
|
||||
dlci1 = source_dlci
|
||||
port = destination_port
|
||||
dlci2 = destination_dlci
|
||||
else:
|
||||
dlci1 = destination_dlci
|
||||
port = source_port
|
||||
dlci2 = source_dlci
|
||||
port_info += " incoming DLCI {dlci1} is switched to port {port} outgoing DLCI {dlci2}\n".format(dlci1=dlci1,
|
||||
port=port,
|
||||
dlci2=dlci2)
|
||||
break
|
||||
|
||||
return info + port_info
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
:returns: QWidget object
|
||||
"""
|
||||
|
||||
from .pages.frame_relay_switch_configuration_page import FrameRelaySwitchConfigurationPage
|
||||
return FrameRelaySwitchConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this node.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/frame_relay_switch.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Frame Relay switch"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.switches]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Frame Relay switch"
|
||||
@@ -1,108 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from gns3.node import Node
|
||||
from .cloud import Cloud
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Host(Cloud):
|
||||
|
||||
"""
|
||||
Pseudo host based on a Dynamips Cloud.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
_name_instance_count = 1
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
super().__init__(module, server, project)
|
||||
|
||||
log.info("host is being created")
|
||||
# create an unique id and name
|
||||
self._name_id = Host._name_instance_count
|
||||
Host._name_instance_count += 1
|
||||
|
||||
name = "Host{}".format(self._name_id)
|
||||
self._settings["name"] = name
|
||||
|
||||
def setup(self, name=None, additional_settings={}):
|
||||
"""
|
||||
Setups this host.
|
||||
|
||||
:param name: optional name for this host
|
||||
"""
|
||||
|
||||
if name:
|
||||
self._settings["name"] = name
|
||||
|
||||
if additional_settings and "nios" in additional_settings:
|
||||
self._settings["nios"] = additional_settings["nios"]
|
||||
else:
|
||||
self.created_signal.connect(self._autoConfigure)
|
||||
|
||||
self._server.get("/interfaces", self._setupCallback)
|
||||
|
||||
def _autoConfigure(self, node_id):
|
||||
"""
|
||||
Auto adds all Ethernet and TAP interfaces.
|
||||
|
||||
:param node_id: ignored
|
||||
"""
|
||||
|
||||
new_settings = {"nios": []}
|
||||
for interface in self._settings["interfaces"]:
|
||||
if interface["name"].startswith("tap"):
|
||||
new_settings["nios"].append("nio_tap:{}".format(interface["name"]))
|
||||
else:
|
||||
new_settings["nios"].append("nio_gen_eth:{}".format(interface["name"]))
|
||||
self.update(new_settings)
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this host.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/computer.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Host"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node category (integer)
|
||||
"""
|
||||
|
||||
return [Node.end_devices]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Host"
|
||||
143
gns3/modules/builtin/nat.py
Normal file
143
gns3/modules/builtin/nat.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Nat(Node):
|
||||
|
||||
"""
|
||||
Nat node
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
URL_PREFIX = "nat"
|
||||
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self._nat_settings = {}
|
||||
self.settings().update(self._nat_settings)
|
||||
|
||||
def interfaces(self):
|
||||
|
||||
return self._interfaces
|
||||
|
||||
def create(self, name=None, node_id=None, default_name_format="Nat{0}"):
|
||||
"""
|
||||
Creates this nat.
|
||||
|
||||
:param name: optional name for this nat
|
||||
:param node_id: Node identifier on the server
|
||||
"""
|
||||
|
||||
params = {}
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("Error while creating nat: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this nat.
|
||||
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
def _updateCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
:param result: server response
|
||||
"""
|
||||
if error:
|
||||
log.error("Error while creating nat: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this nat.
|
||||
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Nat device {name} is always-on
|
||||
This is a node for external connections
|
||||
""".format(name=self.name())
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
if port.isFree():
|
||||
port_info += " Port {} is empty\n".format(port.name())
|
||||
else:
|
||||
port_info += " Port {name} {description}\n".format(name=port.name(),
|
||||
description=port.description())
|
||||
|
||||
return info + port_info
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this nat.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/cloud.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Nat"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node categories
|
||||
"""
|
||||
|
||||
return [Node.end_devices]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Nat"
|
||||
@@ -195,7 +195,5 @@ class ATMSwitchConfigurationPage(QtWidgets.QWidget, Ui_atmSwitchConfigPageWidget
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "ATM switch name cannot be empty!")
|
||||
else:
|
||||
settings["name"] = name
|
||||
else:
|
||||
del settings["name"]
|
||||
|
||||
settings["mappings"] = self._mapping.copy()
|
||||
return settings
|
||||
63
gns3/modules/builtin/pages/builtin_preferences_page.py
Normal file
63
gns3/modules/builtin/pages/builtin_preferences_page.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for Built-in preferences.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from .. import Builtin
|
||||
from ..ui.builtin_preferences_page_ui import Ui_BuiltinPreferencesPageWidget
|
||||
from ..settings import BUILTIN_SETTINGS
|
||||
|
||||
|
||||
class BuiltinPreferencesPage(QtWidgets.QWidget, Ui_BuiltinPreferencesPageWidget):
|
||||
"""QWidget preference page for Built-in."""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
# connect signals
|
||||
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
|
||||
|
||||
def _restoreDefaultsSlot(self):
|
||||
"""Slot to populate the page widgets with the default settings."""
|
||||
|
||||
self._populateWidgets(BUILTIN_SETTINGS)
|
||||
|
||||
def _populateWidgets(self, settings):
|
||||
"""Populates the widgets with the settings.
|
||||
|
||||
:param settings: Built-in settings
|
||||
"""
|
||||
|
||||
self.uiUseLocalServercheckBox.setChecked(settings["use_local_server"])
|
||||
|
||||
def loadPreferences(self):
|
||||
"""Loads Built-in preferences."""
|
||||
|
||||
builtin_settings = Builtin.instance().settings()
|
||||
self._populateWidgets(builtin_settings)
|
||||
|
||||
def savePreferences(self):
|
||||
"""Saves Built-in preferences."""
|
||||
|
||||
new_settings = {}
|
||||
new_settings["use_local_server"] = self.uiUseLocalServercheckBox.isChecked()
|
||||
Builtin.instance().setSettings(new_settings)
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -19,9 +19,13 @@
|
||||
Configuration page for clouds.
|
||||
"""
|
||||
|
||||
import re
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
from gns3.qt import QtGui, QtCore, QtWidgets
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.controller import Controller
|
||||
from gns3.node import Node
|
||||
|
||||
from ..ui.cloud_configuration_page_ui import Ui_cloudConfigPageWidget
|
||||
from ..cloud import Cloud
|
||||
|
||||
|
||||
class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
@@ -34,492 +38,331 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
self._nios = []
|
||||
self._node = None
|
||||
self._ports = []
|
||||
self._interfaces = []
|
||||
|
||||
# connect NIO generic Ethernet slots
|
||||
self.uiGenericEthernetComboBox.currentIndexChanged.connect(self._genericEthernetSelectedSlot)
|
||||
self.uiGenericEthernetListWidget.itemSelectionChanged.connect(self._genericEthernetChangedSlot)
|
||||
self.uiAddGenericEthernetPushButton.clicked.connect(self._genericEthernetAddSlot)
|
||||
self.uiDeleteGenericEthernetPushButton.clicked.connect(self._genericEthernetDeleteSlot)
|
||||
# add the categories
|
||||
for name, category in Node.defaultCategories().items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
|
||||
# connect NIO Linux Ethernet slots
|
||||
self.uiLinuxEthernetComboBox.currentIndexChanged.connect(self._linuxEthernetSelectedSlot)
|
||||
self.uiLinuxEthernetListWidget.itemSelectionChanged.connect(self._linuxEthernetChangedSlot)
|
||||
self.uiAddLinuxEthernetPushButton.clicked.connect(self._linuxEthernetAddSlot)
|
||||
self.uiDeleteLinuxEthernetPushButton.clicked.connect(self._linuxEthernetDeleteSlot)
|
||||
# connect Ethernet slots
|
||||
self.uiEthernetListWidget.itemSelectionChanged.connect(self._EthernetChangedSlot)
|
||||
self.uiAddEthernetPushButton.clicked.connect(self._EthernetAddSlot)
|
||||
self.uiAddAllEthernetPushButton.clicked.connect(self._EthernetAddAllSlot)
|
||||
self.uiDeleteEthernetPushButton.clicked.connect(self._EthernetDeleteSlot)
|
||||
|
||||
# connect NIO NAT slots
|
||||
self.uiNIONATListWidget.currentRowChanged.connect(self._NIONATSelectedSlot)
|
||||
self.uiNIONATListWidget.itemSelectionChanged.connect(self._NIONATChangedSlot)
|
||||
self.uiAddNIONATPushButton.clicked.connect(self._NIONATAddSlot)
|
||||
self.uiDeleteNIONATPushButton.clicked.connect(self._NIONATDeleteSlot)
|
||||
# connect TAP slots
|
||||
self.uiTAPComboBox.currentIndexChanged.connect(self._TAPSelectedSlot)
|
||||
self.uiTAPListWidget.itemSelectionChanged.connect(self._TAPChangedSlot)
|
||||
self.uiAddTAPPushButton.clicked.connect(self._TAPAddSlot)
|
||||
self.uiAddAllTAPPushButton.clicked.connect(self._TAPAddAllSlot)
|
||||
self.uiDeleteTAPPushButton.clicked.connect(self._TAPDeleteSlot)
|
||||
|
||||
# connect NIO UDP slots
|
||||
self.uiNIOUDPListWidget.currentRowChanged.connect(self._NIOUDPSelectedSlot)
|
||||
self.uiNIOUDPListWidget.itemSelectionChanged.connect(self._NIOUDPChangedSlot)
|
||||
self.uiAddNIOUDPPushButton.clicked.connect(self._NIOUDPAddSlot)
|
||||
self.uiDeleteNIOUDPPushButton.clicked.connect(self._NIOUDPDeleteSlot)
|
||||
# connect UDP slots
|
||||
self.uiUDPTreeWidget.itemActivated.connect(self._UDPSelectedSlot)
|
||||
self.uiUDPTreeWidget.itemSelectionChanged.connect(self._UDPChangedSlot)
|
||||
self.uiAddUDPPushButton.clicked.connect(self._UDPAddSlot)
|
||||
self.uiDeleteUDPPushButton.clicked.connect(self._UDPDeleteSlot)
|
||||
|
||||
# connect NIO TAP slots
|
||||
self.uiNIOTAPListWidget.currentRowChanged.connect(self._NIOTAPSelectedSlot)
|
||||
self.uiNIOTAPListWidget.itemSelectionChanged.connect(self._NIOTAPChangedSlot)
|
||||
self.uiAddNIOTAPPushButton.clicked.connect(self._NIOTAPAddSlot)
|
||||
self.uiDeleteNIOTAPPushButton.clicked.connect(self._NIOTAPDeleteSlot)
|
||||
self.uiShowSpecialInterfacesCheckBox.stateChanged.connect(self._showSpecialInterfacesSlot)
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
|
||||
# connect NIO UNIX slots
|
||||
self.uiNIOUNIXListWidget.currentRowChanged.connect(self._NIOUNIXSelectedSlot)
|
||||
self.uiNIOUNIXListWidget.itemSelectionChanged.connect(self._NIOUNIXChangedSlot)
|
||||
self.uiAddNIOUNIXPushButton.clicked.connect(self._NIOUNIXAddSlot)
|
||||
self.uiDeleteNIOUNIXPushButton.clicked.connect(self._NIOUNIXDeleteSlot)
|
||||
|
||||
# connect NIO VDE slots
|
||||
self.uiNIOVDEListWidget.currentRowChanged.connect(self._NIOVDESelectedSlot)
|
||||
self.uiNIOVDEListWidget.itemSelectionChanged.connect(self._NIOVDEChangedSlot)
|
||||
self.uiAddNIOVDEPushButton.clicked.connect(self._NIOVDEAddSlot)
|
||||
self.uiDeleteNIOVDEPushButton.clicked.connect(self._NIOVDEDeleteSlot)
|
||||
|
||||
# connect NIO NULL slots
|
||||
self.uiNIONullListWidget.currentRowChanged.connect(self._NIONullSelectedSlot)
|
||||
self.uiNIONullListWidget.itemSelectionChanged.connect(self._NIONullChangedSlot)
|
||||
self.uiAddNIONullPushButton.clicked.connect(self._NIONullAddSlot)
|
||||
self.uiDeleteNIONullPushButton.clicked.connect(self._NIONullDeleteSlot)
|
||||
|
||||
def _genericEthernetSelectedSlot(self, index):
|
||||
"""
|
||||
Loads the selected generic Ethernet interface in lineEdit.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
self.uiGenericEthernetLineEdit.setText(self.uiGenericEthernetComboBox.currentText())
|
||||
|
||||
def _genericEthernetChangedSlot(self):
|
||||
def _EthernetChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiGenericEthernetListWidget.currentItem()
|
||||
item = self.uiEthernetListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteGenericEthernetPushButton.setEnabled(True)
|
||||
self.uiDeleteEthernetPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteGenericEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteEthernetPushButton.setEnabled(False)
|
||||
|
||||
def _genericEthernetAddSlot(self):
|
||||
def _EthernetAddSlot(self, interface=None):
|
||||
"""
|
||||
Adds a new generic Ethernet NIO.
|
||||
Adds a new Ethernet interface.
|
||||
"""
|
||||
|
||||
interface = self.uiGenericEthernetLineEdit.text()
|
||||
if not interface:
|
||||
interface = self.uiEthernetComboBox.currentText()
|
||||
if interface:
|
||||
nio = "nio_gen_eth:{interface}".format(interface=interface)
|
||||
if nio not in self._nios:
|
||||
self.uiGenericEthernetListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
|
||||
def _genericEthernetDeleteSlot(self):
|
||||
"""
|
||||
Deletes the selected generic Ethernet NIO.
|
||||
"""
|
||||
|
||||
item = self.uiGenericEthernetListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
# check we can delete that NIO
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
for port in self._ports:
|
||||
if port["name"] == interface and port["type"] == "ethernet":
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiGenericEthernetListWidget.takeItem(self.uiGenericEthernetListWidget.currentRow())
|
||||
self.uiEthernetListWidget.addItem(interface)
|
||||
self._ports.append({"name": interface,
|
||||
"port_number": len(self._ports),
|
||||
"type": "ethernet",
|
||||
"interface": interface})
|
||||
index = self.uiEthernetComboBox.findText(interface)
|
||||
if index != -1:
|
||||
self.uiEthernetComboBox.removeItem(index)
|
||||
|
||||
def _linuxEthernetSelectedSlot(self, index):
|
||||
def _EthernetAddAllSlot(self):
|
||||
"""
|
||||
Loads the selected Linux interface in lineEdit.
|
||||
Adds all Ethernet interfaces.
|
||||
"""
|
||||
|
||||
for index in range(0, self.uiEthernetComboBox.count()):
|
||||
interface = self.uiEthernetComboBox.itemText(index)
|
||||
self._EthernetAddSlot(interface)
|
||||
|
||||
def _EthernetDeleteSlot(self):
|
||||
"""
|
||||
Deletes the selected Ethernet interface.
|
||||
"""
|
||||
|
||||
if self._node:
|
||||
for item in self.uiEthernetListWidget.selectedItems():
|
||||
interface = item.text()
|
||||
# check we can delete that interface
|
||||
for node_port in self._node.ports():
|
||||
if node_port.name() == interface and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to {}, please remove it first".format(interface))
|
||||
return
|
||||
|
||||
for item in self.uiEthernetListWidget.selectedItems():
|
||||
interface = item.text()
|
||||
for port in self._ports.copy():
|
||||
if port["name"] == interface:
|
||||
self._ports.remove(port)
|
||||
self.uiEthernetListWidget.takeItem(self.uiEthernetListWidget.row(item))
|
||||
for interface in self._interfaces:
|
||||
if not self.uiShowSpecialInterfacesCheckBox.isChecked() and interface["special"]:
|
||||
continue
|
||||
if interface["name"] == port["name"] and interface["type"] == "ethernet":
|
||||
self.uiEthernetComboBox.addItem(interface["name"])
|
||||
break
|
||||
break
|
||||
|
||||
def _TAPSelectedSlot(self, index):
|
||||
"""
|
||||
Loads the selected TAP interface.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
self.uiLinuxEthernetLineEdit.setText(self.uiLinuxEthernetComboBox.currentText())
|
||||
self.uiTAPLineEdit.setText(self.uiTAPComboBox.currentText())
|
||||
|
||||
def _linuxEthernetChangedSlot(self):
|
||||
def _TAPChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiLinuxEthernetListWidget.currentItem()
|
||||
item = self.uiTAPListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteLinuxEthernetPushButton.setEnabled(True)
|
||||
self.uiDeleteTAPPushButton.setEnabled(True)
|
||||
self.uiTAPLineEdit.setText(item.text())
|
||||
else:
|
||||
self.uiDeleteLinuxEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteTAPPushButton.setEnabled(False)
|
||||
|
||||
def _linuxEthernetAddSlot(self):
|
||||
def _TAPAddSlot(self, interface=None):
|
||||
"""
|
||||
Adds a new Linux Ethernet NIO.
|
||||
Adds a new TAP interface.
|
||||
"""
|
||||
|
||||
interface = self.uiLinuxEthernetLineEdit.text()
|
||||
if not interface:
|
||||
interface = self.uiTAPLineEdit.text()
|
||||
if interface:
|
||||
nio = "nio_gen_linux:{interface}".format(interface=interface)
|
||||
if nio not in self._nios:
|
||||
self.uiLinuxEthernetListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
|
||||
def _linuxEthernetDeleteSlot(self):
|
||||
"""
|
||||
Deletes the selected Linux Ethernet NIO.
|
||||
"""
|
||||
|
||||
item = self.uiLinuxEthernetListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
# check we can delete that NIO
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
for port in self._ports:
|
||||
if port["name"] == interface and port["type"] == "tap":
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiLinuxEthernetListWidget.takeItem(self.uiLinuxEthernetListWidget.currentRow())
|
||||
self.uiTAPListWidget.addItem(interface)
|
||||
self._ports.append({"name": interface,
|
||||
"port_number": len(self._ports),
|
||||
"type": "tap",
|
||||
"interface": interface})
|
||||
index = self.uiTAPComboBox.findText(interface)
|
||||
if index != -1:
|
||||
self.uiTAPComboBox.removeItem(index)
|
||||
|
||||
def _NIONATSelectedSlot(self, index):
|
||||
def _TAPAddAllSlot(self):
|
||||
"""
|
||||
Loads a selected NAT NIO.
|
||||
|
||||
:param index: ignored
|
||||
Adds all TAP interfaces
|
||||
"""
|
||||
|
||||
item = self.uiNIONATListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_nat:(.+)$""", nio)
|
||||
if match:
|
||||
self.uiNIONATIdentiferLineEdit.setText(match.group(1))
|
||||
for index in range(0, self.uiTAPComboBox.count()):
|
||||
interface = self.uiTAPComboBox.itemText(index)
|
||||
self._TAPAddSlot(interface)
|
||||
|
||||
def _NIONATChangedSlot(self):
|
||||
def _TAPDeleteSlot(self):
|
||||
"""
|
||||
Deletes a TAP interface.
|
||||
"""
|
||||
|
||||
if self._node:
|
||||
for item in self.uiTAPListWidget.selectedItems():
|
||||
interface = item.text()
|
||||
# check we can delete that interface
|
||||
for node_port in self._node.ports():
|
||||
if node_port.name() == interface and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to {}, please remove it first".format(interface))
|
||||
return
|
||||
|
||||
for item in self.uiTAPListWidget.selectedItems():
|
||||
interface = item.text()
|
||||
for port in self._ports.copy():
|
||||
if port["name"] == interface:
|
||||
self._ports.remove(port)
|
||||
self.uiTAPListWidget.takeItem(self.uiTAPListWidget.row(item))
|
||||
for interface in self._interfaces:
|
||||
if interface["name"] == port["name"] and interface["type"] == "tap":
|
||||
self.uiTAPComboBox.addItem(interface["name"])
|
||||
break
|
||||
|
||||
def _UDPSelectedSlot(self, item, column):
|
||||
"""
|
||||
Loads a selected UDP tunnel.
|
||||
|
||||
:param item: selected TreeWidgetItem instance
|
||||
:param column: ignored
|
||||
"""
|
||||
|
||||
name = item.text(0)
|
||||
local_port = int(item.text(1))
|
||||
remote_host = item.text(2)
|
||||
remote_port = int(item.text(3))
|
||||
self.uiUDPNameLineEdit.setText(name)
|
||||
self.uiLocalPortSpinBox.setValue(local_port)
|
||||
self.uiRemoteHostLineEdit.setText(remote_host)
|
||||
self.uiRemotePortSpinBox.setValue(remote_port)
|
||||
|
||||
def _UDPChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiNIONATListWidget.currentItem()
|
||||
item = self.uiUDPTreeWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIONATPushButton.setEnabled(True)
|
||||
self.uiDeleteUDPPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteNIONATPushButton.setEnabled(False)
|
||||
self.uiDeleteUDPPushButton.setEnabled(False)
|
||||
|
||||
def _NIONATAddSlot(self):
|
||||
def _UDPAddSlot(self):
|
||||
"""
|
||||
Adds a new NAT NIO.
|
||||
"""
|
||||
|
||||
identifier = self.uiNIONATIdentiferLineEdit.text()
|
||||
if identifier:
|
||||
nio = "nio_nat:{}".format(identifier)
|
||||
if nio not in self._nios:
|
||||
self.uiNIONATListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
|
||||
def _NIONATDeleteSlot(self):
|
||||
"""
|
||||
Deletes a NAT NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIONATListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
# check we can delete that NIO
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIONATListWidget.takeItem(self.uiNIONATListWidget.currentRow())
|
||||
|
||||
def _NIOUDPSelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected UDP.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIOUDPListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_udp:(\d+):(.+):(\d+)$""", nio)
|
||||
if match:
|
||||
self.uiLocalPortSpinBox.setValue(int(match.group(1)))
|
||||
self.uiRemoteHostLineEdit.setText(match.group(2))
|
||||
self.uiRemotePortSpinBox.setValue(int(match.group(3)))
|
||||
|
||||
def _NIOUDPChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiNIOUDPListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIOUDPPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteNIOUDPPushButton.setEnabled(False)
|
||||
|
||||
def _NIOUDPAddSlot(self):
|
||||
"""
|
||||
Adds a new UDP NIO.
|
||||
Adds a new UDP tunnel
|
||||
"""
|
||||
|
||||
name = self.uiUDPNameLineEdit.text()
|
||||
local_port = self.uiLocalPortSpinBox.value()
|
||||
remote_host = self.uiRemoteHostLineEdit.text()
|
||||
remote_port = self.uiRemotePortSpinBox.value()
|
||||
if remote_host:
|
||||
nio = "nio_udp:{lport}:{rhost}:{rport}".format(lport=local_port,
|
||||
rhost=remote_host,
|
||||
rport=remote_port)
|
||||
if nio not in self._nios:
|
||||
self.uiNIOUDPListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
self.uiLocalPortSpinBox.setValue(local_port + 1)
|
||||
self.uiRemotePortSpinBox.setValue(remote_port + 1)
|
||||
|
||||
def _NIOUDPDeleteSlot(self):
|
||||
"""
|
||||
Deletes an UDP NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIOUDPListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
# check we can delete that NIO
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
if name and remote_host:
|
||||
for port in self._ports:
|
||||
if port["name"] == name:
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIOUDPListWidget.takeItem(self.uiNIOUDPListWidget.currentRow())
|
||||
|
||||
def _NIOTAPSelectedSlot(self, index):
|
||||
# add a new entry in the tree widget
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiUDPTreeWidget)
|
||||
item.setText(0, name)
|
||||
item.setText(1, str(local_port))
|
||||
item.setText(2, remote_host)
|
||||
item.setText(3, str(remote_port))
|
||||
self.uiUDPTreeWidget.addTopLevelItem(item)
|
||||
self._ports.append({"name": name,
|
||||
"port_number": len(self._ports),
|
||||
"type": "udp",
|
||||
"lport": local_port,
|
||||
"rhost": remote_host,
|
||||
"rport": remote_port})
|
||||
self.uiLocalPortSpinBox.setValue(local_port + 1)
|
||||
self.uiRemotePortSpinBox.setValue(remote_port + 1)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(0)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(1)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(2)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(3)
|
||||
nb_tunnels = 0
|
||||
for port in self._ports:
|
||||
if port["type"] == "udp":
|
||||
nb_tunnels += 1
|
||||
self.uiUDPNameLineEdit.setText("UDP tunnel {}".format(nb_tunnels + 1))
|
||||
|
||||
def _UDPDeleteSlot(self):
|
||||
"""
|
||||
Loads the selected NIO TAP in lineEdit.
|
||||
|
||||
:param index: ignored
|
||||
Deletes an UDP tunnel.
|
||||
"""
|
||||
|
||||
item = self.uiNIOTAPListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_tap:(.+)$""", nio)
|
||||
if match:
|
||||
self.uiNIOTAPLineEdit.setText(match.group(1))
|
||||
if self._node:
|
||||
for item in self.uiUDPTreeWidget.selectedItems():
|
||||
name = item.text(0)
|
||||
# check we can delete that UDP tunnel
|
||||
for node_port in self._node.ports():
|
||||
if node_port.name() == name and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to {}, please remove it first".format(name))
|
||||
return
|
||||
|
||||
def _NIOTAPChangedSlot(self):
|
||||
for item in self.uiUDPTreeWidget.selectedItems():
|
||||
name = item.text(0)
|
||||
for port in self._ports.copy():
|
||||
if port["name"] == name:
|
||||
self._ports.remove(port)
|
||||
self.uiUDPTreeWidget.takeTopLevelItem(self.uiUDPTreeWidget.indexOfTopLevelItem(item))
|
||||
nb_tunnels = 0
|
||||
for port in self._ports:
|
||||
if port["type"] == "udp":
|
||||
nb_tunnels += 1
|
||||
self.uiUDPNameLineEdit.setText("UDP tunnel {}".format(nb_tunnels + 1))
|
||||
|
||||
def _showSpecialInterfacesSlot(self, state):
|
||||
|
||||
self.uiEthernetComboBox.clear()
|
||||
index = 0
|
||||
for interface in self._interfaces:
|
||||
if interface["type"] == "ethernet":
|
||||
if not state and interface["special"]:
|
||||
continue
|
||||
if self.uiEthernetListWidget.findItems(interface["name"], QtCore.Qt.MatchFixedString):
|
||||
continue
|
||||
self.uiEthernetComboBox.addItem(interface["name"])
|
||||
index += 1
|
||||
|
||||
def _symbolBrowserSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
Slot to open the symbol browser and select a new symbol.
|
||||
"""
|
||||
|
||||
item = self.uiNIOTAPListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIOTAPPushButton.setEnabled(True)
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
new_symbol_path = dialog.getSymbol()
|
||||
self.uiSymbolLineEdit.setText(new_symbol_path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
|
||||
|
||||
def _loadNetworkInterfaces(self, interfaces):
|
||||
|
||||
self.uiEthernetComboBox.clear()
|
||||
index = 0
|
||||
for interface in interfaces:
|
||||
if interface["type"] == "ethernet" and not interface["special"]:
|
||||
self.uiEthernetComboBox.addItem(interface["name"])
|
||||
index += 1
|
||||
|
||||
# load all TAP interfaces
|
||||
self.uiTAPComboBox.clear()
|
||||
index = 0
|
||||
for interface in interfaces:
|
||||
if interface["type"] == "tap":
|
||||
self.uiTAPComboBox.addItem(interface["name"])
|
||||
index += 1
|
||||
|
||||
def _getInterfacesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for retrieving the network interfaces
|
||||
|
||||
:param progress_dialog: QProgressDialog instance
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Network interfaces", "{}".format(result["message"]))
|
||||
else:
|
||||
self.uiDeleteNIOTAPPushButton.setEnabled(False)
|
||||
self._interfaces = result
|
||||
self._loadNetworkInterfaces(result)
|
||||
|
||||
def _NIOTAPAddSlot(self):
|
||||
"""
|
||||
Adds a new UDP NIO.
|
||||
"""
|
||||
|
||||
tap_interface = self.uiNIOTAPLineEdit.text()
|
||||
if tap_interface:
|
||||
nio = "nio_tap:{}".format(tap_interface.lower())
|
||||
if nio not in self._nios:
|
||||
self.uiNIOTAPListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
|
||||
def _NIOTAPDeleteSlot(self):
|
||||
"""
|
||||
Deletes a TAP NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIOTAPListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
# check we can delete that NIO
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIOTAPListWidget.takeItem(self.uiNIOTAPListWidget.currentRow())
|
||||
|
||||
def _NIOUNIXSelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected UNIX NIO.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIOUNIXListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_unix:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
self.uiLocalFileLineEdit.setText(match.group(1))
|
||||
self.uiRemoteFileLineEdit.setText(match.group(2))
|
||||
|
||||
def _NIOUNIXChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiNIOUNIXListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIOUNIXPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteNIOUNIXPushButton.setEnabled(False)
|
||||
|
||||
def _NIOUNIXAddSlot(self):
|
||||
"""
|
||||
Adds a new UNIX NIO.
|
||||
"""
|
||||
|
||||
local_file = self.uiLocalFileLineEdit.text()
|
||||
remote_file = self.uiRemoteFileLineEdit.text()
|
||||
if local_file and remote_file:
|
||||
nio = "nio_unix:{local}:{remote}".format(local=local_file,
|
||||
remote=remote_file)
|
||||
if nio not in self._nios:
|
||||
self.uiNIOUNIXListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
|
||||
def _NIOUNIXDeleteSlot(self):
|
||||
"""
|
||||
Deletes an UNIX NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIOUNIXListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
# check we can delete that NIO
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIOUNIXListWidget.takeItem(self.uiNIOUNIXListWidget.currentRow())
|
||||
|
||||
def _NIOVDESelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected VDE NIO.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIOVDEListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_vde:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
self.uiVDEControlFileLineEdit.setText(match.group(1))
|
||||
self.uiVDELocalFileLineEdit.setText(match.group(2))
|
||||
|
||||
def _NIOVDEChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiNIOVDEListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIOVDEPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteNIOVDEPushButton.setEnabled(False)
|
||||
|
||||
def _NIOVDEAddSlot(self):
|
||||
"""
|
||||
Adds a new VDE NIO.
|
||||
"""
|
||||
|
||||
control_file = self.uiVDEControlFileLineEdit.text()
|
||||
local_file = self.uiVDELocalFileLineEdit.text()
|
||||
if local_file and control_file:
|
||||
nio = "nio_vde:{control}:{local}".format(control=control_file, local=local_file)
|
||||
if nio not in self._nios:
|
||||
self.uiNIOVDEListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
|
||||
def _NIOVDEDeleteSlot(self):
|
||||
"""
|
||||
Deletes a VDE NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIOVDEListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
# check we can delete that NIO
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIOVDEListWidget.takeItem(self.uiNIOVDEListWidget.currentRow())
|
||||
|
||||
def _NIONullSelectedSlot(self, index):
|
||||
"""
|
||||
Loads a selected NULL NIO.
|
||||
|
||||
:param index: ignored
|
||||
"""
|
||||
|
||||
item = self.uiNIONullListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
match = re.search(r"""^nio_null:(.+)$""", nio)
|
||||
if match:
|
||||
self.uiNIONullIdentiferLineEdit.setText(match.group(1))
|
||||
|
||||
def _NIONullChangedSlot(self):
|
||||
"""
|
||||
Enables the use of the delete button.
|
||||
"""
|
||||
|
||||
item = self.uiNIONullListWidget.currentItem()
|
||||
if item:
|
||||
self.uiDeleteNIONullPushButton.setEnabled(True)
|
||||
else:
|
||||
self.uiDeleteNIONullPushButton.setEnabled(False)
|
||||
|
||||
def _NIONullAddSlot(self):
|
||||
"""
|
||||
Adds a new NULL NIO.
|
||||
"""
|
||||
|
||||
identifier = self.uiNIONullIdentiferLineEdit.text()
|
||||
if identifier:
|
||||
nio = "nio_null:{}".format(identifier)
|
||||
if nio not in self._nios:
|
||||
self.uiNIONullListWidget.addItem(nio)
|
||||
self._nios.append(nio)
|
||||
|
||||
def _NIONullDeleteSlot(self):
|
||||
"""
|
||||
Deletes a NULL NIO.
|
||||
"""
|
||||
|
||||
item = self.uiNIONullListWidget.currentItem()
|
||||
if item:
|
||||
nio = item.text()
|
||||
# check we can delete that NIO
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.name() == nio and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to NIO {}, please remove it first".format(nio))
|
||||
return
|
||||
self._nios.remove(nio)
|
||||
self.uiNIONullListWidget.takeItem(self.uiNIONullListWidget.currentRow())
|
||||
|
||||
def loadSettings(self, settings, node, group=False):
|
||||
def loadSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Loads the cloud settings.
|
||||
|
||||
@@ -533,58 +376,72 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
else:
|
||||
self.uiNameLineEdit.setEnabled(False)
|
||||
|
||||
self._node = node
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# load all network interfaces
|
||||
self.uiGenericEthernetComboBox.clear()
|
||||
index = 0
|
||||
for interface in settings["interfaces"]:
|
||||
if interface["name"].startswith("tap"):
|
||||
# do not add TAP interfaces
|
||||
continue
|
||||
self.uiGenericEthernetComboBox.addItem(interface["name"])
|
||||
self.uiGenericEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
|
||||
index += 1
|
||||
self.uiGenericEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
# rename the label from "Name" to "Template name"
|
||||
self.uiNameLabel.setText("Template name:")
|
||||
|
||||
# load all network interfaces
|
||||
self.uiLinuxEthernetComboBox.clear()
|
||||
index = 0
|
||||
for interface in settings["interfaces"]:
|
||||
if not interface["name"].startswith(r"\Device\NPF_") and not interface["name"].startswith("tap"):
|
||||
self.uiLinuxEthernetComboBox.addItem(interface["name"])
|
||||
self.uiLinuxEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
|
||||
index += 1
|
||||
self.uiLinuxEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
# load the default name format
|
||||
self.uiDefaultNameFormatLineEdit.setText(settings["default_name_format"])
|
||||
|
||||
# populate the NIO lists
|
||||
self.nios = []
|
||||
self.uiGenericEthernetListWidget.clear()
|
||||
self.uiLinuxEthernetListWidget.clear()
|
||||
self.uiNIOUDPListWidget.clear()
|
||||
self.uiNIOTAPListWidget.clear()
|
||||
self.uiNIOUNIXListWidget.clear()
|
||||
self.uiNIOVDEListWidget.clear()
|
||||
self.uiNIONullListWidget.clear()
|
||||
# load the symbol
|
||||
self.uiSymbolLineEdit.setText(settings["symbol"])
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(settings["symbol"]))
|
||||
|
||||
for nio in settings["nios"]:
|
||||
self._nios.append(nio)
|
||||
if nio.lower().startswith("nio_gen_eth"):
|
||||
self.uiGenericEthernetListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_gen_linux"):
|
||||
self.uiLinuxEthernetListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_udp"):
|
||||
self.uiNIOUDPListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_tap"):
|
||||
self.uiNIOTAPListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_unix"):
|
||||
self.uiNIOUNIXListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_vde"):
|
||||
self.uiNIOVDEListWidget.addItem(nio)
|
||||
elif nio.lower().startswith("nio_null"):
|
||||
self.uiNIONullListWidget.addItem(nio)
|
||||
# load the category
|
||||
index = self.uiCategoryComboBox.findData(settings["category"])
|
||||
if index != -1:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
|
||||
def saveSettings(self, settings, node, group=False):
|
||||
Controller.instance().getCompute("/network/interfaces", settings["server"],
|
||||
self._getInterfacesFromServerCallback,
|
||||
progressText="Retrieving network interfaces...")
|
||||
|
||||
else:
|
||||
self.uiDefaultNameFormatLabel.hide()
|
||||
self.uiDefaultNameFormatLineEdit.hide()
|
||||
self.uiSymbolLabel.hide()
|
||||
self.uiSymbolLineEdit.hide()
|
||||
self.uiSymbolToolButton.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
self.uiCategoryLabel.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
self._node = node
|
||||
self._interfaces = self._node.interfaces()
|
||||
self._loadNetworkInterfaces(self._interfaces)
|
||||
|
||||
# load the current ports
|
||||
self._ports = []
|
||||
self.uiEthernetListWidget.clear()
|
||||
self.uiTAPListWidget.clear()
|
||||
self.uiUDPTreeWidget.clear()
|
||||
|
||||
for port in settings["ports_mapping"]:
|
||||
self._ports.append(port)
|
||||
if port["type"] == "ethernet":
|
||||
self.uiEthernetListWidget.addItem(port["name"])
|
||||
index = self.uiEthernetComboBox.findText(port["name"])
|
||||
if index != -1:
|
||||
self.uiEthernetComboBox.removeItem(index)
|
||||
elif port["type"] == "tap":
|
||||
self.uiTAPListWidget.addItem(port["name"])
|
||||
index = self.uiTAPComboBox.findText(port["name"])
|
||||
if index != -1:
|
||||
self.uiTAPComboBox.removeItem(index)
|
||||
elif port["type"] == "udp":
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiUDPTreeWidget)
|
||||
item.setText(0, port["name"])
|
||||
item.setText(1, str(port["lport"]))
|
||||
item.setText(2, port["rhost"])
|
||||
item.setText(3, str(port["rport"]))
|
||||
self.uiUDPTreeWidget.addTopLevelItem(item)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(0)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(1)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(2)
|
||||
self.uiUDPTreeWidget.resizeColumnToContents(3)
|
||||
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Saves the cloud settings.
|
||||
|
||||
@@ -595,7 +452,22 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
|
||||
if not group:
|
||||
settings["name"] = self.uiNameLineEdit.text()
|
||||
else:
|
||||
del settings["name"]
|
||||
|
||||
settings["nios"] = list(self._nios)
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# save the default name format
|
||||
default_name_format = self.uiDefaultNameFormatLineEdit.text().strip()
|
||||
if '{0}' not in default_name_format and '{id}' not in default_name_format:
|
||||
QtWidgets.QMessageBox.critical(self, "Default name format", "The default name format must contain at least {0} or {id}")
|
||||
else:
|
||||
settings["default_name_format"] = default_name_format
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
settings["symbol"] = symbol_path
|
||||
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
settings["ports_mapping"] = self._ports
|
||||
else:
|
||||
settings["ports_mapping"] = self._ports
|
||||
return settings
|
||||
|
||||
185
gns3/modules/builtin/pages/cloud_preferences_page.py
Normal file
185
gns3/modules/builtin/pages/cloud_preferences_page.py
Normal file
@@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for cloud node preferences.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.controller import Controller
|
||||
|
||||
from .. import Builtin
|
||||
from ..settings import CLOUD_SETTINGS
|
||||
from ..ui.cloud_preferences_page_ui import Ui_CloudPreferencesPageWidget
|
||||
from ..pages.cloud_configuration_page import CloudConfigurationPage
|
||||
from ..dialogs.cloud_wizard import CloudWizard
|
||||
|
||||
|
||||
class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
"""
|
||||
QWidget preference page for cloud node preferences.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = MainWindow.instance()
|
||||
self._cloud_nodes = {}
|
||||
self._items = []
|
||||
|
||||
self.uiNewCloudNodePushButton.clicked.connect(self._newCloudNodeSlot)
|
||||
self.uiEditCloudNodePushButton.clicked.connect(self._editCloudNodeSlot)
|
||||
self.uiDeleteCloudNodePushButton.clicked.connect(self._deleteCloudNodeSlot)
|
||||
self.uiCloudNodesTreeWidget.itemSelectionChanged.connect(self._cloudNodeChangedSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
|
||||
section_item = QtWidgets.QTreeWidgetItem(self.uiCloudNodeInfoTreeWidget)
|
||||
section_item.setText(0, name)
|
||||
font = section_item.font(0)
|
||||
font.setBold(True)
|
||||
section_item.setFont(0, font)
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, cloud_node):
|
||||
|
||||
self.uiCloudNodeInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", cloud_node["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", cloud_node["default_name_format"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["server"]).name()])
|
||||
|
||||
self.uiCloudNodeInfoTreeWidget.expandAll()
|
||||
self.uiCloudNodeInfoTreeWidget.resizeColumnToContents(0)
|
||||
self.uiCloudNodeInfoTreeWidget.resizeColumnToContents(1)
|
||||
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _cloudNodeChangedSlot(self):
|
||||
"""
|
||||
Loads a selected cloud node template from the tree widget.
|
||||
"""
|
||||
|
||||
selection = self.uiCloudNodesTreeWidget.selectedItems()
|
||||
self.uiDeleteCloudNodePushButton.setEnabled(len(selection) != 0)
|
||||
single_selected = len(selection) == 1
|
||||
self.uiEditCloudNodePushButton.setEnabled(single_selected)
|
||||
|
||||
if single_selected:
|
||||
key = selection[0].data(0, QtCore.Qt.UserRole)
|
||||
cloud_node = self._cloud_nodes[key]
|
||||
self._refreshInfo(cloud_node)
|
||||
else:
|
||||
self.uiCloudNodeInfoTreeWidget.clear()
|
||||
|
||||
def _newCloudNodeSlot(self):
|
||||
"""
|
||||
Creates a new cloud node template.
|
||||
"""
|
||||
|
||||
wizard = CloudWizard(self._cloud_nodes, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_cloud_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_cloud_settings["server"], name=new_cloud_settings["name"])
|
||||
self._cloud_nodes[key] = CLOUD_SETTINGS.copy()
|
||||
self._cloud_nodes[key].update(new_cloud_settings)
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiCloudNodesTreeWidget)
|
||||
item.setText(0, self._cloud_nodes[key]["name"])
|
||||
Controller.instance().getSymbolIcon(self._cloud_nodes[key]["symbol"], qpartial(self._setItemIcon, item))
|
||||
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
self.uiCloudNodesTreeWidget.setCurrentItem(item)
|
||||
|
||||
def _editCloudNodeSlot(self):
|
||||
"""
|
||||
Edits a cloud node template.
|
||||
"""
|
||||
|
||||
item = self.uiCloudNodesTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
cloud_node = self._cloud_nodes[key]
|
||||
dialog = ConfigurationDialog(cloud_node["name"], cloud_node, CloudConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(cloud_node["symbol"], qpartial(self._setItemIcon, item))
|
||||
if cloud_node["name"] != item.text(0):
|
||||
new_key = "{server}:{name}".format(server=cloud_node["server"], name=cloud_node["name"])
|
||||
if new_key in self._cloud_nodes:
|
||||
QtWidgets.QMessageBox.critical(self, "Cloud node", "Cloud node name {} already exists for server {}".format(cloud_node["name"],
|
||||
cloud_node["server"]))
|
||||
cloud_node["name"] = item.text(0)
|
||||
return
|
||||
self._cloud_nodes[new_key] = self._cloud_nodes[key]
|
||||
del self._cloud_nodes[key]
|
||||
item.setText(0, cloud_node["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(cloud_node)
|
||||
|
||||
def _deleteCloudNodeSlot(self):
|
||||
"""
|
||||
Deletes a cloud node template.
|
||||
"""
|
||||
|
||||
for item in self.uiCloudNodesTreeWidget.selectedItems():
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
del self._cloud_nodes[key]
|
||||
self.uiCloudNodesTreeWidget.takeTopLevelItem(self.uiCloudNodesTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
def loadPreferences(self):
|
||||
"""
|
||||
Loads the cloud node preferences.
|
||||
"""
|
||||
|
||||
builtin_module = Builtin.instance()
|
||||
self._cloud_nodes = copy.deepcopy(builtin_module.cloudNodes())
|
||||
self._items.clear()
|
||||
|
||||
for key, cloud_node in self._cloud_nodes.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiCloudNodesTreeWidget)
|
||||
item.setText(0, cloud_node["name"])
|
||||
Controller.instance().getSymbolIcon(cloud_node["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
|
||||
if self._items:
|
||||
self.uiCloudNodesTreeWidget.setCurrentItem(self._items[0])
|
||||
self.uiCloudNodesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
item.setIcon(0, icon)
|
||||
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def savePreferences(self):
|
||||
"""
|
||||
Saves the cloud node preferences.
|
||||
"""
|
||||
|
||||
Builtin.instance().setCloudNodes(self._cloud_nodes)
|
||||
152
gns3/modules/builtin/pages/ethernet_hub_configuration_page.py
Normal file
152
gns3/modules/builtin/pages/ethernet_hub_configuration_page.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for Ethernet hubs.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.dialogs.node_properties_dialog import ConfigurationError
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.node import Node
|
||||
|
||||
from ..ui.ethernet_hub_configuration_page_ui import Ui_ethernetHubConfigPageWidget
|
||||
|
||||
|
||||
class EthernetHubConfigurationPage(QtWidgets.QWidget, Ui_ethernetHubConfigPageWidget):
|
||||
|
||||
"""
|
||||
QWidget configuration page for Ethernet hubs.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
# add the categories
|
||||
for name, category in Node.defaultCategories().items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
|
||||
def _symbolBrowserSlot(self):
|
||||
"""
|
||||
Slot to open the symbol browser and select a new symbol.
|
||||
"""
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
new_symbol_path = dialog.getSymbol()
|
||||
self.uiSymbolLineEdit.setText(new_symbol_path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
|
||||
|
||||
def loadSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Loads the Ethernet hub settings.
|
||||
|
||||
:param settings: the settings (dictionary)
|
||||
:param node: Node instance
|
||||
:param group: indicates the settings apply to a group
|
||||
"""
|
||||
|
||||
if not group:
|
||||
self.uiNameLineEdit.setText(settings["name"])
|
||||
else:
|
||||
self.uiNameLineEdit.hide()
|
||||
self.uiNameLabel.hide()
|
||||
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# rename the label from "Name" to "Template name"
|
||||
self.uiNameLabel.setText("Template name:")
|
||||
|
||||
# load the default name format
|
||||
self.uiDefaultNameFormatLineEdit.setText(settings["default_name_format"])
|
||||
|
||||
# load the symbol
|
||||
self.uiSymbolLineEdit.setText(settings["symbol"])
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(settings["symbol"]))
|
||||
|
||||
# load the category
|
||||
index = self.uiCategoryComboBox.findData(settings["category"])
|
||||
if index != -1:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
self.uiDefaultNameFormatLabel.hide()
|
||||
self.uiDefaultNameFormatLineEdit.hide()
|
||||
self.uiSymbolLabel.hide()
|
||||
self.uiSymbolLineEdit.hide()
|
||||
self.uiSymbolToolButton.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
self.uiCategoryLabel.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
|
||||
nb_ports = len(settings["ports_mapping"])
|
||||
self.uiPortsSpinBox.setValue(nb_ports)
|
||||
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Saves the Ethernet hub settings.
|
||||
|
||||
:param settings: the settings (dictionary)
|
||||
:param node: Node instance
|
||||
:param group: indicates the settings apply to a group
|
||||
"""
|
||||
|
||||
if not group:
|
||||
# set the device name
|
||||
name = self.uiNameLineEdit.text()
|
||||
if not name:
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "Ethernet hub name cannot be empty!")
|
||||
else:
|
||||
settings["name"] = name
|
||||
|
||||
nb_ports = self.uiPortsSpinBox.value()
|
||||
|
||||
if node:
|
||||
# check that a link isn't connected to a port before we delete it
|
||||
ports = node.ports()
|
||||
for port in ports:
|
||||
if not port.isFree() and port.portNumber() > nb_ports:
|
||||
self.loadSettings(settings, node)
|
||||
QtWidgets.QMessageBox.critical(self, node.name(), "A link is connected to port {}, please remove it first".format(port.name()))
|
||||
raise ConfigurationError()
|
||||
|
||||
else:
|
||||
# these are template settings
|
||||
|
||||
# save the default name format
|
||||
default_name_format = self.uiDefaultNameFormatLineEdit.text().strip()
|
||||
if '{0}' not in default_name_format and '{id}' not in default_name_format:
|
||||
QtWidgets.QMessageBox.critical(self, "Default name format", "The default name format must contain at least {0} or {id}")
|
||||
else:
|
||||
settings["default_name_format"] = default_name_format
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
settings["symbol"] = symbol_path
|
||||
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
|
||||
settings["ports_mapping"] = []
|
||||
for port_number in range(1, nb_ports + 1):
|
||||
settings["ports_mapping"].append({"port_number": int(port_number),
|
||||
"name": "Ethernet{}".format(port_number)})
|
||||
return settings
|
||||
186
gns3/modules/builtin/pages/ethernet_hub_preferences_page.py
Normal file
186
gns3/modules/builtin/pages/ethernet_hub_preferences_page.py
Normal file
@@ -0,0 +1,186 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for Ethernet hub preferences.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.controller import Controller
|
||||
|
||||
from .. import Builtin
|
||||
from ..settings import ETHERNET_HUB_SETTINGS
|
||||
from ..ui.ethernet_hub_preferences_page_ui import Ui_EthernetHubPreferencesPageWidget
|
||||
from ..pages.ethernet_hub_configuration_page import EthernetHubConfigurationPage
|
||||
from ..dialogs.ethernet_hub_wizard import EthernetHubWizard
|
||||
|
||||
|
||||
class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPageWidget):
|
||||
"""
|
||||
QWidget preference page for Ethernet hub preferences.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = MainWindow.instance()
|
||||
self._ethernet_hubs = {}
|
||||
self._items = []
|
||||
|
||||
self.uiNewEthernetHubPushButton.clicked.connect(self._newEthernetHubSlot)
|
||||
self.uiEditEthernetHubPushButton.clicked.connect(self._editEthernetHubSlot)
|
||||
self.uiDeleteEthernetHubPushButton.clicked.connect(self._deleteEthernetHubSlot)
|
||||
self.uiEthernetHubsTreeWidget.itemSelectionChanged.connect(self._ethernetHubChangedSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
|
||||
section_item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubInfoTreeWidget)
|
||||
section_item.setText(0, name)
|
||||
font = section_item.font(0)
|
||||
font.setBold(True)
|
||||
section_item.setFont(0, font)
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, ethernet_hub):
|
||||
|
||||
self.uiEthernetHubInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_hub["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_hub["default_name_format"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["server"]).name()])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Number of ports:", str(len(ethernet_hub["ports_mapping"]))])
|
||||
|
||||
self.uiEthernetHubInfoTreeWidget.expandAll()
|
||||
self.uiEthernetHubInfoTreeWidget.resizeColumnToContents(0)
|
||||
self.uiEthernetHubInfoTreeWidget.resizeColumnToContents(1)
|
||||
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _ethernetHubChangedSlot(self):
|
||||
"""
|
||||
Loads a selected Ethernet hub template from the tree widget.
|
||||
"""
|
||||
|
||||
selection = self.uiEthernetHubsTreeWidget.selectedItems()
|
||||
self.uiDeleteEthernetHubPushButton.setEnabled(len(selection) != 0)
|
||||
single_selected = len(selection) == 1
|
||||
self.uiEditEthernetHubPushButton.setEnabled(single_selected)
|
||||
|
||||
if single_selected:
|
||||
key = selection[0].data(0, QtCore.Qt.UserRole)
|
||||
ethernet_hub = self._ethernet_hubs[key]
|
||||
self._refreshInfo(ethernet_hub)
|
||||
else:
|
||||
self.uiEthernetHubInfoTreeWidget.clear()
|
||||
|
||||
def _newEthernetHubSlot(self):
|
||||
"""
|
||||
Creates a new Ethernet hub template.
|
||||
"""
|
||||
|
||||
wizard = EthernetHubWizard(self._ethernet_hubs, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_ethernet_hub_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_ethernet_hub_settings["server"], name=new_ethernet_hub_settings["name"])
|
||||
self._ethernet_hubs[key] = ETHERNET_HUB_SETTINGS.copy()
|
||||
self._ethernet_hubs[key].update(new_ethernet_hub_settings)
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubsTreeWidget)
|
||||
item.setText(0, self._ethernet_hubs[key]["name"])
|
||||
Controller.instance().getSymbolIcon(self._ethernet_hubs[key]["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
self.uiEthernetHubsTreeWidget.setCurrentItem(item)
|
||||
|
||||
def _editEthernetHubSlot(self):
|
||||
"""
|
||||
Edits an Ethernet hub template.
|
||||
"""
|
||||
|
||||
item = self.uiEthernetHubsTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
ethernet_hub = self._ethernet_hubs[key]
|
||||
dialog = ConfigurationDialog(ethernet_hub["name"], ethernet_hub, EthernetHubConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(ethernet_hub["symbol"], qpartial(self._setItemIcon, item))
|
||||
if ethernet_hub["name"] != item.text(0):
|
||||
new_key = "{server}:{name}".format(server=ethernet_hub["server"], name=ethernet_hub["name"])
|
||||
if new_key in self._ethernet_hubs:
|
||||
QtWidgets.QMessageBox.critical(self, "Ethernet hub", "Ethernet hub name {} already exists for server {}".format(ethernet_hub["name"],
|
||||
ethernet_hub["server"]))
|
||||
ethernet_hub["name"] = item.text(0)
|
||||
return
|
||||
self._ethernet_hubs[new_key] = self._ethernet_hubs[key]
|
||||
del self._ethernet_hubs[key]
|
||||
item.setText(0, ethernet_hub["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(ethernet_hub)
|
||||
|
||||
def _deleteEthernetHubSlot(self):
|
||||
"""
|
||||
Deletes an Ethernet hub template.
|
||||
"""
|
||||
|
||||
for item in self.uiEthernetHubsTreeWidget.selectedItems():
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
del self._ethernet_hubs[key]
|
||||
self.uiEthernetHubsTreeWidget.takeTopLevelItem(self.uiEthernetHubsTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
def loadPreferences(self):
|
||||
"""
|
||||
Loads the ethernet hub preferences.
|
||||
"""
|
||||
|
||||
builtin_module = Builtin.instance()
|
||||
self._ethernet_hubs = copy.deepcopy(builtin_module.ethernetHubs())
|
||||
self._items.clear()
|
||||
|
||||
for key, ethernet_hub in self._ethernet_hubs.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubsTreeWidget)
|
||||
item.setText(0, ethernet_hub["name"])
|
||||
Controller.instance().getSymbolIcon(ethernet_hub["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
|
||||
if self._items:
|
||||
self.uiEthernetHubsTreeWidget.setCurrentItem(self._items[0])
|
||||
self.uiEthernetHubsTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
item.setIcon(0, icon)
|
||||
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def savePreferences(self):
|
||||
"""
|
||||
Saves the Ethernet hub preferences.
|
||||
"""
|
||||
|
||||
Builtin.instance().setEthernetHubs(self._ethernet_hubs)
|
||||
@@ -16,10 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for Dynamips Ethernet switches.
|
||||
Configuration page for Ethernet switches.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
from gns3.qt import QtGui, QtCore, QtWidgets
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.node import Node
|
||||
|
||||
from ..utils.tree_widget_item import TreeWidgetItem
|
||||
from ..ui.ethernet_switch_configuration_page_ui import Ui_ethernetSwitchConfigPageWidget
|
||||
|
||||
@@ -36,6 +39,10 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
self.setupUi(self)
|
||||
self._ports = {}
|
||||
|
||||
# add the categories
|
||||
for name, category in Node.defaultCategories().items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
|
||||
# connect slots
|
||||
self.uiAddPushButton.clicked.connect(self._addPortSlot)
|
||||
self.uiDeletePushButton.clicked.connect(self._deletePortSlot)
|
||||
@@ -47,6 +54,21 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
self.uiPortsTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiPortsTreeWidget.setSortingEnabled(True)
|
||||
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
|
||||
def _symbolBrowserSlot(self):
|
||||
"""
|
||||
Slot to open the symbol browser and select a new symbol.
|
||||
"""
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
new_symbol_path = dialog.getSymbol()
|
||||
self.uiSymbolLineEdit.setText(new_symbol_path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
|
||||
|
||||
def _portSelectedSlot(self, item, column):
|
||||
"""
|
||||
Loads a selected port from the tree widget.
|
||||
@@ -123,7 +145,9 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
item.setText(3, port_ethertype)
|
||||
self.uiPortsTreeWidget.addTopLevelItem(item)
|
||||
|
||||
self._ports[port] = {"type": port_type,
|
||||
self._ports[port] = {"name": "Ethernet{}".format(port),
|
||||
"port_number": port,
|
||||
"type": port_type,
|
||||
"vlan": vlan,
|
||||
"ethertype": port_ethertype}
|
||||
|
||||
@@ -138,11 +162,11 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
item = self.uiPortsTreeWidget.currentItem()
|
||||
if item:
|
||||
port = int(item.text(0))
|
||||
node_ports = self._node.ports()
|
||||
for node_port in node_ports:
|
||||
if node_port.portNumber() == port and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to port {}, please remove it first".format(node_port.name()))
|
||||
return
|
||||
if self._node:
|
||||
for node_port in self._node.ports():
|
||||
if node_port.portNumber() == port and not node_port.isFree():
|
||||
QtWidgets.QMessageBox.critical(self, self._node.name(), "A link is connected to port {}, please remove it first".format(node_port.name()))
|
||||
return
|
||||
del self._ports[port]
|
||||
self.uiPortsTreeWidget.takeTopLevelItem(self.uiPortsTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
@@ -151,7 +175,7 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
else:
|
||||
self.uiPortSpinBox.setValue(1)
|
||||
|
||||
def loadSettings(self, settings, node, group=False):
|
||||
def loadSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Loads the Ethernet switch settings.
|
||||
|
||||
@@ -169,21 +193,48 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
self._ports = {}
|
||||
self._node = node
|
||||
|
||||
for port, info in settings["ports"].items():
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# rename the label from "Name" to "Template name"
|
||||
self.uiNameLabel.setText("Template name:")
|
||||
|
||||
# load the default name format
|
||||
self.uiDefaultNameFormatLineEdit.setText(settings["default_name_format"])
|
||||
|
||||
# load the symbol
|
||||
self.uiSymbolLineEdit.setText(settings["symbol"])
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(settings["symbol"]))
|
||||
|
||||
# load the category
|
||||
index = self.uiCategoryComboBox.findData(settings["category"])
|
||||
if index != -1:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
self.uiDefaultNameFormatLabel.hide()
|
||||
self.uiDefaultNameFormatLineEdit.hide()
|
||||
self.uiSymbolLabel.hide()
|
||||
self.uiSymbolLineEdit.hide()
|
||||
self.uiSymbolToolButton.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
self.uiCategoryLabel.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
|
||||
for port_info in settings["ports_mapping"]:
|
||||
item = TreeWidgetItem(self.uiPortsTreeWidget)
|
||||
item.setText(0, str(port))
|
||||
item.setText(1, str(info["vlan"]))
|
||||
item.setText(2, info["type"])
|
||||
item.setText(3, info["ethertype"])
|
||||
item.setText(0, str(port_info["port_number"]))
|
||||
item.setText(1, str(port_info["vlan"]))
|
||||
item.setText(2, port_info["type"])
|
||||
item.setText(3, port_info.get("ethertype", ""))
|
||||
self.uiPortsTreeWidget.addTopLevelItem(item)
|
||||
self._ports[port] = info
|
||||
self._ports[port_info["port_number"]] = port_info
|
||||
|
||||
self.uiPortsTreeWidget.resizeColumnToContents(0)
|
||||
self.uiPortsTreeWidget.resizeColumnToContents(1)
|
||||
if len(self._ports) > 0:
|
||||
self.uiPortSpinBox.setValue(max(self._ports) + 1)
|
||||
|
||||
def saveSettings(self, settings, node, group=False):
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Saves the Ethernet switch settings.
|
||||
|
||||
@@ -199,7 +250,21 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "Ethernet switch name cannot be empty!")
|
||||
else:
|
||||
settings["name"] = name
|
||||
else:
|
||||
del settings["name"]
|
||||
|
||||
settings["ports"] = self._ports.copy()
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
# save the default name format
|
||||
default_name_format = self.uiDefaultNameFormatLineEdit.text().strip()
|
||||
if '{0}' not in default_name_format and '{id}' not in default_name_format:
|
||||
QtWidgets.QMessageBox.critical(self, "Default name format", "The default name format must contain at least {0} or {id}")
|
||||
else:
|
||||
settings["default_name_format"] = default_name_format
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
settings["symbol"] = symbol_path
|
||||
|
||||
settings["category"] = self.uiCategoryComboBox.itemData(self.uiCategoryComboBox.currentIndex())
|
||||
|
||||
settings["ports_mapping"] = list(self._ports.values())
|
||||
return settings
|
||||
191
gns3/modules/builtin/pages/ethernet_switch_preferences_page.py
Normal file
191
gns3/modules/builtin/pages/ethernet_switch_preferences_page.py
Normal file
@@ -0,0 +1,191 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Configuration page for Ethernet switch preferences.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from gns3.controller import Controller
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.compute_manager import ComputeManager
|
||||
|
||||
from .. import Builtin
|
||||
from ..settings import ETHERNET_SWITCH_SETTINGS
|
||||
from ..ui.ethernet_switch_preferences_page_ui import Ui_EthernetSwitchPreferencesPageWidget
|
||||
from ..pages.ethernet_switch_configuration_page import EthernetSwitchConfigurationPage
|
||||
from ..dialogs.ethernet_switch_wizard import EthernetSwitchWizard
|
||||
|
||||
|
||||
class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferencesPageWidget):
|
||||
"""
|
||||
QWidget preference page for Ethernet switch preferences.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
||||
self._main_window = MainWindow.instance()
|
||||
self._ethernet_switches = {}
|
||||
self._items = []
|
||||
|
||||
self.uiNewEthernetSwitchPushButton.clicked.connect(self._newEthernetSwitchSlot)
|
||||
self.uiEditEthernetSwitchPushButton.clicked.connect(self._editEthernetSwitchSlot)
|
||||
self.uiDeleteEthernetSwitchPushButton.clicked.connect(self._deleteEthernetSwitchSlot)
|
||||
self.uiEthernetSwitchesTreeWidget.itemSelectionChanged.connect(self._ethernetSwitchChangedSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
|
||||
section_item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchInfoTreeWidget)
|
||||
section_item.setText(0, name)
|
||||
font = section_item.font(0)
|
||||
font.setBold(True)
|
||||
section_item.setFont(0, font)
|
||||
return section_item
|
||||
|
||||
def _refreshInfo(self, ethernet_switch):
|
||||
|
||||
self.uiEthernetSwitchInfoTreeWidget.clear()
|
||||
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", ethernet_switch["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_switch["default_name_format"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["server"]).name()])
|
||||
|
||||
for port in ethernet_switch["ports_mapping"]:
|
||||
section_item = self._createSectionItem("Port{}".format(port["port_number"]))
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Name:", port["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Type:", port["type"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["VLAN:", str(port["vlan"])])
|
||||
|
||||
self.uiEthernetSwitchInfoTreeWidget.expandAll()
|
||||
self.uiEthernetSwitchInfoTreeWidget.resizeColumnToContents(0)
|
||||
self.uiEthernetSwitchInfoTreeWidget.resizeColumnToContents(1)
|
||||
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _ethernetSwitchChangedSlot(self):
|
||||
"""
|
||||
Loads a selected Ethernet switch template from the tree widget.
|
||||
"""
|
||||
|
||||
selection = self.uiEthernetSwitchesTreeWidget.selectedItems()
|
||||
self.uiDeleteEthernetSwitchPushButton.setEnabled(len(selection) != 0)
|
||||
single_selected = len(selection) == 1
|
||||
self.uiEditEthernetSwitchPushButton.setEnabled(single_selected)
|
||||
|
||||
if single_selected:
|
||||
key = selection[0].data(0, QtCore.Qt.UserRole)
|
||||
ethernet_switch = self._ethernet_switches[key]
|
||||
self._refreshInfo(ethernet_switch)
|
||||
else:
|
||||
self.uiEthernetSwitchInfoTreeWidget.clear()
|
||||
|
||||
def _newEthernetSwitchSlot(self):
|
||||
"""
|
||||
Creates a new Ethernet switch template.
|
||||
"""
|
||||
|
||||
wizard = EthernetSwitchWizard(self._ethernet_switches, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
new_ethernet_switch_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_ethernet_switch_settings["server"], name=new_ethernet_switch_settings["name"])
|
||||
self._ethernet_switches[key] = ETHERNET_SWITCH_SETTINGS.copy()
|
||||
self._ethernet_switches[key].update(new_ethernet_switch_settings)
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchesTreeWidget)
|
||||
item.setText(0, self._ethernet_switches[key]["name"])
|
||||
Controller.instance().getSymbolIcon(self._ethernet_switches[key]["symbol"], qpartial(self._setItemIcon, item))
|
||||
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
self.uiEthernetSwitchesTreeWidget.setCurrentItem(item)
|
||||
|
||||
def _editEthernetSwitchSlot(self):
|
||||
"""
|
||||
Edits an Ethernet switch template.
|
||||
"""
|
||||
|
||||
item = self.uiEthernetSwitchesTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
ethernet_switch = self._ethernet_switches[key]
|
||||
dialog = ConfigurationDialog(ethernet_switch["name"], ethernet_switch, EthernetSwitchConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(ethernet_switches["symbol"], qpartial(self._setItemIcon, item))
|
||||
if ethernet_switch["name"] != item.text(0):
|
||||
new_key = "{server}:{name}".format(server=ethernet_switch["server"], name=ethernet_switch["name"])
|
||||
if new_key in self._ethernet_switches:
|
||||
QtWidgets.QMessageBox.critical(self, "Ethernet switch", "Ethernet switch name {} already exists for server {}".format(ethernet_switch["name"],
|
||||
ethernet_switch["server"]))
|
||||
ethernet_switch["name"] = item.text(0)
|
||||
return
|
||||
self._ethernet_switches[new_key] = self._ethernet_switches[key]
|
||||
del self._ethernet_switches[key]
|
||||
item.setText(0, ethernet_switch["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(ethernet_switch)
|
||||
|
||||
def _deleteEthernetSwitchSlot(self):
|
||||
"""
|
||||
Deletes an Ethernet switch template.
|
||||
"""
|
||||
for item in self.uiEthernetSwitchesTreeWidget.selectedItems():
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
del self._ethernet_switches[key]
|
||||
self.uiEthernetSwitchesTreeWidget.takeTopLevelItem(self.uiEthernetSwitchesTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
def loadPreferences(self):
|
||||
"""
|
||||
Loads the ethernet switch preferences.
|
||||
"""
|
||||
|
||||
builtin_module = Builtin.instance()
|
||||
self._ethernet_switches = copy.deepcopy(builtin_module.ethernetSwitches())
|
||||
self._items.clear()
|
||||
|
||||
for key, ethernet_switch in self._ethernet_switches.items():
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchesTreeWidget)
|
||||
item.setText(0, ethernet_switch["name"])
|
||||
Controller.instance().getSymbolIcon(ethernet_switch["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
|
||||
if self._items:
|
||||
self.uiEthernetSwitchesTreeWidget.setCurrentItem(self._items[0])
|
||||
self.uiEthernetSwitchesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def savePreferences(self):
|
||||
"""
|
||||
Saves the Ethernet switch preferences.
|
||||
"""
|
||||
|
||||
Builtin.instance().setEthernetSwitches(self._ethernet_switches)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
item.setIcon(0, icon)
|
||||
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
@@ -170,7 +170,6 @@ class FrameRelaySwitchConfigurationPage(QtWidgets.QWidget, Ui_frameRelaySwitchCo
|
||||
QtWidgets.QMessageBox.critical(self, "Name", "Frame relay switch name cannot be empty!")
|
||||
else:
|
||||
settings["name"] = name
|
||||
else:
|
||||
del settings["name"]
|
||||
|
||||
settings["mappings"] = self._mapping.copy()
|
||||
return settings
|
||||
59
gns3/modules/builtin/settings.py
Normal file
59
gns3/modules/builtin/settings.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Default Built-in settings.
|
||||
"""
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
BUILTIN_SETTINGS = {
|
||||
"use_local_server": True,
|
||||
}
|
||||
|
||||
|
||||
NAT_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Nat{0}",
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"category": Node.end_devices,
|
||||
"ports_mapping": [],
|
||||
}
|
||||
|
||||
CLOUD_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Cloud{0}",
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"category": Node.end_devices,
|
||||
"ports_mapping": [],
|
||||
}
|
||||
|
||||
ETHERNET_HUB_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Hub{0}",
|
||||
"symbol": ":/symbols/hub.svg",
|
||||
"category": Node.switches,
|
||||
"ports_mapping": [],
|
||||
}
|
||||
|
||||
ETHERNET_SWITCH_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Switch{0}",
|
||||
"symbol": ":/symbols/ethernet_switch.svg",
|
||||
"category": Node.switches,
|
||||
"ports_mapping": [],
|
||||
}
|
||||
102
gns3/modules/builtin/ui/builtin_preferences_page.ui
Normal file
102
gns3/modules/builtin/ui/builtin_preferences_page.ui
Normal file
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>BuiltinPreferencesPageWidget</class>
|
||||
<widget class="QWidget" name="BuiltinPreferencesPageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>330</width>
|
||||
<height>200</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Built-in</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="uiTabWidget">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="uiServerSettingsTabWidget">
|
||||
<attribute name="title">
|
||||
<string>General settings</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="uiUseLocalServercheckBox">
|
||||
<property name="text">
|
||||
<string>Use the local server</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>5</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>254</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiRestoreDefaultsPushButton">
|
||||
<property name="text">
|
||||
<string>Restore defaults</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
<designerdata>
|
||||
<property name="gridDeltaX">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="gridDeltaY">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="gridSnapX">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="gridSnapY">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="gridVisible">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</designerdata>
|
||||
</ui>
|
||||
52
gns3/modules/builtin/ui/builtin_preferences_page_ui.py
Normal file
52
gns3/modules/builtin/ui/builtin_preferences_page_ui.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/builtin_preferences_page.ui'
|
||||
#
|
||||
# Created: Thu Jun 9 21:08:46 2016
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_BuiltinPreferencesPageWidget(object):
|
||||
def setupUi(self, BuiltinPreferencesPageWidget):
|
||||
BuiltinPreferencesPageWidget.setObjectName("BuiltinPreferencesPageWidget")
|
||||
BuiltinPreferencesPageWidget.resize(330, 200)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(BuiltinPreferencesPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(BuiltinPreferencesPageWidget)
|
||||
self.uiTabWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.uiTabWidget.setObjectName("uiTabWidget")
|
||||
self.uiServerSettingsTabWidget = QtWidgets.QWidget()
|
||||
self.uiServerSettingsTabWidget.setObjectName("uiServerSettingsTabWidget")
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.uiServerSettingsTabWidget)
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.uiUseLocalServercheckBox = QtWidgets.QCheckBox(self.uiServerSettingsTabWidget)
|
||||
self.uiUseLocalServercheckBox.setChecked(True)
|
||||
self.uiUseLocalServercheckBox.setObjectName("uiUseLocalServercheckBox")
|
||||
self.verticalLayout_2.addWidget(self.uiUseLocalServercheckBox)
|
||||
spacerItem = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout_2.addItem(spacerItem)
|
||||
self.uiTabWidget.addTab(self.uiServerSettingsTabWidget, "")
|
||||
self.verticalLayout.addWidget(self.uiTabWidget)
|
||||
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||
spacerItem1 = QtWidgets.QSpacerItem(254, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_2.addItem(spacerItem1)
|
||||
self.uiRestoreDefaultsPushButton = QtWidgets.QPushButton(BuiltinPreferencesPageWidget)
|
||||
self.uiRestoreDefaultsPushButton.setObjectName("uiRestoreDefaultsPushButton")
|
||||
self.horizontalLayout_2.addWidget(self.uiRestoreDefaultsPushButton)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout_2)
|
||||
|
||||
self.retranslateUi(BuiltinPreferencesPageWidget)
|
||||
self.uiTabWidget.setCurrentIndex(0)
|
||||
QtCore.QMetaObject.connectSlotsByName(BuiltinPreferencesPageWidget)
|
||||
|
||||
def retranslateUi(self, BuiltinPreferencesPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
BuiltinPreferencesPageWidget.setWindowTitle(_translate("BuiltinPreferencesPageWidget", "Built-in"))
|
||||
self.uiUseLocalServercheckBox.setText(_translate("BuiltinPreferencesPageWidget", "Use the local server"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("BuiltinPreferencesPageWidget", "General settings"))
|
||||
self.uiRestoreDefaultsPushButton.setText(_translate("BuiltinPreferencesPageWidget", "Restore defaults"))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,171 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.4.2
|
||||
# Created: Fri Jun 10 16:26:54 2016
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_cloudConfigPageWidget(object):
|
||||
|
||||
def setupUi(self, cloudConfigPageWidget):
|
||||
cloudConfigPageWidget.setObjectName("cloudConfigPageWidget")
|
||||
cloudConfigPageWidget.resize(653, 478)
|
||||
self.vboxlayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
|
||||
self.vboxlayout.setObjectName("vboxlayout")
|
||||
self.uiNIOsTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
|
||||
self.uiNIOsTabWidget.setObjectName("uiNIOsTabWidget")
|
||||
self.NIOEthernetTab = QtWidgets.QWidget()
|
||||
self.NIOEthernetTab.setObjectName("NIOEthernetTab")
|
||||
self.vboxlayout1 = QtWidgets.QVBoxLayout(self.NIOEthernetTab)
|
||||
self.vboxlayout1.setObjectName("vboxlayout1")
|
||||
self.uiGenericEthernetGroupBox = QtWidgets.QGroupBox(self.NIOEthernetTab)
|
||||
self.uiGenericEthernetGroupBox.setObjectName("uiGenericEthernetGroupBox")
|
||||
self.gridlayout = QtWidgets.QGridLayout(self.uiGenericEthernetGroupBox)
|
||||
self.gridlayout.setObjectName("gridlayout")
|
||||
self.uiGenericEthernetComboBox = QtWidgets.QComboBox(self.uiGenericEthernetGroupBox)
|
||||
cloudConfigPageWidget.resize(758, 299)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiTabWidget = QtWidgets.QTabWidget(cloudConfigPageWidget)
|
||||
self.uiTabWidget.setObjectName("uiTabWidget")
|
||||
self.EthernetTab = QtWidgets.QWidget()
|
||||
self.EthernetTab.setObjectName("EthernetTab")
|
||||
self.gridLayout_3 = QtWidgets.QGridLayout(self.EthernetTab)
|
||||
self.gridLayout_3.setObjectName("gridLayout_3")
|
||||
self.uiEthernetComboBox = QtWidgets.QComboBox(self.EthernetTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiGenericEthernetComboBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiGenericEthernetComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiGenericEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
self.uiGenericEthernetComboBox.setObjectName("uiGenericEthernetComboBox")
|
||||
self.gridlayout.addWidget(self.uiGenericEthernetComboBox, 0, 0, 1, 3)
|
||||
self.uiGenericEthernetLineEdit = QtWidgets.QLineEdit(self.uiGenericEthernetGroupBox)
|
||||
self.uiGenericEthernetLineEdit.setObjectName("uiGenericEthernetLineEdit")
|
||||
self.gridlayout.addWidget(self.uiGenericEthernetLineEdit, 1, 0, 1, 1)
|
||||
self.uiAddGenericEthernetPushButton = QtWidgets.QPushButton(self.uiGenericEthernetGroupBox)
|
||||
self.uiAddGenericEthernetPushButton.setObjectName("uiAddGenericEthernetPushButton")
|
||||
self.gridlayout.addWidget(self.uiAddGenericEthernetPushButton, 1, 1, 1, 1)
|
||||
self.uiDeleteGenericEthernetPushButton = QtWidgets.QPushButton(self.uiGenericEthernetGroupBox)
|
||||
self.uiDeleteGenericEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteGenericEthernetPushButton.setObjectName("uiDeleteGenericEthernetPushButton")
|
||||
self.gridlayout.addWidget(self.uiDeleteGenericEthernetPushButton, 1, 2, 1, 1)
|
||||
self.uiGenericEthernetListWidget = QtWidgets.QListWidget(self.uiGenericEthernetGroupBox)
|
||||
self.uiGenericEthernetListWidget.setObjectName("uiGenericEthernetListWidget")
|
||||
self.gridlayout.addWidget(self.uiGenericEthernetListWidget, 2, 0, 1, 3)
|
||||
self.vboxlayout1.addWidget(self.uiGenericEthernetGroupBox)
|
||||
self.uiLinuxEthernetGroupBox = QtWidgets.QGroupBox(self.NIOEthernetTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiLinuxEthernetGroupBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiLinuxEthernetGroupBox.setSizePolicy(sizePolicy)
|
||||
self.uiLinuxEthernetGroupBox.setObjectName("uiLinuxEthernetGroupBox")
|
||||
self.gridlayout1 = QtWidgets.QGridLayout(self.uiLinuxEthernetGroupBox)
|
||||
self.gridlayout1.setObjectName("gridlayout1")
|
||||
self.uiLinuxEthernetComboBox = QtWidgets.QComboBox(self.uiLinuxEthernetGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiLinuxEthernetComboBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiLinuxEthernetComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiLinuxEthernetComboBox.setObjectName("uiLinuxEthernetComboBox")
|
||||
self.gridlayout1.addWidget(self.uiLinuxEthernetComboBox, 0, 0, 1, 3)
|
||||
self.uiLinuxEthernetLineEdit = QtWidgets.QLineEdit(self.uiLinuxEthernetGroupBox)
|
||||
self.uiLinuxEthernetLineEdit.setObjectName("uiLinuxEthernetLineEdit")
|
||||
self.gridlayout1.addWidget(self.uiLinuxEthernetLineEdit, 1, 0, 1, 1)
|
||||
self.uiAddLinuxEthernetPushButton = QtWidgets.QPushButton(self.uiLinuxEthernetGroupBox)
|
||||
self.uiAddLinuxEthernetPushButton.setObjectName("uiAddLinuxEthernetPushButton")
|
||||
self.gridlayout1.addWidget(self.uiAddLinuxEthernetPushButton, 1, 1, 1, 1)
|
||||
self.uiDeleteLinuxEthernetPushButton = QtWidgets.QPushButton(self.uiLinuxEthernetGroupBox)
|
||||
self.uiDeleteLinuxEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteLinuxEthernetPushButton.setObjectName("uiDeleteLinuxEthernetPushButton")
|
||||
self.gridlayout1.addWidget(self.uiDeleteLinuxEthernetPushButton, 1, 2, 1, 1)
|
||||
self.uiLinuxEthernetListWidget = QtWidgets.QListWidget(self.uiLinuxEthernetGroupBox)
|
||||
self.uiLinuxEthernetListWidget.setObjectName("uiLinuxEthernetListWidget")
|
||||
self.gridlayout1.addWidget(self.uiLinuxEthernetListWidget, 2, 0, 1, 3)
|
||||
self.vboxlayout1.addWidget(self.uiLinuxEthernetGroupBox)
|
||||
spacerItem = QtWidgets.QSpacerItem(21, 16, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
self.vboxlayout1.addItem(spacerItem)
|
||||
self.uiNIOsTabWidget.addTab(self.NIOEthernetTab, "")
|
||||
self.NIONATTab = QtWidgets.QWidget()
|
||||
self.NIONATTab.setObjectName("NIONATTab")
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout(self.NIONATTab)
|
||||
sizePolicy.setHeightForWidth(self.uiEthernetComboBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiEthernetComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiEthernetComboBox.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
|
||||
self.uiEthernetComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
self.uiEthernetComboBox.setObjectName("uiEthernetComboBox")
|
||||
self.gridLayout_3.addWidget(self.uiEthernetComboBox, 0, 0, 1, 1)
|
||||
self.uiAddEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiAddEthernetPushButton.setObjectName("uiAddEthernetPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiAddEthernetPushButton, 0, 1, 1, 1)
|
||||
self.uiAddAllEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiAddAllEthernetPushButton.setObjectName("uiAddAllEthernetPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiAddAllEthernetPushButton, 0, 2, 1, 1)
|
||||
self.uiDeleteEthernetPushButton = QtWidgets.QPushButton(self.EthernetTab)
|
||||
self.uiDeleteEthernetPushButton.setEnabled(False)
|
||||
self.uiDeleteEthernetPushButton.setObjectName("uiDeleteEthernetPushButton")
|
||||
self.gridLayout_3.addWidget(self.uiDeleteEthernetPushButton, 0, 3, 1, 1)
|
||||
self.uiEthernetListWidget = QtWidgets.QListWidget(self.EthernetTab)
|
||||
self.uiEthernetListWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.uiEthernetListWidget.setObjectName("uiEthernetListWidget")
|
||||
self.gridLayout_3.addWidget(self.uiEthernetListWidget, 1, 0, 1, 4)
|
||||
self.uiShowSpecialInterfacesCheckBox = QtWidgets.QCheckBox(self.EthernetTab)
|
||||
self.uiShowSpecialInterfacesCheckBox.setObjectName("uiShowSpecialInterfacesCheckBox")
|
||||
self.gridLayout_3.addWidget(self.uiShowSpecialInterfacesCheckBox, 2, 0, 1, 1)
|
||||
self.uiTabWidget.addTab(self.EthernetTab, "")
|
||||
self.TAPTab = QtWidgets.QWidget()
|
||||
self.TAPTab.setObjectName("TAPTab")
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout(self.TAPTab)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
self.uiNIONATSettingsGroupBox = QtWidgets.QGroupBox(self.NIONATTab)
|
||||
self.uiNIONATSettingsGroupBox.setObjectName("uiNIONATSettingsGroupBox")
|
||||
self._2 = QtWidgets.QGridLayout(self.uiNIONATSettingsGroupBox)
|
||||
self._2.setObjectName("_2")
|
||||
self.uiNIONATIdentifierLabel = QtWidgets.QLabel(self.uiNIONATSettingsGroupBox)
|
||||
self.uiNIONATIdentifierLabel.setObjectName("uiNIONATIdentifierLabel")
|
||||
self._2.addWidget(self.uiNIONATIdentifierLabel, 0, 0, 1, 1)
|
||||
self.uiNIONATIdentiferLineEdit = QtWidgets.QLineEdit(self.uiNIONATSettingsGroupBox)
|
||||
self.uiDeleteTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
|
||||
self.uiDeleteTAPPushButton.setEnabled(False)
|
||||
self.uiDeleteTAPPushButton.setObjectName("uiDeleteTAPPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiDeleteTAPPushButton, 1, 4, 1, 1)
|
||||
self.uiTAPListWidget = QtWidgets.QListWidget(self.TAPTab)
|
||||
self.uiTAPListWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.uiTAPListWidget.setObjectName("uiTAPListWidget")
|
||||
self.gridLayout_2.addWidget(self.uiTAPListWidget, 2, 0, 1, 5)
|
||||
self.uiTAPLineEdit = QtWidgets.QLineEdit(self.TAPTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiTAPLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiTAPLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiTAPLineEdit.setObjectName("uiTAPLineEdit")
|
||||
self.gridLayout_2.addWidget(self.uiTAPLineEdit, 1, 1, 1, 1)
|
||||
self.uiAddTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
|
||||
self.uiAddTAPPushButton.setObjectName("uiAddTAPPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiAddTAPPushButton, 1, 2, 1, 1)
|
||||
self.uiTAPComboBox = QtWidgets.QComboBox(self.TAPTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIONATIdentiferLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIONATIdentiferLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiNIONATIdentiferLineEdit.setObjectName("uiNIONATIdentiferLineEdit")
|
||||
self._2.addWidget(self.uiNIONATIdentiferLineEdit, 1, 0, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.uiNIONATSettingsGroupBox, 0, 0, 1, 2)
|
||||
self.uiNIONATListGroupBox = QtWidgets.QGroupBox(self.NIONATTab)
|
||||
self.uiNIONATListGroupBox.setObjectName("uiNIONATListGroupBox")
|
||||
self._3 = QtWidgets.QVBoxLayout(self.uiNIONATListGroupBox)
|
||||
self._3.setObjectName("_3")
|
||||
self.uiNIONATListWidget = QtWidgets.QListWidget(self.uiNIONATListGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
sizePolicy.setHeightForWidth(self.uiTAPComboBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiTAPComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiTAPComboBox.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
|
||||
self.uiTAPComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
|
||||
self.uiTAPComboBox.setObjectName("uiTAPComboBox")
|
||||
self.gridLayout_2.addWidget(self.uiTAPComboBox, 0, 1, 1, 4)
|
||||
self.uiAddAllTAPPushButton = QtWidgets.QPushButton(self.TAPTab)
|
||||
self.uiAddAllTAPPushButton.setObjectName("uiAddAllTAPPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiAddAllTAPPushButton, 1, 3, 1, 1)
|
||||
self.uiTabWidget.addTab(self.TAPTab, "")
|
||||
self.UDPTab = QtWidgets.QWidget()
|
||||
self.UDPTab.setObjectName("UDPTab")
|
||||
self.gridLayout_5 = QtWidgets.QGridLayout(self.UDPTab)
|
||||
self.gridLayout_5.setObjectName("gridLayout_5")
|
||||
self.uiUDPTunnelSettingsGroupBox = QtWidgets.QGroupBox(self.UDPTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIONATListWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIONATListWidget.setSizePolicy(sizePolicy)
|
||||
self.uiNIONATListWidget.setObjectName("uiNIONATListWidget")
|
||||
self._3.addWidget(self.uiNIONATListWidget)
|
||||
self.gridLayout_2.addWidget(self.uiNIONATListGroupBox, 0, 2, 3, 1)
|
||||
self.uiAddNIONATPushButton = QtWidgets.QPushButton(self.NIONATTab)
|
||||
self.uiAddNIONATPushButton.setObjectName("uiAddNIONATPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiAddNIONATPushButton, 1, 0, 1, 1)
|
||||
self.uiDeleteNIONATPushButton = QtWidgets.QPushButton(self.NIONATTab)
|
||||
self.uiDeleteNIONATPushButton.setEnabled(False)
|
||||
self.uiDeleteNIONATPushButton.setObjectName("uiDeleteNIONATPushButton")
|
||||
self.gridLayout_2.addWidget(self.uiDeleteNIONATPushButton, 1, 1, 1, 1)
|
||||
spacerItem1 = QtWidgets.QSpacerItem(20, 294, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout_2.addItem(spacerItem1, 2, 0, 2, 1)
|
||||
spacerItem2 = QtWidgets.QSpacerItem(20, 194, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout_2.addItem(spacerItem2, 3, 2, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.NIONATTab, "")
|
||||
self.NIOUDPTab = QtWidgets.QWidget()
|
||||
self.NIOUDPTab.setObjectName("NIOUDPTab")
|
||||
self.gridlayout2 = QtWidgets.QGridLayout(self.NIOUDPTab)
|
||||
self.gridlayout2.setObjectName("gridlayout2")
|
||||
self.uiNIOUDPSettingsGroupBox = QtWidgets.QGroupBox(self.NIOUDPTab)
|
||||
self.uiNIOUDPSettingsGroupBox.setObjectName("uiNIOUDPSettingsGroupBox")
|
||||
self.gridlayout3 = QtWidgets.QGridLayout(self.uiNIOUDPSettingsGroupBox)
|
||||
self.gridlayout3.setObjectName("gridlayout3")
|
||||
self.uiLocalPortLabel = QtWidgets.QLabel(self.uiNIOUDPSettingsGroupBox)
|
||||
self.uiLocalPortLabel.setObjectName("uiLocalPortLabel")
|
||||
self.gridlayout3.addWidget(self.uiLocalPortLabel, 0, 0, 1, 1)
|
||||
self.uiLocalPortSpinBox = QtWidgets.QSpinBox(self.uiNIOUDPSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiLocalPortSpinBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiLocalPortSpinBox.setSizePolicy(sizePolicy)
|
||||
self.uiLocalPortSpinBox.setMaximum(65535)
|
||||
self.uiLocalPortSpinBox.setProperty("value", 30000)
|
||||
self.uiLocalPortSpinBox.setObjectName("uiLocalPortSpinBox")
|
||||
self.gridlayout3.addWidget(self.uiLocalPortSpinBox, 0, 1, 1, 1)
|
||||
self.uiRemoteHostLabel = QtWidgets.QLabel(self.uiNIOUDPSettingsGroupBox)
|
||||
self.uiRemoteHostLabel.setObjectName("uiRemoteHostLabel")
|
||||
self.gridlayout3.addWidget(self.uiRemoteHostLabel, 1, 0, 1, 1)
|
||||
self.uiRemoteHostLineEdit = QtWidgets.QLineEdit(self.uiNIOUDPSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHeightForWidth(self.uiUDPTunnelSettingsGroupBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiUDPTunnelSettingsGroupBox.setSizePolicy(sizePolicy)
|
||||
self.uiUDPTunnelSettingsGroupBox.setObjectName("uiUDPTunnelSettingsGroupBox")
|
||||
self.gridLayout_4 = QtWidgets.QGridLayout(self.uiUDPTunnelSettingsGroupBox)
|
||||
self.gridLayout_4.setObjectName("gridLayout_4")
|
||||
self.uiRemoteHostLineEdit = QtWidgets.QLineEdit(self.uiUDPTunnelSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiRemoteHostLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiRemoteHostLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiRemoteHostLineEdit.setMinimumSize(QtCore.QSize(80, 0))
|
||||
self.uiRemoteHostLineEdit.setObjectName("uiRemoteHostLineEdit")
|
||||
self.gridlayout3.addWidget(self.uiRemoteHostLineEdit, 1, 1, 1, 1)
|
||||
self.uiRemotePortLabel = QtWidgets.QLabel(self.uiNIOUDPSettingsGroupBox)
|
||||
self.gridLayout_4.addWidget(self.uiRemoteHostLineEdit, 2, 1, 1, 1)
|
||||
self.uiRemotePortLabel = QtWidgets.QLabel(self.uiUDPTunnelSettingsGroupBox)
|
||||
self.uiRemotePortLabel.setObjectName("uiRemotePortLabel")
|
||||
self.gridlayout3.addWidget(self.uiRemotePortLabel, 2, 0, 1, 1)
|
||||
self.uiRemotePortSpinBox = QtWidgets.QSpinBox(self.uiNIOUDPSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
self.gridLayout_4.addWidget(self.uiRemotePortLabel, 3, 0, 1, 1)
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.uiAddUDPPushButton = QtWidgets.QPushButton(self.uiUDPTunnelSettingsGroupBox)
|
||||
self.uiAddUDPPushButton.setObjectName("uiAddUDPPushButton")
|
||||
self.horizontalLayout.addWidget(self.uiAddUDPPushButton)
|
||||
self.uiDeleteUDPPushButton = QtWidgets.QPushButton(self.uiUDPTunnelSettingsGroupBox)
|
||||
self.uiDeleteUDPPushButton.setEnabled(False)
|
||||
self.uiDeleteUDPPushButton.setObjectName("uiDeleteUDPPushButton")
|
||||
self.horizontalLayout.addWidget(self.uiDeleteUDPPushButton)
|
||||
spacerItem = QtWidgets.QSpacerItem(50, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout.addItem(spacerItem)
|
||||
self.gridLayout_4.addLayout(self.horizontalLayout, 4, 0, 1, 2)
|
||||
self.uiRemotePortSpinBox = QtWidgets.QSpinBox(self.uiUDPTunnelSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiRemotePortSpinBox.sizePolicy().hasHeightForWidth())
|
||||
@@ -173,220 +132,52 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.uiRemotePortSpinBox.setMaximum(65535)
|
||||
self.uiRemotePortSpinBox.setProperty("value", 20000)
|
||||
self.uiRemotePortSpinBox.setObjectName("uiRemotePortSpinBox")
|
||||
self.gridlayout3.addWidget(self.uiRemotePortSpinBox, 2, 1, 1, 1)
|
||||
self.gridlayout2.addWidget(self.uiNIOUDPSettingsGroupBox, 0, 0, 1, 2)
|
||||
self.uiNIOUDPListGroupBox = QtWidgets.QGroupBox(self.NIOUDPTab)
|
||||
self.uiNIOUDPListGroupBox.setObjectName("uiNIOUDPListGroupBox")
|
||||
self.vboxlayout2 = QtWidgets.QVBoxLayout(self.uiNIOUDPListGroupBox)
|
||||
self.vboxlayout2.setObjectName("vboxlayout2")
|
||||
self.uiNIOUDPListWidget = QtWidgets.QListWidget(self.uiNIOUDPListGroupBox)
|
||||
self.uiNIOUDPListWidget.setObjectName("uiNIOUDPListWidget")
|
||||
self.vboxlayout2.addWidget(self.uiNIOUDPListWidget)
|
||||
self.gridlayout2.addWidget(self.uiNIOUDPListGroupBox, 0, 2, 2, 1)
|
||||
self.uiAddNIOUDPPushButton = QtWidgets.QPushButton(self.NIOUDPTab)
|
||||
self.uiAddNIOUDPPushButton.setObjectName("uiAddNIOUDPPushButton")
|
||||
self.gridlayout2.addWidget(self.uiAddNIOUDPPushButton, 1, 0, 1, 1)
|
||||
self.uiDeleteNIOUDPPushButton = QtWidgets.QPushButton(self.NIOUDPTab)
|
||||
self.uiDeleteNIOUDPPushButton.setEnabled(False)
|
||||
self.uiDeleteNIOUDPPushButton.setObjectName("uiDeleteNIOUDPPushButton")
|
||||
self.gridlayout2.addWidget(self.uiDeleteNIOUDPPushButton, 1, 1, 1, 1)
|
||||
spacerItem3 = QtWidgets.QSpacerItem(20, 211, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridlayout2.addItem(spacerItem3, 2, 1, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.NIOUDPTab, "")
|
||||
self.NIOTAPTab = QtWidgets.QWidget()
|
||||
self.NIOTAPTab.setObjectName("NIOTAPTab")
|
||||
self.vboxlayout3 = QtWidgets.QVBoxLayout(self.NIOTAPTab)
|
||||
self.vboxlayout3.setObjectName("vboxlayout3")
|
||||
self.uiNIOTAPGroupBox = QtWidgets.QGroupBox(self.NIOTAPTab)
|
||||
self.uiNIOTAPGroupBox.setObjectName("uiNIOTAPGroupBox")
|
||||
self.gridlayout4 = QtWidgets.QGridLayout(self.uiNIOTAPGroupBox)
|
||||
self.gridlayout4.setObjectName("gridlayout4")
|
||||
self.uiNIOTAPLineEdit = QtWidgets.QLineEdit(self.uiNIOTAPGroupBox)
|
||||
self.uiNIOTAPLineEdit.setObjectName("uiNIOTAPLineEdit")
|
||||
self.gridlayout4.addWidget(self.uiNIOTAPLineEdit, 0, 0, 1, 1)
|
||||
self.uiAddNIOTAPPushButton = QtWidgets.QPushButton(self.uiNIOTAPGroupBox)
|
||||
self.uiAddNIOTAPPushButton.setObjectName("uiAddNIOTAPPushButton")
|
||||
self.gridlayout4.addWidget(self.uiAddNIOTAPPushButton, 0, 1, 1, 1)
|
||||
self.uiDeleteNIOTAPPushButton = QtWidgets.QPushButton(self.uiNIOTAPGroupBox)
|
||||
self.uiDeleteNIOTAPPushButton.setEnabled(False)
|
||||
self.uiDeleteNIOTAPPushButton.setObjectName("uiDeleteNIOTAPPushButton")
|
||||
self.gridlayout4.addWidget(self.uiDeleteNIOTAPPushButton, 0, 2, 1, 1)
|
||||
self.uiNIOTAPListWidget = QtWidgets.QListWidget(self.uiNIOTAPGroupBox)
|
||||
self.uiNIOTAPListWidget.setObjectName("uiNIOTAPListWidget")
|
||||
self.gridlayout4.addWidget(self.uiNIOTAPListWidget, 1, 0, 1, 3)
|
||||
self.vboxlayout3.addWidget(self.uiNIOTAPGroupBox)
|
||||
spacerItem4 = QtWidgets.QSpacerItem(20, 191, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.vboxlayout3.addItem(spacerItem4)
|
||||
self.uiNIOsTabWidget.addTab(self.NIOTAPTab, "")
|
||||
self.NIOUnixTab = QtWidgets.QWidget()
|
||||
self.NIOUnixTab.setObjectName("NIOUnixTab")
|
||||
self.gridlayout5 = QtWidgets.QGridLayout(self.NIOUnixTab)
|
||||
self.gridlayout5.setObjectName("gridlayout5")
|
||||
self.uiNIOUNIXSettingsGroupBox = QtWidgets.QGroupBox(self.NIOUnixTab)
|
||||
self.uiNIOUNIXSettingsGroupBox.setObjectName("uiNIOUNIXSettingsGroupBox")
|
||||
self.gridlayout6 = QtWidgets.QGridLayout(self.uiNIOUNIXSettingsGroupBox)
|
||||
self.gridlayout6.setObjectName("gridlayout6")
|
||||
self.gridlayout7 = QtWidgets.QGridLayout()
|
||||
self.gridlayout7.setObjectName("gridlayout7")
|
||||
self.uiLocalFileLabel = QtWidgets.QLabel(self.uiNIOUNIXSettingsGroupBox)
|
||||
self.uiLocalFileLabel.setObjectName("uiLocalFileLabel")
|
||||
self.gridlayout7.addWidget(self.uiLocalFileLabel, 0, 0, 1, 1)
|
||||
self.uiLocalFileLineEdit = QtWidgets.QLineEdit(self.uiNIOUNIXSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
self.gridLayout_4.addWidget(self.uiRemotePortSpinBox, 3, 1, 1, 1)
|
||||
self.uiUDPNameLineEdit = QtWidgets.QLineEdit(self.uiUDPTunnelSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiLocalFileLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiLocalFileLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiLocalFileLineEdit.setObjectName("uiLocalFileLineEdit")
|
||||
self.gridlayout7.addWidget(self.uiLocalFileLineEdit, 1, 0, 1, 1)
|
||||
self.gridlayout6.addLayout(self.gridlayout7, 0, 0, 1, 1)
|
||||
self.gridlayout8 = QtWidgets.QGridLayout()
|
||||
self.gridlayout8.setObjectName("gridlayout8")
|
||||
self.uiRemoteFileLabel = QtWidgets.QLabel(self.uiNIOUNIXSettingsGroupBox)
|
||||
self.uiRemoteFileLabel.setObjectName("uiRemoteFileLabel")
|
||||
self.gridlayout8.addWidget(self.uiRemoteFileLabel, 0, 0, 1, 1)
|
||||
self.uiRemoteFileLineEdit = QtWidgets.QLineEdit(self.uiNIOUNIXSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHeightForWidth(self.uiUDPNameLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiUDPNameLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiUDPNameLineEdit.setObjectName("uiUDPNameLineEdit")
|
||||
self.gridLayout_4.addWidget(self.uiUDPNameLineEdit, 0, 1, 1, 1)
|
||||
self.uiRemoteHostLabel = QtWidgets.QLabel(self.uiUDPTunnelSettingsGroupBox)
|
||||
self.uiRemoteHostLabel.setObjectName("uiRemoteHostLabel")
|
||||
self.gridLayout_4.addWidget(self.uiRemoteHostLabel, 2, 0, 1, 1)
|
||||
self.uiLocalPortSpinBox = QtWidgets.QSpinBox(self.uiUDPTunnelSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiRemoteFileLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiRemoteFileLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiRemoteFileLineEdit.setObjectName("uiRemoteFileLineEdit")
|
||||
self.gridlayout8.addWidget(self.uiRemoteFileLineEdit, 1, 0, 1, 1)
|
||||
self.gridlayout6.addLayout(self.gridlayout8, 1, 0, 1, 1)
|
||||
self.gridlayout5.addWidget(self.uiNIOUNIXSettingsGroupBox, 0, 0, 1, 2)
|
||||
self.uiNIOUNIXListGroupBox = QtWidgets.QGroupBox(self.NIOUnixTab)
|
||||
self.uiNIOUNIXListGroupBox.setObjectName("uiNIOUNIXListGroupBox")
|
||||
self.vboxlayout4 = QtWidgets.QVBoxLayout(self.uiNIOUNIXListGroupBox)
|
||||
self.vboxlayout4.setObjectName("vboxlayout4")
|
||||
self.uiNIOUNIXListWidget = QtWidgets.QListWidget(self.uiNIOUNIXListGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
sizePolicy.setHeightForWidth(self.uiLocalPortSpinBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiLocalPortSpinBox.setSizePolicy(sizePolicy)
|
||||
self.uiLocalPortSpinBox.setMaximum(65535)
|
||||
self.uiLocalPortSpinBox.setProperty("value", 30000)
|
||||
self.uiLocalPortSpinBox.setObjectName("uiLocalPortSpinBox")
|
||||
self.gridLayout_4.addWidget(self.uiLocalPortSpinBox, 1, 1, 1, 1)
|
||||
self.uiLocalPortLabel = QtWidgets.QLabel(self.uiUDPTunnelSettingsGroupBox)
|
||||
self.uiLocalPortLabel.setObjectName("uiLocalPortLabel")
|
||||
self.gridLayout_4.addWidget(self.uiLocalPortLabel, 1, 0, 1, 1)
|
||||
self.uiUDPNameLabel = QtWidgets.QLabel(self.uiUDPTunnelSettingsGroupBox)
|
||||
self.uiUDPNameLabel.setObjectName("uiUDPNameLabel")
|
||||
self.gridLayout_4.addWidget(self.uiUDPNameLabel, 0, 0, 1, 1)
|
||||
spacerItem1 = QtWidgets.QSpacerItem(20, 10, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout_4.addItem(spacerItem1, 5, 0, 1, 1)
|
||||
self.gridLayout_5.addWidget(self.uiUDPTunnelSettingsGroupBox, 0, 0, 1, 1)
|
||||
self.uiUDPTunnelsGroupBox = QtWidgets.QGroupBox(self.UDPTab)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIOUNIXListWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIOUNIXListWidget.setSizePolicy(sizePolicy)
|
||||
self.uiNIOUNIXListWidget.setObjectName("uiNIOUNIXListWidget")
|
||||
self.vboxlayout4.addWidget(self.uiNIOUNIXListWidget)
|
||||
self.gridlayout5.addWidget(self.uiNIOUNIXListGroupBox, 0, 2, 3, 1)
|
||||
self.uiAddNIOUNIXPushButton = QtWidgets.QPushButton(self.NIOUnixTab)
|
||||
self.uiAddNIOUNIXPushButton.setObjectName("uiAddNIOUNIXPushButton")
|
||||
self.gridlayout5.addWidget(self.uiAddNIOUNIXPushButton, 1, 0, 1, 1)
|
||||
self.uiDeleteNIOUNIXPushButton = QtWidgets.QPushButton(self.NIOUnixTab)
|
||||
self.uiDeleteNIOUNIXPushButton.setEnabled(False)
|
||||
self.uiDeleteNIOUNIXPushButton.setObjectName("uiDeleteNIOUNIXPushButton")
|
||||
self.gridlayout5.addWidget(self.uiDeleteNIOUNIXPushButton, 1, 1, 1, 1)
|
||||
spacerItem5 = QtWidgets.QSpacerItem(160, 190, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
self.gridlayout5.addItem(spacerItem5, 2, 0, 2, 2)
|
||||
spacerItem6 = QtWidgets.QSpacerItem(196, 132, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridlayout5.addItem(spacerItem6, 3, 2, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.NIOUnixTab, "")
|
||||
self.NIOVDETab = QtWidgets.QWidget()
|
||||
self.NIOVDETab.setObjectName("NIOVDETab")
|
||||
self.gridlayout9 = QtWidgets.QGridLayout(self.NIOVDETab)
|
||||
self.gridlayout9.setObjectName("gridlayout9")
|
||||
self.uiNIOVDESettingsGroupBox = QtWidgets.QGroupBox(self.NIOVDETab)
|
||||
self.uiNIOVDESettingsGroupBox.setObjectName("uiNIOVDESettingsGroupBox")
|
||||
self.gridlayout10 = QtWidgets.QGridLayout(self.uiNIOVDESettingsGroupBox)
|
||||
self.gridlayout10.setObjectName("gridlayout10")
|
||||
self.gridlayout11 = QtWidgets.QGridLayout()
|
||||
self.gridlayout11.setObjectName("gridlayout11")
|
||||
self.uiVDEControlFileLabel = QtWidgets.QLabel(self.uiNIOVDESettingsGroupBox)
|
||||
self.uiVDEControlFileLabel.setObjectName("uiVDEControlFileLabel")
|
||||
self.gridlayout11.addWidget(self.uiVDEControlFileLabel, 0, 0, 1, 1)
|
||||
self.uiVDEControlFileLineEdit = QtWidgets.QLineEdit(self.uiNIOVDESettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiVDEControlFileLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiVDEControlFileLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiVDEControlFileLineEdit.setObjectName("uiVDEControlFileLineEdit")
|
||||
self.gridlayout11.addWidget(self.uiVDEControlFileLineEdit, 1, 0, 1, 1)
|
||||
self.gridlayout10.addLayout(self.gridlayout11, 0, 0, 1, 1)
|
||||
self.gridlayout12 = QtWidgets.QGridLayout()
|
||||
self.gridlayout12.setObjectName("gridlayout12")
|
||||
self.uiVDELocalFileLabel = QtWidgets.QLabel(self.uiNIOVDESettingsGroupBox)
|
||||
self.uiVDELocalFileLabel.setObjectName("uiVDELocalFileLabel")
|
||||
self.gridlayout12.addWidget(self.uiVDELocalFileLabel, 0, 0, 1, 1)
|
||||
self.uiVDELocalFileLineEdit = QtWidgets.QLineEdit(self.uiNIOVDESettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiVDELocalFileLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiVDELocalFileLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiVDELocalFileLineEdit.setObjectName("uiVDELocalFileLineEdit")
|
||||
self.gridlayout12.addWidget(self.uiVDELocalFileLineEdit, 1, 0, 1, 1)
|
||||
self.gridlayout10.addLayout(self.gridlayout12, 1, 0, 1, 1)
|
||||
self.gridlayout9.addWidget(self.uiNIOVDESettingsGroupBox, 0, 0, 1, 2)
|
||||
self.uiNIOVDEListGroupBox = QtWidgets.QGroupBox(self.NIOVDETab)
|
||||
self.uiNIOVDEListGroupBox.setObjectName("uiNIOVDEListGroupBox")
|
||||
self.vboxlayout5 = QtWidgets.QVBoxLayout(self.uiNIOVDEListGroupBox)
|
||||
self.vboxlayout5.setObjectName("vboxlayout5")
|
||||
self.uiNIOVDEListWidget = QtWidgets.QListWidget(self.uiNIOVDEListGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIOVDEListWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIOVDEListWidget.setSizePolicy(sizePolicy)
|
||||
self.uiNIOVDEListWidget.setObjectName("uiNIOVDEListWidget")
|
||||
self.vboxlayout5.addWidget(self.uiNIOVDEListWidget)
|
||||
self.gridlayout9.addWidget(self.uiNIOVDEListGroupBox, 0, 2, 3, 1)
|
||||
self.uiAddNIOVDEPushButton = QtWidgets.QPushButton(self.NIOVDETab)
|
||||
self.uiAddNIOVDEPushButton.setObjectName("uiAddNIOVDEPushButton")
|
||||
self.gridlayout9.addWidget(self.uiAddNIOVDEPushButton, 1, 0, 1, 1)
|
||||
self.uiDeleteNIOVDEPushButton = QtWidgets.QPushButton(self.NIOVDETab)
|
||||
self.uiDeleteNIOVDEPushButton.setEnabled(False)
|
||||
self.uiDeleteNIOVDEPushButton.setObjectName("uiDeleteNIOVDEPushButton")
|
||||
self.gridlayout9.addWidget(self.uiDeleteNIOVDEPushButton, 1, 1, 1, 1)
|
||||
spacerItem7 = QtWidgets.QSpacerItem(161, 201, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
self.gridlayout9.addItem(spacerItem7, 2, 0, 2, 2)
|
||||
spacerItem8 = QtWidgets.QSpacerItem(196, 132, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridlayout9.addItem(spacerItem8, 3, 2, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.NIOVDETab, "")
|
||||
self.NIONullTab = QtWidgets.QWidget()
|
||||
self.NIONullTab.setObjectName("NIONullTab")
|
||||
self.gridlayout13 = QtWidgets.QGridLayout(self.NIONullTab)
|
||||
self.gridlayout13.setObjectName("gridlayout13")
|
||||
self.uiNIONullSettingsGroupBox = QtWidgets.QGroupBox(self.NIONullTab)
|
||||
self.uiNIONullSettingsGroupBox.setObjectName("uiNIONullSettingsGroupBox")
|
||||
self.gridlayout14 = QtWidgets.QGridLayout(self.uiNIONullSettingsGroupBox)
|
||||
self.gridlayout14.setObjectName("gridlayout14")
|
||||
self.uiNIONullIdentifierLabel = QtWidgets.QLabel(self.uiNIONullSettingsGroupBox)
|
||||
self.uiNIONullIdentifierLabel.setObjectName("uiNIONullIdentifierLabel")
|
||||
self.gridlayout14.addWidget(self.uiNIONullIdentifierLabel, 0, 0, 1, 1)
|
||||
self.uiNIONullIdentiferLineEdit = QtWidgets.QLineEdit(self.uiNIONullSettingsGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIONullIdentiferLineEdit.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIONullIdentiferLineEdit.setSizePolicy(sizePolicy)
|
||||
self.uiNIONullIdentiferLineEdit.setObjectName("uiNIONullIdentiferLineEdit")
|
||||
self.gridlayout14.addWidget(self.uiNIONullIdentiferLineEdit, 1, 0, 1, 1)
|
||||
self.gridlayout13.addWidget(self.uiNIONullSettingsGroupBox, 0, 0, 1, 2)
|
||||
self.uiNIONullListGroupBox = QtWidgets.QGroupBox(self.NIONullTab)
|
||||
self.uiNIONullListGroupBox.setObjectName("uiNIONullListGroupBox")
|
||||
self.vboxlayout6 = QtWidgets.QVBoxLayout(self.uiNIONullListGroupBox)
|
||||
self.vboxlayout6.setObjectName("vboxlayout6")
|
||||
self.uiNIONullListWidget = QtWidgets.QListWidget(self.uiNIONullListGroupBox)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiNIONullListWidget.sizePolicy().hasHeightForWidth())
|
||||
self.uiNIONullListWidget.setSizePolicy(sizePolicy)
|
||||
self.uiNIONullListWidget.setObjectName("uiNIONullListWidget")
|
||||
self.vboxlayout6.addWidget(self.uiNIONullListWidget)
|
||||
self.gridlayout13.addWidget(self.uiNIONullListGroupBox, 0, 2, 3, 1)
|
||||
self.uiAddNIONullPushButton = QtWidgets.QPushButton(self.NIONullTab)
|
||||
self.uiAddNIONullPushButton.setObjectName("uiAddNIONullPushButton")
|
||||
self.gridlayout13.addWidget(self.uiAddNIONullPushButton, 1, 0, 1, 1)
|
||||
self.uiDeleteNIONullPushButton = QtWidgets.QPushButton(self.NIONullTab)
|
||||
self.uiDeleteNIONullPushButton.setEnabled(False)
|
||||
self.uiDeleteNIONullPushButton.setObjectName("uiDeleteNIONullPushButton")
|
||||
self.gridlayout13.addWidget(self.uiDeleteNIONullPushButton, 1, 1, 1, 1)
|
||||
spacerItem9 = QtWidgets.QSpacerItem(20, 261, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridlayout13.addItem(spacerItem9, 2, 0, 2, 2)
|
||||
spacerItem10 = QtWidgets.QSpacerItem(20, 181, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridlayout13.addItem(spacerItem10, 3, 2, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.NIONullTab, "")
|
||||
sizePolicy.setHeightForWidth(self.uiUDPTunnelsGroupBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiUDPTunnelsGroupBox.setSizePolicy(sizePolicy)
|
||||
self.uiUDPTunnelsGroupBox.setObjectName("uiUDPTunnelsGroupBox")
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.uiUDPTunnelsGroupBox)
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.uiUDPTreeWidget = QtWidgets.QTreeWidget(self.uiUDPTunnelsGroupBox)
|
||||
self.uiUDPTreeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.uiUDPTreeWidget.setObjectName("uiUDPTreeWidget")
|
||||
self.verticalLayout_2.addWidget(self.uiUDPTreeWidget)
|
||||
self.gridLayout_5.addWidget(self.uiUDPTunnelsGroupBox, 0, 1, 1, 1)
|
||||
self.uiTabWidget.addTab(self.UDPTab, "")
|
||||
self.MiscTab = QtWidgets.QWidget()
|
||||
self.MiscTab.setObjectName("MiscTab")
|
||||
self.gridLayout = QtWidgets.QGridLayout(self.MiscTab)
|
||||
@@ -396,64 +187,74 @@ class Ui_cloudConfigPageWidget(object):
|
||||
self.gridLayout.addWidget(self.uiNameLabel, 0, 0, 1, 1)
|
||||
self.uiNameLineEdit = QtWidgets.QLineEdit(self.MiscTab)
|
||||
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
|
||||
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 1, 1, 1)
|
||||
spacerItem11 = QtWidgets.QSpacerItem(20, 399, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout.addItem(spacerItem11, 1, 1, 1, 1)
|
||||
self.uiNIOsTabWidget.addTab(self.MiscTab, "")
|
||||
self.vboxlayout.addWidget(self.uiNIOsTabWidget)
|
||||
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 2, 1, 1)
|
||||
self.uiDefaultNameFormatLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.uiDefaultNameFormatLabel.setObjectName("uiDefaultNameFormatLabel")
|
||||
self.gridLayout.addWidget(self.uiDefaultNameFormatLabel, 1, 0, 1, 2)
|
||||
self.uiDefaultNameFormatLineEdit = QtWidgets.QLineEdit(self.MiscTab)
|
||||
self.uiDefaultNameFormatLineEdit.setObjectName("uiDefaultNameFormatLineEdit")
|
||||
self.gridLayout.addWidget(self.uiDefaultNameFormatLineEdit, 1, 2, 1, 1)
|
||||
self.uiSymbolLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.uiSymbolLabel.setObjectName("uiSymbolLabel")
|
||||
self.gridLayout.addWidget(self.uiSymbolLabel, 2, 0, 1, 2)
|
||||
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
|
||||
self.uiSymbolLineEdit = QtWidgets.QLineEdit(self.MiscTab)
|
||||
self.uiSymbolLineEdit.setObjectName("uiSymbolLineEdit")
|
||||
self.horizontalLayout_7.addWidget(self.uiSymbolLineEdit)
|
||||
self.uiSymbolToolButton = QtWidgets.QToolButton(self.MiscTab)
|
||||
self.uiSymbolToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
|
||||
self.uiSymbolToolButton.setObjectName("uiSymbolToolButton")
|
||||
self.horizontalLayout_7.addWidget(self.uiSymbolToolButton)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_7, 2, 2, 1, 1)
|
||||
self.uiCategoryLabel = QtWidgets.QLabel(self.MiscTab)
|
||||
self.uiCategoryLabel.setObjectName("uiCategoryLabel")
|
||||
self.gridLayout.addWidget(self.uiCategoryLabel, 3, 0, 1, 2)
|
||||
self.uiCategoryComboBox = QtWidgets.QComboBox(self.MiscTab)
|
||||
self.uiCategoryComboBox.setObjectName("uiCategoryComboBox")
|
||||
self.gridLayout.addWidget(self.uiCategoryComboBox, 3, 2, 1, 1)
|
||||
spacerItem2 = QtWidgets.QSpacerItem(20, 399, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.gridLayout.addItem(spacerItem2, 4, 1, 1, 2)
|
||||
self.uiTabWidget.addTab(self.MiscTab, "")
|
||||
self.verticalLayout.addWidget(self.uiTabWidget)
|
||||
|
||||
self.retranslateUi(cloudConfigPageWidget)
|
||||
self.uiNIOsTabWidget.setCurrentIndex(0)
|
||||
self.uiTabWidget.setCurrentIndex(0)
|
||||
QtCore.QMetaObject.connectSlotsByName(cloudConfigPageWidget)
|
||||
|
||||
def retranslateUi(self, cloudConfigPageWidget):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
cloudConfigPageWidget.setWindowTitle(_translate("cloudConfigPageWidget", "Cloud configuration"))
|
||||
self.uiGenericEthernetGroupBox.setTitle(_translate("cloudConfigPageWidget", "Generic Ethernet NIO (Administrator or root access required)"))
|
||||
self.uiAddGenericEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteGenericEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiLinuxEthernetGroupBox.setTitle(_translate("cloudConfigPageWidget", "Linux Ethernet NIO (Linux only, root access required)"))
|
||||
self.uiAddLinuxEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteLinuxEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOEthernetTab), _translate("cloudConfigPageWidget", "Ethernet"))
|
||||
self.uiNIONATSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
|
||||
self.uiNIONATIdentifierLabel.setText(_translate("cloudConfigPageWidget", "Local identifier:"))
|
||||
self.uiNIONATListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
|
||||
self.uiAddNIONATPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIONATPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIONATTab), _translate("cloudConfigPageWidget", "NAT"))
|
||||
self.uiNIOUDPSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
|
||||
self.uiLocalPortLabel.setText(_translate("cloudConfigPageWidget", "Local port:"))
|
||||
self.uiRemoteHostLabel.setText(_translate("cloudConfigPageWidget", "Remote host:"))
|
||||
self.uiAddEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiAddAllEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
|
||||
self.uiDeleteEthernetPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiEthernetListWidget.setSortingEnabled(True)
|
||||
self.uiShowSpecialInterfacesCheckBox.setText(_translate("cloudConfigPageWidget", "&Show special Ethernet interfaces"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.EthernetTab), _translate("cloudConfigPageWidget", "Ethernet interfaces"))
|
||||
self.uiDeleteTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiTAPListWidget.setSortingEnabled(True)
|
||||
self.uiAddTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiAddAllTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add all"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.TAPTab), _translate("cloudConfigPageWidget", "TAP interfaces"))
|
||||
self.uiUDPTunnelSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "UDP tunnel settings"))
|
||||
self.uiRemoteHostLineEdit.setText(_translate("cloudConfigPageWidget", "127.0.0.1"))
|
||||
self.uiRemotePortLabel.setText(_translate("cloudConfigPageWidget", "Remote port:"))
|
||||
self.uiNIOUDPListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
|
||||
self.uiAddNIOUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIOUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOUDPTab), _translate("cloudConfigPageWidget", "UDP"))
|
||||
self.uiNIOTAPGroupBox.setTitle(_translate("cloudConfigPageWidget", "TAP interface (require root access)"))
|
||||
self.uiAddNIOTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIOTAPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOTAPTab), _translate("cloudConfigPageWidget", "TAP"))
|
||||
self.uiNIOUNIXSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
|
||||
self.uiLocalFileLabel.setText(_translate("cloudConfigPageWidget", "Local file:"))
|
||||
self.uiRemoteFileLabel.setText(_translate("cloudConfigPageWidget", "Remote file:"))
|
||||
self.uiNIOUNIXListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
|
||||
self.uiAddNIOUNIXPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIOUNIXPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOUnixTab), _translate("cloudConfigPageWidget", "UNIX"))
|
||||
self.uiNIOVDESettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
|
||||
self.uiVDEControlFileLabel.setText(_translate("cloudConfigPageWidget", "Control file:"))
|
||||
self.uiVDELocalFileLabel.setText(_translate("cloudConfigPageWidget", "Local file:"))
|
||||
self.uiNIOVDEListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
|
||||
self.uiAddNIOVDEPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIOVDEPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIOVDETab), _translate("cloudConfigPageWidget", "VDE"))
|
||||
self.uiNIONullSettingsGroupBox.setTitle(_translate("cloudConfigPageWidget", "Settings"))
|
||||
self.uiNIONullIdentifierLabel.setText(_translate("cloudConfigPageWidget", "Local identifier:"))
|
||||
self.uiNIONullListGroupBox.setTitle(_translate("cloudConfigPageWidget", "NIOs"))
|
||||
self.uiAddNIONullPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteNIONullPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.NIONullTab), _translate("cloudConfigPageWidget", "NULL"))
|
||||
self.uiAddUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Add"))
|
||||
self.uiDeleteUDPPushButton.setText(_translate("cloudConfigPageWidget", "&Delete"))
|
||||
self.uiUDPNameLineEdit.setText(_translate("cloudConfigPageWidget", "UDP tunnel 1"))
|
||||
self.uiRemoteHostLabel.setText(_translate("cloudConfigPageWidget", "Remote host:"))
|
||||
self.uiLocalPortLabel.setText(_translate("cloudConfigPageWidget", "Local port:"))
|
||||
self.uiUDPNameLabel.setText(_translate("cloudConfigPageWidget", "Name:"))
|
||||
self.uiUDPTunnelsGroupBox.setTitle(_translate("cloudConfigPageWidget", "UDP tunnels"))
|
||||
self.uiUDPTreeWidget.headerItem().setText(0, _translate("cloudConfigPageWidget", "Name"))
|
||||
self.uiUDPTreeWidget.headerItem().setText(1, _translate("cloudConfigPageWidget", "Local port"))
|
||||
self.uiUDPTreeWidget.headerItem().setText(2, _translate("cloudConfigPageWidget", "Remote host"))
|
||||
self.uiUDPTreeWidget.headerItem().setText(3, _translate("cloudConfigPageWidget", "Remote port"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.UDPTab), _translate("cloudConfigPageWidget", "UDP tunnels"))
|
||||
self.uiNameLabel.setText(_translate("cloudConfigPageWidget", "Name:"))
|
||||
self.uiNIOsTabWidget.setTabText(self.uiNIOsTabWidget.indexOf(self.MiscTab), _translate("cloudConfigPageWidget", "Misc."))
|
||||
self.uiDefaultNameFormatLabel.setText(_translate("cloudConfigPageWidget", "Default name format:"))
|
||||
self.uiSymbolLabel.setText(_translate("cloudConfigPageWidget", "Symbol:"))
|
||||
self.uiSymbolToolButton.setText(_translate("cloudConfigPageWidget", "&Browse..."))
|
||||
self.uiCategoryLabel.setText(_translate("cloudConfigPageWidget", "Category:"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.MiscTab), _translate("cloudConfigPageWidget", "Misc."))
|
||||
|
||||
|
||||
160
gns3/modules/builtin/ui/cloud_preferences_page.ui
Normal file
160
gns3/modules/builtin/ui/cloud_preferences_page.ui
Normal file
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>CloudPreferencesPageWidget</class>
|
||||
<widget class="QWidget" name="CloudPreferencesPageWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>546</width>
|
||||
<height>455</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Cloud nodes</string>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string>Cloud node templates</string>
|
||||
</property>
|
||||
<property name="accessibleDescription">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QTreeWidget" name="uiCloudNodesTreeWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>160</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string notr="true">1</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="uiCloudNodeInfoTreeWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="indentation">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="allColumnsShowFocus">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>1</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>2</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiNewCloudNodePushButton">
|
||||
<property name="text">
|
||||
<string>&New</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiEditCloudNodePushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Edit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="uiDeleteCloudNodePushButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Delete</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>uiNewCloudNodePushButton</tabstop>
|
||||
<tabstop>uiDeleteCloudNodePushButton</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
<designerdata>
|
||||
<property name="gridDeltaX">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="gridDeltaY">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="gridSnapX">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="gridSnapY">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="gridVisible">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</designerdata>
|
||||
</ui>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user