Compare commits

...

998 Commits

Author SHA1 Message Date
grossmj
9d68302a5d Release v3.0.6 2026-01-28 22:30:37 +08:00
grossmj
071181717b Fix Qemu VM wizard after PyQt6 migration 2026-01-28 22:23:44 +08:00
grossmj
dee6d50723 Fix issues after PyQt6 migration 2026-01-28 18:54:17 +08:00
grossmj
3a81825cd1 Fix ImageDialog to support PyQt6 2026-01-27 22:06:32 +08:00
grossmj
26754082a0 Merge branch '2.2' into 3.0 2026-01-27 22:05:56 +08:00
grossmj
c4881446e1 Fix cannot add IOS. Ref #3779 2026-01-27 22:01:55 +08:00
grossmj
cce0bd4009 Fix cannot add IOU. Fixes #3779 2026-01-27 21:55:30 +08:00
grossmj
4c80723128 Merge branch '2.2' into 3.0
# Conflicts:
#	dev-requirements.txt
#	requirements.txt
#	setup.py
2026-01-24 19:23:46 +08:00
grossmj
de5ad831e2 Upgrade dependencies 2026-01-24 19:21:41 +08:00
grossmj
8593357ab0 Drop Python 3.8 support 2026-01-24 18:52:09 +08:00
grossmj
b8da378212 Warn that qt6-svg-plugins must be installed 2026-01-24 18:32:42 +08:00
grossmj
571f816508 Fix PyQt6 errors after merging 2026-01-24 18:09:02 +08:00
grossmj
4165939c99 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/application.py
#	gns3/controller.py
#	gns3/crash_report.py
#	gns3/dialogs/appliance_wizard.py
#	gns3/dialogs/project_export_wizard.py
#	gns3/dialogs/setup_wizard.py
#	gns3/dialogs/style_editor_dialog.py
#	gns3/dialogs/vm_with_images_wizard.py
#	gns3/http_client.py
#	gns3/items/drawing_item.py
#	gns3/link.py
#	gns3/local_server.py
#	gns3/main_window.py
#	gns3/modules/builtin/ui/cloud_configuration_page_ui.py
#	gns3/modules/docker/dialogs/docker_vm_wizard.py
#	gns3/modules/docker/ui/docker_vm_configuration_page_ui.py
#	gns3/modules/dynamips/ui/ios_router_configuration_page_ui.py
#	gns3/modules/qemu/dialogs/qemu_image_wizard.py
#	gns3/modules/qemu/dialogs/qemu_vm_wizard.py
#	gns3/modules/qemu/pages/qemu_vm_configuration_page.py
#	gns3/modules/qemu/ui/qemu_image_wizard_ui.py
#	gns3/modules/qemu/ui/qemu_vm_configuration_page_ui.py
#	gns3/modules/qemu/ui/qemu_vm_wizard_ui.py
#	gns3/modules/traceng/dialogs/traceng_node_wizard.py
#	gns3/modules/traceng/pages/traceng_node_configuration_page.py
#	gns3/modules/traceng/pages/traceng_node_preferences_page.py
#	gns3/modules/traceng/ui/traceng_node_configuration_page_ui.py
#	gns3/modules/traceng/ui/traceng_node_preferences_page_ui.py
#	gns3/modules/traceng/ui/traceng_node_wizard_ui.py
#	gns3/modules/traceng/ui/traceng_preferences_page_ui.py
#	gns3/modules/virtualbox/dialogs/virtualbox_vm_wizard.py
#	gns3/modules/vmware/dialogs/vmware_vm_wizard.py
#	gns3/nodes_view.py
#	gns3/qt/__init__.py
#	gns3/settings.py
#	gns3/ui/appliance_wizard_ui.py
#	gns3/ui/controller_preferences_page_ui.py
#	gns3/ui/edit_compute_dialog_ui.py
#	gns3/ui/edit_project_dialog_ui.py
#	gns3/ui/export_project_wizard_ui.py
#	gns3/ui/general_preferences_page.ui
#	gns3/ui/general_preferences_page_ui.py
#	gns3/ui/main_window_ui.py
#	gns3/ui/setup_wizard_ui.py
#	gns3/utils/export_project_worker.py
#	gns3/version.py
2026-01-24 17:36:48 +08:00
grossmj
5b2a102ef4 Development on 2.2.57.dev1 2026-01-24 16:53:48 +08:00
grossmj
48b0e96e37 Display a warning if SVG image format isn't supported 2026-01-24 01:14:12 +08:00
grossmj
702709b7f9 Fix error in profile selection window after PyQt6 migration 2026-01-23 18:46:49 +08:00
grossmj
b2b50ce273 Release v2.2.56 2026-01-21 22:15:17 +08:00
grossmj
fe2537319b Merge remote-tracking branch 'origin/master' into 2.2 2026-01-17 18:38:34 +08:00
grossmj
ec5800d7fc Fix XDG Config Home support.Fixes #3770 2026-01-17 18:35:53 +08:00
grossmj
ce4b74e4ea Merge remote-tracking branch 'origin/master' into 2.2 2026-01-17 18:29:24 +08:00
grossmj
b3579861d3 Fix double click on device view 2026-01-17 18:19:47 +08:00
grossmj
37ffd741e3 Revert "MacOS testing, disable startup loading"
This reverts commit 6a91bf22f1.
2026-01-09 17:38:13 +08:00
grossmj
6a91bf22f1 MacOS testing, disable startup loading 2026-01-09 17:05:48 +08:00
Jeremy Grossmann
8dc3ef0784 Merge pull request #3773 from SalehAlolayan/master
Fixing tab name in MobaXterm
2026-01-09 15:34:53 +08:00
Saleh Alolayan
862f77fa7b Fixing tab name in MobaXterm 2026-01-02 00:28:32 +03:00
grossmj
55d08125d5 Use pyrcc5 to compile resources 2025-12-16 19:00:17 +08:00
grossmj
81fe00bd31 Add old resources_rc.py 2025-12-16 18:32:18 +08:00
grossmj
0f613c03c6 Reset fonts in preferences.
Ref https://github.com/GNS3/gns3-gui/issues/3547#issuecomment-3659410038
2025-12-16 17:41:53 +08:00
grossmj
64eb41d275 FollowRedirectsAttribute is not supported in Qt6.
Ref https://github.com/GNS3/gns3-gui/issues/3547#issuecomment-3659410038
2025-12-16 17:41:05 +08:00
grossmj
52feb4a07b Development on v2.2.56.dev1 2025-12-16 16:46:35 +08:00
grossmj
b2c5ed755e Fix small issues after PyQt6 migration 2025-12-16 13:19:17 +08:00
Jeremy Grossmann
1219877a90 Merge pull request #3767 from GNS3/pyqt6-migration
PyQt6 migration
2025-12-15 19:42:19 +08:00
grossmj
1cdec97a12 Finalizing PyQt6 migration 2025-12-15 19:27:55 +08:00
grossmj
e6a083eb0e Fix event position issues 2025-12-14 19:52:17 +08:00
grossmj
370dd6ed82 Base migration to PyQt6 2025-12-14 18:19:47 +08:00
Jeremy Grossmann
d892b77012 Merge pull request #3766 from fluoriteByte/master
Add XDG Config Home support
2025-12-13 19:34:58 +10:00
illy
07a8657ec1 Add XDG Config Home support 2025-12-06 03:06:35 +01:00
grossmj
d385195346 Add Python 3.14 in pyproject.toml 2025-11-29 11:12:30 +10:00
grossmj
10e66c648d Merge remote-tracking branch 'origin/2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3/crash_report.py
#	gns3/version.py
#	setup.py
2025-11-29 11:11:10 +10:00
grossmj
e224a9ac29 Release v2.2.55 2025-11-19 12:13:57 +10:00
grossmj
5b87f36cfd Fix SyntaxWarning: invalid escape sequence. Fixes #3760 2025-11-17 11:47:55 +10:00
grossmj
a9354da184 Support for Python 3.14 2025-11-16 19:30:47 +10:00
grossmj
91d7ed187b Upgrade dependencies 2025-11-16 19:16:44 +10:00
grossmj
0f8724edc4 Merge branch 'master' into 2.2 2025-09-01 18:42:19 +02:00
grossmj
4e7f480af4 Upgrade pywin32 package 2025-08-30 22:02:16 +02:00
grossmj
5ce4863baa Upgrade sentry-sdk package 2025-08-30 21:51:24 +02:00
Jeremy Grossmann
bb5bfee256 Merge pull request #3740 from Raizo62/master
"console connect to all nodes" is now case-insensitively
2025-07-12 18:41:36 +02:00
Raizo62
0a74872ea2 Clicking the "console connect to all nodes" opens all consoles in name order with case-insensitively
ChatGPT : Use `sorted(nodes.keys(), key=str.casefold)` instead of simple sorted()
to ensure case-insensitive ordering. Improves UX by merging uppercase
and lowercase entries seamlessly. Python 3.11+ recommended approach.
2025-06-21 12:00:12 +02:00
grossmj
e20aec1f80 Update pyproject.toml to support LICENSE file 2025-06-09 22:24:29 +02:00
grossmj
bd9430094a Release v3.0.5 2025-05-14 19:11:45 +02:00
grossmj
c0d3d51c5c Merge remote-tracking branch 'origin/2.2' into 3.0 2025-05-14 16:39:16 +02:00
grossmj
20ed599e65 Merge branch 'master' into 2.2 2025-05-13 20:48:44 +02:00
grossmj
3720f9e3e3 Merge remote-tracking branch 'origin/2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3/crash_report.py
#	gns3/version.py
2025-04-21 20:07:07 +07:00
grossmj
2722b2854b Development on 2.2.55.dev1 2025-04-21 19:35:19 +07:00
Jeremy Grossmann
6b32d8bad6 Merge pull request #3727 from GNS3/release/v2.2.54
Release v2.2.54
2025-04-21 19:30:42 +07:00
grossmj
fcf62fc507 Release v2.2.54 2025-04-21 17:15:26 +07:00
grossmj
c811c270ec Merge branch 'master' into 2.2 2025-04-21 17:08:42 +07:00
grossmj
dbc519e0af Development on 2.2.54.dev1 2025-04-18 15:05:37 +07:00
grossmj
5107f14e31 Merge branch '2.2' into 3.0 2025-04-16 17:51:51 +07:00
grossmj
bcbf8be182 Replace "Docker hub" by "Docker repository" because it is possible to use different repositories 2025-04-16 17:42:44 +07:00
grossmj
43df10520f Upgrade dependencies 2025-04-16 17:08:13 +07:00
grossmj
25455c116b Merge branch '2.2' into 3.0 2025-04-16 16:55:51 +07:00
grossmj
55b37716a3 Fix bring console in front when clicking on "Open all consoles". Fixes #3706 2025-04-16 16:55:11 +07:00
grossmj
882ec5a4cf Merge branch '2.2' into 3.0 2025-04-16 16:09:40 +07:00
grossmj
5f7cf2cc42 Upgrade dependencies 2025-04-16 14:39:41 +07:00
grossmj
2c4d3b4be2 Allow password greater than 8 characters. Ref #3714 2025-03-16 20:27:17 +07:00
grossmj
1fbc0d7d97 Development on 3.0.5.dev1 2025-02-25 22:39:39 +08:00
grossmj
fefd1097de Release v3.0.4 2025-02-25 18:24:32 +08:00
grossmj
4c35ea7f17 Upgrade dependencies 2025-02-22 21:10:34 +10:00
grossmj
200fbc533b Fix auto idle-pc for IOS templates 2025-02-22 20:14:19 +10:00
grossmj
c94f63e636 Add user info and password change for logged-in user. Fixes #3698 2025-02-19 18:26:08 +10:00
grossmj
e08253e362 Add -F arg to wmctrl. Ref #3706 2025-02-16 00:10:10 +10:00
grossmj
f53124f09a Development on 3.0.4 2025-01-23 13:31:37 +10:00
grossmj
d7a51ed588 Release v3.0.3 2025-01-22 19:06:43 +10:00
grossmj
dbca1b7106 Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3/crash_report.py
#	gns3/version.py
2025-01-22 18:37:46 +10:00
grossmj
15e5cac33b Set minimum duration for progress dialog when uploading. Ref https://github.com/GNS3/gns3-gui/issues/3682 2025-01-21 16:35:11 +10:00
Jeremy Grossmann
b9a59183a1 Merge pull request #3699 from GNS3/release/v2.2.53
Release v2.2.53
2025-01-21 09:46:53 +07:00
grossmj
342ca95bd2 Release v2.2.53 2025-01-21 11:52:12 +10:00
grossmj
8191a51b2b Add logs when uploading images to the controller 2025-01-20 11:39:19 +10:00
grossmj
7d112551a8 Option to disable SSL certificate verification for future connections. Fixes https://github.com/GNS3/gns3-gui/issues/3694 2025-01-19 13:23:06 +10:00
grossmj
b5e867f2cd Fix packet capture when connected to a controller with SSL. Fixes https://github.com/GNS3/gns3-gui/issues/3696 2025-01-19 10:49:05 +10:00
grossmj
e5632e565d Upgrade pytest to the latest version 2025-01-17 18:47:36 +10:00
grossmj
bd785bf6cd Update status after importing an image when installing a new appliance. Fixes #3691 2025-01-07 17:53:52 +07:00
grossmj
2ae788a8f5 Update file browser filters to find IOU images without extension. Fixes #3692 2025-01-07 11:36:48 +07:00
grossmj
d7c1754323 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/main_window.py
#	gns3/modules/traceng/pages/traceng_preferences_page.py
2025-01-07 11:33:41 +07:00
grossmj
187ef561fd Update file browser filters for all files and IOU images 2025-01-07 11:32:17 +07:00
grossmj
ad2ce4cfef Development on 3.0.3.dev1 2025-01-07 11:12:06 +07:00
grossmj
9dd4020666 Release v3.0.2 2025-01-03 21:44:01 +07:00
grossmj
83d0957d50 Add button to create templates based on images that are not used by any yet 2025-01-03 21:34:17 +07:00
grossmj
1767a441ab Add "prune" images button in image management dialog. 2025-01-02 22:53:36 +07:00
grossmj
41b7c2c33e Use the controller image endpoint to install appliances 2024-12-30 22:10:36 +07:00
Jeremy Grossmann
1cebcabff3 Merge pull request #3688 from GNS3/drop-python3.8
Drop Python 3.8
2024-12-30 15:39:19 +07:00
grossmj
53cc823859 Drop Python 3.8 2024-12-30 15:36:45 +07:00
grossmj
dd6329a1f3 Add image info tooltip in image management dialog. 2024-12-30 12:00:46 +07:00
grossmj
97070718fa Upgrade dependencies 2024-12-30 10:49:40 +07:00
grossmj
68633c4732 Upgrade dependencies 2024-12-28 18:21:22 +07:00
grossmj
161421abcf Merge branch '2.2' into 3.0 2024-12-28 18:04:18 +07:00
grossmj
9466b2a1fb Merge branch 'master' into 2.2 2024-12-28 18:03:26 +07:00
grossmj
889d41636d Release v3.0.1 2024-12-27 21:05:25 +07:00
grossmj
878cfb2fe5 Fix issue when image is already on the local server. Fixes https://github.com/GNS3/gns3-gui/issues/3678 2024-12-26 22:05:36 +07:00
grossmj
141767e0d9 Fix image uploading when image name differs with the image name recorded in the appliance. Fixes https://github.com/GNS3/gns3-gui/issues/3682 2024-12-26 19:33:00 +07:00
grossmj
a5976a61ac Merge branch '2.2' into 3.0 2024-12-25 22:17:08 +07:00
grossmj
3edde1274b Fix Linux Mint default terminal configuration 2024-12-25 22:14:25 +07:00
grossmj
2e2a2c362f Release v3.0.0 2024-12-20 18:20:43 +07:00
grossmj
0654b79703 Bump version to 3.0.0.dev14 2024-12-17 16:51:14 +07:00
grossmj
acfcabb743 Bump version to 3.0.0.dev13 2024-12-17 16:32:30 +07:00
Jeremy Grossmann
e17e7fc033 Merge pull request #3672 from ob7/enable-css-grid-colors
Add CSS Grid Color Customization Support
2024-12-02 19:56:29 +10:00
ob7
bfe11d7976 apply grid color via css property 2024-12-02 00:13:01 -09:00
grossmj
b4271b0d7e Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3/crash_report.py
#	gns3/version.py
2024-12-02 12:03:29 +10:00
Jeremy Grossmann
2b98d51ff7 Merge pull request #3668 from GNS3/release/v2.2.52
release/v2.2.52
2024-12-02 11:35:42 +10:00
grossmj
f6fb0100e2 Development on 2.2.53.dev1 2024-12-02 11:34:00 +10:00
grossmj
804b871cd6 Release v2.2.52 2024-12-02 11:14:23 +10:00
grossmj
c604ff70c7 Change title of QMessageBox 2024-12-02 10:55:49 +10:00
grossmj
dd745e0081 Add iol extension filter to image dialog. Ref #3664 2024-11-25 11:00:28 +10:00
grossmj
c2033ccf06 Merge branch '2.2' into 3.0 2024-11-25 10:59:23 +10:00
grossmj
63a7f36cfe Add iol extension filter. Ref #3664 2024-11-25 10:59:00 +10:00
grossmj
731715f5a3 Release v3.0.0rc2 2024-11-20 14:00:49 +10:00
grossmj
fef7280e55 Merge branch '2.2' into 3.0 2024-11-20 13:29:46 +10:00
grossmj
731fee1217 Remove maximum 64GB RAM limitation for QEMU VMs. Fixes #3658 2024-11-18 14:56:00 +10:00
grossmj
c3ccbfe7b8 Merge branch '2.2' into 3.0
# Conflicts:
#	setup.py
2024-11-18 11:45:31 +10:00
grossmj
de38c4a561 Remove debug message 2024-11-17 11:46:58 +10:00
grossmj
45f5155efc Alternative fix for GUI connection to server. Ref https://github.com/GNS3/gns3-server/issues/2411 2024-11-17 11:45:32 +10:00
grossmj
5fbba828bb Fix GUI connection to server. Ref https://github.com/GNS3/gns3-server/issues/2411 2024-11-14 14:26:05 +10:00
Jeremy Grossmann
714dae44f3 Merge pull request #3659 from GNS3/feature/bring-to-front-linux
Bring to front support for consoles on Linux.
2024-11-10 18:26:21 +10:00
grossmj
c67dc19ec8 Bring to front support for consoles on Linux. 2024-11-10 18:23:01 +10:00
grossmj
1c5f6b362b Relax setuptools requirement to allow for easier Debian packaging on Ubuntu Focal & Jammy 2024-11-08 12:46:56 +10:00
grossmj
50a10ae72c Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/crash_report.py
#	gns3/version.py
2024-11-07 23:16:53 +10:00
grossmj
683400c204 Development on 2.2.52.dev1 2024-11-07 23:13:31 +10:00
Jeremy Grossmann
5efb3019f4 Merge pull request #3656 from GNS3/release/v2.2.51
release/v2.2.51
2024-11-07 23:13:11 +10:00
grossmj
23e0520cd2 Release v2.2.51 2024-11-07 15:08:27 +10:00
grossmj
996709e930 Do not include tokens in notification logs 2024-10-31 12:39:25 +10:00
grossmj
604f308dad Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3/controller.py
#	gns3/crash_report.py
#	gns3/project.py
#	gns3/version.py
#	requirements.txt
#	setup.py
2024-10-31 12:18:35 +10:00
grossmj
1199de27df Merge branch 'master' into 2.2 2024-10-28 17:32:19 +10:00
grossmj
b3140f9d8e Python 3.13 support 2024-10-26 18:38:07 +10:00
grossmj
1b50cdc341 Upgrade dependencies 2024-10-26 18:22:40 +10:00
Jeremy Grossmann
bd15734c30 Merge pull request #3649 from ob7/add-link-shortcut-key
Add Keyboard Shortcut (Ctrl+L) for Add Link Tool
2024-10-24 21:23:15 +10:00
ob7
24fff8972a Add keyboard shortcut for Add Link 2024-10-23 17:13:17 -08:00
grossmj
9614a1253c Development on 2.2.51.dev1 2024-10-21 13:24:14 +10:00
Jeremy Grossmann
a3a0be863e Merge pull request #3644 from GNS3/2.2
Release v2.2.50
2024-10-21 13:22:00 +10:00
grossmj
9b5713df03 Release v2.2.50 2024-10-21 12:14:18 +10:00
grossmj
6df7dc4730 Replace AppVeyor testing with GH Actions 2024-10-19 16:07:54 +10:00
grossmj
e84ef8bf13 Fix issue when pid file contains invalid data 2024-10-16 16:28:46 +10:00
grossmj
f7703e3fa2 Add comment to indicate sentry-sdk is optional. Ref https://github.com/GNS3/gns3-server/issues/2423 2024-10-14 17:46:53 +10:00
grossmj
de8b9bc8f1 Fix f-string syntax error. Fixes #3639 2024-09-28 11:20:39 +07:00
grossmj
f21a530729 Improve information provided when uploading invalid appliance image. Fixes #3637 2024-09-26 16:14:02 +07:00
grossmj
be2b5eecf6 Use "experimental features" option to force listening for HTTP notification streams. Ref #3579 2024-09-24 12:25:26 +07:00
grossmj
ca9d3e41a5 Fixes after merging 2024-09-23 14:08:25 +07:00
grossmj
f97e18cf47 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/modules/docker/docker_vm.py
#	gns3/modules/docker/ui/docker_vm_configuration_page.ui
#	gns3/modules/docker/ui/docker_vm_configuration_page_ui.py
#	gns3/modules/dynamips/ui/ios_router_configuration_page.ui
#	gns3/modules/dynamips/ui/ios_router_configuration_page_ui.py
#	gns3/modules/qemu/qemu_vm.py
#	gns3/modules/qemu/ui/qemu_vm_configuration_page.ui
#	gns3/modules/qemu/ui/qemu_vm_configuration_page_ui.py
2024-09-23 13:25:07 +07:00
Jeremy Grossmann
2d4f6e6ecf Merge pull request #3635 from GNS3/revert-3633-backport-aux-console-support
Revert "Backport auxiliary console support for Qemu, Docker and Dynamips nodes"
2024-09-23 13:15:32 +07:00
Jeremy Grossmann
c152de84de Revert "Backport auxiliary console support for Qemu, Docker and Dynamips nodes" 2024-09-23 13:11:23 +07:00
grossmj
89a182171d Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/modules/docker/docker_vm.py
#	gns3/modules/docker/ui/docker_vm_configuration_page.ui
#	gns3/modules/docker/ui/docker_vm_configuration_page_ui.py
#	gns3/modules/dynamips/ui/ios_router_configuration_page.ui
#	gns3/modules/dynamips/ui/ios_router_configuration_page_ui.py
#	gns3/modules/qemu/qemu_vm.py
#	gns3/modules/qemu/ui/qemu_vm_configuration_page.ui
#	gns3/modules/qemu/ui/qemu_vm_configuration_page_ui.py
2024-09-22 21:49:07 +07:00
Jeremy Grossmann
d34e7b377c Merge pull request #3633 from GNS3/backport-aux-console-support
Backport auxiliary console support for Qemu, Docker and Dynamips nodes
2024-09-22 21:44:25 +07:00
grossmj
74c55241cf Backport auxiliary console support for Qemu, Docker and Dynamips nodes 2024-09-22 18:29:05 +07:00
grossmj
d971214614 Fix for using a custom QNetworkAccessManager 2024-09-22 11:12:05 +07:00
grossmj
507afac692 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/link.py
2024-09-22 11:01:51 +07:00
grossmj
6edb9c9303 Fix to allow packet capture on more than 6 links. Fixes #3594 2024-09-21 16:40:22 +07:00
grossmj
cae1848103 Add mac address field for Docker VM configuration page 2024-09-18 17:23:14 +07:00
grossmj
e08aebf0b8 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/modules/docker/ui/docker_vm_configuration_page.ui
#	gns3/modules/docker/ui/docker_vm_configuration_page_ui.py
2024-09-18 17:19:50 +07:00
grossmj
5d14dd9ab8 Merge branch 'master' into 2.2 2024-09-18 17:14:33 +07:00
Jeremy Grossmann
0c7cae2222 Merge pull request #3632 from GNS3/docker-mac-address
Support for custom MAC addresses in Docker containers
2024-09-18 04:05:46 -06:00
grossmj
9b882924c0 Support for configuring MAC address in Docker containers 2024-09-18 16:30:22 +07:00
Jeremy Grossmann
fccfc01f2e Merge pull request #3630 from griffi-gh/patch-1
Add KRDC to pre-configured VNC console commands
2024-09-12 12:08:38 -06:00
griffi-gh
46872c664d Add KRDC to pre-configured VNC console commands 2024-09-12 17:37:16 +02:00
grossmj
5afa1edc4b Release v3.0.0rc1 2024-08-11 11:35:20 -06:00
grossmj
b056de321a Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/crash_report.py
#	gns3/version.py
2024-08-10 18:57:54 -06:00
grossmj
89166b9d35 Development on 2.2.50.dev1 2024-08-06 20:33:55 +02:00
Jeremy Grossmann
d0421e8b1f Merge pull request #3616 from GNS3/2.2
Release v2.2.49
2024-08-06 20:32:46 +02:00
grossmj
b35ce7303d Release v2.2.49 2024-08-06 12:55:15 +02:00
grossmj
da2852713f Merge remote-tracking branch 'origin/2.2' into 3.0
# Conflicts:
#	dev-requirements.txt
#	setup.py
2024-08-03 12:55:59 +02:00
grossmj
47432568e6 Upgrade development packages 2024-08-03 12:32:43 +02:00
grossmj
42ea34ca6c Upgrade jsonschema and sentry-sdk packages 2024-08-03 11:57:15 +02:00
grossmj
9f3598d36d Upgrade to PyQt5 v5.15.11 2024-08-03 11:55:40 +02:00
grossmj
8df248a1e6 Add shortcuts info dialog 2024-07-27 16:23:23 +02:00
grossmj
7196aeb3cf Merge branch 'master' into 2.2 2024-07-27 15:46:14 +02:00
Jeremy Grossmann
38657f4112 Merge pull request #3612 from braza2004/master
Control menu Shortcut Keys
2024-07-27 15:43:53 +02:00
Bilal
84017fa0f1 modified Control menu shortcuts further 2024-07-24 07:05:38 +00:00
Bilal
3aa5a5369f modified Control menu shortcuts 2024-07-24 07:02:14 +00:00
athaarnaqvi
0ed791b946 Added Key Shortcuts 2024-07-23 16:59:55 +05:00
Jeremy Grossmann
133732b7ae Merge pull request #3608 from braza2004/FitInViewKeyShortcut
Fit In View Shortcut Key
2024-07-22 23:27:11 +02:00
grossmj
2ed48def9f Add Fit In View Shortcut in Ui file instead 2024-07-22 23:25:05 +02:00
grossmj
279a91d402 Fix tests. Fixes #3605 2024-07-22 18:58:32 +02:00
Bilal_Raza
4906678d13 feature/FitInViewShortcut 2024-07-22 16:53:58 +05:00
grossmj
6468151dba Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/crash_report.py
2024-07-13 17:18:49 +02:00
grossmj
a5ff9318f0 Development on 2.2.49.dev1 2024-07-13 16:32:13 +02:00
Jeremy Grossmann
faa802d59c Merge pull request #3597 from GNS3/release/v2.2.48.1
Release v2.2.48.1
2024-07-13 14:43:39 +02:00
grossmj
01b5b8bfa8 Release v2.2.48.1 2024-07-12 18:26:17 +02:00
grossmj
52450989c9 Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3/crash_report.py
#	gns3/graphics_view.py
#	gns3/modules/dynamips/pages/ios_router_configuration_page.py
#	gns3/modules/iou/pages/iou_device_configuration_page.py
#	gns3/version.py
2024-07-09 11:53:04 +02:00
grossmj
b22c5c8442 Development on 2.2.49.dev1 2024-07-09 00:32:45 +02:00
Jeremy Grossmann
141e7d8307 Merge pull request #3592 from GNS3/2.2
Release v2.2.48
2024-07-09 00:31:15 +02:00
grossmj
1e80354e4e Release v2.2.48 2024-07-08 18:44:09 +02:00
grossmj
d340b1f50a Use "experimental features" to allow bypassing hostname validation. Ref #3524 2024-07-08 14:19:42 +02:00
grossmj
887e1b72c1 Add keep the original compute IDs option when exporting a project 2024-07-07 18:54:11 +02:00
grossmj
6a460171c2 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/dialogs/project_export_wizard.py
#	gns3/ui/export_project_wizard.ui
#	gns3/ui/export_project_wizard_ui.py
#	gns3/utils/export_project_worker.py
2024-07-07 18:49:39 +02:00
grossmj
d7bb195610 Update appliance_v8.json. Ref https://github.com/GNS3/gns3-registry/pull/897 2024-07-07 17:47:24 +02:00
Jeremy Grossmann
d963dd4746 Merge pull request #3591 from GNS3/feature/keep-compute-ids
Option to keep the compute IDs unchanged when exporting a project
2024-07-06 17:12:42 +02:00
grossmj
60addccd95 Option to keep the compute IDs unchanged when exporting a project 2024-07-06 17:08:16 +02:00
grossmj
976cce8391 Merge branch '2.2' into 3.0 2024-07-03 18:55:54 +02:00
grossmj
80b654ba53 Upgrade sentry-sdk and psutil packages 2024-07-03 18:52:49 +02:00
grossmj
ade73ecad8 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/version.py
2024-07-01 18:52:58 +02:00
grossmj
b67a8c87a7 Switch to PyQt5 5.15.10 for macOS build 2024-06-10 17:20:52 +02:00
grossmj
53ece94a08 Development on 2.2.48.dev1 2024-05-23 12:19:15 +07:00
grossmj
11f7423d2d Development on 3.0.0.dev14 2024-05-19 20:57:11 +07:00
grossmj
21c1acc48a Release v3.0.0b3 2024-05-19 17:23:16 +07:00
Jeremy Grossmann
a6c7e0be59 Merge pull request #3586 from GNS3/update-ga-workflows
Update GitHub Action workflows
2024-05-17 12:21:05 +07:00
grossmj
fcea25dcbb Update GitHub Action workflows 2024-05-17 12:14:47 +07:00
grossmj
17c49d3e9a Development on 3.0.0.dev13 2024-05-16 19:18:53 +07:00
grossmj
5c8a8e97d8 Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	dev-requirements.txt
#	gns3/crash_report.py
#	gns3/version.py
#	requirements.txt
#	setup.py
2024-05-16 17:35:26 +07:00
grossmj
50b3dc3dc7 Fix updating IOS router 2024-05-16 17:10:11 +07:00
Jeremy Grossmann
f29c065164 Merge pull request #3585 from GNS3/release/v2.2.47
Release v2.2.47
2024-05-15 17:27:30 +07:00
grossmj
c6b5494ce6 Use system Python 2024-05-15 17:19:54 +07:00
grossmj
02b14f6aea Call pip as a Python module 2024-05-15 17:12:42 +07:00
grossmj
e2cc378aee Use ubuntu:latest for running tests inside Docker container 2024-05-15 17:10:04 +07:00
grossmj
812aedebe3 Release v2.2.47 2024-05-15 12:14:48 +07:00
grossmj
91dd83b285 Ensure Python >= 3.8 is used in pyproject.toml 2024-05-12 16:44:09 +07:00
grossmj
2f0d2063cf Remove maximum size for capture dialog. Ref #3576 2024-05-10 12:58:35 +07:00
Jeremy Grossmann
ce0515f0ae Merge pull request #3582 from GNS3/drop-python3.7
Drop Python 3.7
2024-05-09 19:09:14 +07:00
grossmj
bc10c69a2d Change sentry-sdk version 2024-05-09 19:03:08 +07:00
grossmj
3707758388 Upgrade aiohttp, sentry-sdk and truststore 2024-05-09 18:58:38 +07:00
grossmj
8aaefac91b Upgrade jsonschema and aiohttp 2024-05-09 18:37:41 +07:00
grossmj
19157ab49d Drop Python 3.7 2024-05-09 18:23:59 +07:00
grossmj
38b98cd883 Remove dev requirements for Python 3.6 2024-05-09 18:05:08 +07:00
grossmj
696a501eaa Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/version.py
2024-04-22 19:04:59 +07:00
Jeremy Grossmann
e9419924c5 Merge pull request #3580 from GNS3/feature/nat-symbols
NAT symbols
2024-04-22 19:00:15 +07:00
grossmj
cf3c5c09fa Add NAT symbols 2024-04-22 18:51:29 +07:00
grossmj
bb58671c23 Release v3.0.0b2 2024-04-07 18:52:24 +07:00
Jeremy Grossmann
34ef843bbe Merge pull request #3575 from GNS3/feature/local-controller-linux
Support for local controller on Linux
2024-04-07 12:34:02 +07:00
grossmj
b0535a26e7 Change name of websocket disconnect slot 2024-04-07 12:32:17 +07:00
grossmj
fc6f8109cc Enable local controller support on Linux 2024-04-07 12:02:41 +07:00
grossmj
dbfaa48ca9 Fix retrieving value from uiQemuPathLineEdit 2024-04-04 18:43:31 +07:00
Jeremy Grossmann
5d997ba53e Merge pull request #3574 from GNS3/feature/custom-qemu-path
Support for custom Qemu paths
2024-04-03 21:24:20 +07:00
grossmj
f5d49d1b69 Support for custom Qemu path in templates and nodes 2024-04-03 21:16:41 +07:00
grossmj
4ef0d1606a Round CPUs value in Docker templates and VMs. Ref https://github.com/GNS3/gns3-gui/issues/3572 2024-04-03 17:11:45 +07:00
grossmj
a1b577d617 Upgrade dev packages 2024-03-24 18:31:57 +01:00
grossmj
9e89cf5ad5 Only show log message if event has "message" 2024-03-07 17:18:30 +01:00
grossmj
131ef09b55 Development on 2.2.47.dev1 2024-03-05 01:07:17 +08:00
grossmj
e82c82236d Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/crash_report.py
#	gns3/version.py
#	requirements.txt
2024-03-05 01:00:30 +08:00
grossmj
2df18ee04e Upgrade sentry-sdk to version 1.40.6 2024-03-05 00:45:26 +08:00
Jeremy Grossmann
854e1fded6 Merge pull request #3568 from GNS3/release/v2.2.46
Release v2.2.46
2024-02-26 17:50:19 +08:00
grossmj
31711a9d4e Release v2.2.46 2024-02-26 16:55:13 +08:00
grossmj
2089149fbd Require Python 3.7 in pyproject.toml 2024-02-21 12:20:48 +08:00
grossmj
4bfe8b0c99 Merge branch '2.2' into 3.0
# Conflicts:
#	requirements.txt
#	setup.py
2024-02-21 12:18:00 +08:00
grossmj
4d178fd011 Revert "Upgrade to PyQt5 v5.15.10 on macOS"
This reverts commit eae7cfe39f.
2024-02-15 18:51:05 +08:00
grossmj
eae7cfe39f Upgrade to PyQt5 v5.15.10 on macOS 2024-02-15 15:32:27 +08:00
grossmj
fd2e236927 Add GNS3 console command "env" to show what environment variables are used. Ref https://github.com/GNS3/gns3-server/issues/2306 2024-02-14 19:29:07 +08:00
grossmj
21409a899d Add CTRL+C shortcut to copy status bar message. Ref #3561 2024-02-14 17:33:15 +08:00
grossmj
25be9e7ec7 Key modifier (ALT) to ignore snap to grid. Fixes #3538 2024-02-12 17:32:22 +11:00
grossmj
1260c2bc2d Increase timeout to 5s for status bar messages.
The coordinates message has no timeout and can be reset when clicking on the scene. Ref #3561
2024-02-12 16:32:17 +11:00
grossmj
1441e38876 Add reset GUI state feature. Ref #3549 2024-02-12 16:16:07 +11:00
grossmj
3c8cff20b7 Possible fix for hiding Windows terminal. Ref #3290 2024-02-12 15:05:49 +11:00
Jeremy Grossmann
f831d71c3f Merge pull request #3566 from GNS3/feature/drop-python-3.6
Drop support for Python 3.6
2024-02-09 17:02:27 +11:00
grossmj
f71b6dcda1 Change runtime checks for Python version 2024-02-09 16:49:59 +11:00
grossmj
62289e7be3 Drop support for Python 3.6 2024-02-09 16:28:23 +11:00
grossmj
1daf77ed8d Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3/crash_report.py
#	gns3/version.py
#	requirements.txt
2024-01-27 17:08:21 +11:00
grossmj
7447e9b7d4 Merge branch 'master' into 2.2 2024-01-27 17:04:54 +11:00
grossmj
c5961f400e Upgrade sentry-sdk, psutil and distro dependencies 2024-01-27 17:04:02 +11:00
grossmj
6ca61905b2 Development on 2.2.46.dev1 2024-01-14 23:45:35 +11:00
Jeremy Grossmann
130e91da76 Merge pull request #3559 from GNS3/2.2
Release v2.2.45
2024-01-14 22:02:24 +11:00
grossmj
bbc5b3e4ac Release v2.2.45 2024-01-12 21:38:26 +11:00
grossmj
2a72ad5e0b Add missing console_type values in appliance_v8.json. Ref https://github.com/GNS3/gns3-registry/issues/849 2024-01-12 16:39:06 +11:00
grossmj
88ed9407b9 Move PATH debug message 2024-01-12 15:46:20 +11:00
grossmj
18be274fed Handle moved project notifications on controller stream 2024-01-12 13:22:48 +11:00
grossmj
fefda50378 Add debug for PATH env variable 2024-01-11 22:57:14 +11:00
grossmj
b197f0dad1 Fix tests 2023-12-05 22:37:23 +10:00
grossmj
9adcaa617c Merge branch '2.2' into 3.0 2023-12-05 22:13:50 +10:00
grossmj
3df374e784 Add custom executable paths on Windows 2023-12-05 21:24:41 +10:00
grossmj
04fb449b44 Add --suppressApplicationTitle for Windows terminal. Fixes https://github.com/GNS3/gns3-gui/issues/3544 2023-11-28 11:23:20 +10:00
grossmj
b77f867acd Development on 3.0.0.dev12 2023-11-27 11:33:22 +10:00
grossmj
ec41dfa60c Release v3.0.0b1 2023-11-27 11:13:39 +10:00
grossmj
39bd40591c Upgrade sentry-sdk to v1.37.1 2023-11-27 11:05:20 +10:00
grossmj
4876f7ec4b Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3/crash_report.py
#	gns3/version.py
#	requirements.txt
2023-11-22 12:43:18 +10:00
grossmj
c68694687f Deactivate showing a percentage in progress bar. Ref #3543 2023-11-22 12:18:25 +10:00
grossmj
fc54b76ee1 Upgrade sentry-sdk and aiohttp 2023-11-22 10:32:51 +10:00
grossmj
8aaa58dd61 Release v3.0.0a6 2023-11-15 12:07:48 +10:00
grossmj
d9c1d11480 Upgrade dependencies 2023-11-15 10:27:06 +10:00
grossmj
86d27bb474 Possible fix for stuck image upload. Ref https://github.com/GNS3/gns3-server/issues/2310 2023-11-12 18:01:02 +10:00
grossmj
59b284e18b Development in 2.2.45.dev3 2023-11-07 19:11:57 +10:00
Jeremy Grossmann
1d4492c911 Merge pull request #3537 from GNS3/release-v2.2.44.1
Release v2.2.44.1
2023-11-07 18:34:54 +10:00
grossmj
9d8b6a172e Release v2.2.44.1 2023-11-07 14:59:28 +10:00
grossmj
8c3ef7a968 Bump version to v2.2.45.dev2 2023-11-07 12:21:39 +10:00
grossmj
0f584a062c Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3/crash_report.py
#	gns3/modules/qemu/__init__.py
#	gns3/version.py
#	requirements.txt
2023-11-06 17:08:09 +10:00
Jeremy Grossmann
947733aada Merge pull request #3536 from GNS3/release-v2.2.44
Release v2.2.44
2023-11-06 17:01:17 +10:00
grossmj
a199fef03b Development on 2.2.45.dev1 2023-11-06 17:00:17 +10:00
grossmj
29b851207b Release v2.2.44 2023-11-06 16:02:23 +10:00
grossmj
ca5557e579 Upgrade sentry-sdk 2023-11-06 15:44:30 +10:00
grossmj
7331ae29ef Fix timeout issue when creating Qemu disk image. Fixes https://github.com/GNS3/gns3-server/issues/2313 2023-11-05 15:41:46 +10:00
grossmj
d6b89831e3 Add ISO file to images filter in Image Manager. Ref https://github.com/GNS3/gns3-server/issues/2310 2023-11-05 11:12:19 +10:00
grossmj
da83230622 Merge branch '2.2' into 3.0 2023-11-03 14:55:32 +10:00
grossmj
3a7e06e14b Revert to subprocess.Popen since subprocess.call is the old API 2023-11-03 14:43:25 +10:00
grossmj
0a81af8248 Merge remote-tracking branch 'origin/2.2' into 2.2 2023-10-31 15:53:13 +10:00
grossmj
a882956ec9 Merge branch 'master' into 2.2 2023-10-31 15:52:45 +10:00
Jeremy Grossmann
9f4361d66f Merge pull request #3534 from GNS3/command-variables
Refactor command variables
2023-10-31 15:44:23 +10:00
grossmj
d10c3c7308 Fix tests 2023-10-31 15:40:50 +10:00
Jeremy Grossmann
0cd7d7e4c2 Merge branch '2.2' into command-variables 2023-10-31 15:07:49 +10:00
grossmj
8f3f72ff54 Fix tests 2023-10-31 15:07:35 +10:00
Jeremy Grossmann
7fbc0befa1 Merge branch '2.2' into command-variables 2023-10-31 14:51:53 +10:00
grossmj
9d9668442e Use Python 3.8 in appveyor.yml 2023-10-31 14:48:08 +10:00
grossmj
932083be88 Update custom command help and protect against double quote in project name 2023-10-31 14:43:50 +10:00
grossmj
82e7c151c4 Refactor command variables support 2023-10-31 12:33:52 +10:00
Jeremy Grossmann
7e5c363bc3 Merge pull request #3533 from GNS3/fix/2306
Allow local server to get $PATH environment variable
2023-10-30 14:06:17 +10:00
grossmj
15d029a7fb Pass os.environ in Popen() 2023-10-30 14:03:10 +10:00
grossmj
71362104ea Development on 3.0.0.dev10 2023-10-27 14:12:23 +10:00
grossmj
8d58898b0b Release v3.0.0a5 2023-10-27 13:38:14 +10:00
grossmj
3dfba11d51 Merge branch '2.2' into 3.0 2023-10-24 18:29:54 +10:00
Jeremy Grossmann
9087ba8f5a Merge pull request #3512 from magister990/style_edit_width_and_height
Add the ability to edit width and hight in the style edit dialog.
2023-10-24 18:25:58 +10:00
grossmj
3d89d6e6cc Fix issue with line item 2023-10-24 18:01:49 +10:00
grossmj
91bae81300 Merge branch '2.2' into style_edit_width_and_height 2023-10-24 17:40:35 +10:00
Jeremy Grossmann
7de5bf6bd5 Merge pull request #3516 from ventaquil/feature/add-qemu-igb-nic
Add Qemu IGB network device
2023-10-24 17:15:21 +10:00
Jeremy Grossmann
9de238619a Merge branch '2.2' into feature/add-qemu-igb-nic 2023-10-24 17:14:30 +10:00
grossmj
ed88466d63 Upgrade to actions/checkout@v3 and actions/setup-python@v3 2023-10-23 16:17:31 +10:00
grossmj
e2f5f92a54 Fix cannot change default VLAN for an access port for builtin Ethernet switch. Fixes #3528 2023-10-23 12:28:50 +10:00
grossmj
1ac555ca7d Development on 3.0.0.dev9 2023-10-18 20:15:11 +10:00
grossmj
fcb5ed3272 Release v3.0.0a4 2023-10-18 19:01:13 +10:00
grossmj
dfe3ccf460 Merge branch '2.2' into 3.0 2023-10-18 17:47:28 +10:00
grossmj
478b793b04 Merge branch 'master' into 2.2 2023-10-18 17:46:45 +10:00
grossmj
5e75d5a6d2 Merge branch '2.2' into 3.0
# Conflicts:
#	requirements.txt
2023-10-18 14:54:08 +10:00
grossmj
841c29e6f6 Upgrade sentry and psutil dependencies 2023-10-18 14:53:25 +10:00
grossmj
afb3d30e9f Merge branch '2.2' into 3.0
# Conflicts:
#	requirements.txt
#	setup.py
2023-10-18 14:48:40 +10:00
grossmj
43f464de31 Do not add version in template names created from an appliance 2023-10-18 14:48:06 +10:00
grossmj
f9d96051f5 Downgrade to PyQt5 v5.15.9 2023-10-18 14:33:27 +10:00
grossmj
607e201674 Fix packaging issue on macOS 2023-10-18 14:29:53 +10:00
grossmj
18950ca64f Upgrade to PyQt5 v5.15.10 2023-10-18 13:24:45 +10:00
grossmj
c0b5f39c4c Add Python 3.12 support. Fixes https://github.com/GNS3/gns3-server/issues/2273 2023-10-09 16:54:47 +10:00
grossmj
3e717999ca Add vendor_logo_url in appliance schemas. Ref https://github.com/GNS3/gns3-registry/pull/825 2023-10-09 16:52:52 +10:00
Konrad Goławski
800d14363d Add Qemu IGB network device 2023-10-02 14:35:09 +02:00
Alex Scott
2dd9d61c57 Add the ability to edit width and hight in the style edit dialog. 2023-09-27 17:08:33 -05:00
grossmj
906db5e3a1 Bump version to 3.0.0.dev8 2023-09-25 21:38:20 +10:00
Jeremy Grossmann
879b5b821c Merge pull request #3511 from GNS3/fix/3422
Support to create empty disk images on the controller
2023-09-25 21:29:19 +10:00
grossmj
ded088a28b Fix tests 2023-09-25 21:08:23 +10:00
grossmj
ed1b479381 Support to create empty disk images on the controller 2023-09-25 17:51:14 +10:00
grossmj
10afb5a8de Revert "Revert "Install importlib-resources only with Python < '3.9'. Ref #2147""
This reverts commit 3413afe952.
2023-09-23 20:50:39 +10:00
grossmj
0d23a930e3 Fix bug with right click on any device the Servers Summary panel. Fixes #3508 2023-09-23 16:47:46 +10:00
grossmj
960fda17f9 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/crash_report.py
#	gns3/version.py
#	requirements.txt
2023-09-23 15:50:05 +10:00
grossmj
3413afe952 Revert "Install importlib-resources only with Python < '3.9'. Ref #2147"
This reverts commit 7222da9512.
2023-09-23 14:47:56 +10:00
grossmj
7222da9512 Install importlib-resources only with Python < '3.9'. Ref #2147 2023-09-23 14:33:15 +10:00
grossmj
dbe8df5a37 Development on 2.2.44.dev1 2023-09-19 21:08:24 +07:00
Jeremy Grossmann
a9890265b9 Merge pull request #3509 from GNS3/release-v2.2.43
Release v2.2.43
2023-09-19 21:04:43 +07:00
grossmj
97b777ceea Release v2.2.43 2023-09-19 20:16:52 +07:00
grossmj
c06e534935 Merge branch 'master' into 2.2 2023-09-19 20:08:11 +07:00
grossmj
025276f8a7 Upgrade sentry-sdk and truststore 2023-09-19 20:07:57 +07:00
grossmj
6777961d29 Add KiTTY to preconfigured telnet consoles. Fixes #3507 2023-09-18 20:23:09 +07:00
grossmj
7f6cace0d5 Fix generic icon in Wayland. Ref #3501 2023-09-15 16:08:45 +07:00
grossmj
45307824ab Update users endpoints 2023-09-06 16:02:21 +07:00
grossmj
bf0b0290eb Upgrade dependencies 2023-09-02 18:49:16 +07:00
grossmj
7185068256 Fix not passing version to new_template() 2023-08-17 17:35:27 +10:00
grossmj
bb65a80788 Fix issues after merge 2023-08-17 00:29:09 +10:00
grossmj
520c5f67f5 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/dialogs/appliance_wizard.py
#	gns3/registry/appliance.py
#	gns3/registry/appliance_to_template.py
#	gns3/ui/appliance_wizard.ui
#	gns3/ui/appliance_wizard_ui.py
#	gns3/utils/get_resource.py
#	requirements.txt
#	tests/conftest.py
#	tests/registry/test_appliance_to_template.py
2023-08-16 21:05:48 +10:00
Jeremy Grossmann
1a739c0c37 Merge pull request #3490 from GNS3/appliance-v8-support
Support for appliance version 8 format
2023-08-16 14:18:59 +10:00
grossmj
6d855045ef Show installation instructions when available and fix regression when installing Docker appliance. 2023-08-16 14:11:55 +10:00
grossmj
fef734bbbe Finalize appliance v8 support and add tests. 2023-08-16 00:30:02 +10:00
grossmj
b079443735 Merge remote-tracking branch 'origin/appliance-v8-support' into appliance-v8-support 2023-08-14 12:01:18 +10:00
grossmj
4a32ae9736 Drop "kvm" field. 2023-08-14 12:01:09 +10:00
Jeremy Grossmann
9793d00131 Merge branch '2.2' into appliance-v8-support 2023-08-13 12:21:18 +10:00
grossmj
2b7840279a Downgrade jsonschema 2023-08-12 17:51:24 +10:00
grossmj
9243083321 Upgrade dependencies 2023-08-12 17:47:48 +10:00
Jeremy Grossmann
3a8b3e5c4a Merge pull request #3506 from GNS3/fix/3505
Use importlib instead of pkg_resources
2023-08-12 17:26:51 +10:00
grossmj
e2168a3c81 Use importlib instead of pkg_resources 2023-08-12 17:20:33 +10:00
grossmj
75fc344ce7 Merge branch '2.2' into 3.0 2023-08-11 18:14:07 +10:00
grossmj
01deb01e6a Upgrade to PyQt 5.15.9 and pywin32 2023-08-11 18:13:41 +10:00
Jeremy Grossmann
31df679e54 Merge pull request #3504 from GNS3/packaging-migration
Packaging migration
2023-08-11 18:09:24 +10:00
grossmj
411035a8bc Use dev for optional development dependencies 2023-08-10 23:16:57 +10:00
grossmj
61de398850 Rename qt_resources back to resources 2023-08-10 23:09:12 +10:00
grossmj
adb78dbabb New packaging relying only pyproject.toml 2023-08-10 22:44:37 +10:00
grossmj
1f7bb58220 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/crash_report.py
#	gns3/version.py
#	requirements.txt
2023-08-10 14:55:56 +10:00
grossmj
1133ee6e1b Development on v2.2.43.dev1 2023-08-09 22:15:15 +10:00
Jeremy Grossmann
7512ffec64 Merge pull request #3503 from GNS3/release-v2.2.42
Release v2.2.42
2023-08-09 22:08:18 +10:00
grossmj
3527e5551c Release v2.2.42 2023-08-09 21:11:57 +10:00
grossmj
72960f8f2b Enable system certificate store later in the code and bump version to 2.2.42.dev4 2023-08-08 17:22:27 +10:00
grossmj
8abb502c72 Use the system's certificate store for SSL connections 2023-08-07 21:33:25 +10:00
grossmj
08c729e83a Upgrade dependencies 2023-08-06 20:39:27 +10:00
grossmj
aac004bd2f Use certifi to get SSL root certificates 2023-08-06 20:37:10 +10:00
grossmj
70677d8f18 Bump version to 2.2.42.dev3 2023-08-06 18:14:22 +10:00
Jeremy Grossmann
fba1ff4208 Merge pull request #3502 from GNS3/use-bundled-cacert
Use bundled cacert file for frozen app
2023-08-05 22:37:08 +10:00
grossmj
e4edbefc23 Use bundled cacert file on Windows and macOS 2023-08-05 22:21:08 +10:00
grossmj
d93f9afe74 Bump version to 2.2.42.dev2 2023-08-05 20:16:30 +10:00
grossmj
564415cfeb Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/crash_report.py
#	gns3/dialogs/style_editor_dialog.py
2023-08-02 18:31:07 +10:00
grossmj
162d197e36 Give a node some time to start before opening the console (for console auto start). Fixes #3474 2023-08-02 11:08:58 +10:00
grossmj
5c21dd8a2f Merge branch 'master' into 2.2 2023-08-01 15:50:51 +10:00
Jeremy Grossmann
aa9b9d3b0b Merge pull request #3364 from AbdelbakiBoukerche/feature_rounded_rectangle
Rounded Rectangle
2023-08-01 15:46:32 +10:00
grossmj
eae9eec15b Support for horizontal and vertical corner radius 2023-08-01 15:34:30 +10:00
Jeremy Grossmann
a3bf832721 Merge branch 'master' into feature_rounded_rectangle 2023-07-31 18:34:50 +10:00
Jeremy Grossmann
67890d74d9 Merge pull request #3500 from GNS3/fix/3449
Support for gnome-terminal tabs to be opened in the same window
2023-07-31 18:30:22 +10:00
grossmj
ab4325f951 Remove warning to set open new terminals in tabs for gnome-terminal 2023-07-31 12:32:49 +10:00
grossmj
2a947b9cc5 Add comments for gnome-terminal special case 2023-07-31 01:44:39 +10:00
grossmj
601c082288 Use Mate Terminal by default if installed on Debian, Ubuntu and Linux Mint. 2023-07-31 01:40:20 +10:00
grossmj
7701d57bd0 Check that gnome-terminal is configured to open new terminals in tabs. 2023-07-31 01:34:59 +10:00
grossmj
f0b4148a20 Support for gnome-terminal tabs to be opened in the same window. 2023-07-30 22:15:38 +10:00
grossmj
2fdcbafbc1 Revert "Support for Python 3.12"
This reverts commit 5d82cea935.
2023-07-30 17:50:21 +10:00
grossmj
5d82cea935 Support for Python 3.12 2023-07-30 17:48:30 +10:00
grossmj
b0e3e93c41 Remove import urllib3 and let sentry_sdk import and patch it. Fixes https://github.com/GNS3/gns3-gui/issues/3498 2023-07-30 17:42:52 +10:00
grossmj
5b18183cd5 Downgrade jsonschema 2023-07-25 19:44:30 +10:00
grossmj
edc13c3f90 Upgrade dependencies 2023-07-23 12:47:34 +10:00
grossmj
a3eec47066 Merge branch '2.2' into 3.0 2023-07-23 12:36:24 +10:00
grossmj
535f53737d Merge remote-tracking branch 'origin/master' into 2.2 2023-07-23 12:35:21 +10:00
Jeremy Grossmann
354f3eecec Merge pull request #3497 from kevinchevreuil/master
Add import sys in sudo.py
2023-07-23 12:04:29 +10:00
Kevin Chevreuil - Kaisen
35a6a5c8c7 Add import sys in sudo.py 2023-07-22 23:12:34 +02:00
grossmj
58a00744a2 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/crash_report.py
#	gns3/modules/qemu/ui/qemu_vm_configuration_page.ui
#	gns3/modules/qemu/ui/qemu_vm_configuration_page_ui.py
#	gns3/pages/general_preferences_page.py
#	gns3/ui/general_preferences_page.ui
#	gns3/ui/general_preferences_page_ui.py
#	gns3/version.py
2023-07-13 11:18:44 +10:00
grossmj
23cba0a28d Development on 2.2.42.dev1 2023-07-12 18:26:26 +10:00
Jeremy Grossmann
9c3d7bc95a Merge pull request #3495 from GNS3/release-v2.2.41
Release v2.2.41
2023-07-12 18:24:05 +10:00
grossmj
cf2802b15a Release v2.2.41 2023-07-12 17:07:39 +10:00
grossmj
b162c55078 Merge remote-tracking branch 'origin/master' into 2.2 2023-07-12 17:03:50 +10:00
grossmj
bb42b0ed0b Change chown in authorize_ubridge.py 2023-07-12 13:28:24 +10:00
grossmj
e108b5194d Bump version to 2.2.41.dev3 2023-07-12 13:26:22 +10:00
grossmj
e9ef8735be Use a small executable to set the correct permissions on uBridge on macOS 2023-07-12 12:40:53 +10:00
grossmj
e8e90bb16a Add debugging for AuthorizationExecuteWithPrivileges 2023-07-11 21:23:15 +10:00
grossmj
49f77930f4 Use alternative method to setuid uBridge on macOS 2023-07-11 18:27:19 +10:00
grossmj
a58451a552 Update to support template_type & template_properties 2023-07-11 17:38:00 +10:00
grossmj
0a43b9e6e9 Fix tests 2023-07-09 20:18:22 +10:00
Jeremy Grossmann
4f32619ed8 Merge pull request #3493 from GNS3/remove-analytics
Remove sending usage stats
2023-07-09 19:04:40 +10:00
grossmj
20740748c1 Remove sending stats to GA 2023-07-09 18:55:31 +10:00
grossmj
8a5ab6b374 Update schema for appliance version 8 2023-07-09 14:10:15 +10:00
grossmj
e11ce27f7b Catch urllib3 exceptions when sending crash report. Ref https://github.com/GNS3/gns3-gui/issues/3483 2023-07-06 17:16:05 +10:00
grossmj
4b7cf4e553 Support legacy "idlepc" field 2023-07-03 19:12:44 +10:00
grossmj
a9bfc96dc9 Upgrade dependencies 2023-06-30 17:47:55 +10:00
grossmj
b72358461c Add support for appliance version 8 format 2023-06-25 16:54:12 +09:30
grossmj
fa3a2bc714 Apply Snap-to-grid of drawing items not on their center position. Fixes #3465 2023-06-23 16:43:56 +09:30
Jeremy Grossmann
2987bcf91a Merge pull request #3489 from GNS3/backport-uefi-boot-mode
Backport UEFI boot mode support for Qemu VMs
2023-06-23 11:31:49 +09:30
grossmj
5e97bc0f86 Backport UEFI boot mode support for Qemu VMs 2023-06-23 11:18:25 +09:30
Jeremy Grossmann
ff1d3a71a6 Merge pull request #3487 from GNS3/dynamic-compute-allocation
Allow computes to be dynamically or manually allocated
2023-06-21 22:41:51 +09:30
grossmj
a604c1b9f9 Allow computes to be dynamically or manually allocated 2023-06-21 22:28:09 +09:30
Jeremy Grossmann
09891680cc Merge pull request #3485 from GNS3/add-uefi-boot-mode
Add UEFI boot mode option for Qemu VMs
2023-06-21 18:12:26 +09:30
grossmj
99a535f6c0 Add UEFI boot mode option for Qemu VMs 2023-06-21 17:56:34 +09:30
grossmj
287c829fdb Upgrade dev dependencies 2023-06-20 16:17:13 +09:30
grossmj
e1699470ab Upgrade dependencies 2023-06-20 16:08:17 +09:30
grossmj
a8b55b5807 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/controller.py
#	gns3/crash_report.py
#	gns3/project.py
#	gns3/version.py
2023-06-20 15:57:00 +09:30
grossmj
e3a3de5df7 Add debug for dropEvent. Ref https://github.com/GNS3/gns3-server/issues/2242 2023-06-20 14:49:25 +09:30
grossmj
e1693ce113 Developement on v2.2.41.dev2 2023-06-10 21:40:39 +09:30
Jeremy Grossmann
522091d219 Merge pull request #3481 from GNS3/release-v2.2.40.1
Release v2.2.40.1
2023-06-10 21:36:42 +09:30
grossmj
1446748934 Release v2.2.40.1 2023-06-10 20:06:40 +09:30
grossmj
3f0ce380e8 Merge branch 'master' into 2.2 2023-06-10 17:22:50 +09:30
grossmj
bd71383354 Development on v2.2.41.dev1 2023-06-06 12:43:20 +09:30
Jeremy Grossmann
9649895378 Merge pull request #3479 from GNS3/release-v2.2.40
Release v2.2.40
2023-06-06 12:41:46 +09:30
grossmj
8579ffa20a Release v2.2.40 2023-06-06 10:23:42 +09:30
grossmj
3206743329 Merge branch 'master' into 2.2 2023-06-06 10:19:45 +09:30
grossmj
29c87b6e96 Change log messages for Websocket errors 2023-06-03 21:05:26 +09:30
grossmj
93b2721d6a Do not proceed if an appliance symbol cannot be downloaded. Ref #3466 2023-05-23 16:31:51 +08:00
grossmj
1ff369683f Revert "Fix open IPv6 address for HTTP consoles. Fixes #3448"
This reverts commit 7c56a2467c.
2023-05-22 19:39:03 +08:00
grossmj
7c56a2467c Fix open IPv6 address for HTTP consoles. Fixes #3448 2023-05-22 16:46:57 +08:00
grossmj
5c20302e17 Upgrade dependencies 2023-05-22 15:21:57 +08:00
grossmj
6c538a2102 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/crash_report.py
#	gns3/version.py
#	requirements.txt
2023-05-22 15:03:31 +08:00
grossmj
59ef34c17d Delete a node or link from topology summary view using Delete key. Ref #3445 2023-05-14 21:19:19 +08:00
grossmj
d1fae54049 Fix "Start the capture visualization program" checkbox works only one (first) time for a given link. Fixes #3442 2023-05-14 20:38:34 +08:00
grossmj
49bd61f769 Let the selected link style applied when editing a link. Fixes #3460 2023-05-14 15:33:08 +08:00
grossmj
9a4faddd10 Fix hovered color shown in style editing dialog. Fixes #3460 2023-05-14 14:52:27 +08:00
Jeremy Grossmann
7654681a94 Merge pull request #3468 from GNS3/release-v2.2.39
Release v2.2.39
2023-05-08 20:28:43 +08:00
grossmj
9bfecde957 Development on v2.2.40.dev1 2023-05-08 20:26:40 +08:00
grossmj
705cbf8bb9 Release v2.2.39 2023-05-08 19:17:02 +08:00
Jeremy Grossmann
ab6e0ce496 Merge pull request #3461 from GNS3/fix/3441
Fix nodes are not snapped to the grid at the moment of creation
2023-05-08 17:13:20 +08:00
grossmj
8042c9eb6f Fix tests 2023-05-08 17:08:10 +08:00
grossmj
7334e1509c Upgrade dependencies 2023-04-30 23:17:02 -10:00
grossmj
9d0e940e8d Upgrade dependencies 2023-04-23 05:33:12 -10:00
grossmj
ad3c8a09db Fix nodes are not snapped to the grid at the moment of creation 2023-04-23 05:18:02 -10:00
grossmj
ea4a7f201e Upgrade sentry-sdk 2023-03-23 21:41:50 -10:00
Jeremy Grossmann
55632b7f13 Merge pull request #3457 from GNS3/pyproject-migration
pyproject.toml support
2023-03-21 18:00:27 +08:00
grossmj
d79230f715 Adjust some values in pyproject.toml 2023-03-21 17:40:39 +10:00
grossmj
358348b07b Migrate to pyproject.toml 2023-03-21 11:46:00 +10:00
grossmj
af7d9e6704 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/crash_report.py
#	gns3/version.py
#	requirements.txt
2023-03-17 16:35:53 +10:00
grossmj
885acafa04 Upgrade dependencies 2023-03-17 16:23:49 +10:00
grossmj
28c82b8718 Upgrade distro and aiohttp dependencies 2023-03-01 18:03:28 +10:00
grossmj
6a4dd59e81 Development on 2.2.39.dev1 2023-02-28 17:09:39 +10:00
Jeremy Grossmann
7418c190a8 Merge pull request #3447 from GNS3/2.2
Release v2.2.38
2023-02-28 14:22:10 +08:00
grossmj
737e32f5c3 Release v2.2.38 2023-02-28 15:35:17 +10:00
grossmj
cfc09d2c14 Merge branch 'master' into 2.2 2023-02-28 15:07:07 +10:00
Jeremy Grossmann
366fc3d854 Merge pull request #3432 from GNS3/vmware-virtualbox-deprecated
Warn that VMware and VirtualBox support is deprecated
2023-02-06 06:26:38 +05:45
grossmj
db5f96556c Mark VMware and VirtualBox support as deprecated 2023-02-05 10:09:55 +08:00
grossmj
f6ab5cae16 Add long description content type in setup.py 2023-02-01 09:56:02 +08:00
grossmj
39ec7eb8ea Automatically add new issues to GNS3 project 2023-01-31 09:31:52 +08:00
grossmj
64c579d43c Development 2.2.38.dev1 2023-01-25 18:36:39 +08:00
grossmj
98cc82e6fd Release v2.2.37 2023-01-25 15:06:12 +08:00
grossmj
4b795112b4 Merge branch 'master' into 2.2 2023-01-25 14:44:11 +08:00
grossmj
0e186afaf1 Comment AllocConsole() 2023-01-25 14:41:55 +08:00
grossmj
b218f7fdf8 Bump version to 2.2.37.dev3 2023-01-25 14:13:07 +08:00
grossmj
f1cb6d66f3 Try to fix hiding console. Ref #3290 2023-01-25 14:08:52 +08:00
grossmj
29758f1b4f Debug win32console 2023-01-24 22:25:39 +08:00
grossmj
445dcf3e3b Upgrade to PyQt5 v5.15.7 2023-01-24 18:54:15 +08:00
grossmj
f623f28509 Add venv to .gitignore 2023-01-17 13:40:37 +08:00
grossmj
9d02d57162 Bump version to 2.2.37.dev2 2023-01-17 13:30:09 +08:00
Jeremy Grossmann
df03f50e3d Merge pull request #3427 from eantowne/master
Changed Windows Terminal telnet console profile from OS X to Windows ref: #3193
2023-01-17 05:52:31 +05:45
Ean Towne
3b72a66ca5 Changed Windows Terminal telnet console profile from OS X to windows ref: issue #3193 2023-01-16 16:50:57 -05:00
grossmj
c68a2a7734 Fix and adjustments after merge 2023-01-10 12:09:36 +08:00
grossmj
bab1027396 Merge branch '2.2' into 3.0
# Conflicts:
#	README.md
#	tests/test_http_client.py
2023-01-10 11:50:24 +08:00
grossmj
f8aee44442 Fix tests 2023-01-10 08:42:25 +08:00
grossmj
6dd4d1700e Convert README to Markdown 2023-01-10 08:16:17 +08:00
grossmj
25ed017d5d Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3/crash_report.py
#	gns3/modules/qemu/ui/qemu_vm_configuration_page.ui
#	gns3/modules/qemu/ui/qemu_vm_configuration_page_ui.py
#	gns3/version.py
#	requirements.txt
2023-01-05 12:03:03 +08:00
grossmj
9b674669db Development on 2.2.37.dev1 2023-01-05 09:13:30 +08:00
grossmj
53073d458f Merge branch '2.2' 2023-01-05 09:12:01 +08:00
grossmj
957d89d450 Release v2.2.36 2023-01-04 19:46:30 +08:00
Jeremy Grossmann
db02cbdb2f Merge pull request #3421 from GNS3/qemu-tpm-support
Trusted Platform Module (TPM) support for Qemu VMs
2023-01-04 10:55:52 +05:45
grossmj
0c9f70152f Add checkbox to enable TPM 2023-01-04 12:45:40 +08:00
grossmj
9016975958 Add Trusted Platform Module (TPM) support for Qemu VMs 2023-01-04 12:13:19 +08:00
grossmj
bf295060fd Add "on_close" setting to appliance schema. Fixes https://github.com/GNS3/gns3-server/issues/2148 2023-01-01 16:23:17 +08:00
grossmj
c22ec9f8bd Add default 'ide' disk interface when manually creating Qemu VM template. Fixes #3360 2022-12-29 08:54:40 +08:00
grossmj
bb49cadc6a Fix zoom factor is multiplied when loading projects. Fixes #3408 2022-12-29 08:49:10 +08:00
grossmj
131a49160c Update sentry-sdk dependency 2022-12-28 15:13:26 +08:00
grossmj
d69c915d3f Development on 3.0.0.dev7 2022-12-28 08:29:26 +08:00
grossmj
6670b0d362 Release v3.0.0a3 2022-12-27 15:22:16 +08:00
grossmj
59a89cc40b Catch timeout error while updating appliance files 2022-12-27 13:54:57 +08:00
grossmj
25e665ff2c Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/crash_report.py
#	gns3/version.py
#	requirements.txt
2022-12-27 13:26:46 +08:00
grossmj
225b829eae Fix syntax error in .whitesource 2022-12-27 12:27:26 +08:00
grossmj
e5ef6180b1 Add more base branches for Mend to scan 2022-12-27 12:25:49 +08:00
grossmj
617cf7ef02 Fix RecursionError with invalid credentials. Fixes #3374 2022-12-27 12:19:55 +08:00
grossmj
b4daafffad Allow raw images by default. Fixes https://github.com/GNS3/gns3-server/issues/2097 2022-12-26 11:28:51 +08:00
grossmj
6e7947eea3 Remove deprecated PuTTY option in preferences. Ref https://github.com/GNS3/gns3-gui/discussions/3415 2022-12-24 11:19:29 +08:00
grossmj
f329039987 Do not send empty EtherType when updating an Ethernet switch 2022-12-24 08:26:59 +08:00
grossmj
d43804374f Fix bad message for config file of HDPI. Fixes #3389 2022-12-22 19:12:40 +08:00
grossmj
f54a7f6096 Fix test_add_appliance_with_symbol_from_symbols_dir 2022-12-22 19:03:50 +08:00
grossmj
8a51d5edbe Fix some more tests on Windows 2022-12-22 18:57:02 +08:00
grossmj
2a845a1cf8 Fix more tests on Windows 2022-12-22 18:19:20 +08:00
grossmj
2e793f065a Try to fix tests on Windows 2022-12-22 17:00:19 +08:00
grossmj
0a69cced62 Fix tests 2022-12-22 16:13:35 +08:00
grossmj
320e8243ee Downgrade to flake8 v5.0.4 2022-12-22 09:46:11 +08:00
grossmj
37af40e594 Update dependencies 2022-12-22 09:42:14 +08:00
grossmj
669fee1877 Update GH actions checkout and setup-python 2022-12-22 09:35:46 +08:00
Jeremy Grossmann
3922d370a8 Create SECURITY.md 2022-12-20 21:28:08 +08:00
grossmj
833b9d00c9 Upgrade dependencies 2022-12-18 14:14:56 +08:00
Jeremy Grossmann
377b8dfcaf Create codeql.yml 2022-12-18 14:08:00 +08:00
grossmj
e68937475f Development on v2.2.36.dev2 2022-11-11 00:36:47 +08:00
grossmj
6f418f0853 Release v2.2.35.1 2022-11-10 22:21:14 +08:00
grossmj
8e59927ada Merge branch 'master' into 2.2 2022-11-10 22:06:25 +08:00
grossmj
db8155a818 Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	dev-requirements.txt
#	gns3/crash_report.py
#	gns3/dialogs/edit_project_dialog.py
#	gns3/http_client.py
#	gns3/version.py
#	requirements.txt
#	win-requirements.txt
2022-11-09 20:14:39 +08:00
grossmj
1012686053 Development on 2.2.36.dev1 2022-11-09 20:02:20 +08:00
grossmj
672bd850ad Merge branch '2.2' 2022-11-09 20:00:38 +08:00
grossmj
5db5e1f9fe Release v2.2.35 2022-11-08 23:40:25 +08:00
grossmj
ca94c71bf2 Use Visual Studio 2022 in appveyor.yml 2022-11-08 23:09:23 +08:00
grossmj
76264c55ce Merge branch 'master' into 2.2 2022-11-08 19:29:49 +08:00
grossmj
fd243c42a8 Downgrade psutil to v5.9.2 2022-11-08 19:18:07 +08:00
grossmj
a6521ef9e4 Upgrade psutil to v5.9.4 2022-11-08 18:49:31 +08:00
grossmj
9fa833762c Upgrade pywin32 to v305 2022-11-08 18:29:51 +08:00
grossmj
ca0c6468b5 Fix "variables": [] in project file leads to unlimited increase of empty name/value pairs in GUI. Fixes #3397 2022-11-07 22:29:02 +08:00
grossmj
15f6945a94 Upgrade dependencies 2022-11-07 21:48:45 +08:00
grossmj
645deb8c79 Ignore local revision when comparing versions. 2022-11-07 20:03:05 +08:00
grossmj
428f12a2b3 Make version PEP 440 compliant 2022-11-06 17:51:31 +08:00
grossmj
9ad5760ee6 Support for Python 3.11 2022-10-30 19:04:54 +08:00
grossmj
82fc4fb3c9 Revert "Fix issue when using tail.exe with non-ascii paths. Fixes #3021"
This reverts commit df42147d92.
2022-10-19 19:38:43 +08:00
grossmj
df42147d92 Fix issue when using tail.exe with non-ascii paths. Fixes #3021 2022-10-19 19:04:29 +08:00
grossmj
da5520aa90 Upgrade PyQt to 5.15.7 and pywin32 to v304 2022-10-19 18:31:40 +08:00
grossmj
04d81efe92 Upgrade PyQt5 to v5.15.7 and pywin32 to v304 2022-10-19 18:30:03 +08:00
Jeremy Grossmann
491c66a315 Merge pull request #3395 from GNS3/fix/3393
Update requirements.txt
2022-10-18 22:30:11 +08:00
Jeremy Grossmann
e5c81da700 Merge branch '2.2' into fix/3393 2022-10-18 22:28:32 +08:00
grossmj
65fad1b4f4 Upgrade to Visual Studio 2022 in appveyor.yml 2022-10-18 21:47:10 +08:00
grossmj
34661908d9 Upgrade to Python 3.7 in appveyor.yml 2022-10-18 21:42:51 +08:00
grossmj
aee5ffa17f Upgrade pip and setuptools in appveyor.yml 2022-10-18 21:40:08 +08:00
grossmj
e9e8be42b5 Upgrade pytest. Fixes #3399 2022-10-18 21:16:46 +08:00
grossmj
ae0d928383 Use jsonschema v3.2.0 for Python 3.6 2022-10-12 22:13:34 +08:00
grossmj
8db3c1be42 Allow for more dependency versions at patch level 2022-10-12 22:07:37 +08:00
grossmj
f50da3ebd7 Replace deprecated distro.linux_distribution() call 2022-10-11 23:28:11 +08:00
grossmj
75b52fc9a4 Update dev-requirements.txt 2022-10-11 23:23:15 +08:00
grossmj
1952da5876 Update requirements.txt 2022-10-11 23:01:24 +08:00
Jeremy Grossmann
1f620026d4 Merge pull request #3394 from KaisenCAS/master
CVE-2007-4559 patch
2022-10-10 23:31:52 +08:00
grossmj
1d293618e5 Upgrade dependencies 2022-10-10 14:29:04 +08:00
Kevin Chevreuil - Kaisen
2622549ce6 Add a fix for the CVE-2007-4559 2022-10-09 22:55:15 +02:00
grossmj
96e2a42012 Revert "Revert "Dot not allow "no border" style for line items""
This reverts commit 2af2a67fae.
2022-09-08 00:15:43 +02:00
grossmj
2af2a67fae Revert "Dot not allow "no border" style for line items"
This reverts commit 80908feeba.
2022-09-08 00:12:46 +02:00
grossmj
26a0da0564 Development on 3.0.0dev5 2022-09-07 01:16:14 +02:00
grossmj
7b52cd4f81 Release v3.0.0a2 2022-09-07 01:05:41 +02:00
grossmj
f0d12c91a2 Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	dev-requirements.txt
#	gns3/crash_report.py
#	gns3/version.py
2022-08-30 11:43:19 +02:00
grossmj
900bd1c0b4 Development on 2.2.35dev1 2022-08-29 11:14:44 +02:00
grossmj
0b3dbb2843 Release v2.2.34 2022-08-28 23:28:12 +02:00
grossmj
7d0b6bf0d2 Add missing 'sys' module. Ref #3373 2022-08-28 16:40:45 +02:00
grossmj
b45cbeaced Upgrade dev dependencies 2022-08-28 16:12:45 +02:00
grossmj
ef4f6b2b27 Upgrade Sentry dependency 2022-08-28 00:02:54 +02:00
grossmj
e9806345ca Downgrade to pytest v7.0.1 (last version to support 2022-08-27 19:35:36 +02:00
grossmj
ee23e32c75 Upgrade dev dependencies 2022-08-27 19:29:42 +02:00
grossmj
80908feeba Dot not allow "no border" style for line items 2022-08-26 21:54:55 +02:00
grossmj
9a8f2e65de Use "none" for solid line style in drawing items 2022-08-25 22:27:09 +02:00
grossmj
4bb74be5a8 Upgrade dependencies 2022-08-24 12:02:56 +02:00
grossmj
fbeacdcb2a Implement new option (Delete All) to contextual menu in "Console" dock. Fixes #3325 2022-08-16 17:48:58 +02:00
grossmj
da6581d1ac Fix editing Docker container config generates exception and empty config. Fixes #3371 2022-08-10 11:34:21 +02:00
grossmj
1fcb9a4cd4 Merge branch '2.2' into 3.0 2022-08-07 23:56:33 +02:00
grossmj
b3937c7b94 Fix 2560x1440 resolution for Docker container 2022-08-07 23:56:11 +02:00
grossmj
036e9ef8a4 Fix cannot detect images by default when trying to upload them in the Image Manager. Fixes #3367 2022-08-06 13:08:30 +02:00
grossmj
4bb99e7917 Fix unable to set VNC console resolution. Fixes #3365 2022-08-06 12:48:39 +02:00
Abdelbaki Boukerche
181bf3f360 Rounded Rectangle 2022-08-04 18:20:54 +01:00
grossmj
f10c86160f Development on v3.0.0dev4 2022-08-04 12:29:49 +02:00
grossmj
f3b3d7565d Release v3.0.0a1 2022-08-04 11:38:17 +02:00
Jeremy Grossmann
1d0a173689 Merge pull request #3358 from GNS3/use-themed-symbols
Let the controller allocate symbols
2022-07-25 20:45:40 +02:00
grossmj
b2363434a2 Set default symbol theme to "Affinity-square-blue" 2022-07-25 20:38:26 +02:00
grossmj
ca3e6f0472 Fix creating a custom Ethernet switch template 2022-07-25 17:24:28 +02:00
grossmj
f1b19f4633 Update decorative symbols (for Wizards etc.) 2022-07-25 17:13:23 +02:00
grossmj
ed57ac3de5 Use generic symbol names 2022-07-25 15:12:29 +02:00
grossmj
6b35992e5a Set raw image param when uploading an image from the appliance wizard 2022-07-22 12:40:32 +02:00
grossmj
5f97d7891f Fix incorrect param in getCompute() 2022-07-22 12:06:27 +02:00
Jeremy Grossmann
6a46c26a37 Merge pull request #3355 from GNS3/enhancement/2076
Remove valid hostname checks for Dynamips, IOU, Qemu and Docker nodes
2022-07-17 11:56:29 +02:00
grossmj
d43411dafd Checks for valid hostname on server side for Dynamips, IOU, Qemu and Docker nodes 2022-07-17 11:51:29 +02:00
grossmj
0427383457 Upgrade dependencies 2022-07-15 11:32:18 +02:00
grossmj
991387f483 Fix incorrect call to QProgress.setValue() with float 2022-07-12 00:50:50 +02:00
Jeremy Grossmann
8ff966dd54 Merge pull request #3350 from GNS3/import-project
Project import
2022-07-05 23:08:47 +02:00
grossmj
271850cfec Remove "portable" from project import/export references 2022-07-05 23:07:10 +02:00
grossmj
f09c67dd3d Reactivate project importation 2022-07-05 23:01:44 +02:00
grossmj
aea8e01d13 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/http_client.py
#	gns3/modules/docker/ui/docker_vm_configuration_page_ui.py
#	gns3/version.py
2022-06-21 16:33:42 +02:00
grossmj
f2711732db Back to development on v2.2.34dev2 2022-06-21 11:52:58 +02:00
grossmj
148ac4b072 Revert "Development on v2.2.34dev2"
This reverts commit 65eeb79b26.
2022-06-21 11:51:01 +02:00
grossmj
65eeb79b26 Development on v2.2.34dev2 2022-06-21 11:38:09 +02:00
grossmj
537304ce08 Release v2.2.33.1 2022-06-21 10:48:02 +02:00
grossmj
f22df5f016 Development on v2.2.34dev1 2022-06-20 21:47:42 +02:00
grossmj
8dfc8b7714 Release v2.2.33 2022-06-20 20:53:21 +02:00
grossmj
8c6fa9433f Upgrade sentry-sdk and psutil 2022-06-20 20:05:34 +02:00
Jeremy Grossmann
63837578c5 Merge pull request #3340 from GNS3/node-name-checks
Check node names
2022-06-20 19:02:37 +02:00
grossmj
b719703dbe Check that node names for Qemu and Docker are valid 2022-06-18 16:59:14 +02:00
grossmj
084d14c17e Backport reset all console connections. Fixes #2072 2022-06-15 15:58:15 +02:00
grossmj
8c0fca1dd7 Add more video resolutions to Docker containers using VNC. Fixes #3329 2022-06-09 00:26:39 +08:00
grossmj
863d05c923 Add python_requires=">=3.4" in setup.py. Fixes #3326 2022-06-07 18:27:01 +08:00
grossmj
3ebaac8a2c Only allow post release corrective versions of GUI and server to interact 2022-06-07 18:22:06 +08:00
grossmj
16878c9dfa Allow minor versions of GUI and server to interact 2022-06-07 18:06:53 +08:00
grossmj
45da18bb7c Update VirtViewer path. Fixes #3334 2022-06-07 17:35:55 +08:00
Jeremy Grossmann
5615141ed7 Merge pull request #3318 from GNS3/remove-qemu-binaries-requirement
Remove Qemu binary requirement
2022-06-06 14:50:52 +08:00
Jeremy Grossmann
c06491b112 Merge pull request #3336 from GNS3/project-export-zstd
zstandard compression support for project export
2022-06-03 11:32:18 +07:00
grossmj
b01c03855e Support compression levels 2022-06-01 20:26:12 +07:00
grossmj
01e101beac Add zstandard compression 2022-06-01 15:35:01 +07:00
grossmj
18eba37b85 Upgrade dependencies 2022-05-26 19:21:37 +07:00
grossmj
3b7fb3329f Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/ui/main_window.ui
#	gns3/ui/main_window_ui.py
#	gns3/version.py
#	requirements.txt
2022-04-27 22:28:16 +07:00
grossmj
7a6d06ea0c Development on 2.2.33dev1 2022-04-27 19:51:24 +07:00
grossmj
d371042647 Upgrade distro package to v1.7.0 2022-04-27 19:48:50 +07:00
grossmj
0321c11c34 Release v2.2.32 2022-04-27 18:47:20 +07:00
grossmj
522df41a57 Use public DSNs for Sentry 2022-04-20 18:41:18 +07:00
grossmj
afccdf5b9e Fix exception when doubleclick on NAT node. Fixes #3312 2022-04-20 17:55:16 +07:00
grossmj
b2cd24b511 Upgrade some packages 2022-04-20 17:38:54 +07:00
grossmj
6d131a05f1 Fix "Apply" button in the "Preferences" dialog stays gray when templates/nodes are opened by double-click. Fixes #3307 2022-04-20 16:49:22 +07:00
grossmj
35e6156c6c Add 'reset docks' in the view menu. Ref #3317 2022-04-20 15:53:32 +07:00
grossmj
5b901fa115 Remove Qemu binary requirement 2022-04-19 18:21:39 +07:00
Jeremy Grossmann
25bd1aa974 Merge pull request #3315 from GNS3/qemu-disk-images
New implementation to create/resize Qemu disk images
2022-04-14 17:23:43 +07:00
grossmj
b0c374a043 Use controller API to list images 2022-04-13 18:13:18 +07:00
grossmj
548663c964 Use new API endpoints to create/resize Qemu disk images. 2022-04-07 16:23:14 +08:00
grossmj
567cd6485f Fix QFileDialog calls 2022-03-30 18:40:50 +08:00
Jeremy Grossmann
d69f00a370 Merge pull request #3306 from GNS3/image-management
Image management dialog
2022-03-20 19:56:47 +10:00
grossmj
1c6eb5eb4d Image management dialog 2022-03-20 19:28:11 +10:00
grossmj
d3c3ae5143 Use README.md in setup.py 2022-03-16 11:22:28 +10:00
grossmj
f2b2e6f618 Drop Python 3.6 support and require Python >= 3.7 2022-03-14 15:13:27 +10:30
grossmj
a617d5aedf Convert README to markdown 2022-03-13 15:44:26 +10:30
grossmj
9e33cd24bb Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/version.py
#	requirements.txt
2022-03-13 14:50:44 +10:30
grossmj
6f767d7455 Upgrade dependencies 2022-03-12 16:23:17 +10:30
grossmj
96d8de4da8 Development on 2.2.32dev1 2022-02-26 20:39:42 +10:30
grossmj
6b5a6f3dfe Release v2.2.31 2022-02-26 18:22:17 +10:30
grossmj
8f82eac321 Development on 2.2.31dev1 2022-02-25 15:59:19 +10:30
grossmj
e03ed64f59 Install setuptools v59.6.0 when using Python 3.6 2022-02-25 15:50:35 +10:30
grossmj
3d702aabd0 Release v2.2.30 2022-02-25 14:51:39 +10:30
grossmj
f5e63c2321 Set setuptools to v60.6.0 2022-02-06 21:02:56 +10:30
grossmj
1047eb916a Upgrade dependencies 2022-02-06 17:33:10 +10:30
grossmj
5dc7d0fbda Upgrade to pywin32 v303. Ref #3290 2022-02-06 17:31:32 +10:30
Jeremy Grossmann
f3ba40de43 Merge pull request #3270 from GNS3/server-settings-refactoring
Consolidation of "Server" settings and simplified wizard
2022-01-16 21:13:26 +10:00
grossmj
7e60e4021b Merge branch '3.0' into server-settings-refactoring
# Conflicts:
#	gns3/dialogs/setup_wizard.py
#	tests/test_link.py
2022-01-16 21:36:38 +10:30
grossmj
2609be98b6 Fix int() call. Ref #3283 2022-01-15 18:57:15 +10:30
grossmj
6286e596c0 Fix QPoint() as unexpected type 'float'. Fixes #3283 2022-01-15 18:55:38 +10:30
grossmj
3c546086ed Fix painter.drawRect() has unexpected type 'float'. Fixes #3282 2022-01-15 18:32:35 +10:30
grossmj
557c47d995 Merge branch 'master' into 3.0
# Conflicts:
#	gns3/version.py
2022-01-11 23:14:01 +10:30
grossmj
f4b2c1c5b9 Fix SpinBox.setValue() requires integer. Fixes #3281 2022-01-11 23:12:54 +10:30
grossmj
e578ecdd8a Development on 2.2.30dev1 2022-01-08 22:52:59 +10:30
grossmj
da8adbaa18 Release v2.2.29 2022-01-08 22:14:59 +10:30
grossmj
6d1333f5fe Clear cache when opening symbol selection dialog. Fixes #3256 2021-12-27 12:43:32 +10:30
grossmj
ce8f9206d9 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/http_client.py
2021-12-25 11:21:10 +10:30
grossmj
92c858dd07 Fix @ in username issue with HTTP authentication. Fixes #3275 2021-12-25 11:19:07 +10:30
grossmj
0c7a12f68c Merge branch 'master' into 2.2 2021-12-25 10:58:46 +10:30
Jeremy Grossmann
a4d08cce8c Merge pull request #3277 from etiennewan/etiennewan-patch-2
Fixed QPoint called with floats
2021-12-25 10:27:00 +10:00
grossmj
e0dd7a66e1 Use '//' operator instead of int() 2021-12-24 13:39:19 +10:30
grossmj
23be668c97 Fix create drawing item calls since mapToScene() returns a QPointF
https://doc.qt.io/qt-5/qgraphicsview.html#mapToScene-4
2021-12-24 13:38:26 +10:30
grossmj
1ac14e0f6d Improvements when connecting and updating computes 2021-12-24 13:08:10 +10:30
Etienne Wan
68d0278140 Fixed QPoint called with floats 2021-12-23 18:37:26 +01:00
grossmj
e0651e349c Use current directory when searching for images. Fixes #3198 2021-12-19 18:54:07 +10:30
Jeremy Grossmann
d8e4c1de4d Merge pull request #3273 from tsndqst/fix_create_link_test
Fix create_link test
2021-12-16 12:26:38 +10:00
Your Name
a5aa9bfb7a Remove problematic lines 2021-12-15 20:13:57 -06:00
grossmj
15889a0ac5 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/version.py
2021-12-16 12:35:51 +10:30
grossmj
3e0273848f Development on 2.2.29dev1 2021-12-15 21:38:34 +10:30
grossmj
ec374f173c Release v2.2.28 2021-12-15 13:54:24 +10:30
grossmj
b8abdc79dc Merge branch 'master' into 2.2 2021-12-15 13:52:41 +10:30
Jeremy Grossmann
43744eab7e Merge pull request #3272 from etiennewan/patch-1
Fixed drawLine called with float arguments
2021-12-15 09:28:35 +10:00
Etienne Wan
e16f700e49 Fixed drawLine called with float arguments 2021-12-13 23:27:28 +01:00
grossmj
f38ab34ea0 Fix tests 2021-12-12 17:26:52 +10:30
grossmj
087172d024 Refactor server settings and wizard 2021-12-12 16:35:09 +10:30
grossmj
a46b08bea1 Disable local server and GNS3 VM preferences 2021-12-09 18:52:01 +10:30
grossmj
a6c3e2a4bb Fix project readme edit/refresh 2021-12-09 18:50:36 +10:30
Jeremy Grossmann
f89ff86808 Merge pull request #3266 from GNS3/http-client-refactoring
HTTP client refactoring
2021-12-09 16:31:04 +10:30
grossmj
50cdb2432d Fix tests 2021-12-08 20:03:23 +10:30
grossmj
04ea58395a Image uploading to controller and project export 2021-12-07 23:37:59 +10:30
grossmj
6331f54b88 HTTP client refactoring completed 2021-12-03 18:14:37 +10:30
grossmj
a91683f6ff Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/version.py
2021-12-02 14:28:57 +10:30
grossmj
62b7d29e4c Start HTTP client refactoring 2021-12-02 14:22:46 +10:30
grossmj
926aec9089 Upgrade dependencies 2021-11-24 17:48:58 +10:30
grossmj
586b640967 Handle empty compute_id in preferences. Ref #3265 2021-11-24 17:41:40 +10:30
Jeremy Grossmann
925d57b2f8 Merge pull request #3263 from FocusedOne/master
Fixed dead VIX API link
2021-11-23 09:15:54 +10:30
FocusedOne
eceaea1317 Fixed dead VIX API link
Replaced old dead vmware link with current 1.17 version download.
2021-11-22 16:40:01 -06:00
grossmj
52322eb982 Remove direct upload to compute 2021-11-16 16:44:28 +10:30
grossmj
4326785dfc Development on 2.2.28dev1 2021-11-13 16:31:21 +10:30
grossmj
3920c28bde Release v2.2.27 2021-11-12 15:33:53 +10:30
grossmj
b34f51e4b0 Merge branch 'master' into 2.2 2021-11-12 14:50:55 +10:30
grossmj
ef45b2e0f1 Fix symbols in "Symbol selection" dialog are not placed in alphabetical order. Fixes #3245 2021-11-08 22:20:22 +10:30
grossmj
545a9f53a8 Fix links duplicates in topology summary. Fixes #3251 2021-11-08 21:55:29 +10:30
grossmj
340ec2d543 Send JWT token in query string when connecting to websocket. Ref https://github.com/GNS3/gns3-server/pull/1992 2021-11-01 16:52:43 +10:30
grossmj
2d3c7ac0c2 Remove traceng code 2021-10-22 16:11:12 +10:30
grossmj
3cd15e5f1a Option to delete orphaned image files from disk when template is removed. Fixes #3249 2021-10-21 15:59:29 +10:30
grossmj
1a2d3d65c1 Remove Qemu legacy networking code 2021-10-20 15:59:05 +10:30
grossmj
c93a543ccb Update appliance files 2021-10-17 15:09:49 +10:30
grossmj
579fb6ceb8 Merge branch 'master' into 3.0
# Conflicts:
#	gns3/version.py
2021-10-09 12:13:24 +10:30
grossmj
83d9367860 Development on 2.2.27dev1 2021-10-08 21:49:11 +10:30
grossmj
2131f07e5f Merge branch '2.2' 2021-10-08 21:46:38 +10:30
grossmj
cf3e716e63 Release v2.2.26 2021-10-08 21:02:04 +10:30
grossmj
c79f14bcab Open "template configuration" dialog with double click on template name in "Preferences". Fixes #3239 2021-10-08 16:35:25 +10:30
grossmj
acd044a88a Only show "virtio" network adapter when legacy node is enabled. Fixes https://github.com/GNS3/gns3-gui/issues/1969 2021-10-08 15:46:56 +10:30
grossmj
391ac73f1a Merge branch '2.2' into 3.0 2021-10-07 21:01:46 +10:30
grossmj
eedec3f999 Upgrade dependencies and test using Python 3.10 2021-10-07 14:59:54 +10:30
grossmj
c103e2deba Upgrade dependencies 2021-09-23 10:39:27 +09:30
Jeremy Grossmann
f26c638350 Merge pull request #3237 from SDN-Projects/optimization/pip-no-cache-dir
chore : use --no-cache-dir flag to pip in dockerfiles to save space
2021-09-23 09:59:58 +09:30
Pratik Raj
4ea24e622b chore : use --no-cache-dir flag to pip in dockerfiles to save space
using --no-cache-dir flag in pip install ,make sure downloaded packages
by pip don't cached on system . This is a best practice which make sure
to fetch from repo instead of using local cached one . Further , in case
of Docker Containers , by restricting caching , we can reduce image size.
In term of stats , it depends upon the number of python packages
multiplied by their respective size . e.g for heavy packages with a lot
of dependencies it reduce a lot by don't caching pip packages.

Further , more detail information can be found at

https://medium.com/sciforce/strategies-of-docker-images-optimization-2ca9cc5719b6

Signed-off-by: Pratik Raj <rajpratik71@gmail.com>
2021-09-22 15:13:38 +05:30
grossmj
ab854752d9 Double-click on a template opens "template configuration" dialog. Fixes #3236 2021-09-20 20:28:56 +09:30
grossmj
5cee045a65 Fix "Custom symbols" can't be unfolded after using "Filter" field. Fixes #3231 2021-09-20 18:39:32 +09:30
grossmj
48e7ef07cf Isolate and unisolate support. Fixes https://github.com/GNS3/gns3-gui/issues/3190 2021-09-15 18:48:02 +09:30
grossmj
c6f8198974 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/version.py
2021-09-15 16:58:57 +09:30
grossmj
37cd82fb44 Development on v2.2.26dev1 2021-09-14 21:13:04 +09:30
grossmj
334eb5175c Release v2.2.25 2021-09-14 19:20:11 +09:30
grossmj
c4561e81eb Support authentication using JWT tokens 2021-09-11 19:53:40 +09:30
grossmj
25841ea7db Fix menu disabled for modal dialogs on macOS. Fixes #3007 2021-09-09 21:21:22 +09:30
grossmj
b3603ea364 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/node.py
#	gns3/version.py
#	win-requirements.txt
2021-09-09 16:27:25 +09:30
grossmj
3d3b4f92b2 Change method to display the recent files menu. Fixes #3007 2021-09-09 09:23:45 +09:30
grossmj
82740da89d Fix bug when using empty port names for custom adapters. Fixes #3228 2021-09-08 16:16:27 +09:30
grossmj
ad19b3dda0 Upgrade PyQt5 to version 5.15.4 for macOS 2021-09-08 15:36:04 +09:30
grossmj
bb8fd18f98 Fix mouse zoom-in/out step value is two times bigger than keyboard one. Fixes #3226 2021-09-08 15:26:56 +09:30
grossmj
336eaf443a Upgrade to Qt 5.15.4 on Windows. Ref #3210 2021-09-08 14:25:25 +09:30
grossmj
0b94be6805 Fix issue with custom adapters at the node level. Fixes #3223 2021-09-05 21:15:30 +09:30
grossmj
671ced78ff Merge branch 'master' into 2.2 2021-09-02 14:45:00 +09:30
Jeremy Grossmann
c8766ce529 Merge pull request #3157 from hrnciar/setuptools
Explicitly require setuptools, utils/get_resource.py imports pkg_resources
2021-08-29 22:06:22 -07:00
Jeremy Grossmann
bec9512c78 Merge branch 'master' into setuptools 2021-08-29 22:02:55 -07:00
grossmj
dd44582347 Merge branch 'master' into 3.0
# Conflicts:
#	dev-requirements.txt
#	gns3/version.py
#	win-requirements.txt
2021-08-29 19:15:35 +09:30
grossmj
b2ad5f4158 Development on 2.2.25dev1 2021-08-25 21:23:19 +09:30
grossmj
966873bc6c Release v2.2.24 2021-08-25 20:31:26 +09:30
grossmj
5b9111b55d Merge branch 'master' into 2.2 2021-08-25 20:08:45 +09:30
grossmj
56688f2236 Update dependencies 2021-08-24 21:12:27 +09:30
grossmj
2e656a9d53 Fix incorrect Qemu binary selected when importing template. Fixes https://github.com/GNS3/gns3-gui/issues/3216 2021-08-24 17:26:07 +09:30
grossmj
2790f707c3 Early support for Python3.10 2021-08-15 15:10:02 +09:30
grossmj
270301d9b5 Bump version to 3.0.0dev3 2021-08-12 16:05:20 +09:30
grossmj
4614543edf Upgrade dependencies 2021-08-10 23:08:14 +09:30
grossmj
80b7ece5a8 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/version.py
#	mac-requirements.txt
2021-08-10 15:28:01 +09:30
Jeremy Grossmann
ee9002df61 Merge pull request #3217 from GNS3/dependabot/pip/pywin32-301
Bump pywin32 from 300 to 301
2021-08-09 17:59:04 -07:00
dependabot[bot]
52626e9fe9 Bump pywin32 from 300 to 301
Bumps [pywin32](https://github.com/mhammond/pywin32) from 300 to 301.
- [Release notes](https://github.com/mhammond/pywin32/releases)
- [Changelog](https://github.com/mhammond/pywin32/blob/master/CHANGES.txt)
- [Commits](https://github.com/mhammond/pywin32/commits)

---
updated-dependencies:
- dependency-name: pywin32
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-09 20:48:41 +00:00
grossmj
6619c6af97 Add PyQt5==5.12.3 for macOS build 2021-08-07 20:09:41 +09:30
grossmj
60e04c7248 Development on 2.2.24dev1 2021-08-05 21:16:10 +09:30
grossmj
724858f977 Release v2.2.23 2021-08-05 15:58:54 +09:30
Jeremy Grossmann
5a2e05a4fd Merge pull request #3212 from GNS3/handle_no-kvm_deprecated
Handle -no-kvm param deprecated in Qemu >= v5.2
2021-07-27 17:37:45 +09:30
grossmj
010888e3ca Handle -no-kvm param deprecated in Qemu >= v5.2 2021-07-27 16:34:51 +09:30
grossmj
3226921536 Support for invisible links. Fixes #2461 2021-07-27 15:30:58 +09:30
grossmj
022e918301 Add kitty console application command line. Fixes #3203 2021-07-25 16:12:35 +09:30
grossmj
846b19a9e7 Add Windows Terminal profile as an option for Console Applications. Fixes #3193 2021-06-14 13:10:01 +09:30
grossmj
54511b2c45 Upgrade dependencies. 2021-06-12 15:30:38 +09:30
grossmj
ed36917cf3 Fix tests 2021-06-12 15:09:18 +09:30
grossmj
7545c600bc Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/items/link_item.py
#	gns3/main_window.py
#	gns3/ui/resources_rc.py
#	gns3/version.py
#	win-requirements.txt
2021-06-12 14:25:47 +09:30
grossmj
45f5c6e010 Development on 2.2.23dev1 2021-06-10 16:20:10 +09:30
grossmj
963bbb7b89 Release v2.2.22 2021-06-10 15:41:58 +09:30
grossmj
016ad7a775 Fix exception shown when GNS3 is started with empty config. Fixes #3188 2021-06-10 12:28:25 +09:30
grossmj
e8c82566c6 Add ZOC8 console terminal for macOS command line 2021-06-07 19:41:09 +09:30
grossmj
1ed6fceade Fix tests. Ref https://github.com/GNS3/gns3-gui/issues/2461 2021-06-07 14:26:20 +09:30
grossmj
d945fd8b7b Minor changes to style editor dialog. 2021-06-07 14:10:32 +09:30
grossmj
fd6c7eccd0 Link style support. Fixes https://github.com/GNS3/gns3-gui/issues/2461 2021-06-07 14:09:58 +09:30
grossmj
7a1afe2aec Upgrade dependencies 2021-06-07 13:46:06 +09:30
grossmj
6debe56d8e Fix charcoal theme. Ref #3137 2021-06-06 21:45:16 +09:30
grossmj
a4c7d41c26 Fix issue when showing menu to select port. Fixes #3169 2021-05-20 22:15:21 -07:00
grossmj
ea9243dcd9 Merge remote-tracking branch 'origin/2.2' into 2.2 2021-05-20 15:00:49 +09:30
grossmj
e9d8337bd6 Revert "Downgrade to PyQt5 5.12.1. Fixes https://github.com/GNS3/gns3-gui/issues/3169"
This reverts commit ece4d51213.
2021-05-20 14:59:44 +09:30
Jeremy Grossmann
3c92e463f8 Update setup.py
Fixes https://github.com/GNS3/gns3-server/issues/1897
2021-05-16 17:45:11 +09:30
grossmj
4dd7db5a86 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/version.py
#	win-requirements.txt
2021-05-15 20:12:52 +09:30
grossmj
3d07db5c5f Development on 2.2.22dev1 2021-05-10 23:44:40 +09:30
grossmj
20cc309ac8 Release v2.2.21 2021-05-10 22:42:47 +09:30
grossmj
262a2839c5 Fix issue with empty project variable name. Fixes #3162 2021-05-10 17:55:06 +09:30
grossmj
ece4d51213 Downgrade to PyQt5 5.12.1. Fixes https://github.com/GNS3/gns3-gui/issues/3169 2021-05-10 17:03:22 +09:30
grossmj
6d2ffc4614 Revert "Rename __json__() to asdict()"
This reverts commit bc14f15a
2021-04-17 23:38:08 +09:30
grossmj
bc14f15a61 Rename __json__() to asdict() 2021-04-17 23:34:28 +09:30
grossmj
2c7de627f7 Merge branch 'master' into 3.0
# Conflicts:
#	gns3/version.py
#	requirements.txt
#	win-requirements.txt
2021-04-10 12:31:08 +09:30
grossmj
0ef39ba129 Development on 2.2.21dev1 2021-04-09 13:50:21 +09:30
grossmj
f90267b4f0 Release v2.2.20 2021-04-09 12:14:38 +09:30
grossmj
8f16706a22 Merge branch 'master' into 2.2 2021-04-09 12:05:15 +09:30
grossmj
2d3ee3abf9 Fix project does not load anymore. Fixes #3140 2021-04-07 16:47:09 +09:30
grossmj
b8b209fa55 Fix errors while connecting to server 2021-04-07 15:51:52 +09:30
grossmj
18129e3d29 Do not connect to server while waiting for user to accept/reject SSL certificate. Fixes #3144 2021-04-07 12:09:38 +09:30
grossmj
7a2b9c024f Fix invalid server version check request. Fixes #3144 2021-04-07 12:03:37 +09:30
grossmj
4923a6dc17 Revert to PyQt5 v5.12.3 because of SSL not working
Probably the SSL DLLs weren't properly found and included by cx_Freeze
2021-04-06 23:01:07 +09:30
grossmj
73dfc047aa Set PyQt5 version to 5.15.2 on Windows 2021-04-06 22:12:34 +09:30
grossmj
fe0a70c4be Upgrade dependencies 2021-04-06 13:57:00 +09:30
Tomas Hrnciar
67014965be Explicitly require setuptools, utils/get_resource.py imports pkg_resources 2021-03-31 11:53:28 +02:00
Jeremy Grossmann
f14cb43404 Merge pull request #3153 from VidVidex/master
Add terminator as a predefined custom console option
2021-03-26 14:53:00 +10:30
Vid
f8517ee5ac Add terminator as a predefined custom console option 2021-03-24 20:28:19 +01:00
grossmj
7dc607b4c5 Development on 2.2.20dev1 2021-03-05 16:48:09 +10:30
grossmj
882fa76550 Release v2.2.19 2021-03-05 14:51:03 +10:30
grossmj
76d9fcf60e Fix merge 2021-02-16 20:58:00 +10:30
grossmj
c3049ea843 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/modules/qemu/pages/qemu_vm_configuration_page.py
#	gns3/modules/qemu/qemu_vm.py
#	gns3/modules/qemu/ui/qemu_vm_configuration_page.ui
#	gns3/modules/qemu/ui/qemu_vm_configuration_page_ui.py
#	gns3/version.py
2021-02-16 20:55:41 +10:30
grossmj
1490a1ad8f Development on 2.2.19dev1 2021-02-16 20:44:58 +10:30
grossmj
aab0c99cc6 Release v2.2.18 2021-02-16 19:09:46 +10:30
grossmj
a6a987d74c Fix bug with SSL connection on projet websocket stream. 2021-02-16 17:42:18 +10:30
grossmj
9c58b18c20 Merge remote-tracking branch 'origin/2.2' into 2.2 2021-02-16 16:42:45 +10:30
grossmj
8bc499c68f Bump version to 2.2.18dev2 2021-02-16 16:35:26 +10:30
Jeremy Grossmann
bd5eb288b7 Merge pull request #3130 from GNS3/ssl-support
SSL support.
2021-02-16 16:16:32 +10:30
grossmj
465a289568 SSL support. 2021-02-16 16:08:27 +10:30
grossmj
d240ba3056 Merge remote-tracking branch 'origin/2.2' into 2.2 2021-01-26 23:11:11 +10:30
grossmj
3cedfd3649 Remove the useless file "zoom-in (copy).svg". Fixes #3114 2021-01-26 23:10:34 +10:30
Jeremy Grossmann
276d7abdd9 Merge pull request #3104 from b-ehlers/QemuConfig
Add Qemu config disk
2020-12-14 14:23:09 +10:30
grossmj
d6432c2e88 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/version.py
2020-12-07 17:56:09 +10:30
grossmj
927e38bd6d Development on 2.2.18dev1 2020-12-04 18:10:11 +10:30
grossmj
376cc29995 Release v2.2.17 2020-12-04 16:26:56 +10:30
grossmj
1f8ebeb084 Merge branch 'master' into 2.2 2020-12-04 16:21:42 +10:30
grossmj
0212755c78 Remove "-nographic" option by default for Qemu VM. Fixes #3094 2020-12-02 18:44:18 +10:30
Jeremy Grossmann
2f7d75eae9 Fix app cannot start on macOS Big Sur. Ref #3037 2020-11-30 20:02:52 +10:30
Jeremy Grossmann
fc1c060922 Merge pull request #3097 from SpikefishSolutions/master
Add yes/no prompts to gui for global project level buttons start/stop/reload/suspend to prevent bad day.
2020-11-21 18:32:45 +10:30
John
0ea72ce782 one more spacing update 2020-11-20 21:33:44 -05:00
John
3de2d2eda2 spacing updates 2020-11-20 21:32:26 -05:00
John
c08262f8af Correct stop/start/reload/suspend button names 2020-11-20 21:26:08 -05:00
John
9ae70bf2fe Add yes/no prompts to all major buttons 2020-11-20 21:11:15 -05:00
John
fa6d250602 oops.. need to build after commit. 2020-11-20 20:27:43 -05:00
John
0668840a2b i don't get it. 2020-11-20 20:26:19 -05:00
John
8b25d1b06c can't fix indent? 2020-11-20 20:25:06 -05:00
John
58c3ba0755 update indent 2020-11-20 20:23:46 -05:00
John
5a91c9aaf8 Create a message box for stopping all devives instead of blindly making someone's day terrible. 2020-11-20 18:28:30 -05:00
grossmj
89b9e6c332 Providing the path to create a project is now deprecated. 2020-11-13 15:17:55 +10:30
grossmj
54b5d8f347 Upgrade packages to latest versions 2020-11-08 21:35:55 +10:30
grossmj
237ef785ad Fix tests 2020-11-05 18:26:15 +10:30
grossmj
0ddd91af43 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/link.py
#	gns3/version.py
2020-11-05 17:05:58 +10:30
grossmj
0fc3f4ef16 Development on 2.2.17dev1 2020-11-05 16:59:58 +10:30
grossmj
f0e5cd2ba2 Release v2.2.16 2020-11-05 15:38:19 +10:30
grossmj
f59ef6378a Fix broken security link (replaced by email). Fixes #3085 2020-11-05 15:00:04 +10:30
grossmj
61ef08d1b7 Fix packets capture stops after some time. Fixes #3067 2020-11-05 14:21:22 +10:30
grossmj
e812c000fd Option to allocate or not the vCPUs and RAM settings for the GNS3 VM. Fixes https://github.com/GNS3/gns3-gui/issues/3069 2020-11-05 11:13:57 +10:30
grossmj
7f5db61722 Client to use version 3 of the API. 2020-11-02 18:15:43 +10:30
grossmj
fcc5c4c114 Merge remote-tracking branch 'origin/3.0' into 3.0 2020-10-19 18:28:30 +10:30
grossmj
68f3dc763d Fix some API calls. 2020-10-19 18:28:17 +10:30
Bernhard Ehlers
d3d9e1e8ae Use HDD disk image as startup QEMU config disk 2020-10-19 03:45:27 +02:00
Jeremy Grossmann
786306304b Merge pull request #3075 from b-ehlers/fix_hdd_layout
Fix HDD configuration layout
2020-10-16 19:21:41 +10:30
Bernhard Ehlers
05f8df345a Fix HDD configuration layout
(cherry picked from commit 4f631669e5)
2020-10-16 10:22:32 +02:00
Bernhard Ehlers
4f631669e5 Fix HDD configuration layout 2020-10-16 10:22:32 +02:00
grossmj
0b8fb93752 Merge branch '2.3' into 3.0
# Conflicts:
#	gns3/version.py
2020-10-12 21:21:51 +10:30
grossmj
422f6004b1 Bump version to 3.0.0dev1 2020-10-12 17:48:32 +10:30
grossmj
717d683b44 Merge branch '2.2' into 3.0 2020-10-12 17:47:26 +10:30
grossmj
4b0cc11cab Development on 2.2.16dev1 2020-10-07 16:30:03 +10:30
grossmj
b5285cd142 Release v2.2.15 2020-10-07 15:29:52 +10:30
grossmj
69482343ba Fix custom symbol not sent to remote controller when installing appliance 2020-10-07 15:09:08 +10:30
grossmj
932f737ed9 Create branch 3.0 2020-10-06 11:59:24 +10:30
grossmj
d4639c2e61 Development on 2.2.15dev1 2020-09-15 06:49:11 +09:30
grossmj
b85ade9dd7 Release v2.2.14 2020-09-15 05:52:48 +09:30
grossmj
e191cb8aa3 Fix tests. Ref #3002 2020-09-14 00:10:11 +09:30
grossmj
e6bc75ce26 Improvements to add a new version of an appliance from wizard. Fixes #3002. 2020-09-14 00:04:58 +09:30
grossmj
bc1df346f2 Development on 2.2.14dev1 2020-09-05 04:26:16 +09:30
grossmj
27c35321f0 Release v2.2.13 2020-09-04 23:13:28 +09:30
grossmj
64a0ee37de Change Qemu disk descriptions. Fixes #3035 2020-08-18 18:07:01 +09:30
Jeremy Grossmann
ec8d214c08 Merge pull request #3040 from b-ehlers/edit_config_text
Edit only text mode config files
2020-08-18 08:44:56 +08:00
Bernhard Ehlers
3e212fc629 Edit only text mode config files
(cherry picked from commit 880ac5e8c3)
2020-08-18 02:27:31 +02:00
Bernhard Ehlers
880ac5e8c3 Edit only text mode config files 2020-08-18 02:27:31 +02:00
Jeremy Grossmann
feb40a6250 Merge pull request #3039 from b-ehlers/hide_empty_config
Hide config import/export when configFiles attribute is empty
2020-08-17 19:36:34 +08:00
Bernhard Ehlers
25e41dc0f1 Hide config import/export when configFiles attribute is empty
(cherry picked from commit fd7b915e96)
2020-08-17 13:09:59 +02:00
Bernhard Ehlers
fd7b915e96 Hide config import/export when configFiles attribute is empty 2020-08-17 13:09:59 +02:00
grossmj
5fbb6cbf61 Qemu disk interfaces must be set to "none" by default. Ref #3035 2020-08-17 12:49:21 +09:30
grossmj
c58c7774c4 Qemu disk interfaces must be set to "none" by default. Ref #3035
(cherry picked from commit 5fbb6cbf61)
2020-08-17 12:49:21 +09:30
Jeremy Grossmann
fda948cc5b Merge pull request #3024 from GNS3/qemu-config-disk
QEMU config disk - enable QEMU config import/export
2020-08-15 16:40:13 +08:00
Jeremy Grossmann
a9b18f1771 Merge branch '2.3' into qemu-config-disk 2020-08-15 16:36:58 +08:00
grossmj
bd2bc8265c Do not allow image to be configured on Qemu VM secondary slave disk if create config disk option is enabled.
(cherry picked from commit 04f9a1cf8c)
2020-08-15 16:05:43 +09:30
grossmj
04f9a1cf8c Do not allow image to be configured on Qemu VM secondary slave disk if create config disk option is enabled. 2020-08-15 16:05:43 +09:30
grossmj
f2209a2780 Add explicit option to automatically create or not the config disk. Off by default.
(cherry picked from commit af79471afd)
2020-08-14 17:57:24 +09:30
grossmj
af79471afd Add explicit option to automatically create or not the config disk. Off by default. 2020-08-14 17:57:24 +09:30
grossmj
6067786783 Bump version to 2.3.0dev2 2020-08-13 01:49:25 +09:30
grossmj
090fc63bb6 Merge branch '2.2' into 2.3
# Conflicts:
#	gns3/version.py
2020-08-13 01:48:49 +09:30
Jeremy Grossmann
029a1df7f7 Merge pull request #3029 from GNS3/aux-console-refactoring
Auxiliary console support
2020-08-12 15:59:04 +08:00
grossmj
7b99ba325b Development on 2.2.13dev1 2020-08-07 21:12:46 +09:30
grossmj
74763287fb Release v2.2.12 2020-08-07 19:27:32 +09:30
grossmj
737ff42d64 Merge branch 'master' into 2.2 2020-08-07 19:04:08 +09:30
Jeremy Grossmann
b47aa95b3e Downgrade psutil to version 5.6.7 2020-07-29 17:36:46 +09:30
Jeremy Grossmann
5656bd2d48 Downgrade psutil to version 5.6.7 2020-07-29 17:36:14 +09:30
grossmj
ffb364591f Auxiliary console support for Qemu. Ref #2873
Improvements for auxiliary console support for Docker and Dynamips.
2020-07-29 16:23:51 +09:30
Jeremy Grossmann
058c069394 Fix log shows the GUI command line without spaces between its arguments. Fixes #3026 2020-07-27 18:27:23 +09:30
grossmj
6a9440c978 Support to reset all console connections. Ref https://github.com/GNS3/gns3-server/issues/1619 2020-07-26 18:27:18 +09:30
grossmj
758054cfd3 Support to reset links. Fixes https://github.com/GNS3/gns3-server/issues/1620 2020-07-24 21:48:49 +09:30
grossmj
39e6a6e2ab Fix bug when recent files cannot be seen in the new project dialog. 2020-07-24 18:49:32 +09:30
grossmj
7a74685c0a Wait for the controller to be online before allowing actions like creating or opening a project. Fixes #2907 2020-07-22 17:32:01 +09:30
grossmj
926ec48d00 Upgrade to psutil version 5.7.2 2020-07-21 15:49:58 +09:30
grossmj
f48eff2344 Upgrade to psutil version 5.7.2 2020-07-21 15:49:06 +09:30
grossmj
b8a583d3f6 Show progress dialog immediately when connecting to server. Ref #2907 2020-07-20 19:14:54 +09:30
Bernhard Ehlers
d01f15c4df QEMU config disk - enable QEMU config import/export 2020-07-19 17:56:24 +09:30
grossmj
3cbad22a04 Fix tests 2020-07-19 14:27:09 +09:30
grossmj
c61a99e78d Add total RAM, CPUs and disk size to servers summary as well as disk usage in percent. Fixes https://github.com/GNS3/gns3-server/issues/1532 2020-07-19 14:16:07 +09:30
Jeremy Grossmann
e318610983 Merge pull request #3023 from GNS3/docker-resource-constraints
Resource constraints for Docker VMs.
2020-07-18 19:39:08 +08:00
grossmj
ab7cc29fa2 Resource constraints for Docker VMs. 2020-07-18 21:03:55 +09:30
grossmj
2788019e17 Do not show project readme when loading a snapshot. 2020-07-18 17:41:05 +09:30
grossmj
410e5353b2 Use server host is console host is equal to "0:0:0:0:0:0:0:0" 2020-07-17 21:13:27 +09:30
grossmj
bfb90406ed Remove VMware promotion. 2020-07-17 21:12:59 +09:30
grossmj
819bb1c58e Wait for readme to be updated before exporting the project. 2020-07-17 16:36:35 +09:30
grossmj
723f806a52 Support for "usage" for "Cloud" nodes. Fixes https://github.com/GNS3/gns3-gui/issues/2887
Allow "usage" for all builtin nodes (not exposed in Ui).
2020-07-15 19:15:51 +09:30
grossmj
a5093e06d1 Markdown support in project Readme. Fixes #2550 #2289
Allow project README to be edited from "File->Edit project". Fixes #2829
2020-07-14 22:40:05 +09:30
grossmj
f33c01ac58 Bump PyQt version to 5.15.0 LTS. Ref #3020 2020-07-13 14:07:38 +09:30
grossmj
8c382e5b7d Bump version to 2.3.0dev1 2020-07-11 11:41:51 +09:30
grossmj
439cdce287 Development on 2.2.12dev1 2020-07-09 21:37:03 +09:30
grossmj
4e50c2a4b1 Release v2.2.11 2020-07-09 20:37:10 +09:30
grossmj
94c636ae61 Merge branch 'master' into 2.2 2020-07-09 20:23:31 +09:30
grossmj
f53b9a266e Try to fix "Recent project" selection not working. Ref #3007 2020-07-09 15:37:47 +09:30
grossmj
2476448032 Fix debug entries shown twice in console window and double error messages with remote GNS3VM. Fixes #3010 #3011
Note that the debug level is broken (has been for a long time apparently).
2020-07-07 21:06:56 +09:30
grossmj
a34cd742e3 Merge branch '2.2' 2020-07-06 22:20:05 +09:30
grossmj
39698196ac Fix deprecation warning. Ref #3009 2020-07-06 22:08:10 +09:30
grossmj
61432ced4f Fix tests on macOS. Ref #3009 2020-07-06 22:03:29 +09:30
grossmj
2ddd13c445 Add Snyk badges. 2020-06-27 18:27:04 +09:30
grossmj
af6c4c5b3e Merge branch 'master' into 2.2 2020-06-26 21:30:37 +09:30
grossmj
d4012294bf Run tests inside container 2020-06-26 20:50:48 +09:30
grossmj
4b04b0e855 Use xvfb to run tests 2020-06-26 20:41:08 +09:30
grossmj
1bec5019bf Install PyQt5 using pip 2020-06-26 19:42:42 +09:30
grossmj
ec6b876baa Explicitly install sip 2020-06-26 19:38:10 +09:30
grossmj
7cee0d01ab Install PyQt5 for tests and add GitHub Actions badge 2020-06-26 19:34:02 +09:30
Jeremy Grossmann
dd3314d06b Set up GitHub Actions for running tests 2020-06-26 19:27:22 +09:30
grossmj
9c58b26265 Remove tox, Travis CI and pep8.sh script
Update dependencies
2020-06-26 19:21:51 +09:30
Jeremy Grossmann
83c26f47da Merge pull request #3005 from GNS3/whitesource/configure
Configure WhiteSource for GitHub.com
2020-06-26 09:04:39 +08:00
whitesource-for-github-com[bot]
8ed2f55600 Add .whitesource configuration file 2020-06-26 01:01:54 +00:00
Jeremy Grossmann
b435317904 Merge pull request #3003 from GNS3/snyk-fix-8bf3e4840df4587cc42afb85b857d470
[Snyk] Security upgrade psutil from 5.6.6 to 5.6.7
2020-06-25 11:41:01 +08:00
snyk-bot
acb8aa8ca2 fix: requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-PSUTIL-483082
2020-06-24 13:10:33 +00:00
grossmj
c55d6b8a6f Fix sentry SDK is configured twice. 2020-06-24 12:40:06 +09:30
grossmj
a4039a254e Development on 2.2.11dev1 2020-06-18 19:06:00 +09:30
grossmj
85ed4b3026 Release v2.2.10 2020-06-18 12:29:33 +09:30
grossmj
5207a99692 New fix for multi-device selection/deselection not working as expected with right click. Fixes #2986 2020-06-09 13:50:31 +09:30
grossmj
d69527995d Revert "Fix Multi-device selection/deselection not working as expected with right click. Fixes #2986"
This reverts commit ddeb95cb0a.
2020-06-09 13:19:20 +09:30
grossmj
4b000ba2f7 Optimize snap-to-grid code for drawing items. Fixes #2997 2020-06-09 11:47:05 +09:30
grossmj
1b302b77a0 Move jsonschema 2.6.0 requirement in build repository.
https://github.com/GNS3/gns3-server/issues/1751
https://github.com/GNS3/gns3-gui/issues/2849

This is to avoid the following error:

```
ERROR: Double requirement given: jsonschema==2.6.0 (from -r gns3-gui\win-requirements.txt (line 4)) (already in jsonschema==3.2.0 (from -r gns3-gui\requirements.txt (line 1)), name='jsonschema')
```
2020-06-07 13:07:16 +09:30
grossmj
a5b5c404ec Only use jsonschema 2.6.0 on Windows and macOS.
https://github.com/GNS3/gns3-server/issues/1751
https://github.com/GNS3/gns3-gui/issues/2849
2020-06-07 12:55:22 +09:30
grossmj
6b97b0c6cd Disable default integrations for sentry sdk. 2020-06-06 15:37:17 +09:30
grossmj
115dd43eee Development on 2.2.10dev1 2020-06-04 21:06:30 +09:30
grossmj
2530bf97a8 Release v2.2.9 2020-06-04 18:39:27 +09:30
grossmj
9892fd0654 Fix issue editing README.txt on Windows. 2020-06-04 18:12:50 +09:30
grossmj
c71ee73da8 Merge branch 'master' into 2.2 2020-06-04 12:22:11 +09:30
Jeremy Grossmann
0643fd516d Merge pull request #2993 from GNS3/replicate-network-connection-state
Support to activate/deactive network connection state replication in Qemu
2020-06-04 10:49:41 +08:00
grossmj
a25680f2ce Fix GUI doesn't detect another GUI on macOS. Fixes #2994 2020-06-03 20:38:55 +09:30
grossmj
58bd5be920 Support to activate/deactive network connection state replication in Qemu. 2020-06-02 18:45:22 +09:30
Jeremy Grossmann
d95633ba2c Merge pull request #2989 from GNS3/reset-mac-addresses
Option to reset all MAC addresses when exporting or duplicating a project.
2020-05-27 10:53:22 +08:00
grossmj
dfea6d1723 Option to reset or not all MAC addresses when exporting or duplicating a project. 2020-05-27 12:14:47 +09:30
grossmj
ddeb95cb0a Fix Multi-device selection/deselection not working as expected with right click. Fixes #2986 2020-05-26 16:11:53 +09:30
grossmj
5f7ff0d70d Generate MainWindow Ui file. 2020-05-26 13:01:04 +09:30
Jeremy Grossmann
a00e039cec Merge pull request #2875 from fatoms/master
Proposed fix for "Edit readme" is missing in GNS3 GUI. #2854
2020-05-26 11:30:19 +08:00
Dominic
a24e9adef1 Merge branch 'Edit_Readme' 2020-05-21 20:03:03 +02:00
Dominic Harford
ee5f8e8edd Resolve conflict with GNS3 repo 2020-05-21 19:08:29 +02:00
grossmj
f5470130f5 Fix issues with crash reporting & bump version to 2.2.9dev2. Ref https://github.com/GNS3/gns3-server/issues/1758 2020-05-21 18:19:19 +09:30
grossmj
1ff405885e Merge branch 'master' into 2.2 2020-05-20 17:25:37 +09:30
Dominic
9fb42ead9f Revert "Updated GUI pyqt files from Tab Order 'fixes' in "Tab Order in Preferences and Project Dialog #2872""
This reverts commit 0d2f91709c.
2020-05-19 18:31:02 +02:00
grossmj
2ea1946c0f Replace Raven by Sentry SDK. Fixes https://github.com/GNS3/gns3-server/issues/1758 2020-05-19 15:48:53 +09:30
grossmj
963e054918 Fix online help menu URL. Fixes #2984 2020-05-08 12:42:06 +09:30
grossmj
0f5f6ab645 Require setuptools>=17.1 in setup.py. Ref https://github.com/GNS3/gns3-server/issues/1751
This is to support environmental markers.
https://github.com/pypa/setuptools/blob/master/CHANGES.rst#171
2020-05-08 12:34:58 +09:30
grossmj
8a905b5c39 Development on 2.2.9dev1 2020-05-07 23:10:20 +09:30
grossmj
e917193f06 Merge branch '2.2' 2020-05-07 23:09:04 +09:30
grossmj
16846ce49c Release v2.2.8 2020-05-07 18:10:57 +09:30
grossmj
624a670ae7 Make sure "port" is defined. 2020-05-07 17:51:32 +09:30
grossmj
406326ccd8 Merge remote-tracking branch 'origin/master' 2020-05-06 11:57:25 +09:30
grossmj
24bc15fb73 Default port set to 80 for server running in the GNS3 VM. Fixes #1737 2020-05-05 12:40:50 +09:30
grossmj
348d8b9438 Make the Web UI the default page. Ref https://github.com/GNS3/gns3-server/issues/1737 2020-04-30 17:27:06 +09:30
grossmj
6787982408 Fix "export portable project forgets contents of README". Fixes #1724 2020-04-30 16:43:00 +09:30
Jeremy Grossmann
c2384917fa Update README. Ref https://github.com/GNS3/gns3-server/issues/1719 2020-04-29 15:01:45 +09:30
grossmj
b80178d0cf Activate unified title and toolbar on MacOS. Fixes #2968 2020-04-29 13:08:51 +09:30
grossmj
e6084ed834 Confirmation dialog for "console connect to all nodes". Fixes #2971 2020-04-28 15:04:39 +09:30
grossmj
ba924cd0d9 Add "Resume all suspended links". Fixes #2858 2020-04-28 14:00:26 +09:30
grossmj
0c3d43346f Revert "Change default path for SecureCRT. Fixes #2896"
This reverts commit 0c4367d77e.
2020-04-28 13:19:38 +09:30
grossmj
fcf6ef3027 Remove @property from ConfigurationDialog(). Fixes #2819 #2965 2020-04-28 11:57:54 +09:30
grossmj
e0f87e573d Use Environmental Markers to force jsonschema version. Fixes https://github.com/GNS3/gns3-gui/issues/2849
Version 3.2.0 with Python >= 3.8
Version 2.6.0 with Python < 3.8
2020-04-27 12:54:17 +09:30
grossmj
3ec068f0cb Use Environmental Markers to force jsonschema version 2.6.0 on Windows/macOS. Ref https://github.com/GNS3/gns3-gui/issues/2849 2020-04-27 12:43:07 +09:30
grossmj
37f1fcf6f7 Remove preferences dialog geometry restoration. Fixes #2807 2020-04-27 11:55:01 +09:30
grossmj
c51dd1605d Merge branch '2.3'
# Conflicts:
#	gns3/version.py
2020-04-13 11:57:04 +09:30
grossmj
4ebf3b4e1c Development on 2.2.8dev1 2020-04-08 01:26:42 +09:30
grossmj
b1ec9d535c Release v2.2.7 2020-04-08 00:03:13 +09:30
grossmj
7fc9087cf0 Fix unable to configure custom adapters for Qemu VMs. Fixes #2961 2020-04-07 15:47:53 +09:30
Bernhard Ehlers
5dc2c77806 QEMU config disk - enable QEMU config import/export
(cherry picked from commit d01f15c4df)
2020-04-06 13:42:00 +02:00
grossmj
4972d460d2 Fix tests. 2020-04-06 21:09:47 +09:30
grossmj
c388836be7 Fix VNC console template doesn't extract %i (Project UUID). Fixes #2960 2020-04-06 18:34:37 +09:30
grossmj
18ae4a6ce9 Fix contextual menu issues. Ref #2955 2020-03-30 21:37:52 -07:00
grossmj
3020e1fc9f Downgrade to PyQt 5.12.3 Ref #2955 #2952 2020-03-29 18:18:53 +10:30
grossmj
fe2f8424db Downgrade to PyQt 5.13.2 Ref #2955 #2952 2020-03-29 14:46:34 +10:30
grossmj
a744f65199 Merge branch 'master' into 2.3
# Conflicts:
#	gns3/version.py
2020-03-28 13:44:08 +10:30
grossmj
d27578f0fc Release v2.2.6 2020-03-26 12:37:59 +10:30
grossmj
b01c11f19b Prevent locked drawings to be deleted. Fixes https://github.com/GNS3/gns3-gui/issues/2948 2020-03-16 16:30:09 +10:30
grossmj
fb269da4d3 Fix issues with empty project variables. Fixes https://github.com/GNS3/gns3-gui/issues/2941 2020-03-14 17:22:44 +10:30
grossmj
ab15f96bb5 Upgrade psutil to version 5.6.6 due to CVE-2019-18874
https://github.com/advisories/GHSA-qfc5-mcwq-26q8
2020-03-14 15:47:12 +10:30
grossmj
5bb8b8e8bd Use existing README.txt if existing when exporting portable project. Fixes https://github.com/GNS3/gns3-server/issues/1724 2020-03-10 17:32:13 +10:30
grossmj
3f9632fae0 Allow creation of a diskless Qemu VMs. Fixes #2939 2020-03-10 17:04:07 +10:30
grossmj
b5f8195abb Re-enable "create new version" in appliance wizard. Fixes #2837 2020-03-03 13:11:01 +08:00
grossmj
73a293bd17 Fix unable to load project from project library. Fixes #2932 2020-03-03 09:34:45 +08:00
grossmj
0a1dfb99e9 Merge remote-tracking branch 'origin/2.2' into 2.2 2020-02-19 14:13:16 +08:00
grossmj
d352919264 Fix some permission denied errors when loading remote project. Ref #2871 Fixes #2901 2020-02-19 14:13:03 +08:00
Jeremy Grossmann
65f2a1e461 Merge pull request #2931 from inthought/2.2
Add 'Royal TS V5' to predefined console list
2020-02-18 13:29:46 +10:30
Travis Abram
71f289721b Add 'Royal TS V5' to predefined console list 2020-02-16 20:46:40 -08:00
grossmj
c28089d400 Disallow invalid grid sized. Fixes #2908 2020-02-10 16:59:17 +08:00
grossmj
64f009bf71 Check if hostname is blank. Fixes #2924 2020-01-25 18:21:02 +08:00
Jeremy Grossmann
edb2fd7fd9 Merge pull request #2925 from GNS3/qemu-changes
GUI changes to support recent versions of Qemu
2020-01-25 16:04:54 +07:00
grossmj
62e7ad8c8a Add nvme disk interface and fix scsi disk interface for Qemu VMs. 2020-01-25 16:22:34 +08:00
grossmj
caeb5d71c3 Add latest Qemu nic models. 2020-01-24 19:05:46 +08:00
grossmj
cfe96b2311 Upgrade Qt version to 5.14.1. Ref #2778 #2903 2020-01-24 17:47:01 +08:00
grossmj
8955b9ee29 Upgrade to PyQt 5.14.1. Ref #2778 2020-01-22 17:53:04 +08:00
grossmj
e727abf27a Change version to 2.3.0dev1 on 2.3 branch 2020-01-16 18:06:51 +08:00
grossmj
f209bf7644 Development on 2.2.6dev1 2020-01-10 00:32:10 +08:00
grossmj
5860dedc32 Release v2.2.5 2020-01-09 23:52:40 +08:00
grossmj
9e2df17a4e Add gns3-gui.xml and update Linux icons paths & permissions. Ref #2919 2020-01-09 23:49:44 +08:00
grossmj
a95761437a Development on 2.2.5dev1 2020-01-09 05:17:01 +08:00
grossmj
626510865f Update paths to icons for Linux 2020-01-09 04:19:21 +08:00
grossmj
2e248aa340 Release v2.2.4 2020-01-09 00:45:09 +08:00
grossmj
e306f73f01 Fix "Console to all nodes" doesn't open cloud objects with console configured. Fixes #2902 2020-01-08 00:35:57 +08:00
grossmj
0c4367d77e Change default path for SecureCRT. Fixes #2896 2019-12-26 06:13:23 +08:00
grossmj
6dda0ff787 Add icons in setup.py Ref #2898 2019-12-26 06:04:40 +08:00
grossmj
d7d4b84309 Add remote viewer as a VNC console for Linux. Fixes #2913 2019-12-26 04:13:02 +08:00
grossmj
7b57983699 Development on 2.2.4dev1 2019-11-12 16:43:21 +08:00
grossmj
bb89fe2275 Release v2.2.3 2019-11-12 15:29:54 +08:00
grossmj
7eb2a923b2 Fix issue when binding on 0.0.0.0. Fixes #2892 2019-11-11 17:49:09 +08:00
grossmj
58052e3cce Allow double click on cloud with configured console to open session. Fixes #2894 2019-11-11 14:31:35 +08:00
grossmj
e431104f6b Officially support Python 3.8. Ref https://github.com/GNS3/gns3-gui/issues/2895 2019-11-11 12:56:11 +08:00
grossmj
a4e9d6b8ce Set psutil to version 5.6.3 in requirements.txt 2019-11-08 10:44:17 +08:00
grossmj
f58f5c7b95 Developement version on 2.2.3dev1 2019-11-04 19:45:18 +08:00
Dominic
c8a8663ff0 Restore editReadme attribute which was removed in Change 'New export project wizard' ( ID c2472bcb22 ) 2019-10-18 17:11:28 +02:00
Dominic
d27e5c1795 Revert "Remove unused edit readme action. Fixes #2816"
This reverts commit 7cd0187f33.
2019-10-18 16:46:48 +02:00
Dominic
0d2f91709c Updated GUI pyqt files from Tab Order 'fixes' in "Tab Order in Preferences and Project Dialog #2872" 2019-10-18 16:45:39 +02:00
410 changed files with 108157 additions and 126059 deletions

View File

@@ -0,0 +1,16 @@
name: Add new issues to GNS3 project
on:
issues:
types:
- opened
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v1.0.1
with:
project-url: https://github.com/orgs/GNS3/projects/3
github-token: ${{ secrets.ADD_NEW_ISSUES_TO_PROJECT }}

93
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,93 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '17 22 * * 6'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: python
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

19
.github/workflows/testing.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: testing
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and run Docker image
run: |
docker build -t gns3-gui-test .
docker run gns3-gui-test

1
.gitignore vendored
View File

@@ -63,3 +63,4 @@ __pycache__
# Virtualenv
env
venv

View File

@@ -1,2 +0,0 @@
branch:
2.2

View File

@@ -1,21 +0,0 @@
sudo: required
services:
- docker
notifications:
email: false
script:
- docker build -t gns3-gui-test .
- docker run gns3-gui-test
before_deploy:
- sudo pip install twine
- sudo pip install urllib3[secure]
deploy:
provider: pypi
edge:
branch: v1.8.45
user: noplay
password:
secure: FofcqlJjgqf2jaDaXpLHeigVoexbrOz3WwnDuiJpwJxeFUlPY8s2cQs/Bm+dzxzZaOaGiVE0A83v/Xa10yD5tflThHt4sqYJK3iQCinA7wgeAlDimB4xrWUNplfNJZ/Eod5Ssa++E02W+3i29PxpXY//mjCY7qDxaoxul1gnFJY=
on:
tags: true
repo: GNS3/gns3-gui

14
.whitesource Normal file
View File

@@ -0,0 +1,14 @@
{
"scanSettings": {
"configMode": "AUTO",
"configExternalURL": "",
"projectToken" : "",
"baseBranches": ["master", "2.2", "3.0"]
},
"checkRunSettings": {
"vulnerableCheckRunConclusionLevel": "failure"
},
"issueSettings": {
"minSeverityLevel": "LOW"
}
}

View File

@@ -1,2 +0,0 @@
Jeremy Grossmann
Julien Duponchelle

681
CHANGELOG
View File

@@ -1,5 +1,686 @@
# Change Log
## 3.0.6 28/01/2026
* Fixing tab name in MobaXterm
* PyQt6 migration
* Add XDG Config Home support
* Support for Python 3.14
* Clicking the "console connect to all nodes" opens all consoles in name order with case-insensitively
## 2.2.56 21/01/2026
* Fixing tab name in MobaXterm
* PyQt6 migration
* Add XDG Config Home support
## 2.2.55 19/11/2025
* Fix SyntaxWarning: invalid escape sequence. Fixes #3760
* Support for Python 3.14
* Clicking the "console connect to all nodes" opens all consoles in name order with case-insensitively
## 3.0.5 14/05/2025
* Merge remote-tracking branch 'origin/2.2' into 3.0
* Merge remote-tracking branch 'origin/2.2' into 3.0
* Development on 2.2.55.dev1
* Release v2.2.54
* Development on 2.2.54.dev1
* Replace "Docker hub" by "Docker repository" because it is possible to use different repositories
* Upgrade dependencies
* Fix bring console in front when clicking on "Open all consoles". Fixes #3706
* Upgrade dependencies
* Allow password greater than 8 characters. Ref #3714
* Development on 3.0.5.dev1
* Add -F arg to wmctrl. Ref #3706
## 2.2.54 21/04/2025
* Replace "Docker hub" by "Docker repository" because it is possible to use different repositories
* Upgrade dependencies
* Fix bring console in front when clicking on "Open all consoles". Fixes #3706
* Add -F arg to wmctrl. Ref #3706
## 3.0.4 25/02/2025
* Upgrade dependencies
* Fix auto idle-pc for IOS templates
* Add user info and password change for logged-in user. Fixes #3698
## 3.0.3 22/01/2025
* Set minimum duration for progress dialog when uploading. Ref https://github.com/GNS3/gns3-gui/issues/3682
* Add logs when uploading images to the controller
* Option to disable SSL certificate verification for future connections. Fixes https://github.com/GNS3/gns3-gui/issues/3694
* Fix packet capture when connected to a controller with SSL. Fixes https://github.com/GNS3/gns3-gui/issues/3696
* Update status after importing an image when installing a new appliance. Fixes #3691
* Update file browser filters to find IOU images without extension. Fixes #3692
* Upgrade dependencies
## 2.2.53 21/01/2025
* Update file browser filters for all files and IOU images
* Upgrade dependencies
* Fix Linux Mint default terminal configuration
## 3.0.2 03/01/2025
* Add button to create templates based on images that are not used by any yet.
* Add "prune" images button in image management dialog.
* Use the controller image endpoint to install appliances
* Drop Python 3.8
* Add image info tooltip in image management dialog.
* Upgrade dependencies
* Apply grid color via css property
## 3.0.1 27/12/2024
* Fix issue when image is already on the local server. Fixes https://github.com/GNS3/gns3-gui/issues/3678
* Fix image uploading when image name differs with the image name recorded in the appliance. Fixes https://github.com/GNS3/gns3-gui/issues/3682
* Fix Linux Mint default terminal configuration
## 3.0.0 20/12/2024
* Change title of QMessageBox
## 2.2.52 02/12/2024
* Add iol extension filter. Ref #3664
* Remove maximum 64GB RAM limitation for QEMU VMs. Fixes #3658
* Bring to front support for consoles on Linux.
* Relax setuptools requirement to allow for easier Debian packaging on Ubuntu Focal & Jammy
## 3.0.0rc2 20/11/2024
* Remove maximum 64GB RAM limitation for QEMU VMs. Fixes #3658
* Fix GUI connection to server.
* Bring to front support for consoles on Linux.
* Relax setuptools requirement to allow for easier Debian packaging on Ubuntu Focal & Jammy
* Do not include tokens in notification logs
* Python 3.13 support
## 2.2.51 07/11/2024
* Python 3.13 support
* Upgrade dependencies
* Add keyboard shortcut for Add Link
## 2.2.50 21/10/2024
* Fix issue when pid file contains invalid data
* Add comment to indicate sentry-sdk is optional. Ref https://github.com/GNS3/gns3-server/issues/2423
* Improve information provided when uploading invalid appliance image. Fixes #3637
* Use "experimental features" option to force listening for HTTP notification streams. Ref #3579
* Fix to allow packet capture on more than 6 links. Fixes #3594
* Support for configuring MAC address in Docker containers
* Add KRDC to pre-configured VNC console commands
## 3.0.0rc1 11/08/2024
* Add keep the original compute IDs option when exporting a project
## 2.2.49 06/08/2024
* Upgrade jsonschema and sentry-sdk packages
* Upgrade to PyQt5 v5.15.11
* Add shortcuts info dialog
* Added Key Shortcuts
## 2.2.48.1 12/07/2024
* No changes
## 2.2.48 08/07/2024
* Use "experimental features" to allow bypassing hostname validation. Ref #3524
* Update appliance_v8.json. Ref https://github.com/GNS3/gns3-registry/pull/897
* Option to keep the compute IDs unchanged when exporting a project
* Upgrade sentry-sdk and psutil packages
* Switch to PyQt5 5.15.10 for macOS build
## 3.0.0b3 19/05/2024
* Fix updating IOS router
* Ensure Python >= 3.8 is used in pyproject.toml
## 2.2.47 15/05/2024
* Remove maximum size for capture dialog. Ref #3576
* Change sentry-sdk version
* Upgrade aiohttp, sentry-sdk and truststore
* Upgrade jsonschema and aiohttp
* Drop Python 3.7
* Remove dev requirements for Python 3.6
* Add NAT symbols
* Only show log message if event has "message"
## 3.0.0b2 07/04/2024
* Enable local controller support on Linux
* Support for custom Qemu path in templates and nodes
* Round CPUs value in Docker templates and VMs. Ref https://github.com/GNS3/gns3-gui/issues/3572
## 2.2.46 26/02/2024
* Add GNS3 console command "env" to show what environment variables are used. Ref https://github.com/GNS3/gns3-server/issues/2306
* Add CTRL+C shortcut to copy status bar message. Ref #3561
* Key modifier (ALT) to ignore snap to grid. Fixes #3538
* Increase timeout to 5s for status bar messages. The coordinates message has no timeout and can be reset when clicking on the scene. Ref #3561
* Add reset GUI state feature. Ref #3549
* Fix for hiding Windows terminal. Ref #3290
* Drop support for Python 3.6
* Upgrade sentry-sdk, psutil and distro dependencies
## 2.2.45 12/01/2024
* Add missing console_type values in appliance_v8.json. Ref https://github.com/GNS3/gns3-registry/issues/849
* Handle moved project notifications on controller stream
* Add debug for PATH env variable
* Add custom executable paths on Windows
* Add --suppressApplicationTitle for Windows terminal. Fixes https://github.com/GNS3/gns3-gui/issues/3544
* Upgrade sentry-sdk and aiohttp
## 3.0.0b1 27/11/2023
* Upgrade sentry-sdk to v1.37.1
* Deactivate showing a percentage in progress bar. Ref #3543
## 3.0.0a6 15/11/2023
* Possible fix for stuck image upload. Ref https://github.com/GNS3/gns3-server/issues/2310
* Fix timeout issue when creating Qemu disk image. Fixes https://github.com/GNS3/gns3-server/issues/2313
* Add ISO file to images filter in Image Manager. Ref https://github.com/GNS3/gns3-server/issues/2310
* Update custom command help and protect against double quote in project name
* Refactor command variables support
* Pass os.environ in Popen()
* Add the ability to edit width and height in the style edit dialog.
## 2.2.44.1 07/11/2023
* No changes
## 2.2.44 06/11/2023
* Fix timeout issue when creating Qemu disk image. Fixes https://github.com/GNS3/gns3-server/issues/2313
* Refactor command variables support
* Add vendor_logo_url in appliance schemas. Ref https://github.com/GNS3/gns3-registry/pull/825
* Add Qemu IGB network device
* Add the ability to edit width and height in the style edit dialog.
## 3.0.0a5 27/10/2023
* Upgrade to actions/checkout@v3 and actions/setup-python@v3
* Fix cannot change default VLAN for an access port for builtin Ethernet switch. Fixes #3528
* Add Qemu IGB network device
## 3.0.0a4 18/10/2023
* New packaging relying only pyproject.toml
* Apply Snap-to-grid of drawing items not on their center position. Fixes #3465
* Allow computes to be dynamically or manually allocated
* Add UEFI boot mode option for Qemu VMs
* Adjust some values in pyproject.toml
* Migrate to pyproject.toml
* Mark VMware and VirtualBox support as deprecated
* Fix RecursionError with invalid credentials. Fixes #3374
* Allow raw images by default. Fixes https://github.com/GNS3/gns3-server/issues/2097
* Dot not allow "no border" style for line items
* Use "none" for solid line style in drawing items
* Fix editing Docker container config generates exception and empty config. Fixes #3371
* Fix cannot detect images by default when trying to upload them in the Image Manager. Fixes #3367
* Fix unable to set VNC console resolution. Fixes #3365
* Set default symbol theme to "Affinity-square-blue"
* Fix creating a custom Ethernet switch template
* Update decorative symbols (for Wizards etc.)
* Use generic symbol names
* Set raw image param when uploading an image from the appliance wizard
* Fix incorrect param in getCompute()
* Checks for valid hostname on server side for Dynamips, IOU, Qemu and Docker nodes
* Fix incorrect call to QProgress.setValue() with float
* Reactivate project importation
* Support compression levels
* Add zstandard compression
* Upgrade dependencies
* Remove Qemu binary requirement
* Use controller API to list images
* Use new API endpoints to create/resize Qemu disk images.
* Drop Python 3.6 support and require Python >= 3.7
* Improvements when connecting and updating computes
* Use current directory when searching for images. Fixes #3198
* Refactor server settings and wizard
* Disable local server and GNS3 VM preferences
* Image uploading to controller and project export
* HTTP client refactoring completed
* Start HTTP client refactoring
* Upgrade dependencies
* Handle empty compute_id in preferences. Ref #3265
* Remove direct upload to compute
* Send JWT token in query string when connecting to websocket. Ref https://github.com/GNS3/gns3-server/pull/1992
* Option to delete orphaned image files from disk when template is removed. Fixes #3249
* Remove Qemu legacy networking code
* Isolate and unisolate support. Fixes https://github.com/GNS3/gns3-gui/issues/3190
* Support authentication using JWT tokens
* Change Qemu disk descriptions. Fixes #3035
* Edit only text mode config files
* Hide config import/export when configFiles attribute is empty
* Qemu disk interfaces must be set to "none" by default. Ref #3035
* Do not allow image to be configured on Qemu VM secondary slave disk if create config disk option is enabled.
* Add explicit option to automatically create or not the config disk. Off by default.
* Auxiliary console support for Qemu. Ref #2873 Improvements for auxiliary console support for Docker and Dynamips.
* Support to reset all console connections. Ref https://github.com/GNS3/gns3-server/issues/1619
* Support to reset links. Fixes https://github.com/GNS3/gns3-server/issues/1620
* Fix bug when recent files cannot be seen in the new project dialog.
* Wait for the controller to be online before allowing actions like creating or opening a project. Fixes #2907
* Show progress dialog immediately when connecting to server. Ref #2907
* QEMU config disk - enable QEMU config import/export
* Add total RAM, CPUs and disk size to servers summary as well as disk usage in percent. Fixes https://github.com/GNS3/gns3-server/issues/1532
* Resource constraints for Docker VMs.
* Wait for readme to be updated before exporting the project.
* Support for "usage" for "Cloud" nodes. Fixes https://github.com/GNS3/gns3-gui/issues/2887 Allow "usage" for all builtin nodes (not exposed in Ui).
## 2.2.43 19/09/2023
* Add KiTTY to preconfigured telnet consoles. Fixes #3507
* Fix generic icon in Wayland. Ref #3501
* Support for appliance format version 8.
* Use importlib instead of pkg_resources
* Upgrade to PyQt 5.15.9 and pywin32
* Add support for appliance version 8 format
## 2.2.42 09/08/2023
* Use the system's certificate store for SSL connections
* Give a node some time to start before opening the console (for console auto start). Fixes #3474
* Use Mate Terminal by default if installed on Debian, Ubuntu and Linux Mint.
* Support for gnome-terminal tabs to be opened in the same window.
* Remove import urllib3 and let sentry_sdk import and patch it. Fixes https://github.com/GNS3/gns3-gui/issues/3498
* Add import sys in sudo.py
* Rounded Rectangle support
## 2.2.41 12/07/2023
* Use alternative method to set the correct permissions for uBridge on macOS
* Remove sending stats to GA
* Catch urllib3 exceptions when sending crash report. Ref https://github.com/GNS3/gns3-gui/issues/3483
* Backport UEFI boot mode support for Qemu VMs
* Add debug for dropEvent. Ref https://github.com/GNS3/gns3-server/issues/2242
## 2.2.40.1 10/06/2023
* No changes
## 2.2.40 06/06/2023
* Change log messages for Websocket errors
* Do not proceed if an appliance symbol cannot be downloaded. Ref #3466
* Delete a node or link from topology summary view using Delete key. Ref #3445
* Fix "Start the capture visualization program" checkbox works only one (first) time for a given link. Fixes #3442
* Let the selected link style applied when editing a link. Fixes #3460
* Fix hovered color shown in style editing dialog. Fixes #3460
## 2.2.39 08/05/2023
* Fix nodes are not snapped to the grid at the moment of creation
* Upgrade distro and aiohttp dependencies
## 2.2.38 28/02/2023
* Add long description content type in setup.py
* Automatically add new issues to GNS3 project
* Development 2.2.38.dev1
## 2.2.37 25/01/2023
* Upgrade to PyQt5 v5.15.7
* Changed Windows Terminal telnet console profile from OS X to windows ref: issue #3193
## 2.2.36 04/01/2023
* Add Trusted Platform Module (TPM) support for Qemu VMs
* Add "on_close" setting to appliance schema. Fixes https://github.com/GNS3/gns3-server/issues/2148
* Add default 'ide' disk interface when manually creating Qemu VM template. Fixes #3360
* Fix zoom factor is multiplied when loading projects. Fixes #3408
* Remove deprecated PuTTY option in preferences. Ref https://github.com/GNS3/gns3-gui/discussions/3415
## 3.0.0a3 27/12/2022
* Catch timeout error while updating appliance files
* Fix RecursionError with invalid credentials. Fixes #3374
* Allow raw images by default. Fixes https://github.com/GNS3/gns3-server/issues/2097
* Remove deprecated PuTTY option in preferences. Ref https://github.com/GNS3/gns3-gui/discussions/3415
* Fix "variables": [] in project file leads to unlimited increase of empty name/value pairs in GUI. Fixes #3397
* Ignore local revision when comparing versions.
* Make version PEP 440 compliant
* Support for Python 3.11
* Replace deprecated distro.linux_distribution() call
* Add a fix for the CVE-2007-4559
## 2.2.35.1 10/11/2022
* Re-release Web-Ui v2.2.35
## 2.2.35 08/11/2022
* Fix "variables": [] in project file leads to unlimited increase of empty name/value pairs in GUI. Fixes #3397
* Make version PEP 440 compliant
* Support for Python 3.11
* Upgrade PyQt to 5.15.7 and pywin32 to v305
* Allow for more dependency versions at patch level
* Replace deprecated distro.linux_distribution() call
* Add a fix for the CVE-2007-4559
## 3.0.0a2 06/09/2022
* Add missing 'sys' module. Ref #3373
* Upgrade dev dependencies
* Dot not allow "no border" style for line items
* Use "none" for solid line style in drawing items
* Implement new option (Delete All) to contextual menu in "Console" dock. Fixes #3325
* Fix editing Docker container config generates exception and empty config. Fixes #3371
* Fix 2560x1440 resolution for Docker container
* Fix cannot detect images by default when trying to upload them in the Image Manager. Fixes #3367
* Fix unable to set VNC console resolution. Fixes #3365
## 3.0.0a1 04/08/2022
* Set default symbol theme to "Affinity-square-blue"
* Fix creating a custom Ethernet switch template
* Update decorative symbols (for Wizards etc.)
* Use generic symbol names
* Set raw image param when uploading an image from the appliance wizard
* Checks for valid hostname on server side for Dynamips, IOU, Qemu and Docker nodes
* Support compression levels
* Add zstandard compression
* Remove Qemu binary requirement
* Use controller API to list images
* Use new API endpoints to create/resize Qemu disk images.
* Image management dialog
* Drop Python 3.6 support and require Python >= 3.7
* Improvements when connecting and updating computes
* Use current directory when searching for images. Fixes #3198
* Refactor server settings and wizard
* Disable local server and GNS3 VM preferences
* Image uploading to controller and project export
* HTTP client refactoring
* Handle empty compute_id in preferences. Ref #3265
* Remove direct upload to compute
* Send JWT token in query string when connecting to websocket. Ref https://github.com/GNS3/gns3-server/pull/1992
* Remove traceng code
* Option to delete orphaned image files from disk when template is removed. Fixes #3249
* Remove Qemu legacy networking code
* Isolate and unisolate support. Fixes https://github.com/GNS3/gns3-gui/issues/3190
* Support authentication using JWT tokens
* Providing the path to create a project is now deprecated.
* Client to use version 3 of the API.
* Change Qemu disk descriptions. Fixes #3035
* Edit only text mode config files
* Hide config import/export when configFiles attribute is empty
* Qemu disk interfaces must be set to "none" by default. Ref #3035
* Do not allow image to be configured on Qemu VM secondary slave disk if create config disk option is enabled.
* Add explicit option to automatically create or not the config disk. Off by default.
* Auxiliary console support for Qemu. Ref #2873 Improvements for auxiliary console support for Docker and Dynamips.
* Support to reset all console connections. Ref https://github.com/GNS3/gns3-server/issues/1619
* Support to reset links. Fixes https://github.com/GNS3/gns3-server/issues/1620
* Fix bug when recent files cannot be seen in the new project dialog.
* Wait for the controller to be online before allowing actions like creating or opening a project. Fixes #2907
* Show progress dialog immediately when connecting to server. Ref #2907
* QEMU config disk - enable QEMU config import/export
* Add total RAM, CPUs and disk size to servers summary as well as disk usage in percent. Fixes https://github.com/GNS3/gns3-server/issues/1532
* Resource constraints for Docker VMs.
* Support for "usage" for "Cloud" nodes. Fixes https://github.com/GNS3/gns3-gui/issues/2887 Allow "usage" for all builtin nodes (not exposed in Ui).
* Markdown support in project Readme. Fixes #2550 #2289 Allow project README to be edited from "File->Edit project". Fixes #2829
## 2.2.34 28/08/2022
* Upgrade dev dependencies
* Implement new option (Delete All) to contextual menu in "Console" dock. Fixes #3325
* Fix 2560x1440 resolution for Docker container
## 2.2.33.1 21/06/2022
* Match GNS3 server version
## 2.2.33 20/06/2022
* Upgrade sentry-sdk and psutil
* Check that node names for Qemu and Docker are valid
* Backport reset all console connections. Fixes #2072
* Add more video resolutions to Docker containers using VNC. Fixes #3329
* Add python_requires=">=3.4" in setup.py. Fixes #3326
* Only allow post release corrective versions of GUI and server to interact
* Allow minor versions of GUI and server to interact
* Update VirtViewer path. Fixes #3334
## 2.2.32 27/04/2022
* Use public DSNs for Sentry
* Fix exception when doubleclick on NAT node. Fixes #3312
* Fix "Apply" button in the "Preferences" dialog stays gray when templates/nodes are opened by double-click. Fixes #3307
* Add 'reset docks' in the view menu. Ref #3317
## 2.2.31 26/02/2022
* Install setuptools v59.6.0 when using Python 3.6
## 2.2.30 25/02/2022
* Set setuptools to v60.6.0
* Upgrade to pywin32 v303. Ref #3290
* Fix int() call. Ref #3283
* Fix QPoint() as unexpected type 'float'. Fixes #3283
* Fix painter.drawRect() has unexpected type 'float'. Fixes #3282
* Fix SpinBox.setValue() requires integer. Fixes #3281
## 2.2.29 08/01/2022
* Clear cache when opening symbol selection dialog. Fixes #3256
* Fix @ in username issue with HTTP authentication. Fixes #3275
* Use '//' operator instead of int()
* Fix create drawing item calls since mapToScene() returns a QPointF https://doc.qt.io/qt-5/qgraphicsview.html#mapToScene-4
* Fixed QPoint called with floats
## 2.2.28 15/12/2021
* Fixed drawLine called with float arguments
* Fixed dead VIX API link
## 2.2.27 12/11/2021
* Fix symbols in "Symbol selection" dialog are not placed in alphabetical order. Fixes #3245
* Fix links duplicates in topology summary. Fixes #3251
* chore : use --no-cache-dir flag to pip in dockerfiles to save space
## 2.2.26 08/10/2021
* Upgrade embedded Python to version 3.7 in Windows package
* Upgrade Visual C++ Redistributable for Visual Studio 2019 in Windows package
* Fix SSL support in Windows package
* Open "template configuration" dialog with double click on template name in "Preferences". Fixes #3239
* Only show "virtio" network adapter when legacy node is enabled. Fixes https://github.com/GNS3/gns3-gui/issues/1969
* Double-click on a template opens "template configuration" dialog. Fixes #3236
* Fix "Custom symbols" can't be unfolded after using "Filter" field. Fixes #3231
## 2.2.25 14/09/2021
* Fix menu disabled for modal dialogs on macOS. Fixes #3007
* Change method to display the recent files menu. Fixes #3007
* Fix bug when using empty port names for custom adapters. Fixes #3228
* Upgrade Qt to version 5.15.4 on macOS
* Fix mouse zoom-in/out step value is two times bigger than keyboard one. Fixes #3226
* Upgrade to Qt 5.15.4 on Windows. Ref #3210
* Fix issue with custom adapters at the node level. Fixes #3223
* Explicitly require setuptools, utils/get_resource.py imports pkg_resources
## 2.2.24 25/08/2021
* Fix incorrect Qemu binary selected when importing template. Fixes https://github.com/GNS3/gns3-gui/issues/3216
* Early support for Python3.10
* Bump pywin32 from 300 to 301
* Add PyQt5==5.12.3 for macOS build
## 2.2.23 05/08/2021
* Handle -no-kvm param deprecated in Qemu >= v5.2
* Support for invisible links. Fixes #2461
* Add kitty console application command line. Fixes #3203
* Add Windows Terminal profile as an option for Console Applications. Fixes #3193
## 2.2.22 10/06/2021
* Fix exception shown when GNS3 is started with empty config. Fixes #3188
* Add ZOC8 console terminal for macOS command line
* Link style support. Fixes https://github.com/GNS3/gns3-gui/issues/2461
* Fix charcoal theme. Ref #3137
* Fix issue when showing menu to select port. Fixes #3169
## 2.2.21 10/05/2021
* Fix issue with empty project variable name. Fixes #3162
* Downgrade to PyQt5 5.12.1. Fixes https://github.com/GNS3/gns3-gui/issues/3169
## 2.2.20 09/04/2021
* Fix project does not load anymore. Fixes #3140
* Do not connect to server while waiting for user to accept/reject SSL certificate. Fixes #3144
* Fix invalid server version check request. Fixes #3144
* Upgrade dependencies
* Add terminator as a predefined custom console option
## 2.2.19 05/03/2021
* No changes
## 2.2.18 16/02/2021
* SSL support.
* Remove the useless file "zoom-in (copy).svg". Fixes #3114
* Use HDD disk image as startup QEMU config disk
* Edit only text mode config files
* Hide config import/export when configFiles attribute is empty
* Qemu disk interfaces must be set to "none" by default. Ref #3035
* Do not allow image to be configured on Qemu VM secondary slave disk if create config disk option is enabled.
* Add explicit option to automatically create or not the config disk. Off by default.
* QEMU config disk support
## 2.2.17 04/12/2020
* Remove "-nographic" option by default for Qemu VM. Fixes #3094
* Fix app cannot start on macOS Big Sur. Ref #3037
* Require confirmation before stopping all devices.
## 2.2.16 05/11/2020
* Fix packets capture stops after some time. Fixes #3067
* Option to allocate or not the vCPUs and RAM settings for the GNS3 VM. Fixes https://github.com/GNS3/gns3-gui/issues/3069
## 2.2.15 07/10/2020
* Fix custom symbol not sent to remote controller when installing appliance
## 2.2.14 14/09/2020
* Improvements to add a new version of an appliance from wizard. Fixes #3002.
## 2.2.13 04/09/2020
* No changes
## 2.2.12 07/08/2020
* Downgrade psutil to version 5.6.7
* Fix log shows the GUI command line without spaces between its arguments. Fixes #3026
* Use server host is console host is equal to "0:0:0:0:0:0:0:0"
* Remove VMware promotion.
## 2.2.11 09/07/2020
* Try to fix "Recent project" selection not working. Ref #3007
* Fix debug entries shown twice in console window and double error messages with remote GNS3VM. Fixes #3010
* Fix deprecation warning. Ref #3009
* Fix tests on macOS. Ref #3009
* Fix sentry SDK is configured twice.
## 2.2.10 18/06/2020
* New fix for multi-device selection/deselection not working as expected with right click. Fixes #2986
* Optimize snap-to-grid code for drawing items. Fixes #2997
* Move jsonschema 2.6.0 requirement in build repository.
* Only use jsonschema 2.6.0 on Windows and macOS.
* Disable default integrations for sentry sdk.
## 2.2.9 04/06/2020
* Fix GUI doesn't detect another GUI on macOS. Fixes #2994
* Support to activate/deactive network connection state replication in Qemu.
* Option to reset or not all MAC addresses when exporting or duplicating a project.
* Fix Multi-device selection/deselection not working as expected with right click. Fixes #2986
* Replace Raven by Sentry SDK. Fixes https://github.com/GNS3/gns3-server/issues/1758
* Fix online help menu URL. Fixes #2984
* Require setuptools>=17.1 in setup.py. Ref https://github.com/GNS3/gns3-server/issues/1751 This is to support environmental markers. https://github.com/pypa/setuptools/blob/master/CHANGES.rst#171
* Update README. Ref https://github.com/GNS3/gns3-server/issues/1719
* Restore editReadme attribute which was removed in Change 'New export project wizard'
* Updated GUI pyqt files from Tab Order 'fixes' in "Tab Order in Preferences and Project Dialog #2872"
## 2.2.8 07/05/2020
* Default port set to 80 for server running in the GNS3 VM. Fixes #1737
* Make the Web UI the default page. Ref https://github.com/GNS3/gns3-server/issues/1737
* Fix "export portable project forgets contents of README". Fixes #1724
* Activate unified title and toolbar on MacOS. Fixes #2968
* Confirmation dialog for "console connect to all nodes". Fixes #2971
* Add "Resume all suspended links". Fixes #2858
* Revert "Change default path for SecureCRT. Fixes #2896"
* Remove @property from ConfigurationDialog(). Fixes #2819 #2965
* Use Environmental Markers to force jsonschema version. Fixes https://github.com/GNS3/gns3-gui/issues/2849 Version 3.2.0 with Python >= 3.8 Version 2.6.0 with Python < 3.8
* Use Environmental Markers to force jsonschema version 2.6.0 on Windows/macOS. Ref https://github.com/GNS3/gns3-gui/issues/2849
* Remove preferences dialog geometry restoration. Fixes #2807
* Fix unable to configure custom adapters for Qemu VMs. Fixes #2961
## 2.2.7 07/04/2020
* Fix VNC console template doesn't extract %i (Project UUID). Fixes #2960
* Fix contextual menu issues. Ref #2955
## 2.2.6 26/03/2020
* Prevent locked drawings to be deleted. Fixes https://github.com/GNS3/gns3-gui/issues/2948
* Fix issues with empty project variables. Fixes https://github.com/GNS3/gns3-gui/issues/2941
* Upgrade psutil to version 5.6.6 due to CVE-2019-18874 https://github.com/advisories/GHSA-qfc5-mcwq-26q8
* Use existing README.txt if existing when exporting portable project. Fixes https://github.com/GNS3/gns3-server/issues/1724
* Allow creation of a diskless Qemu VMs. Fixes #2939
* Re-enable "create new version" in appliance wizard. Fixes #2837
* Fix unable to load project from project library. Fixes #2932
* Fix some permission denied errors when loading remote project. Ref #2871 Fixes #2901
* Add 'Royal TS V5' to predefined console list
* Disallow invalid grid sized. Fixes #2908
* Check if hostname is blank. Fixes #2924
* Add nvme disk interface and fix scsi disk interface for Qemu VMs.
* Add latest Qemu nic models.
* Upgrade Qt version to 5.14.1. Ref #2778 #2903
## 2.2.5 09/01/2020
* Add gns3-gui.xml and update Linux icons paths & permissions. Ref #2919
## 2.2.4 08/01/2020
* Fix "Console to all nodes" doesn't open cloud objects with console configured. Fixes #2902
* Change default path for SecureCRT. Fixes #2896
* Add icons in setup.py Ref #2898
* Add remote viewer as a VNC console for Linux. Fixes #2913
## 2.2.3 12/11/2019
* Fix issue when binding on 0.0.0.0. Fixes #2892
* Allow double click on cloud with configured console to open session. Fixes #2894
* Officially support Python 3.8. Ref https://github.com/GNS3/gns3-gui/issues/2895
* Set psutil to version 5.6.3 in requirements.txt
## 2.2.2 04/11/2019
* Fix KeyError: 'spice+agent'. Fixes #2890

504
COPYING
View File

@@ -1,504 +0,0 @@
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

View File

@@ -1,22 +1,16 @@
# Run tests inside a container
FROM ubuntu:18.04
FROM ubuntu:latest
MAINTAINER GNS3 Team
#ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -y --force-yes python3.6 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3.6-dev xvfb
RUN apt-get install -y --force-yes python3 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3-dev xvfb
RUN apt-get clean
ADD dev-requirements.txt /dev-requirements.txt
ADD requirements.txt /requirements.txt
RUN pip3 install -r /dev-requirements.txt
RUN python3 -m pip install --break-system-packages --no-cache-dir -r /dev-requirements.txt
ADD . /src
WORKDIR /src
CMD xvfb-run python3.6 -m pytest -vv
CMD xvfb-run python3 -m pytest -vv

View File

@@ -1,11 +1,6 @@
include README.rst
include AUTHORS
include README.md
include LICENSE
include MANIFEST.in
include requirements.txt
include tox.ini
recursive-include tests *
include CHANGELOG
recursive-include gns3 *
recursive-include resources *
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

58
README.md Normal file
View File

@@ -0,0 +1,58 @@
GNS3-gui
========
[![GitHub Actions tests](https://github.com/GNS3/gns3-gui/workflows/testing/badge.svg)](https://github.com/GNS3/gns3-gui/actions?query=workflow%3Atesting)
[![Latest PyPi version](https://img.shields.io/pypi/v/gns3-gui.svg)](https://pypi.python.org/pypi/gns3-gui)
[![Snyk scanning](https://snyk.io/test/github/GNS3/gns3-gui/badge.svg)](https://snyk.io/test/github/GNS3/gns3-gui)
Installation
------------
Please see the documentation on our [website](https://docs.gns3.com)
Software dependencies
---------------------
PyQt6 which is either part of the Linux distribution or installable from
PyPi. The other Python dependencies are automatically installed during
the GNS3 GUI installation and are listed
[here](https://github.com/GNS3/gns3-gui/blob/3.0/requirements.txt)
For connecting to nodes using Telnet, a Telnet client is required. On
Linux that's a terminal emulator like xterm, gnome-terminal, konsole
plus the telnet program. For connecting to nodes with a GUI, a VNC
client is required, optionally a SPICE client can be used for Qemu
nodes.
For using packet captures within GNS3, Wireshark should be installed.
It's recommended, but if you don't need that functionality you can go
without it.
Development
-----------
If you want to update the interface, modify the .ui files using QT
tools. And:
``` {.bash}
cd scripts
python build_pyqt.py
```
### Debugging
If you want to see the full logs in the internal shell you can type:
``` {.bash}
debug 2
```
Or start the app with --debug flag.
Due to the fact PyQT intercept you can use a web debugger for inspecting
stuff: <https://github.com/Kozea/wdb>
Security issues
---------------
Please contact us at <security@gns3.net>

View File

@@ -1,47 +0,0 @@
GNS3-gui
========
.. image:: https://travis-ci.org/GNS3/gns3-gui.svg?branch=master
:target: https://travis-ci.org/GNS3/gns3-gui
.. image:: https://img.shields.io/pypi/v/gns3-gui.svg
:target: https://pypi.python.org/pypi/gns3-gui
GNS3 GUI repository.
Installation
------------
Please see https://docs.gns3.com/
Development
-------------
If you want to update the interface, modify the .ui files using QT tools. And:
.. code:: bash
cd scripts
python build_pyqt.py
Debug
"""""
If you want to see the full logs in the internal shell you can type:
.. code:: bash
debug 2
Or start the app with --debug flag.
Due to the fact PyQT intercept you can use a web debugger for inspecting stuff:
https://github.com/Kozea/wdb
Security issues
----------------
Please contact us using contact informations available here:
http://docs.gns3.com/1ON9JBXSeR7Nt2-Qum2o3ZX0GU86BZwlmNSUgvmqNWGY/index.html

5
SECURITY.md Normal file
View File

@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Please use GitHub's report a vulnerability feature. More information can be found in https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability

View File

@@ -1,19 +0,0 @@
version: '{build}-{branch}'
image: Visual Studio 2017
platform: x64
environment:
PYTHON: "C:\\Python36-x64"
DISTUTILS_USE_SDK: "1"
install:
- cinst nmap
- "%PYTHON%\\python.exe -m pip install -r dev-requirements.txt"
- "%PYTHON%\\python.exe -m pip install -r win-requirements.txt"
build: off
test_script:
- "%PYTHON%\\python.exe -m pytest -v"

View File

@@ -1,6 +1,2 @@
-rrequirements.txt
pep8==1.7.0
pytest==4.4.1
pytest-pythonpath==0.7.3 # useful for running tests outside tox
pytest-timeout==1.3.3
pytest==8.4.2 # version 8.4.2 is the last one supporting Python 3.9
pytest-timeout==2.4.0

View File

@@ -19,6 +19,7 @@ from .qt import QtCore
from .controller import Controller
from .local_config import LocalConfig
from .settings import GENERAL_SETTINGS
from .http_client_error import HttpClientError
import logging
@@ -49,7 +50,17 @@ class ApplianceManager(QtCore.QObject):
settings = LocalConfig.instance().loadSectionSettings("MainWindow", GENERAL_SETTINGS)
symbol_theme = settings["symbol_theme"]
if update is True:
self._controller.get("/appliances?update=yes&symbol_theme={}".format(symbol_theme), self._listAppliancesCallback, progressText="Downloading appliances from online registry...")
try:
self._controller.get(
"/appliances?update=yes&symbol_theme={}".format(symbol_theme),
self._listAppliancesCallback,
progress_text="Downloading appliances from online registry...",
wait=True,
timeout=300
)
except HttpClientError as e:
log.error(f"Error while getting appliances list: {e}")
return
else:
self._controller.get("/appliances?symbol_theme={}".format(symbol_theme), self._listAppliancesCallback)

View File

@@ -20,6 +20,7 @@ import sys
from .qt import QtWidgets, QtGui, QtCore
from gns3.utils import parse_version
from gns3.local_config import LocalConfig
from .version import __version__
import logging
@@ -27,25 +28,16 @@ log = logging.getLogger(__name__)
class Application(QtWidgets.QApplication):
file_open_signal = QtCore.pyqtSignal(str)
file_open_signal = QtCore.Signal(str)
def __init__(self, argv, hdpi=True):
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
if hdpi:
if sys.platform.startswith("linux"):
log.warning("HDPI mode is enabled. HDPI support on Linux is not fully stable and GNS3 may crash depending of your version of Linux. To disabled HDPI mode please edit ~/.config/GNS3/gns3_gui.conf and set 'hdpi' to 'false'")
self.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
else:
log.info("HDPI mode is disabled")
self.setAttribute(QtCore.Qt.AA_DisableHighDpiScaling)
super().__init__(argv)
# this is tell Wayland what is the name of the desktop file (gns3.desktop)
self.setDesktopFileName("gns3")
# this info is necessary for QSettings
self.setOrganizationName("GNS3")
self.setOrganizationDomain("gns3.net")
@@ -57,7 +49,7 @@ class Application(QtWidgets.QApplication):
self.open_file_at_startup = None
def event(self, event):
# When you double click file you receive an event
# When you double click on a file, you receive an event
# and not the file as command line parameter
if sys.platform.startswith("darwin"):
if isinstance(event, QtGui.QFileOpenEvent):

View File

@@ -37,6 +37,7 @@ class Compute:
self._password = None
self._cpu_usage_percent = None
self._memory_usage_percent = None
self._disk_usage_percent = None
self._capabilities = {"node_types": []}
self._last_error = None
@@ -202,6 +203,24 @@ class Compute:
return self._memory_usage_percent
def setDiskUsagePercent(self, usage):
"""
Sets the compute disk usage.
:returns: disk usage (integer)
"""
self._disk_usage_percent = usage
def diskUsagePercent(self):
"""
Returns the compute disk usage.
:param usage: disk usage (integer)
"""
return self._disk_usage_percent
def capabilities(self):
"""
Returns the compute capabilities

View File

@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .qt import QtCore
from .qt import QtCore, QtWidgets
from .compute import Compute
from .controller import Controller
@@ -50,11 +50,11 @@ class ComputeManager(QtCore.QObject):
# No need to refresh via an API call if we received fresh data from the notification feed
self._last_computes_refresh = datetime.datetime.now().timestamp()
self._timer = QtCore.QTimer()
self._timer.setInterval(1000)
self._refreshingComputes = False
self._timer.timeout.connect(self._refreshComputesSlot)
self._timer.start()
# self._timer = QtCore.QTimer()
# self._timer.setInterval(1000)
# self._timer.timeout.connect(self._refreshComputesSlot)
# self._timer.start()
def _refreshComputesSlot(self):
"""
@@ -66,7 +66,7 @@ class ComputeManager(QtCore.QObject):
if self._controller.connected() and datetime.datetime.now().timestamp() - self._last_computes_refresh > 1:
self._last_computes_refresh = datetime.datetime.now().timestamp()
self._refreshingComputes = True
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
self._controller.get("/computes", self._listComputesCallback, show_progress=False, timeout=30)
def _controllerConnectedSlot(self):
"""
@@ -75,11 +75,11 @@ class ComputeManager(QtCore.QObject):
if self._controller.connected():
self._refreshingComputes = True
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
self._controller.get("/computes", self._listComputesCallback, show_progress=False, timeout=30)
def _controllerDisconnectedSlot(self):
"""
Called when disconnected from a compute.
Called when disconnected from the controller.
"""
for compute_id in list(self._computes):
@@ -96,8 +96,9 @@ class ComputeManager(QtCore.QObject):
log.error("Error while getting compute list: {}".format(result["message"]))
return
for compute in result:
self.computeDataReceivedCallback(compute)
if result:
for compute in result:
self.computeDataReceivedCallback(compute)
def computeDataReceivedCallback(self, compute):
"""
@@ -113,15 +114,17 @@ class ComputeManager(QtCore.QObject):
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"])
self._computes[compute_id].setLastError(compute.get("last_error"))
if "connected" in compute:
self._computes[compute_id].setConnected(compute["connected"])
self._computes[compute_id].setCpuUsagePercent(compute["cpu_usage_percent"])
self._computes[compute_id].setMemoryUsagePercent(compute["memory_usage_percent"])
self._computes[compute_id].setDiskUsagePercent(compute["disk_usage_percent"])
self._computes[compute_id].setCapabilities(compute["capabilities"])
self._computes[compute_id].setLastError(compute.get("last_error"))
if new_node:
self.created_signal.emit(compute_id)
@@ -208,6 +211,20 @@ class ComputeManager(QtCore.QObject):
self.created_signal.emit(compute_id)
return self._computes[compute_id]
def connectToCompute(self, compute_id):
"""
Connect to a compute
"""
self._controller.post(f"/computes/{compute_id}/connect", callback=self._computeConnectCallback)
def _computeConnectCallback(self, result, error=False, **kwargs):
if error and "message" in result:
from gns3.main_window import MainWindow
parent = MainWindow.instance()
QtWidgets.QMessageBox.critical(parent, "Remote compute", result.get("message"))
def deleteCompute(self, compute_id):
"""
Deletes a compute by ID
@@ -245,10 +262,16 @@ class ComputeManager(QtCore.QObject):
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__())
params = {"connect": True}
self._controller.post("/computes", callback=self._computeCreatedCallback, body=compute.__json__(), params=params)
self._computes[compute.id()] = compute
self.created_signal.emit(compute.id())
def _computeCreatedCallback(self, result, error=False, **kwargs):
if error:
log.error(result.get("message"))
@staticmethod
def reset():
ComputeManager._instance = None

View File

@@ -16,13 +16,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Compute summary view that list all the compute, their status.
Compute summary view that list all the computes and their status.
"""
from .qt import QtGui, QtCore, QtWidgets
from .compute_manager import ComputeManager
from .topology import Topology
from .node import Node
from .utils import human_size
from .utils.get_icon import get_icon
import logging
log = logging.getLogger(__name__)
@@ -43,9 +45,16 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
self._compute = compute
self._parent = parent
self._status = "unknown"
self._refreshStatusSlot()
def getCompute(self):
return self._compute
def setCompute(self, compute):
self._compute = compute
def _refreshStatusSlot(self):
"""
Changes the icon to show the node status (started, stopped etc.)
@@ -58,14 +67,20 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
text = self._compute.name()
if self._compute.cpuUsagePercent() is not None:
text = "{} CPU {}%, RAM {}%".format(text, self._compute.cpuUsagePercent(), self._compute.memoryUsagePercent())
text = "{} CPU {}%, RAM {}%, DISK {}%".format(text,
self._compute.cpuUsagePercent(),
self._compute.memoryUsagePercent(),
self._compute.diskUsagePercent())
self.setText(0, text)
if self._compute.connected():
self._status = "connected"
self.setToolTip(0, "Server {} version {} running on {}".format(self._compute.name(),
self._compute.capabilities().get("version", "n/a"),
self._compute.capabilities().get("platform", "")))
self.setToolTip(0, "Server {} v{} running on {} (CPUs={} / RAM={} / DISK={})".format(self._compute.name(),
self._compute.capabilities().get("version", "n/a"),
self._compute.capabilities().get("platform", ""),
self._compute.capabilities().get("cpus", 0),
human_size(self._compute.capabilities().get("memory", 0)),
human_size(self._compute.capabilities().get("disk_size", 0))))
if usage is None or (self._compute.cpuUsagePercent() < 90 and self._compute.memoryUsagePercent() < 90):
self.setIcon(0, QtGui.QIcon(':/icons/led_green.svg'))
else:
@@ -82,7 +97,7 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
self._status = "stopped"
self.setToolTip(0, "{} is stopped or cannot be reached".format(self._compute.name()))
self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
self._parent.sortItems(0, QtCore.Qt.AscendingOrder)
self._parent.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder)
# add nodes belonging to this compute
self.takeChildren()
@@ -98,7 +113,7 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
else:
item.setIcon(0, QtGui.QIcon(':/icons/led_red.svg'))
self.addChild(item)
self.sortChildren(0, QtCore.Qt.AscendingOrder)
self.sortChildren(0, QtCore.Qt.SortOrder.AscendingOrder)
class ComputeSummaryView(QtWidgets.QTreeWidget):
@@ -111,13 +126,44 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
def __init__(self, parent):
super().__init__(parent)
self._computes = {}
self._compute_items = {}
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 contextMenuEvent(self, event):
"""
Handles all context menu events.
:param event: QContextMenuEvent instance
"""
self._showContextualMenu(event.globalPos())
def _showContextualMenu(self, pos):
item = self.currentItem()
if item and isinstance(item, ComputeItem):
compute = item.getCompute()
if not compute.connected():
menu = QtWidgets.QMenu()
connect_action = QtGui.QAction("Connect to server", menu)
connect_action.setIcon(get_icon("start.svg"))
connect_action.triggered.connect(lambda: ComputeManager.instance().connectToCompute(compute.id()))
menu.addAction(connect_action)
menu.exec(pos)
def _computeConnectSlot(self, compute_id):
"""
:param compute_id:
:return:
"""
ComputeManager.instance().connectToCompute(compute_id)
def _computeAddedSlot(self, compute_id):
"""
Called when a compute is added to the list of computes
@@ -128,7 +174,7 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
compute = ComputeManager.instance().getCompute(compute_id)
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
return
self._computes[compute_id] = ComputeItem(self, compute)
self._compute_items[compute_id] = ComputeItem(self, compute)
def _computeUpdatedSlot(self, compute_id):
"""
@@ -137,13 +183,14 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
:params url: URL of the compute
"""
if compute_id in self._computes:
if compute_id in self._compute_items:
compute = ComputeManager.instance().getCompute(compute_id)
# We hide the remote GNS3 VM
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
self._computeRemovedSlot(compute_id)
else:
self._computes[compute_id]._refreshStatusSlot()
self._compute_items[compute_id].setCompute(compute)
self._compute_items[compute_id]._refreshStatusSlot()
else:
self._computeAddedSlot(compute_id)
@@ -154,6 +201,6 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
: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]
if compute_id in self._compute_items:
self.takeTopLevelItem(self.indexOfTopLevelItem(self._compute_items[compute_id]))
del self._compute_items[compute_id]

View File

@@ -19,6 +19,7 @@
Handles commands typed in the GNS3 console.
"""
import os
import sys
import cmd
import struct
@@ -34,6 +35,14 @@ log = logging.getLogger(__name__)
class ConsoleCmd(cmd.Cmd):
def do_env(self, args):
"""
Show the environment variables used by GNS3.
"""
for key, val in os.environ.items():
print("{}={}".format(key, val))
def do_version(self, args):
"""
Show the version of GNS3 and its dependencies.
@@ -223,17 +232,10 @@ class ConsoleCmd(cmd.Cmd):
level = int(args[0])
if level == 0:
print("Deactivating debugging")
for handler in root.handlers:
if isinstance(handler, logging.StreamHandler):
root.removeHandler(handler)
root.setLevel(logging.INFO)
else:
root.addHandler(logging.StreamHandler(sys.stdout))
if level == 1:
print("Activating debugging")
else:
print("Activating full debugging")
root.setLevel(logging.DEBUG)
print("Activating debugging")
root.setLevel(logging.DEBUG)
from .main_window import MainWindow
MainWindow.instance().setSettings({"debug_level": level})
else:

View File

@@ -22,7 +22,7 @@ import inspect
import datetime
import platform
from .qt import QtCore
from .qt import QtCore, QtGui
from .topology import Topology
from .version import __version__
from .console_cmd import ConsoleCmd
@@ -109,6 +109,29 @@ class ConsoleView(PyCutExt, ConsoleCmd):
self.stdout = sys.stdout
self._topology = Topology.instance()
def contextMenuEvent(self, event):
"""
Handles all context menu events.
:param event: QContextMenuEvent instance
"""
menu = self.createStandardContextMenu()
delete_all_action = QtGui.QAction("Delete All", menu)
delete_all_action.triggered.connect(self._deleteAllActionSlot)
menu.addAction(delete_all_action)
menu.exec(event.globalPos())
def _deleteAllActionSlot(self):
"""
Delete all action slot
"""
self.clear()
self.write(self.prompt)
self.lines = []
self._clearLine()
def _writeMessageSlot(self, message, level):
"""
Write a message in the console.

View File

@@ -21,11 +21,14 @@ import tempfile
import json
import pathlib
from .qt import QtCore, QtNetwork, QtGui, QtWidgets, QtWebSockets, qpartial, qslot
from urllib.parse import urlparse
from .qt import QtCore, QtGui, QtWebSockets, qpartial, qslot
from .symbol import Symbol
from .local_server_config import LocalServerConfig
from .settings import LOCAL_SERVER_SETTINGS
from .local_config import LocalConfig
from .settings import CONTROLLER_SETTINGS
from gns3.utils import parse_version
from gns3.http_client import HTTPClient
import logging
log = logging.getLogger(__name__)
@@ -40,6 +43,7 @@ class Controller(QtCore.QObject):
disconnected_signal = QtCore.Signal()
connection_failed_signal = QtCore.Signal()
project_list_updated_signal = QtCore.Signal()
image_list_updated_signal = QtCore.Signal()
def __init__(self):
@@ -54,16 +58,47 @@ class Controller(QtCore.QObject):
self._error_dialog = None
self._display_error = True
self._projects = []
self._images = []
self._websocket = QtWebSockets.QWebSocket()
# If we do multiple call in order to download the same symbol we queue them
self._static_asset_download_queue = {}
self._loadSettings()
def settings(self):
"""
Returns the graphics view settings.
:returns: settings dictionary
"""
return self._settings
def _loadSettings(self):
"""
Loads the settings from the persistent settings file.
"""
self._settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, CONTROLLER_SETTINGS)
def setSettings(self, new_settings):
"""
Set new controller settings.
:param new_settings: settings dictionary
"""
# save the settings
self._settings.update(new_settings)
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
def host(self):
return self._http_client.host()
def version(self):
return self._version
def isRemote(self):
@@ -71,8 +106,7 @@ class Controller(QtCore.QObject):
:returns Boolean: True if the controller is remote
"""
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
return not settings["auto_start"]
return self._settings["remote"]
def connecting(self):
"""
@@ -102,10 +136,8 @@ class Controller(QtCore.QObject):
self._http_client = http_client
if self._http_client:
if self.isRemote():
self._http_client.setMaxTimeDifferenceBetweenQueries(120)
self._http_client.connection_connected_signal.connect(self._httpClientConnectedSlot)
self._http_client.connection_disconnected_signal.connect(self._httpClientDisconnectedSlot)
self._http_client.connected_signal.connect(self._httpClientConnectedSlot)
self._http_client.disconnected_signal.connect(self._httpClientDisconnectedSlot)
self._connectingToServer()
def getHttpClient(self):
@@ -123,6 +155,14 @@ class Controller(QtCore.QObject):
self._display_error = val
self._first_error = True
def connect(self):
"""
Connect to controller
"""
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
def _connectingToServer(self):
"""
Connection process as started
@@ -130,41 +170,16 @@ class Controller(QtCore.QObject):
self._connected = False
self._connecting = True
self.get('/version', self._versionGetSlot)
self.httpClient().connectToServer()
def _httpClientDisconnectedSlot(self):
if self._connected:
self._connected = False
self.disconnected_signal.emit()
self._connectingToServer()
self.stopListenNotifications()
def _versionGetSlot(self, result, error=False, **kwargs):
"""
Called after the initial version get
"""
if error:
if self._first_error:
self._connecting = False
self.connection_failed_signal.emit()
if "message" in result and self._display_error:
self._error_dialog = QtWidgets.QMessageBox(self.parent())
self._error_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
self._error_dialog.setWindowTitle("Connection to server")
self._error_dialog.setText("Error when connecting to the GNS3 server:\n{}".format(result["message"]))
self._error_dialog.setIcon(QtWidgets.QMessageBox.Critical)
self._error_dialog.show()
# Try to connect again in 5 seconds
QtCore.QTimer.singleShot(5000, qpartial(self.get, '/version', self._versionGetSlot, showProgress=self._first_error))
self._first_error = False
else:
self._first_error = True
if self._error_dialog:
self._error_dialog.reject()
self._error_dialog = None
self._version = result.get("version")
def _httpClientConnectedSlot(self):
if not self._connected:
@@ -175,16 +190,16 @@ class Controller(QtCore.QObject):
self._startListenNotifications()
def post(self, *args, **kwargs):
return self.createHTTPQuery("POST", *args, **kwargs)
return self.request("POST", *args, **kwargs)
def get(self, *args, **kwargs):
return self.createHTTPQuery("GET", *args, **kwargs)
return self.request("GET", *args, **kwargs)
def put(self, *args, **kwargs):
return self.createHTTPQuery("PUT", *args, **kwargs)
return self.request("PUT", *args, **kwargs)
def delete(self, *args, **kwargs):
return self.createHTTPQuery("DELETE", *args, **kwargs)
return self.request("DELETE", *args, **kwargs)
def getCompute(self, path, compute_id, *args, **kwargs):
"""
@@ -220,31 +235,13 @@ class Controller(QtCore.QObject):
return compute_id
return compute_id
def getEndpoint(self, path, compute_id, *args, **kwargs):
def request(self, method, path, *args, **kwargs):
"""
API post on a specific compute
"""
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/endpoint/{}{}".format(compute_id, path)
return self.get(path, *args, **kwargs)
def putCompute(self, path, compute_id, *args, **kwargs):
"""
API put on a specific compute
"""
compute_id = self.__fix_compute_id(compute_id)
path = "/computes/{}{}".format(compute_id, path)
return self.put(path, *args, **kwargs)
def createHTTPQuery(self, method, path, *args, **kwargs):
"""
Forward the query to the HTTP client or controller depending of the path
Forward the query to the HTTP client or controller depending on the path
"""
if self._http_client:
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
return self._http_client.sendRequest(method, path, *args, **kwargs)
@staticmethod
def instance():
@@ -277,9 +274,10 @@ class Controller(QtCore.QObject):
self._static_asset_download_queue[path].append((callback, fallback, ))
else:
self._static_asset_download_queue[path] = [(callback, fallback, )]
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, url, path))
self._http_client.sendRequest("GET", url, qpartial(self._getStaticCallback, url, path), raw=True)
def _getStaticCallback(self, url, path, result, error=False, **kwargs):
def _getStaticCallback(self, url, path, result, error=False, raw_body=None, **kwargs):
if path not in self._static_asset_download_queue:
return
@@ -295,7 +293,7 @@ class Controller(QtCore.QObject):
return
try:
with open(path, "wb+") as f:
f.write(raw_body)
f.write(result)
except OSError as e:
log.error("Can't write to {}: {}".format(path, str(e)))
return
@@ -359,9 +357,14 @@ class Controller(QtCore.QObject):
def uploadSymbol(self, symbol_id, path):
self.post("/symbols/" + symbol_id + "/raw",
qpartial(self._finishSymbolUpload, path),
body=pathlib.Path(path), progressText="Uploading {}".format(symbol_id), timeout=None)
self.post(
"/symbols/" + symbol_id + "/raw",
qpartial(self._finishSymbolUpload, path),
body=pathlib.Path(path),
progress_text="Uploading {}".format(symbol_id),
timeout=None,
wait=True
)
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
@@ -390,6 +393,17 @@ class Controller(QtCore.QObject):
if callback:
callback(result, error=error, **kwargs)
def createDiskImage(self, disk_name, options, callback):
"""
Create a disk image on the controller
:param callback: callback for the reply from the server
"""
self.post(f"/images/qemu/{disk_name}", callback, body=options)
@qslot
def refreshProjectList(self, *args):
self.get("/projects", self._projectListCallback)
@@ -402,27 +416,50 @@ class Controller(QtCore.QObject):
def projects(self):
return self._projects
@qslot
def refreshImageList(self, *args):
self.get("/images", self._imageListCallback)
def _imageListCallback(self, result, error=False, **kwargs):
if not error:
self._images = result
self.image_list_updated_signal.emit()
def images(self):
return self._images
def _startListenNotifications(self):
if not self.connected():
return
# Due to bug in Qt on some version we need a dedicated network manager
self._notification_network_manager = QtNetwork.QNetworkAccessManager()
self._notification_stream = None
# Qt websocket before Qt 5.6 doesn't support auth
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.6.0") or parse_version(QtCore.PYQT_VERSION_STR) < parse_version("5.6.0"):
self._notification_stream = Controller.instance().createHTTPQuery("GET", "/notifications", self._endListenNotificationCallback,
downloadProgressCallback=self._event_received,
networkManager=self._notification_network_manager,
timeout=None,
showProgress=False,
ignoreErrors=True)
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.6.0") or parse_version(QtCore.PYQT_VERSION_STR) < parse_version("5.6.0") or LocalConfig.instance().experimental():
self._notification_stream = Controller.instance().request(
"GET",
"/notifications",
self._endListenNotificationCallback,
download_progress_callback=self._event_received,
timeout=None,
show_progress=False
)
url = urlparse(self._http_client.url() + '/notifications')
log.info(f"Listening for controller notifications on {url.scheme}://{url.netloc}{url.path}")
else:
self._notification_stream = self._http_client.connectWebSocket(self._websocket, "/notifications/ws")
self._notification_stream.textMessageReceived.connect(self._websocket_event_received)
self._notification_stream.error.connect(self._websocket_error)
self._notification_stream.sslErrors.connect(self._sslErrorsSlot)
self._notification_stream.disconnected.connect(self._websocket_disconnected)
url = urlparse(self._notification_stream.requestUrl().toString())
log.info(f"Listening for controller notifications on {url.scheme}://{url.netloc}{url.path}")
def _websocket_disconnected(self):
self._connected = False
self.disconnected_signal.emit()
self.stopListenNotifications()
def stopListenNotifications(self):
if self._notification_stream:
@@ -430,7 +467,6 @@ class Controller(QtCore.QObject):
stream = self._notification_stream
self._notification_stream = None
stream.abort()
self._notification_network_manager = None
def _endListenNotificationCallback(self, result, error=False, **kwargs):
"""
@@ -442,10 +478,15 @@ class Controller(QtCore.QObject):
@qslot
def _websocket_error(self, error):
if self._notification_stream:
log.error("Websocket notification stream error: {}".format(self._notification_stream.errorString()))
self._notification_stream = None
self._startListenNotifications()
log.error("Websocket controller notification stream error: {}".format(self._notification_stream.errorString()))
self.stopListenNotifications()
@qslot
def _sslErrorsSlot(self, ssl_errors):
self._http_client.handleSslError(self._notification_stream, ssl_errors)
@qslot
def _websocket_event_received(self, event):
@@ -468,11 +509,21 @@ class Controller(QtCore.QObject):
elif result["action"] == "compute.created" or result["action"] == "compute.updated":
from .compute_manager import ComputeManager
ComputeManager.instance().computeDataReceivedCallback(result["event"])
elif result["action"] == "log.error":
log.error(result["event"]["message"])
elif result["action"] == "log.warning":
log.warning(result["event"]["message"])
elif result["action"] == "log.info":
log.info(result["event"]["message"], extra={"show": True})
elif result["action"] == "project.closed":
from .topology import Topology
project = Topology.instance().project()
if project and project.id() == result["event"]["project_id"]:
Topology.instance().setProject(None)
elif result["action"] == "project.updated":
from .topology import Topology
project = Topology.instance().project()
if project and project.id() == result["event"]["project_id"]:
project.projectUpdatedCallback(result["event"])
elif result["action"] == "log.error" and result["event"].get("message"):
log.error(result["event"].get("message"))
elif result["action"] == "log.warning" and result["event"].get("message"):
log.warning(result["event"].get("message"))
elif result["action"] == "log.info" and result["event"].get("message"):
log.info(result["event"].get("message"), extra={"show": True})
elif result["action"] == "ping":
pass

View File

@@ -16,21 +16,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import psutil
import os
import platform
import struct
import distro
try:
import raven
from raven.transport.http import HTTPTransport
RAVEN_AVAILABLE = True
import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
SENTRY_SDK_AVAILABLE = True
except ImportError:
# raven is not installed with deb package in order to simplify packaging
RAVEN_AVAILABLE = False
# Sentry SDK is not installed with deb package in order to simplify packaging
SENTRY_SDK_AVAILABLE = False
from .utils.get_resource import get_resource
from .version import __version__, __version_info__
import logging
@@ -52,66 +50,61 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "https://dbedb95015d948b3b38917d7ac01e15b:1fcb50016b474c12b14c53d2b83eeabc@sentry.io/38506"
if hasattr(sys, "frozen"):
cacert = get_resource("cacert.pem")
if cacert is not None and os.path.isfile(cacert):
DSN += "?ca_certs={}".format(cacert)
else:
log.warning("The SSL certificate bundle file '{}' could not be found".format(cacert))
DSN = "https://3385f7a13ef84851fef5de4aef7d25dd@o19455.ingest.us.sentry.io/38506"
_instance = None
def __init__(self):
# We don't want sentry making noise if an error is catched when you don't have internet
# We don't want sentry making noise if an error is caught when we don't have internet
sentry_errors = logging.getLogger('sentry.errors')
sentry_errors.disabled = True
sentry_uncaught = logging.getLogger('sentry.errors.uncaught')
sentry_uncaught.disabled = True
self._sentry_initialized = False
def captureException(self, exception, value, tb):
from .local_server import LocalServer
from .local_config import LocalConfig
from .controller import Controller
from .compute_manager import ComputeManager
local_server = LocalServer.instance().localServerSettings()
if local_server["report_errors"]:
if not RAVEN_AVAILABLE:
if SENTRY_SDK_AVAILABLE:
# Don't send log records as events.
sentry_logging = LoggingIntegration(level=logging.INFO, event_level=None)
try:
sentry_sdk.init(dsn=CrashReport.DSN,
release=__version__,
default_integrations=False,
integrations=[sentry_logging])
except Exception as e:
log.error("Crash report could not be sent: {}".format(e))
return
if os.path.exists(LocalConfig.instance().runAsRootPath()):
log.warning("User has run application as root. Crash reports are disabled.")
sys.exit(1)
return
if os.path.exists(".git"):
log.warning("A .git directory exist crash report is turn off for developers. Instant exit")
sys.exit(1)
return
if hasattr(exception, "fingerprint"):
client = raven.Client(CrashReport.DSN, release=__version__, fingerprint=['{{ default }}', exception.fingerprint], transport=HTTPTransport)
else:
client = raven.Client(CrashReport.DSN, release=__version__, transport=HTTPTransport)
context = {
tags = {
"os:name": platform.system(),
"os:release": platform.release(),
"os:win_32": " ".join(platform.win32_ver()),
"os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]),
"os:linux": " ".join(distro.linux_distribution()),
"os:linux": distro.name(pretty=True),
}
self._add_qt_information(tags)
with sentry_sdk.configure_scope() as scope:
for key, value in tags.items():
scope.set_tag(key, value)
extra_context = {
"python:version": "{}.{}.{}".format(sys.version_info[0],
sys.version_info[1],
sys.version_info[2]),
"python:bit": struct.calcsize("P") * 8,
"python:encoding": sys.getdefaultencoding(),
"python:frozen": "{}".format(hasattr(sys, "frozen")),
"python:frozen": "{}".format(hasattr(sys, "frozen"))
}
# extra controller and compute information
extra_context = {"controller:version": Controller.instance().version(),
"controller:host": Controller.instance().host(),
"controller:connected": Controller.instance().connected()}
from .controller import Controller
from .compute_manager import ComputeManager
extra_context["controller:version"] = Controller.instance().version()
extra_context["controller:host"] = Controller.instance().host()
extra_context["controller:connected"] = Controller.instance().connected()
for index, compute in enumerate(ComputeManager.instance().computes()):
extra_context["compute{}:id".format(index)] = compute.id()
extra_context["compute{}:name".format(index)] = compute.name(),
@@ -120,27 +113,48 @@ class CrashReport:
extra_context["compute{}:platform".format(index)] = compute.capabilities().get("platform")
extra_context["compute{}:version".format(index)] = compute.capabilities().get("version")
context = self._add_qt_information(context)
client.tags_context(context)
client.extra_context(extra_context)
try:
report = client.captureException((exception, value, tb))
except Exception as e:
log.error("Can't send crash report to Sentry: {}".format(e))
return
log.debug("Crash report sent with event ID: {}".format(client.get_ident(report)))
with sentry_sdk.configure_scope() as scope:
for key, value in extra_context.items():
scope.set_extra(key, value)
def captureException(self, exception, value, tb):
from .local_server import LocalServer
from .local_config import LocalConfig
local_server = LocalServer.instance().localServerSettings()
if local_server["report_errors"]:
if not SENTRY_SDK_AVAILABLE:
log.warning("Cannot capture exception: Sentry SDK is not available")
return
if os.path.exists(LocalConfig.instance().runAsRootPath()):
log.warning("User is running application as root. Crash reports disabled.")
return
if not hasattr(sys, "frozen") and os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")):
log.warning(".git directory detected, crash reporting is turned off for developers.")
return
try:
error = (exception, value, tb)
sentry_sdk.capture_exception(error=error)
log.info("Crash report sent with event ID: {}".format(sentry_sdk.last_event_id()))
except Exception as e:
log.warning("Can't send crash report to Sentry: {}".format(e))
def _add_qt_information(self, tags):
def _add_qt_information(self, context):
try:
from .qt import QtCore
from .qt import sip
except ImportError:
return context
context["psutil:version"] = psutil.__version__
context["pyqt:version"] = QtCore.PYQT_VERSION_STR
context["qt:version"] = QtCore.QT_VERSION_STR
context["sip:version"] = sip.SIP_VERSION_STR
return context
return tags
tags["pyqt:version"] = QtCore.PYQT_VERSION_STR
tags["qt:version"] = QtCore.QT_VERSION_STR
tags["sip:version"] = sip.SIP_VERSION_STR
return tags
@classmethod
def instance(cls):

View File

@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
from ..qt import sip
import shutil
@@ -29,7 +30,7 @@ from ..registry.registry import Registry
from ..registry.config import Config
from ..registry.appliance_to_template import ApplianceToTemplate
from ..registry.image import Image
from ..utils import human_filesize
from ..utils import human_size
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
from ..utils.progress_dialog import ProgressDialog
from ..compute_manager import ComputeManager
@@ -52,7 +53,6 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.setupUi(self)
self._refreshing = False
self._server_check = False
self._template_created = False
self._path = path
@@ -72,19 +72,22 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.uiCreateVersionPushButton.clicked.connect(self._createVersionPushButtonClickedSlot)
self.allowCustomFiles.clicked.connect(self._allowCustomFilesChangedSlot)
#FIXME: deactivate the create version feature (confusing and maybe not necessary, TBD)
self.uiCreateVersionPushButton.hide()
# directories where to search for images
images_directories = list()
# add the current directory
if hasattr(sys, "frozen"):
images_directories.append(os.path.dirname(os.path.abspath(sys.executable)))
else:
images_directories.append(os.path.abspath(os.curdir))
for emulator in ("QEMU", "IOU", "DYNAMIPS"):
emulator_images_dir = ImageManager.instance().getDirectoryForType(emulator)
if os.path.exists(emulator_images_dir):
images_directories.append(emulator_images_dir)
images_directories.append(os.path.dirname(self._path))
download_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
download_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DownloadLocation)
if download_directory != "" and download_directory != os.path.dirname(self._path):
images_directories.append(download_directory)
@@ -97,15 +100,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.setWindowTitle("Install {} appliance".format(self._appliance["name"]))
# add a custom button to show appliance information
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Appliance info")
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
self.customButtonClicked.connect(self._showApplianceInfoSlot)
if self._appliance["registry_version"] < 8:
# FIXME: show appliance info for v8
self.setButtonText(QtWidgets.QWizard.WizardButton.CustomButton1, "&Appliance info")
self.setOption(QtWidgets.QWizard.WizardOption.HaveCustomButton1, True)
self.customButtonClicked.connect(self._showApplianceInfoSlot)
# customize the server selection
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
if hasattr(self, "uiVMRadioButton"):
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
if Controller.instance().isRemote():
self.uiLocalRadioButton.setText("Install the appliance on the main server")
@@ -139,29 +141,19 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
# add symbol
if self._appliance["category"] == "guest":
symbol = ":/symbols/computer.svg"
if self._appliance.template_type() == "docker":
symbol = ":/symbols/docker_guest.svg"
elif self._appliance.template_type() == "qemu":
symbol = ":/symbols/qemu_guest.svg"
else:
symbol = ":/symbols/computer.svg"
else:
symbol = ":/symbols/{}.svg".format(self._appliance["category"])
self.page(page_id).setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(symbol))
self.page(page_id).setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(symbol))
if self.page(page_id) == self.uiServerWizardPage:
Controller.instance().getSymbols(self._getSymbolsCallback)
if "qemu" in self._appliance:
emulator_type = "qemu"
elif "iou" in self._appliance:
emulator_type = "iou"
elif "docker" in self._appliance:
emulator_type = "docker"
elif "dynamips" in self._appliance:
emulator_type = "dynamips"
else:
QtWidgets.QMessageBox.warning(self, "Appliance", "Could not determine the emulator type")
is_mac = ComputeManager.instance().localPlatform().startswith("darwin")
is_win = ComputeManager.instance().localPlatform().startswith("win")
self.uiRemoteServersComboBox.clear()
if len(ComputeManager.instance().remoteComputes()) == 0:
self.uiRemoteRadioButton.setEnabled(False)
@@ -170,66 +162,47 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
for compute in ComputeManager.instance().remoteComputes():
self.uiRemoteServersComboBox.addItem(compute.name(), compute)
if not ComputeManager.instance().vmCompute():
self.uiVMRadioButton.setEnabled(False)
#if ComputeManager.instance().localPlatform() is None:
# self.uiLocalRadioButton.setEnabled(False)
if ComputeManager.instance().localPlatform() is None:
self.uiLocalRadioButton.setEnabled(False)
elif is_mac or is_win:
if emulator_type == "qemu":
# disallow usage of the local server because Qemu has issues on OSX and Windows
if not LocalConfig.instance().experimental():
self.uiLocalRadioButton.setEnabled(False)
elif emulator_type != "dynamips":
self.uiLocalRadioButton.setEnabled(False)
if ComputeManager.instance().vmCompute():
self.uiVMRadioButton.setChecked(True)
elif ComputeManager.instance().localCompute() and self.uiLocalRadioButton.isEnabled():
if ComputeManager.instance().localCompute() and self.uiLocalRadioButton.isEnabled():
self.uiLocalRadioButton.setChecked(True)
elif self.uiRemoteRadioButton.isEnabled():
self.uiRemoteRadioButton.setChecked(True)
else:
self.uiRemoteRadioButton.setChecked(False)
if is_mac or is_win:
if not self.uiRemoteRadioButton.isEnabled() and not self.uiVMRadioButton.isEnabled() and not self.uiLocalRadioButton.isEnabled():
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "The GNS3 VM is not available, please configure the GNS3 VM before adding a new appliance.")
elif self.page(page_id) == self.uiFilesWizardPage:
if Controller.instance().isRemote() or self._compute_id != "local":
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
self._registry.getRemoteImageList()
else:
self.images_changed_signal.emit()
elif self.page(page_id) == self.uiQemuWizardPage:
if self._appliance['qemu'].get('kvm', 'require') == 'require':
self._server_check = False
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
else:
self._server_check = True
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
elif self.page(page_id) == self.uiInstructionsPage:
installation_instructions = self._appliance.get("installation_instructions", "No installation instructions available")
self.uiInstructionsTextEdit.setText(installation_instructions.strip())
elif self.page(page_id) == self.uiUsageWizardPage:
self.uiUsageTextEdit.setText("The template will be available in the {} category.\n\n{}".format(self._appliance["category"].replace("_", " "), self._appliance.get("usage", "")))
# TODO: allow taking these info fields at the version level in v8
category = self._appliance["category"].replace("_", " ")
usage = self._appliance.get("usage", "No usage information available")
if self._appliance["registry_version"] >= 8:
default_username = self._appliance.get("default_username")
default_password = self._appliance.get("default_password")
if default_username and default_password:
usage += "\n\nDefault username: {}\nDefault password: {}".format(default_username, default_password)
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
"""
Check if the server supports KVM or not
"""
usage_info = """
The template will be available in the {} category.
Usage: {}
""".format(category, usage)
if error is None and "kvm" in result and self._appliance["qemu"]["arch"] in result["kvm"]:
self._server_check = True
else:
if error:
msg = result["message"]
else:
msg = "The selected server does not support KVM. A Linux server or the GNS3 VM running in VMware is required."
QtWidgets.QMessageBox.critical(self, "KVM support", msg)
self._server_check = False
self.uiUsageTextEdit.setText(usage_info.strip())
def _uiServerWizardPage_isComplete(self):
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
return self.uiRemoteRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
def _imageUploadedCallback(self, result, error=False, context=None, **kwargs):
if context is None:
@@ -239,7 +212,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
else:
log.info("Image '{}' has been successfully uploaded".format(image_path))
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
self._registry.getRemoteImageList()
def _showApplianceInfoSlot(self):
"""
@@ -309,10 +282,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
msgbox = QtWidgets.QMessageBox(self)
msgbox.setWindowTitle("Appliance information")
msgbox.setStyleSheet("QLabel{min-width: 600px;}") # TODO: resize details box QTextEdit{min-height: 500px;}
msgbox.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
msgbox.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
msgbox.setText(text_info)
msgbox.setDetailedText(self._appliance["description"])
msgbox.exec_()
msgbox.exec()
@qslot
def _refreshVersions(self, *args):
@@ -350,18 +323,18 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
size += image.get("filesize", 0)
image_widget = QtWidgets.QTreeWidgetItem([image["filename"],
human_filesize(image.get("filesize", 0)),
human_size(image.get("filesize", 0)),
image["status"]])
if image["status"] == "Missing":
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("red")))
else:
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
image_widget.setToolTip(2, image["path"])
image_widget.setToolTip(0, f'{image["status"]} with path: {image["path"]}')
# 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)
image_widget.setData(0, QtCore.Qt.ItemDataRole.UserRole, version)
image_widget.setData(1, QtCore.Qt.ItemDataRole.UserRole, image)
image_widget.setData(2, QtCore.Qt.ItemDataRole.UserRole, self._appliance)
top.addChild(image_widget)
font = top.font(0)
@@ -375,10 +348,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
expand = False
top.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
top.setData(1, QtCore.Qt.DisplayRole, human_filesize(size))
top.setData(2, QtCore.Qt.DisplayRole, status)
top.setData(0, QtCore.Qt.UserRole, version)
top.setData(2, QtCore.Qt.UserRole, self._appliance)
top.setData(1, QtCore.Qt.ItemDataRole.DisplayRole, human_size(size))
top.setData(2, QtCore.Qt.ItemDataRole.DisplayRole, status)
top.setData(0, QtCore.Qt.ItemDataRole.UserRole, version)
top.setData(2, QtCore.Qt.ItemDataRole.UserRole, self._appliance)
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
if expand:
top.setExpanded(True)
@@ -410,11 +383,13 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
for version in self._appliance["versions"]:
for image in version["images"].values():
img = self._registry.search_image_file(self._appliance.emulator(),
image["filename"],
image.get("md5sum"),
image.get("filesize"),
strict_md5_check=not self.allowCustomFiles.isChecked())
img = self._registry.search_image_file(
self._appliance.template_type(),
image["filename"],
image.get("md5sum"),
image.get("filesize"),
strict_md5_check=not self.allowCustomFiles.isChecked()
)
if img:
if img.location == "local":
image["status"] = "Found locally"
@@ -441,7 +416,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if current is None or sip.isdeleted(current):
return
image = current.data(1, QtCore.Qt.UserRole)
image = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
if image is not None:
if "direct_download_url" in image or "download_url" in image:
self.uiDownloadPushButton.show()
@@ -462,7 +437,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if current is None or sip.isdeleted(current):
return
data = current.data(1, QtCore.Qt.UserRole)
data = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
if data is not None:
if "direct_download_url" in data:
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["direct_download_url"]))
@@ -478,8 +453,24 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
Allow user to create a new version of an appliance
"""
new_version, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Creating a new version allows to import unknown files to use with this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.Normal)
current = self.uiApplianceVersionTreeWidget.currentItem()
if current is None:
QtWidgets.QMessageBox.critical(self.parent(), "Base version", "Please select a base version")
return
base_version = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
new_version_name, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Create a new version for this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.EchoMode.Normal, base_version.get("name"))
if ok:
new_version = {"name": new_version_name}
new_version["images"] = {}
for disk_type in base_version["images"]:
base_filename = base_version["images"][disk_type]["filename"]
filename, ok = QtWidgets.QInputDialog.getText(self, "Image", "Disk image filename for {}".format(disk_type), QtWidgets.QLineEdit.EchoMode.Normal, base_filename)
if not ok:
filename = base_filename
new_version["images"][disk_type] = {"filename": filename, "version": new_version_name}
try:
self._appliance.create_new_version(new_version)
except ApplianceError as e:
@@ -500,50 +491,42 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
current = self.uiApplianceVersionTreeWidget.currentItem()
if not current:
return
disk = current.data(1, QtCore.Qt.UserRole)
disk = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
path, _ = QtWidgets.QFileDialog.getOpenFileName()
if len(path) == 0:
return
image = Image(self._appliance.emulator(), path, filename=disk["filename"])
image = Image(self._appliance.template_type(), path, filename=disk["filename"])
try:
if "md5sum" in disk and image.md5sum != disk["md5sum"]:
reply = QtWidgets.QMessageBox.question(self, "Add appliance",
"This is not the correct file. The MD5 sum is {} and should be {}.\nDo you want to accept it at your own risks?".format(image.md5sum, disk["md5sum"]),
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
reply = QtWidgets.QMessageBox.question(
self,
"Add appliance",
"This is not the correct file.\n\n"
"MD5 checksum\n"
f"actual:\t{image.md5sum}\n"
f"expected:\t{disk['md5sum']}\n\n"
"File size\n"
f"actual:\t{image.filesize} bytes\n"
f"expected:\t{disk['filesize']} bytes\n\n"
"Do you want to accept it at your own risks?",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
except OSError as e:
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
return
image_upload_manger = ImageUploadManager(image, Controller.instance(), self._compute_id, self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger = ImageUploadManager(image, Controller.instance(), self.parent())
image_upload_manger.upload()
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"]))
# refresh the images list
if Controller.instance().isRemote() or self._compute_id != "local":
self._registry.getRemoteImageList()
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()
else:
i = self.uiQemuListComboBox.findText(self._appliance["qemu"]["arch"], QtCore.Qt.MatchContains)
if i != -1:
self.uiQemuListComboBox.setCurrentIndex(i)
self.images_changed_signal.emit()
def _install(self, version):
"""
@@ -554,8 +537,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if version is None:
appliance_configuration = self._appliance.copy()
if "docker" not in appliance_configuration:
# only Docker do not have version
if self._appliance.template_type() != "docker":
# only Docker do not have versions
return False
else:
try:
@@ -567,33 +550,15 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
template_manager = TemplateManager().instance()
while len(appliance_configuration["name"]) == 0 or not template_manager.is_name_available(appliance_configuration["name"]):
QtWidgets.QMessageBox.warning(self.parent(), "Add template", "The name \"{}\" is already used by another template".format(appliance_configuration["name"]))
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add template", "New name:", QtWidgets.QLineEdit.Normal, appliance_configuration["name"])
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add template", "New name:", QtWidgets.QLineEdit.EchoMode.Normal, appliance_configuration["name"])
if not ok:
return False
appliance_configuration["name"] = appliance_configuration["name"].strip()
if "qemu" in appliance_configuration:
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, self._symbols, parent=self)
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, version, self._symbols, parent=self)
TemplateManager.instance().createTemplate(Template(new_template), callback=self._templateCreatedCallback)
return False
#worker = WaitForLambdaWorker(lambda: self._create_template(appliance_configuration, self._compute_id), allowed_exceptions=[ConfigException, OSError])
#progress_dialog = ProgressDialog(worker, "Add template", "Installing a new template...", None, busy=True, parent=self)
#progress_dialog.show()
#if progress_dialog.exec_():
# QtWidgets.QMessageBox.information(self.parent(), "Add template", "{} template has been installed!".format(appliance_configuration["name"]))
# return True
#return False
# worker = WaitForLambdaWorker(lambda: config.save(), allowed_exceptions=[ConfigException, OSError])
# progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
# progress_dialog.show()
# if progress_dialog.exec_():
# QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
# return True
def _templateCreatedCallback(self, result, error=False, **kwargs):
if error is True:
@@ -613,34 +578,25 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
appliance_configuration = self._appliance.search_images_for_version(version)
except ApplianceError as e:
QtWidgets.QMessageBox.critical(self, "Appliance","Cannot install {} version {}: {}".format(name, version, e))
return
return False
for image in appliance_configuration["images"]:
if image["location"] == "local":
if not Controller.instance().isRemote() and self._compute_id == "local" and image["path"].startswith(ImageManager.instance().getDirectory()):
log.debug("{} is already on the local server".format(image["path"]))
return
image = Image(self._appliance.emulator(), image["path"], filename=image["filename"])
image_upload_manager = ImageUploadManager(image, Controller.instance(), self._compute_id, self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manager.upload()
self._image_uploading_count += 1
def _applianceImageUploadedCallback(self, result, error=False, context=None, **kwargs):
if context is None:
context = {}
image_path = context.get("image_path", "unknown")
if error:
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
else:
log.info("Image '{}' has been successfully uploaded".format(image_path))
self._image_uploading_count -= 1
return True
image = Image(self._appliance.template_type(), image["path"], filename=image["filename"])
image_upload_manager = ImageUploadManager(image, Controller.instance(), self.parent())
if not image_upload_manager.upload():
return False
return True
def nextId(self):
if self.currentPage() == self.uiServerWizardPage:
if "docker" in self._appliance:
if self._appliance.template_type() == "docker":
# skip Qemu binary selection and files pages if this is a Docker appliance
return super().nextId() + 2
elif "qemu" not in self._appliance:
# skip the Qemu binary selection page if not a Qemu appliance
elif not self._appliance.get("installation_instructions"):
# skip the installation instructions page if there are no instructions
return super().nextId() + 1
return super().nextId()
@@ -657,21 +613,21 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
current = self.uiApplianceVersionTreeWidget.currentItem()
if current is None or sip.isdeleted(current):
return False
version = current.data(0, QtCore.Qt.UserRole)
version = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
if version is None:
return False
appliance = current.data(2, QtCore.Qt.UserRole)
appliance = current.data(2, QtCore.Qt.ItemDataRole.UserRole)
try:
self._appliance.search_images_for_version(version["name"])
except ApplianceError as e:
QtWidgets.QMessageBox.critical(self, "Appliance", "Cannot install {} version {}: {}".format(appliance["name"], version["name"], e))
return False
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Would you like to install {} version {}?".format(appliance["name"], version["name"]),
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return False
self._uploadImages(appliance["name"], version["name"])
return self._uploadImages(appliance["name"], version["name"])
elif self.currentPage() == self.uiUsageWizardPage:
# validate the usage page
@@ -683,7 +639,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
return False
current = self.uiApplianceVersionTreeWidget.currentItem()
if current:
version = current.data(0, QtCore.Qt.UserRole)
version = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
return self._install(version["name"])
else:
return self._install(None)
@@ -696,40 +652,17 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote servers configured in your preferences")
return False
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
self._compute_id = "vm"
else:
if ComputeManager.instance().localPlatform():
if (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
if "qemu" in self._appliance:
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and macOS is not supported by the GNS3 team. Do you want to continue?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and macOS is not supported by the GNS3 team. Do you want to continue?", QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return False
self._compute_id = "local"
elif self.currentPage() == self.uiQemuWizardPage:
# validate the Qemu
if self._server_check is False:
QtWidgets.QMessageBox.critical(self, "Checking for KVM support", "Please wait for the server to reply...")
return False
if self.uiQemuListComboBox.currentIndex() == -1:
QtWidgets.QMessageBox.critical(self, "Qemu binary", "No compatible Qemu binary selected")
return False
return True
@qslot
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()
@qslot
def _remoteServerToggledSlot(self, checked):
"""
@@ -765,8 +698,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
reply = QtWidgets.QMessageBox.question(self, "Custom files",
"This option allows files with different MD5 checksums. This feature is only for advanced users and can lead "
"to unexpected problems. Do you want to proceed?",
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.No:
if reply == QtWidgets.QMessageBox.StandardButton.No:
self.allowCustomFiles.setChecked(False)
return False

View File

@@ -24,15 +24,15 @@ log = logging.getLogger(__name__)
class CaptureDialog(QtWidgets.QDialog, Ui_CaptureDialog):
"""
This dialog allow configure the packet capture
This dialog allow to 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)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).clicked.connect(self._okButtonClickedSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Cancel).clicked.connect(self.reject)
if ethernet_link:
self.uiDataLinkTypeComboBox.addItem("Ethernet", "DLT_EN10MB")
@@ -70,6 +70,6 @@ if __name__ == '__main__':
main = QtWidgets.QMainWindow()
dialog = CaptureDialog(main, "test.pcap")
dialog.show()
exit_code = app.exec_()
exit_code = app.exec()
print(dialog.dataLink())
print(dialog.fileName())

View File

@@ -49,7 +49,6 @@ class ConfigurationDialog(QtWidgets.QDialog, Ui_configurationDialog):
self._settings = settings
self._configuration_page = configuration_page
@property
def settings(self):
return self._settings
@@ -60,7 +59,7 @@ class ConfigurationDialog(QtWidgets.QDialog, Ui_configurationDialog):
:param button: button that was clicked (QAbstractButton)
"""
if button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
if button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Cancel):
QtWidgets.QDialog.reject(self)
else:
try:

View File

@@ -93,7 +93,7 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
"""
Save a custom command to the list
"""
name, ok = QtWidgets.QInputDialog.getText(self, "Add a command", "Command name:", QtWidgets.QLineEdit.Normal)
name, ok = QtWidgets.QInputDialog.getText(self, "Add a command", "Command name:", QtWidgets.QLineEdit.EchoMode.Normal)
command = self.uiCommandPlainTextEdit.toPlainText().strip()
if ok and len(command) > 0:
if command not in self._consoles.values():
@@ -123,7 +123,7 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
def getCommand(parent, console_type="telnet", current=None):
dialog = ConsoleCommandDialog(parent, console_type=console_type, current=current)
dialog.show()
if dialog.exec_():
if dialog.exec():
return True, dialog.uiCommandPlainTextEdit.toPlainText().replace("\n", " ")
return False, None

View File

@@ -67,7 +67,7 @@ class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConf
self._custom_adapters = custom_adapters
self._base_mac_address = base_mac_address
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).clicked.connect(self._resetSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Reset).clicked.connect(self._resetSlot)
if self._default_adapter_type and self._adapter_types:
self.uiAdaptersTreeWidget.setColumnCount(3)
@@ -115,10 +115,10 @@ class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConf
adapter_number = 0
for port_name in self._ports:
item = TreeWidgetItem(self.uiAdaptersTreeWidget)
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
item.setFlags(item.flags() | QtCore.Qt.ItemFlag.ItemIsEditable)
item.setText(0, "Adapter {}".format(adapter_number))
item.setData(0, QtCore.Qt.UserRole, adapter_number)
item.setData(1, QtCore.Qt.UserRole, port_name)
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, adapter_number)
item.setData(1, QtCore.Qt.ItemDataRole.UserRole, port_name)
custom_adapter = self._getCustomAdapterSettings(adapter_number)
item.setText(1, custom_adapter.get("port_name", port_name))
@@ -131,7 +131,7 @@ class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConf
index = 0
for adapter_type, adapter_description in self._adapter_types.items():
combobox.addItem("{}".format(adapter_type))
combobox.setItemData(index, adapter_description, QtCore.Qt.ToolTipRole)
combobox.setItemData(index, adapter_description, QtCore.Qt.ItemDataRole.ToolTipRole)
index += 1
adapter_type_index = combobox.findText(custom_adapter.get("adapter_type", self._default_adapter_type))
combobox.setCurrentIndex(adapter_type_index)
@@ -147,7 +147,7 @@ class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConf
adapter_number += 1
self.uiAdaptersTreeWidget.setItemDelegateForColumn(0, NoEditDelegate(self))
self.uiAdaptersTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiAdaptersTreeWidget.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
self.uiAdaptersTreeWidget.setSortingEnabled(True)
for column in range(self.uiAdaptersTreeWidget.columnCount()):
@@ -166,9 +166,12 @@ class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConf
custom_adapter_settings = {}
item = self.uiAdaptersTreeWidget.topLevelItem(row)
port_name = item.text(1)
adapter_number = item.data(0, QtCore.Qt.UserRole)
adapter_number = item.data(0, QtCore.Qt.ItemDataRole.UserRole)
custom_adapter_settings["adapter_number"] = adapter_number
original_port_name = item.data(1, QtCore.Qt.UserRole)
original_port_name = item.data(1, QtCore.Qt.ItemDataRole.UserRole)
if not port_name:
QtWidgets.QMessageBox.critical(self, "Port name", "Port name cannot be empty for adapter {}".format(adapter_number))
return False
if original_port_name != port_name:
custom_adapter_settings["port_name"] = port_name
if self._default_adapter_type and self._adapter_types:
@@ -180,13 +183,14 @@ class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConf
if mac_address and mac_address != ":::::":
if not re.search(r"""^([0-9a-fA-F]{2}[:]){5}[0-9a-fA-F]{2}$""", mac_address):
QtWidgets.QMessageBox.critical(self, "MAC address", "Invalid MAC address (format required: hh:hh:hh:hh:hh:hh)")
return
return False
default_mac_address = self._IntegerToMac(self._MacToInteger(self._base_mac_address) + adapter_number)
if mac_address != default_mac_address:
custom_adapter_settings["mac_address"] = mac_address
if len(custom_adapter_settings) > 1:
# only save if there is more than the adapter_number key
self._custom_adapters.append(custom_adapter_settings.copy())
return True
def done(self, result):
"""
@@ -196,5 +200,6 @@ class CustomAdaptersConfigurationDialog(QtWidgets.QDialog, Ui_CustomAdaptersConf
"""
if result:
self._updateCustomAdapters()
if not self._updateCustomAdapters():
return
super().done(result)

View File

@@ -15,11 +15,11 @@
# 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 psutil
import platform
import os
import stat
import sys
import struct
from gns3.qt import QtWidgets
@@ -92,19 +92,6 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
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"""
if sys.platform.startswith("win32"):
for proc in psutil.process_iter():
try:
psinfo = proc.as_dict(["exe"])
if psinfo["exe"] and "AVG\\" in psinfo["exe"]:
return (2, "AVG has known issues with GNS3, even after you disable it. You must whitelist dynamips.exe in the AVG preferences.")
except psutil.NoSuchProcess:
pass
return (0, None)
def checkFreeRam(self):
"""Checking for amount of free virtual memory"""
@@ -186,41 +173,11 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
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_()
#exit_code = app.exec()

View File

@@ -34,39 +34,15 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
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())
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)
index = self.uiServerProtocolComboBox.findText(self._compute.protocol().upper())
self.uiServerProtocolComboBox.setCurrentIndex(index)
self.uiServerUserLineEdit.setText(self._compute.user())
def compute(self):
return self._compute
@@ -78,22 +54,22 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
host = self.uiServerHostLineEdit.text().strip()
name = self.uiServerNameLineEdit.text().strip()
protocol = "http"
protocol = self.uiServerProtocolComboBox.currentText().lower()
port = self.uiServerPortSpinBox.value()
user = self.uiServerUserLineEdit.text().strip()
password = self.uiServerPasswordLineEdit.text().strip()
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))
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid compute hostname {}".format(host))
return
if name == "gns3vm":
QtWidgets.QMessageBox.critical(self, "Remote compute", "{} is a reserved name".format(name))
return
if len(name) == 0:
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server name {}".format(name))
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid compute name {}".format(name))
return
if port is None or port < 1:
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server port {}".format(port))
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid compute port {}".format(port))
return
if not self._compute:
@@ -102,12 +78,8 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
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)
self._compute.setUser(user)
self._compute.setPassword(password)
super().accept()
@@ -118,4 +90,4 @@ if __name__ == '__main__':
main = QtWidgets.QMainWindow()
dialog = EditComputeDialog(main)
dialog.show()
exit_code = app.exec_()
exit_code = app.exec()

View File

@@ -15,7 +15,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from ..qt import QtWidgets, QtCore, qslot, qpartial
from gns3.utils import parse_version
from ..qt import QtGui, QtWidgets, QtCore, qslot, qpartial
from ..topology import Topology
from ..ui.edit_project_dialog_ui import Ui_EditProjectDialog
@@ -39,25 +41,45 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
self.uiNodeGridSizeSpinBox.setValue(self._project.nodeGridSize())
self.uiDrawingGridSizeSpinBox.setValue(self._project.drawingGridSize())
self.uiGlobalVariablesGrid.setAlignment(QtCore.Qt.AlignTop)
self.uiGlobalVariablesGrid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
self.uiNewVarButton = QtWidgets.QPushButton('Add new variable', self)
self.uiNewVarButton.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.uiNewVarButton.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
self.uiNewVarButton.clicked.connect(self.onAddNewVariable)
self.uiGlobalVariablesGrid.addWidget(self.uiNewVarButton, 0, 3, QtCore.Qt.AlignRight)
self.uiGlobalVariablesGrid.addWidget(self.uiNewVarButton, 0, 3, QtCore.Qt.AlignmentFlag.AlignRight)
self._variables = self.setUpVariables()
self._readme_filename = "README.txt"
self.uiTabWidget.currentChanged.connect(self._previewMarkdownSlot)
self._loadReadme()
self._variables = self._project.variables()
if not self._variables:
self._variables = [{"name": "", "value": ""}]
self.updateGlobalVariables()
def setUpVariables(self):
new_variable = {"name": "", "value": ""}
variables = self._project.variables()
def _loadReadme(self):
if variables is not None:
variables.append(new_variable)
else:
variables = [new_variable]
return variables
self._project.get("/files/{}".format(self._readme_filename), self._loadedReadme, raw=True)
def _loadedReadme(self, result, error=False, context={}, **kwargs):
if not error:
content = result.decode("utf-8", errors="replace")
self.uiReadmeTextEdit.setPlainText(content)
def _previewMarkdownSlot(self, index):
# index 1 is preview tab
if index == 1:
# QTextDocument before Qt version 5.14 doesn't support Markdown
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.14.0") or parse_version(QtCore.PYQT_VERSION_STR) < parse_version("5.14.0"):
QtWidgets.QMessageBox.critical(self, "Markdown preview", "Markdown preview is only support with Qt version 5.14.0 or above")
return
# show Markdown preview
document = QtGui.QTextDocument()
self.uiReadmePreview.setDocument(document)
document.setMarkdown(self.uiReadmeTextEdit.toPlainText())
def updateGlobalVariables(self):
while True:
@@ -98,7 +120,7 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
variable["value"] = text
def _cleanVariables(self):
return [v for v in self._variables if v.get("name", "").strip() != ""]
return [v for v in self._variables if v.get("name").strip() != ""]
def done(self, result):
"""
@@ -108,14 +130,27 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
"""
if result:
self._project.setName(self.uiProjectNameLineEdit.text())
self._project.setAutoOpen(self.uiProjectAutoOpenCheckBox.isChecked())
self._project.setAutoClose(not self.uiProjectAutoCloseCheckBox.isChecked())
self._project.setAutoStart(self.uiProjectAutoStartCheckBox.isChecked())
self._project.setSceneHeight(self.uiSceneHeightSpinBox.value())
self._project.setSceneWidth(self.uiSceneWidthSpinBox.value())
self._project.setNodeGridSize(self.uiNodeGridSizeSpinBox.value())
self._project.setDrawingGridSize(self.uiDrawingGridSizeSpinBox.value())
self._project.setVariables(self._cleanVariables())
self._project.update()
node_grid_size = self.uiNodeGridSizeSpinBox.value()
drawing_grid_size = self.uiDrawingGridSizeSpinBox.value()
if node_grid_size % drawing_grid_size != 0:
QtWidgets.QMessageBox.critical(self, "Grid sizes", "Invalid grid sizes which will create overlapping lines")
else:
self._project.setNodeGridSize(node_grid_size)
self._project.setDrawingGridSize(drawing_grid_size)
self._project.setName(self.uiProjectNameLineEdit.text())
self._project.setAutoOpen(self.uiProjectAutoOpenCheckBox.isChecked())
self._project.setAutoClose(not self.uiProjectAutoCloseCheckBox.isChecked())
self._project.setAutoStart(self.uiProjectAutoStartCheckBox.isChecked())
self._project.setSceneHeight(self.uiSceneHeightSpinBox.value())
self._project.setSceneWidth(self.uiSceneWidthSpinBox.value())
self._project.setVariables(self._cleanVariables())
self._project.update()
content = self.uiReadmeTextEdit.toPlainText()
if content:
self._project.post("/files/{}".format(self._readme_filename), self._saveReadmeCallback, body=content)
super().done(result)
def _saveReadmeCallback(self, result, error=False, **kwargs):
if error:
QtWidgets.QMessageBox.critical(self, "Edit project", "Could not created readme file")

View File

@@ -48,8 +48,8 @@ class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
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.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Save).clicked.connect(self._okButtonClickedSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Cancel).clicked.connect(self.reject)
self._refreshSlot()
@@ -64,9 +64,10 @@ class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
def _refreshSlot(self):
self._target.get("/files/" + self._path, self._getCallback)
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
def _getCallback(self, result, error=False, **kwargs):
if not error:
self.uiFileTextEdit.setText(raw_body.decode("utf-8", errors="ignore"))
self.uiFileTextEdit.setText(result)
elif result.get("status") == 404:
if self._default:
self.uiFileTextEdit.setText(self._default)

View File

@@ -38,9 +38,9 @@ class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
self._link.listAvailableFilters(self._listAvailableFiltersCallback)
self._initialized = False
self._filter_items = {}
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).clicked.connect(self._resetSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).clicked.connect(self._applyPreferencesSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help).clicked.connect(self._helpSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Reset).clicked.connect(self._resetSlot)
def _listAvailableFiltersCallback(self, result, error=False, *args, **kwargs):
if error:
@@ -91,7 +91,7 @@ class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
spinBox.setMinimum(param["minimum"])
spinBox.setMaximum(param["maximum"])
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(spinBox.sizePolicy().hasHeightForWidth())
@@ -112,7 +112,7 @@ class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
textEdit = QtWidgets.QTextEdit()
textEdit.setAcceptRichText(False)
filter["textEdits"].append(textEdit)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
textEdit.setMinimumWidth(300)
@@ -128,7 +128,7 @@ class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
line += 1
spacerItem = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
spacerItem = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
gridLayout.addItem(spacerItem, line, 0, 1, 1)
vlayout.addLayout(gridLayout)
tab.setLayout(vlayout)

View File

@@ -33,8 +33,8 @@ class IdlePCDialog(QtWidgets.QDialog, Ui_IdlePCDialog):
super().__init__(parent)
self.setupUi(self)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applySlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).clicked.connect(self._applySlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help).clicked.connect(self._helpSlot)
self._router = router
self._idlepcs = idlepcs

View File

@@ -0,0 +1,256 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2022 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import pathlib
from gns3.http_client_error import HttpClientError, HttpClientCancelledRequestError
from ..qt import QtCore, QtGui, QtWidgets, qslot, sip_is_deleted
from ..ui.image_dialog_ui import Ui_ImageDialog
from ..utils import human_size
from ..controller import Controller
import logging
log = logging.getLogger(__name__)
class ImageDialog(QtWidgets.QDialog, Ui_ImageDialog):
"""
Image management dialog.
"""
def __init__(self, parent):
"""
:param parent: parent widget.
"""
super().__init__(parent)
self.setupUi(self)
self.uiUploadImagePushButton.clicked.connect(self._uploadImageSlot)
self.uiDeleteImagePushButton.clicked.connect(self._deleteImageSlot)
self.uiInstallAllPushButton.clicked.connect(self._installAllSlot)
self.uiPruneImagesPushButton.clicked.connect(self._pruneImagesSlot)
self.uiRefreshImagesPushButton.clicked.connect(Controller.instance().refreshImageList)
Controller.instance().image_list_updated_signal.connect(self._updateImageListSlot)
self._updateImageListSlot()
Controller.instance().refreshImageList()
@qslot
def _uploadImageSlot(self, *args):
files, _ = QtWidgets.QFileDialog.getOpenFileNames(
self,
"Select one or more images to upload",
QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DownloadLocation),
"Images (*.bin *.image *.iol *.qcow2 *.vmdk *.iso x86_64* i86bi*);;All files (*)"
)
error_msgs = ""
for path in files:
image_filename = os.path.basename(path)
install_appliances = self.uiInstallApplianceCheckBox.isChecked()
log.info("Uploading image '{}' to controller".format(image_filename))
try:
Controller.instance().post(
f"/images/upload/{image_filename}",
params={"install_appliances": install_appliances},
body=pathlib.Path(path),
context={"image_path": path},
progress_text="Uploading {}".format(image_filename),
timeout=None,
wait=True
)
except HttpClientCancelledRequestError:
return
except HttpClientError as e:
error_msgs += f"{e}\n"
if error_msgs:
error_dialog = QtWidgets.QMessageBox(self)
error_dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
error_dialog.setWindowTitle("Image upload")
error_dialog.setText(f"Error while uploading images to the controller")
error_dialog.setDetailedText(error_msgs)
error_dialog.setIcon(QtWidgets.QMessageBox.Icon.Critical)
error_dialog.show()
Controller.instance().refreshImageList()
@qslot
def _deleteImageSlot(self, *args):
if len(self.uiImagesTreeWidget.selectedItems()) == 0:
QtWidgets.QMessageBox.critical(self, "Delete image", "No images selected")
return
reply = QtWidgets.QMessageBox.warning(
self,
"Delete image(s)",
"Delete the selected images?\nThis cannot be reverted.",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No
)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
images_to_delete = set()
for image in self.uiImagesTreeWidget.selectedItems():
if sip_is_deleted(image):
continue
image_filename = image.data(0, QtCore.Qt.ItemDataRole.UserRole)
images_to_delete.add(image_filename)
error_msgs = ""
for image_filename in images_to_delete:
try:
Controller.instance().delete(
f"/images/{image_filename}",
progress_text=f"Deleting {image_filename}",
timeout=None,
wait=True
)
except HttpClientCancelledRequestError:
return
except HttpClientError as e:
error_msgs += f"{e}\n"
if error_msgs:
error_dialog = QtWidgets.QMessageBox(self)
error_dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
error_dialog.setWindowTitle("Image deletion")
error_dialog.setText(f"Error while deleting images on the controller")
error_dialog.setDetailedText(error_msgs)
error_dialog.setIcon(QtWidgets.QMessageBox.Icon.Critical)
error_dialog.show()
Controller.instance().refreshImageList()
def _installAllSlot(self, *args):
reply = QtWidgets.QMessageBox.warning(
self,
"Install appliance(s)",
"This will attempt to automatically create templates based on image checksums.\nContinue?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No
)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
Controller.instance().post(
f"/images/install",
progress_text=f"Installing appliances",
timeout=None,
wait=True
)
@qslot
def _pruneImagesSlot(self, *args):
reply = QtWidgets.QMessageBox.warning(
self,
"Prune image(s)",
"Delete all images not used by a template?\nThis cannot be reverted.",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No
)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
error_msgs = ""
try:
Controller.instance().delete(
f"/images/prune",
progress_text=f"Pruning images",
timeout=None,
wait=True
)
except HttpClientCancelledRequestError:
return
except HttpClientError as e:
error_msgs += f"{e}\n"
if error_msgs:
error_dialog = QtWidgets.QMessageBox(self)
error_dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
error_dialog.setWindowTitle("Image pruning")
error_dialog.setText(f"Error while deleting images on the controller")
error_dialog.setDetailedText(error_msgs)
error_dialog.setIcon(QtWidgets.QMessageBox.Icon.Critical)
error_dialog.show()
Controller.instance().refreshImageList()
@qslot
def _updateImageListSlot(self, *args):
self.uiImagesTreeWidget.clear()
self.uiDeleteImagePushButton.setEnabled(False)
self.uiImagesTreeWidget.setUpdatesEnabled(False)
items = []
for image in Controller.instance().images():
item = QtWidgets.QTreeWidgetItem([image["filename"], image["image_type"], human_size(image["image_size"])])
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, image["filename"])
item.setToolTip(0, f'{image["filename"]} {image["checksum"]}')
items.append(item)
self.uiImagesTreeWidget.addTopLevelItems(items)
if len(Controller.instance().images()):
self.uiDeleteImagePushButton.setEnabled(True)
self.uiImagesTreeWidget.header().setResizeContentsPrecision(100) # How many rows are checked for the resize for performance reason
self.uiImagesTreeWidget.resizeColumnToContents(0)
self.uiImagesTreeWidget.resizeColumnToContents(1)
self.uiImagesTreeWidget.resizeColumnToContents(2)
self.uiImagesTreeWidget.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder)
self.uiImagesTreeWidget.setUpdatesEnabled(True)
def contextMenuEvent(self, event):
"""
Handles all context menu events.
:param event: QContextMenuEvent instance
"""
items = self.uiImagesTreeWidget.selectedItems()
if items:
menu = QtWidgets.QMenu()
copy = QtGui.QAction("&Copy image information to clipboard", menu)
copy.triggered.connect(self._copyToClipboardSlot)
menu.addAction(copy)
menu.exec(event.globalPos())
def _copyToClipboardSlot(self):
"""
Copies the selected image tooltip to the clipboard.
"""
items = self.uiImagesTreeWidget.selectedItems()
if items:
QtWidgets.QApplication.clipboard().setText(items[0].toolTip(0))
log.info(f"'{items[0].toolTip(0)}' copied to clipboard")
def keyPressEvent(self, e):
"""
Event handler in order to properly handle escape.
"""
if e.key() == QtCore.Qt.Key.Key_Escape:
self.close()
elif e.matches(QtGui.QKeySequence.StandardKey.Copy):
self._copyToClipboardSlot()

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 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 QtWidgets
from ..ui.login_dialog_ui import Ui_LoginDialog
import logging
log = logging.getLogger(__name__)
class LoginDialog(QtWidgets.QDialog, Ui_LoginDialog):
"""
Login dialog.
"""
def __init__(self, parent):
"""
:param parent: parent widget.
"""
super().__init__(parent)
self.setupUi(self)
self._parent = parent
self._username = None
self._password = None
def getUsername(self):
return self._username
def setUsername(self, username):
self.uiUsernameLineEdit.setText(username)
def getPassword(self):
return self._password
def done(self, result):
if result:
self._username = self.uiUsernameLineEdit.text()
self._password = self.uiPasswordLineEdit.text()
super().done(result)

View File

@@ -18,9 +18,9 @@
import sys
import tempfile
import json
import sip
import os
from ..qt import sip
from gns3.qt import QtCore, QtWidgets, qpartial
from gns3.controller import Controller
from gns3.appliance_manager import ApplianceManager
@@ -41,16 +41,16 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
super().__init__(parent)
self.setupUi(self)
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
self.setWizardStyle(QtWidgets.QWizard.WizardStyle.ModernStyle)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
self.setOptions(QtWidgets.QWizard.WizardOption.NoDefaultButton)
# add a custom button to show appliance information
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Update from online registry")
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
self.setButtonText(QtWidgets.QWizard.WizardButton.CustomButton1, "&Update from online registry")
self.setOption(QtWidgets.QWizard.WizardOption.HaveCustomButton1, True)
self.customButtonClicked.connect(self._downloadAppliancesSlot)
self.button(QtWidgets.QWizard.CustomButton1).hide()
self.button(QtWidgets.QWizard.WizardButton.CustomButton1).hide()
self.uiFilterLineEdit.textChanged.connect(self._filterTextChangedSlot)
ApplianceManager.instance().appliances_changed_signal.connect(self._appliancesChangedSlot)
@@ -154,16 +154,16 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
self.uiAppliancesTreeWidget.clear()
parent_routers = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_routers.setText(0, "Routers")
parent_routers.setFlags(parent_routers.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_routers.setFlags(parent_routers.flags() & ~QtCore.Qt.ItemFlag.ItemIsSelectable)
parent_switches = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_switches.setText(0, "Switches")
parent_switches.setFlags(parent_switches.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_switches.setFlags(parent_switches.flags() & ~QtCore.Qt.ItemFlag.ItemIsSelectable)
parent_guests = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_guests.setText(0, "Guests")
parent_guests.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_guests.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemFlag.ItemIsSelectable)
parent_firewalls = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_firewalls.setText(0, "Firewalls")
parent_firewalls.setFlags(parent_firewalls.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_firewalls.setFlags(parent_firewalls.flags() & ~QtCore.Qt.ItemFlag.ItemIsSelectable)
self.uiAppliancesTreeWidget.expandAll()
for appliance in ApplianceManager.instance().appliances():
@@ -200,14 +200,14 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
item.setText(1, "N/A")
item.setText(2, appliance["vendor_name"])
item.setData(0, QtCore.Qt.UserRole, appliance)
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, appliance)
#item.setSizeHint(0, QtCore.QSize(32, 32))
item.setToolTip(0, self._get_tooltip_text(appliance))
Controller.instance().getSymbolIcon(appliance.get("symbol"), qpartial(self._setItemIcon, item),
fallback=":/symbols/" + appliance["category"] + ".svg")
self.uiAppliancesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiAppliancesTreeWidget.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
self.uiAppliancesTreeWidget.resizeColumnToContents(0)
if not appliance_filter:
self.uiAppliancesTreeWidget.collapseAll()
@@ -221,19 +221,19 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
super().initializePage(page_id)
if self.page(page_id) == self.uiApplianceFromServerWizardPage:
self.button(QtWidgets.QWizard.CustomButton1).show()
self.setButtonText(QtWidgets.QWizard.FinishButton, "&Install")
self.button(QtWidgets.QWizard.WizardButton.CustomButton1).show()
self.setButtonText(QtWidgets.QWizard.WizardButton.FinishButton, "&Install")
self._get_appliances_from_server()
else:
self.button(QtWidgets.QWizard.CustomButton1).hide()
self.button(QtWidgets.QWizard.WizardButton.CustomButton1).hide()
def cleanupPage(self, page_id):
"""
Restore button default settings on the first page.
"""
self.button(QtWidgets.QWizard.CustomButton1).hide()
self.setButtonText(QtWidgets.QWizard.FinishButton, "&Finish")
self.button(QtWidgets.QWizard.WizardButton.CustomButton1).hide()
self.setButtonText(QtWidgets.QWizard.WizardButton.FinishButton, "&Finish")
super().cleanupPage(page_id)
def validateCurrentPage(self):
@@ -274,7 +274,7 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
items = self.uiAppliancesTreeWidget.selectedItems()
for item in items:
f = tempfile.NamedTemporaryFile(mode="w+", suffix=".builtin.gns3a", delete=False)
json.dump(item.data(0, QtCore.Qt.UserRole), f)
json.dump(item.data(0, QtCore.Qt.ItemDataRole.UserRole), f)
f.close()
MainWindow.instance().loadPath(f.name)
try:

View File

@@ -40,8 +40,8 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
self._node_items = node_items
self._parent_items = {}
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Reset).setEnabled(False)
self.previousItem = None
self.previousPage = None
@@ -84,7 +84,7 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
ConfigurationPageItem(self._parent_items[parent], node_item)
# sort the tree
self.uiNodesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiNodesTreeWidget.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
if len(self._node_items) == 1:
parent = " {} group".format(str(node_item.node()))
@@ -135,19 +135,19 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
self.uiConfigStackedWidget.setCurrentWidget(page)
if page != self.uiEmptyPageWidget:
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(True)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).setEnabled(True)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).setEnabled(True)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Reset).setEnabled(True)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help).setEnabled(True)
else:
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Reset).setEnabled(False)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help).setEnabled(False)
# hide the contextual help button if there is no help text
if page.whatsThis():
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).show()
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help).show()
else:
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).hide()
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help).hide()
def on_uiButtonBox_clicked(self, button):
"""
@@ -157,13 +157,13 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
"""
try:
if button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply):
if button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply):
self.applySettings()
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset):
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Reset):
self.resetSettings()
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help):
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help):
self.showHelp()
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Cancel):
QtWidgets.QDialog.reject(self)
else:
self.applySettings()

View File

@@ -54,12 +54,12 @@ class NotifDialog(QtWidgets.QWidget):
super().__init__(parent)
self._notifs = []
self.setWindowFlags(QtCore.Qt.FramelessWindowHint |
QtCore.Qt.WindowDoesNotAcceptFocus |
QtCore.Qt.SubWindow)
# QtCore.Qt.Tool)
# QtCore.Qt.WindowStaysOnTopHint)
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) # | QtCore.Qt.WA_TranslucentBackground)
self.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint |
QtCore.Qt.WindowType.WindowDoesNotAcceptFocus |
QtCore.Qt.WindowType.SubWindow)
# QtCore.Qt.WindowType.Tool)
# QtCore.Qt.WindowType.WindowStaysOnTopHint)
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_ShowWithoutActivating) # | QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
self._layout = QtWidgets.QVBoxLayout()
@@ -70,7 +70,7 @@ class NotifDialog(QtWidgets.QWidget):
for i in range(0, MAX_ELEMENTS):
l = QtWidgets.QLabel()
l.setAlignment(QtCore.Qt.AlignTop)
l.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
l.setWordWrap(True)
l.hide()
self._layout.addWidget(l)
@@ -187,4 +187,4 @@ if __name__ == '__main__':
main.setMinimumWidth(600)
main.setMinimumHeight(600)
main.show()
exit_code = app.exec_()
exit_code = app.exec()

View File

@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 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 ..qt import QtCore, QtGui, QtWidgets
from ..ui.password_dialog_ui import Ui_PasswordDialog
import logging
log = logging.getLogger(__name__)
class PasswordDialog(QtWidgets.QDialog, Ui_PasswordDialog):
"""
Password dialog.
"""
def __init__(self, parent):
"""
:param parent: parent widget.
"""
super().__init__(parent)
self.setupUi(self)
self._password = None
self._eye_on_icon = QtGui.QIcon(':/icons/eye-on.svg')
self._eye_off_icon = QtGui.QIcon(':/icons/eye-off.svg')
for line_edit in [self.uiPasswordLineEdit, self.uiConfirmPasswordLineEdit]:
action = line_edit.addAction(self._eye_on_icon, QtWidgets.QLineEdit.TrailingPosition)
button = action.associatedWidgets()[-1]
button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
button.pressed.connect(self.onPressedSlot)
#button.released.connect(self.onReleasedSlot)
def onPressedSlot(self):
button = self.sender()
line_edit = button.parent()
if line_edit.echoMode() == QtWidgets.QLineEdit.Password:
button.setIcon(self._eye_off_icon)
line_edit.setEchoMode(QtWidgets.QLineEdit.Normal)
else:
button.setIcon(self._eye_on_icon)
line_edit.setEchoMode(QtWidgets.QLineEdit.Password)
# def onReleasedSlot(self):
#
# button = self.sender()
# button.setIcon(self._eye_on_icon)
# button.parent().setEchoMode(QtWidgets.QLineEdit.Password)
def getPassword(self):
return self._password
def done(self, result):
if result:
new_password = self.uiPasswordLineEdit.text()
confirm_password = self.uiConfirmPasswordLineEdit.text()
if new_password != confirm_password:
QtWidgets.QMessageBox.critical(self, "Error", "Passwords do not match.")
return
pattern = re.compile(r'^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$')
if not pattern.match(new_password):
QtWidgets.QMessageBox.critical(self, "Error", "Password must be at least 8 characters long and contain at least one digit, one lowercase letter and one uppercase letter.")
return
self._password = new_password
super().done(result)

View File

@@ -19,11 +19,12 @@
Dialog to load module and built-in preference pages.
"""
from ..qt import QtCore, QtWidgets
from ..qt import QtGui, QtCore, QtWidgets
from ..ui.preferences_dialog_ui import Ui_PreferencesDialog
from ..pages.server_preferences_page import ServerPreferencesPage
from ..pages.controller_preferences_page import ControllerPreferencesPage
from ..pages.general_preferences_page import GeneralPreferencesPage
from ..pages.packet_capture_preferences_page import PacketCapturePreferencesPage
from ..pages.user_preferences_page import UserPreferencesPage
from ..pages.gns3_vm_preferences_page import GNS3VMPreferencesPage
from ..modules import MODULES
@@ -49,8 +50,9 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
# 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
geometry = QtGui.QGuiApplication.primaryScreen().geometry()
height = geometry.height() - 100
width = geometry.width() - 100
# 980 is the default width
if self.width() > width:
@@ -60,7 +62,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
self.resize(self.width(), height)
self.uiTreeWidget.currentItemChanged.connect(self._showPreferencesPageSlot)
self._applyButton = self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply)
self._applyButton = self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply)
self._applyButton.clicked.connect(self._applyPreferences)
self._applyButton.setEnabled(False)
self._applyButton.setStyleSheet("QPushButton:disabled {color: gray}")
@@ -73,7 +75,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
# set the maximum width based on the content of column 0
self.uiTreeWidget.setMaximumWidth(self.uiTreeWidget.sizeHintForColumn(0) + 10)
# Something has change?
# Something has changed?
self._modified_pages = set()
def _loadPreferencePages(self):
@@ -84,9 +86,10 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
# load built-in preference pages
pages = [
GeneralPreferencesPage,
ServerPreferencesPage,
GNS3VMPreferencesPage,
PacketCapturePreferencesPage,
ControllerPreferencesPage,
UserPreferencesPage,
#GNS3VMPreferencesPage,
PacketCapturePreferencesPage
]
for page in pages:
@@ -95,7 +98,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
name = preferences_page.windowTitle()
item = QtWidgets.QTreeWidgetItem(self.uiTreeWidget)
item.setText(0, name)
item.setData(0, QtCore.Qt.UserRole, preferences_page)
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, preferences_page)
self.uiStackedWidget.addWidget(preferences_page)
self._items.append(item)
self._watchForChanges(preferences_page)
@@ -111,7 +114,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
name = preferences_page.windowTitle()
item = QtWidgets.QTreeWidgetItem(parent)
item.setText(0, name)
item.setData(0, QtCore.Qt.UserRole, preferences_page)
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, preferences_page)
self.uiStackedWidget.addWidget(preferences_page)
self._items.append(item)
if cls is preference_pages[0]:
@@ -131,6 +134,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
QtWidgets.QLineEdit: "textChanged",
QtWidgets.QPlainTextEdit: "textChanged",
# QtWidgets.QTreeWidget: "itemChanged",
QtWidgets.QTreeWidget: "itemDoubleClicked",
QtWidgets.QComboBox: "currentIndexChanged",
QtWidgets.QSpinBox: "valueChanged",
QtWidgets.QAbstractButton: "pressed"
@@ -175,7 +179,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
if current is None:
current = previous
preferences_page = current.data(0, QtCore.Qt.UserRole)
preferences_page = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
accessible_name = preferences_page.accessibleName()
if accessible_name:
self.uiTitleLabel.setText(accessible_name)
@@ -191,9 +195,9 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
for index in range(0, self.uiStackedWidget.count()):
page = self.uiStackedWidget.widget(index)
if self.uiStackedWidget.currentIndex() == index:
page.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
page.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
else:
page.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
page.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored, QtWidgets.QSizePolicy.Policy.Ignored)
def _applyPreferences(self):
"""
@@ -222,9 +226,9 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
reply = QtWidgets.QMessageBox.warning(self,
"Preferences",
"You have unsaved preferences in {}.\n\nContinue without saving?".format(pages_title),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
QtWidgets.QDialog.reject(self)

View File

@@ -19,7 +19,7 @@ import os
import sys
import shutil
from gns3.qt import QtWidgets
from gns3.qt import QtWidgets, QtGui
from gns3.local_config import LocalConfig
from gns3.ui.profile_select_dialog_ui import Ui_ProfileSelectDialog
from gns3.version import __version_info__
@@ -46,7 +46,7 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
self.uiDeletePushButton.clicked.connect(self._deletePushButtonSlot)
# Center on screen
screen = QtWidgets.QApplication.desktop().screenGeometry()
screen = QtGui.QGuiApplication.primaryScreen().geometry()
self.move(screen.center() - self.rect().center())
version = "{}.{}".format(__version_info__[0], __version_info__[1])
@@ -54,8 +54,14 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
appdata = os.path.expandvars("%APPDATA%")
path = os.path.join(appdata, "GNS3", version)
else:
home = os.path.expanduser("~")
path = os.path.join(home, ".config", "GNS3", version)
xgd_config_var = "$XDG_CONFIG_HOME"
xdg_config_res = os.path.expandvars(xgd_config_var)
if xdg_config_res != xgd_config_var:
path = os.path.join(xdg_config_res, "GNS3", version)
else:
home = os.path.expanduser("~")
path = os.path.join(home, ".config", "GNS3", version)
self.profiles_path = os.path.join(path, "profiles")
self.uiShowAtStartupCheckBox.setChecked(LocalConfig.instance().multiProfiles())
@@ -103,4 +109,4 @@ if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
dialog = ProfileSelectDialog()
dialog.show()
exit_code = app.exec_()
exit_code = app.exec()

View File

@@ -53,7 +53,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
if show_open_options:
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
self._addRecentFilesMenu()
else:
self.uiOpenProjectGroupBox.hide()
self.uiProjectTabWidget.removeTab(1)
@@ -93,15 +93,15 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
for project in self.uiProjectsTreeWidget.selectedItems():
if sip_is_deleted(project):
continue
project_id = project.data(0, QtCore.Qt.UserRole)
project_name = project.data(1, QtCore.Qt.UserRole)
project_id = project.data(0, QtCore.Qt.ItemDataRole.UserRole)
project_name = project.data(1, QtCore.Qt.ItemDataRole.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:
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
projects_to_delete.add(project_id)
for project_id in projects_to_delete:
@@ -118,8 +118,8 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
return
for project in self.uiProjectsTreeWidget.selectedItems():
project_id = project.data(0, QtCore.Qt.UserRole)
project_name = project.data(1, QtCore.Qt.UserRole)
project_id = project.data(0, QtCore.Qt.ItemDataRole.UserRole)
project_name = project.data(1, QtCore.Qt.ItemDataRole.UserRole)
new_project_name = project_name + "-1"
existing_project_name = [p["name"] for p in Controller.instance().projects()]
@@ -131,23 +131,32 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
name, reply = QtWidgets.QInputDialog.getText(self,
"Duplicate project",
'Duplicate project "{}"?.'.format(project_name),
QtWidgets.QLineEdit.Normal,
QtWidgets.QLineEdit.EchoMode.Normal,
new_project_name)
name = name.strip()
if reply and len(name) > 0:
reset_mac_addresses = self.uiResetMacAddressesCheckBox.isChecked()
if Controller.instance().isRemote():
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
self._duplicateCallback,
body={"name": name},
progressText="Duplicating project '{}'...".format(name),
timeout=None)
Controller.instance().post(
"/projects/{project_id}/duplicate".format(project_id=project_id),
self._duplicateCallback,
body={"name": name, "reset_mac_addresses": reset_mac_addresses},
progress_text="Duplicating project '{}'...".format(name),
timeout=None,
wait=True
)
else:
project_location = os.path.join(Topology.instance().projectsDirPath(), name)
Controller.instance().post("/projects/{project_id}/duplicate".format(project_id=project_id),
self._duplicateCallback,
body={"name": name, "path": project_location},
progressText="Duplicating project '{}'...".format(name),
timeout=None)
Controller.instance().post(
"/projects/{project_id}/duplicate".format(project_id=project_id),
self._duplicateCallback,
body={"name": name, "path": project_location, "reset_mac_addresses": reset_mac_addresses},
progress_text="Duplicating project '{}'...".format(name),
timeout=None,
wait=True
)
def _duplicateCallback(self, result, error=False, **kwargs):
if error:
@@ -164,9 +173,9 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
for project in Controller.instance().projects():
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)
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, project["project_id"])
item.setData(1, QtCore.Qt.ItemDataRole.UserRole, project["name"])
item.setData(2, QtCore.Qt.ItemDataRole.UserRole, path)
items.append(item)
self.uiProjectsTreeWidget.addTopLevelItems(items)
@@ -177,7 +186,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
self.uiProjectsTreeWidget.resizeColumnToContents(0)
self.uiProjectsTreeWidget.resizeColumnToContents(1)
self.uiProjectsTreeWidget.resizeColumnToContents(2)
self.uiProjectsTreeWidget.sortItems(0, QtCore.Qt.AscendingOrder)
self.uiProjectsTreeWidget.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder)
self.uiProjectsTreeWidget.setUpdatesEnabled(True)
def keyPressEvent(self, e):
@@ -185,7 +194,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
Event handler in order to properly handle escape.
"""
if e.key() == QtCore.Qt.Key_Escape:
if e.key() == QtCore.Qt.Key.Key_Escape:
self.close()
def _projectNameSlot(self, text):
@@ -228,20 +237,20 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
self._main_window.openProjectActionSlot()
self.reject()
def _showRecentProjectsSlot(self):
def _addRecentFilesMenu(self):
"""
lot to show all the recent projects in a menu.
Add recent projects in a menu.
"""
menu = QtWidgets.QMenu()
menu.triggered.connect(self._menuTriggeredSlot)
menu = QtWidgets.QMenu(parent=self)
if Controller.instance().isRemote():
for action in self._main_window.recent_project_actions:
menu.addAction(action)
else:
for action in self._main_window.recent_file_actions:
menu.addAction(action)
menu.exec_(QtGui.QCursor.pos())
menu.triggered.connect(self._menuTriggeredSlot)
self.uiRecentProjectsPushButton.setMenu(menu)
def _overwriteProjectCallback(self, result, error=False, **kwargs):
if error:
@@ -286,10 +295,10 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
reply = QtWidgets.QMessageBox.warning(self,
"New project",
'Project "{}" already exists in location "{}", overwrite it?'.format(existing_project["name"], existing_project["path"]),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.Yes:
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
Controller.instance().deleteProject(existing_project["project_id"], self._overwriteProjectCallback)
# In all cases we cancel the new project and if project success to delete
@@ -310,6 +319,6 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
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_id"] = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
self._project_settings["project_name"] = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
super().done(result)

View File

@@ -18,11 +18,11 @@
import sys
import datetime
from gns3.qt import QtCore, QtWidgets
from ..local_server import LocalServer
from ..utils.progress_dialog import ProgressDialog
from ..utils.export_project_worker import ExportProjectWorker
from ..ui.export_project_wizard_ui import Ui_ExportProjectWizard
from gns3.qt import QtCore, QtWidgets, QtGui
from gns3.utils import parse_version
from gns3.http_client_error import HttpClientError, HttpClientCancelledRequestError
from gns3.local_server import LocalServer
from gns3.ui.export_project_wizard_ui import Ui_ExportProjectWizard
import logging
log = logging.getLogger(__name__)
@@ -33,6 +33,8 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
Export project wizard.
"""
readme_signal = QtCore.pyqtSignal()
def __init__(self, project, parent):
super().__init__(parent)
@@ -40,38 +42,78 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
self._project = project
self._path = None
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
self.setWizardStyle(QtWidgets.QWizard.WizardStyle.ModernStyle)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
self.setOptions(QtWidgets.QWizard.WizardOption.NoDefaultButton)
self.uiCompressionComboBox.addItem("None", "none")
self.uiCompressionComboBox.addItem("Zip compression (deflate)", "zip")
self.uiCompressionComboBox.addItem("Bzip2 compression", "bzip2")
self.uiCompressionComboBox.addItem("Lzma compression", "lzma")
self.uiCompressionComboBox.addItem("Zstandard compression", "zstd")
self.uiCompressionComboBox.currentIndexChanged.connect(self._compressionChangedSlot)
# set zip compression by default
self.uiCompressionComboBox.setCurrentIndex(1)
# set zstd compression by default
self.uiCompressionComboBox.setCurrentIndex(4)
self.helpRequested.connect(self._showHelpSlot)
self.uiPathBrowserToolButton.clicked.connect(self._pathBrowserSlot)
readme_text = "Project: '{}' created on {}\nAuthor: John Doe <john.doe@example.com>\n\nNo project description was given".format(self._project.name(), datetime.date.today())
self.uiReadmeTextEdit.setPlainText(readme_text)
# QTextDocument before Qt version 5.14 doesn't support Markdown
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.14.0") or parse_version(QtCore.PYQT_VERSION_STR) < parse_version("5.14.0"):
self._use_markdown = False
else:
self._use_markdown = True
self._readme_filename = "README.txt"
self.uiTabWidget.currentChanged.connect(self._previewMarkdownSlot)
self._loadReadme()
def _loadReadme(self):
self._project.get("/files/{}".format(self._readme_filename), self._loadedReadme, raw=True)
def _loadedReadme(self, result, error=False, context={}, **kwargs):
if not error:
content = result.decode("utf-8", errors="replace")
self.uiReadmeTextEdit.setPlainText(content)
else:
if self._use_markdown:
readme_markdown = "# Project {}\n\nCreated on: {}\n\nAuthor: John Doe <john.doe@example.com>\n\n## Description\n\nNo project description was given".format(self._project.name(), datetime.date.today())
self.uiReadmeTextEdit.setPlainText(readme_markdown)
else:
readme_text = "Project: '{}' created on {}\nAuthor: John Doe <john.doe@example.com>\n\nNo project description was given".format(self._project.name(), datetime.date.today())
self.uiReadmeTextEdit.setPlainText(readme_text)
def _pathBrowserSlot(self):
directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation)
if len(directory) == 0:
directory = LocalServer.instance().localServerSettings()["projects_path"]
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export portable project", directory,
"GNS3 Portable Project (*.gns3project *.gns3p)",
"GNS3 Portable Project (*.gns3project *.gns3p)")
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export project", directory,
"GNS3 Project (*.gns3project *.gns3p)",
"GNS3 Project (*.gns3project *.gns3p)")
if path is None or len(path) == 0:
return
self.uiPathLineEdit.setText(path)
def _previewMarkdownSlot(self, index):
# index 1 is preview tab
if index == 1:
if self._use_markdown is False:
QtWidgets.QMessageBox.critical(self, "Markdown preview", "Markdown preview is only support with Qt version 5.14.0 or above")
return
# show Markdown preview
document = QtGui.QTextDocument()
self.uiReadmePreviewEdit.setDocument(document)
document.setMarkdown(self.uiReadmeTextEdit.toPlainText())
def _showHelpSlot(self):
include_image_help = """Including base images means additional images will not be requested to
@@ -99,15 +141,39 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
QtWidgets.QMessageBox.critical(self, "Export project", "Cannot export project to '{}': {}".format(path, e))
return False
self._path = path
elif self.currentPage() == self.uiProjectReadmeWizardPage:
text = self.uiReadmeTextEdit.toPlainText().strip()
if text:
self._project.post("/files/README.txt", self._saveReadmeCallback, body=text)
return True
def _saveReadmeCallback(self, result, error=False, **kwargs):
if error:
QtWidgets.QMessageBox.critical(self, "Export project", "Could not created readme file")
self.readme_signal.emit()
def waitForReadme(self, signal, timeout=10000):
# inspired from https://www.jdreaver.com/posts/2014-07-03-waiting-for-signals-pyside-pyqt.html
loop = QtCore.QEventLoop()
signal.connect(loop.quit)
if timeout is not None:
QtCore.QTimer.singleShot(timeout, loop.quit)
loop.exec()
def _compressionChangedSlot(self, index):
"""
Set the default compression level.
"""
compression = self.uiCompressionComboBox.itemData(index)
self.uiCompressionLevelSpinBox.setEnabled(True)
if compression == "zip":
self.uiCompressionLevelSpinBox.setValue(6) # ZIP default compression level is 6
elif compression == "bzip2":
self.uiCompressionLevelSpinBox.setValue(9) # BZIP2 default compression level is 9
elif compression == "zstd":
self.uiCompressionLevelSpinBox.setValue(3) # ZSTD default compression level is 3
else:
# compression level is not supported
self.uiCompressionLevelSpinBox.setValue(0)
self.uiCompressionLevelSpinBox.setEnabled(False)
def done(self, result):
"""
@@ -115,17 +181,62 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
"""
if result:
content = self.uiReadmeTextEdit.toPlainText()
if content:
self._project.post("/files/{}".format(self._readme_filename), self._saveReadmeCallback, body=content)
include_images = include_snapshots = reset_mac_addresses = keep_compute_ids = "no"
if self.uiIncludeImagesCheckBox.isChecked():
include_images = "yes"
else:
include_images = "no"
if self.uiIncludeSnapshotsCheckBox.isChecked():
include_snapshots = "yes"
else:
include_snapshots = "no"
if self.uiResetMacAddressesCheckBox.isChecked():
reset_mac_addresses = "yes"
if self.uiKeepComputeIdsCheckBox.isChecked():
keep_compute_ids = "yes"
compression = self.uiCompressionComboBox.currentData()
export_worker = ExportProjectWorker(self._project, self._path, include_images, include_snapshots, compression)
progress_dialog = ProgressDialog(export_worker, "Exporting project", "Exporting portable project files...", "Cancel", parent=self, create_thread=False)
progress_dialog.show()
progress_dialog.exec_()
self.waitForReadme(self.readme_signal)
params = {
"include_images": include_images,
"include_snapshots": include_snapshots,
"reset_mac_addresses": reset_mac_addresses,
"keep_compute_ids": keep_compute_ids,
"compression": compression
}
try:
self._project.get(
"/export",
callback=None,
download_progress_callback=self._downloadFileProgress,
progress_text="Exporting project files...",
params=params,
timeout=None,
wait=True,
raw=True
)
except HttpClientCancelledRequestError:
pass
except HttpClientError as e:
QtWidgets.QMessageBox.critical(
self,
"Project export",
f"Could not export project: {e}"
)
super().done(result)
def _downloadFileProgress(self, content, **kwargs):
"""
Called for each part of the file
"""
try:
with open(self._path, 'ab') as f:
f.write(content)
except OSError as e:
log.error(f"Could not write project file: {e}")
return

View File

@@ -35,11 +35,9 @@ class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
self._project = project
self.setupUi(self)
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
self.gridLayout.setAlignment(QtCore.Qt.AlignTop)
self.gridLayout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
self.label.setOpenExternalLinks(True)
self._variables = self._getVariables(project)
self._loadReadme()
self._addMisingVariablesEdits()
@@ -50,10 +48,11 @@ class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
return variables
def _addMisingVariablesEdits(self):
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
#TODO: refactor this to use a QListWidget
missing = [v for v in self._variables if v.get("name") and v.get("value", "").strip() == ""]
for i, variable in enumerate(missing, start=0):
nameLabel = QtWidgets.QLabel()
nameLabel.setText(variable.get("name", ""))
nameLabel.setText(variable.get("name") + ":")
self.gridLayout.addWidget(nameLabel, i, 0)
valueEdit = QtWidgets.QLineEdit()
@@ -62,24 +61,24 @@ class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
self.gridLayout.addWidget(valueEdit, i, 1)
def _loadReadme(self):
self._project.get("/files/README.txt", self._loadedReadme)
self._project.get("/files/README.txt", self._loadedReadme, raw=True)
def _loadedReadme(self, result, error=False, raw_body=None, context={}, **kwargs):
def _loadedReadme(self, result, error=False, context={}, **kwargs):
if not error:
self.label.setText(raw_body.decode("utf-8"))
self.label.setText(result.decode("utf-8"))
def onValueChange(self, variable, text):
variable["value"] = text
def _okButtonClickedSlot(self):
missing = [v for v in self._variables if v.get("value", "").strip() == ""]
missing = [v for v in self._variables if v.get("name") and v.get("value", "").strip() == ""]
if len(missing) > 0:
reply = QtWidgets.QMessageBox.warning(
self, 'Missing values',
'Are you sure you want to continue without providing missing values?',
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
reply = QtWidgets.QMessageBox.warning(self,
"Missing values",
"Are you sure you want to continue without providing missing values?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
self._project.setVariables(self._variables)

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
# Copyright (C) 2021 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,14 +19,12 @@ import sys
import os
import shutil
from gns3.qt import QtCore, QtWidgets, QtGui, QtNetwork, qslot
from gns3.qt import QtCore, QtWidgets, QtNetwork
from gns3.controller import Controller
from gns3.local_server import LocalServer
from gns3.utils.interfaces import interfaces
from ..settings import DEFAULT_LOCAL_SERVER_HOST
from ..settings import DEFAULT_CONTROLLER_HOST
from ..ui.setup_wizard_ui import Ui_SetupWizard
from ..version import __version__
import logging
@@ -45,43 +43,21 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
self.setupUi(self)
self.adjustSize()
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)
self.setWizardStyle(QtWidgets.QWizard.WizardStyle.ModernStyle)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
self.setOptions(QtWidgets.QWizard.WizardOption.NoDefaultButton)
self.uiLocalServerToolButton.clicked.connect(self._localServerBrowserSlot)
self.uiGNS3VMDownloadLinkUrlLabel.setText("")
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
self.uiVirtualBoxRadioButton.clicked.connect(self._listVirtualBoxVMsSlot)
self.uiVMwareBannerButton.clicked.connect(self._VMwareBannerButtonClickedSlot)
settings = parent.settings()
self.uiShowCheckBox.setChecked(settings["hide_setup_wizard"])
# by default all radio buttons are unchecked
self.uiVmwareRadioButton.setAutoExclusive(False)
self.uiVirtualBoxRadioButton.setAutoExclusive(False)
self.uiVmwareRadioButton.setChecked(False)
self.uiVirtualBoxRadioButton.setChecked(False)
# Mandatory fields
self.uiLocalServerWizardPage.registerField("path*", self.uiLocalServerPathLineEdit)
self.uiLocalControllerWizardPage.registerField("path*", self.uiLocalServerPathLineEdit)
# load all available addresses
for address in QtNetwork.QNetworkInterface.allAddresses():
if address.protocol() in [QtNetwork.QAbstractSocket.IPv4Protocol, QtNetwork.QAbstractSocket.IPv6Protocol]:
if address.protocol() in [QtNetwork.QAbstractSocket.NetworkLayerProtocol.IPv4Protocol, QtNetwork.QAbstractSocket.NetworkLayerProtocol.IPv6Protocol]:
address_string = address.toString()
if address_string.startswith("169.254") or address_string.startswith("fe80"):
# ignore link-local addresses, could not use https://doc.qt.io/qt-5/qhostaddress.html#isLinkLocal
@@ -89,17 +65,16 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
continue
self.uiLocalServerHostComboBox.addItem(address_string, address_string)
if sys.platform.startswith("darwin"):
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_fusion_banner.png"))
else:
self.uiVMwareBannerButton.setIcon(QtGui.QIcon(":/images/vmware_workstation_banner.png"))
self.uiLocalServerHostComboBox.addItem("localhost", "localhost") # local host
self.uiLocalServerHostComboBox.addItem("::", "::") # all IPv6 addresses
self.uiLocalServerHostComboBox.addItem("0.0.0.0", "0.0.0.0") # all IPv4 addresses
if sys.platform.startswith("linux"):
self.uiLocalRadioButton.setChecked(True)
self.uiLocalLabel.setText("Dependencies like Dynamips and Qemu must be manually installed")
Controller.instance().connected_signal.connect(self._refreshLocalServerStatusSlot)
Controller.instance().connection_failed_signal.connect(self._refreshLocalServerStatusSlot)
# only support local controller on Linux
self.uiLocalControllerRadioButton.setChecked(True)
else:
self.uiLocalControllerRadioButton.setEnabled(False)
self.uiRemoteControllerRadioButton.setChecked(True)
def _localServerBrowserSlot(self):
"""
@@ -108,7 +83,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
filter = ""
if sys.platform.startswith("win"):
filter = "Executable (*.exe);;All files (*.*)"
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:
@@ -116,43 +91,6 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
self.uiLocalServerPathLineEdit.setText(path)
def _VMwareBannerButtonClickedSlot(self):
if sys.platform.startswith("darwin"):
url = "http://send.onenetworkdirect.net/z/621395/CD225091/"
else:
url = "http://send.onenetworkdirect.net/z/616207/CD225091/"
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
def _listVMwareVMsSlot(self):
"""
Slot to refresh the VMware VMs list.
"""
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{version}.zip".format(version=__version__)
self.uiGNS3VMDownloadLinkUrlLabel.setText('The GNS3 VM can <a href="{download_url}">downloaded here</a>.<br>Import the VM in your virtualization software and hit refresh.'.format(download_url=download_url))
self.uiVirtualBoxRadioButton.setChecked(False)
from gns3.modules import VMware
settings = VMware.instance().settings()
if not os.path.exists(settings["vmrun_path"]):
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed. You can download it from https://www.vmware.com/support/developer/vix-api/. After installation you need to restart GNS3.")
return
self._refreshVMListSlot()
def _listVirtualBoxVMsSlot(self):
"""
Slot to refresh the VirtualBox VMs list.
"""
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format(version=__version__)
self.uiGNS3VMDownloadLinkUrlLabel.setText('If you don\'t have the GNS3 Virtual Machine you can <a href="{download_url}">download it here</a>.<br>And import the VM in the virtualization software and hit refresh.'.format(download_url=download_url))
self.uiVmwareRadioButton.setChecked(False)
from gns3.modules import VirtualBox
settings = VirtualBox.instance().settings()
if not os.path.exists(settings["vboxmanage_path"]):
QtWidgets.QMessageBox.critical(self, "VirtualBox", "VBoxManage could not be found, VirtualBox is probably not installed. After installation you need to restart GNS3.")
return
self._refreshVMListSlot()
def _setPreferencesPane(self, dialog, name):
"""
Finds the first child of the QTreeWidgetItem name.
@@ -163,18 +101,11 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
:returns: current QWidget
"""
pane = dialog.uiTreeWidget.findItems(name, QtCore.Qt.MatchFixedString)[0]
pane = dialog.uiTreeWidget.findItems(name, QtCore.Qt.MatchFlag.MatchFixedString)[0]
child_pane = pane.child(0)
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.
@@ -183,35 +114,16 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
super().initializePage(page_id)
if self.page(page_id) == self.uiServerWizardPage:
if self.page(page_id) == self.uiControllerWizardPage:
Controller.instance().setDisplayError(False)
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:
elif self.page(page_id) == self.uiLocalControllerWizardPage:
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)
else:
if self.uiVMRadioButton.isChecked():
# Try to bind with the IP address allocated for VMnet1
for interface in interfaces():
if "vmnet1" in interface["name"].lower():
index = self.uiLocalServerHostComboBox.findText(interface["ip_address"])
break
else:
index = self.uiLocalServerHostComboBox.findText(DEFAULT_LOCAL_SERVER_HOST)
index = self.uiLocalServerHostComboBox.findText(DEFAULT_CONTROLLER_HOST)
if index != -1:
self.uiLocalServerHostComboBox.setCurrentIndex(index)
@@ -220,61 +132,28 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
elif self.page(page_id) == self.uiRemoteControllerWizardPage:
local_server_settings = LocalServer.instance().localServerSettings()
if local_server_settings["host"] is None:
self.uiRemoteMainServerHostLineEdit.setText(DEFAULT_LOCAL_SERVER_HOST)
self.uiRemoteMainServerAuthCheckBox.setChecked(False)
self.uiRemoteMainServerHostLineEdit.setText(DEFAULT_CONTROLLER_HOST)
self.uiRemoteMainServerUserLineEdit.setText("")
self.uiRemoteMainServerPasswordLineEdit.setText("")
else:
self.uiRemoteMainServerHostLineEdit.setText(local_server_settings["host"])
self.uiRemoteMainServerAuthCheckBox.setChecked(local_server_settings["auth"])
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["user"])
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["username"])
self.uiRemoteMainServerPasswordLineEdit.setText(local_server_settings["password"])
self.uiRemoteMainServerPortSpinBox.setValue(local_server_settings["port"])
elif self.page(page_id) == self.uiLocalServerStatusWizardPage:
self._refreshLocalServerStatusSlot()
elif self.page(page_id) == self.uiSummaryWizardPage:
self.uiSummaryTreeWidget.clear()
if self.uiLocalRadioButton.isChecked():
if self.uiLocalControllerRadioButton.isChecked():
local_server_settings = LocalServer.instance().localServerSettings()
self._addSummaryEntry("Server type:", "Local")
self._addSummaryEntry("Type:", "Local")
self._addSummaryEntry("Path:", local_server_settings["path"])
self._addSummaryEntry("Host:", local_server_settings["host"])
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
elif self.uiRemoteControllerRadioButton.isChecked():
local_server_settings = LocalServer.instance().localServerSettings()
self._addSummaryEntry("Server type:", "Remote")
self._addSummaryEntry("Type:", "Remote")
self._addSummaryEntry("Host:", local_server_settings["host"])
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
self._addSummaryEntry("User:", local_server_settings["user"])
else:
self._addSummaryEntry("Server type:", "GNS3 Virtual Machine")
self._addSummaryEntry("VM engine:", self._GNS3VMSettings()["engine"].capitalize())
self._addSummaryEntry("VM name:", self._GNS3VMSettings()["vmname"])
self._addSummaryEntry("VM vCPUs:", str(self._GNS3VMSettings()["vcpus"]))
self._addSummaryEntry("VM RAM:", str(self._GNS3VMSettings()["ram"]) + " MB")
@qslot
def _refreshLocalServerStatusSlot(self):
"""
Refresh the local server status page
"""
self.uiLocalServerTextEdit.clear()
if Controller.instance().connected():
self.uiLocalServerTextEdit.setText("Connection to the local GNS3 server has been successful!")
Controller.instance().get("/gns3vm", self._getSettingsCallback)
elif Controller.instance().connecting():
self.uiLocalServerTextEdit.setText("Please wait connection to the GNS3 server...")
else:
local_server_settings = LocalServer.instance().localServerSettings()
self.uiLocalServerTextEdit.setText("Connection to local server failed. Please try one of the following:\n\n- Make sure GNS3 is allowed to run by your firewall.\n- Go back and try to change the server host binding and/or the port\n- Check with a browser if you can connect to {protocol}://{host}:{port}.\n- Try to run {path} in a terminal to see if you have an error.".format(protocol=local_server_settings["protocol"], host=local_server_settings["host"], port=local_server_settings["port"], path=local_server_settings["path"]))
def _GNS3VMSettings(self):
return self._gns3_vm_settings
def _setGNS3VMSettings(self, settings):
Controller.instance().put("/gns3vm", self._saveSettingsCallback, settings, timeout=60 * 5)
self._addSummaryEntry("User:", local_server_settings["username"])
def _saveSettingsCallback(self, result, error=False, **kwargs):
if error:
@@ -296,112 +175,45 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
Controller.instance().setDisplayError(True)
if self.currentPage() == self.uiVMWizardPage:
vmname = self.uiVMListComboBox.currentText()
if vmname:
# save the GNS3 VM settings
vm_settings = self._GNS3VMSettings()
vm_settings["enable"] = True
vm_settings["vmname"] = vmname
if self.currentPage() == self.uiLocalControllerWizardPage:
if self.uiVmwareRadioButton.isChecked():
vm_settings["engine"] = "vmware"
elif self.uiVirtualBoxRadioButton.isChecked():
vm_settings["engine"] = "virtualbox"
local_controller_settings = LocalServer.instance().localServerSettings()
local_controller_settings["auto_start"] = True
local_controller_settings["remote"] = False
local_controller_settings["path"] = self.uiLocalServerPathLineEdit.text().strip()
local_controller_settings["host"] = self.uiLocalServerHostComboBox.itemData(self.uiLocalServerHostComboBox.currentIndex())
local_controller_settings["port"] = self.uiLocalServerPortSpinBox.value()
# 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 memory to the GNS3 VM")
vm_settings["vcpus"] = vpcus
vm_settings["ram"] = ram
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.")
if not os.path.isfile(local_controller_settings["path"]):
QtWidgets.QMessageBox.critical(self, "Local server", "Could not find local server {}".format(local_controller_settings["path"]))
return False
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"]))
if not os.access(local_controller_settings["path"], os.X_OK):
QtWidgets.QMessageBox.critical(self, "Local server", "{} is not an executable".format(local_controller_settings["path"]))
return False
LocalServer.instance().updateLocalServerSettings(local_server_settings)
LocalServer.instance().updateLocalServerSettings(local_controller_settings)
# start and connect to the controller if required
if not LocalServer.instance().localServerAutoStartIfRequired():
return False
elif self.currentPage() == self.uiRemoteControllerWizardPage:
local_server_settings = LocalServer.instance().localServerSettings()
local_server_settings["auto_start"] = False
local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
local_server_settings["protocol"] = "http"
local_server_settings["user"] = self.uiRemoteMainServerUserLineEdit.text()
local_server_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
local_server_settings["auth"] = self.uiRemoteMainServerAuthCheckBox.isChecked()
LocalServer.instance().updateLocalServerSettings(local_server_settings)
elif self.currentPage() == self.uiSummaryWizardPage:
if self.uiLocalRadioButton.isChecked():
# deactivate the GNS3 VM if using the local server
vm_settings = self._GNS3VMSettings()
vm_settings["enable"] = False
self._setGNS3VMSettings(vm_settings)
elif self.currentPage() == self.uiLocalServerStatusWizardPage:
if not Controller.instance().connected():
return False
remote_controller_settings = Controller.instance().settings()
remote_controller_settings["auto_start"] = False
remote_controller_settings["remote"] = True
remote_controller_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
remote_controller_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
remote_controller_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText().lower()
remote_controller_settings["accept_insecure_ssl_certificate"] = False
remote_controller_settings["username"] = self.uiRemoteMainServerUserLineEdit.text()
remote_controller_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
Controller.instance().setSettings(remote_controller_settings)
Controller.instance().connect()
return Controller.instance().connected()
return True
def _refreshVMListSlot(self):
"""
Refresh the list of VM available in VMware or VirtualBox.
"""
if self.uiVmwareRadioButton.isChecked():
Controller.instance().get("/gns3vm/engines/vmware/vms", self._getVMsFromServerCallback, progressText="Retrieving VMware VM list from server...")
elif self.uiVirtualBoxRadioButton.isChecked():
Controller.instance().get("/gns3vm/engines/virtualbox/vms", self._getVMsFromServerCallback, progressText="Retrieving VirtualBox VM list from server...")
def _getVMsFromServerCallback(self, result, error=False, **kwargs):
"""
Callback for getVMsFromServer.
:param progress_dialog: QProgressDialog instance
:param result: server response
:param error: indicates an error (boolean)
"""
if error:
QtWidgets.QMessageBox.critical(self, "VM List", "{}".format(result["message"]))
else:
self.uiVMListComboBox.clear()
for vm in result:
self.uiVMListComboBox.addItem(vm["vmname"])
index = self.uiVMListComboBox.findText(self._GNS3VMSettings()["vmname"])
if index != -1:
self.uiVMListComboBox.setCurrentIndex(index)
else:
index = self.uiVMListComboBox.findText("GNS3 VM")
if index != -1:
self.uiVMListComboBox.setCurrentIndex(index)
else:
QtWidgets.QMessageBox.critical(self, "GNS3 VM", "Could not find a VM named 'GNS3 VM', is it imported in VMware or VirtualBox?")
def done(self, result):
"""
This dialog is closed.
@@ -415,9 +227,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
settings["hide_setup_wizard"] = True
else:
local_server_settings = LocalServer.instance().localServerSettings()
if local_server_settings["host"] is None:
local_server_settings["host"] = DEFAULT_LOCAL_SERVER_HOST
LocalServer.instance().updateLocalServerSettings(local_server_settings)
LocalServer.instance().updateLocalServerSettings(local_server_settings)
settings["hide_setup_wizard"] = not self.uiShowCheckBox.isChecked()
self.parentWidget().setSettings(settings)
super().done(result)
@@ -428,20 +238,17 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
current_id = self.currentId()
if self.page(current_id) == self.uiLocalServerStatusWizardPage and not self.uiVMRadioButton.isChecked():
if self.page(current_id) == self.uiLocalControllerWizardPage and self.uiLocalControllerRadioButton.isChecked():
return self._pageId(self.uiSummaryWizardPage)
if self.page(current_id) == self.uiServerWizardPage and self.uiRemoteControllerRadioButton.isChecked():
if self.page(current_id) == self.uiControllerWizardPage and self.uiRemoteControllerRadioButton.isChecked():
return self._pageId(self.uiRemoteControllerWizardPage)
if self.page(current_id) == self.uiVMWizardPage:
return self._pageId(self.uiSummaryWizardPage)
return QtWidgets.QWizard.nextId(self)
def _pageId(self, page):
"""
Return id of the page
"""
for id in self.pageIds():
if self.page(id) == page:
return id

View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 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 QtCore, QtGui, QtWidgets
from gns3.ui.show_readme_dialog_ui import Ui_ShowReadmeDialog
from gns3.utils import parse_version
import logging
log = logging.getLogger(__name__)
class ShowReadmeDialog(QtWidgets.QDialog, Ui_ShowReadmeDialog):
def __init__(self, project, path, content=None, parent=None):
if parent is None:
from gns3.main_window import MainWindow
parent = MainWindow.instance()
super().__init__(parent)
self.setupUi(self)
self._project = project
self._path = path
self.setWindowTitle(project.name() + " " + os.path.basename(path))
self.uiRefreshButton.pressed.connect(self._refreshSlot)
# QTextDocument before Qt version 5.14 doesn't support Markdown
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.14.0") or parse_version(QtCore.PYQT_VERSION_STR) < parse_version("5.14.0"):
self._markdown = False
else:
self._markdown = True
self._document = QtGui.QTextDocument()
self.uiTextBrowser.setDocument(self._document)
if content:
if self._markdown:
self._document.setMarkdown(content)
else:
self._document.setPlainText(content)
else:
self._refreshSlot()
def _refreshSlot(self):
self._project.get("/files/" + self._path, self._getCallback, raw=True)
def _getCallback(self, result, error=False, **kwargs):
if not error:
content = result.decode("utf-8", errors="ignore")
if self._markdown:
self._document.setMarkdown(content)
else:
self._document.setPlainText(content)

View File

@@ -68,7 +68,7 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
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"])
item.setData(QtCore.Qt.ItemDataRole.UserRole, snapshot["snapshot_id"])
if self.uiSnapshotsList.count():
self.uiSnapshotsList.setCurrentRow(0)
@@ -83,13 +83,16 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
Slot to create a snapshot.
"""
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.Normal, "Unnamed")
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.EchoMode.Normal, "Unnamed")
if ok and snapshot_name and self._project:
Controller.instance().post("/projects/{}/snapshots".format(self._project.id()),
self._createSnapshotsCallback,
{"name": snapshot_name},
progressText="Creation of snapshot '{}' in progress...".format(snapshot_name),
timeout=None)
Controller.instance().post(
"/projects/{}/snapshots".format(self._project.id()),
self._createSnapshotsCallback,
{"name": snapshot_name},
progress_text="Creation of snapshot '{}' in progress...".format(snapshot_name),
timeout=None,
wait=True
)
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
@@ -107,7 +110,7 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
item = self.uiSnapshotsList.currentItem()
if item:
snapshot_id = item.data(QtCore.Qt.UserRole)
snapshot_id = item.data(QtCore.Qt.ItemDataRole.UserRole)
Controller.instance().delete("/projects/{}/snapshots/{}".format(self._project.id(), snapshot_id), self._deleteSnapshotsCallback)
def _deleteSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
@@ -125,7 +128,7 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
item = self.uiSnapshotsList.currentItem()
if item:
snapshot_id = item.data(QtCore.Qt.UserRole)
snapshot_id = item.data(QtCore.Qt.ItemDataRole.UserRole)
self._restoreSnapshot(snapshot_id)
def _restoreSnapshot(self, snapshot_id):
@@ -135,12 +138,17 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
:param snapshot_id: id of the snapshot
"""
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken, would you like to proceed?", QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel)
if reply == QtWidgets.QMessageBox.Cancel:
reply = QtWidgets.QMessageBox.question(self, "Snapshots", "This will discard any changes made to your project since the snapshot was taken, would you like to proceed?", QtWidgets.QMessageBox.StandardButton.Ok, QtWidgets.QMessageBox.StandardButton.Cancel)
if reply == QtWidgets.QMessageBox.StandardButton.Cancel:
return
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id),
self._restoreSnapshotsCallback, progressText="Restoring snapshot...", timeout=None)
Controller.instance().post(
"/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id),
self._restoreSnapshotsCallback,
progress_text="Restoring snapshot...",
timeout=None,
wait=True
)
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
@@ -155,5 +163,5 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
Slot to restore a snapshot when it is double clicked.
"""
snapshot_id = item.data(QtCore.Qt.UserRole)
snapshot_id = item.data(QtCore.Qt.ItemDataRole.UserRole)
self._restoreSnapshot(snapshot_id)

View File

@@ -22,6 +22,8 @@ Style editor to edit Shape items.
from ..qt import QtCore, QtWidgets, QtGui
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
from ..items.shape_item import ShapeItem
from ..items.line_item import LineItem
from ..items.rectangle_item import RectangleItem
class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
@@ -41,14 +43,15 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
self._items = items
self.uiColorPushButton.clicked.connect(self._setColorSlot)
self.uiBorderColorPushButton.clicked.connect(self._setBorderColorSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).clicked.connect(self._applyPreferencesSlot)
self.uiBorderStyleComboBox.addItem("Solid", QtCore.Qt.SolidLine)
self.uiBorderStyleComboBox.addItem("Dash", QtCore.Qt.DashLine)
self.uiBorderStyleComboBox.addItem("Dot", QtCore.Qt.DotLine)
self.uiBorderStyleComboBox.addItem("Dash Dot", QtCore.Qt.DashDotLine)
self.uiBorderStyleComboBox.addItem("Dash Dot Dot", QtCore.Qt.DashDotDotLine)
self.uiBorderStyleComboBox.addItem("No border", QtCore.Qt.NoPen)
self.uiBorderStyleComboBox.addItem("Solid", QtCore.Qt.PenStyle.SolidLine)
self.uiBorderStyleComboBox.addItem("Dash", QtCore.Qt.PenStyle.DashLine)
self.uiBorderStyleComboBox.addItem("Dot", QtCore.Qt.PenStyle.DotLine)
self.uiBorderStyleComboBox.addItem("Dash Dot", QtCore.Qt.PenStyle.DashDotLine)
self.uiBorderStyleComboBox.addItem("Dash Dot Dot", QtCore.Qt.PenStyle.DashDotDotLine)
if True not in list(map(lambda item: isinstance(item, LineItem), items)):
self.uiBorderStyleComboBox.addItem("No border", QtCore.Qt.PenStyle.NoPen)
# use the first item in the list as the model
first_item = items[0]
@@ -70,8 +73,27 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
self._border_color.green(),
self._border_color.blue(),
self._border_color.alpha()))
self.uiRotationSpinBox.setValue(first_item.rotation())
if isinstance(first_item, RectangleItem):
# use the horizontal corner radius first and then the vertical one if it's not set
# maybe we allow configuring them separately in the future
corner_radius = first_item.horizontalCornerRadius()
if not corner_radius:
corner_radius = first_item.verticalCornerRadius()
self.uiCornerRadiusSpinBox.setValue(corner_radius)
else:
self.uiCornerRadiusLabel.hide()
self.uiCornerRadiusSpinBox.hide()
self.uiRotationSpinBox.setValue(int(first_item.rotation()))
self.uiBorderWidthSpinBox.setValue(pen.width())
if isinstance(first_item, ShapeItem):
rect = first_item.rect()
self.uiWidthSpinBox.setValue(int(rect.width()))
self.uiHeightSpinBox.setValue(int(rect.height()))
else:
self.uiWidthSpinBox.hide()
self.uiWidthLabel.hide()
self.uiHeightSpinBox.hide()
self.uiHeightLabel.hide()
index = self.uiBorderStyleComboBox.findData(pen.style())
if index != -1:
self.uiBorderStyleComboBox.setCurrentIndex(index)
@@ -81,7 +103,7 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
Slot to select the filling color.
"""
color = QtWidgets.QColorDialog.getColor(self._color, self, "Select Color", QtWidgets.QColorDialog.ShowAlphaChannel)
color = QtWidgets.QColorDialog.getColor(self._color, self, "Select Color", QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel)
if color.isValid():
self._color = color
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
@@ -94,7 +116,7 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
Slot to select the border color.
"""
color = QtWidgets.QColorDialog.getColor(self._border_color, self, "Select Color", QtWidgets.QColorDialog.ShowAlphaChannel)
color = QtWidgets.QColorDialog.getColor(self._border_color, self, "Select Color", QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel)
if color.isValid():
self._border_color = color
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
@@ -108,7 +130,7 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
"""
border_style = QtCore.Qt.PenStyle(self.uiBorderStyleComboBox.itemData(self.uiBorderStyleComboBox.currentIndex()))
pen = QtGui.QPen(self._border_color, self.uiBorderWidthSpinBox.value(), border_style, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
pen = QtGui.QPen(self._border_color, self.uiBorderWidthSpinBox.value(), border_style, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
if self._color:
brush = QtGui.QBrush(self._color)
else:
@@ -116,10 +138,18 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
for item in self._items:
item.setPen(pen)
# on multiselection it's possible to select many type of items
# on multi-selection it's possible to select many type of items
# but brush can be applied only on ShapeItem,
if brush and isinstance(item, ShapeItem):
item.setBrush(brush)
if isinstance(item, RectangleItem):
corner_radius = self.uiCornerRadiusSpinBox.value()
# use the corner radius for both horizontal (rx) and vertical (ry)
# maybe we support setting them separately in the future
item.setHorizontalCornerRadius(corner_radius)
item.setVerticalCornerRadius(corner_radius)
if isinstance(item, ShapeItem):
item.setWidthAndHeight(self.uiWidthSpinBox.value(), self.uiHeightSpinBox.value())
item.setRotation(self.uiRotationSpinBox.value())
def done(self, result):

View File

@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Pekka Helenius
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Style editor to edit Link items.
"""
from ..qt import QtCore, QtWidgets, QtGui
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
"""
Style editor dialog.
:param parent: parent widget
:param link: selected link
"""
def __init__(self, link, parent):
super().__init__(parent)
self.setupUi(self)
self._link = link
self._link_style = {}
self.uiBorderColorPushButton.clicked.connect(self._setBorderColorSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).clicked.connect(self._applyPreferencesSlot)
self.uiBorderStyleComboBox.addItem("Solid", QtCore.Qt.PenStyle.SolidLine)
self.uiBorderStyleComboBox.addItem("Dash", QtCore.Qt.PenStyle.DashLine)
self.uiBorderStyleComboBox.addItem("Dot", QtCore.Qt.PenStyle.DotLine)
self.uiBorderStyleComboBox.addItem("Dash Dot", QtCore.Qt.PenStyle.DashDotLine)
self.uiBorderStyleComboBox.addItem("Dash Dot Dot", QtCore.Qt.PenStyle.DashDotDotLine)
self.uiBorderStyleComboBox.addItem("Invisible", QtCore.Qt.PenStyle.NoPen)
self.uiColorLabel.hide()
self.uiColorPushButton.hide()
self._color = None
self.uiCornerRadiusLabel.hide()
self.uiCornerRadiusSpinBox.hide()
self.uiRotationLabel.hide()
self.uiRotationSpinBox.hide()
link.setHovered(False) # make sure we use the right style
pen = link.pen()
link.setHovered(True)
self._border_color = pen.color()
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
self._border_color.green(),
self._border_color.blue(),
self._border_color.alpha()))
self.uiBorderWidthSpinBox.setValue(pen.width())
index = self.uiBorderStyleComboBox.findData(pen.style())
if index != -1:
self.uiBorderStyleComboBox.setCurrentIndex(index)
self.adjustSize()
def _setBorderColorSlot(self):
"""
Slot to select the border color.
"""
color = QtWidgets.QColorDialog.getColor(self._border_color, self, "Select Color", QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel)
if color.isValid():
self._border_color = color
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
self._border_color.green(),
self._border_color.blue(),
self._border_color.alpha()))
def _applyPreferencesSlot(self):
"""
Applies the new style settings.
"""
border_style = QtCore.Qt.PenStyle(self.uiBorderStyleComboBox.itemData(self.uiBorderStyleComboBox.currentIndex()))
pen = QtGui.QPen(self._border_color, self.uiBorderWidthSpinBox.value(), border_style, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
self._link.setPen(pen)
new_link_style = {}
new_link_style["color"] = self._border_color.name()
new_link_style["width"] = self.uiBorderWidthSpinBox.value()
new_link_style["type"] = border_style
# Store values
self._link.setLinkStyle(new_link_style)
self._link.setHovered(False) # allow to see the new style
def done(self, result):
"""
Called when the dialog is closed.
:param result: boolean (accepted or rejected)
"""
if result:
self._applyPreferencesSlot()
super().done(result)

View File

@@ -50,16 +50,16 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
self.setupUi(self)
self._items = items
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).clicked.connect(self._applyPreferencesSlot)
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
self.uiCustomSymbolRadioButton.toggled.connect(self._customSymbolToggledSlot)
self.uiBuiltInSymbolRadioButton.toggled.connect(self._builtInSymbolToggledSlot)
self.uiSearchLineEdit.textChanged.connect(self._searchTextChangedSlot)
if not SymbolSelectionDialog._symbols_dir:
SymbolSelectionDialog._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
SymbolSelectionDialog._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.PicturesLocation)
if not self._items:
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).hide()
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).hide()
self.uiBuiltInSymbolRadioButton.setChecked(True)
self.uiSymbolTreeWidget.setFocus()
@@ -67,6 +67,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
self._symbol_items = []
self._parents = {}
Controller.instance().clearStaticCache() # TODO: use etag to know when to refresh the cache
Controller.instance().get("/symbols", self._listSymbolsCallback)
def _listSymbolsCallback(self, result, error=False, **kwargs):
@@ -84,14 +85,14 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
font = parent.font(0)
font.setBold(True)
parent.setFont(0, font)
parent.setFlags(parent.flags() & ~QtCore.Qt.ItemIsSelectable)
parent.setFlags(parent.flags() & ~QtCore.Qt.ItemFlag.ItemIsSelectable)
self._parents[theme] = parent
else:
parent = self._parents[theme]
name = os.path.splitext(symbol.filename())[0]
item = QtWidgets.QTreeWidgetItem(parent)
item.setData(0, QtCore.Qt.UserRole, symbol)
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, symbol)
item.setToolTip(0, symbol.id())
self._symbol_items.append(item)
item.setText(0, name)
@@ -100,7 +101,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
if sip_is_deleted(item):
return
svg_renderer = QImageSvgRenderer(path)
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
image = QtGui.QImage(64, 64, QtGui.QImage.Format.Format_ARGB32)
# Set the ARGB to 0 to prevent rendering artifacts
image.fill(0x00000000)
svg_renderer.render(QtGui.QPainter(image))
@@ -108,6 +109,9 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
item.setIcon(0, icon)
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
for parent in self._parents.values():
parent.sortChildren(0, QtCore.Qt.SortOrder.AscendingOrder)
self.adjustSize()
def _searchTextChangedSlot(self, text):
@@ -119,13 +123,13 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
"""
text = self.uiSearchLineEdit.text()
for item in self._symbol_items:
if not item.data(0, QtCore.Qt.UserRole).builtin():
item.setHidden(True)
# if not item.data(0, QtCore.Qt.ItemDataRole.UserRole).builtin():
# item.setHidden(True)
# else:
if not text.strip() or text.strip().lower() in item.text(0).lower():
item.setHidden(False)
else:
if len(text.strip()) == 0 or text.strip().lower() in item.text(0).lower():
item.setHidden(False)
else:
item.setHidden(True)
item.setHidden(True)
def _customSymbolToggledSlot(self, checked):
"""
@@ -172,7 +176,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
if self.uiSymbolTreeWidget.isEnabled():
current = self.uiSymbolTreeWidget.currentItem()
if current and current.parent():
return current.data(0, QtCore.Qt.UserRole).id()
return current.data(0, QtCore.Qt.ItemDataRole.UserRole).id()
else:
return os.path.basename(self.uiSymbolLineEdit.text())
return None
@@ -180,14 +184,21 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
def _symbolBrowserSlot(self):
# supported image file formats
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.pbm *.pgm *.png *.ppm *.xbm *.xpm *.gif);;All files (*.*)"
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)
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)
Controller.instance().post(
"/symbols/" + symbol_id + "/raw",
qpartial(self._finishSymbolUpload, path),
body=pathlib.Path(path),
progress_text="Uploading {}".format(symbol_id),
timeout=None,
wait=True
)
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
if error:

View File

@@ -39,17 +39,17 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
self._items = items
self.uiFontPushButton.clicked.connect(self._setFontSlot)
self.uiColorPushButton.clicked.connect(self._setColorSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).clicked.connect(self._applyPreferencesSlot)
# use the first item in the list as the model
first_item = items[0]
self._setColor(first_item.defaultTextColor())
self.uiRotationSpinBox.setValue(first_item.rotation())
self.uiRotationSpinBox.setValue(int(first_item.rotation()))
self.uiPlainTextEdit.setPlainText(first_item.toPlainText())
self.uiPlainTextEdit.setFont(first_item.font())
if not first_item.editable():
self.uiPlainTextEdit.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
self.uiPlainTextEdit.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction)
if len(self._items) == 1:
self.uiApplyColorToAllItemsCheckBox.setChecked(True)
@@ -77,7 +77,7 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
"""
selected_font, ok = QtWidgets.QFontDialog.getFont(self.uiPlainTextEdit.font(), self,
options=QtWidgets.QFontDialog.DontUseNativeDialog)
options=QtWidgets.QFontDialog.FontDialogOption.DontUseNativeDialog)
if ok:
self.uiPlainTextEdit.setFont(selected_font)
@@ -87,7 +87,7 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
Slot to select the color.
"""
color = QtWidgets.QColorDialog.getColor(self._color, self, None, QtWidgets.QColorDialog.ShowAlphaChannel)
color = QtWidgets.QColorDialog.getColor(self._color, self, None, QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel)
if color.isValid():
self._setColor(color)

View File

@@ -88,9 +88,10 @@ class VMWithImagesWizard(VMWizard):
self._radio_existing_images_buttons.add(radio_button)
def _imageCreateSlot(self, line_edit, create_image_wizard, image_suffix):
create_dialog = create_image_wizard(self, self.getSettings()["compute_id"], self.uiNameLineEdit.text() + image_suffix)
if QtWidgets.QDialog.Accepted == create_dialog.exec_():
line_edit.setText(create_dialog.uiLocationLineEdit.text())
create_dialog = create_image_wizard(self, Controller.instance(), self.uiNameLineEdit.text() + image_suffix)
if create_dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted:
line_edit.setText(create_dialog.uiDiskFilenameLineEdit.text())
def _imageBrowserSlot(self, line_edit, image_selector):
"""
@@ -135,14 +136,17 @@ class VMWithImagesWizard(VMWizard):
if create_button:
create_button.show()
def loadImagesList(self, endpoint):
def loadImagesList(self, image_type):
"""
Fill the list box with available Images"
Fill the list box with available images
:param endpoint: server endpoint with the list of Images
:param image_type: image type (qemu, iou or ios)
"""
Controller.instance().getCompute(endpoint, self._compute_id, self._getImagesFromServerCallback)
params = None
if image_type:
params = {"image_type": image_type}
Controller.instance().get("/images", self._getImagesFromServerCallback, params=params)
def _getImagesFromServerCallback(self, result, error=False, **kwargs):
"""
@@ -179,7 +183,7 @@ class VMWithImagesWizard(VMWizard):
if self._widgetOnCurrentPage(combo_box):
combo_box.clear()
for vm in result:
combo_box.addItem(vm["path"], vm)
combo_box.addItem(vm["filename"], vm)
def _widgetOnCurrentPage(self, widget):
"""

View File

@@ -37,12 +37,13 @@ class VMWizard(QtWidgets.QWizard):
self.setModal(True)
self._devices = devices
self._allow_dynamic_compute_allocation = True
self._local_server_disable = False
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
self.setWizardStyle(QtWidgets.QWizard.WizardStyle.ModernStyle)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
self.setOptions(QtWidgets.QWizard.WizardOption.NoDefaultButton)
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
if hasattr(self, "uiVMRadioButton"):
@@ -102,6 +103,8 @@ class VMWizard(QtWidgets.QWizard):
if hasattr(self, "uiVMRadioButton"):
self.uiVMRadioButton.setEnabled(False)
self.uiLocalRadioButton.setEnabled(False)
if self._allow_dynamic_compute_allocation:
self.uiRemoteServersComboBox.addItem("Any server", None)
for compute in ComputeManager.instance().computes():
if compute.id() == "local":
self.uiLocalRadioButton.setEnabled(True)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

58
gns3/http_client_error.py Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python
#
# Copyright (C) 2021 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/>.
class HttpClientError(Exception):
def __init__(self, message: str):
super().__init__()
self._message = message
def __repr__(self):
return self._message
def __str__(self):
return self._message
class HttpClientNotFoundError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)
class HttpClientCancelledRequestError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)
class HttpClientBadRequestError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)
class HttpClientUnauthorizedError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)
class HttpClientForbiddenError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)
class HttpClientTimeoutError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)

View File

@@ -20,11 +20,10 @@ import copy
import pathlib
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
from gns3.http_client_error import HttpClientError, HttpClientCancelledRequestError
from gns3.registry.image import Image
@@ -69,7 +68,7 @@ class ImageManager:
"""
if (server and server != "local") or Controller.instance().isRemote():
return self._uploadImageToRemoteServer(source_path, server, node_type)
return self._uploadImageToRemoteServer(source_path, node_type, parent)
else:
destination_directory = self.getDirectoryForType(node_type)
destination_path = os.path.join(destination_directory, os.path.basename(source_path))
@@ -94,9 +93,9 @@ class ImageManager:
reply = QtWidgets.QMessageBox.question(parent,
'Image',
'Would you like to copy {} to the default images directory'.format(source_filename),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
try:
os.makedirs(destination_directory, exist_ok=True)
except OSError as e:
@@ -106,7 +105,7 @@ class ImageManager:
worker = FileCopyWorker(source_path, destination_path)
progress_dialog = ProgressDialog(worker, 'Image', 'Copying {}'.format(source_filename), 'Cancel', busy=True, parent=parent)
progress_dialog.show()
progress_dialog.exec_()
progress_dialog.exec()
errors = progress_dialog.errors()
if errors:
QtWidgets.QMessageBox.critical(parent, 'Image', '{}'.format(''.join(errors)))
@@ -115,27 +114,45 @@ class ImageManager:
source_path = destination_path
return source_path
def _uploadImageToRemoteServer(self, path, server, node_type):
def _uploadImageToRemoteServer(self, path, node_type, parent):
"""
Upload image to remote server
:param path: File path on computer
:param server: The server where the images should be located
:param node_type: Image node_type
:returns path: Final path
"""
if node_type == 'QEMU':
upload_endpoint = '/qemu/images'
image_type = 'qemu'
elif node_type == 'IOU':
upload_endpoint = '/iou/images'
image_type = 'iou'
elif node_type == 'DYNAMIPS':
upload_endpoint = '/dynamips/images'
image_type = 'ios'
else:
raise Exception('Invalid node type')
filename = self._getRelativeImagePath(path, node_type).replace("\\", "/")
Controller.instance().postCompute('{}/{}'.format(upload_endpoint, filename), server, None, body=pathlib.Path(path), progressText="Uploading {}".format(filename), timeout=None)
try:
Controller.instance().post(
f"/images/upload/{filename}",
callback=None,
params={"image_type": image_type},
body=pathlib.Path(path),
progress_text="Uploading {}".format(filename),
timeout=None,
wait=True
)
except HttpClientCancelledRequestError:
return
except HttpClientError as e:
QtWidgets.QMessageBox.critical(
parent,
"Image upload",
f"Could not upload image {filename}: {e}"
)
return filename
def _getRelativeImagePath(self, path, node_type):
@@ -164,7 +181,7 @@ class ImageManager:
:returns: path to the default images directory
"""
return copy.copy(LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)['images_path'])
return copy.copy(Controller.instance().settings()['images_path'])
def getDirectoryForType(self, node_type):
"""

View File

@@ -17,9 +17,11 @@
import os
import pathlib
import urllib.parse
from gns3.http_client import HTTPClient
from gns3.http_client_error import HttpClientError, HttpClientCancelledRequestError
from gns3.qt import QtWidgets
from gns3.registry.image import Image
from gns3.controller import Controller
import logging
log = logging.getLogger(__name__)
@@ -27,64 +29,42 @@ log = logging.getLogger(__name__)
class ImageUploadManager(object):
"""
Manager over the image upload. Encapsulates file uploads to computes or via controller.
Manager over the image upload
"""
def __init__(self, image, controller, compute_id, callback=None, directFileUpload=False):
self._image = image
self._compute_id = compute_id
self._callback = callback
self._directFileUpload = directFileUpload
self._controller = controller
def __init__(self, image: Image, controller: Controller, parent: QtWidgets.QWidget):
self._image = image
self._controller = controller
self._parent = parent
def upload(self) -> bool:
def upload(self):
if not os.path.exists(self._image.path):
log.error("Image '{}' could not be found".format(self._image.path))
return
if self._directFileUpload:
# first obtain endpoint and know when target request
self._controller.getEndpoint(self._getComputePath(), self._compute_id, self._onLoadEndpointCallback, showProgress=False)
else:
self._fileUploadToController()
return False
return self._fileUploadToController()
def _getComputePath(self):
return '/{emulator}/images/{filename}'.format(emulator=self._image.emulator, filename=self._image.filename)
def _fileUploadToController(self) -> bool:
def _onLoadEndpointCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
log.error("Error while getting endpoint: {}".format(result["message"]))
return
# we know where is the endpoint and we trying to post there a file
endpoint = result['endpoint']
self._fileUploadToCompute(endpoint)
def _checkIfSuccessfulCallback(self, result, error=False, **kwargs):
if error:
connection_error = kwargs.get('connection_error', False)
if connection_error:
log.debug("During direct file upload compute is not visible. Fallback to upload via controller.")
# there was an issue with connection, probably we don't have a direct access to compute
# we need to fallback to uploading files via controller
self._fileUploadToController()
else:
if "message" in result:
log.error("Error while direct file upload: {}".format(result["message"]))
return
self._callback(result, error, **kwargs)
def _fileUploadToCompute(self, endpoint):
log.debug("Uploading image '{}' to compute".format(self._image.path))
parse_results = urllib.parse.urlparse(endpoint)
network_manager = self._controller.getHttpClient().getNetworkManager()
client = HTTPClient.fromUrl(endpoint, network_manager=network_manager)
# We don't retry connection as in case of fail we try direct file upload
client.setMaxRetryConnection(0)
client.createHTTPQuery('POST', parse_results.path, self._checkIfSuccessfulCallback, body=pathlib.Path(self._image.path),
context={"image_path": self._image.path}, progressText="Uploading {}".format(self._image.filename), timeout=None, prefix="")
def _fileUploadToController(self):
log.debug("Uploading image '{}' to controller".format(self._image.path))
self._controller.postCompute(self._getComputePath(), self._compute_id, self._callback, body=pathlib.Path(self._image.path),
context={"image_path": self._image.path}, progressText="Uploading {}".format(self._image.filename), timeout=None)
log.info("Uploading image '{}' to controller".format(self._image.filename))
try:
self._controller.post(
f"/images/upload/{self._image.filename}",
callback=None,
body=pathlib.Path(self._image.path),
context={"image_path": self._image.path},
progress_text="Uploading {}".format(self._image.filename),
timeout=None,
wait=True
)
except HttpClientCancelledRequestError:
return False
except HttpClientError as e:
QtWidgets.QMessageBox.critical(
self._parent,
"Image upload to controller",
f"Could not upload image {self._image.filename}: {e}"
)
return False
return True

View File

@@ -27,12 +27,12 @@ log = logging.getLogger(__name__)
class DrawingItem:
# Map QT stroke to SVG style
QT_DASH_TO_SVG = {
QtCore.Qt.SolidLine: "",
QtCore.Qt.NoPen: None,
QtCore.Qt.DashLine: "25, 25",
QtCore.Qt.DotLine: "5, 25",
QtCore.Qt.DashDotLine: "5, 25, 25",
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
QtCore.Qt.PenStyle.SolidLine: "none",
QtCore.Qt.PenStyle.NoPen: None,
QtCore.Qt.PenStyle.DashLine: "25, 25",
QtCore.Qt.PenStyle.DotLine: "5, 25",
QtCore.Qt.PenStyle.DashDotLine: "5, 25, 25",
QtCore.Qt.PenStyle.DashDotDotLine: "25, 25, 5, 25, 5"
}
show_layer = False
@@ -44,10 +44,11 @@ class DrawingItem:
def __init__(self, project=None, pos=None, drawing_id=None, svg=None, z=0, locked=False, rotation=0, **kws):
self._id = drawing_id
self._deleting = False
self._allow_snap_to_grid = True
self._locked = locked
if self._id is None:
self._id = str(uuid.uuid4())
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
self.setFlags(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
from ..main_window import MainWindow
self._graphics_view = MainWindow.instance().uiGraphicsView
@@ -56,7 +57,7 @@ class DrawingItem:
self._project = project
# Store a hash of the SVG to avoid him
# to be send if he doesn't change
# to be sent if he doesn't change
self._hash_svg = None
if pos:
@@ -92,7 +93,7 @@ class DrawingItem:
def updateDrawing(self):
if self._id and not self.deleting() and self._project:
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__(), showProgress=False)
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__(), show_progress=False)
@qslot
def updateDrawingCallback(self, result, error=False, **kwargs):
@@ -107,7 +108,7 @@ class DrawingItem:
if error:
log.error("Error while updating drawing: {}".format(result["message"]))
return False
self.setPos(QtCore.QPoint(result["x"], result["y"]))
self.setPos(QtCore.QPointF(result["x"], result["y"]))
self.setZValue(result["z"])
self.setLocked(result["locked"])
self.setRotation(result["rotation"])
@@ -123,18 +124,21 @@ class DrawingItem:
"""
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 key in (QtCore.Qt.Key.Key_P, QtCore.Qt.Key.Key_Plus, QtCore.Qt.Key.Key_Equal) and modifiers & QtCore.Qt.KeyboardModifier.AltModifier \
or key == QtCore.Qt.Key.Key_Plus and modifiers & QtCore.Qt.KeyboardModifier.AltModifier and modifiers & QtCore.Qt.KeyboardModifier.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:
elif key in (QtCore.Qt.Key.Key_M, QtCore.Qt.Key.Key_Minus) and modifiers & QtCore.Qt.KeyboardModifier.AltModifier \
or key == QtCore.Qt.Key.Key_Minus and modifiers & QtCore.Qt.KeyboardModifier.AltModifier and modifiers & QtCore.Qt.KeyboardModifier.KeypadModifier:
if self.rotation() < 360.0:
self.setRotation(self.rotation() + 1)
return True
elif modifiers & QtCore.Qt.KeyboardModifier.AltModifier:
self._allow_snap_to_grid = False
return True
return False
def keyPressEvent(self, event):
@@ -147,6 +151,15 @@ class DrawingItem:
if not self.handleKeyPressEvent(event):
QtWidgets.QGraphicsItem.keyPressEvent(self, event)
def keyReleaseEvent(self, event):
"""
Handles all key release events
:param event: QKeyEvent
"""
self._allow_snap_to_grid = True
def __json__(self):
data = {
"drawing_id": self._id,
@@ -178,9 +191,9 @@ class DrawingItem:
"""
if locked is True:
self.setFlag(self.ItemIsMovable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
else:
self.setFlag(self.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
self._locked = locked
def deleting(self):
@@ -212,16 +225,14 @@ class DrawingItem:
self._project.delete("/drawings/" + self._id, None, body=self.__json__())
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
grid_size = self._graphics_view.drawingGridSize()
mid_x = self.boundingRect().width() / 2
tmp_x = (grid_size * round((self.x() + mid_x) / grid_size)) - mid_x
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 change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._main_window.uiSnapToGridAction.isChecked() \
and self._allow_snap_to_grid:
grid_size = self._graphics_view.drawingGridSize()
value.setX(grid_size * round(value.x() / grid_size))
value.setY(grid_size * round(value.y() / grid_size))
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemSelectedChange:
if not value:
self.updateDrawing()
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
@@ -245,10 +256,10 @@ class DrawingItem:
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)
painter.setBrush(QtCore.Qt.GlobalColor.red)
painter.setPen(QtCore.Qt.GlobalColor.red)
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
painter.setPen(QtCore.Qt.GlobalColor.black)
zval = str(int(self.zValue()))
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
@@ -287,9 +298,9 @@ class DrawingItem:
pen.setColor(colorFromSvg(svg.get("stroke")))
# Map SVG stroke style (border of the element to the Qt version)
if not svg.get("stroke"):
pen.setStyle(QtCore.Qt.NoPen)
pen.setStyle(QtCore.Qt.PenStyle.NoPen)
else:
pen.setStyle(QtCore.Qt.SolidLine)
pen.setStyle(QtCore.Qt.PenStyle.SolidLine)
stroke = svg.get("stroke-dasharray")
if stroke:
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Pekka Helenius
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
@@ -51,10 +52,16 @@ class EthernetLinkItem(LinkItem):
LinkItem.adjust(self)
if self._hovered:
self.setPen(QtGui.QPen(QtCore.Qt.red, self._pen_width + 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
else:
self.setPen(QtGui.QPen(QtCore.Qt.black, self._pen_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
try:
if self._hovered:
self.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.red, self._link._link_style["width"] + 1, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
else:
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], self._link._link_style["type"], QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
except:
if self._hovered:
self.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.red, self._pen_width + 1, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
else:
self.setPen(QtGui.QPen(QtGui.QColor("#000000"), self._pen_width, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
# draw a line between nodes
path = QtGui.QPainterPath(self.source)
@@ -114,17 +121,17 @@ class EthernetLinkItem(LinkItem):
if self._link.suspended() or self._source_port.status() == Port.suspended:
# link or port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.GlobalColor.yellow
shape = QtCore.Qt.PenCapStyle.RoundCap
elif self._source_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.GlobalColor.green
shape = QtCore.Qt.PenCapStyle.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
color = QtCore.Qt.GlobalColor.red
shape = QtCore.Qt.PenCapStyle.SquareCap
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.PenStyle.SolidLine, shape, QtCore.Qt.PenJoinStyle.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
@@ -147,27 +154,27 @@ class EthernetLinkItem(LinkItem):
self._source_port.setLabel(source_port_label)
if self._draw_port_labels:
source_port_label.setFlag(source_port_label.ItemIsMovable, not self._source_item.locked())
source_port_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, not self._source_item.locked())
source_port_label.show()
else:
source_port_label.hide()
if self._settings["draw_link_status_points"]:
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.PenStyle.NoPen:
painter.drawPoint(point1)
if self._link.suspended() or self._destination_port.status() == Port.suspended:
# link or port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.GlobalColor.yellow
shape = QtCore.Qt.PenCapStyle.RoundCap
elif self._destination_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.GlobalColor.green
shape = QtCore.Qt.PenCapStyle.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
color = QtCore.Qt.GlobalColor.red
shape = QtCore.Qt.PenCapStyle.SquareCap
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.PenStyle.SolidLine, shape, QtCore.Qt.PenJoinStyle.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
@@ -190,12 +197,12 @@ class EthernetLinkItem(LinkItem):
self._destination_port.setLabel(destination_port_label)
if self._draw_port_labels:
destination_port_label.setFlag(destination_port_label.ItemIsMovable, not self._destination_item.locked())
destination_port_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, not self._destination_item.locked())
destination_port_label.show()
else:
destination_port_label.hide()
if self._settings["draw_link_status_points"]:
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.PenStyle.NoPen:
painter.drawPoint(point2)
self._drawSymbol()

View File

@@ -19,12 +19,12 @@
Graphical representation of an image on the QGraphicsScene.
"""
from ..qt import QtSvg
from ..qt import QtSvgWidgets
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from .drawing_item import DrawingItem
class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
class ImageItem(QtSvgWidgets.QGraphicsSvgItem, DrawingItem):
"""
Class to insert an image on the scene.

View File

@@ -42,7 +42,7 @@ class LabelItem(QtWidgets.QGraphicsTextItem):
qt_font.fromString(view_settings["default_label_font"])
self.setDefaultTextColor(QtGui.QColor(view_settings["default_label_color"]))
self.setFont(qt_font)
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable)
self.setFlags(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.setZValue(2)
self._editable = True
@@ -91,12 +91,12 @@ class LabelItem(QtWidgets.QGraphicsTextItem):
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 key in (QtCore.Qt.Key.Key_P, QtCore.Qt.Key.Key_Plus, QtCore.Qt.Key.Key_Equal) and modifiers & QtCore.Qt.KeyboardModifier.AltModifier \
or key == QtCore.Qt.Key.Key_Plus and modifiers & QtCore.Qt.KeyboardModifier.AltModifier and modifiers & QtCore.Qt.KeyboardModifier.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:
elif key in (QtCore.Qt.Key.Key_M, QtCore.Qt.Key.Key_Minus) and modifiers & QtCore.Qt.KeyboardModifier.AltModifier \
or key == QtCore.Qt.Key.Key_Minus and modifiers & QtCore.Qt.KeyboardModifier.AltModifier and modifiers & QtCore.Qt.KeyboardModifier.KeypadModifier:
if self.rotation() < 360.0:
self.setRotation(self.rotation() + 1)
else:
@@ -107,11 +107,11 @@ class LabelItem(QtWidgets.QGraphicsTextItem):
Edit mode for this note.
"""
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextEditorInteraction)
self.setSelected(True)
self.setFocus()
cursor = self.textCursor()
cursor.select(QtGui.QTextCursor.Document)
cursor.select(QtGui.QTextCursor.SelectionType.Document)
self.setTextCursor(cursor)
def mouseDoubleClickEvent(self, event):
@@ -131,12 +131,12 @@ class LabelItem(QtWidgets.QGraphicsTextItem):
:param event: QFocusEvent instance
"""
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable, False)
cursor = self.textCursor()
if cursor.hasSelection():
cursor.clearSelection()
self.setTextCursor(cursor)
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction)
if not self.toPlainText():
# delete the note if empty
self.delete()
@@ -163,10 +163,10 @@ class LabelItem(QtWidgets.QGraphicsTextItem):
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)
painter.setBrush(QtCore.Qt.GlobalColor.red)
painter.setPen(QtCore.Qt.GlobalColor.red)
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
painter.setPen(QtCore.Qt.GlobalColor.black)
zval = str(int(self.zValue()))
painter.drawText(QtCore.QPointF(center.x(), center.y()), zval)
@@ -213,7 +213,7 @@ class LabelItem(QtWidgets.QGraphicsTextItem):
:param change: GraphicsItemChange type
:param value: value of the change
"""
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemSelectedChange:
if value == 0:
self.item_unselected_signal.emit()
return super().itemChange(change, value)

View File

@@ -44,7 +44,7 @@ class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
0,
dst.x(),
dst.y())
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
pen = QtGui.QPen(QtCore.Qt.GlobalColor.black, 2, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
self.setPen(pen)
else:
self.fromSvg(svg)
@@ -124,19 +124,19 @@ class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
if self._isHorizontalLine():
if event.pos().x() > (self.line().x2() - self._border):
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeHorCursor)
elif event.pos().x() < self._border:
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeHorCursor)
else:
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeAllCursor)
# Vertical line
else:
if event.pos().y() > (self.line().y2() - self._border):
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeVerCursor)
elif event.pos().y() < self._border:
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeVerCursor)
else:
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeAllCursor)
def mouseMoveEvent(self, event):
"""
@@ -177,17 +177,17 @@ class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
self.update()
if self._isHorizontalLine():
if event.pos().x() > (self.line().x2() - self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
self._edge = "right"
elif event.pos().x() < (self.line().x1() + self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
self._edge = "left"
else:
if event.pos().y() > (self.line().y2() - self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
self._edge = "top"
elif event.pos().y() < (self.line().y1() + self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
self._edge = "bottom"
super().mousePressEvent(event)
@@ -199,7 +199,7 @@ class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
"""
self.update()
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
self._edge = None
super().mouseReleaseEvent(event)
@@ -213,4 +213,4 @@ class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
# locked objects don't need cursors
if not self.locked():
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.ArrowCursor)

View File

@@ -21,18 +21,19 @@ Link items are graphical representation of a link on the QGraphicsScene
"""
import math
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot, sip_is_deleted
from ..qt import QtCore, QtGui, QtWidgets, QtSvgWidgets, qslot, sip_is_deleted
from ..packet_capture import PacketCapture
from ..dialogs.filter_dialog import FilterDialog
from ..dialogs.style_editor_dialog_link import StyleEditorDialogLink
from ..utils.get_icon import get_icon
class SvgIconItem(QtSvg.QGraphicsSvgItem):
class SvgIconItem(QtSvgWidgets.QGraphicsSvgItem):
def __init__(self, symbol, parent):
QtSvg.QGraphicsSvgItem.__init__(self, symbol, parent)
QtSvgWidgets.QGraphicsSvgItem.__init__(self, symbol, parent)
def mousePressEvent(self, event):
@@ -55,7 +56,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
"""
_draw_port_labels = False
delete_link_item_signal = QtCore.pyqtSignal(str)
delete_link_item_signal = QtCore.Signal(str)
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False):
@@ -101,7 +102,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self._link = link
self._link.updated_link_signal.connect(self._drawSymbol)
self._link.delete_link_signal.connect(self._linkDeletedSlot)
self.setFlag(self.ItemIsFocusable)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
source_item.addLink(self)
destination_item.addLink(self)
self.setCustomToolTip()
@@ -131,18 +132,39 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
def _filterActionSlot(self, *args):
dialog = FilterDialog(self._main_window, self._link)
dialog.show()
dialog.exec_()
dialog.exec()
@qslot
def _suspendActionSlot(self, *args):
self._link.toggleSuspend()
@qslot
def _styleActionSlot(self, *args):
style_dialog = StyleEditorDialogLink(self, self._main_window)
style_dialog.show()
style_dialog.exec()
def setLinkStyle(self, link_style):
self._link._link_style["color"] = link_style["color"]
self._link._link_style["width"] = link_style["width"]
self._link._link_style["type"] = link_style["type"]
# This refers to functions in link.py!
self._link.setLinkStyle(link_style)
self._link.update()
def delete(self):
"""
Delete this link
"""
self._link.deleteLink()
def reset(self):
"""
Reset this link
"""
self._link.resetLink()
def link(self):
"""
Returns the link attached to this link item.
@@ -223,51 +245,63 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
if not self._link.capturing():
# start capture
start_capture_action = QtWidgets.QAction("Start capture", menu)
start_capture_action = QtGui.QAction("Start capture", menu)
start_capture_action.setIcon(get_icon('capture-start.svg'))
start_capture_action.triggered.connect(self._startCaptureActionSlot)
menu.addAction(start_capture_action)
if self._link.capturing():
# stop capture
stop_capture_action = QtWidgets.QAction("Stop capture", menu)
stop_capture_action = QtGui.QAction("Stop capture", menu)
stop_capture_action.setIcon(get_icon('capture-stop.svg'))
stop_capture_action.triggered.connect(self._stopCaptureActionSlot)
menu.addAction(stop_capture_action)
# start wireshark
start_wireshark_action = QtWidgets.QAction("Start Wireshark", menu)
start_wireshark_action = QtGui.QAction("Start Wireshark", menu)
start_wireshark_action.setIcon(QtGui.QIcon(":/icons/wireshark.png"))
start_wireshark_action.triggered.connect(self._startWiresharkActionSlot)
menu.addAction(start_wireshark_action)
if PacketCapture.instance().packetAnalyzerAvailable():
analyze_action = QtWidgets.QAction("Analyze capture", menu)
analyze_action = QtGui.QAction("Analyze capture", menu)
analyze_action.setIcon(QtGui.QIcon(':/icons/rtv.png'))
analyze_action.triggered.connect(self._analyzeCaptureActionSlot)
menu.addAction(analyze_action)
if self._link.suspended() is False:
# Edit filters
filter_action = QtWidgets.QAction("Packet filters", menu)
filter_action = QtGui.QAction("Packet filters", menu)
filter_action.setIcon(get_icon('filter.svg'))
filter_action.triggered.connect(self._filterActionSlot)
menu.addAction(filter_action)
# Suspend link
suspend_action = QtWidgets.QAction("Suspend", menu)
suspend_action = QtGui.QAction("Suspend", menu)
suspend_action.setIcon(get_icon('pause.svg'))
suspend_action.triggered.connect(self._suspendActionSlot)
menu.addAction(suspend_action)
else:
# Resume link
resume_action = QtWidgets.QAction("Resume", menu)
resume_action = QtGui.QAction("Resume", menu)
resume_action.setIcon(get_icon('start.svg'))
resume_action.triggered.connect(self._suspendActionSlot)
menu.addAction(resume_action)
# reset
reset_action = QtGui.QAction("Reset", menu)
reset_action.setIcon(get_icon('reload.svg'))
reset_action.triggered.connect(self._resetActionSlot)
menu.addAction(reset_action)
# style
style_action = QtGui.QAction("Style", menu)
style_action.setIcon(get_icon("node_conception.svg"))
style_action.triggered.connect(self._styleActionSlot)
menu.addAction(style_action)
# delete
delete_action = QtWidgets.QAction("Delete", menu)
delete_action = QtGui.QAction("Delete", menu)
delete_action.setIcon(get_icon('delete.svg'))
delete_action.triggered.connect(self._deleteActionSlot)
menu.addAction(delete_action)
@@ -280,23 +314,31 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
:param: QGraphicsSceneMouseEvent instance
"""
if event.button() == QtCore.Qt.RightButton:
if self._adding_flag:
# send a escape key to the main window to cancel the link addition
from ..main_window import MainWindow
key = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Escape, QtCore.Qt.NoModifier)
QtWidgets.QApplication.sendEvent(MainWindow.instance(), key)
return
if event.button() == QtCore.Qt.MouseButton.RightButton and self._adding_flag:
# send a escape key to the main window to cancel the link addition
from ..main_window import MainWindow
key = QtGui.QKeyEvent(QtCore.QEvent.Type.KeyPress, QtCore.Qt.Key.Key_Escape, QtCore.Qt.KeyboardModifier.NoModifier)
QtWidgets.QApplication.sendEvent(MainWindow.instance(), key)
return
else:
super().mousePressEvent(event)
if not sip_is_deleted(self):
# create the contextual menu
self.setAcceptHoverEvents(False)
menu = QtWidgets.QMenu()
self.populateLinkContextualMenu(menu)
menu.exec_(QtGui.QCursor.pos())
self.setAcceptHoverEvents(True)
self._hovered = False
self.adjust()
def contextMenuEvent(self, event):
"""
Handles all context menu events.
:param event: QContextMenuEvent instance
"""
if not sip_is_deleted(self):
# create the contextual menu
self.setHovered(True)
self.setAcceptHoverEvents(False)
menu = QtWidgets.QMenu()
self.populateLinkContextualMenu(menu)
menu.exec(QtGui.QCursor.pos())
self.setAcceptHoverEvents(True)
self.setHovered(False)
def keyPressEvent(self, event):
"""
@@ -306,10 +348,18 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
"""
# On pressing backspace or delete key, the selected link gets deleted
if event.key() == QtCore.Qt.Key_Delete or event.key() == QtCore.Qt.Key_Backspace:
if event.key() == QtCore.Qt.Key.Key_Delete or event.key() == QtCore.Qt.Key.Key_Backspace:
self._deleteActionSlot()
return
def _resetActionSlot(self):
"""
Slot to receive events from the reset action in the
contextual menu.
"""
self.reset()
def _deleteActionSlot(self):
"""
Slot to receive events from the delete action in the

View File

@@ -18,7 +18,7 @@
import urllib.parse
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
from ..qt import QtCore, QtGui, QtWidgets, QtSvgWidgets
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from ..controller import Controller
@@ -27,7 +27,7 @@ import logging
log = logging.getLogger(__name__)
class LogoItem(QtSvg.QGraphicsSvgItem):
class LogoItem(QtSvgWidgets.QGraphicsSvgItem):
"""
Margin for the logo
"""
@@ -60,8 +60,8 @@ class LogoItem(QtSvg.QGraphicsSvgItem):
self.graphicsEffect().setEnabled(False)
# set graphical settings for this item
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True)
from ..main_window import MainWindow
@@ -83,7 +83,7 @@ class LogoItem(QtSvg.QGraphicsSvgItem):
self.setZValue(-2)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.Paint:
if event.type() == QtCore.QEvent.Type.Paint:
self.updatePosition()
return QtWidgets.QWidget.eventFilter(self, source, event)

View File

@@ -21,7 +21,7 @@ Graphical representation of a node on the QGraphicsScene.
from ..qt import sip
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
from ..qt import QtCore, QtGui, QtWidgets, QtSvgWidgets, qslot
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from .label_item import LabelItem
from ..symbol import Symbol
@@ -32,7 +32,7 @@ import logging
log = logging.getLogger(__name__)
class NodeItem(QtSvg.QGraphicsSvgItem):
class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
"""
Node for the scene.
@@ -51,6 +51,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self._links = []
self._symbol = None
self._locked = False
self._allow_snap_to_grid = True
# says if the attached node has been initialized
# by the server.
@@ -59,7 +60,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
# node label
self._node_label = None
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
self.setPos(QtCore.QPointF(self._node.x(), self._node.y()))
# Temporary symbol during loading
renderer = QImageSvgRenderer(":/icons/reload.svg")
@@ -73,10 +74,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self.graphicsEffect().setEnabled(False)
# set graphical settings for this node
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True)
# update z value and locked state
@@ -108,6 +109,9 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
if node.initialized():
self.createdSlot(node.id())
if self._main_window.uiSnapToGridAction.isChecked():
self.setPos(QtCore.QPointF(self._node.x() + 0.1, self._node.y()))
def updateNode(self):
"""
Sync change to the node
@@ -219,7 +223,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
:param base_node_id: base node identifier (integer)
"""
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
self.setPos(QtCore.QPointF(self._node.x(), self._node.y()))
self.setSymbol(self._node.symbol())
self.update()
@@ -380,7 +384,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self._node_label.setRotation(label_data.get("rotation", 0))
if self._node.locked():
self._node_label.setFlag(self.ItemIsMovable, False)
self._node_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
if label_data["x"] is None:
self._centerLabel()
@@ -388,7 +392,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
else:
self._node_label.setPos(label_data["x"], label_data["y"])
def connectToPort(self, unavailable_ports=[]):
def connectToPort(self, pos, unavailable_ports=[]):
"""
Shows a contextual menu for the user to choose port or auto-select one.
@@ -436,7 +440,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port_object.name())
menu.triggered.connect(self.selectedPortSlot)
menu.exec_(QtGui.QCursor.pos())
# add some delay before showing the menu
# https://github.com/GNS3/gns3-gui/issues/3169
QtCore.QThread.msleep(100)
menu.exec(pos)
return self._selected_port
def selectedPortSlot(self, action):
@@ -463,7 +470,8 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
:param value: value of the change
"""
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._main_window.uiSnapToGridAction.isChecked() \
and self._allow_snap_to_grid:
grid_size = self._main_window.uiGraphicsView.nodeGridSize()
mid_x = self.boundingRect().width() / 2
value.setX((grid_size * round((value.x() + mid_x) / grid_size)) - mid_x)
@@ -471,7 +479,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
value.setY((grid_size * round((value.y() + mid_y) / grid_size)) - mid_y)
# dynamically change the renderer when this node item is selected/unselected.
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemSelectedChange:
if value:
self.graphicsEffect().setEnabled(True)
else:
@@ -479,7 +487,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
self.updateNode()
# adjust link item positions when this node is moving or has changed.
if change == QtWidgets.QGraphicsItem.ItemPositionChange or change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange or change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
for link in self._links:
link.adjust()
@@ -496,16 +504,16 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
# don't show the selection rectangle
if not self._settings["draw_rectangle_selected_item"]:
option.state = QtWidgets.QStyle.State_None
option.state = QtWidgets.QStyle.StateFlag.State_None
super().paint(painter, option, widget)
if not self._initialized or self.show_layer:
brect = self.boundingRect()
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)
painter.setBrush(QtCore.Qt.GlobalColor.red)
painter.setPen(QtCore.Qt.GlobalColor.red)
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
painter.setPen(QtCore.Qt.GlobalColor.black)
if self.show_layer:
text = str(int(self.zValue())) # Z value
elif self._last_error:
@@ -525,6 +533,27 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
for link in self._links:
link.adjust()
def keyPressEvent(self, event):
"""
Handles all key press events
:param event: QKeyEvent
"""
if event.modifiers() & QtCore.Qt.KeyboardModifier.AltModifier:
self._allow_snap_to_grid = False
else:
super().keyPressEvent(event)
def keyReleaseEvent(self, event):
"""
Handles all key release events
:param event: QKeyEvent
"""
self._allow_snap_to_grid = True
def locked(self):
return self._locked
@@ -537,13 +566,13 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
"""
if locked is True:
self.setFlag(self.ItemIsMovable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
if self._node_label:
self._node_label.setFlag(self.ItemIsMovable, False)
self._node_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
else:
self.setFlag(self.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
if self._node_label:
self._node_label.setFlag(self.ItemIsMovable, True)
self._node_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
for link in self._links:
link.adjust()
self._locked = locked

View File

@@ -32,8 +32,22 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
"""
def __init__(self, width=200, height=100, **kws):
self._rx = 0
self._ry = 0
super().__init__(width=width, height=height, **kws)
def setHorizontalCornerRadius(self, radius: int):
self._rx = radius
def horizontalCornerRadius(self):
return self._rx
def setVerticalCornerRadius(self, radius: int):
self._ry = radius
def verticalCornerRadius(self):
return self._ry
def paint(self, painter, option, widget=None):
"""
Paints the contents of an item in local coordinates.
@@ -43,7 +57,9 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
:param widget: QWidget instance
"""
super().paint(painter, option, widget)
painter.setPen(self.pen())
painter.setBrush(self.brush())
painter.drawRoundedRect(self.rect(), self._rx, self._ry)
self.drawLayerInfo(painter)
def toSvg(self):
@@ -57,7 +73,27 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
rect = ET.SubElement(svg, "rect")
rect.set("width", str(int(self.rect().width())))
rect.set("height", str(int(self.rect().height())))
if self._rx:
rect.set("rx", str(self._rx))
if self._ry:
rect.set("ry", str(self._ry))
rect = self._styleSvg(rect)
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
def fromSvg(self, svg):
svg_elem = ET.fromstring(svg)
if len(svg_elem):
# handle horizontal corner radius and vertical corner radius (specific to rectangles)
rx = svg_elem[0].get("rx")
ry = svg_elem[0].get("ry")
if rx:
self._rx = int(rx)
elif ry:
self._rx = int(ry) # defaults to ry if it is specified
if ry:
self._ry = int(ry)
elif rx:
self._ry = int(rx) # defaults to rx if it is specified
super().fromSvg(svg)

View File

@@ -50,10 +50,16 @@ class SerialLinkItem(LinkItem):
LinkItem.adjust(self)
if self._hovered:
self.setPen(QtGui.QPen(QtCore.Qt.red, self._pen_width + 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
else:
self.setPen(QtGui.QPen(QtCore.Qt.darkRed, self._pen_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
try:
if self._hovered:
self.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.red, self._link._link_style["width"] + 1, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
else:
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], self._link._link_style["type"], QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
except:
if self._hovered:
self.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.red, self._pen_width + 1, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
else:
self.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.darkRed, self._pen_width, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
# get source to destination angle
vector_angle = math.atan2(self.dy, self.dx)
@@ -116,17 +122,17 @@ class SerialLinkItem(LinkItem):
# source point color
if self._link.suspended() or self._source_port.status() == Port.suspended:
# link or port is suspended
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.yellow
shape = QtCore.Qt.PenCapStyle.RoundCap
color = QtCore.Qt.GlobalColor.yellow
elif self._source_port.status() == Port.started:
# port is active
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.green
shape = QtCore.Qt.PenCapStyle.RoundCap
color = QtCore.Qt.GlobalColor.green
else:
shape = QtCore.Qt.SquareCap
color = QtCore.Qt.red
shape = QtCore.Qt.PenCapStyle.SquareCap
color = QtCore.Qt.GlobalColor.red
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.PenStyle.SolidLine, shape, QtCore.Qt.PenJoinStyle.MiterJoin))
source_port_label = self._source_port.label()
if source_port_label is None:
@@ -136,28 +142,28 @@ class SerialLinkItem(LinkItem):
self._source_port.setLabel(source_port_label)
if self._draw_port_labels:
source_port_label.setFlag(source_port_label.ItemIsMovable, not self._source_item.locked())
source_port_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, not self._source_item.locked())
source_port_label.show()
else:
source_port_label.hide()
if self._settings["draw_link_status_points"]:
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.PenStyle.NoPen:
painter.drawPoint(self.source_point)
# destination point color
if self._link.suspended() or self._destination_port.status() == Port.suspended:
# link or port is suspended
color = QtCore.Qt.yellow
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.GlobalColor.yellow
shape = QtCore.Qt.PenCapStyle.RoundCap
elif self._destination_port.status() == Port.started:
# port is active
color = QtCore.Qt.green
shape = QtCore.Qt.RoundCap
color = QtCore.Qt.GlobalColor.green
shape = QtCore.Qt.PenCapStyle.RoundCap
else:
color = QtCore.Qt.red
shape = QtCore.Qt.SquareCap
color = QtCore.Qt.GlobalColor.red
shape = QtCore.Qt.PenCapStyle.SquareCap
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.PenStyle.SolidLine, shape, QtCore.Qt.PenJoinStyle.MiterJoin))
destination_port_label = self._destination_port.label()
@@ -168,12 +174,12 @@ class SerialLinkItem(LinkItem):
self._destination_port.setLabel(destination_port_label)
if self._draw_port_labels:
destination_port_label.setFlag(destination_port_label.ItemIsMovable, not self._destination_item.locked())
destination_port_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, not self._destination_item.locked())
destination_port_label.show()
else:
destination_port_label.hide()
if self._settings["draw_link_status_points"]:
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.PenStyle.NoPen:
painter.drawPoint(self.destination_point)
self._drawSymbol()

View File

@@ -44,7 +44,7 @@ class ShapeItem(DrawingItem):
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)
pen = QtGui.QPen(QtCore.Qt.GlobalColor.black, 2, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
self.setPen(pen)
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
self.setBrush(brush)
@@ -61,21 +61,21 @@ class ShapeItem(DrawingItem):
"""
self.update()
self._originally_movable = self.flags() & QtWidgets.QGraphicsItem.ItemIsMovable
self._originally_movable = bool(self.flags() & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
if event.pos().x() > (self.rect().right() - self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
self._edge = "right"
elif event.pos().x() < (self.rect().left() + self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
self._edge = "left"
elif event.pos().y() < (self.rect().top() + self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
self._edge = "top"
elif event.pos().y() > (self.rect().bottom() - self._border):
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
self._edge = "bottom"
QtWidgets.QGraphicsItem.mousePressEvent(self, event)
@@ -87,7 +87,7 @@ class ShapeItem(DrawingItem):
"""
self.update()
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, self._originally_movable)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, self._originally_movable)
self._edge = None
QtWidgets.QGraphicsItem.mouseReleaseEvent(self, event)
@@ -150,15 +150,15 @@ class ShapeItem(DrawingItem):
# locked objects don't need cursors
if not self.locked():
if event.pos().x() > (self.rect().right() - self._border):
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeHorCursor)
elif event.pos().x() < (self.rect().left() + self._border):
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeHorCursor)
elif event.pos().y() < (self.rect().top() + self._border):
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeVerCursor)
elif event.pos().y() > (self.rect().bottom() - self._border):
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeVerCursor)
else:
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeAllCursor)
def hoverLeaveEvent(self, event):
"""
@@ -169,11 +169,14 @@ class ShapeItem(DrawingItem):
# locked objects don't need cursors
if not self.locked():
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
self._graphics_view.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
def setWidthAndHeight(self, width, height):
self.setRect(0, 0, width, height)
def fromSvg(self, svg):
"""
Import element informations from an SVG
Import element information from SVG
"""
svg = ET.fromstring(svg)
width = float(svg.get("width", self.rect().width()))
@@ -181,7 +184,7 @@ class ShapeItem(DrawingItem):
self.setRect(0, 0, width, height)
pen = QtGui.QPen()
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
brush = QtGui.QBrush(QtCore.Qt.BrushStyle.SolidPattern)
if len(svg):
pen = self._penFromSVGElement(svg[0])

View File

@@ -62,11 +62,11 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
Edit mode for this note.
"""
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextEditorInteraction)
self.setSelected(True)
self.setFocus()
cursor = self.textCursor()
cursor.select(QtGui.QTextCursor.Document)
cursor.select(QtGui.QTextCursor.SelectionType.Document)
self.setTextCursor(cursor)
def mouseDoubleClickEvent(self, event):
@@ -85,12 +85,12 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
:param event: QFocusEvent instance
"""
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False)
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable, False)
cursor = self.textCursor()
if cursor.hasSelection():
cursor.clearSelection()
self.setTextCursor(cursor)
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction)
if not self.toPlainText():
# delete the note if empty
self.delete()

View File

@@ -19,12 +19,11 @@
Manages and stores everything needed for a connection between 2 devices.
"""
import os
import re
from .qt import sip
import uuid
from .qt import QtCore
from .qt import QtCore, QtNetwork
from .controller import Controller
@@ -79,6 +78,8 @@ class Link(QtCore.QObject):
self._deleting = False
self._capture_file_path = None
self._capture_file = None
self._network_manager = None
self._response_stream = None
self._capture_compute_id = None
self._initialized = False
self._filters = {}
@@ -90,9 +91,7 @@ class Link(QtCore.QObject):
self._creator = False
self._nodes = []
self._source_node.addLink(self)
self._destination_node.addLink(self)
self._link_style = {}
body = self._prepareParams()
if self._link_id:
@@ -113,25 +112,36 @@ class Link(QtCore.QObject):
# We need to stream the pcap file content if the controller or compute is remote
if Controller.instance().isRemote() or self._capture_file_path is None:
self._capture_file = QtCore.QTemporaryFile()
self._capture_file.open(QtCore.QFile.WriteOnly)
self._capture_file.open(QtCore.QIODeviceBase.OpenModeFlag.WriteOnly)
self._capture_file.setAutoRemove(True)
self._capture_file_path = self._capture_file.fileName()
else:
self._capture_file = QtCore.QFile(self._capture_file_path)
self._capture_file.open(QtCore.QFile.WriteOnly)
Controller.instance().get("/projects/{project_id}/links/{link_id}/pcap".format(project_id=self.project().id(), link_id=self._link_id),
None,
showProgress=False,
downloadProgressCallback=self._downloadPcapProgress,
ignoreErrors=True, # If something is wrong avoid disconnect us from server
timeout=None)
log.debug("Capturing packets to '{}'".format(self._capture_file_path))
self._capture_file.open(QtCore.QIODeviceBase.OpenModeFlag.WriteOnly)
if self._network_manager is None:
self._network_manager = QtNetwork.QNetworkAccessManager(self)
self._network_manager.sslErrors.connect(Controller.instance().httpClient().handleSslError)
self._response_stream = Controller.instance().get(
"/projects/{project_id}/links/{link_id}/capture/stream".format(project_id=self.project().id(), link_id=self._link_id),
callback=None,
show_progress=False,
download_progress_callback=self._downloadPcapProgress,
timeout=None,
network_manager=self._network_manager
)
log.debug("Has successfully started capturing packets on link {} to '{}'".format(self._link_id, self._capture_file_path))
else:
self._response_stream = None
if "nodes" in result:
self._nodes = result["nodes"]
self._updateLabels()
if "filters" in result:
self._filters = result["filters"]
if "link_style" in result:
self._link_style = result["link_style"]
if "suspend" in result:
self._suspend = result["suspend"]
self.updated_link_signal.emit(self._id)
@@ -214,6 +224,7 @@ class Link(QtCore.QObject):
}
],
"filters": self._filters,
"link_style": self._link_style,
"suspend": self._suspend
}
if self._source_port.label():
@@ -345,20 +356,42 @@ class Link(QtCore.QObject):
# let the GUI know about this link has been deleted
self.delete_link_signal.emit(self._id)
def resetLink(self):
"""
Resets this link.
"""
log.debug("reset link from {} {} to {} {}".format(self._source_node.name(),
self._source_port.name(),
self._destination_node.name(),
self._destination_port.name()))
Controller.instance().post("/projects/{project_id}/links/{link_id}/reset".format(project_id=self.project().id(),
link_id=self._link_id),
self._linkResetCallback)
def _linkResetCallback(self, result, error=False, **kwargs):
"""
Called after the link is reset.
"""
if error:
log.error("Error while resetting link: {}".format(result["message"]))
return
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),
Controller.instance().post("/projects/{project_id}/links/{link_id}/capture/start".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"]))
log.error("Error while starting capture on link {}: {}".format(self._link_id, result["message"]))
return
#self._parseResponse(result)
def _downloadPcapProgress(self, content, server=None, context={}, **kwargs):
"""
@@ -382,15 +415,16 @@ class Link(QtCore.QObject):
# except OSError as e:
# log.error("Cannot remove file {}: {}".format(self._capture_file_path, e))
self._capture_file_path = None
Controller.instance().post("/projects/{project_id}/links/{link_id}/stop_capture".format(project_id=self.project().id(),
Controller.instance().post("/projects/{project_id}/links/{link_id}/capture/stop".format(project_id=self.project().id(),
link_id=self._link_id),
self._stopCaptureCallback)
def _stopCaptureCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while stopping capture on link: {}".format(result["message"]))
log.error("Error while stopping capture on link {}: {}".format(self._link_id, result["message"]))
return
#self._parseResponse(result)
log.debug("Has successfully stopped capturing packets on link {}".format(self._link_id))
def get(self, path, callback, **kwargs):
"""
@@ -468,3 +502,9 @@ class Link(QtCore.QObject):
:params filters: List of filters
"""
self._filters = filters
def setLinkStyle(self, link_style):
"""
:params _link_style: Set link style attributes
"""
self._link_style = link_style

View File

@@ -0,0 +1,11 @@
[Desktop Entry]
Version=1.0
Type=Application
Terminal=false
Exec=gns3 %f
Name=GNS3
Comment=GNS3 Graphical Network Simulator
Icon=gns3
Categories=Education;Network;
MimeType=application/x-gns3;application/x-gns3appliance;application/x-gns3project;
Keywords=simulator;network;netsim;

28
gns3/linux/gns3-gui.xml Normal file
View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-gns3">
<comment>GNS3 Project File</comment>
<comment xml:lang="en">GNS3 Project File</comment>
<glob pattern="*.gns3"/>
</mime-type>
<mime-type type="application/x-gns3project">
<comment>GNS3 Project File</comment>
<comment xml:lang="en">GNS3 Portable Project File</comment>
<glob pattern="*.gns3project"/>
</mime-type>
<mime-type type="application/x-gns3project">
<comment>GNS3 Project File</comment>
<comment xml:lang="en">GNS3 Portable Project File</comment>
<glob pattern="*.gns3p"/>
</mime-type>
<mime-type type="application/x-gns3appliance">
<comment>GNS3 Appliance File</comment>
<comment xml:lang="en">GNS3 Appliance File</comment>
<glob pattern="*.gns3appliance"/>
</mime-type>
<mime-type type="application/x-gns3appliance">
<comment>GNS3 Appliance File</comment>
<comment xml:lang="en">GNS3 Appliance File</comment>
<glob pattern="*.gns3a"/>
</mime-type>
</mime-info>

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 256 256"
enable-background="new 0 0 792 612"
xml:space="preserve"
inkscape:version="0.48.4 r9939"
width="100%"
height="100%"
sodipodi:docname="gns3_project_base_icon.svg"
inkscape:export-filename="/home/grossmj/Downloads/Logos/gns3_project.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90"><metadata
id="metadata30"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs28" /><sodipodi:namedview
pagecolor="#000000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="688"
inkscape:window-height="508"
id="namedview26"
showgrid="false"
inkscape:zoom="2.8284271"
inkscape:cx="99.209744"
inkscape:cy="115.40526"
inkscape:window-x="692"
inkscape:window-y="203"
inkscape:window-maximized="0"
inkscape:current-layer="g3"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" /><g
id="g3"
transform="translate(-19.09309,-355.40651)"><linearGradient
id="SVGID_1_"
gradientUnits="userSpaceOnUse"
x1="404.2251"
y1="365.32321"
x2="384.2139"
y2="136.595"
gradientTransform="matrix(0.9154681,0,0,0.8343604,-205.93817,243.15241)"><stop
offset="0"
style="stop-color:#00A5CE"
id="stop6" /><stop
offset="0.1989"
style="stop-color:#1295CB"
id="stop8" /><stop
offset="0.5994"
style="stop-color:#426BC5"
id="stop10" /><stop
offset="1"
style="stop-color:#783CBD"
id="stop12" /></linearGradient><path
d="m 171.60071,405.01864 c 0,4.83934 -4.30236,8.76063 -9.61217,8.76063 -5.30966,0 -9.61254,-3.92129 -9.61254,-8.76063 0,-4.83945 4.30288,-8.76118 9.61254,-8.76118 5.30981,0 9.61217,3.92173 9.61217,8.76118 z M 66.413524,449.07279 c -5.767437,28.78523 2.105621,49.56075 14.372861,64.41269 21.879695,28.11798 54.012515,33.04057 54.012515,33.04057 37.53432,6.50778 66.09685,-5.33982 83.21603,-20.02463 21.51357,-18.60601 20.7815,-37.04552 20.87311,-37.29575 0.18299,-8.59424 -2.92957,-14.7682 -8.42235,-19.02378 -9.24633,-7.09206 -20.86017,2.69341 -20.86017,2.69341 -19.49927,4.17192 -15.94169,13.32637 -15.94169,13.32637 1.09845,4.83918 3.84457,8.00978 6.95741,9.84533 5.9504,3.33747 12.26728,2.67002 12.26728,2.67002 9.3378,-1.0009 12.08427,-9.0109 11.53485,-13.01608 -0.91555,-6.34088 -7.23234,-7.50924 -7.23234,-7.50924 -2.38005,-0.41709 -4.21098,-0.0833 -5.58438,0.58407 -2.01358,0.75135 -2.83796,2.00243 -2.83796,2.00243 -1.46454,2.25284 -0.54914,3.50469 0.36639,4.00532 0.54947,0.33369 1.19036,0.33369 1.19036,0.33369 0.73207,0 1.18967,-0.16678 1.73912,-0.41707 1.28185,-0.6675 2.2885,-1.7525 4.21141,-1.085 2.83793,1.00135 2.10546,3.7549 2.10546,3.7549 -0.641,3.00358 -2.5634,4.42215 -4.66897,4.9226 -2.83795,0.66751 -5.76742,-0.16678 -5.76742,-0.16678 -10.07028,-3.75491 -5.58438,-12.93261 -5.58438,-12.93261 3.11245,-5.75734 10.07038,-6.42485 10.07038,-6.42485 10.89377,-1.58499 18.12615,5.08962 16.93565,16.35342 -1.37299,14.0176 -13.18224,23.27863 -25.81601,29.11962 -47.32956,22.19367 -82.66676,-5.92441 -82.66676,-5.92441 -13.18276,-11.43056 -10.07016,-23.52883 -8.60541,-27.3666 0.54929,-1.41865 2.01406,-1.6691 2.74639,-1.6691 10.80253,-0.83407 19.774,17.35498 20.68947,19.85809 1.92249,4.17184 4.94343,6.50765 7.78138,8.00972 6.40841,3.25405 13.18313,1.66883 13.18313,1.66883 9.33745,-2.9202 6.1335,-9.84533 6.1335,-9.84533 -1.09842,-2.75341 -2.74644,-3.50477 -4.02786,-3.42093 -1.19054,0 -2.10564,0.83442 -2.10564,0.83442 -0.45776,0.50034 -1.00731,0.83393 -1.46489,1.25125 -5.95071,4.58892 -8.8803,-0.25044 -8.8803,-0.25044 -2.10546,-3.08697 1.09901,-4.75583 1.09901,-4.75583 0.64083,-0.33355 1.28141,-0.50061 1.73895,-0.66748 2.38042,-0.66714 2.01441,-2.50311 2.01441,-2.50311 -0.36609,-3.42046 -4.57745,-2.00244 -4.57745,-2.00244 -4.1195,1.3353 -5.40139,4.42214 -5.76771,5.75738 -0.0915,0.41727 -0.1831,0.75095 -0.8236,0.83435 -1.18996,0.16676 -1.18996,-1.75225 -1.18996,-1.75225 -0.36654,-13.68349 -4.4859,-19.77428 -6.40854,-21.94348 -0.54936,-0.66796 -1.46475,-1.08497 -1.73923,-1.25176 -4.30289,-1.7521 -9.06329,-5.17302 -9.06329,-5.17302 -9.06314,-5.92391 -5.4928,-11.26375 -5.4928,-11.26375 5.40125,-12.34883 9.79555,-16.60411 9.79555,-16.60411 3.66173,-3.17061 7.50696,-0.33364 7.50696,-0.33364 -2.83792,3.17075 -5.03524,6.25803 -5.03524,6.25803 -6.49982,9.42787 -7.59841,13.26624 -7.59841,13.26624 -0.0915,0.33355 -0.0915,0.91765 0.45776,1.58531 6.68291,10.42945 24.7176,12.01453 24.80912,12.01453 4.94374,0.66741 8.97144,0.0834 8.97144,0.0834 2.38032,-0.41707 2.74642,1.00145 2.74642,1.00145 0.27482,0.83443 1.28199,3.17072 1.28199,3.17072 1.73928,3.33708 4.3939,1.5849 4.3939,1.5849 1.00743,-0.66745 1.465,-1.5849 1.465,-2.33588 0,-0.75128 -0.45757,-1.25175 -0.45757,-1.25175 -1.19053,-1.58542 -1.55653,-3.0869 -1.55653,-3.0869 -0.54909,-1.83594 1.73957,-2.58648 1.73957,-2.58648 4.48544,-1.83599 6.95727,1.66826 6.95727,1.66826 2.38047,2.5865 7.14092,-0.66745 7.14092,-0.66745 3.8449,-2.66992 -4.0976,0.19062 -4.0976,0.19062 -9.88688,-8.26038 -16.56361,-1.34696 -16.56361,-1.34696 -0.64104,0.7511 -3.04976,-2.76495 -3.04976,-2.76495 -9.61199,0.25048 -18.76672,-6.17438 -18.76672,-6.50818 3.66187,-5.5901 10.71075,-15.35234 10.71075,-15.35234 l 37.25985,13.09964 c 1.46442,0.41703 3.02095,0.0833 3.02095,0.0833 l 58.04043,-10.67989 c 1.28195,-0.16687 1.09896,-1.58503 1.09896,-1.58503 0,-1.16837 -0.0919,-1.33515 -1.19046,-1.50194 -1.18994,-0.16678 -56.48449,-4.08849 -56.48449,-4.08849 -3.02097,-0.16681 -3.02097,-3.92122 0.0915,-4.17141 l 40.64728,-3.08733 c 0.91552,-0.0834 1.09848,-0.91783 1.09848,-0.91783 1.09844,-4.92279 0.73198,-11.34711 0.73198,-11.34711 11.80979,-12.01521 6.1339,-23.86288 6.1339,-23.86288 -7.23198,-16.35348 -23.1616,-16.60396 -23.1616,-16.60396 -26.18203,-26.36565 -60.60355,-23.27867 -60.60355,-23.27867 -1.55672,0 -1.6483,1.1683 -1.6483,1.1683 l -3.93608,22.69445 c -0.18325,1.25177 -0.91581,1.58533 -0.91581,1.58533 l -5.21788,3.75447 c -0.6411,0.58421 -0.82414,0.58421 -1.73956,0.58421 -0.82389,0.0835 -3.38706,0.41742 -3.38706,0.41742 -61.519582,11.93091 -70.399667,62.15974 -70.399667,62.15974 z M 232.66269,395.33968 c 2.47189,7.17584 -4.30298,9.42866 -4.30298,9.42866 -1.28144,-5.59034 -5.76742,-13.4333 -5.76742,-13.4333 7.59878,-2.16932 10.0704,4.00464 10.0704,4.00464 z m -38.08373,12.7658 c 0,13.18286 -11.71793,23.94621 -26.27368,23.94621 -14.46437,0 -26.27391,-10.67996 -26.27391,-23.94621 0,-13.26611 11.80954,-23.94585 26.27391,-23.94585 14.55575,0 26.27368,10.76313 26.27368,23.94585 z"
id="path14"
inkscape:connector-curvature="0"
style="fill:url(#SVGID_1_);stroke:#000000;stroke-width:1.59908056;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:nodetypes="ssssscccccccccccccccccccccccccccccccccccccccccccccccccccccccccscccccccccccccccccccccccccccccccccccsssss" /><path
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.53344321;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 158.75467,430.66421 c -20.07367,-7.21051 -22.89938,-32.31848 -4.80964,-42.73635 5.48181,-3.15698 10.67085,-4.37809 16.54241,-3.89283 9.69836,0.8015 17.57306,5.9338 21.90388,14.2758 2.15749,4.15572 2.35397,5.15328 2.09049,10.61352 -0.34596,7.1686 -2.32163,11.43951 -7.45042,16.1059 -5.29156,4.81452 -10.10597,6.6958 -17.7209,6.92464 -4.93337,0.14827 -7.384,-0.1514 -10.55582,-1.29068 z m 8.60473,-19.04379 c 4.68093,-3.03776 4.59848,-10.37728 -0.15058,-13.40894 -4.87998,-3.11524 -12.93893,-0.19374 -14.0254,5.08442 -1.51639,7.36707 7.49622,12.65953 14.17598,8.32452 z"
id="path3756"
inkscape:connector-curvature="0" /><path
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.53344321;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 227.52716,403.78857 c -0.005,-0.70451 -1.22789,-3.82375 -2.7167,-6.93163 -2.55419,-5.33189 -2.6206,-5.67568 -1.17683,-6.09329 5.73392,-1.65864 11.08127,3.87024 9.37974,9.69827 -0.98678,3.37997 -5.465,6.09542 -5.48621,3.32665 z"
id="path3758"
inkscape:connector-curvature="0" /><path
sodipodi:nodetypes="sccccccccccscssccsscccccccsss"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
inkscape:connector-curvature="0"
id="path3810"
d="m 239.93926,461.59913 0.0895,0.12221 -1.17469,-1.46733 -32.98343,9.00545 -37.23862,-8.13752 -55.68743,19.71173 -26.020537,-23.43049 -22.596889,16.06861 -11.889522,-13.06537 -0.909404,1.46024 -0.899284,1.4798 c -10.753621,17.54929 -16.66218,36.40349 -16.66218,54.40796 0,27.619 11.056869,51.53032 32.647508,68.48515 20.408405,16.0284 49.491298,25.55892 80.597778,25.55892 31.10647,0 59.83583,-9.88212 80.24671,-25.91052 21.59065,-16.95484 32.29143,-40.51455 32.29143,-68.13355 -0.002,-19.76102 -7.4557,-37.40288 -19.8109,-56.15529 z m -93.0808,146.64361 c -64.382901,0 -109.352655,-37.21035 -109.352655,-90.48655 0,-17.02801 5.398302,-34.83435 15.234948,-51.5232 l 9.904862,13.47926 23.843928,-16.75492 25.086777,22.57192 56.84998,-20.39383 37.73928,8.09108 30.77268,-8.56604 c 12.01921,18.34365 19.27279,33.79352 19.27279,53.09573 0,53.2762 -44.96719,90.48655 -109.35259,90.48655 z" /><path
style="fill:#fefefa;fill-opacity:1;stroke:#ffffff;stroke-width:1.04580986000000009;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
d="M 138.44788,607.85596 C 113.73945,606.22444 93.962212,600.20959 76.995281,589.1664 55.352378,575.07977 42.490317,555.32103 38.520707,530.06164 c -0.759205,-4.83094 -0.874973,-17.42518 -0.205365,-22.34039 1.791597,-13.15109 5.868285,-25.3134 12.758033,-38.06201 0.84206,-1.55812 1.659698,-2.75565 1.816969,-2.66115 0.157281,0.0945 2.381859,3.03194 4.9435,6.5277 2.561642,3.49575 4.723755,6.35591 4.80468,6.35591 0.0809,0 5.326847,-3.64298 11.657596,-8.0955 6.330738,-4.45253 11.699908,-8.16513 11.931484,-8.25022 0.231577,-0.085 4.398802,3.42528 9.260505,7.80084 4.861701,4.37556 10.457641,9.41045 12.435431,11.18865 l 3.59596,3.23309 28.31322,-10.1561 c 15.57226,-5.58585 28.50887,-10.1561 28.74803,-10.1561 0.23916,0 8.7836,1.79241 18.98766,3.9831 l 18.55285,3.98308 14.95072,-4.15493 c 8.2229,-2.28522 15.17245,-4.15493 15.44345,-4.15493 0.27101,0 1.79664,2.10211 3.39032,4.67137 11.40625,18.38862 15.96668,32.0966 15.97665,48.02357 0.006,9.17148 -1.01587,16.56703 -3.40156,24.6239 -9.62567,32.50771 -38.51348,55.82167 -78.32153,63.20957 -10.50247,1.94913 -25.62193,2.8911 -35.71143,2.22487 z"
id="path3821"
inkscape:connector-curvature="0" /></g></svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -26,8 +26,6 @@ import psutil
from .qt import QtCore, QtWidgets
from .version import __version__, __version_info__
from .utils import parse_version
from .local_server_config import LocalServerConfig
from .settings import LOCAL_SERVER_SETTINGS
import logging
log = logging.getLogger(__name__)
@@ -93,17 +91,22 @@ class LocalConfig(QtCore.QObject):
if sys.platform.startswith("win"):
old_config_path = os.path.join(os.path.expandvars("%APPDATA%"), "GNS3", filename)
else:
old_config_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3", filename)
xgd_config_var = "$XDG_CONFIG_HOME"
xdg_config_res = os.path.expandvars(xgd_config_var)
if xdg_config_res != xgd_config_var:
old_config_path = os.path.join(xdg_config_res, "GNS3", filename)
else:
old_config_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3", filename)
# TODO: migrate versioned config file from a previous version of GNS3 (for instance 2.2 -> 2.3) + support profiles
if os.path.exists(old_config_path):
# migrate post version 2.2.0 configuration file
shutil.copyfile(old_config_path, self._config_file)
# reset the local server path and ubridge path
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
settings["path"] = ""
settings["ubridge_path"] = ""
LocalServerConfig.instance().saveSettings("Server", settings)
# settings = LocalServerConfig.instance().loadSettings("Controller", CONTROLLER_SETTINGS)
# settings["path"] = ""
# settings["ubridge_path"] = ""
# LocalServerConfig.instance().saveSettings("Controller", settings)
else:
# create a new config
with open(self._config_file, "w", encoding="utf-8") as f:
@@ -143,8 +146,13 @@ class LocalConfig(QtCore.QObject):
appdata = os.path.expandvars("%APPDATA%")
path = os.path.join(appdata, "GNS3", version)
else:
home = os.path.expanduser("~")
path = os.path.join(home, ".config", "GNS3", version)
xgd_config_var = "$XDG_CONFIG_HOME"
xdg_config_res = os.path.expandvars(xgd_config_var)
if xdg_config_res != xgd_config_var:
path = os.path.join(xdg_config_res, "GNS3", version)
else:
home = os.path.expanduser("~")
path = os.path.join(home, ".config", "GNS3", version)
if self._profile is not None:
path = os.path.join(path, "profiles", self._profile)
@@ -190,7 +198,7 @@ class LocalConfig(QtCore.QObject):
QtWidgets.QMessageBox.critical(False, "Version error", error_message)
# Exit immediately not clean but we want to avoid any side effect that could corrupt the file
QtCore.QTimer.singleShot(0, app.quit)
app.exec_()
app.exec()
sys.exit(1)
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.0alpha1"):
@@ -394,14 +402,6 @@ class LocalConfig(QtCore.QObject):
from gns3.settings import GENERAL_SETTINGS
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["experimental_features"]
def hdpi(self):
"""
:returns: Boolean. True if hdpi is allowed
"""
from gns3.settings import GENERAL_SETTINGS
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["hdpi"]
def multiProfiles(self):
"""
:returns: Boolean. True if multi_profiles is enabled
@@ -416,20 +416,6 @@ class LocalConfig(QtCore.QObject):
settings["multi_profiles"] = value
self.saveSectionSettings("MainWindow", settings)
def directFileUpload(self):
"""
:returns: Boolean. True if direct_file_upload is enabled
"""
from gns3.settings import GENERAL_SETTINGS
return self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)["direct_file_upload"]
def setDirectFileUpload(self, value):
from gns3.settings import GENERAL_SETTINGS
settings = self.loadSectionSettings("MainWindow", GENERAL_SETTINGS)
settings["direct_file_upload"] = value
self.saveSectionSettings("MainWindow", settings)
def showInterfaceLabelsOnNewProject(self):
"""
:returns: Boolean. True if show_interface_labels_on_new_project is enabled
@@ -483,12 +469,12 @@ class LocalConfig(QtCore.QObject):
if os.path.exists(pid_path):
try:
with open(pid_path) as f:
with open(pid_path, encoding="utf-8") as f:
pid = int(f.read())
if pid != my_pid:
try:
process = psutil.Process(pid=pid)
ps_name = process.name()
ps_name = process.name().lower()
except (OSError, psutil.NoSuchProcess, psutil.AccessDenied):
pass
else:
@@ -498,9 +484,17 @@ class LocalConfig(QtCore.QObject):
return False
else:
return True
except (OSError, ValueError) as e:
except OSError as e:
log.critical("Can't read pid file %s: %s", pid_path, str(e))
return False
except ValueError as e:
log.warning("Invalid data in pid file %s: %s", pid_path, str(e))
try:
# try removing the file since it contains invalid data
os.remove(pid_path)
except OSError:
log.critical("Can't remove pid file %s", pid_path)
return False
try:
with open(pid_path, 'w+') as f:

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
# Copyright (C) 2021 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
@@ -22,8 +22,6 @@ import stat
import shlex
import socket
import shutil
import random
import string
import struct
import psutil
import signal
@@ -31,13 +29,12 @@ import subprocess
from gns3.qt import QtWidgets, QtCore, qslot
from gns3.settings import LOCAL_SERVER_SETTINGS
from gns3.settings import DEFAULT_CONTROLLER_HOST
from gns3.local_config import LocalConfig
from gns3.local_server_config import LocalServerConfig
from gns3.http_client import HTTPClient
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
from gns3.utils.progress_dialog import ProgressDialog
from gns3.utils.sudo import sudo
from gns3.http_client import HTTPClient
from gns3.controller import Controller
@@ -51,9 +48,9 @@ class StopLocalServerWorker(QtCore.QObject):
the server
"""
# signals to update the progress dialog.
error = QtCore.pyqtSignal(str, bool)
finished = QtCore.pyqtSignal()
updated = QtCore.pyqtSignal(int)
error = QtCore.Signal(str, bool)
finished = QtCore.Signal()
updated = QtCore.Signal(int)
def __init__(self, local_server_process):
super().__init__()
@@ -94,13 +91,6 @@ class LocalServer(QtCore.QObject):
self._settings = {}
self.localServerSettings()
self._port = self._settings.get("port", 3080)
if not self._settings.get("auto_start", True):
if self._settings.get("host") is None:
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
else:
self._http_client = None
self._stopping = False
self._timer = QtCore.QTimer()
self._timer.setInterval(5000)
@@ -122,27 +112,6 @@ class LocalServer(QtCore.QObject):
return MainWindow.instance()
return self._parent
def _checkWindowsService(self, service_name):
try:
import pywintypes
import win32service
import win32serviceutil
except ImportError as e:
log.error("Could not check if the {} service is running: {}".format(service_name, e))
return
try:
if win32serviceutil.QueryServiceStatus(service_name, None)[1] != win32service.SERVICE_RUNNING:
return False
except pywintypes.error as e:
if e.winerror == 1060: # service is not installed
return False
else:
log.error("Could not check if the {} service is running: {}".format(service_name, e.strerror))
return True
def _checkUbridgePermissions(self):
"""
Checks that uBridge can interact with network interfaces.
@@ -171,9 +140,9 @@ class LocalServer(QtCore.QObject):
self.parent(),
"uBridge",
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge? All users on the system will be able to read packet from the network interfaces.",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No)
if proceed == QtWidgets.QMessageBox.StandardButton.Yes:
sudo(["setcap", "cap_net_admin,cap_net_raw=ep", path])
except AttributeError:
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
@@ -190,21 +159,19 @@ class LocalServer(QtCore.QObject):
self.parent(),
"uBridge",
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge? All admin users on the system will be able to read packet from the network interfaces.",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No)
if proceed == QtWidgets.QMessageBox.StandardButton.Yes:
from gns3.utils.macos_ubridge_setuid import macos_ubridge_setuid
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
macos_ubridge_setuid()
else:
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set root permissions to uBridge {}: {}".format(path, str(e)))
return False
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.
@@ -212,14 +179,9 @@ class LocalServer(QtCore.QObject):
:returns: local server settings (dict)
"""
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
settings = Controller.instance().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:
@@ -246,16 +208,19 @@ class LocalServer(QtCore.QObject):
"""
Update the local server settings. Keep the key not in new_settings
"""
if "host" in new_settings and new_settings["host"] is None:
new_settings["host"] = DEFAULT_CONTROLLER_HOST
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)
Controller.instance().setSettings(self._settings)
# Settings have changed we need to restart the server
if old_settings != self._settings:
if not Controller.instance().connected() or old_settings != self._settings:
if self._settings["auto_start"]:
# We restart the local server only if we really need. Auth can be hot change
settings_require_restart = ('host', 'port', 'path')
@@ -271,12 +236,8 @@ class LocalServer(QtCore.QObject):
# If the controller is remote:
else:
self.stopLocalServer(wait=True)
if self._settings.get("host") is None:
self._http_client = None
else:
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
if Controller.instance().isRemote() and not Controller.instance().connected():
Controller.instance().connect()
def shouldLocalServerAutoStart(self):
"""
@@ -318,29 +279,24 @@ class LocalServer(QtCore.QObject):
Try to start the embedded gns3 server.
"""
if not self.shouldLocalServerAutoStart():
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
return
if self.isLocalServerRunning() and self._server_started_by_me:
local_server_already_running = self.isLocalServerRunning()
if local_server_already_running and self._server_started_by_me:
return True
# 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)
Controller.instance().connect()
return True
if self.isLocalServerRunning():
if local_server_already_running:
log.debug("A local server already running on this host")
# Try to kill the server. The server can be still running after
# if the server was started by hand
self._killAlreadyRunningServer()
if not self.isLocalServerRunning():
if not local_server_already_running:
if not self.initLocalServer():
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not start the local server process: {}".format(self._settings["path"]))
return False
@@ -351,15 +307,14 @@ class LocalServer(QtCore.QObject):
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),
"Local controller",
"Starting local controller {} on port {}...".format(self._settings["host"], self._port),
"Cancel", busy=True, parent=self.parent())
progress_dialog.show()
if not progress_dialog.exec_():
if not progress_dialog.exec():
return False
self._server_started_by_me = True
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
Controller.instance().connect()
return True
def initLocalServer(self):
@@ -368,15 +323,6 @@ class LocalServer(QtCore.QObject):
"""
self._checkUbridgePermissions()
if sys.platform.startswith("win"):
import pywintypes
try:
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
log.warning("The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
except pywintypes.error as e:
log.warning("Could not check if the NPF or Npcap service is running: {}".format(e.strerror))
self._port = self._settings["port"]
# check the local server path
local_server_path = self.localServerPath()
@@ -469,17 +415,21 @@ class LocalServer(QtCore.QObject):
pass
except OSError as e:
log.warning("could not delete server log file {}: {}".format(logpath, e))
command += ' --log="{}" --pid="{}"'.format(logpath, self._pid_path())
command += ' --logfile="{}" --pid="{}"'.format(logpath, self._pid_path())
log.debug("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, stderr=subprocess.PIPE)
self._local_server_process = subprocess.Popen(
command,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
stderr=subprocess.PIPE,
env=os.environ)
else:
# use arguments on other platforms
args = shlex.split(command)
self._local_server_process = subprocess.Popen(args, stderr=subprocess.PIPE)
self._local_server_process = subprocess.Popen(args, stderr=subprocess.PIPE, env=os.environ)
except (OSError, subprocess.SubprocessError) as e:
log.warning('Could not start local server "{}": {}'.format(command, e))
return False
@@ -518,19 +468,8 @@ class LocalServer(QtCore.QObject):
:returns: boolean
"""
status, json_data = HTTPClient(self._settings).getSynchronous("GET", "/version")
if status == 401: # Auth issue that need to be solved later
return True
elif json_data is None:
return False
elif status != 200:
return False
else:
version = json_data.get("version", None)
if version is None:
log.debug("Server is not a GNS3 server")
return False
return True
http_client = HTTPClient(self._settings)
return http_client.checkServerRunning()
def stopLocalServer(self, wait=False):
"""
@@ -541,15 +480,16 @@ class LocalServer(QtCore.QObject):
if self.localServerProcessIsRunning():
self._stopping = True
log.debug("Stopping local server (PID={})".format(self._local_server_process.pid))
log.debug("Stopping local controller (PID={})".format(self._local_server_process.pid))
# local server is running, let's stop it
if self._http_client:
self._http_client.shutdown()
http_client = Controller.instance().httpClient()
if http_client:
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 = ProgressDialog(worker, "Local server", "Waiting for the local controller to stop...", None, busy=True, parent=self.parent())
progress_dialog.show()
progress_dialog.exec_()
progress_dialog.exec()
if self._local_server_process.returncode is None:
self._killLocalServer()
self._server_started_by_me = False
@@ -570,12 +510,12 @@ class LocalServer(QtCore.QObject):
self._local_server_process.wait(timeout=60)
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)
"Local controller",
"The local controller cannot be stopped, would you like to kill it?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No)
if proceed == QtWidgets.QMessageBox.Yes:
if proceed == QtWidgets.QMessageBox.StandardButton.Yes:
self._local_server_process.kill()
@staticmethod

View File

@@ -1,163 +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/>.
import os
import sys
import configparser
import logging
log = logging.getLogger(__name__)
class LocalServerConfig:
"""
Local server configuration.
"""
def __init__(self, config_file=None):
appname = "GNS3"
self._config = configparser.RawConfigParser()
if config_file:
self._config_file = config_file
else:
if sys.platform.startswith("win"):
filename = "gns3_server.ini"
else:
filename = "gns3_server.conf"
from .local_config import LocalConfig
if sys.platform.startswith("win"):
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
else:
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
try:
# create the config file if it doesn't exist
open(self._config_file, "a").close()
except OSError as e:
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.
"""
try:
self._config.read(self._config_file, encoding="utf-8")
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):
"""
Write the configuration file.
"""
try:
log.debug("Write configuration file %s", self._config_file)
with open(self._config_file, "w", encoding="utf-8") as fp:
self._config.write(fp)
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):
"""
Get all the settings from a given section.
:param section: section name
:param default_settings: setting names and default values (dict)
:returns: settings (dict)
"""
if section not in self._config:
self._config[section] = {}
settings = {}
for name, default in default_settings.items():
if isinstance(default, bool):
settings[name] = self._config[section].getboolean(name, default)
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)
if settings[name] == "None":
settings[name] = None
# sync with the config file
self.saveSettings(section, settings)
return settings
def saveSettings(self, section, settings):
"""
Save all the settings in a given section.
:param section: section name
:param settings: settings to save (dict)
"""
changed = False
if section not in self._config:
self._config[section] = {}
changed = True
for name, value in settings.items():
if name not in self._config[section] or self._config[section][name] != str(value):
self._config[section][name] = str(value)
changed = True
if changed:
self.writeConfig()
def deleteSetting(self, section, name):
"""
Delete a specific setting in a given section.
:param section: section name
:param name: setting name to delete
"""
if section in self._config and name in self._config[section]:
del self._config[section][name]
self.writeConfig()
@staticmethod
def instance():
"""
Singleton to return only on instance of LocalServerConfig.
:returns: instance of Config
"""
if not hasattr(LocalServerConfig, "_instance"):
LocalServerConfig._instance = LocalServerConfig()
return LocalServerConfig._instance

View File

@@ -30,16 +30,6 @@ try:
except Exception as e:
print("Fail update installation: {}".format(str(e)))
# WARNING
# Due to buggy user machines we choose to put this as the first loading modules
# otherwise the egg cache is initialized in his standard location and
# if is not writetable the application crash. It's the user fault
# because one day the user as used sudo to run an egg and break his
# filesystem permissions, but it's a common mistake.
from gns3.utils.get_resource import get_resource
import datetime
import traceback
import time
@@ -59,13 +49,14 @@ 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.utils.install_mime_types import install_mime_types
from gns3.dialogs.profile_select import ProfileSelectDialog
from gns3.version import __version__
import logging
log = logging.getLogger(__name__)
from gns3.version import __version__
def locale_check():
"""
@@ -118,6 +109,9 @@ def main():
# an extra argument starting with -psn_. We filter it
if sys.platform.startswith("darwin"):
sys.argv = [a for a in sys.argv if not a.startswith("-psn_")]
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.15.2"):
# Fixes issue on macOS Big Sur: https://github.com/GNS3/gns3-gui/issues/3037
os.environ["QT_MAC_WANTS_LAYER"] = "1"
parser = argparse.ArgumentParser()
parser.add_argument("project", help="load a GNS3 project (.gns3)", metavar="path", nargs="?")
@@ -126,12 +120,24 @@ def main():
parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout")
parser.add_argument("--config", help="Configuration file")
parser.add_argument("--profile", help="Settings profile (blank will use default settings files)")
parser.add_argument("--install-mime-types", help="Install mime types (Linux only)", action="store_true", default=False)
options = parser.parse_args()
exception_file_path = "exceptions.log"
if options.install_mime_types:
install_mime_types()
return
if options.project:
options.project = os.path.abspath(options.project)
try:
import truststore
truststore.inject_into_ssl()
log.info("Using system certificate store for SSL connections")
except ImportError:
pass
if hasattr(sys, "frozen"):
# We add to the path where the OS search executable our binary location starting by GNS3
# packaged binary
@@ -142,8 +148,8 @@ def main():
frozen_dirs = [
frozen_dir,
os.path.normpath(os.path.join(frozen_dir, 'dynamips')),
os.path.normpath(os.path.join(frozen_dir, 'ubridge')),
os.path.normpath(os.path.join(frozen_dir, 'vpcs')),
os.path.normpath(os.path.join(frozen_dir, 'traceng'))
]
os.environ["PATH"] = os.pathsep.join(frozen_dirs) + os.pathsep + os.environ.get("PATH", "")
@@ -151,6 +157,7 @@ def main():
if options.project:
os.chdir(frozen_dir)
def exceptionHook(exception, value, tb):
if exception == KeyboardInterrupt:
@@ -182,12 +189,12 @@ def main():
# catch exceptions to write them in a file
sys.excepthook = exceptionHook
# we only support Python 3 version >= 3.4
if sys.version_info < (3, 4):
raise SystemExit("Python 3.4 or higher is required")
# we only support Python 3 version >= 3.9
if sys.version_info < (3, 9):
raise SystemExit("Python 3.9 or higher is required")
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.5.0"):
raise SystemExit("Requirement is PyQt5 version 5.5.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
if parse_version(QtCore.QT_VERSION_STR) < parse_version("6.3.1"):
raise SystemExit("Requirement is PyQt6 version 6.3.1 or higher, got version {}".format(QtCore.QT_VERSION_STR))
if parse_version(psutil.__version__) < parse_version("2.2.1"):
raise SystemExit("Requirement is psutil version 2.2.1 or higher, got version {}".format(psutil.__version__))
@@ -204,7 +211,7 @@ def main():
# always use the INI format on Windows and OSX (because we don't like the registry and plist files)
if sys.platform.startswith('win') or sys.platform.startswith('darwin'):
QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat)
QtCore.QSettings.setDefaultFormat(QtCore.QSettings.Format.IniFormat)
if sys.platform.startswith('win') and hasattr(sys, "frozen"):
try:
@@ -217,20 +224,27 @@ def main():
if not options.debug:
try:
# hide the console
# win32console.AllocConsole()
console_window = win32console.GetConsoleWindow()
win32gui.ShowWindow(console_window, win32con.SW_HIDE)
parent_window = win32gui.GetParent(console_window)
if not parent_window and console_window:
win32gui.ShowWindow(console_window, win32con.SW_HIDE)
elif parent_window:
win32gui.ShowWindow(parent_window, win32con.SW_HIDE)
else:
log.warning("Could not get the console window")
except win32console.error as e:
print("warning: could not allocate console: {}".format(e))
log.warning("Could not allocate console: {}".format(e))
local_config = LocalConfig.instance()
global app
app = Application(sys.argv, hdpi=local_config.hdpi())
app = Application(sys.argv)
if local_config.multiProfiles() and not options.profile:
profile_select = ProfileSelectDialog()
profile_select.show()
if profile_select.exec_():
if profile_select.exec():
options.profile = profile_select.profile()
else:
sys.exit(0)
@@ -255,8 +269,8 @@ def main():
current_year = datetime.date.today().year
log.info("GNS3 GUI version {}".format(__version__))
log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
log.info("Application started with {}".format("".join(sys.argv)))
log.info("Application started with {}".format(" ".join(sys.argv)))
log.debug("PATH={}".format(os.environ["PATH"]))
# update the exception file path to have it in the same directory as the settings file.
exception_file_path = os.path.join(LocalConfig.instance().configDirectory(), exception_file_path)
@@ -268,7 +282,7 @@ def main():
error_message = "GNS3.app must be moved to the '/Applications' folder before it can be used"
QtWidgets.QMessageBox.critical(False, "Loading error", error_message)
QtCore.QTimer.singleShot(0, app.quit)
app.exec_()
app.exec()
sys.exit(1)
global mainwindow
@@ -291,7 +305,7 @@ def main():
mainwindow.show()
exit_code = app.exec_()
exit_code = app.exec()
signal.signal(signal.SIGINT, orig_sigint)
signal.signal(signal.SIGTERM, orig_sigterm)

View File

@@ -39,6 +39,7 @@ from .dialogs.snapshots_dialog import SnapshotsDialog
from .dialogs.export_debug_dialog import ExportDebugDialog
from .dialogs.doctor_dialog import DoctorDialog
from .dialogs.edit_project_dialog import EditProjectDialog
from .dialogs.image_dialog import ImageDialog
from .dialogs.setup_wizard import SetupWizard
from .settings import GENERAL_SETTINGS
from .items.node_item import NodeItem
@@ -49,7 +50,6 @@ from .topology import Topology
from .http_client import HTTPClient
from .progress import Progress
from .update_manager import UpdateManager
from .utils.analytics import AnalyticsClient
from .dialogs.appliance_wizard import ApplianceWizard
from .dialogs.new_template_wizard import NewTemplateWizard
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
@@ -70,7 +70,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
# signal to tell the view if the user is adding a link or not
adding_link_signal = QtCore.pyqtSignal(bool)
adding_link_signal = QtCore.Signal(bool)
# Signal of settings updates
settings_updated_signal = QtCore.Signal()
@@ -84,6 +84,42 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._settings = {}
self.setupUi(self)
self.setUnifiedTitleAndToolBarOnMac(True)
# This widgets will be disabled when you have no project loaded
self.disableWhenNoProjectWidgets = [
self.uiGraphicsView,
self.uiAnnotateMenu,
self.uiAnnotationToolBar,
self.uiControlToolBar,
self.uiControlMenu,
self.uiSaveProjectAsAction,
self.uiExportProjectAction,
self.uiScreenshotAction,
self.uiSnapshotAction,
self.uiEditProjectAction,
self.uiDeleteProjectAction,
self.uiImportExportConfigsAction,
self.uiLockAllAction,
self.uiShowReadmeAction
]
for widget in self.disableWhenNoProjectWidgets:
widget.setEnabled(False)
self.disableWhenControllerNotConnectedWidgets = [
self.uiNewProjectAction,
self.uiOpenProjectAction,
self.uiImportProjectAction,
self.uiNewTemplateAction,
self.uiImportProjectAction,
self.uiOpenApplianceAction,
self.uiWebUIAction,
self.uiNodesDockWidget
]
for widget in self.disableWhenControllerNotConnectedWidgets:
widget.setEnabled(False)
self._notif_dialog = NotifDialog(self)
# Setup logger
@@ -99,8 +135,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Controller.instance().setParent(self)
LocalServer.instance().setParent(self)
HTTPClient.setProgressCallback(Progress.instance(self))
self._first_file_load = True
self._open_project_path = None
self._loadSettings()
@@ -115,11 +149,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._local_config_timer = QtCore.QTimer(self)
self._local_config_timer.timeout.connect(local_config.checkConfigChanged)
self._local_config_timer.start(1000) # milliseconds
self._analytics_client = AnalyticsClient()
self._template_manager = TemplateManager().instance()
self._appliance_manager = ApplianceManager().instance()
# restore the geometry and state of the main window.
self._save_gui_state_geometry = True
self.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["geometry"].encode()))
self.restoreState(QtCore.QByteArray().fromBase64(self._settings["state"].encode()))
@@ -137,28 +171,28 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiNodesDockWidget.setVisible(False)
# default directories for QFileDialog
self._import_configs_from_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
self._export_configs_to_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
self._screenshots_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
self._pictures_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
self._appliance_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
self._portable_project_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
self._import_configs_from_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation)
self._export_configs_to_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation)
self._screenshots_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.PicturesLocation)
self._pictures_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.PicturesLocation)
self._appliance_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DownloadLocation)
self._portable_project_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DownloadLocation)
self._project_dir = None
# add recent file actions to the File menu
for i in range(0, self._maxrecent_files):
action = QtWidgets.QAction(self.uiFileMenu)
action = QtGui.QAction(self.uiFileMenu)
action.setVisible(False)
action.triggered.connect(self.openRecentFileSlot)
self.recent_file_actions.append(action)
self.uiFileMenu.insertActions(self.uiQuitAction, self.recent_file_actions)
self.recent_file_actions_separator = self.uiFileMenu.insertSeparator(self.uiQuitAction)
self.recent_file_actions_separator.setVisible(False)
self.updateRecentFileActions()
#self.updateRecentFileActions()
# add recent projects to the File menu
for i in range(0, self._maxrecent_files):
action = QtWidgets.QAction(self.uiFileMenu)
action = QtGui.QAction(self.uiFileMenu)
action.setVisible(False)
action.triggered.connect(self.openRecentProjectSlot)
self.recent_project_actions.append(action)
@@ -177,27 +211,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.setWindowTitle("[*] GNS3")
# This widgets will be disable when you have no project loaded
self.disableWhenNoProjectWidgets = [
self.uiGraphicsView,
self.uiAnnotateMenu,
self.uiAnnotationToolBar,
self.uiControlToolBar,
self.uiControlMenu,
self.uiSaveProjectAsAction,
self.uiExportProjectAction,
self.uiScreenshotAction,
self.uiSnapshotAction,
self.uiEditProjectAction,
self.uiDeleteProjectAction,
self.uiImportExportConfigsAction,
self.uiLockAllAction
]
# This widgets are not enabled if it's a remote controller (no access to the local file system)
self.disableWhenRemoteContollerWidgets = [
# self.uiImportExportConfigsAction
]
# detect if the SVG module is correctly installed
supported_image_formats = [fmt.data().decode('utf-8') for fmt in QtGui.QImageReader().supportedImageFormats()]
log.debug("Supported image formats: %s", ", ".join(supported_image_formats))
if "svg" not in supported_image_formats:
log.warning("SVG image format is not supported, is the Qt SVG module installed? (qt6-svg-plugins)")
# load initial stuff once the event loop isn't busy
self.run_later(0, self.startupLoading)
@@ -212,6 +230,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiOpenProjectAction.triggered.connect(self.openProjectActionSlot)
self.uiOpenApplianceAction.triggered.connect(self.openApplianceActionSlot)
self.uiNewTemplateAction.triggered.connect(self._newTemplateActionSlot)
self.uiImageManagementAction.triggered.connect(self._imageManagementActionSlot)
self.uiSaveProjectAsAction.triggered.connect(self._saveProjectAsActionSlot)
self.uiExportProjectAction.triggered.connect(self._exportProjectActionSlot)
self.uiImportProjectAction.triggered.connect(self._importProjectActionSlot)
@@ -236,11 +255,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiResetPortLabelsAction.triggered.connect(self._resetPortLabelsActionSlot)
self.uiShowPortNamesAction.triggered.connect(self._showPortNamesActionSlot)
self.uiShowGridAction.triggered.connect(self._showGridActionSlot)
self.uiShowReadmeAction.triggered.connect(self._showReadmeActionSlot)
self.uiSnapToGridAction.triggered.connect(self._snapToGridActionSlot)
self.uiLockAllAction.triggered.connect(self._lockActionSlot)
self.uiResetGUIStateAction.triggered.connect(self._resetGUIState)
self.uiResetDocksAction.triggered.connect(self._resetDocksSlot)
# tool menu connections
self.uiWebInterfaceAction.triggered.connect(self._openLightWebInterfaceActionSlot)
self.uiWebUIAction.triggered.connect(self._openWebInterfaceActionSlot)
# control menu connections
@@ -250,6 +271,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiReloadAllAction.triggered.connect(self._reloadAllActionSlot)
self.uiAuxConsoleAllAction.triggered.connect(self._auxConsoleAllActionSlot)
self.uiConsoleAllAction.triggered.connect(self._consoleAllActionSlot)
self.uiResetConsoleAllAction.triggered.connect(self._consoleResetAllActionSlot)
# device menu is contextual and is build on-the-fly
self.uiDeviceMenu.aboutToShow.connect(self._deviceMenuActionSlot)
@@ -260,6 +282,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiDrawRectangleAction.triggered.connect(self._drawRectangleActionSlot)
self.uiDrawEllipseAction.triggered.connect(self._drawEllipseActionSlot)
self.uiDrawLineAction.triggered.connect(self._drawLineActionSlot)
self.uiEditReadmeAction.triggered.connect(self._editReadmeActionSlot)
# help menu connections
self.uiOnlineHelpAction.triggered.connect(self._onlineHelpActionSlot)
@@ -270,6 +293,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiExportDebugInformationAction.triggered.connect(self._exportDebugInformationSlot)
self.uiDoctorAction.triggered.connect(self._doctorSlot)
self.uiAcademyAction.triggered.connect(self._academyActionSlot)
self.uiShortcutsAction.triggered.connect(self._shortcutsActionSlot)
# browsers tool bar connections
self.uiBrowseRoutersAction.triggered.connect(self._browseRoutersActionSlot)
@@ -322,14 +346,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
self.settings_updated_signal.emit()
def _openLightWebInterfaceActionSlot(self):
if Controller.instance().connected():
QtGui.QDesktopServices.openUrl(QtCore.QUrl(Controller.instance().httpClient().fullUrl()))
def _openWebInterfaceActionSlot(self):
if Controller.instance().connected():
base_url = Controller.instance().httpClient().fullUrl()
webui_url = "{}/static/web-ui/bundled".format(base_url)
webui_url = f"{base_url}/static/web-ui/bundled"
QtGui.QDesktopServices.openUrl(QtCore.QUrl(webui_url))
def _showGridActionSlot(self):
@@ -374,12 +394,27 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
item.updateNode()
item.update()
def analyticsClient(self):
def _resetGUIState(self):
"""
Return the analytics client
Reset the GUI state.
"""
return self._analytics_client
self._save_gui_state_geometry = False
self.close()
if hasattr(sys, "frozen"):
QtCore.QProcess.startDetached(os.path.abspath(sys.executable), sys.argv)
else:
QtWidgets.QMessageBox.information(self, "GUI state","The GUI state has been reset, please restart the application")
def _resetDocksSlot(self):
"""
Reset the dock widgets.
"""
self.uiTopologySummaryDockWidget.setFloating(False)
self.uiComputeSummaryDockWidget.setFloating(False)
self.uiConsoleDockWidget.setFloating(False)
self.uiNodesDockWidget.setFloating(False)
def _newProjectActionSlot(self):
"""
@@ -392,7 +427,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._project_dialog = ProjectDialog(self)
self._project_dialog.show()
create_new_project = self._project_dialog.exec_()
create_new_project = self._project_dialog.exec()
if create_new_project:
Topology.instance().createLoadProject(self._project_dialog.getProjectSettings())
@@ -406,7 +441,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
dialog = NewTemplateWizard(self)
dialog.show()
dialog.exec_()
dialog.exec()
def _imageManagementActionSlot(self):
"""
Called when user wants to manage images
"""
dialog = ImageDialog(self)
dialog.show()
dialog.exec()
@qslot
def openApplianceActionSlot(self, *args):
@@ -418,7 +462,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if not os.path.exists(self._appliance_dir):
directory = Topology.instance().projectsDirPath()
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Import appliance", directory,
"All files (*.*);;GNS3 Appliance (*.gns3appliance *.gns3a)",
"All files (*);;GNS3 Appliance (*.gns3appliance *.gns3a)",
"GNS3 Appliance (*.gns3appliance *.gns3a)")
if path:
self.loadPath(path)
@@ -437,7 +481,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if self._project_dir is None or not os.path.exists(self._project_dir):
directory = Topology.instance().projectsDirPath()
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open project", directory,
"All files (*.*);;GNS3 Project (*.gns3);;GNS3 Portable Project (*.gns3project *.gns3p);;NET files (*.net)",
"All files (*);;GNS3 Project (*.gns3);;GNS3 Portable Project (*.gns3project *.gns3p);;NET files (*.net)",
"GNS3 Project (*.gns3)")
if path:
self.loadPath(path)
@@ -501,10 +545,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
QtWidgets.QMessageBox.critical(self, "Appliance", "Error while importing appliance {}: {}".format(path, str(e)))
return
self._appliance_wizard.show()
self._appliance_wizard.exec_()
self._appliance_wizard.exec()
elif path.endswith(".gns3"):
if Controller.instance().isRemote():
QtWidgets.QMessageBox.critical(self, "Open project", "Cannot open a .gns3 file on a remote server, please use a portable project (.gns3p) instead")
QtWidgets.QMessageBox.critical(self, "Open project", "Cannot open a .gns3 file on a remote server, please use a project (.gns3p) instead")
return
else:
Topology.instance().loadProject(path)
@@ -543,8 +587,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
Refresh widgets that should be visible or not
"""
for widget in self.disableWhenRemoteContollerWidgets:
widget.setVisible(not Controller.instance().isRemote())
for widget in self.disableWhenControllerNotConnectedWidgets:
widget.setEnabled(Controller.instance().connected())
# No projects
if Topology.instance().project() is None:
@@ -580,7 +625,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Exports all configs to a directory.
"""
path = QtWidgets.QFileDialog.getExistingDirectory(self, "Export directory", self._export_configs_to_dir, QtWidgets.QFileDialog.ShowDirsOnly)
path = QtWidgets.QFileDialog.getExistingDirectory(self, "Export directory", self._export_configs_to_dir, QtWidgets.QFileDialog.Option.ShowDirsOnly)
if path:
self._export_configs_to_dir = os.path.dirname(path)
for module in MODULES:
@@ -593,7 +638,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Imports all configs from a directory.
"""
path = QtWidgets.QFileDialog.getExistingDirectory(self, "Import directory", self._import_configs_from_dir, QtWidgets.QFileDialog.ShowDirsOnly)
path = QtWidgets.QFileDialog.getExistingDirectory(self, "Import directory", self._import_configs_from_dir, QtWidgets.QFileDialog.Option.ShowDirsOnly)
if path:
self._import_configs_from_dir = os.path.dirname(path)
for module in MODULES:
@@ -611,12 +656,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
scene = self.uiGraphicsView.scene()
scene.clearSelection()
source = scene.itemsBoundingRect().adjusted(-20.0, -20.0, 20.0, 20.0)
image = QtGui.QImage(source.size().toSize(), QtGui.QImage.Format_RGB32)
image.fill(QtCore.Qt.white)
image = QtGui.QImage(source.size().toSize(), QtGui.QImage.Format.Format_RGB32)
image.fill(QtCore.Qt.GlobalColor.white)
painter = QtGui.QPainter(image)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
painter.setRenderHint(QtGui.QPainter.TextAntialiasing, True)
painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True)
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True)
painter.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing, True)
painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True)
scene.render(painter, source=source)
painter.end()
# TODO: quality option
@@ -706,7 +751,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
dialog = SnapshotsDialog(self, project)
dialog.show()
dialog.exec_()
dialog.exec()
def _selectAllActionSlot(self):
"""
@@ -731,12 +776,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot to switch to full screen.
"""
if not self.windowState() & QtCore.Qt.WindowFullScreen:
if not self.windowState() & QtCore.Qt.WindowState.WindowFullScreen:
# switch to full screen
self.setWindowState(self.windowState() | QtCore.Qt.WindowFullScreen)
self.setWindowState(self.windowState() | QtCore.Qt.WindowState.WindowFullScreen)
else:
# switch back to normal
self.setWindowState(self.windowState() & ~QtCore.Qt.WindowFullScreen)
self.setWindowState(self.windowState() & ~QtCore.Qt.WindowState.WindowFullScreen)
def _zoomInActionSlot(self):
"""
@@ -772,7 +817,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
view = self.uiGraphicsView
bounding_rect = view.scene().itemsBoundingRect().adjusted(-20.0, -20.0, 20.0, 20.0)
view.ensureVisible(bounding_rect)
view.fitInView(bounding_rect, QtCore.Qt.KeepAspectRatio)
view.fitInView(bounding_rect, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
def _showLayersActionSlot(self):
"""
@@ -813,6 +858,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
Slot called when starting all the nodes.
"""
reply = QtWidgets.QMessageBox.question(self, "Confirm Start All", "Are you sure you want to start all devices?",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
project = Topology.instance().project()
if project is not None:
project.start_all_nodes()
@@ -822,6 +874,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot called when suspending all the nodes.
"""
reply = QtWidgets.QMessageBox.question(self, "Confirm Suspend All", "Are you sure you want to suspend all devices?",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
project = Topology.instance().project()
if project is not None:
project.suspend_all_nodes()
@@ -831,6 +889,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot called when stopping all the nodes.
"""
reply = QtWidgets.QMessageBox.question(self, "Confirm Stop All", "Are you sure you want to stop all devices?",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
project = Topology.instance().project()
if project is not None:
project.stop_all_nodes()
@@ -840,10 +904,25 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot called when reloading all the nodes.
"""
reply = QtWidgets.QMessageBox.question(self, "Confirm Reload All", "Are you sure you want to reload all devices?",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
project = Topology.instance().project()
if project is not None:
project.reload_all_nodes()
def _consoleResetAllActionSlot(self):
"""
Slot called when reset all console connections.
"""
project = Topology.instance().project()
if project is not None:
project.reset_console_all_nodes()
def _deviceMenuActionSlot(self):
"""
Slot to contextually show the device menu.
@@ -878,7 +957,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot called when inserting an image on the scene.
"""
# supported image file formats
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.gif *.pbm *.pgm *.png *.ppm *.xbm *.xpm);;All files (*.*)"
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.gif *.pbm *.pgm *.png *.ppm *.xbm *.xpm);;All files (*)"
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Image", self._pictures_dir, file_formats)
if not path:
@@ -914,7 +993,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot to launch a browser pointing to the documentation page.
"""
QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://gns3.com/support/docs"))
QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://docs.gns3.com/"))
def _checkForUpdateActionSlot(self, silent=False):
"""
@@ -931,12 +1010,18 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot to open the setup wizard.
"""
with Progress.instance().context(min_duration=0):
setup_wizard = SetupWizard(self)
setup_wizard.show()
res = setup_wizard.exec_()
# start and connect to the local server if needed
LocalServer.instance().localServerAutoStartIfRequired()
setup_wizard = SetupWizard(self)
setup_wizard.show()
setup_wizard.exec()
def _shortcutsActionSlot(self):
shortcuts_text = ""
for action in self.findChildren(QtGui.QAction):
shortcut = action.shortcut().toString()
if shortcut:
shortcuts_text += f"{action.toolTip()}: {shortcut}\n"
QtWidgets.QMessageBox.information(self, "Shortcuts", shortcuts_text)
def _aboutQtActionSlot(self):
"""
@@ -952,7 +1037,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
dialog = AboutDialog(self)
dialog.show()
dialog.exec_()
dialog.exec()
def _exportDebugInformationSlot(self):
"""
@@ -961,7 +1046,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
dialog = ExportDebugDialog(self, Topology.instance().project())
dialog.show()
dialog.exec_()
dialog.exec()
def _doctorSlot(self):
"""
@@ -970,7 +1055,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
dialog = DoctorDialog(self)
dialog.show()
dialog.exec_()
dialog.exec()
def _academyActionSlot(self):
"""
@@ -1058,11 +1143,23 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
with Progress.instance().context(min_duration=0):
dialog = PreferencesDialog(self)
dialog.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["preferences_dialog_geometry"].encode()))
#dialog.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["preferences_dialog_geometry"].encode()))
dialog.show()
dialog.exec_()
self._settings["preferences_dialog_geometry"] = bytes(dialog.saveGeometry().toBase64()).decode()
self.setSettings(self._settings)
dialog.exec()
#self._settings["preferences_dialog_geometry"] = bytes(dialog.saveGeometry().toBase64()).decode()
#self.setSettings(self._settings)
def _editReadmeActionSlot(self):
"""
Slot to edit the README file
"""
Topology.instance().editReadme()
def _showReadmeActionSlot(self):
"""
Slot to show the README file
"""
Topology.instance().showReadme()
def resizeEvent(self, event):
self._notif_dialog.resize()
@@ -1077,9 +1174,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
key = event.key()
# if the user is adding a link and press Escape, then cancel the link addition.
if self.uiAddLinkAction.isChecked() and key == QtCore.Qt.Key_Escape:
if self.uiAddLinkAction.isChecked() and key == QtCore.Qt.Key.Key_Escape:
self.uiAddLinkAction.setChecked(False)
self._addLinkActionSlot()
elif key == QtCore.Qt.Key.Key_C and (event.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier):
status_bar_message = self.uiStatusBar.currentMessage()
if status_bar_message:
QtWidgets.QApplication.clipboard().setText(status_bar_message)
else:
super().keyPressEvent(event)
@@ -1092,8 +1193,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if Topology.instance().project():
reply = QtWidgets.QMessageBox.question(self, "Confirm Exit", "Are you sure you want to exit GNS3?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.No:
event.ignore()
return
@@ -1102,8 +1203,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
progress.setCancelButtonText("Force quit")
log.debug("Close the Main Window")
self._analytics_client.sendScreenView("Main Window", session_start=False)
self._finish_application_closing(close_windows=False)
event.accept()
self.uiConsoleTextEdit.closeIO()
@@ -1118,8 +1217,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
log.debug("_finish_application_closing")
self._settings["geometry"] = bytes(self.saveGeometry().toBase64()).decode()
self._settings["state"] = bytes(self.saveState().toBase64()).decode()
if self._save_gui_state_geometry:
self._settings["geometry"] = bytes(self.saveGeometry().toBase64()).decode()
self._settings["state"] = bytes(self.saveState().toBase64()).decode()
else:
self._settings["geometry"] = ""
self._settings["state"] = ""
self.setSettings(self._settings)
Controller.instance().stopListenNotifications()
@@ -1153,8 +1256,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if not LocalConfig.instance().isMainGui():
reply = QtWidgets.QMessageBox.warning(self, "GNS3", "Another GNS3 GUI is already running. Continue?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.No:
sys.exit(1)
return
@@ -1182,8 +1285,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# restore debug level
if self._settings["debug_level"]:
print("Activating debugging (use command 'debug 0' to deactivate)")
root = logging.getLogger()
root.addHandler(logging.StreamHandler(sys.stdout))
root.setLevel(logging.DEBUG)
# restore the style
self._setStyle(self._settings.get("style"))
@@ -1191,20 +1295,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Controller.instance().connected_signal.connect(self._controllerConnectedSlot)
Controller.instance().project_list_updated_signal.connect(self.updateRecentProjectActions)
self._analytics_client.sendScreenView("Main Window")
self.uiGraphicsView.setEnabled(False)
# show the setup wizard
if not self._settings["hide_setup_wizard"]:
self._setupWizardActionSlot()
else:
# start and connect to the local server if needed
LocalServer.instance().localServerAutoStartIfRequired()
if self._open_file_at_startup:
self.loadPath(self._open_file_at_startup)
self._open_file_at_startup = None
elif Topology.instance().project() is None:
self._newProjectActionSlot()
if Controller.instance().isRemote():
Controller.instance().connect()
else:
# start and connect to the local server if needed
LocalServer.instance().localServerAutoStartIfRequired()
if self._settings["check_for_update"]:
# automatic check for update every week (604800 seconds)
@@ -1357,9 +1458,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.recent_file_actions_separator.setVisible(False)
def _controllerConnectedSlot(self):
self.updateRecentFileActions()
self._refreshVisibleWidgets()
if self._settings["hide_setup_wizard"]:
if self._open_file_at_startup:
self.loadPath(self._open_file_at_startup)
self._open_file_at_startup = None
elif Topology.instance().project() is None and QtWidgets.QApplication.activeModalWidget() is None:
self._newProjectActionSlot()
def run_later(self, counter, callback):
"""
Run a function after X milliseconds
@@ -1372,21 +1481,21 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def _exportProjectActionSlot(self):
"""
Slot called to export a portable project
Slot called to export a project
"""
Topology.instance().exportProject()
def _importProjectActionSlot(self):
"""
Slot called to import a portable project
Slot called to import a project
"""
directory = self._portable_project_dir
if not os.path.exists(directory):
directory = Topology.instance().projectsDirPath()
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open portable project", directory,
"All files (*.*);;GNS3 Portable Project (*.gns3project *.gns3p)",
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open project", directory,
"All files (*);;GNS3 Portable Project (*.gns3project *.gns3p)",
"GNS3 Portable Project (*.gns3project *.gns3p)")
if path:
Topology.instance().importProject(path)
@@ -1397,7 +1506,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
return
dialog = EditProjectDialog(self)
dialog.show()
dialog.exec_()
dialog.exec()
def _deleteProjectActionSlot(self):
if Topology.instance().project() is None:
@@ -1406,8 +1515,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self,
"GNS3",
"The project will be deleted from disk. All files will be removed including the project subdirectories. Continue?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
Topology.instance().deleteProject()
def _setStyle(self, style_name):

View File

@@ -19,12 +19,9 @@ from gns3.modules.builtin import Builtin
from gns3.modules.dynamips import Dynamips
from gns3.modules.iou import IOU
from gns3.modules.vpcs import VPCS
from gns3.modules.traceng import TraceNG
from gns3.modules.virtualbox import VirtualBox
from gns3.modules.qemu import Qemu
from gns3.modules.vmware import VMware
from gns3.modules.docker import Docker
#MODULES = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker, TraceNG]
#FIXME: deactivate TraceNG module
MODULES = [Builtin, VPCS, Dynamips, IOU, Qemu, VirtualBox, VMware, Docker]

View File

@@ -19,9 +19,7 @@
Built-in module implementation.
"""
from gns3.qt import QtWidgets
from gns3.local_config import LocalConfig
from gns3.local_server_config import LocalServerConfig
from ..module import Module
from .cloud import Cloud
@@ -53,14 +51,15 @@ class Builtin(Module):
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
server_settings = {}
config = LocalServerConfig.instance()
if self._settings["default_nat_interface"]:
# save some settings to the local server config file
server_settings["default_nat_interface"] = self._settings["default_nat_interface"]
config.saveSettings(self.__class__.__name__, server_settings)
else:
config.deleteSetting(self.__class__.__name__, "default_nat_interface")
# FIXME: handle server side config
# server_settings = {}
# config = LocalServerConfig.instance()
# if self._settings["default_nat_interface"]:
# # save some settings to the local server config file
# server_settings["default_nat_interface"] = self._settings["default_nat_interface"]
# config.saveSettings(self.__class__.__name__, server_settings)
# else:
# config.deleteSetting(self.__class__.__name__, "default_nat_interface")
def _loadSettings(self):
"""

View File

@@ -53,7 +53,7 @@ class ATMSwitch(Node):
info = """ATM switch {name} is always-on
Running on server {host} with port {port}
Local ID is {id} and server ID is {node_id}
Local ID is {id} and node ID is {node_id}
Hardware is Dynamips emulated simple ATM switch
""".format(name=self.name(),
id=self.id(),

View File

@@ -42,6 +42,7 @@ class Cloud(Node):
self._always_on = True
self._interfaces = {}
self._cloud_settings = {"ports_mapping": [],
"usage": "",
"remote_console_host": CLOUD_SETTINGS["remote_console_host"],
"remote_console_port": CLOUD_SETTINGS["remote_console_port"],
"remote_console_type": CLOUD_SETTINGS["remote_console_type"],
@@ -139,7 +140,8 @@ class Cloud(Node):
port_info += " Port {name} {description}\n".format(name=port.name(),
description=port.description())
return info + port_info
usage = "\n" + self._settings.get("usage")
return info + port_info + usage
def configPage(self):
"""

View File

@@ -37,7 +37,7 @@ class CloudWizard(VMWizard, Ui_CloudNodeWizard):
super().__init__(cloud_nodes, parent)
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/cloud.svg"))
self.setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(":/symbols/cloud.svg"))
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
def getSettings(self):
@@ -48,7 +48,7 @@ class CloudWizard(VMWizard, Ui_CloudNodeWizard):
"""
settings = {"name": self.uiNameLineEdit.text(),
"symbol": ":/symbols/cloud.svg",
"symbol": "cloud",
"compute_id": self._compute_id}
return settings

View File

@@ -38,7 +38,7 @@ class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
super().__init__(ethernet_hubs, parent)
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/hub.svg"))
self.setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(":/symbols/hub.svg"))
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
def getSettings(self):
@@ -54,7 +54,7 @@ class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
"name": "Ethernet{}".format(port_number)})
settings = {"name": self.uiNameLineEdit.text(),
"symbol": ":/symbols/hub.svg",
"symbol": "hub",
"category": Node.switches,
"compute_id": self._compute_id,
"ports_mapping": ports}

View File

@@ -38,7 +38,7 @@ class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
super().__init__(ethernet_switches, parent)
self.setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/ethernet_switch.svg"))
self.setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(":/symbols/ethernet_switch.svg"))
self.uiNameWizardPage.registerField("name*", self.uiNameLineEdit)
def getSettings(self):
@@ -54,10 +54,10 @@ class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
"name": "Ethernet{}".format(port_number),
"type": "access",
"vlan": 1,
"ethertype": ""})
"ethertype": "0x8100"})
settings = {"name": self.uiNameLineEdit.text(),
"symbol": ":/symbols/ethernet_switch.svg",
"symbol": "ethernet_switch",
"category": Node.switches,
"compute_id": self._compute_id,
"ports_mapping": ports}

View File

@@ -49,7 +49,7 @@ class EthernetHub(Node):
info = """Ethernet hub {name} is always-on
Running on server {host} with port {port}
Local ID is {id} and server ID is {node_id}
Local ID is {id} and node ID is {node_id}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,

Some files were not shown because too many files have changed in this diff Show More