mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-06-07 11:06:28 +03:00
Compare commits
1477 Commits
v1.4.0alph
...
v2.0.0a1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
716f65786d | ||
|
|
0c397ba0a8 | ||
|
|
bff8a09147 | ||
|
|
f65ea13c6f | ||
|
|
3975b09898 | ||
|
|
6525d3130c | ||
|
|
5d8ad83cbe | ||
|
|
13a819ee87 | ||
|
|
cd1beb5191 | ||
|
|
483259ba2c | ||
|
|
3d828c42de | ||
|
|
d70dbe82d7 | ||
|
|
243c5a0f82 | ||
|
|
f4470d8190 | ||
|
|
b435163a3c | ||
|
|
26f6315b69 | ||
|
|
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 | ||
|
|
27f65a92e9 | ||
|
|
858f33f782 | ||
|
|
fa072bf387 | ||
|
|
154ea7354d | ||
|
|
53dbac1d5c | ||
|
|
a780866cd8 | ||
|
|
bf6f1af217 | ||
|
|
29c85f3c65 | ||
|
|
ba51d3ebf5 | ||
|
|
e4f3ea1da9 | ||
|
|
f037452188 | ||
|
|
3d35601d47 | ||
|
|
146e642097 | ||
|
|
e2c53443e3 | ||
|
|
965a98d5a7 | ||
|
|
41a68e7b0e | ||
|
|
20e62b824c | ||
|
|
36e9fcbddf | ||
|
|
bd3d33d67b | ||
|
|
caf77a24f1 | ||
|
|
729f9e103d | ||
|
|
7261debb79 | ||
|
|
b4d0deddfc | ||
|
|
d62a32c7d7 | ||
|
|
cb94fc4d1e | ||
|
|
554888b21d | ||
|
|
4ae01282d7 | ||
|
|
020e62ddf5 | ||
|
|
c1dce98595 | ||
|
|
676187061c | ||
|
|
14f4f26791 | ||
|
|
57cf10c251 | ||
|
|
70b4fae62c | ||
|
|
5c3b9e7fae | ||
|
|
9ea39a78db | ||
|
|
8f77697482 | ||
|
|
1b5e53ddc6 | ||
|
|
86ee9595f4 | ||
|
|
dd7937dba4 | ||
|
|
a4c646152e | ||
|
|
3d7db9a9c7 | ||
|
|
06ed266278 | ||
|
|
5df16db823 | ||
|
|
4f23706b19 | ||
|
|
e9ceadfc34 | ||
|
|
267cdc365d | ||
|
|
6dfbaccee1 | ||
|
|
f97f48d428 | ||
|
|
70b3bc680e | ||
|
|
c125579a38 | ||
|
|
9bc2a7e7bc | ||
|
|
a5358dec14 | ||
|
|
c84f554f36 | ||
|
|
0dd9803f74 | ||
|
|
2baaa26a42 | ||
|
|
3fd62f906e | ||
|
|
41e8b44dde | ||
|
|
09cbd9b606 | ||
|
|
3ab8d79ff7 | ||
|
|
97eaed6d2c | ||
|
|
805aa23948 | ||
|
|
a25c45a502 | ||
|
|
6ebda209ac | ||
|
|
d226b31fab | ||
|
|
27a3009ce1 | ||
|
|
654ad61105 | ||
|
|
31a461195f | ||
|
|
331eef3164 | ||
|
|
cb0d576ade | ||
|
|
32978381b2 | ||
|
|
a7f7a688d3 | ||
|
|
5d87726397 | ||
|
|
b9601cb54a | ||
|
|
bb66555896 | ||
|
|
37726630ce | ||
|
|
3d12f85f66 | ||
|
|
e26762fce3 | ||
|
|
32e59fcce4 | ||
|
|
9b43330e95 | ||
|
|
b3b94243b4 | ||
|
|
fba1032388 | ||
|
|
ccc0803c5b | ||
|
|
f0340bcc98 | ||
|
|
8d19b8fcbf | ||
|
|
dbf66d5a9a | ||
|
|
0fa8d61b19 | ||
|
|
02d326419b | ||
|
|
79b8baac9f | ||
|
|
8a6df8abc7 | ||
|
|
8b8d763fb7 | ||
|
|
608e80a80b | ||
|
|
d90f11eb86 | ||
|
|
dc39187091 | ||
|
|
e4cd418533 | ||
|
|
8559469c73 | ||
|
|
ac8d2beb80 | ||
|
|
5e6384074e | ||
|
|
e6ba7bdd98 | ||
|
|
784055689f | ||
|
|
c937811f45 | ||
|
|
79c8021faa | ||
|
|
a428730f59 | ||
|
|
2522bd44d6 | ||
|
|
3cad8ea046 | ||
|
|
76131f1cc7 | ||
|
|
54fb5dc765 | ||
|
|
4110af56e7 | ||
|
|
ab1775e44b | ||
|
|
9c3facb07a | ||
|
|
e5ae7b2d25 | ||
|
|
60462ff986 | ||
|
|
39443cd676 | ||
|
|
25ab8249ae | ||
|
|
6e86c606cc | ||
|
|
8878b96e74 | ||
|
|
c3ef2edbab | ||
|
|
f7e398edc3 | ||
|
|
8d4fc9585e | ||
|
|
5fe65e26ce | ||
|
|
daaebe7f96 | ||
|
|
f9f6a52e8b | ||
|
|
601c0217b9 | ||
|
|
b0971b4ba3 | ||
|
|
5a4549c36c | ||
|
|
fe2cc362f8 | ||
|
|
dd0220fd59 | ||
|
|
26fdd9ef6f | ||
|
|
733ee259e5 | ||
|
|
762fecbcff | ||
|
|
5e85dfe5fd | ||
|
|
e01632d60a | ||
|
|
60916e8b80 | ||
|
|
4fe55634ae | ||
|
|
b1b861c99d | ||
|
|
07c0474386 | ||
|
|
755fb5c8f3 | ||
|
|
fdb382874d | ||
|
|
471b3e1009 | ||
|
|
f64a226336 | ||
|
|
dec257bb6b | ||
|
|
e736fbbb87 | ||
|
|
aeb42b3ffe | ||
|
|
1694a57ed9 | ||
|
|
26001463a0 | ||
|
|
36c1197f1f | ||
|
|
1fa16936fe | ||
|
|
fca65784ee | ||
|
|
8983e6c5a9 | ||
|
|
c6e06f3941 | ||
|
|
b7e32a60ce | ||
|
|
da100e494b | ||
|
|
9d7b5bccb8 | ||
|
|
112b05c3dd | ||
|
|
987e85cce8 | ||
|
|
8142d0baa7 | ||
|
|
cc32b2661c | ||
|
|
781857f598 | ||
|
|
69179dcb63 | ||
|
|
8017838d60 | ||
|
|
81946493ec | ||
|
|
a7f40c3d50 | ||
|
|
c28723287f | ||
|
|
c7a8588647 | ||
|
|
4a64261c5d | ||
|
|
5f4365542c | ||
|
|
2e2c951ffc | ||
|
|
2ba7dde326 | ||
|
|
a1579ca86b | ||
|
|
81c4ddb85f | ||
|
|
285a8d413a | ||
|
|
672c86b38d | ||
|
|
905611d2a8 | ||
|
|
339fb22217 | ||
|
|
a8f5fa5dd5 | ||
|
|
bd365dd6eb | ||
|
|
8e4a6169e0 | ||
|
|
b1790844f3 | ||
|
|
4b0050d26c | ||
|
|
19bf40dc89 | ||
|
|
23cda61d17 | ||
|
|
c74ffde65a | ||
|
|
0a9c10e748 | ||
|
|
6973eaaa02 | ||
|
|
9ce483398b | ||
|
|
55744ab129 | ||
|
|
0e1cb47aa1 | ||
|
|
3bf12753df | ||
|
|
8907659220 | ||
|
|
a4ccb0b620 | ||
|
|
7d845c0ef8 | ||
|
|
8282ec1da6 | ||
|
|
edfe2c7f47 | ||
|
|
ca69673439 | ||
|
|
8677707a9a | ||
|
|
f3abbe5d58 | ||
|
|
c0f36590be | ||
|
|
bdb097a173 | ||
|
|
510491e26d | ||
|
|
6f0015a678 | ||
|
|
690e1d5ea0 | ||
|
|
19734e0bd3 | ||
|
|
5172c0b19e | ||
|
|
d74898c8a3 | ||
|
|
eb10f10988 | ||
|
|
eb2e504acc | ||
|
|
6c502e1213 | ||
|
|
462c00b358 | ||
|
|
b83eb61a42 | ||
|
|
96c35c49ff | ||
|
|
36b48f24dc | ||
|
|
efb21665f0 | ||
|
|
9ab77cfe20 | ||
|
|
6031a349be | ||
|
|
7163daf480 | ||
|
|
f662c39df0 | ||
|
|
d7caba76c4 | ||
|
|
36ff527cc0 | ||
|
|
d96ab77a67 | ||
|
|
b4ade1982e | ||
|
|
4c9ef92c2d | ||
|
|
078fc0f5ed | ||
|
|
40cc73c4f1 | ||
|
|
07c1443a8d | ||
|
|
dc592e0d4f | ||
|
|
c873b6c603 | ||
|
|
c7d0a7a504 | ||
|
|
e7eddf0a7e | ||
|
|
579e382971 | ||
|
|
95ecb05434 | ||
|
|
a2353f32e4 | ||
|
|
793181d208 | ||
|
|
932ff53190 | ||
|
|
e92e23a8be | ||
|
|
a16adb2b46 | ||
|
|
f579ebf5a0 | ||
|
|
67593c97ab | ||
|
|
fe185283a0 | ||
|
|
d2f8214a52 | ||
|
|
a2293b21ce | ||
|
|
57872c38b3 | ||
|
|
51f998c177 | ||
|
|
7abbc630c3 | ||
|
|
a0dee6cb42 | ||
|
|
d38143ae1c | ||
|
|
1db36dfb3e | ||
|
|
a347de96ed | ||
|
|
31c76bf56d | ||
|
|
8abf321d9a | ||
|
|
3f7b6e7be4 | ||
|
|
7b1337cd83 | ||
|
|
7f63a221af | ||
|
|
cfeb5c9495 | ||
|
|
17655cd855 | ||
|
|
ab071cd989 | ||
|
|
c73dd10783 | ||
|
|
6a5584ae41 | ||
|
|
16e82592a6 | ||
|
|
842e771eef | ||
|
|
d31f9c49f5 | ||
|
|
8b61c7ddc3 | ||
|
|
af247e2dd6 | ||
|
|
990cb49854 | ||
|
|
c530924d8a | ||
|
|
35360ae196 | ||
|
|
f37117ed09 | ||
|
|
9d2d80db25 | ||
|
|
364d8db5b2 | ||
|
|
0ac5215d4b | ||
|
|
493d81c519 | ||
|
|
c5ab0c8d02 | ||
|
|
03323e5df8 | ||
|
|
a8c429b77e | ||
|
|
52095c8ed9 | ||
|
|
b13855a062 | ||
|
|
0be67907ba | ||
|
|
cf7285a179 | ||
|
|
ba564869a0 | ||
|
|
a6ddddea99 | ||
|
|
a686a18d56 | ||
|
|
a98bd132e1 | ||
|
|
a100ca33b9 | ||
|
|
d2f75262d8 | ||
|
|
c65fd1683d | ||
|
|
76a8735133 | ||
|
|
da3c1334da | ||
|
|
0c370ec45e | ||
|
|
ec27e418fc | ||
|
|
92500e96d4 | ||
|
|
c2ba6f5cb3 | ||
|
|
07610e7556 | ||
|
|
741749dffb | ||
|
|
593c777ff0 | ||
|
|
8a616fa0c5 | ||
|
|
a191ec7326 | ||
|
|
f842812745 | ||
|
|
3cc43aa11f | ||
|
|
1d9839ed23 | ||
|
|
32a64f1259 | ||
|
|
3c4fc4c1bd | ||
|
|
1013cf527d | ||
|
|
1684ee3074 | ||
|
|
f50e08207c | ||
|
|
d3973d4a7b | ||
|
|
7170e58551 | ||
|
|
02968f1ece | ||
|
|
eb94ba8b93 | ||
|
|
67c6e03f3b | ||
|
|
faa3b519b6 | ||
|
|
3b4dca7545 | ||
|
|
66495cbf83 | ||
|
|
95fea87b29 | ||
|
|
4b0cea36d0 | ||
|
|
ec27683f0e | ||
|
|
a4d7aaf0a1 | ||
|
|
e2bcd1fdd1 | ||
|
|
d7fd04de03 | ||
|
|
f447cd9be6 | ||
|
|
a080b65615 | ||
|
|
bd034237c0 | ||
|
|
66aff0d9a2 | ||
|
|
04a4142128 | ||
|
|
ec71da4062 | ||
|
|
0ae97aa933 | ||
|
|
580e19de0c | ||
|
|
e41e95b0e6 | ||
|
|
1adeee9ba1 | ||
|
|
b598339e55 | ||
|
|
c65e5d8795 | ||
|
|
b8597bc196 | ||
|
|
be75a8e2b8 | ||
|
|
2352840c3b | ||
|
|
98f9f59af3 | ||
|
|
00ba2f1046 | ||
|
|
0389245cb7 | ||
|
|
2f746ff791 | ||
|
|
8b881dfd20 | ||
|
|
cfece32185 | ||
|
|
d09ce859aa | ||
|
|
227a8a0c9e | ||
|
|
1b0da301e3 | ||
|
|
0f3efaa3fe | ||
|
|
55b5f49c79 | ||
|
|
3601acf66e | ||
|
|
383306c0ab | ||
|
|
5f87be16d7 | ||
|
|
63ab01d364 | ||
|
|
4252f19819 | ||
|
|
1095ed1663 | ||
|
|
c5557e45c4 | ||
|
|
f5159a93f3 | ||
|
|
b00d39e531 | ||
|
|
0c077b5647 | ||
|
|
98aed2a8b6 | ||
|
|
0523873e5e | ||
|
|
ed5ce93df6 | ||
|
|
ab4f85b862 | ||
|
|
67912a918f | ||
|
|
0af939bd37 | ||
|
|
b712b54786 | ||
|
|
19bf52661e | ||
|
|
c0c0261c68 | ||
|
|
1eea941b55 | ||
|
|
547a8fb6c4 | ||
|
|
a0271926e6 | ||
|
|
7e796fc3f1 | ||
|
|
ba17b9d789 | ||
|
|
a4c17562ad | ||
|
|
ca173fb491 | ||
|
|
9330689641 | ||
|
|
ca67553e66 | ||
|
|
dabcb6d4f4 | ||
|
|
753eb79a15 | ||
|
|
034c9e8ad5 | ||
|
|
ba04783e8f | ||
|
|
a8ba51cdee | ||
|
|
24997c39f5 | ||
|
|
919311e2ab | ||
|
|
61c3b0bfff | ||
|
|
8024738400 | ||
|
|
ac58bc677f | ||
|
|
3b887f7f1b | ||
|
|
8ea24a156e | ||
|
|
264fe8f0c8 | ||
|
|
3f67cd4a99 | ||
|
|
5d9e9dfc4a | ||
|
|
95bec70e27 | ||
|
|
c1c6c812c8 | ||
|
|
0a8d947375 | ||
|
|
c9a699fc30 | ||
|
|
8bf350a7dd | ||
|
|
bbc468db0f | ||
|
|
2353f074ab | ||
|
|
79b7174a83 | ||
|
|
82e1c088c7 | ||
|
|
1d13a3de3c | ||
|
|
0fb0bafc14 | ||
|
|
6cc5547509 | ||
|
|
a72df53b69 | ||
|
|
f0802038db | ||
|
|
631b4b7a61 | ||
|
|
d9436520af | ||
|
|
d805cfbd6a | ||
|
|
078d5023c4 | ||
|
|
bc053da538 | ||
|
|
5d79bbce39 | ||
|
|
98c8d56dc5 | ||
|
|
1acaa18c3c | ||
|
|
8325fe0cee | ||
|
|
eedd12207c | ||
|
|
64ed8c3de0 | ||
|
|
ab4f2f3922 | ||
|
|
fc5bf2dc4b | ||
|
|
bd0fabd1f6 | ||
|
|
a4a2963bc3 | ||
|
|
310f47b52a | ||
|
|
b4a187dd02 | ||
|
|
f4afdca576 | ||
|
|
28bebff7b0 | ||
|
|
c3a4daef87 | ||
|
|
18a5a28283 | ||
|
|
36d08d2dca | ||
|
|
91bf6e667d | ||
|
|
a88a47e223 | ||
|
|
8ede0f2089 | ||
|
|
576f6c81a1 | ||
|
|
1666fc4aa0 | ||
|
|
1216882e89 | ||
|
|
3f825a163b | ||
|
|
2fcfa10573 | ||
|
|
dd95b24d32 | ||
|
|
bb2777ed5b | ||
|
|
e0f03ec582 | ||
|
|
51e844559e | ||
|
|
dc4a984c41 | ||
|
|
54139845ac | ||
|
|
8f9672d9e2 | ||
|
|
b007aea2f5 | ||
|
|
3900645d1f | ||
|
|
5d644467fc | ||
|
|
29fcd07b1c | ||
|
|
22c2ee31c4 | ||
|
|
69f99742a8 | ||
|
|
e7dc901d5e | ||
|
|
4dba7fa7fc | ||
|
|
55d4201aaf | ||
|
|
74a4e464c8 | ||
|
|
329c196047 | ||
|
|
85e74b482e | ||
|
|
dc12fdd1c9 | ||
|
|
0ee64ecbb8 | ||
|
|
2946e931aa | ||
|
|
30871f0cd1 | ||
|
|
70aa165444 | ||
|
|
beb35c6108 | ||
|
|
90d1cb9a7f | ||
|
|
5754d66560 | ||
|
|
77ba02f0ae | ||
|
|
effdc862a2 | ||
|
|
59c4736f41 | ||
|
|
dff831eafe | ||
|
|
4765640dc7 | ||
|
|
b8799a91b4 | ||
|
|
17aaa90fb1 | ||
|
|
40b8969d44 | ||
|
|
73454d97e1 | ||
|
|
7cf39aac5a | ||
|
|
2880168566 | ||
|
|
615d9240ad | ||
|
|
76f8aa4f60 | ||
|
|
0172dd0b53 | ||
|
|
ccb6a5cf0f | ||
|
|
7dc37c9dca | ||
|
|
69f13621ca | ||
|
|
92b9efea12 | ||
|
|
d1af92942d | ||
|
|
b47b5e2dc0 | ||
|
|
3265a94d26 | ||
|
|
e9d9dc2748 | ||
|
|
8b38d4967c | ||
|
|
3729070e33 | ||
|
|
d415c20446 | ||
|
|
efb5931be3 | ||
|
|
4867a5510a | ||
|
|
932dd79ac1 | ||
|
|
dc5957dd0a | ||
|
|
bb131b4ff5 | ||
|
|
76e3d3523e | ||
|
|
2bc9dd2802 | ||
|
|
0950c3d80d | ||
|
|
808ea00787 | ||
|
|
c4400f8a64 | ||
|
|
4a78cfe00a | ||
|
|
a10f8939e2 | ||
|
|
4b681a5e55 | ||
|
|
4e073b4681 | ||
|
|
e9548b2e03 | ||
|
|
05820e973b | ||
|
|
f779b6fe7d | ||
|
|
210c46f49d | ||
|
|
e746ddc525 | ||
|
|
d186d4ce1e | ||
|
|
cee80cc579 | ||
|
|
1c0232cf96 | ||
|
|
77ff7a8aba | ||
|
|
fed48de0d5 | ||
|
|
95c1df229f | ||
|
|
78b2cb8a32 | ||
|
|
e1ba7a0ad6 | ||
|
|
e64597eaf0 | ||
|
|
f1b7ea13ef | ||
|
|
25d6a0a160 | ||
|
|
5be13a645e | ||
|
|
dfa85e9886 | ||
|
|
8a612970e6 | ||
|
|
f18a9fe7f5 | ||
|
|
a5d0fe5b8b | ||
|
|
3ef70a1213 | ||
|
|
3bd2136450 | ||
|
|
c165146d7c | ||
|
|
adfdae3a8e | ||
|
|
f92aae4e6e | ||
|
|
bed867cf7b | ||
|
|
81aaeaf25f | ||
|
|
3cc62563df | ||
|
|
aaa21be642 | ||
|
|
aa87cb0064 | ||
|
|
1e6e2d32e3 | ||
|
|
a95805bc04 | ||
|
|
a5ccce30d6 | ||
|
|
f6fdd79d0f | ||
|
|
3f9c1af8e8 | ||
|
|
be9a88029b |
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
gns3/version.py merge=ours
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -58,3 +58,5 @@ keys
|
||||
# Custom config
|
||||
/gns3_server.ini
|
||||
updates
|
||||
.cache
|
||||
__pycache__
|
||||
|
||||
30
.travis.yml
30
.travis.yml
@@ -1,23 +1,7 @@
|
||||
language: python
|
||||
sudo: required
|
||||
|
||||
#New container architecture
|
||||
#http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
#sudo: false
|
||||
|
||||
python:
|
||||
- "3.4"
|
||||
|
||||
cache:
|
||||
apt: true
|
||||
directories:
|
||||
- build
|
||||
|
||||
before_install:
|
||||
- sudo add-apt-repository --yes ppa:ubuntu-sdk-team/ppa
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install qtbase5-dev qtdeclarative5-dev libqt5webkit5-dev libsqlite3-dev
|
||||
- sudo apt-get install qt5-default qttools5-dev-tools
|
||||
- sh scripts/prepare_travis.sh
|
||||
services:
|
||||
- docker
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
@@ -29,9 +13,7 @@ notifications:
|
||||
# on_success: change
|
||||
# on_failure: always
|
||||
|
||||
install:
|
||||
- "pip install -r dev-requirements.txt"
|
||||
|
||||
script:
|
||||
- "xvfb-run py.test -vv" # Run tests in a fake X server
|
||||
# - "pep8 --exclude=build,.git,ui"
|
||||
- docker build -t gns3-gui-test .
|
||||
- docker run gns3-gui-test
|
||||
|
||||
|
||||
693
CHANGELOG
693
CHANGELOG
@@ -1,6 +1,669 @@
|
||||
# Change Log
|
||||
# Change Log
|
||||
|
||||
## 1.4.0alpha3 28/07/201
|
||||
## 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
|
||||
* Link to download VIX api
|
||||
* Fix SSH present in the server preferences
|
||||
* Check dynamips and ubridge permission.
|
||||
* Show a dialog for checking some common issues
|
||||
* Show a summary with server usages
|
||||
* Close project on VM when closing the project on all servers
|
||||
* Allow to not rotate the text when changing all text colors
|
||||
* Raise an error when psutil is too old
|
||||
* Fix a race condition when closing the server
|
||||
* Fix a very very rare crash in topology summary view
|
||||
* Reset port label positions. Fixes #811.
|
||||
* Drop SSH support
|
||||
* Fix inversion of port label when loading a topology
|
||||
* Fix error when importing Windows XP OVA
|
||||
* Warn if configuration file contain invalid unicode characters
|
||||
* Fix a crash when importing some OVA
|
||||
* Reset the telnet command on Mac
|
||||
* Fix only one console work for OSX
|
||||
* Fix a rare crash when loading topology with missing image
|
||||
* Fix a rare race condition when inserting an image
|
||||
* Fix a crash when pid file is empty
|
||||
* Fix a rare crash in progress dialogs
|
||||
* Fix a crash if you don't have vms when importing a gns3a
|
||||
* Warn user during appliance install if server is not avaible
|
||||
* Fix error when you stop the GNS3 VM but break the config before
|
||||
* startup_config is not mandatory inside .gns3 file
|
||||
* Fix wrong host on SSH connection
|
||||
* Fix select of image broken if you need to select multiple images
|
||||
* Fix a crash when changing qemu cluster size to more than 512
|
||||
* Fix IOU support in gns3a
|
||||
* Add urxvt support
|
||||
* Add a step in the wizard checking KVM support
|
||||
|
||||
## 1.4.0 12/01/2016
|
||||
|
||||
* Fix rare crash when showing the progress dialog
|
||||
* 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
|
||||
|
||||
* Add information about antivirus and firewall in case of connection fail
|
||||
* Change link to doc for missing router image
|
||||
* On windows and OSX experimental Qemu GNS3A support
|
||||
* Wait server in thread
|
||||
* Fix local server non avaible in appliance wizard when local server started by hand.
|
||||
* Improve pid checks
|
||||
* Allow Qemu appliances to optionally specify desired disk interfaces in their configuration (defaults to "ide").
|
||||
* Fix project non closing when you have only remote servers
|
||||
* Fix Windows layout not saved in some scenario
|
||||
* Added the ability to name Qemu/VirtualBox/VMware interfaces with numbers starting from 1, along with named aliases for numbers starting from 0, and a tooltip explaining the possible name format variables.
|
||||
* Added missing Qemu adapters to the topology schema.
|
||||
* Fix Cannot save my topology getting an error message for temporary topology
|
||||
* Fix creation of ASA devices
|
||||
* Turn off Docker until 1.5
|
||||
* Fix display of server preferences on small screen
|
||||
* Fix If you turn off the local server and close the gui and reopen preferences you have an issue
|
||||
* Zoc 7 support
|
||||
* Fix warning when closing GUI
|
||||
* Fix crash when opening a new topologies after gns3 converter failure
|
||||
|
||||
## 1.3.13 11/12/2015
|
||||
|
||||
* Release server 1.3.13
|
||||
|
||||
## 1.3.12 11/12/2015
|
||||
|
||||
* Fix warning when closing GUI
|
||||
* Update links for new website.
|
||||
* Ask user to send bug reports to GNS3.com
|
||||
* Fix travis dependencies installation
|
||||
* Drop Webkit from 1.3.X
|
||||
* Fix crash when opening a new topologies after gns3 converter failure
|
||||
* Set Wireshark 2.0 as default OSX version
|
||||
* iTerm 2.9 support
|
||||
* Add informations about GNS3 VM
|
||||
* Drops securecrt.vbs
|
||||
* Fix analytics report on OSX
|
||||
* Analytics send windows
|
||||
* Fix the progress dialog freeze bug
|
||||
* Fix typo in analytics
|
||||
* Send stats to GNS3 team
|
||||
* Licenses compliance.
|
||||
* Fix error when importing dynamips config from non existent directory
|
||||
* Fix crash when url is invalid
|
||||
* Add a debug level 2 in the console
|
||||
|
||||
## 1.4.0rc2 10/12/2015
|
||||
|
||||
* Prevent user turning off the Local server when using the GNS3 VM
|
||||
* Force VM wizard to be modal
|
||||
* Fix unicode error when exporting debug informations
|
||||
* Fix Debug can't be deactivated for current session
|
||||
* Support relative path for configuration file.
|
||||
* Report bug to GNS3.com
|
||||
* Avoid crash when cancel connection to a server
|
||||
* Fix version number display twice when installing appliance
|
||||
* Show a warning when you try to save as a remote topology
|
||||
* Fix application restart after self upgrade
|
||||
* Cleanup server autostart
|
||||
* Have default console port start from 2000.
|
||||
* Fix crash when opening a new topologies after gns3 converter failure
|
||||
* Fix SSH support
|
||||
* Fix bus error when writting on console
|
||||
* Fix Ok & Cancel button in preferences are broken
|
||||
* Fix After a project load failure you can't open new project
|
||||
* More fix around closing the GUI
|
||||
* Fix crash in progress dialog on OSX
|
||||
* Kill already running zombie server
|
||||
* Store the pid of the server when started
|
||||
* Fix a crash in rare case after a PyQT garbage collect
|
||||
* Allow to use the VNC port range for console
|
||||
* Preferences dialog resize
|
||||
* Fix a race condition when opening telnet from apple script
|
||||
* Experimental support for tabbed terminal on OSX
|
||||
* Fix validation error when saving topology with an usage
|
||||
* Fix another case of not closing windows
|
||||
* Fix GUI doesn't close after connection error to remote server
|
||||
* Add usage text to device template and on hover
|
||||
* Fix a rare crash in progress dialog
|
||||
* Fix crash when displaying an error from the update
|
||||
* Url encode royal tx url
|
||||
* Use Royal TX URI scheme thanks to @lemonmojo
|
||||
* OSX support for Royal TSX
|
||||
* Merge branch 'master' into unstable
|
||||
* Set Wireshark 2.0 as default OSX version
|
||||
* iTerm 2.9 support
|
||||
* Update debug information dialog.
|
||||
* Change text for export debug information.
|
||||
* Add informations about GNS3 VM
|
||||
|
||||
## 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
|
||||
* Warn users about the need to uncompress the image
|
||||
* Fix crash when you have no qemu vms and use gns3a
|
||||
* Change sentry key
|
||||
* Fix format_exception() missing 2 required positional arguments: 'value' and 'tb' in topologyFile
|
||||
* Fix dialog box not returning their result
|
||||
* Log to console the Qt Message Boxes
|
||||
* Drops securecrt.vbs
|
||||
|
||||
## 1.4.0b5 02/11/2015
|
||||
|
||||
* Fix crash when loading invalid appliance file
|
||||
* Show a message is starting or is stopping in progress dialog
|
||||
* Drop duplicate code
|
||||
* Changes some references for "GNS3 VM" to "Local GNS3 VM". Ref #506.
|
||||
* Fixes progress dialog remains #741.
|
||||
* Support more boot order for Qemu
|
||||
* Add Royal TS terminal
|
||||
* Add a Qt compatible version of partial
|
||||
* Show Upload filename instead of waiting for
|
||||
* Fix error where ISO is detected as an OVA during gns3a import
|
||||
* Removes News Dock Widget.
|
||||
* Support cdrom image in missing images detections
|
||||
* Fix crash when appliance is missing
|
||||
* Support for capture description in Wireshark window title.
|
||||
* Set the name of the VM in OSX Terminal application
|
||||
* Fix crash of symbol selector if the first file is a non builtin symbol
|
||||
* Install appliance from wizard instead of web app
|
||||
* 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
|
||||
|
||||
* Mockup of appliances wizard
|
||||
* Fix tests
|
||||
* Fix Crash when opening an appliance #728
|
||||
* Mock up for appliance wizard.
|
||||
* Registry: add -nographic to Qemu options by default. Fixes #730.
|
||||
* Registry: support for initrd, cpu throttling and process priority.
|
||||
* Support for modifications to a base Qemu VM (not a linked clone).
|
||||
* Registry: adds support for switch category, first_port_name and port_segment_size.
|
||||
* Fix traceback when exiting the GUI
|
||||
* Show a download button
|
||||
* Corrects some typos.
|
||||
* Drops securecrt.vbs
|
||||
* Display an error message if QtWebkit is not installed.
|
||||
* Fix console port lost when applying settings
|
||||
* Remove unused code
|
||||
* Fix analytics report on OSX
|
||||
* Fix invalid path with frozen application
|
||||
* When raising an appliance not found error show full path
|
||||
* Remove unnecessary checks to know if the local server is running.
|
||||
* Analytics send windows
|
||||
* Fixes issue when loading a project using VMware vmnet interfaces. Fixes #319.
|
||||
* Fix the progress dialog freeze bug
|
||||
* Revert "Try to solve Scanning for Appliance images doesn't end"
|
||||
* Try to fix the progress dialog freeze bug
|
||||
* Drop dead code from getting started dialog
|
||||
* Fix Appliance installs image without adapting the filename
|
||||
* Raise error if reference in GNS3a is invalid
|
||||
* Fix typo in analytics
|
||||
* Add information on how to debug
|
||||
* Send stats to GNS3 team
|
||||
* Licenses compliance.
|
||||
* Handles warning notifications.
|
||||
* Try to solve Scanning for Appliance images doesn't end
|
||||
* Fix TypeError: 'NoneType' object is not iterable in isLocalServerRunning
|
||||
* Fix crash AttributeError: 'NoneType' object has no attribute getNewProjectSettings
|
||||
* Fix error when importing dynamips config from non existent directory
|
||||
* Fix crash when url is invalid
|
||||
* Add a debug level 2 in the console
|
||||
* Fix a crash when loading appliance
|
||||
* Merge branch 'master' into unstable
|
||||
* Support upload of multiple vmdk file
|
||||
* Support PNG in the custom symbol selection dialog
|
||||
* Add custom messages when computing Idle-PC values. Fixes #704.
|
||||
* Display the version of Qt in the console
|
||||
* Do not crash when parsing a Qt version with a snapshot notation
|
||||
* Force nc path to /usr/bin/nc on Apple
|
||||
* Revert "Drop netcat for unix socket it's not supported by OSX"
|
||||
* Catch errors when we have an infinite recursion when copying a folder
|
||||
* Fix crash in recent files when changing locale
|
||||
* Catch error when we can't extract egg
|
||||
* Fix securecrt command line (backported from master)
|
||||
* Updates SecureCRT command line.
|
||||
* When it's an ova explain to user he need to download the ova
|
||||
* OVA file support
|
||||
* Ignore .cache directory
|
||||
* Fix update manager crash on Windows
|
||||
* Support for image in local subdirectory
|
||||
* Fix duplicate code
|
||||
* Fixes issue when saving Idle-PC into template. Fixes #674.
|
||||
* Add link for downloading VMware
|
||||
* Cache md5sum in memory when loading a gns3a
|
||||
* Support symbol import from GNS3A
|
||||
* Allow user to select symbol from his library
|
||||
* Improve HTTP progress reliability
|
||||
* Support ubuntu default VNC client (Vinagre)
|
||||
* Adds the COPYING file.
|
||||
* Fix error when receiving an HTTP error during HTTP progress
|
||||
* Support port_name_format in GNS3 a files
|
||||
* options is not mandatory in a .gns3 file
|
||||
* Xshell 5 support
|
||||
* Add missing gns3-converter to requirements.txt
|
||||
* Fixes Qemu binaries not listed in the node configuration dialog. Fixes #683.
|
||||
* Fixes SecureCRT command line.
|
||||
* Speedup directory scan for images when loading a gns3a file
|
||||
* Show a progress bar during directory scan when searching for appliances
|
||||
* 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
|
||||
|
||||
* Display the version of Qt in the console
|
||||
* Catch errors when we have an infinite recursion when copying a folder
|
||||
* Fix crash in recent files when changing locale
|
||||
* Catch error when we can't extract egg
|
||||
* Updates SecureCRT command line.
|
||||
* Fixes issue when saving Idle-PC into template. Fixes #674.
|
||||
* Adds the COPYING file.
|
||||
* Xshell 5 support
|
||||
* Add missing gns3-converter to requirements.txt
|
||||
* Fixes issue when Telnet doesn't let you to login to an appliance on Linux.
|
||||
* Improve alignments. Fixes #215.
|
||||
* Spelling correction
|
||||
|
||||
## 1.4.0b3 22/09/15
|
||||
|
||||
* Add a warning if you don't select VMware or VBox in Setup Wizard
|
||||
* Fix Configuration not always saved in client
|
||||
* Fixes file path reference issue.
|
||||
* Fix Appliance doesn't work on local Server #669
|
||||
* Allows Qemu VM template editing if the server is not running. Fixes #671.
|
||||
* Fixes NIO_VMNET != NIO_VMnet.
|
||||
* Automatically add the -no-kvm option if -icount is detected to help with the migration of ASA VMs created before version 1.4
|
||||
* Fix the Runtime error
|
||||
* Improve alignments. Fixes #215.
|
||||
* Updates kernel command line of ASA.
|
||||
|
||||
## 1.4.0beta2 17/09/15
|
||||
|
||||
* Drop netcat for unix socket it's not supported by OSX
|
||||
* VMware fusion is supported
|
||||
* Fix race conditions in http_client
|
||||
* On OSX show a warning when using an old Qemu
|
||||
* Tab support for xfce4-terminal
|
||||
* Improve alignments. Fixes #215.
|
||||
* Fixes ethertype validation error.
|
||||
* Improve check for uBridge permissions.
|
||||
* Fix an uuid is display instead of the server url
|
||||
* Set root permission to ubridge on OSX
|
||||
* Show GNS3 version at startup
|
||||
* Allows to select a remote server to run a switch. Fixes #653.
|
||||
* Support for packet capture on VMware VM links.
|
||||
* Always use ubridge with VMware by default
|
||||
* Fix PermissionError: [Errno 1] Operation not permitted when kill process
|
||||
* Support drag & drop gns3a
|
||||
* Fix an error when loading IOU schema with private_config
|
||||
* Support open a file via double click on OSX
|
||||
* Initial version of an appliance file format
|
||||
* Adds docker symbols. Fixes #643.
|
||||
* Call the vmnet management script from the GUI (with admin rights). Implements #639.
|
||||
* Use self update only if experimental features are allowed
|
||||
* Add an option for enabling experimental features
|
||||
* Backport docker support from Google Summer Of Code (not enabled)
|
||||
* Merge branch 'qinq_ethertype' of https://github.com/Bevaz/gns3-gui into Bevaz-qinq_ethertype
|
||||
* Allows VMware VMs to use vmnet interfaces for connections without using uBridge.
|
||||
* Add a firewall symbol
|
||||
* Detect broken link in topologies
|
||||
* Fix project not closing
|
||||
* Fix autostart
|
||||
* Fix file not found exception in vpcs list dir
|
||||
* Add missing virtio-net-pci to the json schema
|
||||
* Fix the all devices views
|
||||
* Fix double click event on Note Item
|
||||
* Fix Accepting insecure https connections creates additional server entry
|
||||
* Allow developer to debug packet capture on Windows
|
||||
* Fix saveAs error unsupported operand type(s) for +=: 'NoneType' and 'str'
|
||||
* Catch error when antivirus corrupt our own JSON errors
|
||||
* Add a note about VIX API require for VMware player
|
||||
* Create image directory if not exists
|
||||
* Allow GUI to be start with python -m gns3
|
||||
* Avoid errors in qemu configuration if server has been deleted
|
||||
* Fix error when the IOS image directory is not writable
|
||||
* Do not crash if something intercept the call to the update server
|
||||
* Fix super(): no arguments in SSH client
|
||||
* Catch error when configuration file contain invalid UTF-8 chars
|
||||
* Fix JSON schema for dynamips power supply and sensors
|
||||
* Fix missing boot_priority in JSON schema
|
||||
* Complete the error message about corrupted topologies
|
||||
* Removes ASAv warning in Qemu Wizard.
|
||||
* EthernetSwitch: Allow to choose ethertype for QinQ outer tag.
|
||||
* Adds missing properties for rectangle and ellipse in schema validation.
|
||||
* Use Qemu 0.11.0 instead of version 0.13.0 on Windows.
|
||||
* Fixes bug when opening Node properties dialog via a double click.
|
||||
* SecureCRT (installed on personal profile) command line.
|
||||
|
||||
## 1.3.10 04/09/2015
|
||||
|
||||
* Updates kernel command line of ASA.
|
||||
* Fix file not found exception in vpcs list dir
|
||||
* Fix saveAs error unsupported operand type(s) for +=: 'NoneType' and 'str'
|
||||
* Catch error when antivirus corrupt our own JSON errors
|
||||
* Use Qemu 0.11.0 instead of version 0.13.0 on Windows.
|
||||
* Removes "resources_type" references. Fixes #493.
|
||||
* Fixes bug when opening Node properties dialog via a double click.
|
||||
* SecureCRT (installed on personal profile) command line.
|
||||
|
||||
## 1.4.0beta1 07/08/2015
|
||||
|
||||
* Show an error if you try to use a local server not started
|
||||
* CLear the list before asking for VM list for Vbox and Vmware
|
||||
* Fix password lost for remote servers
|
||||
* Fix schema for virtualbox 1.3.2 topologies
|
||||
* Refactor all VM wizards
|
||||
* Fix Apply not enabled when removing a remote server
|
||||
* Fix remote server list display when use_local_server for Qemu
|
||||
* Fixes #601 (spelling error).
|
||||
* Store the url of server in gns3_gui for third party apps
|
||||
* Fix error when editing a qemu device with a relative path
|
||||
* Catch exception when starting packet capture reader for a remote packet capture. Fixes #597.
|
||||
* Fixes KeyError: 'vmx_path'. Fixes #595.
|
||||
* Support for CPUs setting for Qemu VMs.
|
||||
* Fix chicken of VNC command and add a warning about bugs in OSX VNC
|
||||
* Fix issue whith auto update when we release a new build
|
||||
|
||||
## 1.4.0alpha4 04/08/2015
|
||||
|
||||
* Use half the available physical memory for the GNS3 VM in the Setup Wizard.
|
||||
* Drop useless notion of resource_type
|
||||
* Fix When you modify config from outside router list is not refresh
|
||||
* Sync auth settings between 1.3 and 1.4
|
||||
* Group HTTP bad request by path
|
||||
* Disallow connection to a different major version in all cases
|
||||
* Show the server choice in wizard if you have a VM or a remote server
|
||||
* Fix local server settings erased at each launch
|
||||
* Support for Qemu disk interfaces, cd/dvd-rom image and boot priority.
|
||||
* Prevents progress dialog to stay displayed (fixes graphical bug).
|
||||
* Check that DHCP is enabled on the VirtualBox host-only network associated with the VirtualBox GNS3 VM. Fixes #287.
|
||||
* Fixes Qt5 incompatibility.
|
||||
* Catch exception when trying to launch Wireshark.
|
||||
* Fixes migration of cloud interfaces. Fixes #582.
|
||||
* Wait for the server to be fully started in the GNS3 VM. Fixes #581.
|
||||
* Fix issue with file upload and Qt 5.5
|
||||
* Improves the symbol dialog. Implements #514.
|
||||
|
||||
## 1.3.9 03/08/2015
|
||||
|
||||
* Catch exception when trying to launch Wireshark.
|
||||
* Backport: fixes migration of cloud interfaces.
|
||||
|
||||
## 1.4.0alpha3 28/07/2015
|
||||
|
||||
* IOUVM converter
|
||||
* Create qemu disk image on remote server
|
||||
@@ -13,7 +676,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.
|
||||
@@ -40,7 +703,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.
|
||||
@@ -117,7 +780,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
|
||||
|
||||
@@ -138,7 +801,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
|
||||
@@ -155,7 +818,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.
|
||||
@@ -300,7 +963,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
|
||||
@@ -317,7 +980,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
|
||||
@@ -330,7 +993,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.
|
||||
@@ -339,7 +1002,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.
|
||||
@@ -368,7 +1031,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
|
||||
@@ -389,7 +1052,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.
|
||||
@@ -410,7 +1073,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).
|
||||
@@ -457,7 +1120,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.
|
||||
|
||||
54
CONTRIBUTING.md
Normal file
54
CONTRIBUTING.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Contributing to GNS3
|
||||
|
||||
We welcome contributions and bugs reports from everyone.
|
||||
We are friendly so don't be afraid to ask questions.
|
||||
|
||||
## Bug reports
|
||||
|
||||
Before reporting an issue:
|
||||
* check our website over at https://gns3.com
|
||||
* check if an issue already exists on https://github.com/GNS3/gns3-gui
|
||||
* check if an issue already exists on https://github.com/GNS3/gns3-server
|
||||
|
||||
Please post on our community website if you are unsure you found a bug,
|
||||
you will get faster support and be able to exchange with more users.
|
||||
|
||||
If you are unsure which project you should create an issue for, just do
|
||||
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
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
||||
Don't hesitate to share your plans before starting working on a contribution, we can help
|
||||
you to find the best approach.
|
||||
|
||||
### Contributors License Agreements
|
||||
|
||||
We at GNS3 are eager to work with you. For small changes — little bugfixes, correcting typos, and the like — please just submit pull requests to any of our projects. For larger changes, though, we have to ask you to jump through a little hoop.
|
||||
|
||||
In particular, in order for us to accept any major patches from you, you will have to electronically sign a statement that indicates two things:
|
||||
|
||||
- You are willingly licensing your contributions under the terms of the open source license of the project that you’re contributing to.
|
||||
- You are legally able to license your contributions as stated.
|
||||
|
||||
The reason we do this is to ensure, to the extent possible, that we don’t “taint” the projects we manage with contributions that turn out to be improper. This protects everyone who wants to use the projects, including you!
|
||||
|
||||
More information there: https://github.com/GNS3/cla
|
||||
|
||||
### 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.
|
||||
504
COPYING
Normal file
504
COPYING
Normal file
@@ -0,0 +1,504 @@
|
||||
GNU Public License (GPL)
|
||||
------------------------
|
||||
|
||||
GNS3 is released under the GPLv3 (see LICENSE) with the additional
|
||||
exemption that compiling, linking, and/or using OpenSSL is allowed.
|
||||
|
||||
GNS3 trademark
|
||||
--------------
|
||||
|
||||
"GNS3" is a trademark of GNS3 Technologies, Inc.
|
||||
|
||||
Windows Driver Kit
|
||||
------------------
|
||||
|
||||
The Windows binary distribution includes devcon.exe, a Microsoft(R)
|
||||
Windows Driver Kit (WDK) sample in object code form which is
|
||||
redistributed under the terms of the WDK License terms.
|
||||
|
||||
With respect to binaries built using the Microsoft(R) Windows
|
||||
Driver Kit (WDK), GPLv3 does not extend to any WDK Distributable Code.
|
||||
All WDK Distributable Code is considered by the licensors of GNS3
|
||||
to constitute, or be equivalent to, "System Libraries" as defined in
|
||||
section 1 of GPLv3.
|
||||
|
||||
OpenSSL License
|
||||
---------------
|
||||
|
||||
The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
|
||||
the OpenSSL License and the original SSLeay license apply to the toolkit.
|
||||
See below for the actual license texts. Actually both licenses are BSD-style
|
||||
Open Source licenses. In case of any license issues related to OpenSSL
|
||||
please contact openssl-core@openssl.org.
|
||||
|
||||
/* ====================================================================
|
||||
* Copyright (c) 1998-2003 The OpenSSL Project. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in
|
||||
* the documentation and/or other materials provided with the
|
||||
* distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this
|
||||
* software must display the following acknowledgment:
|
||||
* "This product includes software developed by the OpenSSL Project
|
||||
* for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
|
||||
*
|
||||
* 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
|
||||
* endorse or promote products derived from this software without
|
||||
* prior written permission. For written permission, please contact
|
||||
* openssl-core@openssl.org.
|
||||
*
|
||||
* 5. Products derived from this software may not be called "OpenSSL"
|
||||
* nor may "OpenSSL" appear in their names without prior written
|
||||
* permission of the OpenSSL Project.
|
||||
*
|
||||
* 6. Redistributions of any form whatsoever must retain the following
|
||||
* acknowledgment:
|
||||
* "This product includes software developed by the OpenSSL Project
|
||||
* for use in the OpenSSL Toolkit (http://www.openssl.org/)"
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
|
||||
* EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
|
||||
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY 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.
|
||||
* ====================================================================
|
||||
*
|
||||
* This product includes cryptographic software written by Eric Young
|
||||
* (eay@cryptsoft.com). This product includes software written by Tim
|
||||
* Hudson (tjh@cryptsoft.com).
|
||||
*
|
||||
*/
|
||||
|
||||
Original SSLeay License
|
||||
-----------------------
|
||||
|
||||
/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
|
||||
* All rights reserved.
|
||||
*
|
||||
* This package is an SSL implementation written
|
||||
* by Eric Young (eay@cryptsoft.com).
|
||||
* The implementation was written so as to conform with Netscapes SSL.
|
||||
*
|
||||
* This library is free for commercial and non-commercial use as long as
|
||||
* the following conditions are aheared to. The following conditions
|
||||
* apply to all code found in this distribution, be it the RC4, RSA,
|
||||
* lhash, DES, etc., code; not just the SSL code. The SSL documentation
|
||||
* included with this distribution is covered by the same copyright terms
|
||||
* except that the holder is Tim Hudson (tjh@cryptsoft.com).
|
||||
*
|
||||
* Copyright remains Eric Young's, and as such any Copyright notices in
|
||||
* the code are not to be removed.
|
||||
* If this package is used in a product, Eric Young should be given attribution
|
||||
* as the author of the parts of the library used.
|
||||
* This can be in the form of a textual message at program startup or
|
||||
* in documentation (online or textual) provided with the package.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. All advertising materials mentioning features or use of this software
|
||||
* must display the following acknowledgement:
|
||||
* "This product includes cryptographic software written by
|
||||
* Eric Young (eay@cryptsoft.com)"
|
||||
* The word 'cryptographic' can be left out if the rouines from the library
|
||||
* being used are not cryptographic related :-).
|
||||
* 4. If you include any Windows specific code (or a derivative thereof) from
|
||||
* the apps directory (application code) you must include an acknowledgement:
|
||||
* "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY 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.
|
||||
*
|
||||
* The licence and distribution terms for any publically available version or
|
||||
* derivative of this code cannot be changed. i.e. this code cannot simply be
|
||||
* copied and put under another distribution licence
|
||||
* [including the GNU Public Licence.]
|
||||
*/
|
||||
|
||||
=====================================================================================================
|
||||
Several fantastic pieces of free and open-source software have really get GNS3 to where it is today.
|
||||
A few require that we include their license agreements within our software.
|
||||
=====================================================================================================
|
||||
|
||||
License notice for Qt
|
||||
---------------------
|
||||
http://doc.qt.io/qt-4.8/gpl.html
|
||||
|
||||
License notice for PyQt
|
||||
-----------------------
|
||||
http://www.gnu.org/licenses/gpl.html
|
||||
|
||||
License notice for jsonschema
|
||||
-----------------------------
|
||||
https://github.com/Julian/jsonschema/blob/master/COPYING
|
||||
|
||||
Copyright (c) 2013 Julian Berman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
License notice for aiohttp
|
||||
--------------------------
|
||||
https://github.com/KeepSafe/aiohttp/blob/master/LICENSE.txt
|
||||
|
||||
Copyright (c) 2013, 2014, 2015 Nikolay Kim and Andrew Svetlov
|
||||
|
||||
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 Jinja
|
||||
------------------------
|
||||
https://github.com/KeepSafe/aiohttp/blob/master/LICENSE.txt
|
||||
|
||||
Copyright (c) 2009 by the Jinja Team, see AUTHORS for more details.
|
||||
|
||||
Some rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* The names of the contributors may not be used to endorse or
|
||||
promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
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 raven
|
||||
------------------------
|
||||
https://github.com/getsentry/raven-python/blob/master/LICENSE
|
||||
|
||||
Copyright (c) 2015 Functional Software, Inc and individual contributors.
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* The names of the contributors may not be used to endorse or
|
||||
promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
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 pywin32
|
||||
--------------------------
|
||||
https://github.com/SublimeText/Pywin32/blob/master/License.txt
|
||||
|
||||
Unless stated in the specfic source file, this work is
|
||||
Copyright (c) 1996-2008, Greg Stein and Mark Hammond.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the distribution.
|
||||
|
||||
Neither names of Greg Stein, Mark Hammond nor the name of contributors may be used
|
||||
to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
|
||||
IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 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 Winpcap
|
||||
--------------------------
|
||||
https://www.winpcap.org/misc/copyright.htm
|
||||
|
||||
Copyright (c) 1999 - 2005 NetGroup, Politecnico di Torino (Italy).
|
||||
Copyright (c) 2005 - 2010 CACE Technologies, Davis (California).
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* The names of the contributors may not be used to endorse or
|
||||
promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
|
||||
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 2
|
||||
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, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
(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 cpulimit
|
||||
---------------------------
|
||||
https://github.com/opsengine/cpulimit/blob/master/LICENSE
|
||||
|
||||
Copyright (C) 2005-2012, by: Angelo Marletta <angelo dot marletta at gmail dot com>
|
||||
|
||||
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 2
|
||||
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, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
License notice for Cygwin
|
||||
-------------------------
|
||||
https://cygwin.com/licensing.html
|
||||
|
||||
License notice for SuperPutty
|
||||
-----------------------------
|
||||
https://github.com/jimradford/superputty/blob/master/License.txt
|
||||
|
||||
Copyright (c) 2009 Jim Radford http://www.jimradford.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
License notice for Putty
|
||||
------------------------
|
||||
http://www.chiark.greenend.org.uk/~sgtatham/putty/licence.html
|
||||
|
||||
PuTTY is copyright 1997-2015 Simon Tatham.
|
||||
|
||||
Portions copyright Robert de Bath, Joris van Rantwijk, Delian Delchev, Andreas Schultz, Jeroen Massar,
|
||||
Wez Furlong, Nicolas Barry, Justin Bradford, Ben Harris, Malcolm Smith, Ahmad Khalifa, Markus Kuhn,
|
||||
Colin Watson, Christopher Staite, and CORE SDI S.A.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
License notice for iouyap
|
||||
-------------------------
|
||||
https://github.com/GNS3/iouyap/blob/master/LICENSE
|
||||
|
||||
License notice for Dynamips
|
||||
---------------------------
|
||||
https://github.com/GNS3/dynamips/blob/master/COPYING
|
||||
|
||||
License notice for Qemu
|
||||
-----------------------
|
||||
http://wiki.qemu.org/License
|
||||
|
||||
License notice for VPCS
|
||||
-----------------------
|
||||
|
||||
Copyright (c) 2007-2013, Paul Meng (mirnshi@gmail.com)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY 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 Python
|
||||
-------------------------
|
||||
https://www.python.org/download/releases/3.4.2/license/
|
||||
|
||||
License notice for BusyBox
|
||||
---------------------------
|
||||
BusyBox is distributed under version 2 of the General Public License
|
||||
https://busybox.net/license.html
|
||||
|
||||
Source code is available here:
|
||||
https://github.com/GNS3/busybox
|
||||
|
||||
|
||||
Licence notice for zipstream
|
||||
-----------------------------
|
||||
zipstream is distributed under version 3 of the General Public License
|
||||
https://github.com/allanlei/python-zipstream/blob/master/LICENSE
|
||||
|
||||
Source code is available here:
|
||||
https://pypi.python.org/pypi/zipstream
|
||||
|
||||
|
||||
Licence notice for aiohttp_cors
|
||||
-------------------------------
|
||||
Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>.
|
||||
|
||||
Licensed under the Apache License, Version 2.0, see LICENSE file for details.
|
||||
|
||||
https://github.com/aio-libs/aiohttp_cors
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# Run tests inside a container
|
||||
FROM ubuntu:vivid
|
||||
|
||||
MAINTAINER GNS3 Team
|
||||
|
||||
#ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --force-yes python3.4 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3.4-dev xvfb
|
||||
RUN apt-get clean
|
||||
|
||||
|
||||
ADD dev-requirements.txt /dev-requirements.txt
|
||||
ADD requirements.txt /requirements.txt
|
||||
RUN pip3 install -r /dev-requirements.txt
|
||||
|
||||
|
||||
ADD . /src
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
CMD xvfb-run python3.4 -m pytest -vv
|
||||
@@ -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 *
|
||||
|
||||
113
README.rst
113
README.rst
@@ -10,100 +10,12 @@ 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.
|
||||
https://gns3.com/support/docs
|
||||
|
||||
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://community.gns3.com/community/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>`_.
|
||||
|
||||
Developement
|
||||
Development
|
||||
-------------
|
||||
|
||||
If you want to update the interface, modify the .ui files using QT tools. And:
|
||||
@@ -113,15 +25,18 @@ If you want to update the interface, modify the .ui files using QT tools. And:
|
||||
cd scripts
|
||||
python build_pyqt.py
|
||||
|
||||
Debug
|
||||
"""""
|
||||
|
||||
Test with PyQT4
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to simulate a user with PyQT4:
|
||||
If you want to see the full logs in the internal shell you can type:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
export GNS3_QT4=1
|
||||
python gns3/main.py
|
||||
|
||||
debug 2
|
||||
|
||||
|
||||
Or start the app with --debug flag.
|
||||
|
||||
Due to the fact PyQT intercept you can use a web debugger for inspecting stuff:
|
||||
https://github.com/Kozea/wdb
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -15,13 +15,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Base class for NIOs (Network Input/Output).
|
||||
"""
|
||||
from .main import main
|
||||
|
||||
|
||||
class NIO:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
pass
|
||||
main()
|
||||
60
gns3/application.py
Normal file
60
gns3/application.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
from .qt import QtWidgets, QtGui, QtCore
|
||||
from gns3.utils import parse_version
|
||||
from .version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Application(QtWidgets.QApplication):
|
||||
file_open_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, argv):
|
||||
|
||||
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")
|
||||
self.setApplicationName("GNS3")
|
||||
self.setApplicationVersion(__version__)
|
||||
|
||||
# File path if we have received the path to
|
||||
# a file on system via an OSX event
|
||||
self.open_file_at_startup = None
|
||||
|
||||
def event(self, event):
|
||||
# When you double click file you receive an event
|
||||
# and not the file as command line parameter
|
||||
if sys.platform.startswith("darwin"):
|
||||
if isinstance(event, QtGui.QFileOpenEvent):
|
||||
self.open_file_at_startup = str(event.file())
|
||||
self.file_open_signal.emit(str(event.file()))
|
||||
return super().event(event)
|
||||
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)
|
||||
119
gns3/compute.py
Normal file
119
gns3/compute.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 = None
|
||||
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
|
||||
176
gns3/compute_manager.py
Normal file
176
gns3/compute_manager.py
Normal file
@@ -0,0 +1,176 @@
|
||||
#!/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 copy
|
||||
import logging
|
||||
import datetime
|
||||
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 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 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)
|
||||
|
||||
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,19 +184,14 @@ 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):
|
||||
"""
|
||||
Activate or deactivate debugging messages
|
||||
debug [level] (0 or 1).
|
||||
debug [level] (0, 1 or 2).
|
||||
"""
|
||||
|
||||
if '?' in args or args.strip() == "":
|
||||
@@ -208,16 +199,22 @@ class ConsoleCmd(cmd.Cmd):
|
||||
return
|
||||
|
||||
root = logging.getLogger()
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
|
||||
if len(args) == 1:
|
||||
level = int(args[0])
|
||||
if level == 0:
|
||||
print("Deactivating debugging")
|
||||
root.removeHandler(ch)
|
||||
for handler in root.handlers:
|
||||
if isinstance(handler, logging.StreamHandler):
|
||||
root.removeHandler(handler)
|
||||
root.setLevel(logging.INFO)
|
||||
else:
|
||||
print("Activating debugging")
|
||||
root.addHandler(ch)
|
||||
root.addHandler(logging.StreamHandler(sys.stdout))
|
||||
if level == 1:
|
||||
print("Activating debugging")
|
||||
else:
|
||||
print("Activating full debugging")
|
||||
root.setLevel(logging.DEBUG)
|
||||
from .main_window import MainWindow
|
||||
MainWindow.instance().setSettings({"debug_level": level})
|
||||
else:
|
||||
@@ -272,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
|
||||
|
||||
@@ -303,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() == "":
|
||||
@@ -314,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,12 +21,36 @@ 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
|
||||
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("{}\n".format(message), error=True)
|
||||
elif level_no >= logging.WARNING:
|
||||
self._console_view.write("{}\n".format(message), warning=True)
|
||||
elif level_no >= logging.INFO:
|
||||
# To avoid noise on console we display all event only if log level is debug
|
||||
# or if we force the display in the log record
|
||||
if "show" in record.__dict__ or logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
||||
self._console_view.write("{}\n".format(message))
|
||||
elif level_no >= logging.DEBUG:
|
||||
self._console_view.write("{}\n".format(message))
|
||||
|
||||
|
||||
class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
@@ -40,8 +64,10 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
# Set introduction message
|
||||
bitness = struct.calcsize("P") * 8
|
||||
current_year = datetime.date.today().year
|
||||
self.intro = "GNS3 management console. Running GNS3 version {} on {} ({}-bit) with Qt {}.\n" \
|
||||
"Copyright (c) 2006-{} GNS3 Technologies.".format(__version__, platform.system(), bitness, QtCore.QT_VERSION_STR, current_year)
|
||||
self.intro = "GNS3 management console.\nRunning GNS3 version {} on {} ({}-bit) with Python {} Qt {} and PyQt {}.\n" \
|
||||
"Copyright (c) 2006-{} GNS3 Technologies.\n" \
|
||||
"Use Help -> GNS3 Doctor to detect common issues." \
|
||||
"".format(__version__, platform.system(), bitness, platform.python_version(), QtCore.QT_VERSION_STR, Qt.PYQT_VERSION_STR, current_year)
|
||||
|
||||
# Parent class initialization
|
||||
try:
|
||||
@@ -60,6 +86,11 @@ 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)
|
||||
@@ -68,6 +99,17 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
self.stdout = sys.stdout
|
||||
self._topology = Topology.instance()
|
||||
|
||||
|
||||
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
|
||||
@@ -138,15 +180,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())
|
||||
@@ -156,15 +198,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())
|
||||
@@ -174,26 +216,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):
|
||||
|
||||
169
gns3/controller.py
Normal file
169
gns3/controller.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/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()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__()
|
||||
self._connected = 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 isRemote(self):
|
||||
"""
|
||||
:returns Boolean: True if the controller is remote
|
||||
"""
|
||||
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
|
||||
return not settings["auto_start"]
|
||||
|
||||
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.get('/version', self._versionGetSlot)
|
||||
|
||||
def _versionGetSlot(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Called after the inital version get
|
||||
"""
|
||||
if error:
|
||||
if "message" in result and self._first_error:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Connection", result["message"])
|
||||
# Try to connect again in 1 seconds
|
||||
QtCore.QTimer.singleShot(1000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_error))
|
||||
self._first_error = False
|
||||
else:
|
||||
self._first_error = True
|
||||
|
||||
def _httpClientConnectedSlot(self):
|
||||
if not self._connected:
|
||||
self._connected = True
|
||||
self.connected_signal.emit()
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("GET", *args, **kwargs)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("POST", *args, **kwargs)
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("PUT", *args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
return self.createHTTPQuery("DELETE", *args, **kwargs)
|
||||
|
||||
def createHTTPQuery(self, method, path, *args, **kwargs):
|
||||
"""
|
||||
Forward the query to the HTTP client or controller depending of the path
|
||||
"""
|
||||
if self._http_client:
|
||||
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
|
||||
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
return self._http_client.getSynchronous(endpoint, timeout)
|
||||
|
||||
@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)
|
||||
@@ -16,26 +16,28 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import psutil
|
||||
import os
|
||||
import platform
|
||||
import struct
|
||||
|
||||
try:
|
||||
import raven
|
||||
from raven.transport.http import HTTPTransport
|
||||
RAVEN_AVAILABLE = True
|
||||
except ImportError:
|
||||
# raven is not installed with deb package in order to simplify packaging
|
||||
RAVEN_AVAILABLE = False
|
||||
|
||||
from .utils.get_resource import get_resource
|
||||
from .version import __version__
|
||||
from .version import __version__, __version_info__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Dev build
|
||||
if __version__[4] != 0:
|
||||
# Dev build
|
||||
if __version_info__[3] != 0:
|
||||
import faulthandler
|
||||
# Display a traceback in case of segfault crash. Usefull when frozen
|
||||
# Not enabled by default for security reason
|
||||
@@ -49,7 +51,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "sync+https://a86f12c42f7746288a81af9a9c1145a4:db8b6973bd2c448ea0a98675119ff8ee@app.getsentry.com/38506"
|
||||
DSN = "https://468f899afc3c46d99bad2eb474516d2c:415662f2e4c240829d5eaf8f09ca99b4@app.getsentry.com/38506"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = get_resource("cacert.pem")
|
||||
if cacert is not None and os.path.isfile(cacert):
|
||||
@@ -59,8 +61,6 @@ class CrashReport:
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
self._client = None
|
||||
|
||||
# We don't want sentry making noise if an error is catched when you don't have internet
|
||||
sentry_errors = logging.getLogger('sentry.errors')
|
||||
sentry_errors.disabled = True
|
||||
@@ -69,18 +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 self._client is None:
|
||||
self._client = raven.Client(CrashReport.DSN, release=__version__)
|
||||
if hasattr(exception, "fingerprint"):
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, fingerprint=['{{ default }}', exception.fingerprint], transport=HTTPTransport)
|
||||
else:
|
||||
client = raven.Client(CrashReport.DSN, release=__version__, transport=HTTPTransport)
|
||||
context = {
|
||||
"os:name": platform.system(),
|
||||
"os:release": platform.release(),
|
||||
@@ -94,21 +97,22 @@ class CrashReport:
|
||||
"python:encoding": sys.getdefaultencoding(),
|
||||
"python:frozen": "{}".format(hasattr(sys, "frozen"))
|
||||
}
|
||||
context = self._add_qt_informations(context)
|
||||
self._client.tags_context(context)
|
||||
context = self._add_qt_information(context)
|
||||
client.tags_context(context)
|
||||
try:
|
||||
report = self._client.captureException((exception, value, tb))
|
||||
report = client.captureException((exception, value, tb))
|
||||
except Exception as e:
|
||||
log.error("Can't send crash report to Sentry: {}".format(e))
|
||||
return
|
||||
log.info("Crash report sent with event ID: {}".format(self._client.get_ident(report)))
|
||||
log.info("Crash report sent with event ID: {}".format(client.get_ident(report)))
|
||||
|
||||
def _add_qt_informations(self, context):
|
||||
def _add_qt_information(self, context):
|
||||
try:
|
||||
from .qt import QtCore
|
||||
import sip
|
||||
except ImportError:
|
||||
return context
|
||||
context["psutil:version"] = psutil.__version__
|
||||
context["pyqt:version"] = QtCore.PYQT_VERSION_STR
|
||||
context["qt:version"] = QtCore.QT_VERSION_STR
|
||||
context["sip:version"] = sip.SIP_VERSION_STR
|
||||
|
||||
503
gns3/dialogs/appliance_wizard.py
Normal file
503
gns3/dialogs/appliance_wizard.py
Normal file
@@ -0,0 +1,503 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ..qt import QtWidgets, QtCore, QtGui, qpartial
|
||||
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
|
||||
from ..image_manager import ImageManager
|
||||
from ..modules import Qemu
|
||||
from ..registry.appliance import Appliance
|
||||
from ..registry.registry import Registry
|
||||
from ..registry.config import Config, ConfigException
|
||||
from ..registry.image import Image
|
||||
from ..utils import human_filesize
|
||||
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..compute_manager import ComputeManager
|
||||
from ..local_config import LocalConfig
|
||||
|
||||
|
||||
class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
def __init__(self, parent, path):
|
||||
super().__init__(parent)
|
||||
|
||||
self._path = path
|
||||
self.setupUi(self)
|
||||
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._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.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)
|
||||
self.uiServerWizardPage.isComplete = self._uiServerWizardPage_isComplete
|
||||
|
||||
def initializePage(self, page_id):
|
||||
"""
|
||||
Initialize Wizard pages.
|
||||
|
||||
:param page_id: page identifier
|
||||
"""
|
||||
super().initializePage(page_id)
|
||||
|
||||
if self._appliance["category"] == "guest":
|
||||
symbol = ":/symbols/computer.svg"
|
||||
else:
|
||||
symbol = ":/symbols/{}.svg".format(self._appliance["category"])
|
||||
self.page(page_id).setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(symbol))
|
||||
|
||||
if "qemu" in self._appliance:
|
||||
type = "qemu"
|
||||
elif "iou" in self._appliance:
|
||||
type = "iou"
|
||||
elif "docker" in self._appliance:
|
||||
type = "docker"
|
||||
elif "dynamips" in self._appliance:
|
||||
type = "dynamips"
|
||||
|
||||
if self.page(page_id) == self.uiInfoWizardPage:
|
||||
self.uiInfoWizardPage.setTitle(self._appliance["product_name"])
|
||||
self.uiDescriptionLabel.setText(self._appliance["description"])
|
||||
|
||||
info = (
|
||||
("Category", "category"),
|
||||
("Product", "product_name"),
|
||||
("Vendor", "vendor_name"),
|
||||
("Status", "status"),
|
||||
("Maintainer", "maintainer"),
|
||||
("Architecture", "qemu/arch"),
|
||||
("KVM", "qemu/kvm")
|
||||
)
|
||||
|
||||
self.uiInfoTreeWidget.clear()
|
||||
for (name, key) in info:
|
||||
if "/" in key:
|
||||
key, subkey = key.split("/")
|
||||
value = self._appliance.get(key, {}).get(subkey, None)
|
||||
else:
|
||||
value = self._appliance.get(key, None)
|
||||
if value is None:
|
||||
continue
|
||||
item = QtWidgets.QTreeWidgetItem([name + ":", value])
|
||||
font = item.font(0)
|
||||
font.setBold(True)
|
||||
item.setFont(0, font)
|
||||
self.uiInfoTreeWidget.addTopLevelItem(item)
|
||||
|
||||
elif self.page(page_id) == self.uiServerWizardPage:
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
for compute in ComputeManager.instance().remoteComputes():
|
||||
self.uiRemoteServersComboBox.addItem(compute.name(), compute)
|
||||
|
||||
if not ComputeManager.instance().vmCompute():
|
||||
self.uiVMRadioButton.setEnabled(False)
|
||||
|
||||
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")):
|
||||
if type == "qemu":
|
||||
# Qemu has issues on OSX and Windows we disallow usage of the local server
|
||||
if not LocalConfig.instance().experimental():
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
elif type != "dynamips":
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
|
||||
if ComputeManager.instance().vmCompute():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
elif ComputeManager.instance().localCompute():
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
elif len(ComputeManager.instance().remoteComputes()) > 0:
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiRemoteRadioButton.setChecked(False)
|
||||
|
||||
elif self.page(page_id) == self.uiFilesWizardPage:
|
||||
self._refreshVersions()
|
||||
|
||||
elif self.page(page_id) == self.uiQemuWizardPage:
|
||||
Qemu.instance().getQemuBinariesFromServer(self._server, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
|
||||
|
||||
elif self.page(page_id) == self.uiSummaryWizardPage:
|
||||
self.uiSummaryTreeWidget.clear()
|
||||
|
||||
for key in self._appliance[type]:
|
||||
item = QtWidgets.QTreeWidgetItem([key.replace('_', ' ').capitalize() + ":", str(self._appliance[type][key])])
|
||||
font = item.font(0)
|
||||
font.setBold(True)
|
||||
item.setFont(0, font)
|
||||
self.uiSummaryTreeWidget.addTopLevelItem(item)
|
||||
self.uiSummaryTreeWidget.resizeColumnToContents(0)
|
||||
|
||||
elif self.page(page_id) == self.uiUsageWizardPage:
|
||||
self.uiUsageTextEdit.setText("The appliance is available in the {} category. \n\n{}".format(
|
||||
self._appliance["category"].replace("_", " "),
|
||||
self._appliance.get("usage", ""))
|
||||
)
|
||||
|
||||
elif self.page(page_id) == self.uiCheckServerWizardPage:
|
||||
self.uiCheckServerLabel.setText("Please wait while checking server capacities...")
|
||||
if 'qemu' in self._appliance:
|
||||
if self._appliance['qemu'].get('kvm', 'require') == 'require':
|
||||
self._server_check = False # If the server as the capacities for running the appliance
|
||||
Qemu.instance().getQemuCapabilitiesFromServer(self._server, qpartial(self._qemuServerCapabilitiesCallback))
|
||||
return
|
||||
self.uiCheckServerLabel.setText("")
|
||||
self._server_check = True
|
||||
self.next()
|
||||
|
||||
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
|
||||
"""
|
||||
Check if server support KVM or not
|
||||
"""
|
||||
if error is None and "kvm" in result and self._appliance["qemu"]["arch"] in result["kvm"]:
|
||||
self._server_check = True
|
||||
self.uiCheckServerLabel.setText("GNS3 server requirements is OK you can continue the installation")
|
||||
else:
|
||||
if error:
|
||||
msg = result["message"]
|
||||
else:
|
||||
msg = "The remote server doesn't support KVM. You need a Linux server or the GNS3 VM with VMware and CPU virtualization instructions."
|
||||
self.uiCheckServerLabel.setText(msg)
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu", msg)
|
||||
self._server_check = False
|
||||
|
||||
def _uiServerWizardPage_isComplete(self):
|
||||
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
|
||||
|
||||
def _refreshVersions(self):
|
||||
"""
|
||||
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 files...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if progress_dialog.exec_():
|
||||
for version in self._appliance["versions"]:
|
||||
top = QtWidgets.QTreeWidgetItem(["{} {}".format(self._appliance["product_name"], version["name"])])
|
||||
|
||||
size = 0
|
||||
status = "Ready to install"
|
||||
for image in version["images"].values():
|
||||
if image["status"] == "Missing":
|
||||
status = "Missing files"
|
||||
|
||||
size += image.get("filesize", 0)
|
||||
image_widget = QtWidgets.QTreeWidgetItem(
|
||||
[
|
||||
"",
|
||||
image["filename"],
|
||||
human_filesize(image.get("filesize", 0)),
|
||||
image["status"],
|
||||
image["version"],
|
||||
image.get("md5sum", "")
|
||||
])
|
||||
|
||||
if image["status"] == "Missing":
|
||||
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
|
||||
else:
|
||||
image_widget.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
|
||||
|
||||
# Associated data stored are col 0: version, col 1: image
|
||||
image_widget.setData(0, QtCore.Qt.UserRole, version)
|
||||
image_widget.setData(1, QtCore.Qt.UserRole, image)
|
||||
image_widget.setData(2, QtCore.Qt.UserRole, self._appliance)
|
||||
top.addChild(image_widget)
|
||||
|
||||
font = top.font(0)
|
||||
font.setBold(True)
|
||||
top.setFont(0, font)
|
||||
|
||||
expand = True
|
||||
if status == "Missing files":
|
||||
top.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
|
||||
else:
|
||||
expand = False
|
||||
top.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
|
||||
|
||||
top.setData(2, QtCore.Qt.DisplayRole, human_filesize(size))
|
||||
top.setData(3, QtCore.Qt.DisplayRole, status)
|
||||
top.setData(2, QtCore.Qt.UserRole, self._appliance)
|
||||
top.setData(0, QtCore.Qt.UserRole, version)
|
||||
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
|
||||
if expand:
|
||||
top.setExpanded(True)
|
||||
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(0)
|
||||
self.uiApplianceVersionTreeWidget.resizeColumnToContents(1)
|
||||
self.uiApplianceVersionTreeWidget.setCurrentItem(self.uiApplianceVersionTreeWidget.topLevelItem(0))
|
||||
|
||||
def _resfreshDialogWorker(self):
|
||||
"""
|
||||
Scan local directory in order to found the images on disk
|
||||
"""
|
||||
|
||||
# Docker do not have versions
|
||||
if not "versions" in self._appliance:
|
||||
return
|
||||
|
||||
for version in self._appliance["versions"]:
|
||||
for image in version["images"].values():
|
||||
img = self._registry.search_image_file(image["filename"], image.get("md5sum"), image.get("filesize"))
|
||||
if img:
|
||||
image["status"] = "Found"
|
||||
image["md5sum"] = img.md5sum
|
||||
image["filesize"] = img.filesize
|
||||
else:
|
||||
image["status"] = "Missing"
|
||||
|
||||
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:
|
||||
return
|
||||
|
||||
image = current.data(1, QtCore.Qt.UserRole)
|
||||
if image is not None:
|
||||
if "direct_download_url" in image or "download_url" in image:
|
||||
self.uiDownloadPushButton.show()
|
||||
self.uiImportPushButton.show()
|
||||
|
||||
def _downloadPushButtonClickedSlot(self):
|
||||
"""
|
||||
Called when user want to download an appliance images.
|
||||
He should have selected the file before.
|
||||
"""
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
|
||||
if current is None:
|
||||
return
|
||||
|
||||
data = current.data(1, QtCore.Qt.UserRole)
|
||||
if data is not None:
|
||||
if "direct_download_url" in data:
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["direct_download_url"]))
|
||||
if "compression" in data:
|
||||
QtWidgets.QMessageBox.warning(self, "Add appliance", "The file is compressed with {} you need to uncompress it before using it.".format(data["compression"]))
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, "Add appliance", "Download will redirect you where the required file can be downloaded, you may have to be registered with the vendor in order to download the file.")
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["download_url"]))
|
||||
|
||||
def _createVersionPushButtonClickedSlot(self):
|
||||
"""
|
||||
Allow user to create a new version of an appliance
|
||||
"""
|
||||
|
||||
new_version, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Creating a new version allows to import unknown files to use with this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.Normal)
|
||||
if ok:
|
||||
self._appliance.create_new_version(new_version)
|
||||
self._refreshVersions()
|
||||
|
||||
def _importPushButtonClickedSlot(self):
|
||||
"""
|
||||
Called when user want to import an appliance images.
|
||||
He should have selected the file before.
|
||||
"""
|
||||
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
disk = current.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName()
|
||||
if len(path) == 0:
|
||||
return
|
||||
|
||||
image = Image(path)
|
||||
if "md5sum" in disk and image.md5sum != disk["md5sum"]:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "This is not the correct file. The MD5 sum is {} and should be {}. For OVA you need to import the OVA/OVF not the file inside the archive.".format(image.md5sum, disk["md5sum"]))
|
||||
return
|
||||
|
||||
config = Config()
|
||||
worker = WaitForLambdaWorker(lambda: image.copy(os.path.join(config.images_dir, self._appliance.image_dir_name()), disk["filename"]), allowed_exceptions=[OSError, ValueError])
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Importing the appliance...", None, busy=True, parent=self)
|
||||
if not progress_dialog.exec_():
|
||||
return
|
||||
self._refreshVersions()
|
||||
|
||||
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for getQemuBinariesFromServer.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu binaries", "{}".format(result["message"]))
|
||||
else:
|
||||
self.uiQemuListComboBox.clear()
|
||||
for qemu in result:
|
||||
if qemu["version"]:
|
||||
self.uiQemuListComboBox.addItem("{path} (v{version})".format(path=qemu["path"], version=qemu["version"]), qemu["path"])
|
||||
else:
|
||||
self.uiQemuListComboBox.addItem("{path}".format(path=qemu["path"]), qemu["path"])
|
||||
if self.uiQemuListComboBox.count() == 1:
|
||||
self.next()
|
||||
|
||||
def _install(self, version):
|
||||
"""
|
||||
Install the appliance to GNS3
|
||||
|
||||
:params version: Version name
|
||||
"""
|
||||
|
||||
try:
|
||||
config = Config()
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
||||
return False
|
||||
|
||||
if version is None:
|
||||
appliance_configuration = self._appliance.copy()
|
||||
else:
|
||||
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"]))
|
||||
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add appliance", "New name:", QtWidgets.QLineEdit.Normal, appliance_configuration["name"])
|
||||
appliance_configuration["name"] = appliance_configuration["name"].strip()
|
||||
|
||||
if "qemu" in appliance_configuration:
|
||||
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
|
||||
|
||||
worker = WaitForLambdaWorker(lambda: config.add_appliance(appliance_configuration, self._server), allowed_exceptions=[ConfigException, OSError])
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if not progress_dialog.exec_():
|
||||
return False
|
||||
|
||||
worker = WaitForLambdaWorker(lambda: config.save(), allowed_exceptions=[ConfigException, OSError])
|
||||
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
|
||||
progress_dialog.show()
|
||||
if progress_dialog.exec_():
|
||||
QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
|
||||
return True
|
||||
|
||||
def nextId(self):
|
||||
if self.currentPage() == self.uiServerWizardPage:
|
||||
if "docker" in self._appliance:
|
||||
return super().nextId() + 3
|
||||
elif "qemu" not in self._appliance:
|
||||
return super().nextId() + 1
|
||||
elif self.currentPage() == self.uiFilesWizardPage:
|
||||
if "qemu" not in self._appliance:
|
||||
return super().nextId() + 1
|
||||
return super().nextId()
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the settings.
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiFilesWizardPage:
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
appliance = current.data(2, QtCore.Qt.UserRole)
|
||||
if not self._appliance.is_version_installable(version["name"]):
|
||||
QtWidgets.QMessageBox.warning(self, "Appliance", "Sorry, you cannot install {} with missing files".format(appliance["name"]))
|
||||
return False
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Would you like to install {} version {}?".format(appliance["name"], version["name"]),
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
|
||||
elif self.currentPage() == self.uiUsageWizardPage:
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
if current:
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
return self._install(version["name"])
|
||||
else:
|
||||
return self._install(None)
|
||||
|
||||
elif self.currentPage() == self.uiServerWizardPage:
|
||||
if self.uiRemoteRadioButton.isChecked():
|
||||
if len(ComputeManager.instance().remoteComputes()) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote server registered in your preferences")
|
||||
return False
|
||||
self._server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
|
||||
self._server = "vm"
|
||||
else:
|
||||
if (sys.platform.startswith("darwin") or sys.platform.startswith("win")):
|
||||
if "qemu" in self._appliance:
|
||||
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and MacOSX is not supported by the GNS3 team. Are you sur to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return False
|
||||
|
||||
self._server = "local"
|
||||
|
||||
elif self.currentPage() == self.uiQemuWizardPage:
|
||||
if self.uiQemuListComboBox.currentIndex() == -1:
|
||||
QtWidgets.QMessageBox.critical(self, "Qemu binary", "No compatible Qemu binary selected")
|
||||
return False
|
||||
|
||||
elif self.currentPage() == self.uiCheckServerWizardPage:
|
||||
return self._server_check
|
||||
|
||||
return True
|
||||
|
||||
def _vmToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the VM radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the remote server radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(True)
|
||||
self.uiRemoteServersGroupBox.show()
|
||||
|
||||
def _localToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the local server radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
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)
|
||||
|
||||
224
gns3/dialogs/doctor_dialog.py
Normal file
224
gns3/dialogs/doctor_dialog.py
Normal file
@@ -0,0 +1,224 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import psutil
|
||||
import platform
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import struct
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.doctor_dialog_ui import Ui_DoctorDialog
|
||||
from gns3.local_server import LocalServer
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3 import version
|
||||
from gns3.modules.vmware import VMware
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
"""
|
||||
This dialog allow user to detect error in his GNS3 installation.
|
||||
|
||||
If you want to add a test add a method starting by check. The
|
||||
check return a tuple result and a message in case of failure.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, console=False):
|
||||
|
||||
super().__init__(parent)
|
||||
self._console = console
|
||||
self.setupUi(self)
|
||||
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
|
||||
for method in sorted(dir(self)):
|
||||
if method.startswith('check'):
|
||||
try:
|
||||
self.write(getattr(self, method).__doc__ + "...")
|
||||
(res, msg) = getattr(self, method)()
|
||||
if res == 0:
|
||||
self.write('<span style="color: green"><strong>OK</strong></span>')
|
||||
elif res == 1:
|
||||
self.write('<span style="color: orange"><strong>WARNING</strong> {}</span>'.format(msg))
|
||||
elif res == 2:
|
||||
self.write('<span style="color: red"><strong>ERROR</strong> {}</span>'.format(msg))
|
||||
except Exception as e:
|
||||
log.error("GNS3 doctor exception detected: {}".format(e), exc_info=1)
|
||||
self.write('<span style="color: red"><strong>FAIL</strong> The doctor failed during this test with error: {} Please check on the forum.</span>'.format(str(e)))
|
||||
self.write("<br/>")
|
||||
|
||||
def write(self, text):
|
||||
"""
|
||||
Add text to the text windows
|
||||
"""
|
||||
if self._console:
|
||||
print(text)
|
||||
self.uiDoctorResultTextEdit.setHtml(self.uiDoctorResultTextEdit.toHtml() + text)
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
self.accept()
|
||||
|
||||
def checkLocalServerEnabled(self):
|
||||
"""Checking if the local server is enabled"""
|
||||
if LocalServer.instance().shouldLocalServerAutoStart() is False:
|
||||
return (2, "The local server is disabled. Go to Preferences -> Server -> Local Server and enable the local server.")
|
||||
return (0, None)
|
||||
|
||||
def checkDevVersionOfGNS3(self):
|
||||
"""Checking for stable GNS3 version"""
|
||||
if version.__version_info__[3] != 0:
|
||||
return (1, "You are using a unstable version of GNS3.")
|
||||
return (0, None)
|
||||
|
||||
def checkExperimentalFeaturesEnabled(self):
|
||||
"""Checking if experimental features are not enabled"""
|
||||
if LocalConfig.instance().experimental():
|
||||
return (1, "Experimental features are enabled. Turn them off by going to Preferences -> General -> Miscellaneous.")
|
||||
return (0, None)
|
||||
|
||||
def checkAVGInstalled(self):
|
||||
"""Checking if AVG software is not installed"""
|
||||
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
psinfo = proc.as_dict(["exe"])
|
||||
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
|
||||
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
return (0, None)
|
||||
|
||||
def checkFreeRam(self):
|
||||
"""Checking for amount of free virtual memory"""
|
||||
|
||||
if int(psutil.virtual_memory().available / (1024 * 1024)) < 600:
|
||||
return (2, "You have less than 600MB of available virtual memory, this could prevent nodes to start")
|
||||
return (0, None)
|
||||
|
||||
def checkVmrun(self):
|
||||
"""Checking if vmrun is installed"""
|
||||
vmrun = VMware.instance().findVmrun()
|
||||
if len(vmrun) == 0:
|
||||
return (1, "The vmrun executable could not be found, VMware VMs cannot be used")
|
||||
return (0, None)
|
||||
|
||||
def check64Bit(self):
|
||||
"""Check if processor is 64 bit"""
|
||||
if platform.architecture()[0] != "64bit":
|
||||
return (2, "The architecture {} is not supported.".format(platform.architecture()[0]))
|
||||
return (0, None)
|
||||
|
||||
def checkUbridgePermission(self):
|
||||
"""Check if ubridge has the correct permission"""
|
||||
if not sys.platform.startswith("win") and os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return (0, None)
|
||||
|
||||
path = LocalServer.instance().localServerSettings().get("ubridge_path")
|
||||
if path is None:
|
||||
return (0, None)
|
||||
if not os.path.exists(path):
|
||||
return (2, "Ubridge path {path} doesn't exists".format(path=path))
|
||||
|
||||
request_setuid = False
|
||||
if sys.platform.startswith("linux"):
|
||||
if "security.capability" in os.listxattr(path):
|
||||
caps = os.getxattr(path, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
return(2, "Ubridge require CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
|
||||
else:
|
||||
# capabilities not supported
|
||||
request_setuid = True
|
||||
|
||||
if sys.platform.startswith("darwin") or request_setuid:
|
||||
if os.stat(path).st_uid != 0 or not os.stat(path).st_mode & stat.S_ISUID:
|
||||
return (2, "Ubridge should be setuid. Run sudo chown root {path} and sudo chmod 4755 {path}".format(path=path))
|
||||
return (0, None)
|
||||
|
||||
def checkDynamipsPermission(self):
|
||||
"""Check if dynamips has the correct permission"""
|
||||
if not sys.platform.startswith("win") and os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return (0, None)
|
||||
|
||||
path = LocalServer.instance().localServerSettings().get("dynamips_path")
|
||||
if path is None:
|
||||
return (0, None)
|
||||
if not os.path.exists(path):
|
||||
return (2, "Dynamips path {path} doesn't exists".format(path=path))
|
||||
|
||||
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 (0, None)
|
||||
|
||||
def checkGNS3InstalledTwice(self):
|
||||
"""Check if gns3 is not installed twice"""
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
return (0, None)
|
||||
|
||||
try:
|
||||
if os.path.exists("/usr/local/bin/gns3server") and os.path.exists("/usr/bin/gns3server"):
|
||||
return (2, "GNS3 is installed twice please remove it from /usr/local/bin")
|
||||
except OSError:
|
||||
pass
|
||||
return (0, None)
|
||||
|
||||
def _checkWindowsService(self, service_name):
|
||||
|
||||
import pywintypes
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
try:
|
||||
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
|
||||
return False
|
||||
except pywintypes.error as e:
|
||||
if e.winerror == 1060:
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
return True
|
||||
|
||||
def checkRPFServiceIsRunning(self):
|
||||
"""Check if the RPF service is running (required to use Ethernet NIOs)"""
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
return (0, None)
|
||||
|
||||
import pywintypes
|
||||
try:
|
||||
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
|
||||
return (2, "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot")
|
||||
except pywintypes.error as e:
|
||||
return (2, "Could not check if the NPF or Npcap service is running: {}".format(e.strerror))
|
||||
|
||||
return (0, None)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = DoctorDialog(main, console=True)
|
||||
#dialog.show()
|
||||
#exit_code = app.exec_()
|
||||
121
gns3/dialogs/edit_compute_dialog.py
Normal file
121
gns3/dialogs/edit_compute_dialog.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.compute import Compute
|
||||
from gns3.ui.edit_compute_dialog_ui import Ui_EditComputeDialog
|
||||
|
||||
|
||||
class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
|
||||
"""
|
||||
New compute dialog.
|
||||
|
||||
:param parent: parent widget.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, compute=None):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiEnableAuthenticationCheckBox.toggled.connect(self._enableAuthenticationSlot)
|
||||
self._compute = compute
|
||||
if self._compute:
|
||||
self.uiServerNameLineEdit.setText(self._compute.name())
|
||||
self.uiServerHostLineEdit.setText(self._compute.host())
|
||||
self.uiServerPortSpinBox.setValue(self._compute.port())
|
||||
|
||||
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)
|
||||
123
gns3/dialogs/export_debug_dialog.py
Normal file
123
gns3/dialogs/export_debug_dialog.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from zipfile import ZipFile
|
||||
import platform
|
||||
import psutil
|
||||
import os
|
||||
|
||||
from gns3.version import __version__
|
||||
from gns3.qt import QtWidgets, QtCore
|
||||
from gns3.ui.export_debug_dialog_ui import Ui_ExportDebugDialog
|
||||
from gns3.local_config import LocalConfig
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
"""
|
||||
This dialog allow user to export useful information
|
||||
for remote debugging by a GNS3 developers.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, project):
|
||||
|
||||
super().__init__(parent)
|
||||
self._project = project
|
||||
self.setupUi(self)
|
||||
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export debug file", None, "Zip file (*.zip)", "Zip file (*.zip)")
|
||||
|
||||
if len(path) == 0:
|
||||
self.reject()
|
||||
return
|
||||
|
||||
log.info("Export debug information to %s", path)
|
||||
|
||||
try:
|
||||
with ZipFile(path, 'w') as zip:
|
||||
zip.writestr("debug.txt", self._getDebugData())
|
||||
dir = LocalConfig.instance().configDirectory()
|
||||
for filename in os.listdir(dir):
|
||||
path = os.path.join(dir, filename)
|
||||
if os.path.isfile(path):
|
||||
zip.write(path, filename)
|
||||
|
||||
dir = self._project.filesDir()
|
||||
if dir:
|
||||
for filename in os.listdir(dir):
|
||||
path = os.path.join(dir, filename)
|
||||
if os.path.isfile(path):
|
||||
zip.write(path, filename)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Debug", "Can't export debug information: {}".format(str(e)))
|
||||
self.accept()
|
||||
|
||||
def _getDebugData(self):
|
||||
try:
|
||||
connections = psutil.net_connections()
|
||||
# You need to be root for OSX
|
||||
except psutil.AccessDenied:
|
||||
connections = None
|
||||
|
||||
try:
|
||||
addrs = ["* {}: {}".format(key, val) for key, val in psutil.net_if_addrs().items()]
|
||||
except UnicodeDecodeError:
|
||||
addrs = ["INVALID ADDR WITH UNICODE CHARACTERS"]
|
||||
|
||||
data = """Version: {version}
|
||||
OS: {os}
|
||||
Python: {python}
|
||||
Qt: {qt}
|
||||
PyQt: {pyqt}
|
||||
CPU: {cpu}
|
||||
Memory: {memory}
|
||||
|
||||
Networks:
|
||||
{addrs}
|
||||
|
||||
Open connections:
|
||||
{connections}
|
||||
|
||||
Processus:
|
||||
""".format(
|
||||
version=__version__,
|
||||
qt=QtCore.QT_VERSION_STR,
|
||||
pyqt=QtCore.PYQT_VERSION_STR,
|
||||
os=platform.platform(),
|
||||
python=platform.python_version(),
|
||||
memory=psutil.virtual_memory(),
|
||||
cpu=psutil.cpu_times(),
|
||||
connections=connections,
|
||||
addrs="\n".join(addrs)
|
||||
)
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
psinfo = proc.as_dict(attrs=["name", "exe"])
|
||||
data += "* {} {}\n".format(psinfo["name"], psinfo["exe"])
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
return data
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
print(ExportDebugDialog(None)._getDebugData())
|
||||
72
gns3/dialogs/file_editor_dialog.py
Normal file
72
gns3/dialogs/file_editor_dialog.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.ui.file_editor_dialog_ui import Ui_FileEditorDialog
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
|
||||
"""
|
||||
This dialog allow user to detect error in his GNS3 installation.
|
||||
|
||||
If you want to add a test add a method starting by check. The
|
||||
check return a tuple result and a message in case of failure.
|
||||
"""
|
||||
|
||||
def __init__(self, target, path, parent=None, default=""):
|
||||
|
||||
if parent is None:
|
||||
from gns3.main_window import MainWindow
|
||||
parent = MainWindow.instance()
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._target = target
|
||||
self._path = path
|
||||
self._default = default
|
||||
|
||||
self.setWindowTitle(target.name() + " " + os.path.basename(path))
|
||||
|
||||
self.uiRefreshButton.pressed.connect(self._refreshSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Save).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
||||
|
||||
self._refreshSlot()
|
||||
|
||||
def _okButtonClickedSlot(self):
|
||||
text = self.uiFileTextEdit.toPlainText()
|
||||
self._target.post("/files/" + self._path, self._saveCallback, body=text)
|
||||
|
||||
def _saveCallback(self, result, error=False, **kwargs):
|
||||
if not error:
|
||||
self.accept()
|
||||
|
||||
def _refreshSlot(self):
|
||||
self._target.get("/files/" + self._path, self._getCallback)
|
||||
|
||||
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
|
||||
if not error:
|
||||
self.uiFileTextEdit.setText(raw_body.decode("utf-8"))
|
||||
elif result["status"] == 404:
|
||||
if self._default:
|
||||
self.uiFileTextEdit.setText(self._default)
|
||||
@@ -1,82 +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
|
||||
import sys
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWebKitWidgets, QtWidgets
|
||||
from ..ui.getting_started_dialog_ui import Ui_GettingStartedDialog
|
||||
from ..utils.get_resource import get_resource
|
||||
from ..local_config import LocalConfig
|
||||
|
||||
|
||||
class GettingStartedDialog(QtWidgets.QDialog, Ui_GettingStartedDialog):
|
||||
|
||||
"""
|
||||
GettingStarted dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiWebView.page().mainFrame().setScrollBarPolicy(QtCore.Qt.Horizontal, QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.uiWebView.page().mainFrame().setScrollBarPolicy(QtCore.Qt.Vertical, QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.adjustSize()
|
||||
self.uiWebView.page().setLinkDelegationPolicy(QtWebKitWidgets.QWebPage.DelegateAllLinks)
|
||||
self.uiWebView.linkClicked.connect(self._urlClickedSlot)
|
||||
self._local_config = LocalConfig.instance()
|
||||
settings = parent.settings()
|
||||
self.uiCheckBox.setChecked(settings["hide_getting_started_dialog"])
|
||||
getting_started = get_resource(os.path.join("static", "getting_started.html"))
|
||||
if getting_started and not (sys.platform.startswith("win") and not sys.maxsize > 2 ** 32):
|
||||
# do not show the page on Windows 32-bit (crash when no Internet connection)
|
||||
self.uiWebView.load(QtCore.QUrl.fromLocalFile(getting_started))
|
||||
else:
|
||||
self.uiCheckBox.setChecked(True)
|
||||
self.accept()
|
||||
|
||||
def showit(self):
|
||||
"""
|
||||
Either this dialog should be automatically showed at startup.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return not self.uiCheckBox.isChecked()
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
This dialog is closed.
|
||||
|
||||
:param result: ignored
|
||||
"""
|
||||
|
||||
settings = self.parentWidget().settings()
|
||||
settings["hide_getting_started_dialog"] = self.uiCheckBox.isChecked()
|
||||
self.parentWidget().setSettings(settings)
|
||||
super().done(result)
|
||||
|
||||
def _urlClickedSlot(self, url):
|
||||
"""
|
||||
Opens a clicked URL using user's default browser.
|
||||
|
||||
:param url: URL to open
|
||||
"""
|
||||
|
||||
if QtGui.QDesktopServices.openUrl(url) is False:
|
||||
QtWidgets.QMessageBox.critical(self, "Getting started", "Failed to open the URL: {}".format(url))
|
||||
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)
|
||||
@@ -19,9 +19,6 @@
|
||||
Dialog to configure and update node settings using widget pages.
|
||||
"""
|
||||
|
||||
from gns3.http_client import HTTPClient
|
||||
from gns3.progress import Progress
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from ..ui.node_properties_dialog_ui import Ui_NodePropertiesDialog
|
||||
|
||||
@@ -67,6 +64,10 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
|
||||
for node_item in self._node_items:
|
||||
if not node_item.node().initialized():
|
||||
continue
|
||||
|
||||
# If something of one of the displayed nodes we reload everything
|
||||
node_item.node().updated_signal.connect(self.resetSettings)
|
||||
|
||||
group_name = " {} group".format(str(node_item.node()))
|
||||
parent = group_name
|
||||
if parent not in self._parent_items:
|
||||
@@ -92,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):
|
||||
"""
|
||||
@@ -172,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
|
||||
|
||||
|
||||
@@ -38,8 +39,22 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
def __init__(self, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
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
|
||||
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)
|
||||
self._applyButton.clicked.connect(self._applyPreferences)
|
||||
@@ -51,8 +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):
|
||||
"""
|
||||
@@ -63,6 +81,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
pages = [
|
||||
GeneralPreferencesPage,
|
||||
ServerPreferencesPage,
|
||||
GNS3VMPreferencesPage,
|
||||
PacketCapturePreferencesPage,
|
||||
]
|
||||
|
||||
@@ -105,10 +124,10 @@ 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: "toggled"
|
||||
QtWidgets.QAbstractButton: "pressed"
|
||||
}
|
||||
|
||||
for widget, signal in widget_to_watch.items():
|
||||
@@ -117,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):
|
||||
"""
|
||||
@@ -142,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)
|
||||
|
||||
@@ -152,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):
|
||||
@@ -168,17 +197,18 @@ 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 wihout saving?",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
"Preferences",
|
||||
"You have unsaved preferences in {}.\n\nContinue without saving?".format(pages_title),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
QtWidgets.QDialog.reject(self)
|
||||
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
Saves the preferences and closes this dialog.
|
||||
|
||||
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_()
|
||||
259
gns3/dialogs/project_dialog.py
Normal file
259
gns3/dialogs/project_dialog.py
Normal file
@@ -0,0 +1,259 @@
|
||||
# -*- 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.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):
|
||||
current = self.uiProjectsTreeWidget.currentItem()
|
||||
if current is None:
|
||||
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 _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:
|
||||
if "message" in result:
|
||||
log.error("Error while overwrite project: {}".format(result["message"]))
|
||||
return
|
||||
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,17 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import psutil
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets, QtGui, QtNetwork
|
||||
from gns3.controller import Controller
|
||||
from gns3.local_server import LocalServer
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
|
||||
from gns3.dialogs.new_appliance_dialog import NewApplianceDialog
|
||||
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
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__
|
||||
|
||||
|
||||
class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
@@ -40,15 +41,26 @@ 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.uiGNS3VMDownloadLinkUrlLabel.setText('')
|
||||
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
|
||||
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
|
||||
self.uiVirtualBoxRadioButton.clicked.connect(self._listVirtualBoxVMsSlot)
|
||||
self.uiVMwareBannerButton.clicked.connect(self._VMwareBannerButtonClickedSlot)
|
||||
settings = parent.settings()
|
||||
self.uiShowCheckBox.setChecked(settings["hide_setup_wizard"])
|
||||
|
||||
@@ -58,16 +70,62 @@ 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)
|
||||
|
||||
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/"
|
||||
else:
|
||||
url = "http://send.onenetworkdirect.net/z/616460/CD225091/"
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
|
||||
|
||||
def _listVMwareVMsSlot(self):
|
||||
"""
|
||||
Slot to refresh the VMware VMs list.
|
||||
"""
|
||||
|
||||
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{version}.zip".format(version=__version__)
|
||||
self.uiGNS3VMDownloadLinkUrlLabel.setText('The GNS3 VM can <a href="{download_url}">downloaded here</a>.<br>Import the VM in your virtualization software and hit refresh.'.format(download_url=download_url))
|
||||
self.uiVirtualBoxRadioButton.setChecked(False)
|
||||
from gns3.modules import VMware
|
||||
settings = VMware.instance().settings()
|
||||
if not os.path.exists(settings["vmrun_path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API is probably not installed")
|
||||
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/")
|
||||
return
|
||||
self._refreshVMListSlot()
|
||||
|
||||
@@ -76,6 +134,9 @@ 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()
|
||||
@@ -84,15 +145,6 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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.
|
||||
@@ -108,6 +160,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.
|
||||
@@ -116,99 +175,131 @@ 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 a little bit more than half of the physical memory
|
||||
ram = int(psutil.virtual_memory().total / (1024 * 1024) / 1.8)
|
||||
# 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.uiSummaryWizardPage:
|
||||
use_local_server = self.uiLocalRadioButton.isChecked()
|
||||
self.uiSummaryTreeWidget.clear()
|
||||
if use_local_server:
|
||||
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"]))
|
||||
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")
|
||||
|
||||
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()
|
||||
|
||||
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.uiSummaryWizardPage:
|
||||
use_local_server = self.uiLocalRadioButton.isChecked()
|
||||
if use_local_server:
|
||||
# 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": 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})
|
||||
|
||||
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):
|
||||
@@ -216,14 +307,10 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
Refresh the list of VM available in VMware or VirtualBox.
|
||||
"""
|
||||
|
||||
if not Servers.instance().localServerIsRunning():
|
||||
QtWidgets.QMessageBox.critical(self, "Local server", "{}".format("Local server is not running"))
|
||||
return
|
||||
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):
|
||||
"""
|
||||
@@ -239,9 +326,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:
|
||||
@@ -262,6 +349,8 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
settings["hide_setup_wizard"] = self.uiShowCheckBox.isChecked()
|
||||
self.parentWidget().setSettings(settings)
|
||||
super().done(result)
|
||||
dialog = NewApplianceDialog(self.parentWidget())
|
||||
dialog.show()
|
||||
|
||||
def nextId(self):
|
||||
"""
|
||||
@@ -269,7 +358,9 @@ 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.uiServerWizardPage and self.uiVMRadioButton.isChecked():
|
||||
# skip the local server page if using the GNS3 VM
|
||||
return self.uiLocalServerWizardPage.nextId()
|
||||
if self.page(current_id) == self.uiLocalServerWizardPage:
|
||||
return self.uiVMWizardPage.nextId()
|
||||
return QtWidgets.QWizard.nextId(self)
|
||||
|
||||
@@ -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.svg_node_item import SvgNodeItem
|
||||
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 ..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)
|
||||
@@ -47,34 +53,45 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
self._items = items
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
self._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
|
||||
self.uiCustomSymbolRadioButton.toggled.connect(self._customSymbolToggledSlot)
|
||||
self.uiBuiltInSymbolRadioButton.toggled.connect(self._builtInSymbolToggledSlot)
|
||||
self.uiSearchLineEdit.textChanged.connect(self._searchTextChangedSlot)
|
||||
self.uiBuiltinSymbolOnlyCheckBox.toggled.connect(self._builtinSymbolOnlyToggledSlot)
|
||||
if not SymbolSelectionDialog._symbols_dir:
|
||||
SymbolSelectionDialog._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
|
||||
|
||||
selected_symbol = symbol
|
||||
if not self._items:
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).hide()
|
||||
else:
|
||||
first_item = items[0]
|
||||
if isinstance(first_item, SvgNodeItem):
|
||||
custom_symbol = first_item.renderer().objectName()
|
||||
if not custom_symbol:
|
||||
symbol_name = first_item.node().defaultSymbol()
|
||||
else:
|
||||
symbol_name = custom_symbol
|
||||
selected_symbol = symbol_name
|
||||
elif isinstance(first_item, PixmapNodeItem):
|
||||
self.uiSymbolLineEdit.setText(first_item.pixmapSymbolPath())
|
||||
|
||||
self.uiBuiltInSymbolRadioButton.setChecked(True)
|
||||
self.uiSymbolListWidget.setFocus()
|
||||
self.uiSymbolListWidget.setIconSize(QtCore.QSize(64, 64))
|
||||
symbol_resources = QtCore.QResource(":/symbols")
|
||||
for symbol in symbol_resources.children():
|
||||
if symbol.endswith(".svg"):
|
||||
name = os.path.splitext(symbol)[0]
|
||||
item = QtWidgets.QListWidgetItem(self.uiSymbolListWidget)
|
||||
item.setText(name)
|
||||
resource_path = ":/symbols/" + symbol
|
||||
svg_renderer = QtSvg.QSvgRenderer(resource_path)
|
||||
if resource_path == selected_symbol:
|
||||
self.uiSymbolListWidget.setCurrentItem(item)
|
||||
self._symbol_items = []
|
||||
|
||||
Controller.instance().get("/symbols", self._listSymbolsCallback)
|
||||
|
||||
def _listSymbolsCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while listing symbols: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
self._symbol_items = []
|
||||
for symbol in result:
|
||||
symbol = Symbol(**symbol)
|
||||
name = os.path.splitext(symbol.filename())[0]
|
||||
item = QtWidgets.QListWidgetItem(self.uiSymbolListWidget)
|
||||
item.setData(QtCore.Qt.UserRole, symbol)
|
||||
self._symbol_items.append(item)
|
||||
item.setText(name)
|
||||
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
|
||||
item.setIcon(icon)
|
||||
|
||||
def render(item, path):
|
||||
svg_renderer = QImageSvgRenderer(path)
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
@@ -82,52 +99,93 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
|
||||
item.setIcon(icon)
|
||||
|
||||
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
|
||||
self.adjustSize()
|
||||
|
||||
def _builtinSymbolOnlyToggledSlot(self, checked):
|
||||
self._filter()
|
||||
|
||||
def _searchTextChangedSlot(self, text):
|
||||
self._filter()
|
||||
|
||||
def _filter(self):
|
||||
"""
|
||||
Hide element not matching the search
|
||||
"""
|
||||
text = self.uiSearchLineEdit.text()
|
||||
for item in self._symbol_items:
|
||||
if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not item.data(QtCore.Qt.UserRole).builtin():
|
||||
item.setHidden(True)
|
||||
else:
|
||||
if len(text.strip()) == 0 or text.strip().lower() in item.text().lower():
|
||||
item.setHidden(False)
|
||||
else:
|
||||
item.setHidden(True)
|
||||
|
||||
def _customSymbolToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the custom symbol radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiCustomSymbolGroupBox.setEnabled(True)
|
||||
self.uiCustomSymbolGroupBox.show()
|
||||
self.uiBuiltInGroupBox.setEnabled(False)
|
||||
self.uiBuiltInGroupBox.hide()
|
||||
self.adjustSize()
|
||||
|
||||
def _builtInSymbolToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the built-in symbol radio button is toggled.
|
||||
|
||||
:param checked: either the button is checked or not
|
||||
"""
|
||||
|
||||
if checked:
|
||||
self.uiCustomSymbolGroupBox.setEnabled(False)
|
||||
self.uiCustomSymbolGroupBox.hide()
|
||||
self.uiBuiltInGroupBox.setEnabled(True)
|
||||
self.uiBuiltInGroupBox.show()
|
||||
self.adjustSize()
|
||||
|
||||
def _applyPreferencesSlot(self):
|
||||
"""
|
||||
Applies the selected symbol to the items.
|
||||
"""
|
||||
|
||||
current = self.uiSymbolListWidget.currentItem()
|
||||
if current:
|
||||
name = current.text()
|
||||
path = ":/symbols/{}.svg".format(name)
|
||||
renderer = QtSvg.QSvgRenderer(path)
|
||||
renderer.setObjectName(path)
|
||||
for item in self._items:
|
||||
if isinstance(item, SvgNodeItem):
|
||||
item.setSharedRenderer(renderer)
|
||||
else:
|
||||
log.warning("Built-in SVG symbol cannot be applied on Pixmap node item")
|
||||
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
pixmap = QtGui.QPixmap(symbol_path)
|
||||
if not pixmap.isNull():
|
||||
for item in self._items:
|
||||
if isinstance(item, PixmapNodeItem):
|
||||
item.setPixmap(pixmap)
|
||||
else:
|
||||
log.warning("Custom pixmap symbol cannot be applied on SVG node item")
|
||||
symbol_path = self.getSymbol()
|
||||
for item in self._items:
|
||||
item.setSymbol(symbol_path)
|
||||
return True
|
||||
|
||||
def getSymbol(self):
|
||||
|
||||
if self.uiSymbolListWidget.isEnabled():
|
||||
current = self.uiSymbolListWidget.currentItem()
|
||||
name = current.text()
|
||||
normal_symbol = ":/symbols/{}.svg".format(name)
|
||||
if current:
|
||||
return current.data(QtCore.Qt.UserRole).id()
|
||||
else:
|
||||
normal_symbol = self.uiSymbolLineEdit.text()
|
||||
return normal_symbol
|
||||
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)
|
||||
self.uiSymbolListWidget.setEnabled(False)
|
||||
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))
|
||||
@@ -139,6 +197,9 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
|
||||
if result and self._items:
|
||||
self._applyPreferencesSlot()
|
||||
if result and self._items and not self._applyPreferencesSlot():
|
||||
result = 0
|
||||
super().done(result)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
|
||||
self.uiPlainTextEdit.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
|
||||
|
||||
if len(self._items) == 1:
|
||||
self.uiApplyColorToAllItemsCheckBox.setChecked(True)
|
||||
self.uiApplyColorToAllItemsCheckBox.hide()
|
||||
self.uiApplyRotationToAllItemsCheckBox.setChecked(True)
|
||||
self.uiApplyRotationToAllItemsCheckBox.hide()
|
||||
self.uiApplyTextToAllItemsCheckBox.setChecked(True)
|
||||
self.uiApplyTextToAllItemsCheckBox.hide()
|
||||
|
||||
@@ -90,9 +94,11 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
|
||||
"""
|
||||
|
||||
for item in self._items:
|
||||
item.setDefaultTextColor(self._color)
|
||||
item.setFont(self.uiPlainTextEdit.font())
|
||||
item.setRotation(self.uiRotationSpinBox.value())
|
||||
if self.uiApplyColorToAllItemsCheckBox.isChecked():
|
||||
item.setDefaultTextColor(self._color)
|
||||
if self.uiApplyRotationToAllItemsCheckBox.isChecked():
|
||||
item.setRotation(self.uiRotationSpinBox.value())
|
||||
if item.editable() and self.uiApplyTextToAllItemsCheckBox.isChecked():
|
||||
item.setPlainText(self.uiPlainTextEdit.toPlainText())
|
||||
|
||||
|
||||
192
gns3/dialogs/vm_with_images_wizard.py
Normal file
192
gns3/dialogs/vm_with_images_wizard.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from .vm_wizard import VMWizard
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.controller import Controller
|
||||
|
||||
|
||||
|
||||
class VMWithImagesWizard(VMWizard):
|
||||
"""
|
||||
Base class for VM wizard with image management (Qemu, IOU...)
|
||||
|
||||
:param devices: List of existing device for this type
|
||||
:param use_local_server: Value the use_local_server settings for this module
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, devices, use_local_server, parent):
|
||||
# The list of images combo box (Qemu support multiple images)
|
||||
self._images_combo_boxes = set()
|
||||
|
||||
# The list of radio button for existing image or new images
|
||||
self._radio_existing_images_buttons = set()
|
||||
|
||||
super().__init__(devices, use_local_server, parent)
|
||||
|
||||
def refreshImageStepsButtons(self):
|
||||
"""
|
||||
When changing the server type (remote or local)
|
||||
Refresh all the image selectors
|
||||
"""
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
radio_button.setChecked(radio_button.isChecked())
|
||||
|
||||
def _vmToggledSlot(self, checked):
|
||||
super()._vmToggledSlot(checked)
|
||||
if checked:
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
super()._remoteServerToggledSlot(checked)
|
||||
if checked:
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
def _localToggledSlot(self, checked):
|
||||
super()._localToggledSlot(checked)
|
||||
if checked:
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
def addImageSelector(self, radio_button, combo_box, line_edit, browser, image_selector, create_button=None, create_image_wizard=None, image_suffix=""):
|
||||
"""
|
||||
Add a remote image selector
|
||||
|
||||
:param radio_button: Radio button which toggle display of the listbox
|
||||
:param combo_box: The image choice combo box
|
||||
:param line_edit: The edit for the image
|
||||
:param browser: file upload browser button
|
||||
:param image_selector: function which display an image selector and return path
|
||||
:param create_button: Image create button None if you don't need one
|
||||
:param create_image_wizard: Wizard Class for creating a new image
|
||||
"""
|
||||
|
||||
combo_box.currentIndexChanged.connect(lambda index: self._imageListIndexChangedSlot(index, combo_box, line_edit))
|
||||
self._images_combo_boxes.add(combo_box)
|
||||
|
||||
browser.clicked.connect(lambda: self._imageBrowserSlot(line_edit, image_selector))
|
||||
|
||||
if create_button:
|
||||
assert create_image_wizard is not None
|
||||
create_button.clicked.connect(lambda: self._imageCreateSlot(line_edit, create_image_wizard, image_suffix))
|
||||
|
||||
self._existingImageToggledSlot(True, combo_box, line_edit, browser, create_button)
|
||||
radio_button.toggled.connect(lambda checked: self._existingImageToggledSlot(checked, combo_box, line_edit, browser, create_button))
|
||||
self._radio_existing_images_buttons.add(radio_button)
|
||||
|
||||
def _imageCreateSlot(self, line_edit, create_image_wizard, image_suffix):
|
||||
create_dialog = create_image_wizard(self, self.getSettings()["server"], self.uiNameLineEdit.text() + image_suffix)
|
||||
if QtWidgets.QDialog.Accepted == create_dialog.exec_():
|
||||
line_edit.setText(create_dialog.uiLocationLineEdit.text())
|
||||
|
||||
def _imageBrowserSlot(self, line_edit, image_selector):
|
||||
"""
|
||||
Slot to open a file browser and select an image.
|
||||
"""
|
||||
|
||||
path = image_selector(self, self._compute_id)
|
||||
if not path:
|
||||
return
|
||||
line_edit.clear()
|
||||
line_edit.setText(path)
|
||||
|
||||
def _imageListIndexChangedSlot(self, index, combo_box, line_edit):
|
||||
"""
|
||||
User select a different image in the combo box
|
||||
"""
|
||||
item = combo_box.itemData(index)
|
||||
if item and item["path"]:
|
||||
line_edit.setText(item["path"])
|
||||
else:
|
||||
line_edit.setText("")
|
||||
|
||||
def _existingImageToggledSlot(self, checked, combo_box, line_edit, browser, create_button):
|
||||
"""
|
||||
User select the option of using an existing image
|
||||
"""
|
||||
|
||||
if create_button:
|
||||
create_button.hide()
|
||||
|
||||
if checked:
|
||||
combo_box.show()
|
||||
browser.hide()
|
||||
line_edit.hide()
|
||||
if combo_box.count() > 0:
|
||||
line_edit.setText(combo_box.itemData(combo_box.currentIndex())["path"])
|
||||
else:
|
||||
combo_box.hide()
|
||||
line_edit.setText("")
|
||||
line_edit.show()
|
||||
browser.show()
|
||||
if create_button:
|
||||
create_button.show()
|
||||
|
||||
def loadImagesList(self, endpoint):
|
||||
"""
|
||||
Fill the list box with available Images"
|
||||
|
||||
:param endpoint: server endpoint with the list of Images
|
||||
"""
|
||||
|
||||
Controller.instance().get("/computes/{}{}".format(self._compute_id, endpoint), self._getImagesFromServerCallback)
|
||||
|
||||
def _getImagesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for loadImagesList.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Images", "Error while getting the VMs: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
# Wizard is closed
|
||||
if self.currentPage() is None:
|
||||
return
|
||||
|
||||
if len(result) == 0:
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
if radio_button.isChecked() and self._widgetOnCurrentPage(radio_button):
|
||||
for button in radio_button.parent().findChildren(QtWidgets.QRadioButton):
|
||||
if button != radio_button:
|
||||
button.setChecked(True)
|
||||
button.hide()
|
||||
else:
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
if self._widgetOnCurrentPage(radio_button):
|
||||
for button in radio_button.parent().findChildren(QtWidgets.QRadioButton):
|
||||
if button == radio_button:
|
||||
button.setChecked(True)
|
||||
button.show()
|
||||
|
||||
for combo_box in self._images_combo_boxes:
|
||||
if self._widgetOnCurrentPage(combo_box):
|
||||
combo_box.clear()
|
||||
for vm in result:
|
||||
combo_box.addItem(vm["path"], vm)
|
||||
|
||||
|
||||
def _widgetOnCurrentPage(self, widget):
|
||||
"""
|
||||
:returns Boolean True if widget is current active Wizard page
|
||||
"""
|
||||
return self.currentPage().findChild(widget.__class__, widget.objectName()) is not None
|
||||
|
||||
@@ -18,42 +18,46 @@
|
||||
import sys
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.servers import Servers
|
||||
from gns3.compute_manager import ComputeManager
|
||||
|
||||
|
||||
class VMWizard(QtWidgets.QWizard):
|
||||
|
||||
"""
|
||||
Base class for VM wizard.
|
||||
|
||||
:param devices: List of existing device for this type
|
||||
:param use_local_server: Value the use_local_server settings for this module
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, devices, use_local_server, parent):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.setModal(True)
|
||||
|
||||
self._devices = devices
|
||||
self._use_local_server = use_local_server
|
||||
|
||||
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.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
|
||||
|
||||
# The list of images combo box (Qemu support multiple images)
|
||||
self._images_combo_boxes = set()
|
||||
# By default we use the local server
|
||||
self._compute_id = ComputeManager.instance().computes()[0].id()
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
self._localToggledSlot(True)
|
||||
|
||||
# The list of radio button for existing image or new images
|
||||
self._radio_existing_images_buttons = set()
|
||||
|
||||
def refreshImageStepsButtons(self):
|
||||
"""
|
||||
When changing the server type (remote or local)
|
||||
Refresh all the image selectors
|
||||
"""
|
||||
for radio_button in self._radio_existing_images_buttons:
|
||||
radio_button.setChecked(radio_button.isChecked())
|
||||
if len(ComputeManager.instance().computes()) == 1:
|
||||
# skip the server page if we use the first server
|
||||
self.setStartId(1)
|
||||
|
||||
def _vmToggledSlot(self, checked):
|
||||
"""
|
||||
@@ -64,7 +68,6 @@ class VMWizard(QtWidgets.QWizard):
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
"""
|
||||
@@ -75,8 +78,8 @@ class VMWizard(QtWidgets.QWizard):
|
||||
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(True)
|
||||
self.uiRemoteServersComboBox.setEnabled(True)
|
||||
self.uiRemoteServersGroupBox.show()
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
def _localToggledSlot(self, checked):
|
||||
"""
|
||||
@@ -87,157 +90,67 @@ class VMWizard(QtWidgets.QWizard):
|
||||
if checked:
|
||||
self.uiRemoteServersGroupBox.setEnabled(False)
|
||||
self.uiRemoteServersGroupBox.hide()
|
||||
self.refreshImageStepsButtons()
|
||||
|
||||
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)
|
||||
|
||||
self.uiRemoteRadioButton.setEnabled(False)
|
||||
if hasattr(self, "uiVMRadioButton"):
|
||||
self.uiVMRadioButton.setEnabled(False)
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
for compute in ComputeManager.instance().computes():
|
||||
if compute.id() == "local":
|
||||
self.uiLocalRadioButton.setEnabled(True)
|
||||
elif compute.id() == "vm" 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 self.uiVMRadioButton.isEnabled():
|
||||
self.uiVMRadioButton.setChecked(True)
|
||||
else:
|
||||
if self.uiRemoteRadioButton.isEnabled():
|
||||
self.uiRemoteRadioButton.setChecked(True)
|
||||
else:
|
||||
self.uiLocalRadioButton.setChecked(True)
|
||||
|
||||
def _disableLocalServer(self):
|
||||
"""
|
||||
Turn off the local server
|
||||
"""
|
||||
self.uiLocalRadioButton.hide()
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
self.setStartId(0)
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the server.
|
||||
"""
|
||||
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.currentPage() == self.uiServerWizardPage:
|
||||
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())
|
||||
elif 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 = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
|
||||
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
|
||||
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)
|
||||
|
||||
def addImageSelector(self, radio_button, combo_box, line_edit, browser, image_selector, create_button=None, create_image_wizard=None, image_suffix=""):
|
||||
"""
|
||||
Add a remote image selector
|
||||
|
||||
:param radio_button: Radio button which toggle display of the listbox
|
||||
:param combo_box: The image choice combo box
|
||||
:param line_edit: The edit for the image
|
||||
:param browser: file upload browser button
|
||||
:param image_selector: function which display an image selector and return path
|
||||
:param create_button: Image create button None if you don't need one
|
||||
:param create_image_wizard: Wizard Class for creating a new image
|
||||
"""
|
||||
|
||||
combo_box.currentIndexChanged.connect(lambda index: self._imageListIndexChangedSlot(index, combo_box, line_edit))
|
||||
self._images_combo_boxes.add(combo_box)
|
||||
|
||||
browser.clicked.connect(lambda: self._imageBrowserSlot(line_edit, image_selector))
|
||||
|
||||
if create_button:
|
||||
assert create_image_wizard is not None
|
||||
create_button.clicked.connect(lambda: self._imageCreateSlot(line_edit, create_image_wizard, image_suffix))
|
||||
|
||||
self._existingImageToggledSlot(True, combo_box, line_edit, browser, create_button)
|
||||
radio_button.toggled.connect(lambda checked: self._existingImageToggledSlot(checked, combo_box, line_edit, browser, create_button))
|
||||
self._radio_existing_images_buttons.add(radio_button)
|
||||
|
||||
def _imageCreateSlot(self, line_edit, create_image_wizard, image_suffix):
|
||||
server = Servers.instance().getServerFromString(self.getSettings()["server"])
|
||||
|
||||
create_dialog = create_image_wizard(self, server, self.uiNameLineEdit.text() + image_suffix)
|
||||
if QtWidgets.QDialog.Accepted == create_dialog.exec_():
|
||||
line_edit.setText(create_dialog.uiLocationLineEdit.text() + image_suffix)
|
||||
|
||||
def _imageBrowserSlot(self, line_edit, image_selector):
|
||||
"""
|
||||
Slot to open a file browser and select an image.
|
||||
"""
|
||||
|
||||
server = Servers.instance().getServerFromString(self.getSettings()["server"])
|
||||
path = image_selector(self, server)
|
||||
if not path:
|
||||
return
|
||||
line_edit.clear()
|
||||
line_edit.setText(path)
|
||||
|
||||
def _imageListIndexChangedSlot(self, index, combo_box, line_edit):
|
||||
"""
|
||||
User select a different image in the combo box
|
||||
"""
|
||||
item = combo_box.itemData(index)
|
||||
if item and item["filename"]:
|
||||
line_edit.setText(item["filename"])
|
||||
else:
|
||||
line_edit.setText("")
|
||||
|
||||
def _existingImageToggledSlot(self, checked, combo_box, line_edit, browser, create_button):
|
||||
"""
|
||||
User select the option of using an existing image
|
||||
"""
|
||||
|
||||
if create_button:
|
||||
create_button.hide()
|
||||
|
||||
if checked:
|
||||
combo_box.show()
|
||||
browser.hide()
|
||||
line_edit.hide()
|
||||
if combo_box.count() > 0:
|
||||
line_edit.setText(combo_box.itemData(combo_box.currentIndex())["filename"])
|
||||
else:
|
||||
combo_box.hide()
|
||||
line_edit.setText("")
|
||||
line_edit.show()
|
||||
browser.show()
|
||||
if create_button:
|
||||
create_button.show()
|
||||
|
||||
def loadImagesList(self, endpoint):
|
||||
"""
|
||||
Fill the list box with available Images"
|
||||
|
||||
:param endpoint: server endpoint with the list of Images
|
||||
"""
|
||||
|
||||
self._server.get(endpoint, self._getImagesFromServerCallback)
|
||||
|
||||
def _getImagesFromServerCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for loadImagesList.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Images", "Error while getting the VMs: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
for combo_box in self._images_combo_boxes:
|
||||
combo_box.clear()
|
||||
for vm in result:
|
||||
combo_box.addItem(vm["filename"], vm)
|
||||
|
||||
275
gns3/gns3_vm.py
275
gns3/gns3_vm.py
@@ -1,275 +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":
|
||||
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)")
|
||||
|
||||
@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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,17 +17,17 @@
|
||||
|
||||
|
||||
import json
|
||||
import http
|
||||
import copy
|
||||
import ipaddress
|
||||
import http
|
||||
import uuid
|
||||
import urllib.request
|
||||
import pathlib
|
||||
import urllib.request
|
||||
import base64
|
||||
from functools import partial
|
||||
|
||||
from .version import __version__, __version_info__
|
||||
from .qt import QtCore, QtNetwork
|
||||
from .network_client import getNetworkUrl
|
||||
from .qt import QtCore, QtNetwork, qpartial
|
||||
from .utils import parse_version
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -48,91 +48,41 @@ 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
|
||||
|
||||
connected_signal = QtCore.Signal()
|
||||
connection_error_signal = QtCore.Signal(str)
|
||||
connection_connected_signal = QtCore.Signal()
|
||||
|
||||
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"]
|
||||
self._http_host = settings["host"]
|
||||
if self._host == "0.0.0.0":
|
||||
self._host = "127.0.0.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._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 getTunnel(self, port):
|
||||
"""
|
||||
Get a tunnel to the remote port.
|
||||
For HTTP standard client it's the same port. For SSH it will create a new tunnel.
|
||||
|
||||
:param port: Remote port
|
||||
:returns: Tuple host, port to connect
|
||||
"""
|
||||
return self._host, port
|
||||
|
||||
def releaseTunnel(self, port):
|
||||
"""
|
||||
Release a tunnel to the remote port.
|
||||
For HTTP standard client it's do nothing
|
||||
|
||||
:param port: Allocated remote port
|
||||
"""
|
||||
pass
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -142,7 +92,6 @@ class HTTPClient(QtCore.QObject):
|
||||
|
||||
def setHost(self, host):
|
||||
self._host = host
|
||||
self._http_host = host
|
||||
|
||||
def port(self):
|
||||
"""
|
||||
@@ -152,13 +101,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):
|
||||
"""
|
||||
@@ -166,13 +122,33 @@ class HTTPClient(QtCore.QObject):
|
||||
"""
|
||||
return self._user
|
||||
|
||||
def setUser(self, user):
|
||||
self._user = user
|
||||
def url(self):
|
||||
"""Returns current server url"""
|
||||
|
||||
return "{}://{}:{}".format(self.protocol(), self.host(), self.port())
|
||||
|
||||
def fullUrl(self):
|
||||
"""Returns current server url including user and password"""
|
||||
|
||||
if self._user:
|
||||
return "{}://{}:{}@{}:{}".format(self.protocol(), self._user, self._password, self.host(), self.port())
|
||||
else:
|
||||
return "{}://{}:{}".format(self.protocol(), self.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
|
||||
"""
|
||||
@@ -180,12 +156,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
|
||||
"""
|
||||
@@ -193,19 +166,19 @@ 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(query_id, sent, total)
|
||||
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
|
||||
"""
|
||||
if HTTPClient._progress_callback:
|
||||
HTTPClient._progress_callback.progress(query_id, sent, total)
|
||||
HTTPClient._progress_callback.progress_signal.emit(query_id, sent, total)
|
||||
|
||||
@classmethod
|
||||
def setProgressCallback(cls, progress_callback):
|
||||
@@ -215,59 +188,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.
|
||||
@@ -280,113 +200,8 @@ class HTTPClient(QtCore.QObject):
|
||||
"""
|
||||
Closes the connection with the server.
|
||||
"""
|
||||
log.info("Connection to %s closed", self.url())
|
||||
self._connected = False
|
||||
|
||||
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")
|
||||
local_server = json_data.get("local", False)
|
||||
if version != __version__:
|
||||
log.debug("Client version {} differs with server version {}".format(__version__, version))
|
||||
return False
|
||||
if not local_server:
|
||||
log.debug("Running server is not a GNS3 local server (not started with --local)")
|
||||
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 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):
|
||||
"""
|
||||
Get a QNetworkRequest object. You can mock this
|
||||
@@ -398,17 +213,15 @@ class HTTPClient(QtCore.QObject):
|
||||
|
||||
return QtNetwork.QNetworkRequest(url)
|
||||
|
||||
# FIXME: connect is a method in parent class (QObject)
|
||||
def connect(self, query, callback):
|
||||
def _connect(self, query, server):
|
||||
"""
|
||||
Initialize the connection
|
||||
|
||||
:param query: The query to execute when all network stack is ready
|
||||
:param callback: User callback when connection is finish
|
||||
: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
|
||||
|
||||
@@ -419,35 +232,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, callback, body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText)
|
||||
else:
|
||||
log.info("Connection to {}".format(self.url()))
|
||||
query = partial(self._callbackConnect, method, path, callback, body, context, downloadProgressCallback=downloadProgressCallback, showProgress=showProgress, ignoreErrors=ignoreErrors, progressText=progressText)
|
||||
self.connect(query, callback)
|
||||
# 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 len(msg) > 0:
|
||||
msg = "Can't connect to server {}: {}".format(self.url(), msg)
|
||||
msg = "Cannot connect to server {}: {}".format(self.url(), msg)
|
||||
else:
|
||||
msg = "Can't connect to server {}".format(self.url())
|
||||
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.".format(self.url())
|
||||
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
|
||||
|
||||
@@ -459,42 +291,50 @@ 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__:
|
||||
msg = "Client version {} differs with server version {}".format(__version__, params["version"])
|
||||
log.error(msg)
|
||||
# Official release
|
||||
# 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
|
||||
else:
|
||||
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
|
||||
# We don't allow different major version to interact even with dev build
|
||||
elif parse_version(__version__)[:2] != parse_version(params["version"])[:2]:
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
return
|
||||
log.warning("Use a different client and server version can create bugs. Use it at your own risk.")
|
||||
|
||||
self._connected = True
|
||||
kwargs["context"] = original_context
|
||||
self.executeHTTPQuery(method, path, callback, body, **kwargs)
|
||||
self._version = params["version"]
|
||||
self._retry = 0
|
||||
self.connection_connected_signal.emit()
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if request:
|
||||
request()
|
||||
self._query_waiting_connections = []
|
||||
|
||||
def _addBodyToRequest(self, body, request):
|
||||
"""
|
||||
@@ -524,10 +364,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
|
||||
"""
|
||||
@@ -538,7 +385,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
|
||||
|
||||
@@ -551,86 +398,117 @@ 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))
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}/v1{path}".format(protocol=self._scheme, host=host, port=self._http_port, path=path))
|
||||
if params == {}:
|
||||
query_string = ""
|
||||
else:
|
||||
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)
|
||||
|
||||
import copy
|
||||
context = copy.copy(context)
|
||||
query_id = str(uuid.uuid4())
|
||||
context["query_id"] = query_id
|
||||
if showProgress:
|
||||
self.notify_progress_start_query(context["query_id"], progressText, response)
|
||||
response.uploadProgress.connect(partial(self.notify_progress_upload, query_id))
|
||||
response.downloadProgress.connect(partial(self.notify_progress_download, query_id))
|
||||
context["query_id"] = str(uuid.uuid4())
|
||||
|
||||
response.finished.connect(qpartial(self._processResponse, response, server, callback, context, body, ignoreErrors))
|
||||
|
||||
response.finished.connect(partial(self._processResponse, response, callback, context, body, ignoreErrors))
|
||||
if downloadProgressCallback is not None:
|
||||
response.downloadProgress.connect(partial(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"]))
|
||||
# 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)
|
||||
|
||||
if timeout is not None:
|
||||
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response))
|
||||
|
||||
return response
|
||||
|
||||
def _processDownloadProgress(self, response, callback, context):
|
||||
def _readyReadySlot(self, response, callback, context, server, *args):
|
||||
"""
|
||||
Process a packet receive on the notification feed.
|
||||
The feed can contains partial JSON. If we found a
|
||||
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
|
||||
|
||||
if response.error() == QtNetwork.QNetworkReply.NoError:
|
||||
error_code = response.error()
|
||||
# HTTP error
|
||||
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status >= 300:
|
||||
return
|
||||
|
||||
if error_code >= 300:
|
||||
return
|
||||
content = bytes(response.readAll())
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
if content_type == "application/json":
|
||||
content = content.decode("utf-8")
|
||||
if context["query_id"] in self._buffer:
|
||||
content = self._buffer[context["query_id"]] + content
|
||||
try:
|
||||
while True:
|
||||
content = content.lstrip(" \r\n\t")
|
||||
answer, index = json.JSONDecoder().raw_decode(content)
|
||||
callback(answer, server=server, context=context)
|
||||
content = content[index:]
|
||||
except ValueError: # Partial JSON
|
||||
self._buffer[context["query_id"]] = content
|
||||
else:
|
||||
callback(content, server=server, context=context)
|
||||
|
||||
content = bytes(response.readAll())
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
if content_type == "application/json":
|
||||
content = content.decode("utf-8")
|
||||
if context["query_id"] in self._buffer:
|
||||
content = self._buffer[context["query_id"]] + content
|
||||
try:
|
||||
while True:
|
||||
content = content.lstrip(" \r\n\t")
|
||||
answer, index = json.JSONDecoder().raw_decode(content)
|
||||
callback(answer, server=self, context=context)
|
||||
content = content[index:]
|
||||
except ValueError: # Partial JSON
|
||||
self._buffer[context["query_id"]] = content
|
||||
else:
|
||||
callback(content, server=self, context=context)
|
||||
|
||||
if HTTPClient._progress_callback and HTTPClient._progress_callback.progress_dialog():
|
||||
request_canceled = partial(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()
|
||||
@@ -639,25 +517,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")
|
||||
@@ -667,15 +545,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)
|
||||
callback(json.loads(body), error=True, server=self, context=context)
|
||||
try:
|
||||
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=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
|
||||
@@ -687,71 +576,59 @@ 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:
|
||||
raise HttpBadRequest(body)
|
||||
try:
|
||||
params = json.loads(body)
|
||||
e = HttpBadRequest(body)
|
||||
e.fingerprint = params["path"]
|
||||
# If something goes wrong for a any reason just raise the bad request
|
||||
except Exception:
|
||||
e = HttpBadRequest(body)
|
||||
raise e
|
||||
|
||||
def 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
|
||||
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))
|
||||
|
||||
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 isCloud(self):
|
||||
return False
|
||||
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,10 +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
|
||||
|
||||
@@ -30,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.
|
||||
@@ -38,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,
|
||||
@@ -55,6 +59,11 @@ class ImageManager:
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
destination_path = os.path.join(destination_directory, os.path.basename(path))
|
||||
try:
|
||||
os.makedirs(destination_directory, exist_ok=True)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(parent, 'Image', 'Could not create destination directory {}: {}'.format(destination_directory, str(e)))
|
||||
return path
|
||||
worker = FileCopyWorker(path, destination_path)
|
||||
progress_dialog = ProgressDialog(worker, 'Image', 'Copying {}'.format(os.path.basename(path)), 'Cancel', busy=True, parent=parent)
|
||||
progress_dialog.show()
|
||||
@@ -62,53 +71,35 @@ class ImageManager:
|
||||
errors = progress_dialog.errors()
|
||||
if errors:
|
||||
QtWidgets.QMessageBox.critical(parent, 'Image', '{}'.format(''.join(errors)))
|
||||
return path
|
||||
else:
|
||||
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 = os.path.basename(path)
|
||||
server.post('{}/{}'.format(upload_endpoint, filename), None, body=pathlib.Path(path))
|
||||
filename = self._getRelativeImagePath(path, node_type).replace("\\", "/")
|
||||
|
||||
Controller.instance().post(r'/computes/{}{}/{}'.format(server, upload_endpoint, filename), None, body=pathlib.Path(path), progressText="Uploading {}".format(filename), timeout=None)
|
||||
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):
|
||||
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()
|
||||
@@ -121,6 +112,25 @@ class ImageManager:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _getRelativeImagePath(self, path, node_type):
|
||||
"""
|
||||
Get a path relative to images directory path
|
||||
or just filename if the path is not located inside
|
||||
image directory
|
||||
|
||||
:param path: file path
|
||||
:param node_type: Type of vm
|
||||
:return: file path
|
||||
"""
|
||||
|
||||
if not path:
|
||||
return ""
|
||||
img_directory = self.getDirectoryForType(node_type)
|
||||
path = os.path.abspath(path)
|
||||
if os.path.commonprefix([img_directory, path]) == img_directory:
|
||||
return os.path.relpath(path, img_directory)
|
||||
return os.path.basename(path)
|
||||
|
||||
def getDirectory(self):
|
||||
"""
|
||||
Returns the images directory path.
|
||||
@@ -128,17 +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 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,187 +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 signal
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
try:
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, DEFAULT_BINDING
|
||||
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"] == 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.
|
||||
@@ -172,6 +192,14 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
cls._draw_port_labels = state
|
||||
|
||||
def resetPortLabels(self):
|
||||
"""
|
||||
Resets the port label positions.
|
||||
"""
|
||||
|
||||
self._source_port.deleteLabel()
|
||||
self._destination_port.deleteLabel()
|
||||
|
||||
def populateLinkContextualMenu(self, menu):
|
||||
"""
|
||||
Adds device actions to the link contextual menu.
|
||||
@@ -179,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'))
|
||||
@@ -199,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)
|
||||
@@ -262,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):
|
||||
"""
|
||||
@@ -289,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):
|
||||
"""
|
||||
@@ -311,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()
|
||||
else:
|
||||
self._destination_port.startPacketCaptureReader()
|
||||
elif self._source_port.capturing():
|
||||
self._source_port.startPacketCaptureReader()
|
||||
elif self._destination_port.capturing():
|
||||
self._destination_port.startPacketCaptureReader()
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "Packet capture", "Cannot start Wireshark: {}".format(e))
|
||||
PacketCapture.instance().startPacketCaptureReader(self._link)
|
||||
|
||||
def _analyzeCaptureActionSlot(self):
|
||||
"""
|
||||
@@ -335,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))
|
||||
|
||||
@@ -391,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()
|
||||
@@ -429,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,58 +235,54 @@ 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.
|
||||
"""
|
||||
|
||||
self._node.removeAllocatedName()
|
||||
if self is None or not self.scene():
|
||||
return
|
||||
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
|
||||
"""
|
||||
|
||||
self._last_error = "{message}".format(message=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
|
||||
"""
|
||||
|
||||
self._last_error = "{message}".format(message=message)
|
||||
if self:
|
||||
self._last_error = "{message}".format(message=message)
|
||||
|
||||
def setCustomToolTip(self):
|
||||
"""
|
||||
@@ -262,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):
|
||||
"""
|
||||
@@ -283,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):
|
||||
"""
|
||||
@@ -291,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=[]):
|
||||
"""
|
||||
@@ -324,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:
|
||||
@@ -371,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):
|
||||
"""
|
||||
@@ -437,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):
|
||||
"""
|
||||
@@ -458,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,54 +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 pixmapSymbolPath(self):
|
||||
"""
|
||||
Returns the pixmap path
|
||||
|
||||
:return: 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,19 +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/>.
|
||||
|
||||
"""
|
||||
To show a advanced message box.
|
||||
"""
|
||||
|
||||
from ..qt import QtGui, QtWidgets
|
||||
from ..qt import QtGui
|
||||
|
||||
|
||||
def MessageBox(parent, title, message, details="", icon=QtWidgets.QMessageBox.Critical):
|
||||
|
||||
msgbox = QtWidgets.QMessageBox(parent)
|
||||
msgbox.setWindowTitle(title)
|
||||
msgbox.setText(message)
|
||||
msgbox.setIcon(icon)
|
||||
if details:
|
||||
msgbox.setDetailedText(details)
|
||||
msgbox.exec_()
|
||||
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)
|
||||
@@ -1,75 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2013 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 rsa
|
||||
import sys
|
||||
import os
|
||||
import base64
|
||||
|
||||
PUB_KEY = b"""-----BEGIN RSA PUBLIC KEY-----
|
||||
MIICCgKCAgEAiE4Zgzge3Cg6EUfct7vnzcmXkIvsy6g/QkfEeKSz3Cd+L7kxVZGE
|
||||
weOXySrSSrRBoF1i2JhL2KkqZTY31972deviL+fv+TgE5RueyERFey3fw7+oN/RW
|
||||
i8UIUvRqHjwocCuJq5yUiOv+AdGKG3TNeYXvx4Xvnrr4AJnJRThDfqd0nr8QAXRn
|
||||
/Ifx4MKivL8RDyqHoVlHvHeyJmtaZIzsYthsK3FU2XED6d6xwbga3t2cb4+DfJa3
|
||||
rBtWnoIXHiRdZZUtl34dGiiyxKL2yco+Dpd5pUvw6F7+n77SnSwN+F0ZzrrgUMHA
|
||||
vBHBnF4WB6mjRFxbO+B/H1OxnXcjwxgYWLCbkrhQogqyfdkmacppWLOH9OyzGUkY
|
||||
r7qITLCWSAHuIqXmQF4VAqCPYwEK7o6ndebFk1jaAAPGIw52AA1YOSXJ6jpKiO7f
|
||||
5gXT3xRfv4kW1Fp6le0hp0Laz6VGbOv44vauxk516v5MI+CUL3u5TOmGWM53u1OG
|
||||
qq6SfL+5Cu0/4L+SUaJ7nzN+PgWx6BEd0LRzEVQcmRPA4zHbhJ7ebBbYOul9RFyW
|
||||
8D7yy7mUQZwVQDcuaB6l2pu0BfZppb+Uf81h0nRQIrHt7BRBiyaGojQIHsw8CrqP
|
||||
3fsnHUvqtNLipC26FSTW4wlPIEktsWU8TABgjbuS45+zFTI141/J77ECAwEAAQ==
|
||||
-----END RSA PUBLIC KEY-----"""
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def checkLicence():
|
||||
"""
|
||||
Return true if the user as correctly installed the licence file
|
||||
"""
|
||||
appname = "GNS3"
|
||||
|
||||
filename = "licence.txt"
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
# On windows, the user specific configuration file location is %APPDATA%/GNS3/gns3_gui.conf
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
licence_file = os.path.join(appdata, appname, filename)
|
||||
|
||||
else:
|
||||
# On UNIX-like platforms, the user specific configuration file location is /etc/xdg/GNS3/gns3_gui.conf
|
||||
home = os.path.expanduser("~")
|
||||
licence_file = os.path.join(home, ".config", appname, filename)
|
||||
|
||||
return check_licence_file(licence_file)
|
||||
|
||||
|
||||
def check_licence_file(licence_file):
|
||||
if os.path.exists(licence_file):
|
||||
with open(licence_file) as f:
|
||||
email = f.readline().strip()
|
||||
key = f.readline().strip()
|
||||
pubkey = rsa.PublicKey.load_pkcs1(PUB_KEY)
|
||||
try:
|
||||
rsa.verify(email.encode("utf-8"), base64.b64decode(key), pubkey)
|
||||
log.info("Found a valid licence file. Thanks for your support")
|
||||
return True
|
||||
except rsa.pkcs1.VerificationError:
|
||||
log.error("Invalid licence file.")
|
||||
return False
|
||||
return False
|
||||
496
gns3/link.py
496
gns3/link.py
@@ -19,9 +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
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -36,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__()
|
||||
|
||||
@@ -63,53 +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
|
||||
self._link_id = str(uuid.uuid4())
|
||||
self._creator = True
|
||||
Controller.instance().post("/projects/{project_id}/links".format(project_id=source_node.project().id()), self._linkCreatedCallback, body=body)
|
||||
|
||||
# we must request UDP information if the NIO is a NIO UDP and before
|
||||
# it can be created.
|
||||
if not self._stub:
|
||||
def _parseResponse(self, result):
|
||||
self._capturing = result.get("capturing", False)
|
||||
|
||||
# connect signals used when a NIO has been created by a node
|
||||
# and this NIO need to be attached to a port connected to this link
|
||||
source_node.nio_signal.connect(self.newNIOSlot)
|
||||
destination_node.nio_signal.connect(self.newNIOSlot)
|
||||
|
||||
# currently, we support only NIO_UDP for normal connections (non-stub).
|
||||
if not source_port.defaultNio() == NIOUDP:
|
||||
raise NotImplementedError()
|
||||
|
||||
self._source_udp = None
|
||||
self._destination_udp = None
|
||||
|
||||
# connect signals used to receive a UDP port and host allocated by a node
|
||||
source_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
|
||||
destination_node.allocate_udp_nio_signal.connect(self.UDPPortAllocatedSlot)
|
||||
|
||||
# request the UDP info for each node
|
||||
source_node.allocateUDPPort(self._source_port.id())
|
||||
destination_node.allocateUDPPort(self._destination_port.id())
|
||||
# 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):
|
||||
@@ -126,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.
|
||||
"""
|
||||
@@ -136,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.
|
||||
@@ -194,193 +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 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
|
||||
|
||||
@@ -21,35 +21,17 @@ import json
|
||||
import shutil
|
||||
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__)
|
||||
|
||||
|
||||
class PeriodicCheckConfig(QtCore.QThread):
|
||||
|
||||
"""
|
||||
Timer for checking if the configuration file change
|
||||
on disk.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self._parent = parent
|
||||
|
||||
def run(self):
|
||||
self._timer = QtCore.QTimer()
|
||||
self._timer.timeout.connect(self._parent._checkConfigChanged)
|
||||
self._timer.setInterval(1000) # milliseconds
|
||||
self._timer.start()
|
||||
self.exec_()
|
||||
|
||||
|
||||
class LocalConfig(QtCore.QObject):
|
||||
|
||||
"""
|
||||
@@ -59,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)
|
||||
@@ -82,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):
|
||||
@@ -109,12 +98,48 @@ class LocalConfig(QtCore.QObject):
|
||||
self._settings.update(user_settings)
|
||||
self._migrateOldConfig()
|
||||
self._writeConfig()
|
||||
Controller.instance().connected_signal.connect(self.refreshConfigFromController)
|
||||
|
||||
self._check_thread = PeriodicCheckConfig(self)
|
||||
self._check_thread.start()
|
||||
def profile(self):
|
||||
"""
|
||||
:returns: Current settings profile
|
||||
"""
|
||||
return self._profile
|
||||
|
||||
@staticmethod
|
||||
def configDirectory():
|
||||
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
|
||||
"""
|
||||
@@ -124,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):
|
||||
@@ -131,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")
|
||||
@@ -140,14 +169,24 @@ 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
|
||||
"""
|
||||
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.0"):
|
||||
# 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", {})
|
||||
|
||||
@@ -156,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"])
|
||||
@@ -168,6 +207,25 @@ class LocalConfig(QtCore.QObject):
|
||||
main_window["hide_getting_started_dialog"] = self._settings["GUI"].get("hide_getting_started_dialog", False)
|
||||
self._settings["MainWindow"] = main_window
|
||||
|
||||
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.1dev2"):
|
||||
if sys.platform.startswith("darwin"):
|
||||
from .settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, DEFAULT_TELNET_CONSOLE_COMMAND
|
||||
|
||||
if "MainWindow" in self._settings:
|
||||
if self._settings["MainWindow"]["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):
|
||||
"""
|
||||
Read the configuration file.
|
||||
@@ -204,8 +262,27 @@ 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):
|
||||
|
||||
def _checkConfigChanged(self):
|
||||
try:
|
||||
if self._last_config_changed and self._last_config_changed < os.stat(self._config_file).st_mtime:
|
||||
log.info("Client config has changed, reloading it...")
|
||||
@@ -231,7 +308,7 @@ class LocalConfig(QtCore.QObject):
|
||||
"""
|
||||
|
||||
self._config_file = config_file
|
||||
self._readConfig(self._config_file)
|
||||
self._resetLoadConfig()
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
@@ -263,24 +340,31 @@ class LocalConfig(QtCore.QObject):
|
||||
"""
|
||||
|
||||
settings = self.settings().get(section, dict())
|
||||
changed = False
|
||||
|
||||
def _copySettings(local, default):
|
||||
"""
|
||||
Copy only existing settings, ignore the other.
|
||||
Add default values if require.
|
||||
"""
|
||||
nonlocal changed
|
||||
|
||||
# use default values for missing settings
|
||||
for name, value in default.items():
|
||||
if name not in local:
|
||||
local[name] = value
|
||||
changed = True
|
||||
elif isinstance(value, dict):
|
||||
local[name] = _copySettings(local[name], default[name])
|
||||
return local
|
||||
|
||||
settings = _copySettings(settings, default_settings)
|
||||
|
||||
self._settings[section] = settings
|
||||
|
||||
if changed:
|
||||
log.info("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
|
||||
self._writeConfig()
|
||||
|
||||
return copy.deepcopy(settings)
|
||||
|
||||
def saveSectionSettings(self, section, settings):
|
||||
@@ -295,14 +379,36 @@ class LocalConfig(QtCore.QObject):
|
||||
self._settings[section] = {}
|
||||
|
||||
if self._settings[section] != settings:
|
||||
self._settings[section].update(settings)
|
||||
self._settings[section].update(copy.deepcopy(settings))
|
||||
log.info("Section %s has changed. Saving configuration", section)
|
||||
self._writeConfig()
|
||||
else:
|
||||
log.debug("Section %s has not changed. Skip saving configuration", section)
|
||||
|
||||
def experimental(self):
|
||||
"""
|
||||
:returns: Boolean. True if experimental features allowed
|
||||
"""
|
||||
|
||||
from gns3.settings import GENERAL_SETTINGS
|
||||
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["experimental_features"]
|
||||
|
||||
def 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.
|
||||
|
||||
@@ -310,5 +416,43 @@ 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
|
||||
def isMainGui():
|
||||
"""
|
||||
:returns: Return true if we are the main gui (first gui to start)
|
||||
"""
|
||||
|
||||
my_pid = os.getpid()
|
||||
pid_path = os.path.join(LocalConfig.instance().configDirectory(), "gns3_gui.pid")
|
||||
|
||||
if os.path.exists(pid_path):
|
||||
try:
|
||||
with open(pid_path) as f:
|
||||
pid = int(f.read())
|
||||
if pid != my_pid:
|
||||
try:
|
||||
process = psutil.Process(pid=pid)
|
||||
ps_name = process.name()
|
||||
except (OSError, psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
else:
|
||||
if "gns3" in ps_name or "python" in ps_name:
|
||||
# Process run under the same user id
|
||||
if sys.platform.startswith("win") or process.uids()[0] == os.getuid():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
except (OSError, ValueError) as e:
|
||||
log.critical("Can't read pid file %s: %s", pid_path, str(e))
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(pid_path, 'w+') as f:
|
||||
f.write(str(my_pid))
|
||||
except OSError as e:
|
||||
log.critical("Can't write pid file %s: %s", pid_path, str(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
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()
|
||||
@@ -18,7 +18,6 @@
|
||||
import os
|
||||
import sys
|
||||
import configparser
|
||||
from gns3.qt import QtCore
|
||||
|
||||
|
||||
import logging
|
||||
@@ -41,12 +40,11 @@ class LocalServerConfig:
|
||||
else:
|
||||
filename = "gns3_server.conf"
|
||||
|
||||
from .local_config import LocalConfig
|
||||
if sys.platform.startswith("win"):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
self._config_file = os.path.join(appdata, appname, filename)
|
||||
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
self._config_file = os.path.join(home, ".config", appname, filename)
|
||||
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
|
||||
|
||||
try:
|
||||
# create the config file if it doesn't exist
|
||||
@@ -55,6 +53,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.
|
||||
@@ -62,7 +68,7 @@ class LocalServerConfig:
|
||||
|
||||
try:
|
||||
self._config.read(self._config_file, encoding="utf-8")
|
||||
except (OSError, configparser.Error) as e:
|
||||
except (OSError, configparser.Error, UnicodeEncodeError, UnicodeDecodeError) as e:
|
||||
log.error("Could not read the local server configuration {}: {}".format(self._config_file, e))
|
||||
|
||||
def writeConfig(self):
|
||||
@@ -77,13 +83,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)
|
||||
"""
|
||||
@@ -93,11 +98,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:
|
||||
|
||||
98
gns3/main.py
98
gns3/main.py
@@ -25,7 +25,7 @@ try:
|
||||
if gns3.update_manager.UpdateManager().installDownloadedUpdates():
|
||||
print("Update installed restart the application")
|
||||
python = sys.executable
|
||||
os.execl(python, python, * sys.argv)
|
||||
os.execl(python, *sys.argv)
|
||||
except Exception as e:
|
||||
print("Fail update installation: {}".format(str(e)))
|
||||
|
||||
@@ -45,9 +45,10 @@ import time
|
||||
import locale
|
||||
import argparse
|
||||
import signal
|
||||
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
|
||||
@@ -55,11 +56,14 @@ from gns3.main_window import MainWindow
|
||||
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
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from gns3.version import __version__
|
||||
|
||||
|
||||
@@ -117,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"):
|
||||
@@ -144,6 +144,7 @@ def main():
|
||||
os.environ["PATH"] = os.pathsep.join(frozen_dirs) + os.pathsep + os.environ.get("PATH", "")
|
||||
|
||||
if options.project:
|
||||
options.project = os.path.abspath(options.project)
|
||||
os.chdir(frozen_dir)
|
||||
|
||||
def exceptionHook(exception, value, tb):
|
||||
@@ -153,7 +154,7 @@ def main():
|
||||
|
||||
lines = traceback.format_exception(exception, value, tb)
|
||||
print("****** Exception detected, traceback information saved in {} ******".format(exception_file_path))
|
||||
print("\nPLEASE REPORT ON https://community.gns3.com/community/software/bug\n")
|
||||
print("\nPLEASE REPORT ON https://www.gns3.com\n")
|
||||
print("".join(lines))
|
||||
try:
|
||||
curdate = time.strftime("%d %b %Y %H:%M:%S")
|
||||
@@ -185,18 +186,11 @@ 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 version_string.split('.')]
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.0.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.0.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
|
||||
if version(QtCore.QT_VERSION_STR) < version("4.6"):
|
||||
raise SystemExit("Requirement is Qt version 4.6 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
|
||||
# 4.8.3 because of QSettings (http://pyqt.sourceforge.net/Docs/PyQt4/pyqt_qsettings.html)
|
||||
if DEFAULT_BINDING == "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))
|
||||
if parse_version(psutil.__version__) < parse_version("2.2.1"):
|
||||
raise SystemExit("Requirement is psutil version 2.2.1 or higher, got version {}".format(psutil.__version__))
|
||||
|
||||
# check for the correct locale
|
||||
# (UNIX/Linux only)
|
||||
@@ -228,16 +222,25 @@ def main():
|
||||
except win32console.error as e:
|
||||
print("warning: could not allocate console: {}".format(e))
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
global app
|
||||
app = Application(sys.argv)
|
||||
|
||||
# this info is necessary for QSettings
|
||||
app.setOrganizationName("GNS3")
|
||||
app.setOrganizationDomain("gns3.net")
|
||||
app.setApplicationName("GNS3")
|
||||
app.setApplicationVersion(__version__)
|
||||
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:
|
||||
@@ -246,26 +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)
|
||||
|
||||
mainwindow = MainWindow(options.project)
|
||||
# We disallow to run GNS3 from outside the /Applications folder to avoid
|
||||
# issue when people run GNS3 from the .dmg
|
||||
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
|
||||
if not os.path.realpath(sys.executable).startswith("/Applications"):
|
||||
QtWidgets.QMessageBox.critical(None, "Error", "You need to copy GNS3 in your /Applications folder before using it.")
|
||||
sys.exit(1)
|
||||
|
||||
global mainwindow
|
||||
startup_file = app.open_file_at_startup
|
||||
if not startup_file:
|
||||
startup_file = options.project
|
||||
|
||||
mainwindow = MainWindow(open_file=startup_file)
|
||||
|
||||
# On OSX we can receive the file to open from a system event
|
||||
# loadPath is smart and will load only if a path is present
|
||||
app.file_open_signal.connect(lambda path: mainwindow.loadPath(path))
|
||||
|
||||
# Manage Ctrl + C or kill command
|
||||
def sigint_handler(*args):
|
||||
log.info("Signal received exiting the application")
|
||||
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_()
|
||||
delattr(MainWindow, "_instance")
|
||||
app.deleteLater()
|
||||
|
||||
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()
|
||||
|
||||
|
||||
1461
gns3/main_window.py
1461
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,12 +20,23 @@ Built-in module implementation.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.servers import Servers
|
||||
from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
from .cloud import Cloud
|
||||
from .host import Host
|
||||
from gns3.local_config import LocalConfig
|
||||
|
||||
from ..module import Module
|
||||
from .cloud import Cloud
|
||||
from .nat import Nat
|
||||
from .ethernet_hub import EthernetHub
|
||||
from .ethernet_switch import EthernetSwitch
|
||||
from .frame_relay_switch import FrameRelaySwitch
|
||||
from .atm_switch import ATMSwitch
|
||||
|
||||
from .settings import (
|
||||
BUILTIN_SETTINGS,
|
||||
CLOUD_SETTINGS,
|
||||
NAT_SETTINGS,
|
||||
ETHERNET_HUB_SETTINGS,
|
||||
ETHERNET_SWITCH_SETTINGS
|
||||
)
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -40,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.
|
||||
@@ -69,88 +213,51 @@ class Builtin(Module):
|
||||
Resets the module.
|
||||
"""
|
||||
|
||||
log.info("Built-in module reset")
|
||||
self._nodes.clear()
|
||||
|
||||
def allocateServer(self, node_class):
|
||||
def instantiateNode(self, node_class, server, project):
|
||||
"""
|
||||
Allocates a server.
|
||||
|
||||
:param node_class: Node object
|
||||
|
||||
:returns: allocated server (HTTPClient instance)
|
||||
"""
|
||||
|
||||
# check all other modules to find if they
|
||||
# are using a local server
|
||||
using_local_server = []
|
||||
from gns3.modules import MODULES
|
||||
for module in MODULES:
|
||||
instance = module.instance()
|
||||
if instance != self:
|
||||
module_settings = instance.settings()
|
||||
if "use_local_server" in module_settings:
|
||||
using_local_server.append(module_settings["use_local_server"])
|
||||
|
||||
# allocate a server for the node
|
||||
servers = Servers.instance()
|
||||
local_server = servers.localServer()
|
||||
remote_servers = servers.remoteServers()
|
||||
gns3_vm = Servers.instance().vmServer()
|
||||
|
||||
if not all(using_local_server) and (gns3_vm or len(remote_servers)):
|
||||
# a module is not using a local server
|
||||
|
||||
server_list = ["Local server ({})".format(local_server.url())]
|
||||
if gns3_vm:
|
||||
server_list.append("GNS3 VM ({})".format(gns3_vm.url()))
|
||||
if len(remote_servers):
|
||||
if True not in using_local_server and len(remote_servers) == 1:
|
||||
# no module is using a local server and there is only one
|
||||
# remote server available, so no need to ask the user.
|
||||
return next(iter(servers))
|
||||
for remote_server in remote_servers:
|
||||
server_list.append("{}".format(remote_server))
|
||||
|
||||
# TODO: move this to graphics_view
|
||||
from gns3.main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
(selection, ok) = QtWidgets.QInputDialog.getItem(mainwindow, "Server", "Please choose a server", server_list, 0, False)
|
||||
if ok:
|
||||
if selection.startswith("Local server"):
|
||||
return local_server
|
||||
elif selection.startswith("GNS3 VM"):
|
||||
return gns3_vm
|
||||
else:
|
||||
return remote_servers[selection]
|
||||
else:
|
||||
raise ModuleError("Please select a server")
|
||||
return local_server
|
||||
|
||||
def createNode(self, node_class, server, project):
|
||||
"""
|
||||
Creates a new node.
|
||||
Instantiate a new node.
|
||||
|
||||
:param node_class: Node object
|
||||
:param server: HTTPClient instance
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("creating node {}".format(node_class))
|
||||
|
||||
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):
|
||||
@@ -186,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():
|
||||
"""
|
||||
@@ -194,7 +317,7 @@ class Builtin(Module):
|
||||
:returns: list of classes
|
||||
"""
|
||||
|
||||
return [Cloud, Host]
|
||||
return [Nat, Cloud, EthernetHub, EthernetSwitch, FrameRelaySwitch, ATMSwitch]
|
||||
|
||||
def nodes(self):
|
||||
"""
|
||||
@@ -208,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
|
||||
@@ -218,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,22 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
NIO implementation on the client side (in the form of a pseudo node represented as a cloud).
|
||||
Asynchronously sends JSON messages to the GNS3 server and receives responses with callbacks.
|
||||
"""
|
||||
|
||||
import re
|
||||
from gns3.node import Node
|
||||
from gns3.ports.port import Port
|
||||
from gns3.nios.nio_generic_ethernet import NIOGenericEthernet
|
||||
from gns3.nios.nio_linux_ethernet import NIOLinuxEthernet
|
||||
from gns3.nios.nio_nat import NIONAT
|
||||
from gns3.nios.nio_udp import NIOUDP
|
||||
from gns3.nios.nio_tap import NIOTAP
|
||||
from gns3.nios.nio_unix import NIOUNIX
|
||||
from gns3.nios.nio_vde import NIOVDE
|
||||
from gns3.nios.nio_null import NIONull
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -39,185 +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, initial_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 initial_settings:
|
||||
self._initial_settings = initial_settings
|
||||
|
||||
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._initial_settings and "nios" in self._initial_settings and self._initial_settings["nios"]:
|
||||
self._initial_settings["interfaces"] = {}
|
||||
self.update(self._initial_settings)
|
||||
else:
|
||||
log.info("cloud {} has been created".format(self.name()))
|
||||
self.setInitialized(True)
|
||||
self.created_signal.emit(self.id())
|
||||
|
||||
def _createNIOUDP(self, nio):
|
||||
"""
|
||||
Creates a NIO UDP.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_udp:(\d+):(.+):(\d+)$""", nio)
|
||||
if match:
|
||||
lport = int(match.group(1))
|
||||
rhost = match.group(2)
|
||||
rport = int(match.group(3))
|
||||
return NIOUDP(lport, rhost, rport)
|
||||
return None
|
||||
|
||||
def _createNIOGenericEthernet(self, nio):
|
||||
"""
|
||||
Creates a NIO Generic Ethernet.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_gen_eth:(.+)$""", nio)
|
||||
if match:
|
||||
ethernet_device = match.group(1)
|
||||
return NIOGenericEthernet(ethernet_device)
|
||||
return None
|
||||
|
||||
def _createNIOLinuxEthernet(self, nio):
|
||||
"""
|
||||
Creates a NIO Linux Ethernet.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_gen_linux:(.+)$""", nio)
|
||||
if match:
|
||||
linux_device = match.group(1)
|
||||
return NIOLinuxEthernet(linux_device)
|
||||
return None
|
||||
|
||||
def _createNIONAT(self, nio):
|
||||
"""
|
||||
Creates a NIO NAT.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_nat:(.+)$""", nio)
|
||||
if match:
|
||||
identifier = match.group(1)
|
||||
return NIONAT(identifier)
|
||||
return None
|
||||
|
||||
def _createNIOTAP(self, nio):
|
||||
"""
|
||||
Creates a NIO TAP.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_tap:(.+)$""", nio)
|
||||
if match:
|
||||
tap_device = match.group(1)
|
||||
return NIOTAP(tap_device)
|
||||
return None
|
||||
|
||||
def _createNIOUNIX(self, nio):
|
||||
"""
|
||||
Creates a NIO UNIX.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_unix:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
local_file = match.group(1)
|
||||
remote_file = match.group(2)
|
||||
return NIOUNIX(local_file, remote_file)
|
||||
return None
|
||||
|
||||
def _createNIOVDE(self, nio):
|
||||
"""
|
||||
Creates a NIO VDE.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_vde:(.+):(.+)$""", nio)
|
||||
if match:
|
||||
control_file = match.group(1)
|
||||
local_file = match.group(2)
|
||||
return NIOVDE(control_file, local_file)
|
||||
return None
|
||||
|
||||
def _createNIONull(self, nio):
|
||||
"""
|
||||
Creates a NIO Null.
|
||||
|
||||
:param nio: nio string
|
||||
"""
|
||||
|
||||
match = re.search(r"""^nio_null:(.+)$""", nio)
|
||||
if match:
|
||||
identifier = match.group(1)
|
||||
return NIONull(identifier)
|
||||
return None
|
||||
if "interfaces" in result:
|
||||
self._interfaces = result["interfaces"].copy()
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
@@ -226,74 +79,35 @@ class Cloud(Node):
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
updated = False
|
||||
if "nios" in new_settings:
|
||||
nios = new_settings["nios"]
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
if params:
|
||||
self._update(params)
|
||||
|
||||
# add ports
|
||||
for nio in nios:
|
||||
if nio in self._settings["nios"]:
|
||||
# port already created for this NIO
|
||||
continue
|
||||
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))
|
||||
continue
|
||||
port = Port(nio, nio_object, stub=True)
|
||||
port.setStatus(Port.started)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been added".format(nio))
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Callback for update.
|
||||
|
||||
# delete ports
|
||||
for nio in self._settings["nios"]:
|
||||
if nio not in nios:
|
||||
for port in self._ports.copy():
|
||||
if port.name() == nio:
|
||||
self._ports.remove(port)
|
||||
updated = True
|
||||
log.debug("port {} has been deleted".format(nio))
|
||||
break
|
||||
:param result: server response
|
||||
"""
|
||||
|
||||
self._settings["nios"] = new_settings["nios"].copy()
|
||||
if "ports_mapping" in result:
|
||||
self._settings["ports_mapping"] = result["ports_mapping"].copy()
|
||||
|
||||
if "name" in new_settings and new_settings["name"] != self.name():
|
||||
self._settings["name"] = new_settings["name"]
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
log.info("cloud {} has been updated".format(self.name()))
|
||||
self.updated_signal.emit()
|
||||
|
||||
def deleteNIO(self, port):
|
||||
|
||||
pass
|
||||
if "interfaces" in result:
|
||||
self._interfaces = result["interfaces"].copy()
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Returns information about this cloud.
|
||||
|
||||
:returns: formated string
|
||||
:returns: formatted string
|
||||
"""
|
||||
|
||||
info = """Cloud device {name} is always-on
|
||||
This is a pseudo-device for external connections
|
||||
This is a node for external connections
|
||||
""".format(name=self.name())
|
||||
|
||||
port_info = ""
|
||||
@@ -304,116 +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")
|
||||
self.updated_signal.connect(self._updatePortSettings)
|
||||
log.info("cloud {} is loading".format(name))
|
||||
self._node_info = node_info
|
||||
self.setup(name, settings)
|
||||
|
||||
def _updatePortSettings(self):
|
||||
"""
|
||||
Updates port settings when loading a topology.
|
||||
"""
|
||||
|
||||
self.updated_signal.disconnect(self._updatePortSettings)
|
||||
# update the port with the correct IDs
|
||||
if "ports" in self._node_info:
|
||||
ports = self._node_info["ports"]
|
||||
for topology_port in ports:
|
||||
for port in self._ports:
|
||||
if topology_port["name"] == port.name():
|
||||
port.setId(topology_port["id"])
|
||||
if topology_port["name"].startswith("nio_gen_eth") or topology_port["name"].startswith("nio_linux_eth"):
|
||||
# lookup if the interface exists
|
||||
available_interface = False
|
||||
topology_port_name = topology_port["name"].split(':', 1)[1]
|
||||
for interface in self._settings["interfaces"]:
|
||||
if interface["name"] == topology_port_name:
|
||||
available_interface = True
|
||||
break
|
||||
if not available_interface:
|
||||
alternative_interface = self._module.findAlternativeInterface(self, topology_port_name)
|
||||
if alternative_interface:
|
||||
if topology_port["name"] in self._settings["nios"]:
|
||||
self._settings["nios"].remove(topology_port["name"])
|
||||
topology_port["name"] = topology_port["name"].replace(topology_port_name, alternative_interface)
|
||||
port.setName(topology_port["name"])
|
||||
self._settings["nios"].append(topology_port["name"])
|
||||
|
||||
log.info("cloud {} has been created".format(self.name()))
|
||||
self.setInitialized(True)
|
||||
self.created_signal.emit(self.id())
|
||||
|
||||
def name(self):
|
||||
"""
|
||||
Returns the name of this cloud.
|
||||
|
||||
:returns: name (string)
|
||||
"""
|
||||
|
||||
return self._settings["name"]
|
||||
|
||||
def settings(self):
|
||||
"""
|
||||
Returns all this cloud settings.
|
||||
|
||||
:returns: settings dictionary
|
||||
"""
|
||||
|
||||
return self._settings
|
||||
|
||||
def ports(self):
|
||||
"""
|
||||
Returns all the ports for this cloud.
|
||||
|
||||
:returns: list of Port instances
|
||||
"""
|
||||
|
||||
return self._ports
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
@@ -444,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,109 +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, initial_settings={}):
|
||||
"""
|
||||
Setups this host.
|
||||
|
||||
:param name: optional name for this host
|
||||
"""
|
||||
|
||||
if name:
|
||||
self._settings["name"] = name
|
||||
|
||||
if initial_settings:
|
||||
self._initial_settings = initial_settings
|
||||
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"
|
||||
@@ -20,7 +20,7 @@ Configuration page for Dynamips ATM switches.
|
||||
"""
|
||||
|
||||
import re
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
from ..ui.atm_switch_configuration_page_ui import Ui_atmSwitchConfigPageWidget
|
||||
|
||||
|
||||
@@ -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, QtGui, 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().get("/computes/{}/network/interfaces".format(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, QtGui, 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,16 +39,36 @@ 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)
|
||||
self.uiPortsTreeWidget.itemActivated.connect(self._portSelectedSlot)
|
||||
self.uiPortsTreeWidget.itemSelectionChanged.connect(self._portSelectionChangedSlot)
|
||||
self.uiPortTypeComboBox.currentIndexChanged.connect(self._typeSelectionChangedSlot)
|
||||
|
||||
# enable sorting
|
||||
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.
|
||||
@@ -57,11 +80,19 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
port = int(item.text(0))
|
||||
vlan = int(item.text(1))
|
||||
port_type = item.text(2)
|
||||
port_ethertype = item.text(3)
|
||||
self.uiPortSpinBox.setValue(port)
|
||||
self.uiVlanSpinBox.setValue(vlan)
|
||||
index = self.uiPortTypeComboBox.findText(port_type)
|
||||
if index != -1:
|
||||
self.uiPortTypeComboBox.setCurrentIndex(index)
|
||||
index = self.uiPortEtherTypeComboBox.findText(port_ethertype)
|
||||
if index != -1:
|
||||
self.uiPortEtherTypeComboBox.setCurrentIndex(index)
|
||||
if port_type == "qinq":
|
||||
self.uiPortEtherTypeComboBox.setEnabled(True)
|
||||
else:
|
||||
self.uiPortEtherTypeComboBox.setEnabled(False)
|
||||
|
||||
def _portSelectionChangedSlot(self):
|
||||
"""
|
||||
@@ -74,6 +105,17 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
else:
|
||||
self.uiDeletePushButton.setEnabled(False)
|
||||
|
||||
def _typeSelectionChangedSlot(self):
|
||||
"""
|
||||
Disable Q-in-Q EtherType for access and dot1q ports.
|
||||
"""
|
||||
|
||||
port_type = self.uiPortTypeComboBox.currentText()
|
||||
if port_type == "qinq":
|
||||
self.uiPortEtherTypeComboBox.setEnabled(True)
|
||||
else:
|
||||
self.uiPortEtherTypeComboBox.setEnabled(False)
|
||||
|
||||
def _addPortSlot(self):
|
||||
"""
|
||||
Adds a new port.
|
||||
@@ -82,12 +124,17 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
port = self.uiPortSpinBox.value()
|
||||
vlan = self.uiVlanSpinBox.value()
|
||||
port_type = self.uiPortTypeComboBox.currentText()
|
||||
if port_type == "qinq":
|
||||
port_ethertype = self.uiPortEtherTypeComboBox.currentText()
|
||||
else:
|
||||
port_ethertype = ""
|
||||
|
||||
if port in self._ports:
|
||||
# update a given entry in the tree widget
|
||||
item = self.uiPortsTreeWidget.findItems(str(port), QtCore.Qt.MatchFixedString)[0]
|
||||
item.setText(1, str(vlan))
|
||||
item.setText(2, port_type)
|
||||
item.setText(3, port_ethertype)
|
||||
|
||||
else:
|
||||
# add a new entry in the tree widget
|
||||
@@ -95,10 +142,14 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
item.setText(0, str(port))
|
||||
item.setText(1, str(vlan))
|
||||
item.setText(2, port_type)
|
||||
item.setText(3, port_ethertype)
|
||||
self.uiPortsTreeWidget.addTopLevelItem(item)
|
||||
|
||||
self._ports[port] = {"type": port_type,
|
||||
"vlan": vlan}
|
||||
self._ports[port] = {"name": "Ethernet{}".format(port),
|
||||
"port_number": port,
|
||||
"type": port_type,
|
||||
"vlan": vlan,
|
||||
"ethertype": port_ethertype}
|
||||
|
||||
self.uiPortSpinBox.setValue(max(self._ports) + 1)
|
||||
self.uiPortsTreeWidget.resizeColumnToContents(0)
|
||||
@@ -111,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))
|
||||
|
||||
@@ -124,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.
|
||||
|
||||
@@ -142,20 +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(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.
|
||||
|
||||
@@ -171,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)
|
||||
@@ -19,7 +19,7 @@
|
||||
Configuration page for Dynamips Frame Relay switches.
|
||||
"""
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
from ..ui.frame_relay_switch_configuration_page_ui import Ui_frameRelaySwitchConfigPageWidget
|
||||
|
||||
|
||||
@@ -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": [],
|
||||
}
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/dynamips/ui/atm_switch_configuration_page.ui'
|
||||
#
|
||||
# Created: Wed Jul 15 12:22:32 2015
|
||||
# by: PyQt5 UI code generator 5.4
|
||||
# Created by: PyQt5 UI code generator 5.4.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_atmSwitchConfigPageWidget(object):
|
||||
|
||||
def setupUi(self, atmSwitchConfigPageWidget):
|
||||
atmSwitchConfigPageWidget.setObjectName("atmSwitchConfigPageWidget")
|
||||
atmSwitchConfigPageWidget.resize(459, 419)
|
||||
@@ -185,4 +186,3 @@ class Ui_atmSwitchConfigPageWidget(object):
|
||||
self.uiDestinationPortLabel.setText(_translate("atmSwitchConfigPageWidget", "Port:"))
|
||||
self.uiDestinationVPILabel.setText(_translate("atmSwitchConfigPageWidget", "VPI:"))
|
||||
self.uiDestinationVCILabel.setText(_translate("atmSwitchConfigPageWidget", "VCI:"))
|
||||
|
||||
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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user