Compare commits

...

1477 Commits

Author SHA1 Message Date
Julien Duponchelle
716f65786d 2.0.0 alpha 1 2016-09-29 16:59:35 +02:00
Julien Duponchelle
0c397ba0a8 Retry connection to the server
Fix #1542
2016-09-29 11:38:44 +02:00
Julien Duponchelle
bff8a09147 Change seperator for images directories otherwise bug on windows 2016-09-27 17:09:53 +02:00
Julien Duponchelle
f65ea13c6f Fix tests on Windows 2016-09-27 14:21:59 +02:00
Julien Duponchelle
3975b09898 Add 0.0.0.0 to the list of possible host binding
Fix #1506
2016-09-27 12:03:18 +02:00
Julien Duponchelle
6525d3130c At the end of the setup wizard display add appliance dialog
Fix #1528
2016-09-26 15:45:16 +02:00
Julien Duponchelle
5d8ad83cbe Make first screen of the setup wizard more welcome
Fix #1529
2016-09-23 18:55:11 +02:00
Julien Duponchelle
13a819ee87 Display serial link with a serial line
Fix #1538
2016-09-23 10:36:22 +02:00
grossmj
cd1beb5191 Changes spacer in project dialog. 2016-09-22 22:22:15 -06:00
Julien Duponchelle
483259ba2c Fill cloud interface server side
Fix https://github.com/GNS3/gns3-gui/issues/1535
2016-09-22 17:47:26 +02:00
Julien Duponchelle
3d828c42de Fix tests 2016-09-22 15:55:39 +02:00
Julien Duponchelle
d70dbe82d7 Fix invalid port number for cloud
Fix #1533
2016-09-22 15:44:23 +02:00
Julien Duponchelle
243c5a0f82 Display an error if can't create capture file
Fix #1532
2016-09-22 14:48:43 +02:00
Julien Duponchelle
f4470d8190 Try to improve to speed of Project Dialog with large number of project
Fix #1525
2016-09-22 14:40:58 +02:00
Julien Duponchelle
b435163a3c Prevent a crash in VPCS
Fix #1531
2016-09-22 11:44:47 +02:00
Julien Duponchelle
26f6315b69 Better protection of topologies summary slots
Fix #1526
2016-09-22 11:19:01 +02:00
Julien Duponchelle
b19988784f Merge branch 'master' into 2.0 2016-09-21 17:51:20 +02:00
Julien Duponchelle
5caf576f83 Add vpcus and ram settings for GNS3 VM in preferences pages
Fix #1445
2016-09-21 17:08:15 +02:00
Julien Duponchelle
e61b132c93 Fix a bug in setup wizard when you reduce the ram and increase it 2016-09-21 14:55:51 +02:00
Julien Duponchelle
41b826e9ac Make it clear that asa will not work on Windows 10 2016-09-21 12:34:54 +02:00
Julien Duponchelle
4079f19e25 Repare setup wizard
Ref #1329
2016-09-21 12:01:00 +02:00
Julien Duponchelle
04f108cbdf Fix multi drop
Fix #1524
2016-09-21 10:07:53 +02:00
Julien Duponchelle
e222e3e7c2 Allow to change the size of the scene
https://github.com/GNS3/gns3-server/issues/683
2016-09-21 09:44:13 +02:00
grossmj
cb687205a4 Change text in GNS3 VM preferences. 2016-09-20 20:41:55 -06:00
grossmj
4ef19056bb Merge remote-tracking branch 'origin/master' 2016-09-20 17:46:56 -06:00
grossmj
8a4fb55cdf Changes wording in VM wizards. 2016-09-20 17:46:42 -06:00
Julien Duponchelle
805b573370 Fix port name formatting
Fix #1523
2016-09-20 16:34:19 +02:00
Julien Duponchelle
3f87719d02 Bring back import devices from 1.5
Fix #1411
2016-09-20 10:53:26 +02:00
Julien Duponchelle
2a27bb560c Fix a traceback when dragging an IOS router
Fix #1522
2016-09-20 09:51:39 +02:00
Julien Duponchelle
73dbe68301 Display an error if winpcap or npcap is not installed
Fix https://github.com/GNS3/gns3-server/issues/674
2016-09-20 09:43:50 +02:00
grossmj
c2bd4c8984 Force content margins for tab layouts. 2016-09-19 11:17:23 -06:00
Julien Duponchelle
3021cfb164 Fix crash in cloud 2016-09-18 21:45:36 +02:00
Julien Duponchelle
794f317f83 Fix crash in nat node 2016-09-15 18:42:10 +02:00
Julien Duponchelle
e357581587 Fix Sometimes "Duplicate" put text too far in topology
Fix #1501
2016-09-15 18:36:17 +02:00
Julien Duponchelle
e46b699fcb Custom symbol from gns3a are not working
Fix #1515
2016-09-15 18:02:33 +02:00
Julien Duponchelle
c426ea6e03 Allow blank password for remote controller
Fix #1497
2016-09-15 17:36:58 +02:00
Julien Duponchelle
b0a1fdb65a Fix a crash
Fix #1516
2016-09-15 17:22:16 +02:00
Julien Duponchelle
4515620259 Fix import issue with gns3a
Fix #1507
2016-09-15 17:20:16 +02:00
Julien Duponchelle
090caa967b Do not show progress when reconnect to fail controller
Fix #1496
2016-09-15 16:20:00 +02:00
Julien Duponchelle
e9ac774464 Do not show progress when refresh list of computes
Fix #1498
2016-09-15 16:15:30 +02:00
Julien Duponchelle
de2de45196 Fix crash in Dynamips preferences
Fix #1513
2016-09-15 16:11:54 +02:00
Julien Duponchelle
64dd0e0be3 Display errors about link creation 2016-09-15 14:49:35 +02:00
Julien Duponchelle
6cd5438c89 Fix duplicate in some cases for interfaces
Fix #1502
2016-09-15 11:50:31 +02:00
Julien Duponchelle
df5a09b46d Fix save project as location
Fix #1503
2016-09-14 19:12:28 +02:00
Julien Duponchelle
cba03a7539 Display error when you can't save as 2016-09-14 17:46:32 +02:00
Julien Duponchelle
069d02d908 Fix a crash when deleting an ethernet switch
Fix #1511
2016-09-14 17:22:19 +02:00
Julien Duponchelle
a207ba61de Fix crash in cloud settings 2016-09-14 16:49:11 +02:00
Julien Duponchelle
496db6427d Update node when you release the mouse
Fix #1494
2016-09-14 15:52:26 +02:00
Julien Duponchelle
1bcf6699f0 Get port names from server
Ref #1502, https://github.com/GNS3/gns3-server/issues/676
2016-09-14 15:30:21 +02:00
Julien Duponchelle
fa12721c3c Cleanup dead code around ports 2016-09-14 11:42:07 +02:00
Julien Duponchelle
65ad43431c Allow node are hotplug now 2016-09-14 11:42:07 +02:00
Julien Duponchelle
081544e778 ports => ports_mapping to avoid confusion 2016-09-14 11:42:07 +02:00
Julien Duponchelle
a30daf03d4 Fix Can't remove a slot from dynamips device
Fix #1510
2016-09-14 11:41:22 +02:00
Jeremy Grossmann
f80af230af Merge pull request #1425 from GNS3/colorblind
Support for colorblind people
2016-09-13 14:33:03 -06:00
Julien Duponchelle
71125a4b58 Fix crash in frame relay switch
Fix #1500
2016-09-13 16:09:21 +02:00
Julien Duponchelle
c1dfaf13fb Force icon size in nodes view 2016-09-08 22:25:23 +02:00
Julien Duponchelle
03a3081361 More debug for #1493 2016-09-08 22:02:27 +02:00
Julien Duponchelle
b5715e46d2 Merge branch 'master' into 2.0 2016-09-08 20:56:12 +02:00
Jeremy Grossmann
3aa9ed61c6 Merge pull request #1488 from GNS3/error_bad_version
Display an error if settings come from a more recent version of GNS3
2016-09-08 12:01:14 -06:00
grossmj
0b6cc588b6 Changed sentence. 2016-09-08 12:00:47 -06:00
Julien Duponchelle
ed4af4a8e7 Avoid to be spam by GNS3 VM errors 2016-09-08 19:52:08 +02:00
Julien Duponchelle
c1f6c3ddeb No error is display if refresh the list of GNS3 VM failed in settings
Fix #1492
2016-09-08 19:02:22 +02:00
Julien Duponchelle
d57652815e Support for colorblind people
Fix #1340
2016-09-08 16:22:37 +02:00
Julien Duponchelle
ea3191739b Fix tests 2016-09-08 16:21:39 +02:00
Julien Duponchelle
378b4f973b Display download url for the GNS3 VM in the preferences
Like in the setup wizard

Ref #1489
2016-09-08 16:00:18 +02:00
Julien Duponchelle
6ecbe59011 Suspend the GNS3 VM
Fix https://github.com/GNS3/gns3-server/issues/656
2016-09-08 15:31:42 +02:00
Julien Duponchelle
2fcaa1f6cc Fix crash in server preferences page 2016-09-08 15:02:59 +02:00
Julien Duponchelle
f0a7582fd2 Display an error if settings come from a more recent version of GNS3
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

Fix #1487
2016-09-08 12:54:32 +02:00
Julien Duponchelle
2da076a501 Remove a todo already done in another place of the code 2016-09-08 12:30:35 +02:00
Julien Duponchelle
f31cc4806f Fix error when shutdown local server
Fix https://github.com/GNS3/gns3-server/issues/673
2016-09-08 12:29:15 +02:00
Julien Duponchelle
ae228988a5 Merge branch 'master' into 2.0 2016-09-08 12:16:24 +02:00
Julien Duponchelle
185f0463d4 Fix Error when no GNS3 VM is configured and you click on new Docker or IOU
Fix #1486
2016-09-08 12:14:16 +02:00
Julien Duponchelle
cf668e774b Remove the setting slow_device_start_all
Fix #1485
2016-09-08 10:36:45 +02:00
Julien Duponchelle
b933cdd950 Fix a crash when frozen 2016-09-08 10:21:52 +02:00
Julien Duponchelle
f38d3bdb8e Allow to enable profile selection at startup
Fix #1484
2016-09-07 20:23:27 +02:00
Julien Duponchelle
2c5ed9c884 Disable setup wizard 2016-09-07 18:40:31 +02:00
Julien Duponchelle
f2e457de6c Fix VPCS lost his config
Fix #628
2016-09-07 16:46:24 +02:00
Julien Duponchelle
6b146fc7a7 Process node update events
Fix https://github.com/GNS3/gns3-gui/issues/1482, https://github.com/GNS3/gns3-gui/issues/1483
2016-09-07 15:52:30 +02:00
Julien Duponchelle
25b44a2070 Merge branch 'master' into 2.0 2016-09-07 14:21:12 +02:00
Julien Duponchelle
ac8ef05b06 Disallow / in docker container name
Fix https://github.com/GNS3/gns3-server/issues/668
2016-09-07 14:20:25 +02:00
Julien Duponchelle
663185d93e Add hub, switch and cloud in the new template appliance dialog
Fix #1479
2016-09-07 12:18:37 +02:00
Julien Duponchelle
61ac6ed6e0 Fix crash at the end of the cloud wizard
Fix #1480
2016-09-07 11:38:50 +02:00
Julien Duponchelle
0fa51bcd36 Add a default VPCS node
Fix #1382
2016-09-07 10:50:09 +02:00
Julien Duponchelle
f000425350 Fix After adding a router via the new appliance button the settings are not saved
Fix #1478
2016-09-07 10:22:45 +02:00
Julien Duponchelle
8df07808a9 Fix a crash in IOS template edit 2016-09-07 10:22:27 +02:00
Julien Duponchelle
7f169261d4 Remove --controller 2016-09-07 10:08:37 +02:00
Julien Duponchelle
a86c728a99 Fix error when dragging a node and not default Z
Fix #1477
2016-09-07 09:52:52 +02:00
Julien Duponchelle
11d56d1ea7 Fix can't open two GUI
Fix #1475
2016-09-06 18:44:13 +02:00
Julien Duponchelle
9c52cf0b0b Fix layer is lost & PEP8 cleanup
Fix #1473
2016-09-06 18:17:01 +02:00
Julien Duponchelle
cd96492dff Set a minimum height for item in nodes dock
Fix #1476
2016-09-06 17:57:31 +02:00
Julien Duponchelle
43ab1deb24 Fix position are lost when editing multiple items
Fix #1472
2016-09-06 17:51:54 +02:00
Julien Duponchelle
646bf10017 When multiple node are selected select the group config by default 2016-09-06 16:24:04 +02:00
Julien Duponchelle
6a23874054 Test port name 2016-09-06 15:17:44 +02:00
Julien Duponchelle
b6fe18b975 Fix icon missing in node dock
Fix #1471
2016-09-06 14:48:04 +02:00
Julien Duponchelle
84125fe463 Support right click on VPCS template
Ref #1382
2016-09-06 14:11:53 +02:00
Julien Duponchelle
12732715bd Use the VPCS icon
Ref #1382
2016-09-06 13:31:24 +02:00
Julien Duponchelle
ece0b94ae8 Add VPCS template from the new appliance button
Ref #1382
2016-09-06 13:25:12 +02:00
Julien Duponchelle
84d0532039 Support port name format
Fix  #1400, https://github.com/GNS3/gns3-server/issues/667
2016-09-06 11:46:14 +02:00
Julien Duponchelle
ab68c1f1ab Fix crash with empty config 2016-09-06 10:13:14 +02:00
Julien Duponchelle
b360d8d931 Detection of application outside /Applications more reliable 2016-09-06 09:52:27 +02:00
Julien Duponchelle
f9681f2766 Fix When reloading a topology with Nat we can't link
Fix #1470
2016-09-05 18:54:24 +02:00
Julien Duponchelle
e8a09eef72 Repare console open 2016-09-05 18:08:59 +02:00
Julien Duponchelle
3290639e54 Fix I can't change list of adapters
Fix #1467
2016-09-05 15:27:24 +02:00
Julien Duponchelle
02d3275475 Fix crash when changing IOU symbol
Fix #1463
2016-09-05 14:33:56 +02:00
Julien Duponchelle
fd92049cda Remove the internet VM when migrate to 2.0
Fix https://github.com/GNS3/gns3-server/issues/658
2016-09-05 11:28:51 +02:00
Julien Duponchelle
f48a5655ed Display an error if GNS3 is start from outside /Application
Fix #1468
2016-09-05 09:20:44 +02:00
Julien Duponchelle
22267b4123 Fix a crash when you cancel connection to the controller 2016-09-05 09:08:19 +02:00
Julien Duponchelle
b06230ef3d Try to reconnect to the controller if connection failed
Fix #1459
2016-09-05 08:49:01 +02:00
Bernhard Ehlers
87b37e2839 Update iTerm3 console settings
Fix #1466

Signed-off-by: Julien Duponchelle <julien@gns3.net>
2016-09-03 22:07:45 +02:00
Julien Duponchelle
0f3dd2e05d Merge branch 'master' into 2.0 2016-09-03 21:53:54 +02:00
Julien Duponchelle
0495429df8 Fix rename ethernet switch doesn't release the name
Fix #1460
2016-09-03 21:50:58 +02:00
Julien Duponchelle
2e97f1e037 Fix sometimes console host is missing
Fix #1464
2016-09-02 16:26:52 +02:00
Julien Duponchelle
8133ba61a5 Fix tests 2016-09-02 16:15:47 +02:00
Julien Duponchelle
4cce5cd4ff Fix capture status is incorrect when reconnect to a project
Fix #1464
2016-09-02 15:56:47 +02:00
Julien Duponchelle
1f1f95e3da Fix creation of drawing 2016-09-02 12:02:44 +02:00
Julien Duponchelle
7c9c66470d Support pcap capture on remote controller
Ref #1280
2016-09-02 10:57:06 +02:00
Julien Duponchelle
da5bead39c Fix tests 2016-09-01 16:35:43 +02:00
Julien Duponchelle
68a0c74a1f Improve display of node error (also for GNS3 VM errors)
Fix #1446
2016-09-01 15:38:11 +02:00
Julien Duponchelle
949d1a900a Remove MessageBox it was almost never used 2016-09-01 13:28:34 +02:00
Julien Duponchelle
88c7a472b1 Handle error when controller is not available
Fix #1373
2016-09-01 10:36:01 +02:00
Julien Duponchelle
4b49501630 Fix open project from the CLI
Fix #1456
2016-09-01 10:27:56 +02:00
Julien Duponchelle
9eb71b870a Temporary deactivate setup wizard
Ref #1329
2016-09-01 10:10:09 +02:00
Julien Duponchelle
5d46560427 Allow to select multiple project to delete 2016-08-31 17:18:51 +02:00
Julien Duponchelle
d7856af6db Fix Sometimes node are duplicated
Fix #1455
2016-08-31 16:54:04 +02:00
Julien Duponchelle
c0a093d044 Support overwrite existing project
Fix #1435
2016-08-31 15:38:56 +02:00
Julien Duponchelle
03c7df9dad Improve a lot the speed for parsing notifications
This use a different signal to detect that data is downloaded.
With that, detection of new notification is instant when you
have multiple notifications.

Fix #1447
2016-08-31 11:34:03 +02:00
Julien Duponchelle
d847316914 Merge branch 'master' into 2.0 2016-08-31 09:24:52 +02:00
Julien Duponchelle
190e17b445 Fix tests 2016-08-31 09:23:14 +02:00
Julien Duponchelle
ce5d1b56f0 Support for VNC display number in command line replacement
Fix #1453
2016-08-31 09:20:50 +02:00
Julien Duponchelle
cb7cbc15b3 Merge branch 'master' into 2.0 2016-08-31 09:11:17 +02:00
Julien Duponchelle
dc6756da04 Fix a crash when a directory with image is not accessible at gns3a
import

Fix #1454
2016-08-31 09:10:01 +02:00
Julien Duponchelle
e4eeb437eb Fix the invalid VM was selected in GNS3 VM preferences
Fix #1452
2016-08-30 10:25:32 +02:00
Julien Duponchelle
81b94070ac Remote GNS3 VM support
Fix https://github.com/GNS3/gns3-server/issues/623
2016-08-30 10:19:36 +02:00
Julien Duponchelle
de36a04d88 Server select use the server capabilities to filter what should be
display
2016-08-29 18:20:02 +02:00
Julien Duponchelle
1287b77dfe Watch for compute status change 2016-08-29 17:49:17 +02:00
Julien Duponchelle
146bb004c0 Save preference page only when the page change
Fix #1443
2016-08-26 15:05:42 +02:00
Julien Duponchelle
d26abecea7 Fix Even when you don't modify the preferences the pref dialog say "You have unsaved preferences."
Fix #1443
2016-08-26 15:02:36 +02:00
Julien Duponchelle
d2da14a951 Display name of preferences pages with changes
Ref #1443
2016-08-26 14:34:25 +02:00
Julien Duponchelle
4ae17a1f66 Prevent a crash 2016-08-26 12:14:02 +02:00
Julien Duponchelle
473f60167d Fix crash in settings 2016-08-25 14:19:55 +02:00
Julien Duponchelle
5377590520 Fix tests 2016-08-25 11:17:15 +02:00
Julien Duponchelle
58fe1f6e2b Stop send --host and --port as parameter to the GNS3 server
The server can already get them from config file. And since
we are using the same config file we have no longer risk of sync
issues.
2016-08-25 11:15:50 +02:00
Julien Duponchelle
c0564c89c8 Send GNS3 VM settings from the GUI
Fix #1441
2016-08-25 09:34:05 +02:00
Julien Duponchelle
34d27dc120 Display a settings button in the project dialog page
Fix #1442
2016-08-24 18:53:05 +02:00
Julien Duponchelle
d907af46bc In case of exception on dev machine exit immediately 2016-08-24 18:44:45 +02:00
Julien Duponchelle
a8055471cd Stop logging vmware error when we don't use vmware
Fix #1439
2016-08-23 21:45:39 +02:00
Julien Duponchelle
317c28eaf2 Fix start of the local server 2016-08-23 19:34:24 +02:00
Julien Duponchelle
d2b6aeddbd Display error when not connected to the controller
Ref #1373
2016-08-23 19:02:05 +02:00
Julien Duponchelle
4285d01a19 Fix crash 2016-08-23 18:44:36 +02:00
Julien Duponchelle
802c59a1d1 Hide in project dialog what is not require for remote controller
Fix #1280
2016-08-23 17:09:26 +02:00
Julien Duponchelle
f1879c8d4b Hide menu that should not be visible when using a remote controller
Ref #1280
2016-08-23 16:42:22 +02:00
Julien Duponchelle
97ad294623 Fix isolation issue between settings profil 2016-08-23 16:35:14 +02:00
Julien Duponchelle
6147dcd304 Allow connection to a remote controller
Ref #1280
2016-08-23 16:11:18 +02:00
Julien Duponchelle
d900842363 Merge branch 'profil_support' into 2.0 2016-08-23 10:07:21 +02:00
Julien Duponchelle
e6ac92abb4 Fix error with Qt 5.2
Fix #1431
2016-08-22 19:02:03 +02:00
Julien Duponchelle
944b5fc6c9 Support for profil settings
This PR add a --profil command line arguments.

If set a new set of settings will be create in
~/GNS3/profiles/PROFILENAME

Otherwise it's use the default location.

This allow to test settings without losing
original.

Also with the correct GUI this allow to switch from
local server to a remote (home / office).
2016-08-22 17:27:37 +02:00
Julien Duponchelle
bf6c645281 Only one location in the code for the path of the configuration file 2016-08-22 15:53:43 +02:00
Julien Duponchelle
14f04e5f88 Fix a status sync issue
Fix #1433
2016-08-22 11:19:43 +02:00
grossmj
ecbf7bd661 Do not add fw0 and p2p0 to the interfaces for cloud and nat objects. FIxes #1436. 2016-08-21 21:47:28 -06:00
Jeremy Grossmann
de15edcd0a Merge pull request #1437 from GNS3/nat
A nat node
2016-08-21 21:45:56 -06:00
grossmj
a5d619d6a8 Update some edit dialogs. 2016-08-20 11:31:17 -06:00
grossmj
271811d376 Cosmetic changes. 2016-08-20 11:10:34 -06:00
grossmj
6ed9652a2a Remove setTabBarAutoHide. Ref #1431. 2016-08-20 10:28:37 -06:00
Julien Duponchelle
0d62811a17 A nat node
Fix #599
2016-08-19 20:01:31 +02:00
Julien Duponchelle
615f1f2b5d Topology schema is now on controller 2016-08-19 19:14:12 +02:00
Julien Duponchelle
721bff01b0 Fix restore snapshots when running two clients
Fix #1417
2016-08-19 17:38:55 +02:00
Julien Duponchelle
69f671106c Fix invalid error message on Linux in qemu wizard 2016-08-19 10:08:21 +02:00
Julien Duponchelle
0c59970974 Merge branch 'master' into 2.0 2016-08-18 22:15:32 +02:00
Julien Duponchelle
d161a75ab2 1.5.3dev1 2016-08-18 22:14:58 +02:00
Julien Duponchelle
b87f2b2952 1.5.2 2016-08-18 21:58:17 +02:00
Julien Duponchelle
a0b4c38a44 Fix qemu wizard 2016-08-18 19:39:02 +02:00
Julien Duponchelle
c9cc98ae39 Display the warning about OSX and Windows not recommended for qemu
even when you have only one server.

Fix #1397
2016-08-18 18:56:49 +02:00
Julien Duponchelle
ad492e2b90 Fix sync between GUI and server.conf
Fix #1330
2016-08-18 18:50:30 +02:00
Julien Duponchelle
a977042017 Fix small crashes 2016-08-18 18:30:49 +02:00
Jeremy Grossmann
9e4b5ad02b Merge pull request #1423 from GNS3/vmware_wizard
Make more clear that VMware VM are not ESXi
2016-08-18 09:52:13 -06:00
Julien Duponchelle
90b9b2d29c The delete project button is display only for the project library 2016-08-18 17:27:31 +02:00
Julien Duponchelle
54b6efce59 Fix Save preference is broken
Fix #1421
2016-08-18 17:19:04 +02:00
Julien Duponchelle
1f77a825b3 Fix The wizard for Docker allow to select local on mac and windows
Fix #1406
2016-08-18 16:03:55 +02:00
Julien Duponchelle
283d787c8d Handle error when node can't be create
Fix #1398
2016-08-18 14:49:33 +02:00
Julien Duponchelle
47be4d39c2 Properly handle the error project name duplicate
Fix #1379
2016-08-18 14:28:28 +02:00
Julien Duponchelle
8a4fab9528 Merge branch 'master' into 2.0 2016-08-18 14:18:59 +02:00
Julien Duponchelle
a31586fac9 Fix crash after a bad merge
Fix #1424
2016-08-18 14:12:49 +02:00
Julien Duponchelle
3b88c72778 Cleanup the resources.qrc of references to non existing files 2016-08-18 11:25:55 +02:00
Julien Duponchelle
0e4a5da71a Merge branch 'master' into 2.0 2016-08-18 10:41:35 +02:00
Julien Duponchelle
2a6327b2f2 Make more clear that VMware VM are not ESXi
Fix #1420
2016-08-18 10:29:01 +02:00
Jeremy Grossmann
d4d8adf4ac Merge pull request #1375 from GNS3/qslot
Protect one more method from deleted object
2016-08-17 15:35:19 -06:00
Julien Duponchelle
8492a31dd7 Send label settings to the controller 2016-08-17 17:13:35 +02:00
Julien Duponchelle
bb91402d2c Remove dirty code for sync label position between nodes server is
smarter
2016-08-17 16:12:40 +02:00
Julien Duponchelle
35d8b4f848 Fix you can't quit GNS3 when controller is down 2016-08-17 12:01:21 +02:00
Julien Duponchelle
073dc1afe7 We no longer need to compute node size on client
https://github.com/GNS3/gns3-server/issues/620
2016-08-17 12:00:18 +02:00
Julien Duponchelle
b61e6dabd4 Fix tab display when you open new project 2016-08-16 19:18:56 +02:00
Julien Duponchelle
316fa688c3 Fix to project lost when you open the new blank project dialog 2016-08-16 19:16:36 +02:00
Julien Duponchelle
0724387257 Merge node create and update answer parsing to avoid mistake
This fix the bug where the when project is auto start the node
status was still red.
2016-08-16 15:51:23 +02:00
Julien Duponchelle
8e87c8cdbe Allow to configure auto start/auto close/auto open 2016-08-16 14:07:47 +02:00
Julien Duponchelle
9553d3ba25 Allow to delete a project from the project list
Fix #1419
2016-08-16 13:33:06 +02:00
Julien Duponchelle
4bccd6de25 Add in the project menu the list of recent opened projects
Fix #1410
2016-08-16 11:50:23 +02:00
Julien Duponchelle
dd5bafca0a Add an edit project dialog
For the moment only edit name is supported and sync between
the GUI but the architecure is here.

Ref #1410
2016-08-16 11:12:38 +02:00
Julien Duponchelle
6d3e28226a In telnet console command line replace %c by connection string
Ref #1403
2016-08-15 15:48:48 +02:00
Julien Duponchelle
0fc040773a Fix path for recently opened projects 2016-08-15 15:35:36 +02:00
Julien Duponchelle
1f2294f9bf Correctly support closing project when two client are connected
Fix https://github.com/GNS3/gns3-server/issues/602
2016-08-15 13:38:39 +02:00
Julien Duponchelle
bed3f1d8fa Delete ProjectManager and merge it in Topology
Ref #1326
2016-08-15 12:59:41 +02:00
Julien Duponchelle
b1da0b8279 Merge pull request #1412 from athmane/master
Add AppData and Desktop files for Gnome software support and such
2016-08-15 10:38:06 +02:00
grossmj
32f3137f4d Setup wizard to configure GNS3 VM or Local server. 2016-08-11 16:00:08 -06:00
grossmj
df10bca2c0 Fixes local server and uBridge paths loading. 2016-08-10 16:19:53 -06:00
Athmane Madjoudj
21fba1c4f7 Fix appdata 2016-08-05 17:59:57 +00:00
grossmj
f56e7e8dd8 Remove setTabBarAutoHide property in project dialog. 2016-08-04 10:43:00 -06:00
Athmane Madjoudj
41f6119118 Add AppData and Desktop files 2016-08-02 21:21:51 +00:00
Julien Duponchelle
7732f2a27e Merge branch 'master' into 2.0 2016-07-28 15:13:35 +02:00
Julien Duponchelle
0be4f31162 Fix sample readme 2016-07-28 14:22:24 +02:00
Julien Duponchelle
6bc8428dd0 Fix you can not select the server for VPCS
Fix #1372
2016-07-28 12:48:03 +02:00
Julien Duponchelle
fd92e92a4f Fix error when removing an interface from a cloud
This error was when the interface no longer exist on the system.

Fix #1383
2016-07-28 12:22:18 +02:00
Julien Duponchelle
9dc7a4447b Fix crash when scanning a directory for image and you don't have permission on a file
Fix #1402
2016-07-28 12:17:14 +02:00
Julien Duponchelle
c2a597ffcf Repare edit configuration files 2016-07-28 12:06:47 +02:00
Julien Duponchelle
fc4850afab Fix placeholder symbol 2016-07-28 12:03:08 +02:00
Julien Duponchelle
6c31de36ac Import export 2016-07-27 21:45:13 +02:00
Julien Duponchelle
4082aa8d77 Import config 2016-07-27 21:35:05 +02:00
Julien Duponchelle
71a835ff5f Export configuration 2016-07-27 21:08:22 +02:00
Julien Duponchelle
b156df6fc2 Allow to edit a configuration file from the GUI
Fix #1407
2016-07-27 18:49:49 +02:00
Julien Duponchelle
d077621ee9 Fix a crash when pos is None in ImageItem 2016-07-27 09:38:06 +02:00
Julien Duponchelle
3624502a23 restore snapshots in the GUI 2016-07-26 19:38:29 +02:00
Julien Duponchelle
c9f12fece7 PyQt 5.7 2016-07-26 18:27:36 +02:00
Julien Duponchelle
0f43fd4560 Place holder for loading symbols for nodes
Fix #1377
2016-07-25 19:46:47 +02:00
Julien Duponchelle
1c85f980d3 Fix Transport selection via DSN is deprecated. You should explicitly
pass the transport class to Client()

Fix #1341
2016-07-25 19:33:08 +02:00
Julien Duponchelle
49428d3ead Hide open project field when importing a project in new project dialog 2016-07-25 19:11:17 +02:00
Julien Duponchelle
2f48752ff2 Show a dialog when importing a project 2016-07-25 18:58:55 +02:00
Julien Duponchelle
48ac89abc9 Replace save as by a export / import
Fix #995
2016-07-25 16:17:01 +02:00
Julien Duponchelle
bd8bad5e4c Make sure the README is saved before closing the file editor dialog 2016-07-22 18:16:46 +02:00
Julien Duponchelle
13c189fb00 Merge branch 'master' into 2.0 2016-07-22 18:10:16 +02:00
Julien Duponchelle
eaab3c3f5e Fix example readme for export 2016-07-22 18:07:23 +02:00
Julien Duponchelle
61fb8246f0 Display error during the import to the user 2016-07-22 18:00:23 +02:00
Julien Duponchelle
b09249b384 Support import of .gns3project 2016-07-21 16:21:38 +02:00
Julien Duponchelle
7d6b98766c Repare edit readme
Fix #1401
2016-07-20 21:51:34 +02:00
Julien Duponchelle
96bcf55942 Display export errors 2016-07-20 17:16:59 +02:00
Julien Duponchelle
7bb6078b13 Tmp fix for reloading Vbox topologies
Ref #1400
2016-07-20 16:35:48 +02:00
Julien Duponchelle
c8d6a4640a Fix a crash when reloading some topologies 2016-07-20 16:27:38 +02:00
Julien Duponchelle
27be2b7a1d Fix a crash in VirtualBox wizard 2016-07-20 16:21:28 +02:00
Julien Duponchelle
b05d682aa3 Export project call the controller 2016-07-20 14:15:39 +02:00
Julien Duponchelle
636b26b0e8 Fix for the GNS3VM wizard 2016-07-20 14:00:38 +02:00
Julien Duponchelle
ae2a111536 Fix In the remote server list in preferences do not display the VM server
Fix #1396
2016-07-20 12:19:04 +02:00
Julien Duponchelle
33796a8bd3 Replace project id in open console
Fix #1395
2016-07-20 12:06:59 +02:00
Julien Duponchelle
695e5d3daa Merge the multiple telnet console open code 2016-07-20 12:00:27 +02:00
Julien Duponchelle
9d805d5d42 Avoid a race condition at label update 2016-07-20 11:59:15 +02:00
Julien Duponchelle
ec3fd63138 Fix drawing area not reset when cancel new project
Fix #1394
2016-07-20 11:25:45 +02:00
Julien Duponchelle
8e5e2d4a0c Fix selection of GNS3 VM in the wizard 2016-07-20 11:22:16 +02:00
Julien Duponchelle
ee6e2b41f7 Add Francesco Colista as contributor 2016-07-19 17:09:00 +02:00
Julien Duponchelle
42c54ef02f Fix enable the list of GNS3 VM
Fix #1391
2016-07-19 13:19:21 +02:00
Julien Duponchelle
227cbfc79a Do not add the VM to the list of remotes servers 2016-07-18 18:59:37 +02:00
Julien Duponchelle
0fd5a1a91d Fix crash when you have no routers nodes 2016-07-18 18:37:03 +02:00
Julien Duponchelle
4406c940b5 Merge branch 'master' into 2.0 2016-07-13 17:40:23 +02:00
Julien Duponchelle
8eab44349f Bring back the warning dialog when no router is configured 2016-07-13 17:37:52 +02:00
Julien Duponchelle
f50f7153dc Fix crash at startup 2016-07-13 15:52:22 +02:00
Julien Duponchelle
a994f65d79 Handle error when cloud can't be created 2016-07-12 18:24:24 +02:00
Julien Duponchelle
1a3a17e480 Protect one more method from deleted object
Always the same issue:
http://enki-editor.org/2014/08/23/Pyqt_mem_mgmt.html

I propose the creation of a qslot that we could use to decorate
all slot to protect them.

We can't wrap Signal or inherit from it :( So it's impossible to
do it for all slots without modifications everywhere.

Fix #1374
2016-07-12 10:04:15 +02:00
grossmj
840e4aec54 Load/save GNS3 VM settings on controller side. 2016-07-11 21:43:01 -06:00
grossmj
74b660af61 Basic GNS3 VM configuration using the setup wizard. 2016-07-11 17:45:23 -06:00
grossmj
cc0c56087a Warn users they must start a node to console to it. Fixes #1314. 2016-07-11 10:08:45 -06:00
grossmj
990e6c0eed Allow customizable VPCS templates. Fixes #1306. 2016-07-10 20:33:19 -06:00
grossmj
3bc6cd8b4d Allow ports in cloud templates. Fixes #867. 2016-07-10 14:55:27 -06:00
grossmj
515119e1fa Fixes some imports 2016-07-09 19:02:23 -06:00
Julien Duponchelle
570303273c Fix color losts in drawing items 2016-07-09 11:48:07 +02:00
Julien Duponchelle
1739cc58d4 Support alpha channel for drawings 2016-07-09 11:28:30 +02:00
Julien Duponchelle
b3a7d42f9d Fix rare crash in server summary
Fix #1371
2016-07-08 13:55:47 +02:00
Julien Duponchelle
17ed1f9806 Fix crash during export
Fix #1370
2016-07-08 13:52:41 +02:00
Julien Duponchelle
1e3883674e Fix crash when you cancel the creation of a new project 2016-07-08 11:26:11 +02:00
Julien Duponchelle
bead888c67 1.5.2dev1 2016-07-07 18:53:25 +02:00
Julien Duponchelle
07fcd66d8d 1.5.1 2016-07-07 18:50:45 +02:00
Julien Duponchelle
0f4cac1b76 Try to fix a crash when reseting interface label
Ref #1369
2016-07-07 14:55:08 +02:00
Julien Duponchelle
89fbc537bf Fix At node creation information about the node label formatting is not send
Fix #1366
2016-07-07 10:05:48 +02:00
Jeremy Grossmann
f0ebdf295f Merge pull request #1358 from GNS3/hot_unlink
Stop node before hot unlink
2016-07-07 03:28:01 +00:00
Julien Duponchelle
d396cb911a Fix some issue recently introduce in HTTPClient 2016-07-06 15:43:03 +02:00
Julien Duponchelle
9064487a3e Remove constant that doesn't seem to have an impact
Fix #1345
2016-07-06 15:31:22 +02:00
Julien Duponchelle
8b03f32f95 Fix Open from a project from command line doesn't work bug
Fix #1346
2016-07-06 15:23:56 +02:00
Julien Duponchelle
3295cc514e Merge branch 'master' into 2.0 2016-07-06 14:40:09 +02:00
Julien Duponchelle
c6df492852 Fix a crash with broken file system
Fix #1365
2016-07-06 14:39:34 +02:00
Julien Duponchelle
565c71cb80 Fix UnboundLocalError: local variable 'callback' referenced before assignment
Fix #1364
2016-07-06 10:35:04 +02:00
Julien Duponchelle
30bd710650 Merge branch 'master' into 2.0 2016-07-06 10:33:53 +02:00
Julien Duponchelle
c0dbf95b94 Fix EtherSwitch default name format
Fix #1348
2016-07-06 10:17:20 +02:00
Julien Duponchelle
7f58837111 Fix double call to /version at startup
It's fixe a deeper issue where the http_client open two connection
at the same time

Fix #1363
2016-07-05 18:31:03 +02:00
Julien Duponchelle
53f609c4d7 Make logs less noisy 2016-07-05 18:13:42 +02:00
Julien Duponchelle
26790fd80d Fix When reopen a project the node label could lost his style
Fix #1362
2016-07-05 18:02:47 +02:00
Julien Duponchelle
cfcb24a732 Now the controller take care of deleting links when you delete a node
Fix https://github.com/GNS3/gns3-server/issues/608
2016-07-05 16:29:32 +02:00
Julien Duponchelle
e31746b676 Rebuild server page after merge 2016-07-05 16:29:20 +02:00
Julien Duponchelle
154435d5a5 Merge branch 'master' into 2.0 2016-07-05 15:36:15 +02:00
Julien Duponchelle
d24a0312d8 Fix crash when you have utf-8 char in the README
Fix #1360
2016-07-05 14:54:56 +02:00
Julien Duponchelle
aa5d8b9377 Fix rare crash when creating a link
Fix #1361
2016-07-05 14:51:46 +02:00
Julien Duponchelle
e9703e03cd Stop node before hot unlink
This patch allow to remove any link even if node is already
running by stopping the node before. This solve the issue #1349
until we get a better support in 2.0.

Fix #1349
2016-07-04 19:23:59 +02:00
Julien Duponchelle
13a8d27349 Prevent a crash due to issue in Qt
Fix #1357
2016-07-04 17:30:09 +02:00
Julien Duponchelle
939f8f52c1 Add another security to prevent client to send empty hostname
Fix #1354
2016-07-04 15:10:23 +02:00
Julien Duponchelle
cb1e062f9b Fix rare crash when deleting interface from the cloud
Fix #1352
2016-07-04 14:51:44 +02:00
Julien Duponchelle
a1d1bc5aea Fix rare crash in topology summary view
Fix #1353
2016-07-04 14:49:31 +02:00
Julien Duponchelle
1d81c0521f Ask user to send explanation if they cross a rare error
Fix #1355
2016-07-04 14:37:58 +02:00
Julien Duponchelle
d3ef916b23 Fix rare crash when deleting a node
Fix #1351
2016-07-04 14:15:36 +02:00
Julien Duponchelle
c9b7259cd7 Hotlink support for Docker
Ref https://github.com/GNS3/gns3-server/issues/596
2016-07-04 11:16:17 +02:00
Julien Duponchelle
fa8c135b22 Sync the link between gui 2016-07-01 21:50:05 +02:00
Julien Duponchelle
9d53d806fd Correct center the node label the first time and beginning of interface
label support
2016-07-01 20:14:08 +02:00
Julien Duponchelle
6f499e6c56 Support rotation for labels 2016-07-01 16:54:41 +02:00
Julien Duponchelle
e66bdc936a Use SVG style for labels 2016-07-01 15:26:53 +02:00
Julien Duponchelle
be34e062e7 Bold & italic support in text item 2016-07-01 15:10:09 +02:00
Julien Duponchelle
5664b32cc5 Start to use a svg style property for label and send node witdh and
height
2016-07-01 14:39:51 +02:00
Julien Duponchelle
6bf0ea63d4 Restore label position
Fix #1344
2016-07-01 12:15:23 +02:00
Julien Duponchelle
628970e588 Sync link creation & removal 2016-07-01 11:37:43 +02:00
Julien Duponchelle
fb68ccad15 Fix crash when using two gui 2016-06-30 18:52:19 +02:00
Julien Duponchelle
d6b394500f When updating a node on a GUI change is visible on other GUI 2016-06-30 18:44:57 +02:00
Julien Duponchelle
32508d60b1 Sync node creation and delete
Ref #1250
2016-06-30 16:14:17 +02:00
Julien Duponchelle
0ae23c30c4 Fix Symbol is lost when reloading project
Fix #1342
2016-06-30 15:08:09 +02:00
Julien Duponchelle
e5f18c5e22 Bring back compute node usage
Fix #1261
2016-06-30 10:12:26 +02:00
Julien Duponchelle
df0f25b234 If project creation fail reset the project in the GUI 2016-06-29 17:49:37 +02:00
Julien Duponchelle
dc1d9e59b0 Merge branch '1.5' into 2.0 2016-06-29 17:22:57 +02:00
Julien Duponchelle
266eb77eb5 Fix typo in the a warning dialog
Fix #1338
2016-06-29 17:16:53 +02:00
Julien Duponchelle
c1cac82081 Fix Remote GNS3 VM requires local server
Fix #1319
2016-06-29 17:14:58 +02:00
Julien Duponchelle
323c787d91 Fix AttributeError: 'NoneType' object has no attribute '_server'
Fix #1335
2016-06-29 17:02:30 +02:00
Julien Duponchelle
41070495ba No timeout when importing a .gns3project
Fix #1336
2016-06-29 17:00:23 +02:00
Julien Duponchelle
037e531b22 Send initial config to the controller 2016-06-29 15:17:44 +02:00
Julien Duponchelle
54713b5d68 Support storing list of devices on the remote server
Fix #589
2016-06-29 15:14:18 +02:00
Julien Duponchelle
d94f9a91db Fix a crash in preferences
Fix #1337
2016-06-29 14:41:32 +02:00
Julien Duponchelle
516b8e848f Full symbol on controller support 2016-06-28 22:20:50 +02:00
Julien Duponchelle
6d1d1705b2 Support PNG for symbols 2016-06-28 21:51:54 +02:00
Julien Duponchelle
666a527aa3 Remove symbols not use in interface.
They are available in the controller when you use it
for nodes.
2016-06-28 17:03:35 +02:00
Julien Duponchelle
f296c7fdad Drop ressources not used in the interface 2016-06-28 17:00:48 +02:00
Julien Duponchelle
58e62da913 Script for detecting unused ressources 2016-06-28 16:56:15 +02:00
Julien Duponchelle
ca364d4d56 All place where we display a symbol we use the version from controller 2016-06-28 16:54:32 +02:00
Julien Duponchelle
2232680ded New crash report key 2016-06-27 20:41:07 +02:00
Julien Duponchelle
b662c54a07 Wip symbol from remote 2016-06-27 20:40:34 +02:00
Julien Duponchelle
cd2f897ff2 1.5.1dev1 2016-06-27 20:34:14 +02:00
Julien Duponchelle
c877d4b1d7 1.5.0 2016-06-27 20:30:15 +02:00
Jeremy Grossmann
dc6032aa43 Merge pull request #1333 from GNS3/disallow_export_cloud
Disallow export of project with a cloud
2016-06-27 11:10:21 -06:00
Julien Duponchelle
fec9431ae5 Fix double extension of portable project 2016-06-27 12:47:17 +02:00
Julien Duponchelle
bf1b7e640b Disallow export of project with a cloud
https://github.com/GNS3/gns3-server/issues/590
2016-06-27 12:40:09 +02:00
grossmj
780ab5b14f Docker links are now hot pluggable. 2016-06-24 17:27:35 -06:00
grossmj
7371aebb76 Remove more GNS3VM code from the GUI. Ref #1254. 2016-06-24 15:18:10 -06:00
grossmj
c227f39a03 Allow packet capture on VPCS and Qemu nodes. 2016-06-23 16:52:41 -06:00
Julien Duponchelle
ff794f1578 Use SVG properties instead of style to store info 2016-06-23 18:51:59 +02:00
Julien Duponchelle
f26c342e82 Handle text items in the drawing api 2016-06-23 18:21:20 +02:00
Julien Duponchelle
edacb88ff5 Cleanup topologies test, removed unecessary elments 2016-06-23 13:32:13 +02:00
Julien Duponchelle
4854eac2da Show layer for all drawing items 2016-06-23 13:29:30 +02:00
Julien Duponchelle
60cd105b82 Snap to grid for all drawing items 2016-06-23 13:16:06 +02:00
Julien Duponchelle
184db222c5 Merge branch '1.5' into 2.0 2016-06-23 13:11:07 +02:00
Julien Duponchelle
abcfb9ee12 Handle duplicate of all drawing items 2016-06-23 13:07:45 +02:00
Julien Duponchelle
5f3ba669eb Repare display of symbols 2016-06-23 12:47:29 +02:00
Julien Duponchelle
942d4756c7 Shape => Drawing 2016-06-23 12:26:54 +02:00
Julien Duponchelle
d9b6dfd8d0 Fix test suite 2016-06-23 12:12:11 +02:00
grossmj
ff79e7ad36 Change view grid -> show the grid. 2016-06-22 13:08:20 -06:00
grossmj
ebfdac96ae Check if a link can be removed from a running node. Fixes #1320. 2016-06-22 11:21:13 -06:00
grossmj
e2a85885be Merge remote-tracking branch 'origin/1.5' into 1.5 2016-06-22 10:11:58 -06:00
grossmj
c6b0fb4d65 Hide non implemented console options in general preferences. Ref #1315. 2016-06-22 10:11:42 -06:00
Julien Duponchelle
fb5c4df4db Support sync create of shapes 2016-06-22 18:09:57 +02:00
Julien Duponchelle
9cb4eb775b Send image to the API as SVG element 2016-06-22 17:47:17 +02:00
Julien Duponchelle
8c349e4669 Merge branch '1.5' into 2.0 2016-06-22 14:12:38 +02:00
Bernhard Ehlers
b5d879139a Improve snap to grid
Fix #1327

Signed-off-by: Julien Duponchelle <julien@duponchelle.info>
2016-06-22 13:38:38 +02:00
Bernhard Ehlers
0e05918631 Improve snap to grid
Ref #1327

Signed-off-by: Julien Duponchelle <julien@duponchelle.info>
2016-06-22 11:09:26 +02:00
Julien Duponchelle
c3fee8d323 Change grid color
Ref #1327
2016-06-22 11:09:26 +02:00
Jeremy Grossmann
b5743d9902 Merge pull request #1323 from GNS3/browse_config_directory
A button to open the file browser with the configuration file location
2016-06-21 21:51:46 -06:00
grossmj
44974c04ad Regenerate general preferences Ui. 2016-06-21 21:51:32 -06:00
Julien Duponchelle
336f8d525b Dead code removal 2016-06-21 19:41:26 +02:00
Julien Duponchelle
f76b6afe6a Rotation support for the shapes 2016-06-21 19:39:28 +02:00
Julien Duponchelle
602b58d1df Support z value for shape 2016-06-21 19:23:31 +02:00
Julien Duponchelle
030edccc90 Sync shape delete 2016-06-21 19:16:51 +02:00
Julien Duponchelle
dd3317f4f6 Sync update shape 2016-06-21 19:13:08 +02:00
Julien Duponchelle
f29d0e45b7 Get shapes from the server 2016-06-21 19:03:58 +02:00
Julien Duponchelle
c038ed3db4 Delete shape on remote server 2016-06-21 16:19:00 +02:00
Julien Duponchelle
afcf2a9400 Add a missing email from the crowdfunding 2016-06-21 16:02:20 +02:00
Julien Duponchelle
6bcc4c86e6 Send rectangle and ellipse to the servers 2016-06-21 15:19:29 +02:00
Julien Duponchelle
ded32730bf Fix starting test suites 2016-06-21 09:22:41 +02:00
Julien Duponchelle
2e1b6aef9f Fix a crash 2016-06-20 19:15:25 +02:00
Julien Duponchelle
26d918e218 Avoid a crash with snap to grid and ostinato logo
Fix #1324
2016-06-20 16:22:45 +02:00
Julien Duponchelle
23e1097f89 Add a view grid
Fix #787
2016-06-20 15:19:01 +02:00
Julien Duponchelle
6d1e2d9fab Add Richard Miller to the thanks to 2016-06-20 12:47:27 +02:00
Julien Duponchelle
7c37284901 Merge branch '1.5' into 2.0 2016-06-20 12:45:44 +02:00
Julien Duponchelle
e9384676e1 Fix you can no longer capture if you start stop capture multiple time
Fix #1317
2016-06-20 12:41:22 +02:00
Julien Duponchelle
88bf51c066 Merge branch 'comet0-snap_to_grid' into 1.5 2016-06-20 12:06:24 +02:00
Cometo
fbbe8aff54 Add Snap to Grid to Qt ui file 2016-06-20 10:27:45 +01:00
Julien Duponchelle
e3d441d19f A button to open the file browser with the configuration file location
Fix #1321
2016-06-20 11:16:05 +02:00
Cometo
4f3d20a7c4 Add snap to grid feature 2016-06-20 09:50:24 +01:00
Julien Duponchelle
222ea18bcd Merge branch 'open_project' into 2.0 2016-06-17 15:58:10 +02:00
Julien Duponchelle
df7c91f17f Save as you go
Fix #1243
2016-06-17 15:55:23 +02:00
grossmj
99331fcc54 Try to repair the GNS3 VM support. Ref #1254. 2016-06-16 18:06:51 -06:00
Julien Duponchelle
b59d31855e 1.5.0dev6 2016-06-15 18:57:53 +02:00
Julien Duponchelle
a2211cfa46 1.5.0rc2 2016-06-15 18:54:54 +02:00
Jeremy Grossmann
e9ec42be02 Merge pull request #1307 from GNS3/noplay-patch-1
Add instruction for security issues
2016-06-15 10:45:44 -06:00
Julien Duponchelle
0801d9bf65 Ethernet0 => eth0 for docker
Fix #574
2016-06-15 15:22:37 +02:00
Julien Duponchelle
379b7a56ef Fix tests and a Virtualbox crash 2016-06-15 13:47:59 +02:00
Julien Duponchelle
8e9062c812 Merge branch '1.5' into 2.0 2016-06-15 13:38:31 +02:00
Julien Duponchelle
21e03e8318 Validate appliance schema before loading it
Fix #1311
2016-06-15 12:03:04 +02:00
Julien Duponchelle
fa1b53682c Fix GUI tests 2016-06-15 11:49:32 +02:00
Julien Duponchelle
ec68deb7e4 Drop the commit system
Ref #1243
2016-06-14 10:18:48 +02:00
Julien Duponchelle
1d51f3eed5 Merge branch '1.5' into 2.0 2016-06-14 10:17:42 +02:00
Julien Duponchelle
e8e189d5f3 Drop unused servers 2016-06-13 19:11:00 +02:00
Julien Duponchelle
443e338cc3 Ignore __pycache__ 2016-06-13 17:32:26 +02:00
Julien Duponchelle
9cab049696 Fix a rare crash when loading images
Fix #1299
2016-06-13 16:13:03 +02:00
Julien Duponchelle
e30e869025 Add instruction for security issues 2016-06-13 09:40:23 +02:00
grossmj
b106be2ed5 Removes client side unique node name allocation system. 2016-06-11 16:55:23 -06:00
grossmj
842519d7d0 Merge branch '1.5' into 2.0
Conflicts:
	gns3/main_window.py
	gns3/modules/docker/pages/docker_vm_preferences_page.py
	gns3/modules/dynamips/dialogs/ios_router_wizard.py
	gns3/modules/virtualbox/dialogs/virtualbox_vm_wizard.py
2016-06-10 21:48:57 -06:00
grossmj
d2ff73b579 Fixes doctor failure with 1.5rc1. Fixes #1290. 2016-06-10 21:39:02 -06:00
grossmj
c31d9dfbb2 Check for template name collisions. 2016-06-10 21:36:31 -06:00
grossmj
d7ed734ffb Log GNS3 doctor exceptions. 2016-06-10 21:17:50 -06:00
grossmj
b5a04bfe63 Implements customizable Ethernet hub & switches templates. 2016-06-10 21:02:05 -06:00
grossmj
077e6a110e Implements customizable cloud node templates. Ref #867. 2016-06-10 16:44:53 -06:00
Julien Duponchelle
66d87e8b12 Minor fixes related to IOU
* Fix crash when starting an individual node
* Fix crash when getting project path of the node
2016-06-10 15:34:26 +02:00
Julien Duponchelle
6bf5e7abcc Display HTTP error when no callback is register 2016-06-10 14:47:45 +02:00
Julien Duponchelle
39979a411d Avoid an error when we try to update a node before getting the node id 2016-06-10 14:35:49 +02:00
Julien Duponchelle
fdd5c71711 Fix a crash in IOU wizard 2016-06-10 14:34:26 +02:00
Julien Duponchelle
d6e20fe166 Send changes of label positions to the controller
Fix #1297
2016-06-10 11:24:05 +02:00
Julien Duponchelle
7988b13281 Do not destroy the NodeItem when changing symbol
This allow us to just replace the symbol in place and
notify the server.
2016-06-10 10:57:18 +02:00
Julien Duponchelle
8395865b75 Send X,Y,Z,Label and Symbol to server
We have an issue for the moment to detect change of symbol and
in label
2016-06-10 10:47:55 +02:00
grossmj
c3f33acdb3 Option to hide the new appliance template button. Fixes #1277. 2016-06-09 16:59:47 -06:00
Julien Duponchelle
6d9167c30f Removed the SvgNodeItem because it's was the only sub class of NodeItem 2016-06-09 18:08:22 +02:00
Julien Duponchelle
f8d698aea9 Fix docker VM wizard
Fix #1296
2016-06-08 16:00:36 +02:00
Julien Duponchelle
0cbde5046e Fix GNS3A support
Fix #1274
2016-06-08 15:10:32 +02:00
Julien Duponchelle
7a137a68ae Display human name of compute node in preferences
Fix #1291
2016-06-08 14:33:32 +02:00
Julien Duponchelle
09f7e6ce99 Avoid test side effect on user configuration files 2016-06-08 11:27:14 +02:00
Julien Duponchelle
305cc72485 Fix crash when browsing for local images directory 2016-06-08 11:18:57 +02:00
Julien Duponchelle
9b04901754 Upload missing images is now handled by the controller 2016-06-08 10:13:12 +02:00
Julien Duponchelle
d262f429c4 Fix text when stopping local server 2016-06-08 09:55:13 +02:00
Julien Duponchelle
6bb1223614 Repare IDLE PC in the IOS wizard 2016-06-07 12:04:28 +02:00
Julien Duponchelle
1d97b217cd Repare image upload 2016-06-07 11:39:15 +02:00
Julien Duponchelle
dfe48466e0 Fix list IOS images in the wizard 2016-06-06 16:09:16 +02:00
Julien Duponchelle
6fed45e7a8 Repare local copy for images 2016-06-06 15:56:01 +02:00
Julien Duponchelle
d97b75a3e1 Repare qemu-img create 2016-06-06 15:48:41 +02:00
Julien Duponchelle
87f2e08b3a Merge branch '1.5' into 2.0 2016-06-06 14:30:20 +02:00
Julien Duponchelle
f2c517a4a4 Start fixing qemu wizard 2016-06-02 20:11:36 +02:00
Julien Duponchelle
b8b810cdb1 Repare VMware and VirtualBox support
Fix https://github.com/GNS3/gns3-server/issues/537
2016-06-02 18:43:11 +02:00
Julien Duponchelle
c21900100e Repare Dynamips and IOU preference dialog (without image change) 2016-06-02 17:44:40 +02:00
Julien Duponchelle
50222f5083 Fix save of images search directories
The config file is not a JSON but a INI so we need to export
string.
2016-06-02 14:10:01 +02:00
grossmj
594b596cf9 Revert move "/version" endpoint to "/server/version".
Move "/server/shutdown" endpoint to "/shutdown".
2016-06-01 18:21:07 -06:00
grossmj
3494a4875c Some cleaning + move "/version" endpoint to "/server/version". 2016-06-01 17:50:31 -06:00
Jeremy Grossmann
a5d880e411 Merge pull request #1281 from GNS3/shutdown_server
Shutdown local server via controller
2016-06-01 15:37:52 -06:00
Jeremy Grossmann
e9f445380b Merge pull request #1275 from GNS3/images_directory
UI for settings image directories
2016-06-01 15:32:05 -06:00
grossmj
ea51f15253 Fixes graphical bug when remove interface from the cloud. 2016-06-01 12:48:02 -06:00
Julien Duponchelle
58501c205a 1.5.0dev5 2016-06-01 20:43:16 +02:00
Julien Duponchelle
47f23884b4 1.5.0rc1 2016-06-01 20:39:04 +02:00
grossmj
35ff7fd83e Filter special interfaces in cloud. Fixes #1279. 2016-06-01 11:57:45 -06:00
Julien Duponchelle
1375d7922c Shutdown local server via controller
Fix #1191
2016-06-01 17:22:53 +02:00
Julien Duponchelle
acc0a2ec67 Avoid a segfault when exiting with debug enabled 2016-06-01 16:18:30 +02:00
Julien Duponchelle
eecf1f4a54 Avoid a segfault when exiting with debug enabled 2016-06-01 16:17:53 +02:00
Julien Duponchelle
088d022d5e Fix the GNS3 VM is visible even if deactivated
Fix #1276
2016-06-01 10:58:07 +02:00
grossmj
22fcb14f9a Remove manual console port choice. Fixes #1167. 2016-05-31 21:53:51 -06:00
grossmj
6f2294f9b9 Automatically add ports for the cloud object when creating a new one. 2016-05-31 18:53:38 -06:00
grossmj
d7190b0602 Do not automatically stop the GNS3 VM by default. 2016-05-31 12:34:03 -06:00
grossmj
4cf769e7b6 Block VMnet host traffic by default. Solves the traffic loop issue on Windows. 2016-05-31 11:29:32 -06:00
Julien Duponchelle
3889c8c1fa Fix a crash 2016-05-31 08:34:55 +02:00
grossmj
d122e10703 Small improvements for the packet capture dialog. 2016-05-30 21:51:58 -06:00
grossmj
05f1fa0ecb Drop the host node. 2016-05-30 21:02:03 -06:00
grossmj
3974629e34 Improvements on the cloud interface. Ref #1116.
- Delete multiple interfaces or tunnels at the same time.
- Possibility to filter virtualization interfaces via contextual menu.
2016-05-30 18:56:06 -06:00
grossmj
dfd8147873 Simplify how to count for number of files in a path. 2016-05-30 15:52:26 -06:00
grossmj
bb503d9cc7 Additional checks when adding a binary image path. 2016-05-30 15:41:47 -06:00
Julien Duponchelle
913cb1a3cd UI for settings image directories
Ref https://github.com/GNS3/gns3-server/issues/546
2016-05-30 21:46:41 +02:00
grossmj
7125fb285e Merge remote-tracking branch 'origin/1.5' into 1.5 2016-05-30 11:51:06 -06:00
grossmj
0454868958 Remove tooltip for Qemu VM base mac address. 2016-05-30 11:50:48 -06:00
Julien Duponchelle
8523e3d1a4 Ensure project is not closed twice 2016-05-30 16:06:54 +02:00
Julien Duponchelle
e2415b68d3 Fix a crash when updating local server settings 2016-05-30 14:35:45 +02:00
Julien Duponchelle
0850a3428e Fix Topology Summary view missing link
Fix #1273
2016-05-30 14:33:37 +02:00
Julien Duponchelle
6f44a8b6ee Cleanup dead code around packet capture 2016-05-30 12:33:58 +02:00
Julien Duponchelle
0cc16c232b Fix crash when local server port is already used 2016-05-30 12:06:34 +02:00
Julien Duponchelle
57bd21d346 Merge branch '1.5' into 2.0 2016-05-30 11:41:32 +02:00
Julien Duponchelle
f18e7295bd Fix you cannot select the remote server of your choice in qemu wizard
Fix #1270
2016-05-30 11:12:46 +02:00
Julien Duponchelle
d6a6343aa8 Fix issue when deleting a running container
Fix #1269
2016-05-30 10:27:28 +02:00
grossmj
7397d2da50 Merge branch '1.5' into 2.0
Conflicts:
	gns3/main_window.py
	gns3/modules/builtin/ui/cloud_configuration_page_ui.py
	gns3/modules/vmware/ui/vmware_preferences_page_ui.py
	gns3/modules/vmware/ui/vmware_vm_configuration_page_ui.py
	gns3/node.py
	gns3/version.py
2016-05-29 20:14:08 -06:00
grossmj
f70c457e88 Started to streamline the could. Ref #1116. 2016-05-28 21:49:18 -06:00
grossmj
7750720f4d Allow to block network traffic originating from the host OS for vmnet interfaces (Windows only). 2016-05-28 13:39:21 -06:00
grossmj
950281caa6 Change Qemu VM base mac address tooltip. 2016-05-27 23:00:59 -06:00
grossmj
b5202b5591 Merge remote-tracking branch 'origin/1.5' into 1.5 2016-05-27 22:48:23 -06:00
grossmj
4aa01acce4 Change tooltip for Qemu VM base MAC address. 2016-05-27 22:48:12 -06:00
Julien Duponchelle
c58e788eba Improve image import 2016-05-27 17:49:44 +02:00
Julien Duponchelle
e7b60a1f27 Support dragging an image in the GNS3 topology from the system file
browser
2016-05-27 17:07:22 +02:00
Julien Duponchelle
1e4bbc4ecf Fix an issue with import with no GNS3 VM
Ref #544
2016-05-27 16:04:17 +02:00
Julien Duponchelle
e599da7033 Fix error when using {} in the node name
Fix #1265
2016-05-27 15:42:52 +02:00
Julien Duponchelle
341b5cd947 Display the progress dialog after 250ms
Fix #1262
2016-05-27 15:30:09 +02:00
Julien Duponchelle
fa35f3f9e4 Fix a crash when exporting a project with virtualbox or VMware VM 2016-05-27 10:26:23 +02:00
Julien Duponchelle
1375578b52 Support delete a project from GUI
Fix #822
2016-05-26 11:21:43 +02:00
Julien Duponchelle
1e2326913b Support add a remote compute server 2016-05-26 10:28:17 +02:00
Julien Duponchelle
82c41e09b5 Avoid a crash because VM is not supported in preferences 2016-05-26 10:14:40 +02:00
Julien Duponchelle
16f3b71af4 Support update of computes
Fix #795
2016-05-26 10:09:52 +02:00
grossmj
03373f3cda Add message on GraphicsScene when a project must be created. 2016-05-25 10:04:32 -06:00
Julien Duponchelle
2c9c01b991 Select the correct tab in general preferences 2016-05-25 14:25:33 +02:00
Julien Duponchelle
8a44b6fdb7 Support delete a remote server 2016-05-25 13:40:58 +02:00
Julien Duponchelle
3ceb886ca9 Support all computes informations 2016-05-25 10:34:42 +02:00
Julien Duponchelle
fb3df39263 Remove the auto launch project dialog settings because it's now
always open
2016-05-24 20:04:23 +02:00
Julien Duponchelle
829e8ed745 Drop support for temporary projects
Fix #982
2016-05-24 18:56:56 +02:00
Julien Duponchelle
ed5c52a807 Drop legacy call to /compute/version 2016-05-24 16:32:40 +02:00
Julien Duponchelle
8afc5afadf Less usages of the Servers 2016-05-24 16:31:07 +02:00
Julien Duponchelle
b26401203f VPCS now allow to select the server where the node will run 2016-05-24 15:48:24 +02:00
Julien Duponchelle
c127548dd1 one less Servers references 2016-05-24 15:27:33 +02:00
Julien Duponchelle
f8c1a48350 Tests are green again 2016-05-24 15:23:14 +02:00
Julien Duponchelle
55f634bec3 Remove debug 2016-05-24 11:20:40 +02:00
Julien Duponchelle
8c14e42a09 Merge branch '1.5' into 2.0 2016-05-24 10:16:37 +02:00
grossmj
2e30a96389 Set default VMware VM adapter type to e1000. 2016-05-23 14:14:42 -06:00
Julien Duponchelle
3fc4898904 First step of refactoring Servers 2016-05-23 19:47:52 +02:00
Julien Duponchelle
3561c55174 1.5.0dev4 2016-05-23 15:13:44 +02:00
Julien Duponchelle
5195c647f6 1.5.0 beta 1 2016-05-23 15:05:24 +02:00
Julien Duponchelle
f3a0d1daac Remote server selector not enabled in import appliance wizard
Fix #1258
2016-05-23 15:01:32 +02:00
Julien Duponchelle
dcad6e2d23 New server dialog is now windows modal 2016-05-23 14:57:12 +02:00
grossmj
6a402fe544 First step towards the cloud node re-factoring. 2016-05-22 19:24:14 -06:00
grossmj
310ae5905f More node re-factoring (setup -> create etc.) 2016-05-22 18:23:39 -06:00
grossmj
23aa820cdf Try to reduce node boilerplate code. 2016-05-22 15:57:26 -06:00
grossmj
74f702cea6 Layout change for the capture dialog. 2016-05-22 10:22:43 -06:00
grossmj
52335bddbc Fixes issue when UDPPortAllocatedSlot() is called multiple times. 2016-05-21 19:03:52 -06:00
grossmj
05acf724a8 Private-config is optional. 2016-05-21 18:10:31 -06:00
grossmj
71319a0a7c Fixes alternative IOS image selection when loading a project. 2016-05-21 12:39:36 -06:00
grossmj
c341c55258 Accept fill_color property for rectangle/ellipse objects. Compatibility for old 1.0 projects. 2016-05-21 12:20:01 -06:00
grossmj
cf40e641a6 Fixes check for NPF service and add check for NPCAP service on Windows. 2016-05-20 20:19:28 -06:00
grossmj
7cb6af85a8 Drop VPCS multi host support. Fixes #1252. 2016-05-20 18:18:26 -06:00
grossmj
358ef34918 Fixes base node. 2016-05-20 18:00:46 -06:00
grossmj
a66d194e12 Frame Relay and ATM switches migrated to the new API. 2016-05-20 17:59:59 -06:00
Julien Duponchelle
57b3ce4666 Start to drop Servers class 2016-05-20 13:17:32 +02:00
grossmj
aaa2b6f817 Ethernet hub and Ethernet switch almost fully migrated to the new API. 2016-05-19 22:45:04 -06:00
Julien Duponchelle
97b56e5620 Drop duplicate code already manage by the class VMWizard 2016-05-19 17:47:36 +02:00
Julien Duponchelle
79850176c3 Compute IDLE PC on controller 2016-05-19 16:22:29 +02:00
Julien Duponchelle
8e1896ef5b Support console_host 2016-05-19 14:05:06 +02:00
Julien Duponchelle
8cf911bb15 Merge branch '1.5' into 2.0 2016-05-19 13:34:21 +02:00
Julien Duponchelle
ad0af16fa3 :latest for docker image is managed server side 2016-05-19 13:33:57 +02:00
Julien Duponchelle
2aada61af3 Support update node from another GUI 2016-05-18 20:59:07 +02:00
Julien Duponchelle
66b9b4c68c Fix node not deleted on controller
Fix #1249
2016-05-18 20:45:14 +02:00
Julien Duponchelle
60d6151ce9 Drop unusued code from old cloud servers support 2016-05-18 20:34:24 +02:00
Julien Duponchelle
bfb4b0b9da Merge branch '1.5' into 2.0 2016-05-18 11:38:11 +02:00
Julien Duponchelle
0f00e206bf Remove unbreakable space 2016-05-18 11:27:38 +02:00
grossmj
71536ef9d3 Functional Ethernet hub with new API. 2016-05-17 21:32:57 -06:00
grossmj
354e73b4e7 Fixes project save/load. 2016-05-17 21:32:05 -06:00
Julien Duponchelle
30121e3617 aiohttp_cors 2016-05-17 18:06:19 +02:00
Julien Duponchelle
4d422e716b Support node.updated 2016-05-16 21:50:54 +02:00
Julien Duponchelle
8aa0f8d070 Fix error message for packet capture 2016-05-16 21:50:54 +02:00
grossmj
7c03c0cbcf Fixes #1246. 2016-05-16 10:56:35 -06:00
Julien Duponchelle
c13a4835b2 Test green :) 2016-05-16 17:27:47 +02:00
Julien Duponchelle
412d9b7645 Merge branch '1.5' into 2.0 2016-05-16 16:49:39 +02:00
Julien Duponchelle
27cdaf1ed5 Fix Checkbox and radio button are not readable with charcoal style
Fix #1245
2016-05-16 16:42:42 +02:00
Julien Duponchelle
32e8a45e4e Fix existing remotez server is not recognised
Fix #1240
2016-05-16 15:43:57 +02:00
Julien Duponchelle
34f35aff27 Fix Cannot change docker image adapter number from docker image configuration
Fix #1241
2016-05-16 14:54:48 +02:00
Julien Duponchelle
9b0101321a Fix test 2016-05-16 14:38:39 +02:00
Julien Duponchelle
a4c9487192 Fix got an unexpected keyword argument 'ram_limit'
Fix #1244
2016-05-16 14:37:28 +02:00
Julien Duponchelle
6449973ddc Fix delete node 2016-05-16 14:32:02 +02:00
grossmj
bd9a168667 Base for generic switch nodes. Started to move the Ethernet hub. 2016-05-15 21:02:50 -06:00
grossmj
5c0b03f133 Fixes suspended notification. 2016-05-14 12:24:55 -06:00
grossmj
5708f039c0 Fixes tests. 2016-05-14 12:18:16 -06:00
grossmj
ba0809159c Move most of the project management code its own class. Ref #1224. 2016-05-14 12:15:20 -06:00
grossmj
4aeb4238b2 Move style code to its own class. Ref #1224. 2016-05-14 10:36:30 -06:00
grossmj
119bc8207f Use start/stop/suspend/reload all endpoints. 2016-05-13 20:44:41 -06:00
grossmj
5e7dc27e1f Start, stop, suspend and reload endpoints for all nodes belonging to a project. Fixes #1212. 2016-05-13 19:26:51 -06:00
grossmj
6d3b4db760 Check that both Qt and PyQt version >= 5.6 to enable high DPI scaling. 2016-05-13 18:53:47 -06:00
grossmj
ccba9aa4d5 Update upload endpoints to match with the server. 2016-05-13 18:02:26 -06:00
Julien Duponchelle
47cbc91b02 Fix test 2016-05-13 15:34:06 +02:00
Julien Duponchelle
7a10fa157d Fix show in file manager
Fix #1238
2016-05-12 10:47:58 +02:00
Julien Duponchelle
a27ed4051c Test OK 2016-05-12 09:22:47 +02:00
Julien Duponchelle
dbbde4b098 Rollback some test schema to a previous version of GNS3 2016-05-12 09:00:25 +02:00
grossmj
2f71480849 Check Qt version, not PyQt. Fixes #1232. 2016-05-11 17:46:26 -06:00
grossmj
7e2284e094 Refactoring to use a common node class for all VMs and other (future) objects. 2016-05-11 16:54:55 -06:00
Julien Duponchelle
0ce5c198aa Merge branch '1.5' into 2.0 2016-05-11 10:34:32 +02:00
Julien Duponchelle
c8519188a1 Fix you can not turn off the GNS3VM with remote server
Fix #1235
2016-05-11 10:33:06 +02:00
Julien Duponchelle
bf9f782970 1.5.0dev3 2016-05-11 10:08:36 +02:00
grossmj
72f580efb8 Fixes incomplete merge 2016-05-10 13:31:28 -06:00
Julien Duponchelle
a443e3dcde 1.5.0a2 2016-05-10 19:16:30 +02:00
Julien Duponchelle
5496c6c8af Prepare 1.5.0a2 2016-05-10 19:10:28 +02:00
grossmj
b96d5e765e Minor changes on CHANGELOG 2016-05-10 10:09:47 -06:00
Julien Duponchelle
cee5fb915a Prepare 1.5 alpha 1 2016-05-10 17:58:59 +02:00
Julien Duponchelle
54888ff278 Fix style of table header
Fix #1231
2016-05-10 13:49:57 +02:00
Julien Duponchelle
ab3f3d72ab Removed server from server summary when needed
Fix #1225
2016-05-10 13:42:35 +02:00
Julien Duponchelle
b0eb0d74fb Merge branch '1.5' into 2.0 2016-05-10 10:51:27 +02:00
Julien Duponchelle
8451b4b14e Restore PyQt install from wheel on OSX 2016-05-09 10:40:10 +02:00
Julien Duponchelle
ca85d5e8c0 Merge branch 'master' into 1.5 2016-05-09 10:39:37 +02:00
Julien Duponchelle
9f7cf16335 Change internal timeout never used for clarity 2016-05-09 10:38:23 +02:00
grossmj
e09353b0fe Change some sentences for Docker Ui. 2016-05-08 11:25:16 -06:00
grossmj
56ace4dd31 Improve parse_version 2016-05-07 11:04:09 -06:00
grossmj
3cfd1a0957 Fixes improper use of QThreads for exporting/importing projects.
- Workers cannot have a  parent (moveToThread() will fail if the QObject has a parent and the worker ends up running in the main thread, only a warning message is printed on the console!).
- GUI calls cannot be made from within a thread. It worked before because the worker was running in the main GUI thread.
2016-05-07 10:46:07 -06:00
grossmj
3bd91dc9cb Ignore errors in shutil.rmtree 2016-05-07 10:38:20 -06:00
grossmj
aa805a611a Log aborted requests. 2016-05-06 20:51:39 -06:00
grossmj
b46109a086 Specify "portable project" for .gns3project import/export. 2016-05-06 15:18:12 -06:00
Julien Duponchelle
141b102129 Try to turn off Qt wheel for mac 2016-05-06 12:22:04 +02:00
Julien Duponchelle
2a03953f6c Merge branch 'master' into 1.5 2016-05-06 12:18:44 +02:00
Julien Duponchelle
0ff3bb1a34 Avoid a crash when you send a sigint during final garbage collect
Fix #1217
2016-05-06 12:18:00 +02:00
Julien Duponchelle
45d4c26972 Avoid duplicate in the server list
Fix #1218
2016-05-06 12:14:34 +02:00
grossmj
c05aeffbbb Fixes SetupWizard and NewApplianceDialog calls to PreferencesDialog 2016-05-05 23:35:40 -06:00
Jeremy Grossmann
b37b07bb06 Fixed parse version fail with Qt 5.6.0 string. 2016-05-06 01:11:38 -06:00
grossmj
83bb38b857 Set minimum sizes for spacers in preferences dialog. 2016-05-05 18:56:54 -06:00
grossmj
6ac398f11d Adjustments for the preferences dialog 2016-05-05 17:31:20 -06:00
grossmj
774c210097 Merge branch 'new_appliance_button' into 1.5 2016-05-05 17:01:35 -06:00
grossmj
173aa53cbe Merge branch '1.5' into new_appliance_button
Conflicts:
	gns3/ui/main_window_ui.py
2016-05-05 17:01:16 -06:00
grossmj
be128bc12a Update new appliance dialog for a cleaner interface. 2016-05-05 16:52:29 -06:00
grossmj
305975bb3b Remove pkg_resources.parse_version usage. 2016-05-05 10:11:30 -06:00
grossmj
e6726eb69d Some adjustments for preferences dialog. 2016-05-05 09:45:28 -06:00
Jeremy Grossmann
2988bae855 Merge pull request #1216 from GNS3/gns3vm_init
Always init the GNS3 VM
2016-05-04 16:29:35 -06:00
grossmj
d65e1087f9 .gns3appliance extension is the new default for GNS3 appliance files.
Support .gns3p as an project extension.
2016-05-04 16:22:58 -06:00
Julien Duponchelle
03744a7606 Support HTTP for docker appliances in GNS3A 2016-05-04 18:58:14 +02:00
Julien Duponchelle
65e2a1c8aa Fix crash on custom console 2016-05-04 17:01:54 +02:00
Julien Duponchelle
1e8ef4b208 Always init the GNS3 VM
I'm not sure at 100% but I think we need to
initialize the VM for all cases even we don't need the
autostart

Fix #1215
2016-05-04 10:41:04 +02:00
Julien Duponchelle
2a636481e8 Fix tests 2016-05-04 10:40:47 +02:00
Julien Duponchelle
9efc424462 HTTP support for docker 2016-05-03 16:40:19 +02:00
Julien Duponchelle
ad9db64e8b Mark as stopped a VM if project or VM no longer exists
It's avoid situation where you can not close the client
2016-05-03 11:30:02 +02:00
Julien Duponchelle
0cf04e34c7 Removed right click on docker images in preference list of images
Fix #1209
2016-05-02 16:48:54 +02:00
Julien Duponchelle
f932f96097 Fix name allocation for Docker
Fix #1208
2016-05-02 16:41:42 +02:00
Julien Duponchelle
4c5dac5e13 Merge branch 'master' into 1.5 2016-05-02 15:59:01 +02:00
Julien Duponchelle
abd838de00 Seem to fix crash on fedora 2016-05-02 15:22:47 +02:00
Julien Duponchelle
cd92f69804 Move charcoal CSS to an external file and fix tab color
Fix #1214
2016-05-02 12:19:05 +02:00
Julien Duponchelle
9d4cddb4a0 gns3z => gns3project 2016-05-02 10:25:52 +02:00
grossmj
4f105ced0e Revert "Try to better support HDPI in preferences dialog"
This reverts commit dfaae1df1a.
2016-04-29 18:56:16 -06:00
grossmj
983a69ed5d Revert "Try to scale symbols."
This reverts commit e17b6aa5c0.
2016-04-29 18:55:51 -06:00
grossmj
e17b6aa5c0 Try to scale symbols. 2016-04-29 16:00:42 -06:00
grossmj
c73c302d77 Create Pixmaps for QIcons. 2016-04-29 11:54:25 -06:00
grossmj
bdd40ec59d Drop more PyQt4 related code. Fixes #1203. 2016-04-29 09:05:41 -06:00
Julien Duponchelle
d78064daa6 Drop all PyQt4 code
Fix #1203
2016-04-29 16:40:32 +02:00
Julien Duponchelle
7683f7820f Fix error when checking npf
Fix #1205
2016-04-29 16:17:24 +02:00
Julien Duponchelle
c6b88d1fcd Catch all error in the doctor to avoid one test breaking it 2016-04-29 16:12:16 +02:00
Julien Duponchelle
dfaae1df1a Try to better support HDPI in preferences dialog
Ref #1033
2016-04-29 15:32:48 +02:00
grossmj
58efa8411b Bump version to 1.5.0dev2 2016-04-28 23:47:17 -06:00
grossmj
95f000252b Enabling High Dpi Scaling must be set before creating QApplication. 2016-04-28 23:46:51 -06:00
grossmj
2cf5880940 Enabling High Dpi Scaling is only available in Qt 5.6 and above. 2016-04-28 20:47:40 -06:00
grossmj
88c948f117 Set Fusion style & enable High Dpi Scaling. 2016-04-28 20:42:21 -06:00
Julien Duponchelle
89a369165e 1.4.7dev1 2016-04-28 18:33:52 +02:00
grossmj
9fc53329b5 Merge remote-tracking branch 'origin/1.5' into 1.5 2016-04-28 10:32:42 -06:00
grossmj
8765b7b3bd Tell Qt to generate high-dpi pixmaps. Ref #1033. 2016-04-28 10:32:28 -06:00
Julien Duponchelle
c4710b4bd2 1.4.6 2016-04-28 18:30:47 +02:00
Julien Duponchelle
43bd08a58f PyQt 5.6 is now available as a wheel 2016-04-28 15:20:28 +02:00
Julien Duponchelle
8a78cc2f5e PyWin32 in the requirements 2016-04-28 10:28:26 +02:00
Julien Duponchelle
186429890e Support console_type in GNS3A for docker 2016-04-27 18:00:59 +02:00
Julien Duponchelle
85d9988d79 Fix a typo in qemu preferences
Fix #1200
2016-04-27 15:44:23 +02:00
Julien Duponchelle
25ed2b794d Fix upload of large image to the VM
Fix #1202
2016-04-27 15:42:17 +02:00
Julien Duponchelle
1e3d216961 Capture packet on link instead of port 2016-04-27 11:05:11 +02:00
grossmj
b43a94b3c7 Reduce the number of connection tries from 120 to 40 when connecting the GNS3 server running inside the GNS3 VM. 2016-04-26 20:06:02 -06:00
grossmj
fc5cb3f0ad Update debug messages. 2016-04-26 19:57:34 -06:00
Julien Duponchelle
fc83a9e905 Mac and Win build 2016-04-26 20:46:52 +02:00
grossmj
6635f2f9c1 Include link to the GNS3 academy. Fixes #1178. 2016-04-26 11:42:48 -06:00
Julien Duponchelle
dc054d7e6b New appliance button
Fix #1177
2016-04-26 10:58:36 +02:00
Julien Duponchelle
555d464f8f Regenerate Qt ressources 2016-04-26 10:11:35 +02:00
Julien Duponchelle
c8a8336dc7 Install pyqt via Pypi only for mac & windows 2016-04-26 10:00:17 +02:00
Julien Duponchelle
228c39719d Merge branch 'master' into 1.5 2016-04-26 09:56:41 +02:00
Julien Duponchelle
5207fedd61 Fix NameError: name 'sys' is not defined in VirtualBox preferences
Fix #1197
2016-04-26 09:54:40 +02:00
grossmj
ae9c082cb7 Include environment variables when executing vmrun. Ref #1175. 2016-04-25 18:02:51 -06:00
grossmj
b302f16f65 Snapback feature for port labels. Fixes #1182. 2016-04-25 17:47:45 -06:00
grossmj
bf0bb0519a Prevent users to select VirtualBox.exe instead of VBoxManage.exe. Fixes #1195. 2016-04-25 17:03:46 -06:00
Jeremy Grossmann
c9db57fb7f Merge pull request #1196 from GNS3/improve_vmrun_error
Improve the vmrun error message
2016-04-25 14:10:59 -06:00
Julien Duponchelle
ca0ace0832 Improve the vmrun error message 2016-04-25 21:25:18 +02:00
Julien Duponchelle
32217db357 Merge branch 'master' into 1.5 2016-04-25 17:06:34 +02:00
Julien Duponchelle
a2ddfc5674 Add the server in the list of docker container 2016-04-25 14:05:01 +02:00
Julien Duponchelle
1ae7be4f6a Remove noisy logs 2016-04-21 14:41:41 +02:00
Julien Duponchelle
a418af0aad If we can not read the registry try to guess vmware type from vmrun path 2016-04-21 09:20:58 +02:00
Julien Duponchelle
50a92e9ea0 Merge branch '1.5' into 2.0 2016-04-20 18:02:07 +02:00
Julien Duponchelle
1e63fc14cb Merge branch 'master' into 1.5 2016-04-20 18:01:25 +02:00
Julien Duponchelle
6c00ef65af Ensure that you can not duplicate an interface in a cloud
Fix #1193
2016-04-20 18:00:45 +02:00
Julien Duponchelle
5c29d42d8c Update server summary with server status on controller
Fix #1186
2016-04-20 16:46:18 +02:00
Julien Duponchelle
19055ba004 Try PyQt5.5 has requirement 2016-04-20 14:50:15 +02:00
Julien Duponchelle
0825ae8cb5 Remove .NET from project dialog 2016-04-19 08:50:14 +02:00
Julien Duponchelle
c032c9f458 Update and delete VPCS via controller 2016-04-18 18:56:38 +02:00
Julien Duponchelle
fa5a9621e0 Reload VM on controller 2016-04-18 17:00:53 +02:00
Julien Duponchelle
b2db2cc719 Drop GNS3 converter
Fix #1147
2016-04-18 16:45:05 +02:00
Julien Duponchelle
0f76819936 Drop IOUVM converter
Ref #1147
2016-04-18 16:42:44 +02:00
Julien Duponchelle
66d1597312 Merge branch '1.5' into 2.0 2016-04-18 16:00:28 +02:00
Julien Duponchelle
74f4ae03f3 Disallow docker hot linking 2016-04-18 15:59:17 +02:00
Julien Duponchelle
5894cec3e4 Merge branch 'master' into 1.5 2016-04-18 15:53:07 +02:00
Julien Duponchelle
e66c411989 Dissallow removal of link of running emulator without support of hotlink
Ref https://github.com/GNS3/gns3-server/issues/491
2016-04-18 15:51:44 +02:00
Julien Duponchelle
f74920fd1b Merge branch '1.3' 2016-04-18 10:02:15 +02:00
Julien Duponchelle
c1c98cc7b6 Drop unused modules from 1.3 to avoid build issue on Jenkins 2016-04-18 10:01:22 +02:00
Julien Duponchelle
d217d9a291 Rename hypervisor to compute 2016-04-15 17:56:27 +02:00
Julien Duponchelle
29a73b183c Experimental wheel for PyQt 2016-04-14 14:04:45 +02:00
Julien Duponchelle
79c64f0e38 Start / Stop / Suspend on the controller 2016-04-14 12:23:02 +02:00
Julien Duponchelle
b8a3deeb02 Now each log message with level Error or Warning are display on the
console without the need of using print
2016-04-12 18:54:34 +02:00
Julien Duponchelle
108c774c0f Merge branch '1.5' into 2.0 2016-04-12 17:09:26 +02:00
Julien Duponchelle
830c7556b8 Fix tests 2016-04-12 17:09:06 +02:00
Julien Duponchelle
5470add29a Merge branch '1.5' into 2.0 2016-04-12 17:01:05 +02:00
Julien Duponchelle
f6c9ab0068 Merge branch 'master' into 1.5 2016-04-12 16:27:04 +02:00
Julien Duponchelle
e44b34062c Import / Export images
Fix #1173
2016-04-12 10:31:57 +02:00
Julien Duponchelle
320ae611a1 Check PyQT version support dev version
Signed-off-by: Julien Duponchelle <julien@duponchelle.info>
2016-04-11 15:26:27 +02:00
Julien Duponchelle
e54a87c436 Immedialety use the new GNS3 VM when changing server
Fix #1171
2016-04-08 16:07:35 +02:00
Julien Duponchelle
608cc363a2 Fix issue with aux console and VNC 2016-04-08 14:04:57 +02:00
Jeremy Grossmann
f9609c5871 Merge pull request #1172 from GNS3/readme
Ask for readme at export and menu for editing the readme
2016-04-07 14:43:01 -06:00
grossmj
cc422a6b1d Add edit.svg icon. 2016-04-07 14:41:23 -06:00
grossmj
638d75c388 Small adjustments and icons for each style. 2016-04-07 14:39:15 -06:00
Julien Duponchelle
b02495dd3d Prevent the export of a project not saved
Fix #1169
2016-04-07 18:16:40 +02:00
Julien Duponchelle
90ee8033b0 Add a readme
Fix #1166
2016-04-07 18:00:48 +02:00
Julien Duponchelle
3f5d8fe2a1 Merge branch 'master' into 1.5 2016-04-07 14:44:23 +02:00
Julien Duponchelle
32176d3e2f Show server CPU usage if it's 0
Fix #1168
2016-04-07 14:43:35 +02:00
Julien Duponchelle
c65f55b22a Fix crash when changing docker template name
Fix #1153
2016-04-07 14:40:43 +02:00
Julien Duponchelle
c9d221404b Save AUX port and VNC resolution
Fix #1154, 1165
2016-04-07 14:25:17 +02:00
Julien Duponchelle
ee73961832 Prevent export VMware and VirtualBox VM
Fix #1151
2016-04-07 11:53:36 +02:00
Jeremy Grossmann
ef39c174ed Merge pull request #1160 from GNS3/import_linux
Import improvements
2016-04-06 12:32:52 -06:00
Julien Duponchelle
962d8f77dd Allow to change the symbol on Docker Virtual Machine template
Fix #1161
2016-04-06 17:54:39 +02:00
Julien Duponchelle
bbc7abc50d Fix change number of Docker adapters 2016-04-06 16:10:05 +02:00
Julien Duponchelle
00f1258032 Warn if you try to change the number of adapters of a connected Docker 2016-04-06 16:06:21 +02:00
Julien Duponchelle
beb297967f Remove timeout for docker VM creation since this could be long
Fix #1162
2016-04-06 14:08:03 +02:00
Julien Duponchelle
00c913fd19 Import improvements
* Support import of GNS3Z on Linux https://github.com/GNS3/gns3-server/pull/481
* Warn if GN3VM is require Fix #1158
2016-04-06 12:06:37 +02:00
Julien Duponchelle
a38a8c4ba4 Server make a bigger part of the import
Fix #1156
2016-04-05 18:50:14 +02:00
Julien Duponchelle
56fafba8e9 Hide the text about network config for docker when editing template 2016-04-05 16:29:59 +02:00
grossmj
d0396b3da9 Clear warnings about using linked clones with VMware Player. 2016-04-04 12:10:48 -06:00
grossmj
180eaa2ce5 Add import/export icon files. 2016-04-03 12:16:22 -06:00
grossmj
d8de60afb9 Import/export icons for classic and charcoal styles. 2016-04-03 12:15:13 -06:00
Jeremy Grossmann
d5248e8472 Merge pull request #1150 from GNS3/import_export
Import export
2016-04-02 12:53:10 -06:00
Jeremy Grossmann
a7c199b195 Merge pull request #1152 from GNS3/add_server_dialog
Add server dialog box
2016-04-02 12:33:23 -06:00
grossmj
97a5351a52 Add a checkbox to enable or disable authentication when adding a new remote server. 2016-04-02 12:30:38 -06:00
grossmj
e0b4452007 Fixes local server warning message when settings are changed. 2016-04-02 12:04:03 -06:00
Julien Duponchelle
2e4a532b3c Add server dialog box 2016-04-01 16:21:07 +02:00
Julien Duponchelle
e54266d3a5 Import portable project
Fix #476
2016-04-01 11:17:20 +02:00
Julien Duponchelle
422ed0a5e2 Export portable projects
* Licence for zipstream

Ref #476
2016-04-01 11:16:33 +02:00
grossmj
59e17738cc Warnings about using VirtualBox to run the GNS3 VM. Ref #1044. 2016-03-30 14:00:14 -06:00
grossmj
50644cf3c4 Warning when adding a legacy ASA VM from Qemu Wizard.
Remove other hardcoded templates. Ref #1044.
2016-03-30 13:51:49 -06:00
Jeremy Grossmann
4a3ceb710d Merge pull request #1145 from GNS3/fix_topology_server
Ask user to replace a remote server not found when loading a project.
2016-03-29 15:49:29 -06:00
grossmj
5ab640c380 Minor changes for PR #1145. 2016-03-29 15:42:23 -06:00
grossmj
ba1afca4dd Allow right click on link capture symbol. Fixes #1137. 2016-03-29 13:48:23 -06:00
grossmj
4d4ffdb86c Resize link capture symbol. Ref #1137. 2016-03-29 13:08:40 -06:00
Jeremy Grossmann
0c6002a861 Merge pull request #1146 from GNS3/double_click_topology_view
Double click on an element in topology summary center the view on it
2016-03-29 11:28:47 -06:00
Julien Duponchelle
3ebdd8da14 Do not expand list of link when we double click on a node 2016-03-29 19:17:11 +02:00
Julien Duponchelle
31eb689635 Double click center on link 2016-03-29 19:11:51 +02:00
Julien Duponchelle
c3d66f243a Double click on an element in topology summary center the view on it
Fix #1060
2016-03-29 16:59:59 +02:00
Julien Duponchelle
5c108635d0 If the server doesn't exists in settings we ask to replace it
The user can refuse that and we will continue to create the server
from the topology. It's use the standard server list dialog to keep
code simple.

Fix #1144
2016-03-29 10:03:47 +02:00
Julien Duponchelle
365808eff2 Merge branch 'master' into 1.5 2016-03-29 09:20:18 +02:00
grossmj
5c654e99e4 Hardcoded port for the local GNS3 VM server. Ref #957. 2016-03-28 17:06:10 -06:00
grossmj
709c47d40d Removing old code. 2016-03-28 16:58:39 -06:00
grossmj
8ff8fb9c92 Add warning about using c1700 and c2600 IOS images. Ref #1044.
Modify warning about running Qemu on Windows/OSX.
2016-03-28 12:42:40 -06:00
grossmj
e3cdc5d3ff Merge remote-tracking branch 'origin/1.5' into 1.5 2016-03-28 11:01:35 -06:00
grossmj
69851d1596 Remove old load-balancing code. Fixes #1142. 2016-03-28 11:01:21 -06:00
Julien Duponchelle
c5330246b1 Restore old port in IOUVM converter 2016-03-28 16:58:48 +02:00
Julien Duponchelle
0f2b46b56a Fix a very very rare crash when closing a project
Fix #1141
2016-03-28 16:53:33 +02:00
grossmj
6580ea5891 Improvements for the preferences dialog (splitter layouts, save/restore geometry etc.) 2016-03-27 12:16:05 -06:00
grossmj
1cfd5ae4f0 Allow user/password for HTTP. 2016-03-26 11:43:13 -06:00
grossmj
339beeabaf Fixes bug when switching from a remote GNS3 VM to a VMware local GNS3 VM. 2016-03-25 19:35:31 -06:00
grossmj
3a4b9e2e31 Change layout for GNS3 VM settings in preferences. 2016-03-25 19:16:23 -06:00
grossmj
3055eeaa4f Allow to use a remote server as the GNS3 VM. Ref #957. 2016-03-25 19:02:45 -06:00
grossmj
e517fa6000 Remove load-balancing support. Fixes #1073. 2016-03-25 15:37:20 -06:00
grossmj
3946ebcb92 New console port range 5000 to 10000. 2016-03-25 10:32:04 -06:00
Julien Duponchelle
a34fa04e4f Refresh faster the progress dialog 2016-03-25 15:47:33 +01:00
Julien Duponchelle
2108f3209d Merge branch 'master' into 1.5 2016-03-25 15:45:49 +01:00
Julien Duponchelle
8d2ae5e254 Avoid a small blink of the waiting text 2016-03-25 15:45:22 +01:00
Julien Duponchelle
5092bc571d Change default port to 3080
Ref https://github.com/GNS3/gns3-server/issues/457
2016-03-25 15:37:38 +01:00
Julien Duponchelle
b013e8af50 Edit /etc/network/interfaces 2016-03-25 14:45:00 +01:00
Julien Duponchelle
3af8c4d28f Fix a crash with image item 2016-03-25 12:08:54 +01:00
grossmj
1f1860e53c Show a symbol in the middle of the link when packet capturing is activated. Ref #789. 2016-03-24 19:41:31 -06:00
grossmj
21015bccb5 GNS3 doctor: check if the NPF service is running. Fixes #1124. 2016-03-24 16:44:09 -06:00
grossmj
b27fabea12 Fixes progress dialog is None in accept() 2016-03-24 11:43:53 -06:00
Julien Duponchelle
932c708538 Merge branch 'master' into 1.5 2016-03-24 17:50:16 +01:00
Julien Duponchelle
adf241c146 Fix another race conditions in progress dialog
Fix #1135
2016-03-24 17:44:31 +01:00
Julien Duponchelle
b27a62c625 Fix Aux console lost after merge 2016-03-24 17:13:30 +01:00
Julien Duponchelle
132596a17e Replace the installation instructions by a link to the doc 2016-03-24 13:26:47 +01:00
Julien Duponchelle
f0359dcde9 Add busybox licence
Ref https://github.com/GNS3/gns3-server/pull/460
2016-03-24 13:19:47 +01:00
Julien Duponchelle
08a005b271 Remove libcloud and requests licence since they are not used 2016-03-24 13:15:09 +01:00
Julien Duponchelle
1b873acd72 2.0.0dev1 2016-03-24 10:40:07 +01:00
Julien Duponchelle
a76ac9b5e3 Merge branch '1.5' into 2.0 2016-03-24 10:39:48 +01:00
Julien Duponchelle
f7fa47026c 1.4.6dev1 2016-03-23 18:22:00 +01:00
Julien Duponchelle
36c3fe6a27 1.4.5 2016-03-23 18:17:37 +01:00
grossmj
7c67f08362 Change some sentences. 2016-03-23 10:42:37 -06:00
Jeremy Grossmann
5d71a828c4 Merge pull request #1133 from GNS3/sort_snapshots
Sort snapshots by date
2016-03-23 10:22:38 -06:00
Julien Duponchelle
3e9392b4b7 Sort snapshots by date
Fix #1036
2016-03-23 17:15:43 +01:00
Jeremy Grossmann
02d9d7c22c Merge pull request #1131 from GNS3/block_save_running
Block save and snapshot when a device is running
2016-03-23 10:04:00 -06:00
Jeremy Grossmann
8d60c65e5b Merge pull request #1129 from GNS3/avoid_blink
This should avoid blinking dialog. And display better progress
2016-03-23 10:01:42 -06:00
Jeremy Grossmann
0418cd0a95 Merge pull request #1130 from GNS3/upload_size
Display upload size during progress
2016-03-23 09:52:40 -06:00
Julien Duponchelle
a464295e5b Block save and snapshot when a device is running
Fix #1094
2016-03-23 16:51:02 +01:00
Jeremy Grossmann
95ec16fa92 Merge pull request #1132 from GNS3/enter_new_project
If you hit enter in the new project dialog it's work
2016-03-23 09:38:45 -06:00
Julien Duponchelle
2bd43cdc62 If you hit enter in the new project dialog it's work
Fix #1110
2016-03-23 16:11:34 +01:00
Julien Duponchelle
37e7222371 Display upload size during progress
Fix #1121
2016-03-23 15:27:44 +01:00
Julien Duponchelle
254c766883 This should avoid blinking dialog. And display better progress
Fix #925
2016-03-23 10:55:33 +01:00
Julien Duponchelle
e82a8ad63e Increase default timeout to avoid issue with Idle PC (240 seconds) 2016-03-23 10:54:18 +01:00
grossmj
39b4b233c9 SetupWizard: limit the number of vCPUs for the GNS3 VM to the number of physical cores. 2016-03-22 18:25:18 -06:00
grossmj
3e8208a117 Remove blocking code. Ref #1109. 2016-03-22 15:07:21 -06:00
grossmj
fbc7fd1de3 Bump version to 1.4.5dev2 2016-03-22 14:43:47 -06:00
grossmj
d533733d4b Fixes "QThread: Destroyed while thread is still running", 2016-03-22 14:42:38 -06:00
Jeremy Grossmann
082716e21a Merge pull request #1125 from GNS3/http_timeout
Add a timeout when you are not able to join the remote server
2016-03-22 13:44:51 -06:00
Julien Duponchelle
5ffdecab9e Add a timeout when you are not able to join the remote server
Fix #1119, #1123
2016-03-22 17:53:05 +01:00
Jeremy Grossmann
62c87b4f87 Merge pull request #1122 from GNS3/progress_dialog_improvement
Remove bad smell from progress dialog and handle ESC key
2016-03-22 10:52:59 -06:00
Julien Duponchelle
ab3a50f22f Remove bad smell from progress dialog and handle ESC key
Fix #1120
2016-03-22 17:18:00 +01:00
grossmj
71d3e8dd04 Remove root required messages in cloud node. Ref #608. 2016-03-19 22:32:12 -06:00
grossmj
aa8bbc32c5 Show a warning when the GUI is run with root rights. Fixes #608. 2016-03-19 22:23:08 -06:00
Jeremy Grossmann
05646c03cc Merge pull request #1114 from GNS3/close_running
Ask the user to stop device before closing
2016-03-19 11:27:05 -06:00
grossmj
1453b30e41 Change message when closing GNS3 with running device. 2016-03-19 11:26:15 -06:00
Julien Duponchelle
4f575fda73 Cleanup 2016-03-18 17:04:48 +01:00
Julien Duponchelle
14b6c70f47 Consume the global notification stream 2016-03-18 17:03:41 +01:00
grossmj
07144659b1 Merge remote-tracking branch 'origin/close_running' into close_running
Conflicts:
	gns3/main_window.py
2016-03-15 17:43:05 -06:00
Julien Duponchelle
69b7eb43f6 Fix closing 2016-03-15 21:13:49 +01:00
Julien Duponchelle
2a8b59b79a Ask the user to stop device before closing
This should avoid problem with ghost process remaining.
2016-03-15 21:06:00 +01:00
Julien Duponchelle
8f4e9ac48f Fix duplicate initial VM with network V2 2016-03-15 15:31:39 +01:00
Julien Duponchelle
30069e719b Success to add a dynamips with api V2 2016-03-15 10:45:32 +01:00
Jeremy Grossmann
71fa0dff4b Merge pull request #1115 from GNS3/check_other_gui
At startup display a warning if another GUI is already running
2016-03-14 16:53:38 -06:00
Julien Duponchelle
40f3a78795 Support delete link 2016-03-14 20:52:05 +01:00
Julien Duponchelle
0d11c71bb7 Create a link between two VPCS work 2016-03-14 17:05:16 +01:00
Julien Duponchelle
b58abf2a5c At startup display a warning if another GUI is already running
Fix #1088
2016-03-14 10:11:53 +01:00
Julien Duponchelle
f7911701b1 Ask the user to stop device before closing
This should avoid problem with ghost process remaining.
2016-03-14 10:11:36 +01:00
Julien Duponchelle
f9d4b58588 Fix test on start local servers 2016-03-14 10:11:15 +01:00
grossmj
1269aa273b Fixes #1105. 2016-03-13 12:03:58 -06:00
Julien Duponchelle
758480dd5f Remove /controller from the V2 call 2016-03-11 17:52:06 +01:00
Julien Duponchelle
2ca84501ba Create VPCS VM on controller 2016-03-11 15:06:39 +01:00
Julien Duponchelle
f9756e0977 Close project via controller 2016-03-10 10:57:34 +01:00
Julien Duponchelle
8894c26748 Check if server process correctly start
Fix #1109
2016-03-10 09:45:27 +01:00
Julien Duponchelle
9bb7e3a541 Merge branch 'master' into 1.5 2016-03-10 09:20:41 +01:00
Julien Duponchelle
235cba5ba5 Fix a crash if you delete a file while refreshing the list of appliances
Fix #1108
2016-03-10 09:17:49 +01:00
Julien Duponchelle
6c04b3936a Fix double opening of serial console
Fix #1107
2016-03-10 09:11:10 +01:00
Julien Duponchelle
31ba460553 Create project via controller 2016-03-10 09:07:30 +01:00
Julien Duponchelle
57f519db65 API V2 support 2016-03-08 12:07:11 +01:00
Julien Duponchelle
edf6c65e38 Register server on the controller when added 2016-03-04 08:32:54 +01:00
Julien Duponchelle
349cf1981a Split HTTPClient in two parts
The HTTP client class is responsible to make HTTP calls to
gns3 API server (controller or standard server).

Server is a GNS3 Server. In the transition period the server
will call the HttpClient of the GNS3 controller of the HttpClient
of the server depending of the endpoint.
2016-03-03 15:54:55 +01:00
Julien Duponchelle
a15635d953 Typo 2016-03-02 14:03:02 +01:00
Julien Duponchelle
04d9f3808b Tests for the docker support 2016-03-02 10:03:56 +01:00
Julien Duponchelle
494724c795 Start the local server with --controller
Ref https://github.com/GNS3/gns3-server/issues/417
2016-03-02 09:52:55 +01:00
Julien Duponchelle
71cadad05a Fix a crash when device as no console type
Fix #1096
2016-03-01 18:48:04 +01:00
Julien Duponchelle
30d204dddc Support aux for docker in save 2016-03-01 18:46:43 +01:00
Julien Duponchelle
cc19748fd2 Support aux console for Docker
Fix #1039
2016-03-01 15:17:40 +01:00
Julien Duponchelle
48f197b7ea VNC support for Docker
Fix #947
2016-02-29 23:07:47 +01:00
Julien Duponchelle
99b0ab5f50 Always ask the server for builtin
Fix #1084
2016-02-25 14:38:33 +01:00
Julien Duponchelle
db02b4443b Improve detection of vmrun on OSX
Ref https://github.com/GNS3/gns3-server/issues/435
2016-02-25 11:55:24 +01:00
Julien Duponchelle
8e35500269 Delete image from images dir when no longer need
Fix #1079
2016-02-25 11:21:58 +01:00
Julien Duponchelle
74628642ad Sort node name in topology summary
Fix #1059
2016-02-25 10:07:36 +01:00
Julien Duponchelle
f41edf284c Allow to show a message box for test without starting GNS3
https://gns3.com/discussions/regarding-the-gns3-vm-issues-wit
2016-02-24 09:00:53 +01:00
Julien Duponchelle
f740fde834 Fix crash when editing a Docker node
Fix #1080
2016-02-23 17:43:26 +01:00
Julien Duponchelle
db35c28607 Drop licence for paramiko since we no longer use it 2016-02-23 17:16:55 +01:00
Julien Duponchelle
b1aae4a85a Merge branch 'master' into 1.5 2016-02-23 15:43:29 +01:00
Julien Duponchelle
f4435c255c 1.4.5dev1 2016-02-23 15:41:04 +01:00
Julien Duponchelle
5aaec02af0 1.4.4 2016-02-23 14:36:35 +01:00
Julien Duponchelle
7f4b3edd84 Merge branch 'master' into 1.5 2016-02-23 09:56:52 +01:00
Julien Duponchelle
14cc7fcfeb Fix a conflict 2016-02-23 09:56:22 +01:00
Julien Duponchelle
95c0afd5dd Fix crash when selecting no image in GNS3A but clicking on Download 2016-02-22 21:05:46 +01:00
Julien Duponchelle
306ea31f0b Fix crash when you have a file size None (testing a new gns3a)
Fix #1074
2016-02-22 17:30:00 +01:00
Julien Duponchelle
18b7989e03 1.4.4dev4 2016-02-22 13:06:32 +01:00
Julien Duponchelle
559eef594e Restore the code split between vmware and vbox
Since it doesn't seem to be the problem I split again the code
2016-02-22 13:05:41 +01:00
Julien Duponchelle
7e6d2c6586 Prevent the progress dialog to cancel the GNS3VM when it's finish 2016-02-22 12:53:20 +01:00
Julien Duponchelle
9fd22a92ac Add a command show gns3vm to get the GNS3 VM statusM 2016-02-22 12:52:40 +01:00
Julien Duponchelle
e9470f4c94 Prevent setup wizard to appear if VM is running 2016-02-22 12:45:06 +01:00
Julien Duponchelle
cceb4bb324 Display error dialog if a custom console is invalid
Fix #1072
2016-02-22 11:26:35 +01:00
Julien Duponchelle
f14b4f4429 Crash when you import GNS3A just after installing gns3 Fix #1063 2016-02-22 11:23:11 +01:00
Julien Duponchelle
23db719c36 Change the way we check is setup wizard has been turned off Fix #1071 2016-02-22 11:20:55 +01:00
Julien Duponchelle
8b88a17836 1.4.4dev3 2016-02-22 10:43:28 +01:00
Julien Duponchelle
f1599d6e69 Do not failed if GNS3 VM server has an incorrect version
It's incorrect because we check it later in the HTTP code
and this can lead to half configured VM.
2016-02-21 20:57:05 +01:00
grossmj
df0c72ab0a Include the output from vmrun or VBoxManage when they return an error code. 2016-02-20 20:23:40 -07:00
grossmj
c68395549f Bump version to 1.4.4dev2 2016-02-20 18:41:38 -07:00
grossmj
d69addc3af Fixes bug that forced the GNS3 VM running in VirtualBox to restart even if no preferences had been changed. 2016-02-20 18:41:05 -07:00
grossmj
d96e67e850 Restore some of the changes made to WaitForVMWorker after 1.4.1 to debug an issue. 2016-02-20 18:39:15 -07:00
grossmj
321d9f376e Merge remote-tracking branch 'origin/master' 2016-02-20 11:29:14 -07:00
grossmj
e3450352c5 Restore WaitForVMWorker to the 1.4.1 state to debug an issue. 2016-02-20 11:28:45 -07:00
Jeremy Grossmann
4ba8be67fe Merge pull request #1062 from GNS3/ask_upgrade
Ask user to upgrade via the VM menu
2016-02-20 00:38:53 -07:00
grossmj
abff997179 Allows to cancel the progress dialog when GNS3 tries to contact the server running in the GNS3 VM. 2016-02-20 00:26:32 -07:00
grossmj
55216e1de7 1.4.4dev1 2016-02-19 16:02:51 -07:00
Julien Duponchelle
194f3352a1 1.4.3 2016-02-19 22:16:16 +01:00
Julien Duponchelle
9e0ae5dc96 Merge branch 'master' into 1.5 2016-02-19 19:59:44 +01:00
Julien Duponchelle
ba88fd5306 Allow to configure the type of device for docker container
Fix #1052
2016-02-19 16:41:15 +01:00
Julien Duponchelle
179529214b Ask user to upgrade via the VM menu
Fix #1053
2016-02-19 15:59:45 +01:00
Julien Duponchelle
b27a7a1c31 Allow idlepc 0x0 in topology
Also improve the json schema error reporting
2016-02-19 15:53:21 +01:00
grossmj
1a5b5327dc Show an explicit error message when status code 0 is returned. Fixes #1034. 2016-02-18 16:52:57 -07:00
grossmj
d30ac79a77 Fixes minor bug when dropping a VirtualBox VM on the scene. Fixes #748. 2016-02-18 16:30:13 -07:00
Julien Duponchelle
a1461d9ea6 Correctly check local server if only local is available in vm wizard 2016-02-18 19:09:24 +01:00
Julien Duponchelle
783248d58b Try make the GNS3 VM server running value more reliable
Ref #1049
2016-02-18 16:30:00 +01:00
Julien Duponchelle
d13d77e39a Make VM configuration dialog modal
Fix #1048
2016-02-18 10:26:27 +01:00
grossmj
bf333e1964 Cannot take GIF screenshots (write is not supported by Qt). 2016-02-17 16:02:32 -07:00
Julien Duponchelle
1833e8683b 1.4.3dev1 2016-02-17 19:36:57 +01:00
Julien Duponchelle
e4a336cd67 1.4.2 2016-02-17 19:31:28 +01:00
Jeremy Grossmann
1810956185 Merge pull request #1047 from GNS3/gif
Allow GIF images (not animated) since patent expired in 2004.
2016-02-17 11:25:12 -07:00
Julien Duponchelle
af7f0f49d1 Allow gif image (not animated) since pattent expire in 2004 2016-02-17 18:54:27 +01:00
Jeremy Grossmann
d6ebf8ea04 Merge pull request #1046 from GNS3/countdown
Countdown before starting the GNS3 VM
2016-02-17 10:25:58 -07:00
Julien Duponchelle
13721c9811 Countdown before starting the GNS3 VM
Fix #912
2016-02-17 18:24:34 +01:00
Julien Duponchelle
eafde17259 Merge branch 'master' into 1.5 2016-02-17 10:34:28 +01:00
Julien Duponchelle
02810e84a9 Prevent IOU GNS3A install on Windows 2016-02-17 10:33:57 +01:00
Julien Duponchelle
ac3214fedc Fix refresh of device list when importing docker container
Ref #945
2016-02-17 10:19:31 +01:00
Julien Duponchelle
c376689ad4 Add GNS3A support for docker
Beware it's rename the settings for saving the list of the
containers in the GUI.

Fix #945
2016-02-17 10:01:24 +01:00
grossmj
a12a686a68 Customizable name for VirtualBox VM templates. Fixes #748. 2016-02-16 22:22:13 -07:00
grossmj
16ec69bb8c Set timeout from 1 to 3 seconds when waiting for GNS3 VM server. Ref #1034. 2016-02-16 18:19:25 -07:00
grossmj
82cb95aff7 Redirect stderr to stdout when executing VBoxManage or vmrun. Ref #1027. 2016-02-16 17:24:41 -07:00
Jeremy Grossmann
1eb3693f0a Merge pull request #1031 from GNS3/cancel_vm_start
Allow to cancel the start of the GNS3 VM.
2016-02-16 16:58:18 -07:00
Julien Duponchelle
50925536d1 Merge branch 'master' into 1.5 2016-02-16 19:05:38 +01:00
Julien Duponchelle
bfd9c654aa Apply the correct name to the appliance imported 2016-02-16 19:04:18 +01:00
Julien Duponchelle
1104ce3176 Correctly save the environnement for docker container 2016-02-16 18:31:23 +01:00
Julien Duponchelle
19ef5b7e1d Merge branch 'master' into 1.5 2016-02-16 18:12:51 +01:00
Julien Duponchelle
b1477f5fb5 Update VMware banners
Fix #1032
2016-02-16 18:10:32 +01:00
Julien Duponchelle
8613a89264 Keep the correct vm_id between reload of topology with container
Fix #1043
2016-02-16 17:00:55 +01:00
Julien Duponchelle
6a90bac196 Fix container save
Fix #1038
2016-02-16 15:29:00 +01:00
Julien Duponchelle
cda6398010 Fix crash when loading a SVG image in topology
Fix #1035
2016-02-16 11:52:40 +01:00
Julien Duponchelle
e502f1dcc4 Merge branch 'master' into 1.5 2016-02-16 11:46:48 +01:00
Julien Duponchelle
13b699a183 Prevent a crash in progress dialog
Fix #1037
2016-02-16 11:46:12 +01:00
Julien Duponchelle
0cd1f314f6 Update for 4K monitor
Ref #1033
2016-02-16 09:56:33 +01:00
grossmj
0c894ee48a Do not use the appliance version for create its VM template name. 2016-02-12 21:17:02 -07:00
Julien Duponchelle
f6f1d4a97c Allow to change the docker console port
Fix #1026
2016-02-12 16:37:31 +01:00
Julien Duponchelle
c85820e685 Allow to cancel the start of the GNS3 VM
Fix #912
2016-02-12 16:05:38 +01:00
Jeremy Grossmann
445f721ceb Merge pull request #1024 from GNS3/svg_only_node_item
All node are now SVG items
2016-02-11 18:42:32 -08:00
Jeremy Grossmann
5fa24247c6 Merge pull request #1011 from GNS3/allow_to_import_unknow
Allow to import unknow files via GNS3A
2016-02-11 17:20:38 -08:00
Julien Duponchelle
7ee76fdd2b Merge branch 'master' into 1.5 2016-02-11 15:47:07 +01:00
Julien Duponchelle
2acfe7f1bf Recompute file size during import and add a column with the MD5 size 2016-02-11 12:11:38 +01:00
Julien Duponchelle
b33f660f90 Fix setup.py is not installing gns3-net-converter
Fix #1025
2016-02-11 10:11:08 +01:00
grossmj
91509888af Launch custom console command only if a node is initialized and started. 2016-02-10 15:11:40 -07:00
Jeremy Grossmann
fa3657f736 Merge pull request #1022 from GNS3/custom_console_without_save
Allow to open a custom console on any node
2016-02-10 13:22:51 -08:00
Julien Duponchelle
09638633c1 gns3-net-converter 2016-02-10 19:30:15 +01:00
Julien Duponchelle
11a6f1124f gns3-net-converter 2016-02-10 19:23:33 +01:00
Jeremy Grossmann
98ba58f39b Merge pull request #1013 from GNS3/detect_corrupt_port_name
Detect corrupt port name
2016-02-10 10:06:27 -08:00
Julien Duponchelle
b6fa4f3242 Merge branch 'master' into 1.5 2016-02-10 18:47:56 +01:00
Julien Duponchelle
baa7436e43 Detect and fix duplicate port name in topology
Try to track #992.
2016-02-10 17:45:07 +01:00
Julien Duponchelle
4a9b649e9f Allow to open a custom console on any node
Without saving the console anywhere

Fix #675
2016-02-10 17:39:44 +01:00
Julien Duponchelle
42143f77c5 All node are now SVG items
This create a generic renderer outputing always an SVG item.

Fix #1017
2016-02-10 15:47:27 +01:00
Julien Duponchelle
e2bdd216bb Move Qt code to a module so we can add new code in differents files 2016-02-10 13:53:03 +01:00
Julien Duponchelle
856fde79ec Fix rare crash when updating node
Fix #1018
2016-02-10 11:33:23 +01:00
Julien Duponchelle
91987b2e06 Prevent duplicate server in server summary
Fix #1020
2016-02-10 11:29:18 +01:00
Julien Duponchelle
36c6eeabc9 Fix crash if you create a new version and click on next without file 2016-02-10 11:25:22 +01:00
Julien Duponchelle
549e364da3 Use _updateCallback instead of _setupCallback
Fix #992, #1015
2016-02-10 11:13:55 +01:00
grossmj
1cc2d6c6b7 User configurable default name format for Docker. Fixes #748. 2016-02-09 19:33:13 -07:00
grossmj
d0321ce0fa Change text description in create a new version dialog. 2016-02-09 18:28:34 -07:00
Jeremy Grossmann
0d4cca3aa7 Merge pull request #1010 from GNS3/doctor_double_gns3
Check if GNS3 is not installed twice in doctor
2016-02-09 17:08:08 -08:00
Jeremy Grossmann
d77177a98f Merge pull request #1008 from GNS3/custom_console
Allow to set a custom console per node & refactor common code for save and load topology
2016-02-09 16:55:52 -08:00
Jeremy Grossmann
13bcac7e22 Merge pull request #1000 from GNS3/edit_console_dialog
Create a dialog for choosing the console command.
2016-02-09 16:49:22 -08:00
Julien Duponchelle
0046d8ba90 Merge branch 'master' into 1.5 2016-02-09 18:15:25 +01:00
Julien Duponchelle
4475a93fc5 Fix a regression in Host and Cloud
Fix #1014
2016-02-09 18:13:30 +01:00
Julien Duponchelle
041b609f99 Check if GNS3 is not installed twice in doctor
Fix #1007
2016-02-09 16:10:00 +01:00
Julien Duponchelle
bb6653cecb Docker capture and refactor common capture code
Fix https://github.com/GNS3/gns3-gui/issues/891
2016-02-09 16:06:25 +01:00
Julien Duponchelle
d84ce07038 Allow to add custom command to the list
https://www.youtube.com/watch?v=sEXbQmHJ1uM&feature=em-upload_owner
2016-02-09 11:18:16 +01:00
Julien Duponchelle
c9fec9ad51 Update Readme Python 3.4 is require 2016-02-09 10:04:01 +01:00
Julien Duponchelle
e953ea7212 Allow to import unknow files via GNS3A
Fix #980
2016-02-08 20:27:09 +01:00
Julien Duponchelle
d1d7cd8186 Fix a problem with gns3 running in background after exit
Fix #971
2016-02-08 17:41:46 +01:00
Julien Duponchelle
d3e4f17dab Move common code for ports dump to vm.py 2016-02-08 16:24:14 +01:00
Julien Duponchelle
344fde0d6f Move common code _updatePortSettings to vm.py 2016-02-08 16:16:38 +01:00
Julien Duponchelle
e27c8955c5 Fix a crash when searching for alternative images 2016-02-08 15:24:04 +01:00
Julien Duponchelle
27fa234888 Fix tests
Fix #1009
2016-02-08 14:20:27 +01:00
Julien Duponchelle
59ae047359 Fix a crash with corrupted topology from 1.0
Fix #1005
2016-02-08 14:01:55 +01:00
Julien Duponchelle
a847a2ff91 Merge branch 'master' into 1.5 2016-02-08 13:36:15 +01:00
Julien Duponchelle
8445637dd1 Remove all the docker code from 1.4 gui to avoid confusion 2016-02-08 13:33:27 +01:00
Julien Duponchelle
7f339ede4e Merge commit '19f1b969eae02b062d262994a83cc03af47b5586' into 1.5 2016-02-08 13:23:30 +01:00
grossmj
e54faa3079 User configurable default name format for Docker. Fixes #748. 2016-02-05 12:15:30 -07:00
Julien Duponchelle
4d2a4f3433 Allow to set a custom console per node
This require some refactoring:
* all VM inherit of the same dump and load common code
* move some code for opening a console from graphic views to the VM

Fix #675
2016-02-05 19:00:53 +01:00
Julien Duponchelle
ddd6de24ce Create a dialog for choosing the console command.
This extract all the code for selecting a console command
to a Dialog. With the following improvements:
* detect if a command exists and select it in the combo box
* help about custom variables that can be used in the command

Demonstration here:
https://www.youtube.com/watch?v=fw9XhRtFJuY&feature=em-upload_owner

It's part 1 of the #675 for supporting console application by node.
2016-02-05 16:10:59 +01:00
Julien Duponchelle
19f1b969ea Catch error if dynamips is disabled for local and no remote available
Fix #999
2016-02-05 14:54:46 +01:00
Julien Duponchelle
48beb69103 Merge pull request #993 from GNS3/download_vm_link
Put a link for the GNS3 VM in the setup wizard
2016-02-05 10:18:09 +01:00
Julien Duponchelle
a0be08d62a Put a link for the GNS3 VM in the setup wizard
It's smart the link change with the version and emulator

Fix #978
2016-02-05 10:17:48 +01:00
Julien Duponchelle
a750c7df2a Merge pull request #994 from GNS3/gray_vm
When importing appliance explain why options is gray
2016-02-05 10:11:36 +01:00
Julien Duponchelle
950986c69e When importing appliance explain why options is gray
Fix #979
2016-02-05 10:11:15 +01:00
grossmj
cc7bddefa1 User configurable default name format for VMware and VirtualBox. Ref #748 2016-02-04 23:47:34 -07:00
grossmj
1c4b735880 Changes "base name prefix" to "default name format". Ref #748. 2016-02-04 21:20:42 -07:00
grossmj
0862c135d0 User configurable base name support for Dynamips, IOU and VPCS. Ref #748. 2016-02-04 19:57:53 -07:00
grossmj
01e3cf1b1c Refactor "Import config" router dialog. Fixes #752. 2016-02-04 18:17:20 -07:00
grossmj
519bd389f6 Fixes ValueError: cannot mmap an empty file. Fixes #723. 2016-02-04 17:45:09 -07:00
grossmj
ee6aac2614 Merge remote-tracking branch 'origin/master' 2016-02-04 17:31:17 -07:00
grossmj
fa319b6529 Saves the "show port names" state in topology files. Fixes #778. 2016-02-04 17:31:01 -07:00
Julien Duponchelle
905be4130e Fix KeyError: 'midplane' when loading 7200 in some cases
Perhaps something when importing old config.

Fix #996
2016-02-04 18:40:17 +01:00
Julien Duponchelle
814e973f9a Hide the server select box for builtin switch if dynamips local is off
Fix #872
2016-02-04 17:49:46 +01:00
Julien Duponchelle
d84596860c Right click support for docker 2016-02-04 17:15:54 +01:00
Julien Duponchelle
5d9414e728 Merge branch 'master' into 1.5 2016-02-04 17:13:03 +01:00
Julien Duponchelle
016e279d43 IOU right click on devices 2016-02-04 17:12:14 +01:00
Julien Duponchelle
d432047ac1 Right click edit for VirtualBox 2016-02-04 16:53:14 +01:00
Julien Duponchelle
9938e60e05 Support right click on VMware vms templates 2016-02-04 16:27:36 +01:00
Julien Duponchelle
6e751bbfaf Support right click on dynamips templates 2016-02-04 16:24:17 +01:00
Julien Duponchelle
8bdc07185b Fix an issue where the Existing image button can disapear from wizard
If you select a server with no image and after a server with image.
The existing image button will not be visible.
2016-02-04 11:31:20 +01:00
Julien Duponchelle
8872aceb0b Fix a race condition when you ask for image list but close the windows
Fix #990
2016-02-04 11:13:43 +01:00
Julien Duponchelle
f7b14c7da7 Fix QWidget::setWindowModified: The window title does not contain a
'[*]' placeholder

Fix #991
2016-02-04 11:10:06 +01:00
Julien Duponchelle
86cd732453 Fix alignements of VMware and VirtualBox in VM choice type 2016-02-04 11:07:08 +01:00
Julien Duponchelle
ae6571b18b Better explanation of server type for Docker 2016-02-04 11:05:03 +01:00
Julien Duponchelle
0faf773f71 Merge branch 'master' into 1.5 2016-02-04 11:03:41 +01:00
Julien Duponchelle
3178fb1a46 Better explanation during server choice
Fix #977
2016-02-04 11:03:03 +01:00
Julien Duponchelle
a917e80774 Disabled remote button when we have no remote in server wizard 2016-02-04 11:03:02 +01:00
grossmj
9bde5fcad6 Improved lookup for VMware host type. Fixes #970. 2016-02-03 19:13:10 -07:00
Jeremy Grossmann
e33f423733 Merge pull request #986 from GNS3/righ_edit
Allow to edit a node via a right click in the node
2016-02-03 11:30:26 -08:00
grossmj
cc35408607 Change some text in nodes view. 2016-02-03 12:28:09 -07:00
Jeremy Grossmann
10d91b50ec Merge pull request #976 from GNS3/command_line
Allow to show the command line used to start a VM
2016-02-03 11:10:10 -08:00
Julien Duponchelle
7dd06b5659 Allow to edit a node via a right click in the node
At this time only Qemu is OK. If the change are OK
I can patch VMware, VirtualBox, Dynamips and IOU.

Fix #670
2016-02-03 18:09:20 +01:00
Julien Duponchelle
86087bf505 Show error if a problem occur when getting remote server KVM status
Fix #983
2016-02-03 16:27:21 +01:00
Julien Duponchelle
cb4923815a Display a clean error when an appliance has an invalid JSON
Fix #984
2016-02-03 16:11:42 +01:00
Julien Duponchelle
43258d64d2 MobaXterm integration
Fix #985
2016-02-03 16:08:26 +01:00
Julien Duponchelle
6d3109be67 Allow to show the command line used to start a VM 2016-02-02 18:24:51 +01:00
Julien Duponchelle
a25e0cc23f Add - GNS3 at the end of the windows name
Ref #578
2016-02-02 10:37:59 +01:00
Julien Duponchelle
026a78b1b6 Fix a crash with doctor on windows
Fix #974
2016-02-02 09:44:24 +01:00
Julien Duponchelle
380a4a0395 Fix crash in doctor if ubridge path is empty
Fix #975
2016-02-02 09:42:04 +01:00
Julien Duponchelle
f6c2a6387f 1.4.2dev1 2016-02-01 17:54:58 +01:00
Julien Duponchelle
27f65a92e9 1.4.1 2016-02-01 17:51:30 +01:00
Julien Duponchelle
858f33f782 Fix reload a docker topology with two adapter lead to corrupted topology
Fix #973
2016-02-01 17:30:08 +01:00
Julien Duponchelle
fa072bf387 By default use latest image for Docker 2016-02-01 16:50:28 +01:00
Julien Duponchelle
154ea7354d Merge branch 'master' into 1.5 2016-02-01 15:46:46 +01:00
grossmj
53dbac1d5c Improvement to detect VMware Player on Linux. Ref #970. 2016-01-31 11:32:40 -07:00
Julien Duponchelle
a780866cd8 You can move Dock widgets everywhere 2016-01-28 14:49:15 +01:00
Julien Duponchelle
bf6f1af217 Link to download VIX api 2016-01-28 14:12:40 +01:00
Julien Duponchelle
29c85f3c65 Fix SSH present in the server preferences
Fix also a scaling issue

Fix #968
2016-01-27 16:57:42 +01:00
Julien Duponchelle
ba51d3ebf5 Apply diff from @elhers 2016-01-27 15:46:03 +01:00
Julien Duponchelle
e4f3ea1da9 Pach for linux 2016-01-27 15:25:45 +01:00
Julien Duponchelle
f037452188 Fix doctor for linux 2016-01-27 14:35:47 +01:00
Julien Duponchelle
3d35601d47 Check dynamips and ubridge permission.
Not tested on linux for the moment

Ref #944
2016-01-27 11:49:43 +01:00
Julien Duponchelle
146e642097 Check if processor is 64 bit in Doctor 2016-01-27 11:03:51 +01:00
Julien Duponchelle
e2c53443e3 Merge pull request #929 from GNS3/doctor
Show a dialog for checking some common issues
2016-01-27 10:57:45 +01:00
grossmj
965a98d5a7 Adjustments for PR #929. 2016-01-27 10:52:32 +01:00
Julien Duponchelle
41a68e7b0e Warn if vmrun not accessible 2016-01-27 10:51:36 +01:00
Julien Duponchelle
20e62b824c Show a dialog for checking some common issues
Warn user about issues with AVG

Fix #928, #744
2016-01-27 10:51:36 +01:00
Julien Duponchelle
36e9fcbddf Add Servers Summary in "View / Docks"
Fix #965
2016-01-27 10:44:02 +01:00
Julien Duponchelle
bd3d33d67b Add an unknwon status icon when the server has never been connected
Fix #966
2016-01-27 10:40:03 +01:00
grossmj
caf77a24f1 Some cleaning in MainWindow. 2016-01-26 13:24:40 -07:00
Jeremy Grossmann
729f9e103d Merge pull request #964 from GNS3/server_usage
Show a summary with server usages
2016-01-26 10:36:48 -08:00
grossmj
7261debb79 Revert "Remove unused code."
This reverts commit b4d0deddfc.
2016-01-26 11:30:36 -07:00
grossmj
b4d0deddfc Remove unused code. 2016-01-26 10:58:15 -07:00
Julien Duponchelle
d62a32c7d7 Show a summary with server usages
Fix #963
2016-01-26 18:52:53 +01:00
Julien Duponchelle
cb94fc4d1e Fix a typo 2016-01-26 17:03:51 +01:00
Julien Duponchelle
554888b21d Close project on VM when closing the project on all servers 2016-01-26 17:02:13 +01:00
Julien Duponchelle
4ae01282d7 Avoid a crash during test 2016-01-26 17:01:08 +01:00
Julien Duponchelle
020e62ddf5 Allow to not rotate the text when changing all text colors
Fix #805
2016-01-26 14:10:32 +01:00
Julien Duponchelle
c1dce98595 Fix topology summary view 2016-01-26 11:34:28 +01:00
Julien Duponchelle
676187061c Raise an error when psutil is too old
Also in crash report we include the version of psutil

Fix #960
2016-01-26 11:03:25 +01:00
Julien Duponchelle
14f4f26791 Fix a race condition when closing the server
Fix #959
2016-01-26 10:47:55 +01:00
Julien Duponchelle
57cf10c251 Fix a very very rare crash in topology summary view
Fix #961
2016-01-26 10:44:07 +01:00
Julien Duponchelle
70b4fae62c Code cleanup from @elhers 2016-01-26 10:40:16 +01:00
Julien Duponchelle
5c3b9e7fae Merge pull request #955 from GNS3/fix_label_invert
Fix inversion of port label when loading a topology
2016-01-26 10:37:29 +01:00
Julien Duponchelle
9ea39a78db Code cleanup for label inversion
Following @grossmj and and @ehlers advices
2016-01-26 10:31:56 +01:00
grossmj
8f77697482 Reset port label positions. Fixes #811. 2016-01-25 20:41:38 -07:00
grossmj
1b5e53ddc6 Tentative to fix #951. 2016-01-25 14:42:00 -07:00
Jeremy Grossmann
86ee9595f4 Merge pull request #943 from GNS3/fix_terminal_command
Reset the telnet command on Mac
2016-01-25 12:02:15 -08:00
Jeremy Grossmann
dd7937dba4 Merge pull request #956 from GNS3/drop_ssh
Drop SSH support
2016-01-25 11:37:49 -08:00
Julien Duponchelle
a4c646152e Drop SSH support
Since we introduce the SSH support we got bug and bug
due to the complexity introduce by the tunnels. Even
in the 1.4.0 after long testing nobody notice that
the feature was broke.

And in his actual implementation we have limitation
preventing you to connect a node running on your PC
to a node running on the remote server.

After thinking about that it's better to drop it before
this feature is used by a lot of people. It's better to
use a VPN and less ninja.

Also we know that embeding paramiko require us to distribute
an encryption library creating legal trouble with US embargo
on various country.

Bonus point for the future: SSH support is probalby hard, or impossible for
a web interface and complicate the move to an architecture
with GNS3 server running as controller managing all the servers.

Fix #930
2016-01-25 19:21:36 +01:00
Julien Duponchelle
3d7db9a9c7 Fix inversion of port label when loading a topology
This PR rewrite the way multi link are created and avoid
to invert their directions.

Fix #954
2016-01-25 18:30:15 +01:00
Julien Duponchelle
06ed266278 Fix select an existing container image 2016-01-22 10:02:35 +01:00
Julien Duponchelle
5df16db823 Fix docker wizard bug when you have no container image on the host
Fix #946
2016-01-21 18:04:46 +01:00
Julien Duponchelle
4f23706b19 Merge branch 'master' into 1.5 2016-01-20 17:09:32 +01:00
Julien Duponchelle
e9ceadfc34 Fix error when importing Windows XP OVA
Fix #932
2016-01-20 17:06:46 +01:00
Julien Duponchelle
267cdc365d Warn if configuration file contain invalid unicode characters
Fix #933
2016-01-20 13:43:29 +01:00
Julien Duponchelle
6dfbaccee1 Fix a crash when importing some OVA
Fix #936
2016-01-20 13:41:03 +01:00
Julien Duponchelle
f97f48d428 Fix travis build 2016-01-20 13:09:02 +01:00
Julien Duponchelle
70b3bc680e Reset the telnet command on Mac
Following the issue #942

It's the easiest way to do it. But we can not interact
with the user at this step because the Qt is not yet started.
This mean during the upgrade the command is reset to the standard.

Even if some users could be disapointed it's positive for most user.
Because for most user the current settings is broke. And if someone
has change something he know how to configure it again (for example
switching to iTerm). If we don't do that I think the bug will remain
for years.
2016-01-20 11:50:33 +01:00
Jeremy Grossmann
c125579a38 Merge pull request #942 from GNS3/console_osx
Fix only one console work for OSX
2016-01-19 18:25:42 -08:00
Julien Duponchelle
9bc2a7e7bc Fix only one console work for OSX
In previous releases we got race conditions issues with
the OSX console due to the usage of Apple Script.

Problem. How to fix this situation for all users. Should
we reset terminal preferences for all OSX users coming from version
older than 1.4.1 ?

Fix #941
2016-01-19 20:17:32 +01:00
Julien Duponchelle
a5358dec14 Fix a rare crash when loading topology with missing image 2016-01-19 16:29:51 +01:00
Julien Duponchelle
c84f554f36 Fix a rare race condition when inserting an image
Fix #934
2016-01-19 16:23:33 +01:00
Julien Duponchelle
0dd9803f74 Fix a crash when pid file is empty
Fix #935
2016-01-19 16:12:36 +01:00
Julien Duponchelle
2baaa26a42 Fix a rare crash in progress dialogs
Fix #938
2016-01-19 16:10:45 +01:00
Julien Duponchelle
3fd62f906e Fix a crash if you don't have vms when importing a gns3a 2016-01-19 14:30:11 +01:00
Julien Duponchelle
41e8b44dde Warn user during appliance install if server is not avaible 2016-01-18 17:23:18 +01:00
Julien Duponchelle
09cbd9b606 Fix merge error after adding KVM requirements checks 2016-01-18 16:43:39 +01:00
Julien Duponchelle
3ab8d79ff7 Improve text when KVM is ok 2016-01-18 14:18:56 +01:00
Julien Duponchelle
97eaed6d2c Fix error when you stop the GNS3 VM but break the config before
Fix #924
2016-01-18 11:30:45 +01:00
Jeremy Grossmann
805aa23948 Merge pull request #920 from GNS3/fix_asa_wizard
Fix select of image broken if you need to select multiple images
2016-01-17 09:42:05 -08:00
Jeremy Grossmann
a25c45a502 Merge pull request #916 from GNS3/kvm_check
Add a step in the wizard checking KVM support
2016-01-17 09:41:04 -08:00
Julien Duponchelle
6ebda209ac startup_config is not mandatory inside .gns3 file 2016-01-15 20:57:13 +01:00
Julien Duponchelle
d226b31fab Fix wrong host on SSH connection
Fix #919
2016-01-15 19:33:10 +01:00
Julien Duponchelle
27a3009ce1 Fix select of image broken if you need to select multiple images
In the case of ASA you need to select multiple image
on multiple screen. The problem is we refresh the list
of the VM on each step.

This patch refresh only the VM list when the widget is visible.

Fix #886
2016-01-14 18:42:23 +01:00
Julien Duponchelle
654ad61105 Fix a crash when changing qemu cluster size to more than 512
Fix #915
2016-01-13 17:30:02 +01:00
Julien Duponchelle
31a461195f Fix a crash when closing SSH connections
Fix #917
2016-01-13 17:16:13 +01:00
Julien Duponchelle
331eef3164 Fix IOU support in gns3a
Fix #918
2016-01-13 17:05:47 +01:00
Julien Duponchelle
cb0d576ade 1.4.1dev1 2016-01-13 09:22:00 +01:00
Julien Duponchelle
32978381b2 Add urxvt support
https://gns3.com/qa/trying-to-get-gns3-to-work-with-
2016-01-13 09:21:07 +01:00
Julien Duponchelle
a7f7a688d3 1.4.0 release 2016-01-12 17:38:45 +01:00
Julien Duponchelle
5d87726397 Add a step in the wizard checking KVM support
It's mostly code from PR #904 but less error prone due to the usage
of a step in the wizard. Also it's check the capacities for all servers.
2016-01-11 17:53:36 +01:00
Julien Duponchelle
b9601cb54a Merge branch '1.4' into 1.5 2016-01-07 18:09:43 +01:00
Julien Duponchelle
bb66555896 Merge branch 'master' into 1.4 2016-01-07 18:09:28 +01:00
Julien Duponchelle
37726630ce Fix rare crash when showing the progress dialog
Fix #909
2016-01-07 09:16:42 +01:00
Julien Duponchelle
3d12f85f66 Merge branch '1.4' into 1.5 2016-01-06 14:34:57 +01:00
Julien Duponchelle
e26762fce3 1.4.0dev14 2016-01-06 14:34:08 +01:00
Julien Duponchelle
32e59fcce4 Fix crash on Windows when a gui is already running
Fix #908
2016-01-06 14:24:49 +01:00
grossmj
9b43330e95 Add default idle-pc value for c7200-adventerprisek9-mz.155-2.XB. Fixes #389. 2016-01-05 13:00:39 -07:00
Julien Duponchelle
b3b94243b4 1.4.0rc3 2016-01-05 18:30:48 +01:00
Julien Duponchelle
fba1032388 Fix config tests 2016-01-05 18:27:16 +01:00
Julien Duponchelle
ccc0803c5b Add information about antivirus and firewall in case of connection fail 2016-01-05 17:08:19 +01:00
Julien Duponchelle
f0340bcc98 Change link to doc for missing router image 2016-01-05 16:43:17 +01:00
Julien Duponchelle
8d19b8fcbf On windows and OSX experimental Qemu GNS3A support 2016-01-04 21:53:10 +01:00
Vasil Rangelov
dbf66d5a9a Allow appliances to be installed on a local Windows or OSX server, while also allowing them to instead require KVM be supported on the target server, or to disable it even if supported. Also resolve Qemu binary at appliance install time. 2016-01-04 21:53:10 +01:00
Julien Duponchelle
0fa8d61b19 Wait server in thread 2016-01-04 21:50:42 +01:00
Julien Duponchelle
02d326419b Fix local server non avaible in appliance wizard when local server
started by hand.

Ref #904
2016-01-04 14:12:08 +01:00
Julien Duponchelle
79b8baac9f Fix docker tests 2016-01-04 10:36:51 +01:00
Julien Duponchelle
8a6df8abc7 Fix build 2016-01-04 09:30:53 +01:00
grossmj
8b8d763fb7 Dependencies are listed in one location now. 2016-01-03 14:09:01 -07:00
Julien Duponchelle
608e80a80b Improve pid checks
Fix #900
2016-01-03 20:45:58 +01:00
Julien Duponchelle
d90f11eb86 Add a comment about tests run in a container 2016-01-03 19:54:57 +01:00
Jeremy Grossmann
dc39187091 Merge pull request #894 from boenrobot/port1names
Port names with numbers starting from 1
2015-12-27 20:08:21 -08:00
Jeremy Grossmann
e4cd418533 Merge pull request #898 from boenrobot/diskInterfaceOnAppliance
Disk interfaces on Qemu appliances
2015-12-25 11:09:06 -08:00
Vasil Rangelov
8559469c73 Allow Qemu appliances to optionally specify desired disk interfaces in their configuration (defaults to "ide"). 2015-12-25 04:00:48 +02:00
Jeremy Grossmann
ac8d2beb80 Merge pull request #892 from GNS3/docker_cleanup
Docker support cleanup and improvements
2015-12-24 14:54:31 -08:00
Julien Duponchelle
5e6384074e Merge branch '1.4' into 1.5 2015-12-22 18:10:03 +01:00
Julien Duponchelle
e6ba7bdd98 Fix project non closing when you have only remote servers
Fix #895
2015-12-22 16:21:41 +01:00
Julien Duponchelle
784055689f Fix Windows layout not saved in some scenario
Fix #884
2015-12-22 16:11:50 +01:00
Julien Duponchelle
c937811f45 Force gns3 converter >= 1.2.4 2015-12-22 10:45:16 +01:00
Vasil Rangelov
79c8021faa 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. 2015-12-22 00:41:12 +02:00
Julien Duponchelle
a428730f59 Merge pull request #893 from boenrobot/schemaFix1
Added missing Qemu adapters to the topology schema.
2015-12-21 21:12:44 +01:00
Julien Duponchelle
2522bd44d6 New crash report key 2015-12-21 20:48:51 +01:00
Vasil Rangelov
3cad8ea046 Added missing Qemu adapters to the topology schema. 2015-12-21 20:50:27 +02:00
Julien Duponchelle
76131f1cc7 Enable docker 2015-12-18 18:19:06 +01:00
Julien Duponchelle
54fb5dc765 Docker support cleanup and improvements 2015-12-18 18:13:44 +01:00
Julien Duponchelle
4110af56e7 1.5.0dev1 2015-12-18 18:06:46 +01:00
Julien Duponchelle
ab1775e44b Fix Cannot save my topology getting an error message for temporary
topology

Fix #885
2015-12-18 10:10:26 +01:00
Julien Duponchelle
9c3facb07a Fix creation of ASA devices
Fix #890
2015-12-18 10:08:41 +01:00
Julien Duponchelle
e5ae7b2d25 Turn off Docker until 1.5 2015-12-18 09:49:50 +01:00
Julien Duponchelle
60462ff986 Fix display of server preferences on small screen
Fix #882
2015-12-14 16:37:55 +01:00
Julien Duponchelle
39443cd676 Fix If you turn off the local server and close the gui and reopen
preferences you have an issue

Fix #881
2015-12-14 16:28:50 +01:00
Julien Duponchelle
25ab8249ae Zoc 7 support
Fix #879
2015-12-14 16:19:38 +01:00
Julien Duponchelle
6e86c606cc Fix test on windows
Fix #877
2015-12-14 15:07:45 +01:00
Julien Duponchelle
8878b96e74 Merge branch 'master' into unstable 2015-12-11 14:32:32 +01:00
Julien Duponchelle
c3ef2edbab 1.4.0dev13 2015-12-11 14:27:20 +01:00
Julien Duponchelle
f7e398edc3 1.3.14dev1 2015-12-11 14:26:13 +01:00
Julien Duponchelle
8d4fc9585e 1.3.13 2015-12-11 09:22:46 +01:00
Julien Duponchelle
5fe65e26ce 1.3.12 2015-12-11 08:53:14 +01:00
Julien Duponchelle
daaebe7f96 Fix warning when closing GUI
Fix #875
2015-12-11 08:51:12 +01:00
grossmj
f9f6a52e8b Update links for new website. 2015-12-10 15:00:16 -07:00
grossmj
601c0217b9 Update VMware links. 2015-12-10 14:05:39 -07:00
Julien Duponchelle
b0971b4ba3 1.4.0rc2 2015-12-10 19:36:41 +01:00
Julien Duponchelle
5a4549c36c Merge pull request #874 from GNS3/iou_dynamips_gns3a
IOU and dynamips support for gns3a
2015-12-10 19:32:11 +01:00
Julien Duponchelle
fe2cc362f8 Dynamips GNS3A support 2015-12-10 18:17:51 +01:00
Julien Duponchelle
dd0220fd59 Prevent user turning off the Local server when using the GNS3 VM
Fix #873
2015-12-10 13:57:09 +01:00
Julien Duponchelle
26fdd9ef6f Force VM wizard to be modal
Fix #868
2015-12-10 10:31:03 +01:00
Julien Duponchelle
733ee259e5 Fix unicode error when exporting debug informations
Fix #869
2015-12-10 10:25:19 +01:00
Julien Duponchelle
762fecbcff Fix Debug can't be deactivated for current session
Fix #871
2015-12-10 10:19:13 +01:00
Julien Duponchelle
5e85dfe5fd Ignore missing file when reading config
Fix #870
2015-12-10 10:08:29 +01:00
Jeremy Grossmann
e01632d60a Merge pull request #864 from GNS3/drop_webkit
Drop Webkit from 1.3.X
2015-12-09 19:34:15 -08:00
Julien Duponchelle
60916e8b80 IOU gns3a support 2015-12-09 19:49:23 +01:00
Julien Duponchelle
4fe55634ae Support relative path for configuration file.
It's allow futur GNS3A support for IOU and Dynamips
2015-12-09 18:06:07 +01:00
Julien Duponchelle
b1b861c99d Don't kill the server if two gui are running
Fix #865
2015-12-09 15:26:37 +01:00
Julien Duponchelle
07c0474386 Ask user to send bug reports to GNS3.com 2015-12-09 12:17:32 +01:00
Julien Duponchelle
755fb5c8f3 Report bug to GNS3.com 2015-12-09 12:16:49 +01:00
Julien Duponchelle
fdb382874d Avoid crash when cancel connection to a server
Ref #865
2015-12-09 12:04:18 +01:00
Julien Duponchelle
471b3e1009 Fix travis dependencies installation 2015-12-09 09:11:19 +01:00
Julien Duponchelle
f64a226336 1.4.0dev12 2015-12-08 16:27:54 +01:00
Julien Duponchelle
dec257bb6b Fix version number display twice when installing appliance 2015-12-08 16:05:48 +01:00
Julien Duponchelle
e736fbbb87 Drop Webkit from 1.3.X 2015-12-08 11:44:21 +01:00
Julien Duponchelle
aeb42b3ffe Show a warning when you try to save as a remote topology
Ref #831
2015-12-08 11:34:37 +01:00
Julien Duponchelle
1694a57ed9 Fix application restart after self upgrade
Fix #863
2015-12-07 15:39:13 +01:00
Julien Duponchelle
26001463a0 Cleanup server autostart
Fix #862
2015-12-07 11:30:17 +01:00
grossmj
36c1197f1f Merge remote-tracking branch 'origin/unstable' into unstable 2015-12-05 17:26:52 -07:00
grossmj
1fa16936fe Have default console port start from 2000. 2015-12-05 17:26:33 -07:00
Jeremy Grossmann
fca65784ee Merge pull request #852 from GNS3/vnc
Allow to use the VNC port range for console
2015-12-05 16:03:44 -08:00
Julien Duponchelle
8983e6c5a9 Fix crash when opening a new topologies after gns3 converter failure
Fix #858
2015-12-04 15:18:38 +01:00
Julien Duponchelle
c6e06f3941 Fix crash when opening a new topologies after gns3 converter failure
Fix #858
2015-12-04 15:13:03 +01:00
Julien Duponchelle
b7e32a60ce Fix SSH support
Fix #857
2015-12-04 14:16:14 +01:00
Julien Duponchelle
da100e494b Fix bus error when writting on console
Fix #371
2015-12-04 11:37:42 +01:00
Julien Duponchelle
9d7b5bccb8 Fix Ok & Cancel button in preferences are broken
Fix #855
2015-12-03 16:57:15 +01:00
Julien Duponchelle
112b05c3dd Fix After a project load failure you can't open new project
Fix #851
2015-12-03 16:53:41 +01:00
Julien Duponchelle
987e85cce8 More fix around closing the GUI
Fix #854
2015-12-03 16:37:33 +01:00
Julien Duponchelle
8142d0baa7 Fix crash in progress dialog on OSX
Fix #828
2015-12-03 15:45:55 +01:00
Julien Duponchelle
cc32b2661c Kill already running zombie server
Fix #838
2015-12-03 11:14:16 +01:00
Julien Duponchelle
781857f598 Store the pid of the server when started
Refe #838
2015-12-03 10:33:03 +01:00
Jeremy Grossmann
69179dcb63 Merge pull request #849 from GNS3/resize_preferences
Preferences dialog resize
2015-12-02 11:11:32 -08:00
Julien Duponchelle
8017838d60 Fix a crash in rare case after a PyQT garbage collect
Fix #842
2015-12-02 16:13:59 +01:00
Julien Duponchelle
81946493ec Allow to use the VNC port range for console
This PR allow user to have VNC port in the
console port range. It's not a problem because server
can share the range (we have a common list for all tcp ports).

And we add an informations in the settings in order to allow user
to know that 5900 => 6000 will be used.

Fix #846, #839
2015-12-02 16:08:34 +01:00
Julien Duponchelle
a7f40c3d50 Preferences dialog resize
* add a scrollbar if preferences height is too big
* set a dynamics max size depending of the screen

Fix #844
2015-12-02 12:22:28 +01:00
Julien Duponchelle
c28723287f Fix a race condition when opening telnet from apple script
Prevent closing ssh tunnel on OSX when using apple script
this leak port...

Fix #841, #848
2015-12-02 11:21:16 +01:00
Julien Duponchelle
c7a8588647 Experimental support for tabbed terminal on OSX
Fix #841
2015-12-02 11:05:24 +01:00
Julien Duponchelle
4a64261c5d Fix validation error when saving topology with an usage
Fix #829
2015-12-01 18:50:17 +01:00
Julien Duponchelle
5f4365542c Fix another case of not closing windows
Fix #833
2015-12-01 18:47:42 +01:00
Julien Duponchelle
2e2c951ffc Fix GUI doesn't close after connection error to remote server
Fix #833
2015-12-01 16:12:41 +01:00
Julien Duponchelle
2ba7dde326 Add usage text to device template and on hover
Fix #829
2015-12-01 11:04:05 +01:00
Julien Duponchelle
a1579ca86b Fix a rare crash in progress dialog
Fix #835
2015-12-01 09:49:18 +01:00
Julien Duponchelle
81c4ddb85f Fix crash when displaying an error from the update
Fix #836
2015-11-30 21:35:36 +01:00
Julien Duponchelle
285a8d413a Url encode royal tx url 2015-11-30 17:09:43 +01:00
Julien Duponchelle
672c86b38d Use Royal TX URI scheme thanks to @lemonmojo
Fix #832
2015-11-30 16:49:02 +01:00
Julien Duponchelle
905611d2a8 OSX support for Royal TSX
Can't test because I don't have a Royal TSX licence

Ref #832
2015-11-30 15:13:45 +01:00
Julien Duponchelle
339fb22217 Merge branch 'master' into unstable 2015-11-30 14:46:10 +01:00
Julien Duponchelle
a8f5fa5dd5 Set Wireshark 2.0 as default OSX version
Fix #830
2015-11-30 14:37:39 +01:00
Julien Duponchelle
bd365dd6eb iTerm 2.9 support
Fix #817
2015-11-30 14:27:54 +01:00
Jeremy Grossmann
8e4a6169e0 Merge pull request #809 from GNS3/contributing
Contributing guidelines
2015-11-14 20:20:13 -08:00
Jeremy Grossmann
b1790844f3 Update CONTRIBUTING.md 2015-11-14 19:59:15 -07:00
grossmj
4b0050d26c Update debug information dialog. 2015-11-14 18:33:03 -07:00
grossmj
19bf40dc89 Change text for export debug information. 2015-11-14 18:31:46 -07:00
Julien Duponchelle
23cda61d17 1.4.0rc1 2015-11-12 17:48:51 +01:00
Julien Duponchelle
c74ffde65a Rename an appliance if the default name is already taken 2015-11-12 17:34:50 +01:00
Julien Duponchelle
0a9c10e748 Existing image option should be hidden when none is available
Fix #819
2015-11-12 17:02:05 +01:00
Julien Duponchelle
6973eaaa02 Warn users about the need to uncompress the image
Fix #826
2015-11-12 16:18:49 +01:00
Julien Duponchelle
9ce483398b Fix crash when you have no qemu vms and use gns3a
Fix #824
2015-11-12 15:40:23 +01:00
Julien Duponchelle
55744ab129 Change sentry key
Fix #825
2015-11-12 11:20:17 +01:00
Julien Duponchelle
0e1cb47aa1 Merge branch 'master' into unstable 2015-11-12 10:52:31 +01:00
Julien Duponchelle
3bf12753df Fix format_exception() missing 2 required positional arguments: 'value'
and 'tb' in topologyFile

Fix #823
2015-11-12 10:43:04 +01:00
Julien Duponchelle
8907659220 Fix dialog box not returning their result
Fix #818
2015-11-12 09:34:45 +01:00
Julien Duponchelle
a4ccb0b620 Log to console the Qt Message Boxes 2015-11-11 11:58:31 +01:00
Julien Duponchelle
7d845c0ef8 Add informations about GNS3 VM 2015-11-10 12:45:22 +01:00
Julien Duponchelle
8282ec1da6 Contributing file 2015-11-10 12:43:11 +01:00
Julien Duponchelle
edfe2c7f47 Fix upload images from a non default images path
Fix #797
2015-11-09 17:24:46 +01:00
Julien Duponchelle
ca69673439 Fix error when set cluster size using qemu-img
Fix #798
2015-11-09 15:35:04 +01:00
Julien Duponchelle
8677707a9a Improve export debug informations
* Force adding .zip at the end
* Prevent crash for temporary directory

Fix #408
2015-11-09 14:30:43 +01:00
Julien Duponchelle
f3abbe5d58 Fixes unused import and variables 2015-11-09 12:52:57 +01:00
Julien Duponchelle
c0f36590be Auto pep8 2015-11-09 12:39:14 +01:00
grossmj
bdb097a173 Sets console end port to 7000. 2015-11-08 18:15:14 -07:00
grossmj
510491e26d Merge remote-tracking branch 'origin/unstable' into unstable 2015-11-08 17:23:56 -07:00
grossmj
6f0015a678 Fixes #342. Qemu master image is modified instead of the linked image. 2015-11-08 17:23:43 -07:00
Jeremy Grossmann
690e1d5ea0 Merge pull request #777 from GNS3/export_debug_informations
Allow to export a zip with debug informations
2015-11-07 21:17:48 -08:00
Julien Duponchelle
19734e0bd3 Fix invalid log screen when dumping a topology 2015-11-06 16:29:23 +01:00
Julien Duponchelle
5172c0b19e Warn the user to save the project before exporting debug infos 2015-11-06 10:38:54 +01:00
Julien Duponchelle
d74898c8a3 Allow to export a zip with debug informations
The zip contains:
* informations about the system, network and processus
* configuration
* log
* current .gns3 and screenshoot

Hope it will help to solve issues with can't connect to
http://127.0.0.1:8000

Fix #408
2015-11-05 15:43:11 +01:00
Julien Duponchelle
eb10f10988 Add more safety to HTTP callback to Qt
Fix #757
2015-11-05 11:36:05 +01:00
Julien Duponchelle
eb2e504acc Fix crash on Windows exit
Fix #749
2015-11-05 11:21:54 +01:00
Julien Duponchelle
6c502e1213 Correclty enable faulthandler for dev version 2015-11-05 10:51:08 +01:00
Julien Duponchelle
462c00b358 Fix support of remote servers for VPCS
Fix #756
2015-11-04 15:07:22 +01:00
Julien Duponchelle
b83eb61a42 Drop shellConsole docker will support telnet 2015-11-04 11:53:00 +01:00
Julien Duponchelle
96c35c49ff Drop some references to QtWebkit 2015-11-04 10:20:02 +01:00
Julien Duponchelle
36b48f24dc Prevent call to a destroyed callback in http client connection
Fix #754
2015-11-03 09:55:25 +01:00
Julien Duponchelle
efb21665f0 Avoid crash due to deleted thread
Ref #749
2015-11-03 09:41:14 +01:00
Julien Duponchelle
9ab77cfe20 1.4.0dev11 2015-11-02 21:28:34 +01:00
Julien Duponchelle
6031a349be 1.4.0b5 2015-11-02 18:51:44 +01:00
Julien Duponchelle
7163daf480 Fix crash when loading invalid appliance file
Fix #743
2015-11-02 18:01:46 +01:00
Julien Duponchelle
f662c39df0 Show a message is starting or is stopping in progress dialog
This change the text of the progress dialog when node start
or stop.
2015-11-02 15:34:34 +01:00
Julien Duponchelle
d7caba76c4 Drop duplicate code 2015-11-02 15:34:34 +01:00
grossmj
36ff527cc0 Changes some references for "GNS3 VM" to "Local GNS3 VM". Ref #506. 2015-11-01 18:19:59 -07:00
grossmj
d96ab77a67 Fixes progress dialog remains #741. 2015-10-31 15:34:23 -06:00
Julien Duponchelle
b4ade1982e Add more check to prevent progress dialog crash 2015-10-30 14:34:27 +01:00
Julien Duponchelle
4c9ef92c2d Support more boot order for Qemu
Fix #738
2015-10-30 10:28:10 +01:00
Jeremy Grossmann
078fc0f5ed Merge pull request #740 from GNS3/qpartial
Add a Qt compatible version of partial
2015-10-29 12:03:22 -07:00
Julien Duponchelle
40cc73c4f1 Add Royal TS 2015-10-29 18:18:48 +01:00
Julien Duponchelle
07c1443a8d Fix tests 2015-10-29 16:42:53 +01:00
Julien Duponchelle
dc592e0d4f Wording 2015-10-29 16:38:35 +01:00
Julien Duponchelle
c873b6c603 Fix text not aligned in the appliance wizard 2015-10-29 16:04:32 +01:00
Julien Duponchelle
c7d0a7a504 Add a Qt compatible version of partial
This function detect is the QObject is deleted and
doesn't call the callback.

Fix #596, #735
2015-10-29 15:51:12 +01:00
Julien Duponchelle
e7eddf0a7e Rename Open Appliance to Import appliance 2015-10-29 13:41:50 +01:00
Julien Duponchelle
579e382971 Show Upload filename instead of waiting for 2015-10-29 13:40:55 +01:00
Julien Duponchelle
95ecb05434 Fix error where ISO is detected as an OVA during gns3a import
Fix #737
2015-10-29 11:20:42 +01:00
grossmj
a2353f32e4 Appliance wizard tweaking. 2015-10-28 17:38:43 -06:00
grossmj
793181d208 Removes News Dock Widget. 2015-10-28 14:49:12 -06:00
Julien Duponchelle
932ff53190 Support cdrom image in missing images detections
Fix #737
2015-10-28 17:58:38 +01:00
Julien Duponchelle
e92e23a8be Try to avoid a crash with GUI
Fix #736
2015-10-28 16:42:15 +01:00
Julien Duponchelle
a16adb2b46 Fix crash when appliance is missing 2015-10-28 15:55:41 +01:00
grossmj
f579ebf5a0 Support for capture description in Wireshark window title. 2015-10-26 17:48:57 -06:00
Julien Duponchelle
67593c97ab Move description in the GNS3 appliance 2015-10-23 17:30:05 +02:00
Julien Duponchelle
fe185283a0 Remove a debug 2015-10-22 17:47:26 +02:00
Julien Duponchelle
d2f8214a52 Fix crash with appliance wizard on Linux 2015-10-21 21:15:12 +02:00
Julien Duponchelle
a2293b21ce Set the name of the VM in OSX Terminal application 2015-10-21 17:16:59 +02:00
Julien Duponchelle
57872c38b3 Fix crash of symbol selector if the first file is a non builtin symbol
Fix #734
2015-10-21 13:55:41 +02:00
Julien Duponchelle
51f998c177 Ask for installing appliance
Fix #731
2015-10-21 12:03:36 +02:00
Julien Duponchelle
7abbc630c3 Install appliance from wizard instead of web app
Ref #731
2015-10-21 11:49:44 +02:00
Julien Duponchelle
a0dee6cb42 Fix version number 2015-10-20 16:20:29 +02:00
Julien Duponchelle
d38143ae1c Fix images import
Fix #729
2015-10-20 12:29:07 +02:00
Julien Duponchelle
1db36dfb3e Fix crash when using an old version of 1.4
Fix #733
2015-10-20 12:10:08 +02:00
Julien Duponchelle
a347de96ed Ensure default settings are saved when starting the app
Fix #732
2015-10-20 12:00:58 +02:00
Julien Duponchelle
31c76bf56d Cleanup docker test 2015-10-19 22:15:18 +02:00
Julien Duponchelle
8abf321d9a Use docker for travis build 2015-10-19 22:10:40 +02:00
Julien Duponchelle
3f7b6e7be4 1.4.0dev10 2015-10-19 19:11:45 +02:00
Julien Duponchelle
7b1337cd83 1.4.0b4 2015-10-19 19:04:44 +02:00
Julien Duponchelle
7f63a221af Mokcup of appliances wizard 2015-10-19 18:58:13 +02:00
Julien Duponchelle
cfeb5c9495 Fix tests 2015-10-19 17:04:24 +02:00
Julien Duponchelle
17655cd855 Fix Crash when opening an appliance #728 2015-10-19 17:00:16 +02:00
grossmj
ab071cd989 Mock up for appliance wizard. 2015-10-18 21:54:27 -06:00
grossmj
c73dd10783 Registry: add -nographic to Qemu options by default. Fixes #730. 2015-10-18 21:05:46 -06:00
grossmj
6a5584ae41 Registry: support for initrd, cpu throttling and process priority. 2015-10-18 21:02:42 -06:00
grossmj
16e82592a6 Support for modifications to a base Qemu VM (not a linked clone). 2015-10-18 19:19:27 -06:00
grossmj
842e771eef Registry: adds support for switch category, first_port_name and port_segment_size. 2015-10-18 14:59:35 -06:00
Julien Duponchelle
d31f9c49f5 Fix traceback when exiting the GUI
Ref #726
2015-10-17 21:28:01 +02:00
Julien Duponchelle
8b61c7ddc3 Show a download button
Fix #727
2015-10-17 21:20:19 +02:00
grossmj
af247e2dd6 Corrects some typos. 2015-10-17 11:52:26 -06:00
grossmj
990cb49854 Merge remote-tracking branch 'origin/master' 2015-10-16 15:43:46 -06:00
grossmj
c530924d8a Drops securecrt.vbs 2015-10-16 15:43:31 -06:00
grossmj
35360ae196 Drops securecrt.vbs 2015-10-16 15:42:46 -06:00
grossmj
f37117ed09 Display an error message if QtWebkit is not installed. 2015-10-16 12:51:44 -06:00
Julien Duponchelle
9d2d80db25 Fix console port lost when applying settings
Fix #721
2015-10-16 18:47:20 +02:00
Julien Duponchelle
364d8db5b2 Remove unused code 2015-10-16 17:54:29 +02:00
Julien Duponchelle
0ac5215d4b Merge branch 'master' into unstable 2015-10-16 17:28:50 +02:00
Julien Duponchelle
493d81c519 Fix analytics report on OSX
Fix #724
2015-10-16 17:28:17 +02:00
Julien Duponchelle
c5ab0c8d02 Fix invalid path with frozen application
Fix #722
2015-10-16 11:22:31 +02:00
Julien Duponchelle
03323e5df8 When raising an appliance not found error show full path
Ref #722
2015-10-16 10:43:46 +02:00
grossmj
a8c429b77e Remove unnecessary checks to know if the local server is running. 2015-10-15 18:35:37 -06:00
Julien Duponchelle
52095c8ed9 Merge branch 'master' into unstable 2015-10-15 09:32:43 +02:00
Julien Duponchelle
b13855a062 Analytics send windows 2015-10-15 09:29:37 +02:00
grossmj
0be67907ba Fixes issue when loading a project using VMware vmnet interfaces. Fixes #319. 2015-10-14 21:39:17 -06:00
Julien Duponchelle
cf7285a179 Fix the progress dialog freeze bug 2015-10-14 17:56:18 +02:00
Julien Duponchelle
ba564869a0 Revert "Try to solve Scanning for Appliance images doesn't end"
This reverts commit 07610e7556.
2015-10-14 15:44:26 +02:00
Julien Duponchelle
a6ddddea99 Try to fix the progress dialog freeze bug 2015-10-14 15:32:05 +02:00
Julien Duponchelle
a686a18d56 Drop dead code from getting started dialog
Fix #719
2015-10-14 10:50:14 +02:00
Julien Duponchelle
a98bd132e1 Fix Appliance installs image without adapting the filename
Fix #714
2015-10-14 10:44:02 +02:00
Julien Duponchelle
a100ca33b9 Raise error if reference in GNS3a is invalid 2015-10-13 17:51:54 +02:00
Julien Duponchelle
d2f75262d8 Merge branch 'master' into unstable 2015-10-13 16:56:25 +02:00
Julien Duponchelle
c65fd1683d Fix typo in analytics 2015-10-13 16:55:59 +02:00
Julien Duponchelle
76a8735133 Add information on how to debug 2015-10-13 16:36:47 +02:00
Julien Duponchelle
da3c1334da Merge branch 'master' into unstable 2015-10-13 09:42:15 +02:00
Julien Duponchelle
0c370ec45e Send stats to GNS3 team
Ref #694

* Generate an ID for user and craft a user agent for correct os report
* Track all dialog allowing stats on features
2015-10-13 09:26:45 +02:00
grossmj
ec27e418fc Licenses compliance. 2015-10-12 22:10:43 -06:00
grossmj
92500e96d4 Merge remote-tracking branch 'origin/unstable' into unstable 2015-10-12 15:54:55 -06:00
grossmj
c2ba6f5cb3 Handles warning notifications. 2015-10-12 15:54:32 -06:00
Julien Duponchelle
07610e7556 Try to solve Scanning for Appliance images doesn't end
Ref #713
2015-10-12 19:31:13 +02:00
Julien Duponchelle
741749dffb Fix TypeError: 'NoneType' object is not iterable in isLocalServerRunning
Fix #689
2015-10-12 17:13:23 +02:00
Julien Duponchelle
593c777ff0 Fix crash AttributeError: 'NoneType' object has no attribute
getNewProjectSettings

Fix #711
2015-10-12 17:11:07 +02:00
Julien Duponchelle
8a616fa0c5 Merge branch 'master' into unstable 2015-10-12 15:41:27 +02:00
Julien Duponchelle
a191ec7326 Fix error when importing dynamips config from non existent directory
Fix #710
2015-10-12 15:39:44 +02:00
Julien Duponchelle
f842812745 Merge branch 'master' into unstable 2015-10-12 15:34:00 +02:00
Julien Duponchelle
3cc43aa11f Fix crash when url is invalid
Fix #709
2015-10-12 15:32:04 +02:00
Julien Duponchelle
1d9839ed23 Merge branch 'master' into unstable 2015-10-12 10:25:16 +02:00
Julien Duponchelle
32a64f1259 Add a debug level 2 in the console
Allow to display everything including HTTP queries.
2015-10-12 10:24:38 +02:00
Julien Duponchelle
3c4fc4c1bd Fix a crash when loading appliance 2015-10-08 16:16:17 +02:00
Julien Duponchelle
1013cf527d Merge branch 'master' into unstable 2015-10-08 11:28:38 +02:00
Julien Duponchelle
1684ee3074 Support upload of multiple vmdk file
Fix #702
2015-10-08 11:27:02 +02:00
Julien Duponchelle
f50e08207c 1.3.12dev1 2015-10-07 18:34:39 +02:00
Julien Duponchelle
d3973d4a7b 1.3.11 2015-10-07 18:28:04 +02:00
Julien Duponchelle
7170e58551 Support PNG in the custom symbol selection dialog
Fix #699
2015-10-07 17:53:07 +02:00
grossmj
02968f1ece Add custom messages when computing Idle-PC values. Fixes #704. 2015-10-07 08:41:14 -06:00
Julien Duponchelle
eb94ba8b93 Display the version of Qt in the console 2015-10-07 14:28:47 +02:00
Julien Duponchelle
67c6e03f3b Qt 4.3 is no longer supported 2015-10-07 14:26:16 +02:00
Julien Duponchelle
faa3b519b6 Do not crash when parsing a Qt version with a snapshot notation
Fix #697
2015-10-07 10:39:50 +02:00
Julien Duponchelle
3b4dca7545 Merge branch 'master' into unstable 2015-10-07 10:35:12 +02:00
Julien Duponchelle
66495cbf83 Force nc path to /usr/bin/nc on Apple
Homebrew or macport can break it. Because they install the GNU
verison which doesn't support socket unix.

Fix #664
2015-10-07 10:32:53 +02:00
Julien Duponchelle
95fea87b29 Revert "Drop netcat for unix socket it's not supported by OSX"
This reverts commit a0271926e6.
2015-10-07 10:32:09 +02:00
Julien Duponchelle
4b0cea36d0 Catch errors when we have an infinite recursion when copying a folder
Fix #696
2015-10-07 10:16:21 +02:00
Julien Duponchelle
ec27683f0e Fix crash in recent files when changing locale
Fix #690
2015-10-07 10:12:12 +02:00
Julien Duponchelle
a4d7aaf0a1 Merge branch 'master' into unstable 2015-10-07 10:05:00 +02:00
Julien Duponchelle
e2bcd1fdd1 Catch error when we can't extract egg
Fix #693
2015-10-07 10:04:01 +02:00
Julien Duponchelle
d7fd04de03 Fix securecrt command line (backported from master) 2015-10-07 10:01:20 +02:00
Julien Duponchelle
f447cd9be6 Merge branch 'master' into unstable 2015-10-06 19:17:26 +02:00
grossmj
a080b65615 Updates SecureCRT command line. 2015-10-06 11:10:50 -06:00
Julien Duponchelle
bd034237c0 When it's an ova explain to user he need to download the ova 2015-10-06 18:00:10 +02:00
Julien Duponchelle
66aff0d9a2 OVA file support 2015-10-06 17:49:55 +02:00
Julien Duponchelle
04a4142128 Ignore .cache directory 2015-10-06 10:57:50 +02:00
Julien Duponchelle
ec71da4062 Fix update manager crash on Windows 2015-10-06 10:57:33 +02:00
Julien Duponchelle
0ae97aa933 Support for image in local subdirectory
Ref #700
2015-10-05 11:38:57 +02:00
Julien Duponchelle
580e19de0c Fix duplicate code 2015-10-05 08:51:47 +02:00
grossmj
e41e95b0e6 Fixes issue when saving Idle-PC into template. Fixes #674. 2015-10-04 14:56:34 -06:00
Julien Duponchelle
1adeee9ba1 Add link for downloading VMware 2015-10-02 18:17:09 +02:00
Julien Duponchelle
b598339e55 Cache md5sum in memory when loading a gns3a
Fix #681
2015-10-02 18:12:47 +02:00
Julien Duponchelle
c65e5d8795 Support symbol import from GNS3A
Fix #692
2015-10-02 17:57:49 +02:00
Julien Duponchelle
b8597bc196 Allow user to select symbol from his library
Fix #691
2015-10-02 17:19:45 +02:00
Julien Duponchelle
be75a8e2b8 Merge branch 'master' into unstable 2015-10-02 13:21:28 +02:00
Julien Duponchelle
2352840c3b Improve HTTP progress reliability 2015-10-02 11:21:44 +02:00
Julien Duponchelle
98f9f59af3 Support ubuntu default VNC client (Vinagre)
Fix #687
2015-10-02 11:11:05 +02:00
grossmj
00ba2f1046 Merge remote-tracking branch 'origin/master' 2015-10-02 03:07:39 -06:00
grossmj
0389245cb7 Adds the COPYING file. 2015-10-02 03:06:52 -06:00
Julien Duponchelle
2f746ff791 Fix error when receiving an HTTP error during HTTP progress
Fix #686
2015-10-02 11:05:47 +02:00
Julien Duponchelle
8b881dfd20 Support port_name_format in GNS3 a files
Fix #684
2015-10-01 15:59:50 +02:00
Julien Duponchelle
cfece32185 options is not mandatory in a .gns3 file 2015-10-01 15:12:24 +02:00
Julien Duponchelle
d09ce859aa Merge branch 'master' into unstable 2015-09-30 09:41:52 +02:00
Julien Duponchelle
227a8a0c9e Xshell 5 support
Source: http://www.packetctrl.com/xshell-5-and-gns3-integration/
2015-09-30 09:38:48 +02:00
Julien Duponchelle
1b0da301e3 Add missing gns3-converter to requirements.txt 2015-09-30 08:21:54 +02:00
Julien Duponchelle
0f3efaa3fe Add missing gns3-converter in requirements.txt 2015-09-25 20:08:54 +02:00
grossmj
55b5f49c79 Fixes Qemu binaries not listed in the node configuration dialog. Fixes #683. 2015-09-25 06:06:22 -06:00
grossmj
3601acf66e Fixes SecureCRT command line. 2015-09-24 15:15:43 -06:00
Julien Duponchelle
383306c0ab Speedup directory scan for images when loading a gns3a file
Fix #682
2015-09-23 14:58:04 +02:00
Julien Duponchelle
5f87be16d7 Show a progress bar during directory scan when searching for appliances
Fix #680
2015-09-23 14:37:37 +02:00
Julien Duponchelle
63ab01d364 Search image by default also in the download directory 2015-09-23 14:22:59 +02:00
grossmj
4252f19819 Fixes issue when Telnet doesn't let you to login to an appliance on Linux. 2015-09-22 16:11:35 -06:00
grossmj
1095ed1663 Bump version to 1.4.0.dev9 2015-09-22 16:07:21 -06:00
grossmj
c5557e45c4 Fixes issue when Telnet doesn't let you to login to an appliance on Linux. 2015-09-22 16:06:35 -06:00
Julien Duponchelle
f5159a93f3 Prepare 1.4.0b3 2015-09-22 17:20:24 +02:00
Julien Duponchelle
b00d39e531 Add a warning if you don't select VMware or VBox in Setup Wizard
Fix #677
2015-09-22 15:13:56 +02:00
Julien Duponchelle
0c077b5647 Fix Configuration not always saved in client
Fix #678
2015-09-22 13:38:34 +02:00
grossmj
98aed2a8b6 Fixes file path reference issue. 2015-09-22 03:46:50 -06:00
Julien Duponchelle
0523873e5e Fix Appliance doesn't work on local Server #669 2015-09-22 11:16:40 +02:00
Julien Duponchelle
ed5ce93df6 Merge remote-tracking branch 'origin/master' into unstable 2015-09-22 11:01:44 +02:00
Julien Duponchelle
ab4f85b862 Merge branch 'master' into unstable 2015-09-22 11:01:18 +02:00
grossmj
67912a918f Allows Qemu VM template editing if the server is not running. Fixes #671. 2015-09-20 11:39:36 -06:00
grossmj
0af939bd37 Fixes NIO_VMNET != NIO_VMnet. 2015-09-19 03:34:10 -06:00
grossmj
b712b54786 Bump version to 1.4.0dev8 2015-09-18 15:41:34 -06:00
grossmj
19bf52661e Automatically add the -no-kvm option if -icount is detected to help with the migration of ASA VMs created before version 1.4 2015-09-18 15:38:55 -06:00
grossmj
c0c0261c68 Some improvements. 2015-09-18 09:46:51 -06:00
Julien Duponchelle
1eea941b55 Try to fix the Runtime error
Fix #668
2015-09-18 09:04:31 +02:00
Julien Duponchelle
547a8fb6c4 1.4.0 beta2 2015-09-17 16:41:08 +02:00
Julien Duponchelle
a0271926e6 Drop netcat for unix socket it's not supported by OSX
Fix #664
2015-09-17 16:08:56 +02:00
Julien Duponchelle
7e796fc3f1 Drop a debug 2015-09-17 16:01:49 +02:00
Julien Duponchelle
ba17b9d789 Fix for fusion 2015-09-17 15:22:45 +02:00
Julien Duponchelle
a4c17562ad VMware fusion is supported
Fix #661
2015-09-16 22:04:13 +02:00
Julien Duponchelle
ca173fb491 Fix a crash when a process return an error in sudo 2015-09-16 17:26:44 +02:00
Julien Duponchelle
9330689641 Last 1.3.X is 1.3.10. 2015-09-16 16:12:45 +02:00
Julien Duponchelle
ca67553e66 Fix race conditions in http_client
Fix #660
2015-09-16 15:57:59 +02:00
grossmj
dabcb6d4f4 Merge remote-tracking branch 'origin/unstable' into unstable 2015-09-16 05:26:18 -06:00
grossmj
753eb79a15 Fixes output issue when trying to run gns3vmnet.exe 2015-09-16 05:25:08 -06:00
Julien Duponchelle
034c9e8ad5 Forgot to remove this code 2015-09-16 13:19:29 +02:00
Julien Duponchelle
ba04783e8f Drop the licence POC 2015-09-16 13:16:40 +02:00
grossmj
a8ba51cdee Merge remote-tracking branch 'origin/unstable' into unstable 2015-09-16 05:11:36 -06:00
grossmj
24997c39f5 Fixes issues when trying to run gns3vmnet.exe 2015-09-16 05:11:11 -06:00
Julien Duponchelle
919311e2ab Dissalow gns3a import on local server for win and mac 2015-09-16 12:11:46 +02:00
Julien Duponchelle
61c3b0bfff Cleanup and test server select box 2015-09-16 11:54:50 +02:00
Julien Duponchelle
8024738400 Fix http_client tests 2015-09-16 11:04:03 +02:00
Julien Duponchelle
ac58bc677f On OSX show a warning when using an old Qemu
Fix #658
2015-09-16 10:13:48 +02:00
Julien Duponchelle
3b887f7f1b Refactor select code to use the same for appliance and graphics 2015-09-15 22:36:25 +02:00
Julien Duponchelle
8ea24a156e Disallow to install an appliance when an appliance twice 2015-09-15 22:11:01 +02:00
Julien Duponchelle
264fe8f0c8 Show a progress dialog when importing appliances 2015-09-15 16:11:14 +02:00
Julien Duponchelle
3f67cd4a99 Install gns3a from current directory
Fix #652
2015-09-15 15:36:26 +02:00
Julien Duponchelle
5d9e9dfc4a Tab support for xfce4-terminal 2015-09-15 11:13:30 +02:00
grossmj
95bec70e27 Improve alignments. Fixes #215. 2015-09-14 15:43:26 -06:00
grossmj
c1c6c812c8 Improve alignments. Fixes #215. 2015-09-14 15:41:37 -06:00
grossmj
0a8d947375 Fixes ethertype validation error. 2015-09-14 15:32:49 -06:00
grossmj
c9a699fc30 Bump version to 1.4.0dev7. 2015-09-14 15:19:57 -06:00
grossmj
8bf350a7dd Improve check for uBridge permissions. 2015-09-14 14:58:35 -06:00
Julien Duponchelle
bbc468db0f Fix an uuid is display instead of the server url
Fix #657
2015-09-14 18:10:16 +02:00
Julien Duponchelle
2353f074ab Set root permission to ubridge on OSX
Fix #655
2015-09-14 17:49:23 +02:00
Julien Duponchelle
79b7174a83 Show GNS3 version at startup 2015-09-14 17:49:23 +02:00
grossmj
82e1c088c7 Allows to select a remote server to run a switch. Fixes #653. 2015-09-13 11:11:14 -06:00
grossmj
1d13a3de3c Support for packet capture on VMware VM links. 2015-09-13 09:40:09 -06:00
Julien Duponchelle
0fb0bafc14 VMware OSx use local server by default 2015-09-11 16:33:31 +02:00
Julien Duponchelle
6cc5547509 Always use ubridge with VMware by default
Fix #648
2015-09-11 16:32:36 +02:00
Julien Duponchelle
a72df53b69 Another fix for docker module missing on frozen OSX application
Fix #649
2015-09-11 16:13:20 +02:00
Julien Duponchelle
f0802038db Fix docker module missing on frozen OSX application
Fix #649
2015-09-11 15:49:25 +02:00
Julien Duponchelle
631b4b7a61 Fix appliance error display when opening invalid gns3a 2015-09-11 14:37:58 +02:00
Julien Duponchelle
d9436520af Split the open menu between Open project & Open appliance
Fix #651
2015-09-11 14:33:59 +02:00
Julien Duponchelle
d805cfbd6a Fix PermissionError: [Errno 1] Operation not permitted when kill process
Fix #626
2015-09-11 14:19:54 +02:00
Julien Duponchelle
078d5023c4 VMware fusion support is no longer experimental
Fix #656
2015-09-11 14:13:33 +02:00
Julien Duponchelle
bc053da538 Support drag & drop gns3a
Fix #650
2015-09-11 12:13:02 +02:00
Julien Duponchelle
5d79bbce39 Fix crash when opening gns3a from double click 2015-09-11 09:57:21 +02:00
Julien Duponchelle
98c8d56dc5 Fix an error when loading IOU schema with private_config 2015-09-11 09:15:28 +02:00
Julien Duponchelle
1acaa18c3c UTF-8 support for appliance 2015-09-10 14:08:27 +02:00
Julien Duponchelle
8325fe0cee Try to support open a file via double click on OSX
Ref #646
2015-09-10 13:44:42 +02:00
Julien Duponchelle
eedd12207c Move HTML templates to static 2015-09-10 10:39:01 +02:00
Julien Duponchelle
64ed8c3de0 Fix open project via file menu 2015-09-10 10:24:46 +02:00
Julien Duponchelle
ab4f2f3922 Add templates as resources 2015-09-09 22:11:51 +02:00
Julien Duponchelle
fc5bf2dc4b Initial version of an appliance file format 2015-09-09 21:24:28 +02:00
Julien Duponchelle
bd0fabd1f6 Fix Missing configChangedSlot in Docker
Fix #645
2015-09-09 21:24:28 +02:00
grossmj
a4a2963bc3 Add white background for Docker symbol. 2015-09-09 10:37:03 -06:00
grossmj
310f47b52a Turn off local Docker support for Windows and OSX. Fixes #641. 2015-09-09 05:01:11 -06:00
grossmj
b4a187dd02 Adds docker symbols. Fixes #643. 2015-09-09 04:43:45 -06:00
grossmj
f4afdca576 Call the vmnet management script from the GUI (with admin rights). Implements #639. 2015-09-09 02:46:30 -06:00
Julien Duponchelle
28bebff7b0 Add a scary warning for the experimental mode 2015-09-08 21:34:53 +02:00
Julien Duponchelle
c3a4daef87 Allow to use vmware fusion with experimental support 2015-09-08 21:31:42 +02:00
Julien Duponchelle
18a5a28283 Fix crash at gui startup
Fix #642
2015-09-08 15:10:49 +02:00
Julien Duponchelle
36d08d2dca Fix Docker wizard when used with a remote server 2015-09-08 15:05:01 +02:00
Julien Duponchelle
91bf6e667d Use self update only if experimental features are allowed 2015-09-08 13:30:35 +02:00
Julien Duponchelle
a88a47e223 Fix default page in general preferences 2015-09-08 13:22:43 +02:00
Julien Duponchelle
8ede0f2089 Rename docker VMs to Docker Containers 2015-09-08 13:19:32 +02:00
Julien Duponchelle
576f6c81a1 Add an option for enabling experimental features 2015-09-08 11:50:55 +02:00
Goran Cetusic
1666fc4aa0 Backport docker support from Google Summer Of Code 2015-09-08 11:06:49 +02:00
grossmj
1216882e89 Merge branch 'Bevaz-qinq_ethertype' into unstable 2015-09-08 02:47:58 -06:00
grossmj
3f825a163b Merge branch 'qinq_ethertype' of https://github.com/Bevaz/gns3-gui into Bevaz-qinq_ethertype
Conflicts:
	gns3/modules/dynamips/ui/ethernet_switch_configuration_page_ui.py
2015-09-08 02:45:08 -06:00
Julien Duponchelle
2fcfa10573 Add a run with sudo class for OSX and Linux
Ref #639
2015-09-07 22:01:56 +02:00
Jeremy Grossmann
dd95b24d32 Merge pull request #640 from ryanchapman/master
Spelling correction
2015-09-05 15:40:40 -06:00
Ryan A. Chapman
bb2777ed5b Spelling correction 2015-09-05 15:26:22 -06:00
grossmj
e0f03ec582 Allows VMware VMs to use vmnet interfaces for connections without using uBridge. 2015-09-05 14:38:44 -06:00
Julien Duponchelle
51e844559e Prepare next release 2015-09-04 21:34:20 +02:00
Julien Duponchelle
dc4a984c41 Prepare version 1.3.10 2015-09-04 18:13:18 +02:00
Julien Duponchelle
54139845ac Add a firewall symbol
Fix #631
2015-09-04 10:41:54 +02:00
grossmj
8f9672d9e2 Updates kernel command line of ASA. 2015-09-03 16:52:36 -06:00
Julien Duponchelle
b007aea2f5 Detect broken link in topologies
Ref #635
2015-09-03 18:26:59 +02:00
Julien Duponchelle
3900645d1f Fix project not closing
https://github.com/GNS3/gns3-server/issues/305
2015-09-03 16:54:48 +02:00
Julien Duponchelle
5d644467fc Fix autostart
Fix #633
2015-09-03 15:16:23 +02:00
Julien Duponchelle
29fcd07b1c Merge branch 'master' into unstable 2015-09-03 10:20:37 +02:00
Julien Duponchelle
22c2ee31c4 Fix file not found exception in vpcs list dir
Fix #632
2015-09-03 10:19:55 +02:00
Julien Duponchelle
69f99742a8 Add missing virtio-net-pci to the json schema 2015-09-02 11:03:23 +02:00
Julien Duponchelle
e7dc901d5e Fix the all devices views
Fix #630
2015-08-28 20:55:59 +02:00
Julien Duponchelle
4dba7fa7fc Fix double click event on Note Item
Fix #629
2015-08-28 15:44:10 +02:00
Julien Duponchelle
55d4201aaf Fix Accepting insecure https connections creates additional server entry
And fix server password is regenerated

Fix #605, #627
2015-08-27 15:40:31 +02:00
Julien Duponchelle
74a4e464c8 Allow developer to debug packet capture on Windows 2015-08-26 18:25:10 +02:00
Julien Duponchelle
329c196047 Merge branch 'master' into unstable 2015-08-26 14:36:40 +02:00
Julien Duponchelle
85e74b482e Fix saveAs error unsupported operand type(s) for +=: 'NoneType' and
'str'

Fix #615
2015-08-26 14:16:04 +02:00
Julien Duponchelle
dc12fdd1c9 Drop unused test 2015-08-26 13:54:29 +02:00
Julien Duponchelle
0ee64ecbb8 Catch error when antivirus corrupt our own JSON errors
Fix #611
2015-08-26 13:53:04 +02:00
Julien Duponchelle
2946e931aa Add a note about VIX API require for VMware player 2015-08-26 13:21:33 +02:00
Julien Duponchelle
30871f0cd1 Return original path in case of error when moving image 2015-08-26 10:06:07 +02:00
Julien Duponchelle
70aa165444 Create image directory if not exists 2015-08-26 10:04:41 +02:00
Julien Duponchelle
beb35c6108 Allow GUI to be start with python -m gns3 2015-08-25 18:45:16 +02:00
Julien Duponchelle
90d1cb9a7f Avoid errors in qemu configuration if server has been deleted
Fix #610
2015-08-25 10:20:48 +02:00
Julien Duponchelle
5754d66560 Fix error when the IOS image directory is not writable
Fix #614
2015-08-25 10:05:28 +02:00
Julien Duponchelle
77ba02f0ae Do not crash if something intercept the call to the update server
Fix #620
2015-08-25 10:02:24 +02:00
Julien Duponchelle
effdc862a2 Fix super(): no arguments in SSH client
Fix #622
2015-08-25 09:54:02 +02:00
Julien Duponchelle
59c4736f41 Catch error when configuration file contain invalid UTF-8 chars
Fix #613
2015-08-25 09:50:10 +02:00
grossmj
dff831eafe Merge remote-tracking branch 'origin/unstable' into unstable 2015-08-24 20:18:55 -06:00
grossmj
4765640dc7 Fixes #621. 2015-08-24 20:16:59 -06:00
Julien Duponchelle
b8799a91b4 1.4.0dev6 2015-08-24 17:15:30 +02:00
Julien Duponchelle
17aaa90fb1 Fix JSON schema for dynamips power supply and sensors 2015-08-24 15:35:13 +02:00
Julien Duponchelle
40b8969d44 Fix tests conflict between request fixture and our http_request fixture
Also turn on stdout capture.
2015-08-24 15:09:30 +02:00
Julien Duponchelle
73454d97e1 Fix missing boot_priority in JSON schema 2015-08-24 14:40:05 +02:00
Julien Duponchelle
7cf39aac5a Complete the error message about corrupted topologies
I hope users will understand it better and send
us the .gns3 file with the error.
2015-08-24 14:06:12 +02:00
grossmj
2880168566 Removes ASAv warning in Qemu Wizard. 2015-08-22 17:20:37 -06:00
Anton Fedotov
615d9240ad EthernetSwitch: Allow to choose ethertype for QinQ outer tag. 2015-08-20 10:06:59 +03:00
grossmj
76f8aa4f60 Adds missing properties for rectangle and ellipse in schema validation. 2015-08-17 21:46:32 -06:00
grossmj
0172dd0b53 It is not necessary to catch the FileExistsError exception. 2015-08-13 20:16:41 -06:00
grossmj
ccb6a5cf0f Use Qemu 0.11.0 instead of version 0.13.0 on Windows. 2015-08-11 17:03:12 -06:00
grossmj
7dc37c9dca Removes "resources_type" references. Fixes #493. 2015-08-10 22:21:10 -06:00
grossmj
69f13621ca A thread is not needed to check for local config file changes. Fixes #607. 2015-08-10 18:33:59 -06:00
grossmj
92b9efea12 Merge remote-tracking branch 'origin/unstable' into unstable 2015-08-07 10:42:43 -06:00
grossmj
d1af92942d Fixes bug when validating VMware wizard page. 2015-08-07 10:42:26 -06:00
Julien Duponchelle
b47b5e2dc0 1.4.0beta1 2015-08-07 18:37:35 +02:00
Julien Duponchelle
3265a94d26 Show an error id you try to use a local server not started 2015-08-07 18:30:43 +02:00
Julien Duponchelle
e9d9dc2748 CLear the list before asking for VM list for Vbox and Vmware 2015-08-07 18:09:38 +02:00
Julien Duponchelle
8b38d4967c Fix bugs report by elhers 2015-08-07 17:35:26 +02:00
Julien Duponchelle
3729070e33 Fix password lost for remote servers
Fix #603
2015-08-07 15:31:07 +02:00
Julien Duponchelle
d415c20446 Unused import 2015-08-07 15:19:51 +02:00
Julien Duponchelle
efb5931be3 Fix schema for virtualbox 1.3.2 topologies
https://community.gns3.com/message/40214
2015-08-07 15:17:44 +02:00
Julien Duponchelle
4867a5510a Refactor all VM wizards
* All the wizards inherit of vm_wizard even VMware and VirtualBox
* Code for managing images is in vm_wizard_with_images (Qemu, Dynamips,
IOU)
* If GNS3VM is not running you can't select it at first step

Fix #604
2015-08-07 14:30:56 +02:00
Julien Duponchelle
932dd79ac1 Fix Apply not enabled when removing a remote server
Fix #602
2015-08-07 11:42:48 +02:00
Julien Duponchelle
dc5957dd0a Fix remote server list display when use_local_server for Qemu 2015-08-07 09:53:03 +02:00
grossmj
bb131b4ff5 Keep the "use local server" settings to select the default radio button in VM wizards. Ref #598. 2015-08-06 23:43:12 -06:00
grossmj
76e3d3523e Fixes #601 (spelling error). 2015-08-06 15:11:48 -06:00
Julien Duponchelle
2bc9dd2802 Store the url of server in gns3_gui for third party apps 2015-08-06 17:23:04 +02:00
Julien Duponchelle
0950c3d80d Correctly handle remote server in qemu wizard
Fix #599
2015-08-06 16:35:03 +02:00
Julien Duponchelle
808ea00787 Fix error when editing a qemu device with a relative path
Fix #594
2015-08-06 14:48:50 +02:00
grossmj
c4400f8a64 Fixes issue in VirtualBox wizard when no local server is enabled in the VirtualBox general settings. 2015-08-05 22:42:16 -06:00
grossmj
4a78cfe00a Catch exception when starting packet capture reader for a remote packet capture. Fixes #597. 2015-08-05 19:07:54 -06:00
grossmj
a10f8939e2 Fixes bug when opening Node properties dialog via a double click. 2015-08-05 18:53:56 -06:00
grossmj
4b681a5e55 SecureCRT (installed on personal profile) command line. 2015-08-05 18:45:59 -06:00
grossmj
4e073b4681 Fixes KeyError: 'vmx_path'. Fixes #595. 2015-08-05 17:38:28 -06:00
grossmj
e9548b2e03 Support for CPUs setting for Qemu VMs. 2015-08-05 17:17:55 -06:00
Julien Duponchelle
05820e973b Fix chicken of VNC command and add a warning about bugs in OSX VNC
Fix #593
2015-08-05 14:57:43 +02:00
grossmj
f779b6fe7d Bump version to 1.4.0dev5. 2015-08-04 15:29:03 -06:00
Julien Duponchelle
210c46f49d Fix issue whith auto update when we release a new build 2015-08-04 20:11:48 +02:00
Julien Duponchelle
e746ddc525 1.4.0alpha4 2015-08-04 19:48:09 +02:00
grossmj
d186d4ce1e Use half the available physical memory for the GNS3 VM in the Setup Wizard. 2015-08-04 11:32:40 -06:00
Jeremy Grossmann
cee80cc579 Merge pull request #585 from GNS3/wizard_step_1
Show the server choice in wizard if you have a VM or a remote server
2015-08-04 11:31:13 -06:00
Jeremy Grossmann
1c0232cf96 Merge pull request #591 from GNS3/drop_resource_type
Drop useless notion of resource_type
2015-08-04 11:20:23 -06:00
Julien Duponchelle
77ff7a8aba Fix tests 2015-08-04 18:03:47 +02:00
Julien Duponchelle
fed48de0d5 Drop useless notion of resource_type 2015-08-04 17:58:47 +02:00
Julien Duponchelle
95c1df229f Fix When you modify config from outside router list is not refresh
Fix #590
2015-08-04 17:51:31 +02:00
Julien Duponchelle
78b2cb8a32 Sync auth settings between 1.3 and 1.4
Fix #589
2015-08-04 15:25:15 +02:00
Julien Duponchelle
e1ba7a0ad6 Fix merge error 2015-08-04 15:24:18 +02:00
Julien Duponchelle
e64597eaf0 Group bad request by path
Fix #587
2015-08-04 14:57:03 +02:00
Julien Duponchelle
f1b7ea13ef Fix behavior of wizard for vmware and virtualbox 2015-08-04 12:46:31 +02:00
Julien Duponchelle
25d6a0a160 Disallow connection to a different major version in all cases
And test all cases

Fix #586
2015-08-04 12:35:58 +02:00
Julien Duponchelle
5be13a645e Merge branch 'master' into unstable 2015-08-03 19:24:30 +02:00
Julien Duponchelle
dfa85e9886 Prepare version 1.3.10 2015-08-03 19:21:48 +02:00
Julien Duponchelle
8a612970e6 1.3.9 2015-08-03 18:56:10 +02:00
Julien Duponchelle
f18a9fe7f5 Show the server choice in wizard if you have a VM or a remote server
It's easier for using instead of searching the screen with use
local server options.

But I think it's not complete because we still have the checkbox
in the preferences.

When I read the code I found this:
cd9bb16f79/gns3/modules/builtin/__init__.py (L72)

And I'm not sure in which case it's call. I think it's only for
VPCS and things like Dynamips switch. Is it right? If it's the case
we can
drop the checkbox from IOU, Qemu, VMware, VirtualBox
2015-08-03 15:53:59 +02:00
Julien Duponchelle
a5d0fe5b8b Fix tests 2015-08-03 12:00:55 +02:00
Julien Duponchelle
3ef70a1213 Fix local server settings erased at each launch
Fix #580
2015-08-03 11:44:55 +02:00
grossmj
3bd2136450 Support for Qemu disk interfaces, cd/dvd-rom image and boot priority. 2015-08-02 23:02:17 -06:00
grossmj
c165146d7c Prevents progress dialog to stay displayed (fixes graphical bug). 2015-08-01 15:19:57 -06:00
grossmj
adfdae3a8e Check that DHCP is enabled on the VirtualBox host-only network associated with the VirtualBox GNS3 VM. Fixes #287. 2015-08-01 14:48:41 -06:00
grossmj
f92aae4e6e Fixes Qt5 incompatibility. 2015-08-01 11:58:32 -06:00
grossmj
bed867cf7b Catch exception when trying to launch Wireshark. 2015-08-01 11:55:26 -06:00
grossmj
81aaeaf25f Adds traceback information when there is an assertion error with topologyFile. 2015-08-01 11:54:06 -06:00
grossmj
3cc62563df Catch exception when trying to launch Wireshark. 2015-08-01 11:49:40 -06:00
grossmj
aaa21be642 Backport: fixes migration of cloud interfaces. 2015-08-01 11:28:12 -06:00
grossmj
aa87cb0064 Fixes migration of cloud interfaces. Fixes #582. 2015-08-01 11:19:12 -06:00
grossmj
1e6e2d32e3 Wait for the server to be fully started in the GNS3 VM. Fixes #581. 2015-07-31 15:42:47 -06:00
Julien Duponchelle
a95805bc04 1.4.0dev4 2015-07-31 15:34:10 +02:00
Julien Duponchelle
a5ccce30d6 Fix issue with file upload and Qt 5.5
For an unknow reason it's stop working with Qt 5.5
the fix is to use QtCore.Qt.QueuedConnection on the slot
in order to avoid blocking the HTTP thread.

Fix #584
2015-07-31 12:21:50 +02:00
grossmj
f6fdd79d0f Fixes version. 2015-07-30 19:07:25 -06:00
grossmj
3f9c1af8e8 Improves and fixes a bug with the Symbol dialog. 2015-07-29 21:58:54 -06:00
grossmj
be9a88029b Improves the symbol dialog. Implements #514. 2015-07-29 18:21:14 -06:00
518 changed files with 149363 additions and 149590 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
gns3/version.py merge=ours

2
.gitignore vendored
View File

@@ -58,3 +58,5 @@ keys
# Custom config
/gns3_server.ini
updates
.cache
__pycache__

View File

@@ -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
View File

@@ -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
View 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 changeslittle bugfixes, correcting typos, and the likeplease 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 youre 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 dont “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
View 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
View 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

View File

@@ -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 *

View File

@@ -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

View File

@@ -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
View 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
View 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;

View File

@@ -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
View 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
View 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
View 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
View 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

View 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]

View File

@@ -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

View File

@@ -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__)

View File

@@ -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
View 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)

View File

@@ -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

View 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()

View 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())

View File

@@ -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

View 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)

View 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_()

View 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_()

View 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)

View 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())

View 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)

View File

@@ -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))

View 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_()

View File

@@ -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)

View File

@@ -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

View File

@@ -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.

View 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_()

View 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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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())

View 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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():

View File

@@ -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
View 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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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
View 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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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()

View File

@@ -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)

View File

@@ -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:

View File

@@ -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()

File diff suppressed because it is too large Load Diff

View File

@@ -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]

View File

@@ -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():

View 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"

View File

@@ -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]

View 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

View 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

View 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

View 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"

View 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"

View 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"

View File

@@ -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
View 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"

View File

@@ -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

View 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)

View File

@@ -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

View 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)

View 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

View 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)

View File

@@ -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

View 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)

View File

@@ -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

View 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": [],
}

View File

@@ -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:"))

View 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