Compare commits
293 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dd4020666 | ||
|
|
83d0957d50 | ||
|
|
1767a441ab | ||
|
|
41b7c2c33e | ||
|
|
1cebcabff3 | ||
|
|
53cc823859 | ||
|
|
dd6329a1f3 | ||
|
|
68633c4732 | ||
|
|
161421abcf | ||
|
|
889d41636d | ||
|
|
878cfb2fe5 | ||
|
|
141767e0d9 | ||
|
|
a5976a61ac | ||
|
|
2e2a2c362f | ||
|
|
0654b79703 | ||
|
|
acfcabb743 | ||
|
|
b4271b0d7e | ||
|
|
dd745e0081 | ||
|
|
c2033ccf06 | ||
|
|
731715f5a3 | ||
|
|
fef7280e55 | ||
|
|
c3ccbfe7b8 | ||
|
|
de38c4a561 | ||
|
|
45f5155efc | ||
|
|
5fbba828bb | ||
|
|
50a10ae72c | ||
|
|
996709e930 | ||
|
|
604f308dad | ||
|
|
ca9d3e41a5 | ||
|
|
f97e18cf47 | ||
|
|
89a182171d | ||
|
|
d971214614 | ||
|
|
507afac692 | ||
|
|
cae1848103 | ||
|
|
e08aebf0b8 | ||
|
|
5afa1edc4b | ||
|
|
b056de321a | ||
|
|
da2852713f | ||
|
|
6468151dba | ||
|
|
52450989c9 | ||
|
|
887e1b72c1 | ||
|
|
6a460171c2 | ||
|
|
976cce8391 | ||
|
|
ade73ecad8 | ||
|
|
11f7423d2d | ||
|
|
21c1acc48a | ||
|
|
17c49d3e9a | ||
|
|
5c8a8e97d8 | ||
|
|
50b3dc3dc7 | ||
|
|
91dd83b285 | ||
|
|
696a501eaa | ||
|
|
bb58671c23 | ||
|
|
34ef843bbe | ||
|
|
b0535a26e7 | ||
|
|
fc6f8109cc | ||
|
|
dbfaa48ca9 | ||
|
|
5d997ba53e | ||
|
|
f5d49d1b69 | ||
|
|
4ef0d1606a | ||
|
|
a1b577d617 | ||
|
|
e82c82236d | ||
|
|
2089149fbd | ||
|
|
4bfe8b0c99 | ||
|
|
4d178fd011 | ||
|
|
eae7cfe39f | ||
|
|
1daf77ed8d | ||
|
|
b197f0dad1 | ||
|
|
9adcaa617c | ||
|
|
b77f867acd | ||
|
|
ec41dfa60c | ||
|
|
39bd40591c | ||
|
|
4876f7ec4b | ||
|
|
c68694687f | ||
|
|
8aaa58dd61 | ||
|
|
d9c1d11480 | ||
|
|
86d27bb474 | ||
|
|
0f584a062c | ||
|
|
d6b89831e3 | ||
|
|
da83230622 | ||
|
|
71362104ea | ||
|
|
8d58898b0b | ||
|
|
3dfba11d51 | ||
|
|
e2f5f92a54 | ||
|
|
1ac555ca7d | ||
|
|
fcb5ed3272 | ||
|
|
dfe3ccf460 | ||
|
|
5e75d5a6d2 | ||
|
|
afb3d30e9f | ||
|
|
43f464de31 | ||
|
|
906db5e3a1 | ||
|
|
879b5b821c | ||
|
|
ded088a28b | ||
|
|
ed1b479381 | ||
|
|
0d23a930e3 | ||
|
|
960fda17f9 | ||
|
|
45307824ab | ||
|
|
bf0b0290eb | ||
|
|
7185068256 | ||
|
|
bb65a80788 | ||
|
|
520c5f67f5 | ||
|
|
75fc344ce7 | ||
|
|
31df679e54 | ||
|
|
411035a8bc | ||
|
|
61de398850 | ||
|
|
adb78dbabb | ||
|
|
1f7bb58220 | ||
|
|
564415cfeb | ||
|
|
5b18183cd5 | ||
|
|
edc13c3f90 | ||
|
|
a3eec47066 | ||
|
|
58a00744a2 | ||
|
|
a9bfc96dc9 | ||
|
|
fa3a2bc714 | ||
|
|
ff1d3a71a6 | ||
|
|
a604c1b9f9 | ||
|
|
09891680cc | ||
|
|
99a535f6c0 | ||
|
|
287c829fdb | ||
|
|
e1699470ab | ||
|
|
a8b55b5807 | ||
|
|
5c20302e17 | ||
|
|
6c538a2102 | ||
|
|
7334e1509c | ||
|
|
9d0e940e8d | ||
|
|
55632b7f13 | ||
|
|
d79230f715 | ||
|
|
358348b07b | ||
|
|
af7d9e6704 | ||
|
|
885acafa04 | ||
|
|
366fc3d854 | ||
|
|
db5f96556c | ||
|
|
c68a2a7734 | ||
|
|
bab1027396 | ||
|
|
25ed017d5d | ||
|
|
d69c915d3f | ||
|
|
6670b0d362 | ||
|
|
59a89cc40b | ||
|
|
25e665ff2c | ||
|
|
617cf7ef02 | ||
|
|
b4daafffad | ||
|
|
f329039987 | ||
|
|
d43804374f | ||
|
|
f54a7f6096 | ||
|
|
8a51d5edbe | ||
|
|
2a845a1cf8 | ||
|
|
2e793f065a | ||
|
|
0a69cced62 | ||
|
|
320e8243ee | ||
|
|
37af40e594 | ||
|
|
669fee1877 | ||
|
|
db8155a818 | ||
|
|
04d81efe92 | ||
|
|
96e2a42012 | ||
|
|
2af2a67fae | ||
|
|
26a0da0564 | ||
|
|
7b52cd4f81 | ||
|
|
f0d12c91a2 | ||
|
|
7d0b6bf0d2 | ||
|
|
b45cbeaced | ||
|
|
80908feeba | ||
|
|
9a8f2e65de | ||
|
|
4bb74be5a8 | ||
|
|
da6581d1ac | ||
|
|
1fcb9a4cd4 | ||
|
|
036e9ef8a4 | ||
|
|
4bb99e7917 | ||
|
|
f10c86160f | ||
|
|
f3b3d7565d | ||
|
|
1d0a173689 | ||
|
|
b2363434a2 | ||
|
|
ca3e6f0472 | ||
|
|
f1b19f4633 | ||
|
|
ed57ac3de5 | ||
|
|
6b35992e5a | ||
|
|
5f97d7891f | ||
|
|
6a46c26a37 | ||
|
|
d43411dafd | ||
|
|
0427383457 | ||
|
|
991387f483 | ||
|
|
8ff966dd54 | ||
|
|
271850cfec | ||
|
|
f09c67dd3d | ||
|
|
aea8e01d13 | ||
|
|
5615141ed7 | ||
|
|
c06491b112 | ||
|
|
b01c03855e | ||
|
|
01e101beac | ||
|
|
18eba37b85 | ||
|
|
3b7fb3329f | ||
|
|
5b901fa115 | ||
|
|
25bd1aa974 | ||
|
|
b0c374a043 | ||
|
|
548663c964 | ||
|
|
567cd6485f | ||
|
|
d69f00a370 | ||
|
|
1c6eb5eb4d | ||
|
|
d3c3ae5143 | ||
|
|
f2b2e6f618 | ||
|
|
a617d5aedf | ||
|
|
9e33cd24bb | ||
|
|
6f767d7455 | ||
|
|
f3ba40de43 | ||
|
|
7e60e4021b | ||
|
|
557c47d995 | ||
|
|
ce8f9206d9 | ||
|
|
1ac14e0f6d | ||
|
|
e0651e349c | ||
|
|
15889a0ac5 | ||
|
|
f38ab34ea0 | ||
|
|
087172d024 | ||
|
|
a46b08bea1 | ||
|
|
a6c3e2a4bb | ||
|
|
f89ff86808 | ||
|
|
50cdb2432d | ||
|
|
04ea58395a | ||
|
|
6331f54b88 | ||
|
|
a91683f6ff | ||
|
|
62b7d29e4c | ||
|
|
926aec9089 | ||
|
|
586b640967 | ||
|
|
52322eb982 | ||
|
|
340ec2d543 | ||
|
|
2d3c7ac0c2 | ||
|
|
3cd15e5f1a | ||
|
|
1a2d3d65c1 | ||
|
|
c93a543ccb | ||
|
|
579fb6ceb8 | ||
|
|
391ac73f1a | ||
|
|
eedec3f999 | ||
|
|
c103e2deba | ||
|
|
48e7ef07cf | ||
|
|
c6f8198974 | ||
|
|
c4561e81eb | ||
|
|
b3603ea364 | ||
|
|
dd44582347 | ||
|
|
270301d9b5 | ||
|
|
4614543edf | ||
|
|
80b7ece5a8 | ||
|
|
54511b2c45 | ||
|
|
ed36917cf3 | ||
|
|
7545c600bc | ||
|
|
4dd7db5a86 | ||
|
|
6d2ffc4614 | ||
|
|
bc14f15a61 | ||
|
|
2c7de627f7 | ||
|
|
76d9fcf60e | ||
|
|
c3049ea843 | ||
|
|
d6432c2e88 | ||
|
|
89b9e6c332 | ||
|
|
54b5d8f347 | ||
|
|
237ef785ad | ||
|
|
0ddd91af43 | ||
|
|
7f5db61722 | ||
|
|
fcc5c4c114 | ||
|
|
68f3dc763d | ||
|
|
786306304b | ||
|
|
4f631669e5 | ||
|
|
0b8fb93752 | ||
|
|
422f6004b1 | ||
|
|
717d683b44 | ||
|
|
932f737ed9 | ||
|
|
64a0ee37de | ||
|
|
ec8d214c08 | ||
|
|
880ac5e8c3 | ||
|
|
feb40a6250 | ||
|
|
fd7b915e96 | ||
|
|
5fbb6cbf61 | ||
|
|
fda948cc5b | ||
|
|
a9b18f1771 | ||
|
|
04f9a1cf8c | ||
|
|
af79471afd | ||
|
|
6067786783 | ||
|
|
090fc63bb6 | ||
|
|
029a1df7f7 | ||
|
|
b47aa95b3e | ||
|
|
ffb364591f | ||
|
|
6a9440c978 | ||
|
|
758054cfd3 | ||
|
|
39e6a6e2ab | ||
|
|
7a74685c0a | ||
|
|
f48eff2344 | ||
|
|
b8a583d3f6 | ||
|
|
d01f15c4df | ||
|
|
3cbad22a04 | ||
|
|
c61a99e78d | ||
|
|
e318610983 | ||
|
|
ab7cc29fa2 | ||
|
|
2788019e17 | ||
|
|
819bb1c58e | ||
|
|
723f806a52 | ||
|
|
a5093e06d1 | ||
|
|
f33c01ac58 | ||
|
|
8c382e5b7d |
2
.github/workflows/testing.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build and run Docker image
|
||||
run: |
|
||||
docker build -t gns3-gui-test .
|
||||
|
||||
268
CHANGELOG
@@ -1,79 +1,25 @@
|
||||
# Change Log
|
||||
|
||||
## 2.2.59 08/05/2026
|
||||
## 3.0.2 03/01/2025
|
||||
|
||||
* Remove psutil version check
|
||||
* Fix remaining PyQt6 compatibility issues. Fixes #3822
|
||||
* Add --title to remote-viewer console commands. Fixes #3783
|
||||
* Fix deleting drawings. Ref #3810
|
||||
|
||||
## 2.2.58.1 12/04/2026
|
||||
|
||||
* Fix callback issues in found in v2.2.58
|
||||
|
||||
## 2.2.58 10/04/2026
|
||||
|
||||
* Update snapshot date & time format
|
||||
* Fix bug when HTTP DELETE requests are silently dropped. Fixes #3810
|
||||
* Show if a linked base VM or not in device's properties
|
||||
* Add HTTP method to response timeout slot. Ref #3810
|
||||
* Add default link style section in preferences
|
||||
* Update the default style colors based on the interface style
|
||||
* Support bring to front for vnc and spice consoles on Linux. Fixes #3783
|
||||
* Update appliance schemas
|
||||
* Fix bug when adding multiple nodes at the same time. Fixes #3807
|
||||
* Fix QWebSocket error signal. Fixes #3804
|
||||
|
||||
## 2.2.57 23/03/2026
|
||||
|
||||
* Fix errors from controller are not reported
|
||||
* Deactivate 'use default IOU values' by default and update RAM/NVRAM values
|
||||
* Fix QMenu parents
|
||||
* Fix UltraVNC preconfigured command
|
||||
* Dark style
|
||||
* Fix bug when dragging scene
|
||||
* Upgrade pytest and jsonschema dependencies
|
||||
* Upgrade sentry-sdk and psutil dependencies
|
||||
* Fix mouse manipulation bug
|
||||
* Add Multi VNC tab handling in Superputty
|
||||
* Fixing tab name in Superputty + Adding Superputty VNC support
|
||||
* settings.py: add OpenBSD packet capture command
|
||||
|
||||
## 2.2.56.1 28/01/2026
|
||||
|
||||
* Fix line style support for links
|
||||
* Fix cannot add IOS in preferences
|
||||
* Fix cannot add IOU in preferences
|
||||
* 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
|
||||
* Drop Python 3.8 support
|
||||
* Display a warning if a SVG image format isn't supported
|
||||
* Fix error in profile selection window after PyQt6 migration
|
||||
* Apply grid color via css property
|
||||
|
||||
## 2.2.56 21/01/2026
|
||||
## 3.0.1 27/12/2024
|
||||
|
||||
* 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
|
||||
|
||||
## 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
|
||||
|
||||
## 2.2.53 21/01/2025
|
||||
|
||||
* Update file browser filters for all files and IOU images
|
||||
* Upgrade dependencies
|
||||
* 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
|
||||
@@ -81,6 +27,16 @@
|
||||
* 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
|
||||
@@ -97,6 +53,10 @@
|
||||
* 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
|
||||
@@ -116,6 +76,11 @@
|
||||
* 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
|
||||
@@ -127,6 +92,12 @@
|
||||
* 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
|
||||
@@ -147,6 +118,21 @@
|
||||
* 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
|
||||
@@ -159,6 +145,77 @@
|
||||
* 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
|
||||
@@ -224,6 +281,20 @@
|
||||
* 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
|
||||
@@ -238,6 +309,67 @@
|
||||
* 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
|
||||
|
||||
504
COPYING
@@ -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
|
||||
@@ -3,7 +3,7 @@ FROM ubuntu:latest
|
||||
MAINTAINER GNS3 Team
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --force-yes python3 python3-pyqt6 python3-pip python3-pyqt6.qtsvg python3-pyqt6.qtwebsockets python3-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
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
include README.md
|
||||
include AUTHORS
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
recursive-include tests *
|
||||
include CHANGELOG
|
||||
recursive-include gns3 *
|
||||
recursive-include resources *
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
|
||||
18
README.md
@@ -1,26 +1,22 @@
|
||||
GNS3-gui
|
||||
========
|
||||
|
||||
[](https://github.com/GNS3/gns3-gui/actions?query=workflow%3Atesting)
|
||||
|
||||
[](https://pypi.python.org/pypi/gns3-gui)
|
||||
|
||||
[](https://snyk.io/test/github/GNS3/gns3-gui)
|
||||
|
||||
GNS3 GUI repository.
|
||||
[](https://github.com/GNS3/gns3-gui/actions?query=workflow%3Atesting)
|
||||
[](https://pypi.python.org/pypi/gns3-gui)
|
||||
[](https://snyk.io/test/github/GNS3/gns3-gui)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Please see <https://docs.gns3.com/>
|
||||
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
|
||||
PyQt5 which is either part of the Linux distribution or installable from
|
||||
PyPi. The other Python dependencies are automatically installed during
|
||||
the GNS3 GUI installation and are listed
|
||||
[here](https://github.com/GNS3/gns3-gui/blob/master/requirements.txt)
|
||||
[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
|
||||
@@ -43,7 +39,7 @@ cd scripts
|
||||
python build_pyqt.py
|
||||
```
|
||||
|
||||
### Debug
|
||||
### Debugging
|
||||
|
||||
If you want to see the full logs in the internal shell you can type:
|
||||
|
||||
|
||||
@@ -1,5 +1,2 @@
|
||||
-rrequirements.txt
|
||||
|
||||
pytest==8.4.2; python_version == '3.9' # version 8.4.2 is the last one supporting Python 3.9
|
||||
pytest==9.0.2; python_version >= '3.10'
|
||||
pytest-timeout==2.4.0
|
||||
pytest==8.3.2
|
||||
pytest-timeout==2.3.1
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,11 +28,25 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Application(QtWidgets.QApplication):
|
||||
file_open_signal = QtCore.Signal(str)
|
||||
file_open_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, argv):
|
||||
def __init__(self, argv, hdpi=True):
|
||||
|
||||
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"):
|
||||
local_config_path = LocalConfig.instance().configFilePath()
|
||||
log.warning("HDPI mode is enabled. HDPI support on Linux is not fully stable and GNS3 may crash depending of your version of Linux. "
|
||||
f"To disabled HDPI mode please edit '{local_config_path}' 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)
|
||||
@@ -48,7 +63,7 @@ class Application(QtWidgets.QApplication):
|
||||
self.open_file_at_startup = None
|
||||
|
||||
def event(self, event):
|
||||
# When you double click on a file, you receive an event
|
||||
# When you double click file you receive an event
|
||||
# and not the file as command line parameter
|
||||
if sys.platform.startswith("darwin"):
|
||||
if isinstance(event, QtGui.QFileOpenEvent):
|
||||
|
||||
@@ -251,7 +251,7 @@ class BaseNode(QtCore.QObject):
|
||||
|
||||
return self._ports
|
||||
|
||||
def controllerHttpPost(self, path, callback, body=None, context=None, **kwargs):
|
||||
def controllerHttpPost(self, path, callback, body={}, context={}, **kwargs):
|
||||
"""
|
||||
POST on current server / project
|
||||
|
||||
@@ -263,7 +263,7 @@ class BaseNode(QtCore.QObject):
|
||||
|
||||
self._project.post(path, callback, body=body, context=context, **kwargs)
|
||||
|
||||
def controllerHttpPut(self, path, callback, body=None, context=None, **kwargs):
|
||||
def controllerHttpPut(self, path, callback, body={}, context={}, **kwargs):
|
||||
"""
|
||||
PUT on current server / project
|
||||
|
||||
@@ -275,7 +275,7 @@ class BaseNode(QtCore.QObject):
|
||||
|
||||
self._project.put(path, callback, body=body, context=context, **kwargs)
|
||||
|
||||
def controllerHttpGet(self, path, callback, context=None, **kwargs):
|
||||
def controllerHttpGet(self, path, callback, context={}, **kwargs):
|
||||
"""
|
||||
Get on current server / project
|
||||
|
||||
@@ -287,7 +287,7 @@ class BaseNode(QtCore.QObject):
|
||||
|
||||
self._project.get(path, callback, context=context, **kwargs)
|
||||
|
||||
def controllerHttpDelete(self, path, callback, context=None, **kwargs):
|
||||
def controllerHttpDelete(self, path, callback, context={}, **kwargs):
|
||||
"""
|
||||
Delete on current server / project
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.SortOrder.AscendingOrder)
|
||||
self._parent.sortItems(0, QtCore.Qt.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.SortOrder.AscendingOrder)
|
||||
self.sortChildren(0, QtCore.Qt.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 = QtWidgets.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]
|
||||
|
||||
@@ -22,7 +22,7 @@ import inspect
|
||||
import datetime
|
||||
import platform
|
||||
|
||||
from .qt import QtCore, QtGui
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .topology import Topology
|
||||
from .version import __version__
|
||||
from .console_cmd import ConsoleCmd
|
||||
@@ -117,10 +117,10 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
"""
|
||||
|
||||
menu = self.createStandardContextMenu()
|
||||
delete_all_action = QtGui.QAction("Delete All", menu)
|
||||
delete_all_action = QtWidgets.QAction("Delete All", menu)
|
||||
delete_all_action.triggered.connect(self._deleteAllActionSlot)
|
||||
menu.addAction(delete_all_action)
|
||||
menu.exec(event.globalPos())
|
||||
menu.exec_(event.globalPos());
|
||||
|
||||
def _deleteAllActionSlot(self):
|
||||
"""
|
||||
|
||||
@@ -21,13 +21,14 @@ import tempfile
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
from .qt import QtCore, QtNetwork, QtGui, QtWidgets, QtWebSockets, qpartial, qslot
|
||||
from .symbol import Symbol
|
||||
from .local_server_config import LocalServerConfig
|
||||
from .settings import LOCAL_SERVER_SETTINGS
|
||||
from urllib.parse import urlparse
|
||||
from .qt import QtCore, QtGui, QtWebSockets, qpartial, qslot
|
||||
|
||||
from gns3.local_config import LocalConfig
|
||||
from .symbol import Symbol
|
||||
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__)
|
||||
@@ -42,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):
|
||||
|
||||
@@ -56,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):
|
||||
@@ -73,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):
|
||||
"""
|
||||
@@ -104,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):
|
||||
@@ -125,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
|
||||
@@ -132,46 +170,16 @@ class Controller(QtCore.QObject):
|
||||
|
||||
self._connected = False
|
||||
self._connecting = True
|
||||
status, json_data = self.httpClient().getSynchronous('GET', '/version', timeout=60)
|
||||
self._versionGetSlot(json_data, status is None or status >= 300)
|
||||
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 self._display_error:
|
||||
self._error_dialog = QtWidgets.QMessageBox(self.parent())
|
||||
self._error_dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
|
||||
self._error_dialog.setWindowTitle("Connection to server")
|
||||
if result and "message" in result:
|
||||
self._error_dialog.setText("Error when connecting to the GNS3 server:\n{}".format(result["message"]))
|
||||
else:
|
||||
self._error_dialog.setText("Cannot connect to the GNS3 server")
|
||||
self._error_dialog.setIcon(QtWidgets.QMessageBox.Icon.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")
|
||||
self._http_client.connection_connected_signal.emit()
|
||||
|
||||
def _httpClientConnectedSlot(self):
|
||||
|
||||
if not self._connected:
|
||||
@@ -182,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):
|
||||
"""
|
||||
@@ -227,31 +235,13 @@ class Controller(QtCore.QObject):
|
||||
return compute_id
|
||||
return compute_id
|
||||
|
||||
def getEndpoint(self, path, compute_id, *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):
|
||||
def request(self, method, path, *args, **kwargs):
|
||||
"""
|
||||
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():
|
||||
@@ -284,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
|
||||
|
||||
@@ -302,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
|
||||
@@ -366,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):
|
||||
|
||||
@@ -397,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)
|
||||
@@ -409,35 +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") or LocalConfig.instance().experimental():
|
||||
|
||||
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)
|
||||
url = self._http_client.url() + '/notifications'
|
||||
log.info("Listening for controller notifications on '{}'".format(url))
|
||||
|
||||
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)
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("6.5.0"):
|
||||
self._notification_stream.error.connect(self._websocket_error)
|
||||
else:
|
||||
self._notification_stream.errorOccurred.connect(self._websocket_error)
|
||||
self._notification_stream.error.connect(self._websocket_error)
|
||||
self._notification_stream.sslErrors.connect(self._sslErrorsSlot)
|
||||
log.info("Listening for controller notifications on '{}'".format(self._notification_stream.requestUrl().toString()))
|
||||
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:
|
||||
@@ -445,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):
|
||||
"""
|
||||
@@ -457,10 +478,10 @@ class Controller(QtCore.QObject):
|
||||
|
||||
@qslot
|
||||
def _websocket_error(self, error):
|
||||
|
||||
if self._notification_stream:
|
||||
log.error("Websocket controller notification stream error: {}".format(self._notification_stream.errorString()))
|
||||
self._notification_stream = None
|
||||
self._startListenNotifications()
|
||||
self.stopListenNotifications()
|
||||
|
||||
@qslot
|
||||
def _sslErrorsSlot(self, ssl_errors):
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
# 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 os
|
||||
import platform
|
||||
import struct
|
||||
import distro
|
||||
|
||||
try:
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.logging import LoggingIntegration
|
||||
@@ -23,12 +29,6 @@ except ImportError:
|
||||
# Sentry SDK is not installed with deb package in order to simplify packaging
|
||||
SENTRY_SDK_AVAILABLE = False
|
||||
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
import struct
|
||||
import distro
|
||||
|
||||
from .version import __version__, __version_info__
|
||||
|
||||
import logging
|
||||
@@ -50,7 +50,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "https://dd662ce99d7e4a04714a89939ec523c9@o19455.ingest.us.sentry.io/38506"
|
||||
DSN = "https://142c803f12d32e781a654ef31138c684@o19455.ingest.us.sentry.io/38506"
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -75,13 +75,19 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
# 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.StandardLocation.DownloadLocation)
|
||||
download_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
|
||||
if download_directory != "" and download_directory != os.path.dirname(self._path):
|
||||
images_directories.append(download_directory)
|
||||
|
||||
@@ -96,15 +102,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
# add a custom button to show appliance information
|
||||
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.setButtonText(QtWidgets.QWizard.CustomButton1, "&Appliance info")
|
||||
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
|
||||
self.customButtonClicked.connect(self._showApplianceInfoSlot)
|
||||
|
||||
# customize the server selection
|
||||
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
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")
|
||||
@@ -138,20 +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.WizardPixmap.LogoPixmap, QtGui.QPixmap(symbol))
|
||||
self.page(page_id).setPixmap(QtWidgets.QWizard.LogoPixmap, QtGui.QPixmap(symbol))
|
||||
|
||||
if self.page(page_id) == self.uiServerWizardPage:
|
||||
|
||||
Controller.instance().getSymbols(self._getSymbolsCallback)
|
||||
template_type = self._appliance.template_type()
|
||||
if not template_type:
|
||||
raise ApplianceError("No template type found for appliance {}".format(self._appliance["name"]))
|
||||
is_mac = ComputeManager.instance().localPlatform().startswith("darwin")
|
||||
is_win = ComputeManager.instance().localPlatform().startswith("win")
|
||||
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
if len(ComputeManager.instance().remoteComputes()) == 0:
|
||||
self.uiRemoteRadioButton.setEnabled(False)
|
||||
@@ -160,50 +162,22 @@ 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 template_type == "qemu":
|
||||
# disallow usage of the local server because Qemu has issues on OSX and Windows
|
||||
if not LocalConfig.instance().experimental():
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
elif template_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.template_type(), self._compute_id)
|
||||
self._registry.getRemoteImageList()
|
||||
else:
|
||||
self.images_changed_signal.emit()
|
||||
|
||||
elif self.page(page_id) == self.uiQemuWizardPage:
|
||||
if self._appliance.template_properties().get('kvm', 'require') == 'require':
|
||||
self._server_check = False
|
||||
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
|
||||
else:
|
||||
self._server_check = True
|
||||
if self._appliance["registry_version"] >= 8:
|
||||
qemu_platform = self._appliance.template_properties()["platform"]
|
||||
else:
|
||||
qemu_platform = self._appliance.template_properties()["arch"]
|
||||
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [qemu_platform])
|
||||
|
||||
elif self.page(page_id) == self.uiInstructionsPage:
|
||||
|
||||
installation_instructions = self._appliance.get("installation_instructions", "No installation instructions available")
|
||||
@@ -227,27 +201,8 @@ Usage: {}
|
||||
|
||||
self.uiUsageTextEdit.setText(usage_info.strip())
|
||||
|
||||
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
|
||||
"""
|
||||
Check if the server supports KVM or not
|
||||
"""
|
||||
|
||||
if self._appliance["registry_version"] >= 8:
|
||||
qemu_platform = self._appliance.template_properties()["platform"]
|
||||
else:
|
||||
qemu_platform = self._appliance.template_properties()["arch"]
|
||||
if error is None and "kvm" in result and qemu_platform in result["kvm"]:
|
||||
self._server_check = True
|
||||
else:
|
||||
if error:
|
||||
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
|
||||
|
||||
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:
|
||||
@@ -257,7 +212,7 @@ Usage: {}
|
||||
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
|
||||
else:
|
||||
log.info("Image '{}' has been successfully uploaded".format(image_path))
|
||||
self._registry.getRemoteImageList(self._appliance.template_type(), self._compute_id)
|
||||
self._registry.getRemoteImageList()
|
||||
|
||||
def _showApplianceInfoSlot(self):
|
||||
"""
|
||||
@@ -327,10 +282,10 @@ Usage: {}
|
||||
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.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
|
||||
msgbox.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
msgbox.setText(text_info)
|
||||
msgbox.setDetailedText(self._appliance["description"])
|
||||
msgbox.exec()
|
||||
msgbox.exec_()
|
||||
|
||||
@qslot
|
||||
def _refreshVersions(self, *args):
|
||||
@@ -368,18 +323,18 @@ Usage: {}
|
||||
|
||||
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.ItemDataRole.UserRole, version)
|
||||
image_widget.setData(1, QtCore.Qt.ItemDataRole.UserRole, image)
|
||||
image_widget.setData(2, QtCore.Qt.ItemDataRole.UserRole, self._appliance)
|
||||
image_widget.setData(0, QtCore.Qt.UserRole, version)
|
||||
image_widget.setData(1, QtCore.Qt.UserRole, image)
|
||||
image_widget.setData(2, QtCore.Qt.UserRole, self._appliance)
|
||||
top.addChild(image_widget)
|
||||
|
||||
font = top.font(0)
|
||||
@@ -393,10 +348,10 @@ Usage: {}
|
||||
expand = False
|
||||
top.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
|
||||
|
||||
top.setData(1, QtCore.Qt.ItemDataRole.DisplayRole, human_filesize(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)
|
||||
top.setData(1, QtCore.Qt.DisplayRole, human_size(size))
|
||||
top.setData(2, QtCore.Qt.DisplayRole, status)
|
||||
top.setData(0, QtCore.Qt.UserRole, version)
|
||||
top.setData(2, QtCore.Qt.UserRole, self._appliance)
|
||||
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
|
||||
if expand:
|
||||
top.setExpanded(True)
|
||||
@@ -428,11 +383,13 @@ Usage: {}
|
||||
|
||||
for version in self._appliance["versions"]:
|
||||
for image in version["images"].values():
|
||||
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())
|
||||
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"
|
||||
@@ -459,7 +416,7 @@ Usage: {}
|
||||
if current is None or sip.isdeleted(current):
|
||||
return
|
||||
|
||||
image = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
|
||||
image = current.data(1, QtCore.Qt.UserRole)
|
||||
if image is not None:
|
||||
if "direct_download_url" in image or "download_url" in image:
|
||||
self.uiDownloadPushButton.show()
|
||||
@@ -480,7 +437,7 @@ Usage: {}
|
||||
if current is None or sip.isdeleted(current):
|
||||
return
|
||||
|
||||
data = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
|
||||
data = current.data(1, QtCore.Qt.UserRole)
|
||||
if data is not None:
|
||||
if "direct_download_url" in data:
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["direct_download_url"]))
|
||||
@@ -500,16 +457,16 @@ Usage: {}
|
||||
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)
|
||||
base_version = current.data(0, QtCore.Qt.UserRole)
|
||||
|
||||
new_version_name, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Create a new version for this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.EchoMode.Normal, base_version.get("name"))
|
||||
new_version_name, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Create a new version for this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.Normal, base_version.get("name"))
|
||||
if ok:
|
||||
new_version = {"name": new_version_name}
|
||||
new_version["images"] = {}
|
||||
|
||||
for disk_type in base_version["images"]:
|
||||
base_filename = base_version["images"][disk_type]["filename"]
|
||||
filename, ok = QtWidgets.QInputDialog.getText(self, "Image", "Disk image filename for {}".format(disk_type), QtWidgets.QLineEdit.EchoMode.Normal, base_filename)
|
||||
filename, ok = QtWidgets.QInputDialog.getText(self, "Image", "Disk image filename for {}".format(disk_type), QtWidgets.QLineEdit.Normal, base_filename)
|
||||
if not ok:
|
||||
filename = base_filename
|
||||
new_version["images"][disk_type] = {"filename": filename, "version": new_version_name}
|
||||
@@ -534,7 +491,7 @@ Usage: {}
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
if not current:
|
||||
return
|
||||
disk = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
|
||||
disk = current.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName()
|
||||
if len(path) == 0:
|
||||
@@ -554,45 +511,17 @@ Usage: {}
|
||||
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
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
|
||||
)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
if reply == QtWidgets.QMessageBox.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"]))
|
||||
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:
|
||||
if self._appliance["registry_version"] >= 8:
|
||||
qemu_platform = self._appliance.template_properties()["platform"]
|
||||
else:
|
||||
qemu_platform = self._appliance.template_properties()["arch"]
|
||||
i = self.uiQemuListComboBox.findData(qemu_platform, flags=QtCore.Qt.MatchFlag.MatchEndsWith)
|
||||
if i != -1:
|
||||
self.uiQemuListComboBox.setCurrentIndex(i)
|
||||
|
||||
def _install(self, version):
|
||||
"""
|
||||
Install the appliance in GNS3
|
||||
@@ -615,38 +544,15 @@ Usage: {}
|
||||
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.EchoMode.Normal, appliance_configuration["name"])
|
||||
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add template", "New name:", QtWidgets.QLineEdit.Normal, appliance_configuration["name"])
|
||||
if not ok:
|
||||
return False
|
||||
appliance_configuration["name"] = appliance_configuration["name"].strip()
|
||||
|
||||
if self._appliance["registry_version"] >= 8:
|
||||
if "settings" in appliance_configuration:
|
||||
for settings in appliance_configuration["settings"]:
|
||||
if settings["template_type"] == "qemu":
|
||||
settings["template_properties"]["path"] = self.uiQemuListComboBox.currentData()
|
||||
elif "qemu" in appliance_configuration:
|
||||
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
|
||||
|
||||
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, 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:
|
||||
@@ -666,37 +572,24 @@ Usage: {}
|
||||
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
|
||||
return True
|
||||
image = Image(self._appliance.template_type(), image["path"], filename=image["filename"])
|
||||
image_upload_manager = ImageUploadManager(image, Controller.instance(), self._compute_id, self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
|
||||
image_upload_manager.upload()
|
||||
self._image_uploading_count += 1
|
||||
|
||||
def _applianceImageUploadedCallback(self, result, error=False, 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
|
||||
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 self._appliance.template_type() == "docker":
|
||||
# skip Qemu binary selection and files pages if this is a Docker appliance
|
||||
return super().nextId() + 3
|
||||
elif self._appliance.template_type() != "qemu":
|
||||
# skip the Qemu binary selection page if not a Qemu appliance
|
||||
return super().nextId() + 1
|
||||
if self.currentPage() == self.uiQemuWizardPage:
|
||||
if not self._appliance.get("installation_instructions"):
|
||||
return super().nextId() + 2
|
||||
elif not self._appliance.get("installation_instructions"):
|
||||
# skip the installation instructions page if there are no instructions
|
||||
return super().nextId() + 1
|
||||
return super().nextId()
|
||||
@@ -714,21 +607,21 @@ Usage: {}
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
if current is None or sip.isdeleted(current):
|
||||
return False
|
||||
version = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
if version is None:
|
||||
return False
|
||||
appliance = current.data(2, QtCore.Qt.ItemDataRole.UserRole)
|
||||
appliance = current.data(2, QtCore.Qt.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.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.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
|
||||
@@ -740,7 +633,7 @@ Usage: {}
|
||||
return False
|
||||
current = self.uiApplianceVersionTreeWidget.currentItem()
|
||||
if current:
|
||||
version = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
version = current.data(0, QtCore.Qt.UserRole)
|
||||
return self._install(version["name"])
|
||||
else:
|
||||
return self._install(None)
|
||||
@@ -753,39 +646,17 @@ Usage: {}
|
||||
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.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.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.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.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):
|
||||
"""
|
||||
@@ -821,8 +692,8 @@ Usage: {}
|
||||
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.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
self.allowCustomFiles.setChecked(False)
|
||||
return False
|
||||
|
||||
@@ -24,15 +24,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class CaptureDialog(QtWidgets.QDialog, Ui_CaptureDialog):
|
||||
"""
|
||||
This dialog allow to configure the packet capture
|
||||
This dialog allow configure the packet capture
|
||||
"""
|
||||
|
||||
def __init__(self, parent, file_name, auto_start, ethernet_link=True):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Cancel).clicked.connect(self.reject)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
||||
|
||||
if ethernet_link:
|
||||
self.uiDataLinkTypeComboBox.addItem("Ethernet", "DLT_EN10MB")
|
||||
@@ -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())
|
||||
|
||||
@@ -59,7 +59,7 @@ class ConfigurationDialog(QtWidgets.QDialog, Ui_configurationDialog):
|
||||
:param button: button that was clicked (QAbstractButton)
|
||||
"""
|
||||
|
||||
if button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Cancel):
|
||||
if button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
|
||||
QtWidgets.QDialog.reject(self)
|
||||
else:
|
||||
try:
|
||||
|
||||
@@ -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.EchoMode.Normal)
|
||||
name, ok = QtWidgets.QInputDialog.getText(self, "Add a command", "Command name:", QtWidgets.QLineEdit.Normal)
|
||||
command = self.uiCommandPlainTextEdit.toPlainText().strip()
|
||||
if ok and len(command) > 0:
|
||||
if command not in self._consoles.values():
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.StandardButton.Reset).clicked.connect(self._resetSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.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.ItemFlag.ItemIsEditable)
|
||||
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
|
||||
item.setText(0, "Adapter {}".format(adapter_number))
|
||||
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, adapter_number)
|
||||
item.setData(1, QtCore.Qt.ItemDataRole.UserRole, port_name)
|
||||
item.setData(0, QtCore.Qt.UserRole, adapter_number)
|
||||
item.setData(1, QtCore.Qt.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.ItemDataRole.ToolTipRole)
|
||||
combobox.setItemData(index, adapter_description, QtCore.Qt.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.SortOrder.AscendingOrder)
|
||||
self.uiAdaptersTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiAdaptersTreeWidget.setSortingEnabled(True)
|
||||
|
||||
for column in range(self.uiAdaptersTreeWidget.columnCount()):
|
||||
@@ -166,9 +166,9 @@ 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.ItemDataRole.UserRole)
|
||||
adapter_number = item.data(0, QtCore.Qt.UserRole)
|
||||
custom_adapter_settings["adapter_number"] = adapter_number
|
||||
original_port_name = item.data(1, QtCore.Qt.ItemDataRole.UserRole)
|
||||
original_port_name = item.data(1, QtCore.Qt.UserRole)
|
||||
if not port_name:
|
||||
QtWidgets.QMessageBox.critical(self, "Port name", "Port name cannot be empty for adapter {}".format(adapter_number))
|
||||
return False
|
||||
|
||||
@@ -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_()
|
||||
|
||||
@@ -34,42 +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())
|
||||
|
||||
index = self.uiServerProtocolComboBox.findText(self._compute.protocol().upper())
|
||||
self.uiServerProtocolComboBox.setCurrentIndex(index)
|
||||
|
||||
if self._compute.user():
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(True)
|
||||
self.uiServerUserLineEdit.setText(self._compute.user())
|
||||
else:
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(False)
|
||||
self.uiWarningLabel.setVisible(False)
|
||||
else:
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(False)
|
||||
self.uiWarningLabel.setVisible(False)
|
||||
self._enableAuthenticationSlot(self.uiEnableAuthenticationCheckBox.isChecked())
|
||||
|
||||
def _enableAuthenticationSlot(self, state):
|
||||
"""
|
||||
Slot to enable or not the authentication.
|
||||
"""
|
||||
|
||||
if self.uiEnableAuthenticationCheckBox.isChecked():
|
||||
self.uiServerUserLineEdit.setVisible(True)
|
||||
self.uiServerPasswordLineEdit.setVisible(True)
|
||||
self.uiServerUserLabel.setVisible(True)
|
||||
self.uiServerPasswordLabel.setVisible(True)
|
||||
else:
|
||||
self.uiServerUserLineEdit.setVisible(False)
|
||||
self.uiServerPasswordLineEdit.setVisible(False)
|
||||
self.uiServerUserLabel.setVisible(False)
|
||||
self.uiServerPasswordLabel.setVisible(False)
|
||||
self.uiServerUserLineEdit.setText(self._compute.user())
|
||||
|
||||
def compute(self):
|
||||
return self._compute
|
||||
@@ -87,16 +60,16 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
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:
|
||||
@@ -105,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()
|
||||
|
||||
@@ -121,4 +90,4 @@ if __name__ == '__main__':
|
||||
main = QtWidgets.QMainWindow()
|
||||
dialog = EditComputeDialog(main)
|
||||
dialog.show()
|
||||
exit_code = app.exec()
|
||||
exit_code = app.exec_()
|
||||
|
||||
@@ -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,18 +41,46 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
self.uiNodeGridSizeSpinBox.setValue(self._project.nodeGridSize())
|
||||
self.uiDrawingGridSizeSpinBox.setValue(self._project.drawingGridSize())
|
||||
|
||||
self.uiGlobalVariablesGrid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
|
||||
self.uiGlobalVariablesGrid.setAlignment(QtCore.Qt.AlignTop)
|
||||
|
||||
self.uiNewVarButton = QtWidgets.QPushButton('Add new variable', self)
|
||||
self.uiNewVarButton.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
|
||||
self.uiNewVarButton.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
self.uiNewVarButton.clicked.connect(self.onAddNewVariable)
|
||||
self.uiGlobalVariablesGrid.addWidget(self.uiNewVarButton, 0, 3, QtCore.Qt.AlignmentFlag.AlignRight)
|
||||
self.uiGlobalVariablesGrid.addWidget(self.uiNewVarButton, 0, 3, QtCore.Qt.AlignRight)
|
||||
|
||||
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 _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)
|
||||
|
||||
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:
|
||||
item = self.uiGlobalVariablesGrid.takeAt(1)
|
||||
@@ -115,4 +145,12 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
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")
|
||||
|
||||
@@ -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.StandardButton.Save).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Cancel).clicked.connect(self.reject)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Save).clicked.connect(self._okButtonClickedSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.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)
|
||||
|
||||
@@ -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.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)
|
||||
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)
|
||||
|
||||
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.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.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.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.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.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
|
||||
spacerItem = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
gridLayout.addItem(spacerItem, line, 0, 1, 1)
|
||||
vlayout.addLayout(gridLayout)
|
||||
tab.setLayout(vlayout)
|
||||
|
||||
@@ -33,8 +33,8 @@ class IdlePCDialog(QtWidgets.QDialog, Ui_IdlePCDialog):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).clicked.connect(self._applySlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help).clicked.connect(self._helpSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applySlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpSlot)
|
||||
|
||||
self._router = router
|
||||
self._idlepcs = idlepcs
|
||||
|
||||
256
gns3/dialogs/image_dialog.py
Normal 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.DownloadLocation),
|
||||
"Images (*.bin *.image *.iol *.qcow2 *.vmdk *.iso);;All files (*.*)"
|
||||
)
|
||||
error_msgs = ""
|
||||
for path in files:
|
||||
log.debug("Uploading image '{}' to controller".format(path))
|
||||
image_filename = os.path.basename(path)
|
||||
install_appliances = self.uiInstallApplianceCheckBox.isChecked()
|
||||
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.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.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.Yes,
|
||||
QtWidgets.QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.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.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.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.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.Yes,
|
||||
QtWidgets.QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.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.Yes,
|
||||
QtWidgets.QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.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.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.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.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.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 = QtWidgets.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_Escape:
|
||||
self.close()
|
||||
elif e.matches(QtGui.QKeySequence.Copy):
|
||||
self._copyToClipboardSlot()
|
||||
60
gns3/dialogs/login_dialog.py
Normal 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)
|
||||
@@ -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.WizardStyle.ModernStyle)
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.WizardOption.NoDefaultButton)
|
||||
self.setOptions(QtWidgets.QWizard.NoDefaultButton)
|
||||
|
||||
# add a custom button to show appliance information
|
||||
self.setButtonText(QtWidgets.QWizard.WizardButton.CustomButton1, "&Update from online registry")
|
||||
self.setOption(QtWidgets.QWizard.WizardOption.HaveCustomButton1, True)
|
||||
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Update from online registry")
|
||||
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
|
||||
self.customButtonClicked.connect(self._downloadAppliancesSlot)
|
||||
self.button(QtWidgets.QWizard.WizardButton.CustomButton1).hide()
|
||||
self.button(QtWidgets.QWizard.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.ItemFlag.ItemIsSelectable)
|
||||
parent_routers.setFlags(parent_routers.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
parent_switches = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
|
||||
parent_switches.setText(0, "Switches")
|
||||
parent_switches.setFlags(parent_switches.flags() & ~QtCore.Qt.ItemFlag.ItemIsSelectable)
|
||||
parent_switches.setFlags(parent_switches.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
parent_guests = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
|
||||
parent_guests.setText(0, "Guests")
|
||||
parent_guests.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemFlag.ItemIsSelectable)
|
||||
parent_guests.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemIsSelectable)
|
||||
parent_firewalls = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
|
||||
parent_firewalls.setText(0, "Firewalls")
|
||||
parent_firewalls.setFlags(parent_firewalls.flags() & ~QtCore.Qt.ItemFlag.ItemIsSelectable)
|
||||
parent_firewalls.setFlags(parent_firewalls.flags() & ~QtCore.Qt.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.ItemDataRole.UserRole, appliance)
|
||||
item.setData(0, QtCore.Qt.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.SortOrder.AscendingOrder)
|
||||
self.uiAppliancesTreeWidget.sortByColumn(0, QtCore.Qt.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.WizardButton.CustomButton1).show()
|
||||
self.setButtonText(QtWidgets.QWizard.WizardButton.FinishButton, "&Install")
|
||||
self.button(QtWidgets.QWizard.CustomButton1).show()
|
||||
self.setButtonText(QtWidgets.QWizard.FinishButton, "&Install")
|
||||
self._get_appliances_from_server()
|
||||
else:
|
||||
self.button(QtWidgets.QWizard.WizardButton.CustomButton1).hide()
|
||||
self.button(QtWidgets.QWizard.CustomButton1).hide()
|
||||
|
||||
def cleanupPage(self, page_id):
|
||||
"""
|
||||
Restore button default settings on the first page.
|
||||
"""
|
||||
|
||||
self.button(QtWidgets.QWizard.WizardButton.CustomButton1).hide()
|
||||
self.setButtonText(QtWidgets.QWizard.WizardButton.FinishButton, "&Finish")
|
||||
self.button(QtWidgets.QWizard.CustomButton1).hide()
|
||||
self.setButtonText(QtWidgets.QWizard.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.ItemDataRole.UserRole), f)
|
||||
json.dump(item.data(0, QtCore.Qt.UserRole), f)
|
||||
f.close()
|
||||
MainWindow.instance().loadPath(f.name)
|
||||
try:
|
||||
|
||||
@@ -40,8 +40,8 @@ class NodePropertiesDialog(QtWidgets.QDialog, Ui_NodePropertiesDialog):
|
||||
self._node_items = node_items
|
||||
self._parent_items = {}
|
||||
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Reset).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.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.SortOrder.AscendingOrder)
|
||||
self.uiNodesTreeWidget.sortByColumn(0, QtCore.Qt.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.StandardButton.Apply).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Reset).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(True)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).setEnabled(True)
|
||||
else:
|
||||
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)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).setEnabled(False)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).setEnabled(False)
|
||||
|
||||
# hide the contextual help button if there is no help text
|
||||
if page.whatsThis():
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help).show()
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).show()
|
||||
else:
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help).hide()
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.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.StandardButton.Apply):
|
||||
if button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply):
|
||||
self.applySettings()
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Reset):
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset):
|
||||
self.resetSettings()
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Help):
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help):
|
||||
self.showHelp()
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Cancel):
|
||||
elif button == self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Cancel):
|
||||
QtWidgets.QDialog.reject(self)
|
||||
else:
|
||||
self.applySettings()
|
||||
|
||||
@@ -54,12 +54,12 @@ class NotifDialog(QtWidgets.QWidget):
|
||||
super().__init__(parent)
|
||||
self._notifs = []
|
||||
|
||||
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.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._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.AlignmentFlag.AlignTop)
|
||||
l.setAlignment(QtCore.Qt.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_()
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
Dialog to load module and built-in preference pages.
|
||||
"""
|
||||
|
||||
from ..qt import QtGui, QtCore, QtWidgets
|
||||
from ..qt import 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.gns3_vm_preferences_page import GNS3VMPreferencesPage
|
||||
@@ -49,9 +49,8 @@ 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
|
||||
geometry = QtGui.QGuiApplication.primaryScreen().geometry()
|
||||
height = geometry.height() - 100
|
||||
width = geometry.width() - 100
|
||||
height = QtWidgets.QDesktopWidget().screenGeometry().height() - 100
|
||||
width = QtWidgets.QDesktopWidget().screenGeometry().width() - 100
|
||||
|
||||
# 980 is the default width
|
||||
if self.width() > width:
|
||||
@@ -61,7 +60,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.StandardButton.Apply)
|
||||
self._applyButton = self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply)
|
||||
self._applyButton.clicked.connect(self._applyPreferences)
|
||||
self._applyButton.setEnabled(False)
|
||||
self._applyButton.setStyleSheet("QPushButton:disabled {color: gray}")
|
||||
@@ -74,7 +73,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 changed?
|
||||
# Something has change?
|
||||
self._modified_pages = set()
|
||||
|
||||
def _loadPreferencePages(self):
|
||||
@@ -85,8 +84,8 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
# load built-in preference pages
|
||||
pages = [
|
||||
GeneralPreferencesPage,
|
||||
ServerPreferencesPage,
|
||||
GNS3VMPreferencesPage,
|
||||
ControllerPreferencesPage,
|
||||
#GNS3VMPreferencesPage,
|
||||
PacketCapturePreferencesPage,
|
||||
]
|
||||
|
||||
@@ -96,7 +95,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.ItemDataRole.UserRole, preferences_page)
|
||||
item.setData(0, QtCore.Qt.UserRole, preferences_page)
|
||||
self.uiStackedWidget.addWidget(preferences_page)
|
||||
self._items.append(item)
|
||||
self._watchForChanges(preferences_page)
|
||||
@@ -112,7 +111,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
name = preferences_page.windowTitle()
|
||||
item = QtWidgets.QTreeWidgetItem(parent)
|
||||
item.setText(0, name)
|
||||
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, preferences_page)
|
||||
item.setData(0, QtCore.Qt.UserRole, preferences_page)
|
||||
self.uiStackedWidget.addWidget(preferences_page)
|
||||
self._items.append(item)
|
||||
if cls is preference_pages[0]:
|
||||
@@ -177,7 +176,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
if current is None:
|
||||
current = previous
|
||||
|
||||
preferences_page = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
preferences_page = current.data(0, QtCore.Qt.UserRole)
|
||||
accessible_name = preferences_page.accessibleName()
|
||||
if accessible_name:
|
||||
self.uiTitleLabel.setText(accessible_name)
|
||||
@@ -193,9 +192,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.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
|
||||
page.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
else:
|
||||
page.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored, QtWidgets.QSizePolicy.Policy.Ignored)
|
||||
page.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
|
||||
|
||||
def _applyPreferences(self):
|
||||
"""
|
||||
@@ -224,9 +223,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.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
QtWidgets.QDialog.reject(self)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtWidgets, QtGui
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.ui.profile_select_dialog_ui import Ui_ProfileSelectDialog
|
||||
from gns3.version import __version_info__
|
||||
@@ -46,7 +46,7 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
self.uiDeletePushButton.clicked.connect(self._deletePushButtonSlot)
|
||||
|
||||
# Center on screen
|
||||
screen = QtGui.QGuiApplication.primaryScreen().geometry()
|
||||
screen = QtWidgets.QApplication.desktop().screenGeometry()
|
||||
self.move(screen.center() - self.rect().center())
|
||||
|
||||
version = "{}.{}".format(__version_info__[0], __version_info__[1])
|
||||
@@ -54,14 +54,8 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
path = os.path.join(appdata, "GNS3", version)
|
||||
else:
|
||||
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)
|
||||
|
||||
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())
|
||||
@@ -109,4 +103,4 @@ if __name__ == '__main__':
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
dialog = ProfileSelectDialog()
|
||||
dialog.show()
|
||||
exit_code = app.exec()
|
||||
exit_code = app.exec_()
|
||||
|
||||
@@ -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.ItemDataRole.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.ItemDataRole.UserRole)
|
||||
project_id = project.data(0, QtCore.Qt.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
reply = QtWidgets.QMessageBox.warning(self,
|
||||
"Delete project",
|
||||
'Delete project "{}"?\nThis cannot be reverted.'.format(project_name),
|
||||
QtWidgets.QMessageBox.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.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.ItemDataRole.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.ItemDataRole.UserRole)
|
||||
project_id = project.data(0, QtCore.Qt.UserRole)
|
||||
project_name = project.data(1, QtCore.Qt.UserRole)
|
||||
|
||||
new_project_name = project_name + "-1"
|
||||
existing_project_name = [p["name"] for p in Controller.instance().projects()]
|
||||
@@ -131,7 +131,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
name, reply = QtWidgets.QInputDialog.getText(self,
|
||||
"Duplicate project",
|
||||
'Duplicate project "{}"?.'.format(project_name),
|
||||
QtWidgets.QLineEdit.EchoMode.Normal,
|
||||
QtWidgets.QLineEdit.Normal,
|
||||
new_project_name)
|
||||
name = name.strip()
|
||||
if reply and len(name) > 0:
|
||||
@@ -139,18 +139,24 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
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, "reset_mac_addresses": reset_mac_addresses},
|
||||
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, "reset_mac_addresses": reset_mac_addresses},
|
||||
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:
|
||||
@@ -167,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.ItemDataRole.UserRole, project["project_id"])
|
||||
item.setData(1, QtCore.Qt.ItemDataRole.UserRole, project["name"])
|
||||
item.setData(2, QtCore.Qt.ItemDataRole.UserRole, path)
|
||||
item.setData(0, QtCore.Qt.UserRole, project["project_id"])
|
||||
item.setData(1, QtCore.Qt.UserRole, project["name"])
|
||||
item.setData(2, QtCore.Qt.UserRole, path)
|
||||
items.append(item)
|
||||
self.uiProjectsTreeWidget.addTopLevelItems(items)
|
||||
|
||||
@@ -180,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.SortOrder.AscendingOrder)
|
||||
self.uiProjectsTreeWidget.sortItems(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiProjectsTreeWidget.setUpdatesEnabled(True)
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
@@ -188,7 +194,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
Event handler in order to properly handle escape.
|
||||
"""
|
||||
|
||||
if e.key() == QtCore.Qt.Key.Key_Escape:
|
||||
if e.key() == QtCore.Qt.Key_Escape:
|
||||
self.close()
|
||||
|
||||
def _projectNameSlot(self, text):
|
||||
@@ -289,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.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No)
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
Controller.instance().deleteProject(existing_project["project_id"], self._overwriteProjectCallback)
|
||||
|
||||
# In all cases we cancel the new project and if project success to delete
|
||||
@@ -313,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.ItemDataRole.UserRole)
|
||||
self._project_settings["project_name"] = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
|
||||
self._project_settings["project_id"] = current.data(0, QtCore.Qt.UserRole)
|
||||
self._project_settings["project_name"] = current.data(1, QtCore.Qt.UserRole)
|
||||
super().done(result)
|
||||
|
||||
@@ -16,14 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import os
|
||||
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__)
|
||||
@@ -34,6 +33,8 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
|
||||
Export project wizard.
|
||||
"""
|
||||
|
||||
readme_signal = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, project, parent):
|
||||
|
||||
super().__init__(parent)
|
||||
@@ -41,48 +42,78 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
|
||||
|
||||
self._project = project
|
||||
self._path = None
|
||||
self.setWizardStyle(QtWidgets.QWizard.WizardStyle.ModernStyle)
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.WizardOption.NoDefaultButton)
|
||||
self.setOptions(QtWidgets.QWizard.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)
|
||||
|
||||
# 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/README.txt", self._loadedReadme)
|
||||
self._project.get("/files/{}".format(self._readme_filename), 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.uiReadmeTextEdit.setPlainText(raw_body.decode("utf-8", errors="replace"))
|
||||
content = result.decode("utf-8", errors="replace")
|
||||
self.uiReadmeTextEdit.setPlainText(content)
|
||||
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)
|
||||
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.StandardLocation.DocumentsLocation)
|
||||
directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.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
|
||||
@@ -110,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):
|
||||
"""
|
||||
@@ -126,6 +181,11 @@ 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"
|
||||
@@ -137,8 +197,46 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
|
||||
keep_compute_ids = "yes"
|
||||
|
||||
compression = self.uiCompressionComboBox.currentData()
|
||||
export_worker = ExportProjectWorker(self._project, self._path, include_images, include_snapshots, reset_mac_addresses, keep_compute_ids, 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
|
||||
|
||||
@@ -35,7 +35,7 @@ class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
|
||||
self._project = project
|
||||
self.setupUi(self)
|
||||
self.uiOkButton.clicked.connect(self._okButtonClickedSlot)
|
||||
self.gridLayout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
|
||||
self.gridLayout.setAlignment(QtCore.Qt.AlignTop)
|
||||
self.label.setOpenExternalLinks(True)
|
||||
self._variables = self._getVariables(project)
|
||||
self._loadReadme()
|
||||
@@ -61,11 +61,11 @@ 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
|
||||
@@ -76,9 +76,9 @@ class ProjectWelcomeDialog(QtWidgets.QDialog, Ui_ProjectWelcomeDialog):
|
||||
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:
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
self._project.setVariables(self._variables)
|
||||
|
||||
@@ -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,44 +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",
|
||||
"allocate_vcpus_ram": True,
|
||||
"vcpus": 1,
|
||||
"ram": 2048,
|
||||
"vmname": "GNS3 VM",
|
||||
"port": 80
|
||||
}
|
||||
|
||||
self.setWizardStyle(QtWidgets.QWizard.WizardStyle.ModernStyle)
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.WizardOption.NoDefaultButton)
|
||||
self.setOptions(QtWidgets.QWizard.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)
|
||||
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.NetworkLayerProtocol.IPv4Protocol, QtNetwork.QAbstractSocket.NetworkLayerProtocol.IPv6Protocol]:
|
||||
if address.protocol() in [QtNetwork.QAbstractSocket.IPv4Protocol, QtNetwork.QAbstractSocket.IPv6Protocol]:
|
||||
address_string = address.toString()
|
||||
if address_string.startswith("169.254") or address_string.startswith("fe80"):
|
||||
# ignore link-local addresses, could not use https://doc.qt.io/qt-5/qhostaddress.html#isLinkLocal
|
||||
@@ -95,11 +70,11 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
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):
|
||||
"""
|
||||
@@ -116,36 +91,6 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
|
||||
self.uiLocalServerPathLineEdit.setText(path)
|
||||
|
||||
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://customerconnect.vmware.com/downloads/details?downloadGroup=PLAYER-1400-VIX1170&productId=687. 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.
|
||||
@@ -156,18 +101,11 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
:returns: current QWidget
|
||||
"""
|
||||
|
||||
pane = dialog.uiTreeWidget.findItems(name, QtCore.Qt.MatchFlag.MatchFixedString)[0]
|
||||
pane = dialog.uiTreeWidget.findItems(name, QtCore.Qt.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.
|
||||
@@ -176,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)
|
||||
|
||||
@@ -213,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:
|
||||
@@ -289,112 +175,44 @@ 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"] = "http"
|
||||
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.
|
||||
@@ -419,20 +237,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
|
||||
|
||||
71
gns3/dialogs/show_readme_dialog.py
Normal 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)
|
||||
@@ -67,8 +67,8 @@ 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("%Y-%m-%d at %H:%M:%S")))
|
||||
item.setData(QtCore.Qt.ItemDataRole.UserRole, snapshot["snapshot_id"])
|
||||
item.setText("{} on {}".format(snapshot["name"], datetime.fromtimestamp(snapshot["created_at"]).strftime("%d/%m/%y at %H:%M:%S")))
|
||||
item.setData(QtCore.Qt.UserRole, snapshot["snapshot_id"])
|
||||
|
||||
if self.uiSnapshotsList.count():
|
||||
self.uiSnapshotsList.setCurrentRow(0)
|
||||
@@ -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.EchoMode.Normal, "Unnamed")
|
||||
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.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.ItemDataRole.UserRole)
|
||||
snapshot_id = item.data(QtCore.Qt.UserRole)
|
||||
Controller.instance().delete("/projects/{}/snapshots/{}".format(self._project.id(), snapshot_id), self._deleteSnapshotsCallback)
|
||||
|
||||
def _deleteSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
|
||||
@@ -125,7 +128,7 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
|
||||
|
||||
item = self.uiSnapshotsList.currentItem()
|
||||
if item:
|
||||
snapshot_id = item.data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
snapshot_id = item.data(QtCore.Qt.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.StandardButton.Ok, QtWidgets.QMessageBox.StandardButton.Cancel)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.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.Ok, QtWidgets.QMessageBox.Cancel)
|
||||
if reply == QtWidgets.QMessageBox.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.ItemDataRole.UserRole)
|
||||
snapshot_id = item.data(QtCore.Qt.UserRole)
|
||||
self._restoreSnapshot(snapshot_id)
|
||||
|
||||
@@ -22,6 +22,7 @@ Style editor to edit Shape items.
|
||||
from ..qt import QtCore, QtWidgets, QtGui
|
||||
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
|
||||
from ..items.shape_item import ShapeItem
|
||||
from ..items.line_item import LineItem
|
||||
from ..items.rectangle_item import RectangleItem
|
||||
|
||||
|
||||
@@ -42,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.StandardButton.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.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("No border", QtCore.Qt.PenStyle.NoPen)
|
||||
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)
|
||||
if True not in list(map(lambda item: isinstance(item, LineItem), items)):
|
||||
self.uiBorderStyleComboBox.addItem("No border", QtCore.Qt.NoPen)
|
||||
|
||||
# use the first item in the list as the model
|
||||
first_item = items[0]
|
||||
@@ -101,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.ColorDialogOption.ShowAlphaChannel)
|
||||
color = QtWidgets.QColorDialog.getColor(self._color, self, "Select Color", QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self._color = color
|
||||
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
|
||||
@@ -114,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.ColorDialogOption.ShowAlphaChannel)
|
||||
color = QtWidgets.QColorDialog.getColor(self._border_color, self, "Select Color", QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self._border_color = color
|
||||
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
|
||||
@@ -128,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.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
|
||||
pen = QtGui.QPen(self._border_color, self.uiBorderWidthSpinBox.value(), border_style, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
if self._color:
|
||||
brush = QtGui.QBrush(self._color)
|
||||
else:
|
||||
|
||||
@@ -41,19 +41,15 @@ class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
self._link = link
|
||||
self._link_style = {}
|
||||
|
||||
self.uiBorderColorLabel.setText("Link color")
|
||||
self.uiBorderWidthLabel.setText("Link width")
|
||||
self.uiBorderStyleLabel.setText("Link style")
|
||||
|
||||
self.uiBorderColorPushButton.clicked.connect(self._setBorderColorSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.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.uiBorderStyleComboBox.addItem("Solid", QtCore.Qt.SolidLine)
|
||||
self.uiBorderStyleComboBox.addItem("Dash", QtCore.Qt.DashLine)
|
||||
self.uiBorderStyleComboBox.addItem("Dot", QtCore.Qt.DotLine)
|
||||
self.uiBorderStyleComboBox.addItem("Dash Dot", QtCore.Qt.DashDotLine)
|
||||
self.uiBorderStyleComboBox.addItem("Dash Dot Dot", QtCore.Qt.DashDotDotLine)
|
||||
self.uiBorderStyleComboBox.addItem("Invisible", QtCore.Qt.NoPen)
|
||||
|
||||
self.uiColorLabel.hide()
|
||||
self.uiColorPushButton.hide()
|
||||
@@ -69,13 +65,10 @@ class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
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.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:
|
||||
@@ -88,7 +81,7 @@ class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
Slot to select the border color.
|
||||
"""
|
||||
|
||||
color = QtWidgets.QColorDialog.getColor(self._border_color, self, "Select Color", QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel)
|
||||
color = QtWidgets.QColorDialog.getColor(self._border_color, self, "Select Color", QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self._border_color = color
|
||||
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
|
||||
@@ -102,16 +95,15 @@ class StyleEditorDialogLink(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.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
|
||||
pen = QtGui.QPen(self._border_color, self.uiBorderWidthSpinBox.value(), border_style, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
|
||||
self._link.setPen(pen)
|
||||
|
||||
new_link_style = {
|
||||
"color": self._border_color.name(),
|
||||
"width": self.uiBorderWidthSpinBox.value(),
|
||||
"type": border_style.value,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -50,16 +50,16 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
self.setupUi(self)
|
||||
|
||||
self._items = items
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.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.StandardLocation.PicturesLocation)
|
||||
SymbolSelectionDialog._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
|
||||
|
||||
if not self._items:
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Apply).hide()
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).hide()
|
||||
|
||||
self.uiBuiltInSymbolRadioButton.setChecked(True)
|
||||
self.uiSymbolTreeWidget.setFocus()
|
||||
@@ -85,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.ItemFlag.ItemIsSelectable)
|
||||
parent.setFlags(parent.flags() & ~QtCore.Qt.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.ItemDataRole.UserRole, symbol)
|
||||
item.setData(0, QtCore.Qt.UserRole, symbol)
|
||||
item.setToolTip(0, symbol.id())
|
||||
self._symbol_items.append(item)
|
||||
item.setText(0, name)
|
||||
@@ -101,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.Format_ARGB32)
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
svg_renderer.render(QtGui.QPainter(image))
|
||||
@@ -111,7 +111,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
|
||||
|
||||
for parent in self._parents.values():
|
||||
parent.sortChildren(0, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
parent.sortChildren(0, QtCore.Qt.AscendingOrder)
|
||||
self.adjustSize()
|
||||
|
||||
def _searchTextChangedSlot(self, text):
|
||||
@@ -123,7 +123,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
"""
|
||||
text = self.uiSearchLineEdit.text()
|
||||
for item in self._symbol_items:
|
||||
# if not item.data(0, QtCore.Qt.ItemDataRole.UserRole).builtin():
|
||||
# if not item.data(0, QtCore.Qt.UserRole).builtin():
|
||||
# item.setHidden(True)
|
||||
# else:
|
||||
if not text.strip() or text.strip().lower() in item.text(0).lower():
|
||||
@@ -176,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.ItemDataRole.UserRole).id()
|
||||
return current.data(0, QtCore.Qt.UserRole).id()
|
||||
else:
|
||||
return os.path.basename(self.uiSymbolLineEdit.text())
|
||||
return None
|
||||
@@ -184,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:
|
||||
|
||||
@@ -39,7 +39,7 @@ 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.StandardButton.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
|
||||
# use the first item in the list as the model
|
||||
first_item = items[0]
|
||||
@@ -49,7 +49,7 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
|
||||
self.uiPlainTextEdit.setFont(first_item.font())
|
||||
|
||||
if not first_item.editable():
|
||||
self.uiPlainTextEdit.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction)
|
||||
self.uiPlainTextEdit.setTextInteractionFlags(QtCore.Qt.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.FontDialogOption.DontUseNativeDialog)
|
||||
options=QtWidgets.QFontDialog.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.ColorDialogOption.ShowAlphaChannel)
|
||||
color = QtWidgets.QColorDialog.getColor(self._color, self, None, QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self._setColor(color)
|
||||
|
||||
|
||||
@@ -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.DialogCode.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.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):
|
||||
"""
|
||||
|
||||
@@ -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.WizardStyle.ModernStyle)
|
||||
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtWidgets.QWizard.WizardOption.NoDefaultButton)
|
||||
self.setOptions(QtWidgets.QWizard.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)
|
||||
|
||||
@@ -100,8 +100,6 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._node_grid_color = self.DEFAULT_NODE_GRID_COLOR
|
||||
self._last_mouse_position = None
|
||||
self._topology = Topology.instance()
|
||||
self._background_warning_msgbox = QtWidgets.QErrorMessage(self)
|
||||
self._background_warning_msgbox.setWindowTitle("Layer position")
|
||||
|
||||
# set the scene
|
||||
scene = QtWidgets.QGraphicsScene(parent=self)
|
||||
@@ -111,14 +109,14 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self.setSceneSize(width, height)
|
||||
|
||||
# set the custom flags for this view
|
||||
self.setDragMode(QtWidgets.QGraphicsView.DragMode.RubberBandDrag)
|
||||
self.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
|
||||
self.setTransformationAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.setResizeAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorViewCenter)
|
||||
self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
|
||||
self.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
|
||||
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter)
|
||||
|
||||
# default directories for QFileDialog
|
||||
self._import_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation)
|
||||
self._export_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation)
|
||||
self._import_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
|
||||
self._export_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
|
||||
self._local_addresses = ['0.0.0.0', '127.0.0.1', 'localhost', '::1', '0:0:0:0:0:0:0:1', '::', QtNetwork.QHostInfo.localHostName()]
|
||||
|
||||
def setSceneSize(self, width, height):
|
||||
@@ -237,12 +235,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
if enabled:
|
||||
self.setCursor(QtCore.Qt.CursorShape.CrossCursor)
|
||||
self.setCursor(QtCore.Qt.CrossCursor)
|
||||
else:
|
||||
if self._newlink and self._newlink in self.scene().items():
|
||||
self.scene().removeItem(self._newlink)
|
||||
self._newlink = None
|
||||
self.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_link = enabled
|
||||
|
||||
def addNote(self, state):
|
||||
@@ -254,10 +252,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
if state:
|
||||
self._adding_note = True
|
||||
self.setCursor(QtCore.Qt.CursorShape.IBeamCursor)
|
||||
self.setCursor(QtCore.Qt.IBeamCursor)
|
||||
else:
|
||||
self._adding_note = False
|
||||
self.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addRectangle(self, state):
|
||||
"""
|
||||
@@ -268,10 +266,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
if state:
|
||||
self._adding_rectangle = True
|
||||
self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
|
||||
self.setCursor(QtCore.Qt.PointingHandCursor)
|
||||
else:
|
||||
self._adding_rectangle = False
|
||||
self.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addEllipse(self, state):
|
||||
"""
|
||||
@@ -282,10 +280,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
if state:
|
||||
self._adding_ellipse = True
|
||||
self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
|
||||
self.setCursor(QtCore.Qt.PointingHandCursor)
|
||||
else:
|
||||
self._adding_ellipse = False
|
||||
self.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addLine(self, state):
|
||||
"""
|
||||
@@ -296,10 +294,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
if state:
|
||||
self._adding_line = True
|
||||
self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
|
||||
self.setCursor(QtCore.Qt.PointingHandCursor)
|
||||
else:
|
||||
self._adding_line = False
|
||||
self.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addImage(self, image_path):
|
||||
"""
|
||||
@@ -401,7 +399,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# link addition code
|
||||
if not self._newlink:
|
||||
source_item = item
|
||||
source_port = source_item.connectToPort(event.globalPosition().toPoint())
|
||||
source_port = source_item.connectToPort(event.globalPos())
|
||||
if not source_port:
|
||||
return
|
||||
|
||||
@@ -410,15 +408,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
return
|
||||
|
||||
if source_port.linkType() == "Serial":
|
||||
self._newlink = SerialLinkItem(source_item, source_port, self.mapToScene(event.position().toPoint()), None, adding_flag=True)
|
||||
self._newlink = SerialLinkItem(source_item, source_port, self.mapToScene(event.pos()), None, adding_flag=True)
|
||||
else:
|
||||
self._newlink = EthernetLinkItem(source_item, source_port, self.mapToScene(event.position().toPoint()), None, adding_flag=True)
|
||||
self._newlink = EthernetLinkItem(source_item, source_port, self.mapToScene(event.pos()), None, adding_flag=True)
|
||||
self.scene().addItem(self._newlink)
|
||||
else:
|
||||
source_item = self._newlink.sourceItem()
|
||||
source_port = self._newlink.sourcePort()
|
||||
destination_item = item
|
||||
destination_port = destination_item.connectToPort(event.globalPosition().toPoint())
|
||||
destination_port = destination_item.connectToPort(event.globalPos())
|
||||
if not destination_port:
|
||||
return
|
||||
|
||||
@@ -441,7 +439,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
is_not_link = True
|
||||
is_not_logo = True
|
||||
|
||||
item = self.itemAt(event.position().toPoint())
|
||||
item = self.itemAt(event.pos())
|
||||
if item and sip.isdeleted(item):
|
||||
return
|
||||
elif not item:
|
||||
@@ -456,57 +454,57 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if isinstance(it, LinkItem):
|
||||
it.setHovered(False)
|
||||
|
||||
if (event.buttons() == QtCore.Qt.MouseButton.LeftButton and event.modifiers() == QtCore.Qt.KeyboardModifier.ShiftModifier) or event.buttons() == QtCore.Qt.MouseButton.MiddleButton:
|
||||
if (event.buttons() == QtCore.Qt.LeftButton and event.modifiers() == QtCore.Qt.ShiftModifier) or event.buttons() == QtCore.Qt.MidButton:
|
||||
# checks to see if either the middle mouse is pressed
|
||||
# or a combination of left mouse button and SHIT key are pressed to start dragging the view
|
||||
self._last_mouse_position = self.mapFromGlobal(event.globalPosition())
|
||||
self._last_mouse_position = self.mapFromGlobal(event.globalPos())
|
||||
self._dragging = True
|
||||
self.setCursor(QtCore.Qt.CursorShape.ClosedHandCursor)
|
||||
self.setCursor(QtCore.Qt.ClosedHandCursor)
|
||||
return
|
||||
|
||||
if is_not_link and item and event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier and event.button() == QtCore.Qt.MouseButton.LeftButton and item and not self._adding_link:
|
||||
if is_not_link and item and event.modifiers() == QtCore.Qt.ControlModifier and event.button() == QtCore.Qt.LeftButton and item and not self._adding_link:
|
||||
# manual selection using CTRL
|
||||
if item.isSelected():
|
||||
item.setSelected(False)
|
||||
else:
|
||||
item.setSelected(True)
|
||||
elif is_not_link and is_not_logo and event.button() == QtCore.Qt.MouseButton.RightButton and not self._adding_link:
|
||||
elif is_not_link and is_not_logo and event.button() == QtCore.Qt.RightButton and not self._adding_link:
|
||||
pass #TODO: remove this without creating a bug...
|
||||
elif is_not_link and self._adding_link and event.button() == QtCore.Qt.MouseButton.RightButton:
|
||||
elif is_not_link and self._adding_link and event.button() == QtCore.Qt.RightButton:
|
||||
# send a escape key to the main window to cancel the link addition
|
||||
key = QtGui.QKeyEvent(QtCore.QEvent.Type.KeyPress, QtCore.Qt.Key.Key_Escape, QtCore.Qt.KeyboardModifier.NoModifier)
|
||||
key = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Escape, QtCore.Qt.NoModifier)
|
||||
QtWidgets.QApplication.sendEvent(self._main_window, key)
|
||||
elif item and isinstance(item, NodeItem) and self._adding_link and event.button() == QtCore.Qt.MouseButton.LeftButton:
|
||||
elif item and isinstance(item, NodeItem) and self._adding_link and event.button() == QtCore.Qt.LeftButton:
|
||||
self._userNodeLinking(event, item)
|
||||
#context_event = QtGui.QContextMenuEvent(QtGui.QContextMenuEvent.Reason.Mouse, event.pos())
|
||||
#context_event = QtGui.QContextMenuEvent(QtGui.QContextMenuEvent.Mouse, event.pos())
|
||||
#QtWidgets.QApplication.sendEvent(self, context_event)
|
||||
elif event.button() == QtCore.Qt.MouseButton.LeftButton and self._adding_note:
|
||||
pos = self.mapToScene(event.position().toPoint())
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_note:
|
||||
pos = self.mapToScene(event.pos())
|
||||
note = self.createDrawingItem("text", int(pos.x()), int(pos.y()), 2)
|
||||
pos_x = note.pos().x()
|
||||
pos_y = note.pos().y() - (note.boundingRect().height() / 2)
|
||||
note.setPos(pos_x, pos_y)
|
||||
note.editText()
|
||||
self._main_window.uiAddNoteAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_note = False
|
||||
elif event.button() == QtCore.Qt.MouseButton.LeftButton and self._adding_rectangle:
|
||||
pos = self.mapToScene(event.position().toPoint())
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_rectangle:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("rect", int(pos.x()), int(pos.y()), 1)
|
||||
self._main_window.uiDrawRectangleAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_rectangle = False
|
||||
elif event.button() == QtCore.Qt.MouseButton.LeftButton and self._adding_ellipse:
|
||||
pos = self.mapToScene(event.position().toPoint())
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_ellipse:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("ellipse", int(pos.x()), int(pos.y()), 1)
|
||||
self._main_window.uiDrawEllipseAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_ellipse = False
|
||||
elif event.button() == QtCore.Qt.MouseButton.LeftButton and self._adding_line:
|
||||
pos = self.mapToScene(event.position().toPoint())
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_line:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("line", int(pos.x()), int(pos.y()), 1)
|
||||
self._main_window.uiDrawLineAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_line = False
|
||||
else:
|
||||
super().mousePressEvent(event)
|
||||
@@ -540,7 +538,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if item and not sip.isdeleted(item):
|
||||
# Prevent right clicking on a selected item from de-selecting all other items
|
||||
if not item.isSelected():
|
||||
if not (event.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier):
|
||||
if not event.modifiers() & QtCore.Qt.ControlModifier:
|
||||
for it in self.scene().items():
|
||||
it.setSelected(False)
|
||||
item.setSelected(True)
|
||||
@@ -566,12 +564,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
# If the left mouse button is not still pressed TOGETHER with the SHIFT key and neither is the middle button
|
||||
# this means the user is no longer trying to drag the view
|
||||
if self._dragging and not (event.buttons() == QtCore.Qt.MouseButton.LeftButton and event.modifiers() == QtCore.Qt.KeyboardModifier.ShiftModifier) and not (event.buttons() & QtCore.Qt.MouseButton.MiddleButton):
|
||||
if self._dragging and not (event.buttons() == QtCore.Qt.LeftButton and event.modifiers() == QtCore.Qt.ShiftModifier) and not event.buttons() & QtCore.Qt.MidButton:
|
||||
self._dragging = False
|
||||
self.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
else:
|
||||
item = self.itemAt(event.position().toPoint())
|
||||
if item is not None and not (event.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier):
|
||||
item = self.itemAt(event.pos())
|
||||
if item is not None and not event.modifiers() & QtCore.Qt.ControlModifier:
|
||||
item.setSelected(True)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
@@ -584,7 +582,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
:param: QWheelEvent instance
|
||||
"""
|
||||
|
||||
if event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier:
|
||||
if event.modifiers() == QtCore.Qt.ControlModifier:
|
||||
delta = event.angleDelta()
|
||||
if delta is not None and delta.x() == 0:
|
||||
# CTRL is pressed then use the mouse wheel to zoom in or out.
|
||||
@@ -612,7 +610,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
if event.key() == QtCore.Qt.Key.Key_Delete:
|
||||
if event.key() == QtCore.Qt.Key_Delete:
|
||||
# check if we are editing an LabelItem instance, then send the delete key event to it
|
||||
for item in self.scene().selectedItems():
|
||||
if (isinstance(item, LabelItem) or isinstance(item, TextItem)) and item.hasFocus():
|
||||
@@ -632,19 +630,19 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# This if statement event checks to see if the user is dragging the scene
|
||||
# if so it sets the value of the scene scroll bars based on the change between
|
||||
# the previous and current mouse position
|
||||
mapped_global_pos = self.mapFromGlobal(event.globalPosition())
|
||||
mapped_global_pos = self.mapFromGlobal(event.globalPos())
|
||||
hBar = self.horizontalScrollBar()
|
||||
vBar = self.verticalScrollBar()
|
||||
delta = mapped_global_pos - self._last_mouse_position
|
||||
hBar.setValue(int(hBar.value() + (delta.x() if QtWidgets.QApplication.isRightToLeft() else -delta.x())))
|
||||
vBar.setValue(int(vBar.value() - delta.y()))
|
||||
hBar.setValue(hBar.value() + (delta.x() if QtWidgets.QApplication.isRightToLeft() else -delta.x()))
|
||||
vBar.setValue(vBar.value() - delta.y())
|
||||
self._last_mouse_position = mapped_global_pos
|
||||
if self._adding_link and self._newlink and self._newlink in self.scene().items():
|
||||
# update the mouse position when the user is adding a link.
|
||||
self._newlink.setMousePoint(self.mapToScene(event.position().toPoint()))
|
||||
self._newlink.setMousePoint(self.mapToScene(event.pos()))
|
||||
event.ignore()
|
||||
else:
|
||||
item = self.itemAt(event.position().toPoint())
|
||||
item = self.itemAt(event.pos())
|
||||
if item:
|
||||
# show item coords in the status bar
|
||||
coords = "X: {} Y: {} Z: {}".format(item.x(), item.y(), item.zValue())
|
||||
@@ -663,7 +661,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
:param event: QMouseEvent instance
|
||||
"""
|
||||
|
||||
item = self.itemAt(event.position().toPoint())
|
||||
item = self.itemAt(event.pos())
|
||||
|
||||
if not self._adding_link:
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
@@ -697,7 +695,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
node_properties = NodePropertiesDialog(items, self._main_window)
|
||||
node_properties.setModal(True)
|
||||
node_properties.show()
|
||||
node_properties.exec()
|
||||
node_properties.exec_()
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
"""
|
||||
@@ -725,21 +723,21 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# check if what has been dropped is handled by this view
|
||||
if event.mimeData().hasFormat("application/x-gns3-template"):
|
||||
template_id = event.mimeData().data("application/x-gns3-template").data().decode()
|
||||
event.setDropAction(QtCore.Qt.DropAction.CopyAction)
|
||||
event.setDropAction(QtCore.Qt.CopyAction)
|
||||
event.accept()
|
||||
if event.modifiers() == QtCore.Qt.KeyboardModifier.ShiftModifier:
|
||||
if event.keyboardModifiers() == QtCore.Qt.ShiftModifier:
|
||||
max_nodes_per_line = 10 # max number of nodes on a single line
|
||||
offset = 100 # spacing between elements
|
||||
integer, ok = QtWidgets.QInputDialog.getInt(self, "Nodes", "Number of nodes:", 2, 1, 100, 1)
|
||||
if ok:
|
||||
for node_number in range(integer):
|
||||
x = event.position().x() - (150 // 2) + (node_number % max_nodes_per_line) * offset
|
||||
y = event.position().y() - (70 // 2) + (node_number // max_nodes_per_line) * offset
|
||||
if self.createNodeFromTemplateId(template_id, QtCore.QPointF(x, y)) is False:
|
||||
x = event.pos().x() - (150 // 2) + (node_number % max_nodes_per_line) * offset
|
||||
y = event.pos().y() - (70 // 2) + (node_number // max_nodes_per_line) * offset
|
||||
if self.createNodeFromTemplateId(template_id, QtCore.QPoint(x, y)) is False:
|
||||
event.ignore()
|
||||
break
|
||||
else:
|
||||
if self.createNodeFromTemplateId(template_id, event.position()) is False:
|
||||
if self.createNodeFromTemplateId(template_id, event.pos()) is False:
|
||||
event.ignore()
|
||||
elif event.mimeData().hasFormat("text/uri-list") and event.mimeData().hasUrls():
|
||||
# This should not arrive but we received bug report with it...
|
||||
@@ -762,9 +760,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
:param pos: position where to display the menu
|
||||
"""
|
||||
|
||||
menu = QtWidgets.QMenu(parent=self)
|
||||
menu = QtWidgets.QMenu()
|
||||
self.populateDeviceContextualMenu(menu)
|
||||
menu.exec(pos)
|
||||
menu.exec_(pos)
|
||||
menu.clear()
|
||||
# Make sure to deselect all items.
|
||||
# This is to prevent a bug on Windows
|
||||
@@ -785,145 +783,157 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configPage"), items)):
|
||||
# Action: Configure node
|
||||
configure_action = QtGui.QAction("Configure", menu)
|
||||
configure_action = QtWidgets.QAction("Configure", menu)
|
||||
configure_action.setIcon(get_icon("configuration.svg"))
|
||||
configure_action.triggered.connect(self.configureActionSlot)
|
||||
menu.addAction(configure_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and item.node().console() is not None, items)):
|
||||
console_action = QtGui.QAction("Console", menu)
|
||||
console_action = QtWidgets.QAction("Console", menu)
|
||||
console_action.setIcon(get_icon("console.svg"))
|
||||
console_action.triggered.connect(self.consoleActionSlot)
|
||||
menu.addAction(console_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole"), items)):
|
||||
aux_console_action = QtGui.QAction("Auxiliary console", menu)
|
||||
aux_console_action = QtWidgets.QAction("Auxiliary console", menu)
|
||||
aux_console_action.setIcon(get_icon("aux-console.svg"))
|
||||
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
|
||||
menu.addAction(aux_console_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
start_action = QtGui.QAction("Start", menu)
|
||||
start_action = QtWidgets.QAction("Start", menu)
|
||||
start_action.setIcon(get_icon("start.svg"))
|
||||
start_action.triggered.connect(self.startActionSlot)
|
||||
menu.addAction(start_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
suspend_action = QtGui.QAction("Suspend", menu)
|
||||
suspend_action = QtWidgets.QAction("Suspend", menu)
|
||||
suspend_action.setIcon(get_icon("pause.svg"))
|
||||
suspend_action.triggered.connect(self.suspendActionSlot)
|
||||
menu.addAction(suspend_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
stop_action = QtGui.QAction("Stop", menu)
|
||||
stop_action = QtWidgets.QAction("Stop", menu)
|
||||
stop_action.setIcon(get_icon("stop.svg"))
|
||||
stop_action.triggered.connect(self.stopActionSlot)
|
||||
menu.addAction(stop_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)):
|
||||
reload_action = QtGui.QAction("Reload", menu)
|
||||
reload_action = QtWidgets.QAction("Reload", menu)
|
||||
reload_action.setIcon(get_icon("reload.svg"))
|
||||
reload_action.triggered.connect(self.reloadActionSlot)
|
||||
menu.addAction(reload_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and item.node().console() is not None, items)):
|
||||
console_edit_action = QtGui.QAction("Custom console", menu)
|
||||
console_edit_action = QtWidgets.QAction("Custom console", menu)
|
||||
console_edit_action.setIcon(get_icon("console_edit.svg"))
|
||||
console_edit_action.triggered.connect(self.customConsoleActionSlot)
|
||||
menu.addAction(console_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
|
||||
isolate_action = QtWidgets.QAction("Isolate", menu)
|
||||
isolate_action.setIcon(get_icon("link-pause.svg"))
|
||||
isolate_action.triggered.connect(self.isolateActionSlot)
|
||||
menu.addAction(isolate_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
|
||||
unisolate_action = QtWidgets.QAction("Un-isolate", menu)
|
||||
unisolate_action.setIcon(get_icon("link-start.svg"))
|
||||
unisolate_action.triggered.connect(self.unisolateActionSlot)
|
||||
menu.addAction(unisolate_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
|
||||
# Action: Change hostname
|
||||
change_hostname_action = QtGui.QAction("Change hostname", menu)
|
||||
change_hostname_action = QtWidgets.QAction("Change hostname", menu)
|
||||
change_hostname_action.setIcon(get_icon("show-hostname.svg"))
|
||||
change_hostname_action.triggered.connect(self.changeHostnameActionSlot)
|
||||
menu.addAction(change_hostname_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem), items)):
|
||||
# Action: Change symbol
|
||||
change_symbol_action = QtGui.QAction("Change symbol", menu)
|
||||
change_symbol_action = QtWidgets.QAction("Change symbol", menu)
|
||||
change_symbol_action.setIcon(get_icon("node_conception.svg"))
|
||||
change_symbol_action.triggered.connect(self.changeSymbolActionSlot)
|
||||
menu.addAction(change_symbol_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, DrawingItem) or isinstance(item, NodeItem), items)):
|
||||
duplicate_action = QtGui.QAction("Duplicate", menu)
|
||||
duplicate_action = QtWidgets.QAction("Duplicate", menu)
|
||||
duplicate_action.setIcon(get_icon("duplicate.svg"))
|
||||
duplicate_action.triggered.connect(self.duplicateActionSlot)
|
||||
menu.addAction(duplicate_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "info"), items)):
|
||||
# Action: Show node information
|
||||
show_node_info_action = QtGui.QAction("Show node information", menu)
|
||||
show_node_info_action = QtWidgets.QAction("Show node information", menu)
|
||||
show_node_info_action.setIcon(get_icon("help.svg"))
|
||||
show_node_info_action.triggered.connect(self.showNodeInfoSlot)
|
||||
menu.addAction(show_node_info_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "nodeDir"), items)):
|
||||
# Action: Show in file manager
|
||||
show_in_file_manager_action = QtGui.QAction("Show in file manager", menu)
|
||||
show_in_file_manager_action = QtWidgets.QAction("Show in file manager", menu)
|
||||
show_in_file_manager_action.setIcon(get_icon("open.svg"))
|
||||
show_in_file_manager_action.triggered.connect(self.showInFileManagerSlot)
|
||||
menu.addAction(show_in_file_manager_action)
|
||||
|
||||
if not sys.platform.startswith("darwin") and True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"), items)):
|
||||
# Action: bring console or window to front (Windows and Linux only)
|
||||
bring_to_front_action = QtGui.QAction("Bring to front", menu)
|
||||
bring_to_front_action = QtWidgets.QAction("Bring to front", menu)
|
||||
bring_to_front_action.setIcon(get_icon("front.svg"))
|
||||
bring_to_front_action.triggered.connect(self.bringToFrontSlot)
|
||||
menu.addAction(bring_to_front_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and bool(item.node().configFiles()), items)):
|
||||
import_config_action = QtGui.QAction("Import config", menu)
|
||||
import_config_action = QtWidgets.QAction("Import config", menu)
|
||||
import_config_action.setIcon(get_icon("import.svg"))
|
||||
import_config_action.triggered.connect(self.importConfigActionSlot)
|
||||
menu.addAction(import_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and bool(item.node().configFiles()), items)):
|
||||
export_config_action = QtGui.QAction("Export config", menu)
|
||||
export_config_action = QtWidgets.QAction("Export config", menu)
|
||||
export_config_action.setIcon(get_icon("export.svg"))
|
||||
export_config_action.triggered.connect(self.exportConfigActionSlot)
|
||||
menu.addAction(export_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and bool(item.node().configTextFiles()), items)):
|
||||
export_config_action = QtGui.QAction("Edit config", menu)
|
||||
export_config_action = QtWidgets.QAction("Edit config", menu)
|
||||
export_config_action.setIcon(get_icon("edit.svg"))
|
||||
export_config_action.triggered.connect(self.editConfigActionSlot)
|
||||
menu.addAction(export_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "idlepc"), items)):
|
||||
idlepc_action = QtGui.QAction("Idle-PC", menu)
|
||||
idlepc_action = QtWidgets.QAction("Idle-PC", menu)
|
||||
idlepc_action.setIcon(get_icon("calculate.svg"))
|
||||
idlepc_action.triggered.connect(self.idlepcActionSlot)
|
||||
menu.addAction(idlepc_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "idlepc"), items)):
|
||||
auto_idlepc_action = QtGui.QAction("Auto Idle-PC", menu)
|
||||
auto_idlepc_action = QtWidgets.QAction("Auto Idle-PC", menu)
|
||||
auto_idlepc_action.setIcon(get_icon("calculate.svg"))
|
||||
auto_idlepc_action.triggered.connect(self.autoIdlepcActionSlot)
|
||||
menu.addAction(auto_idlepc_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, LabelItem), items)):
|
||||
text_edit_action = QtGui.QAction("Text edit", menu)
|
||||
text_edit_action = QtWidgets.QAction("Text edit", menu)
|
||||
text_edit_action.setIcon(get_icon("show-hostname.svg"))
|
||||
text_edit_action.triggered.connect(self.textEditActionSlot)
|
||||
menu.addAction(text_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, TextItem), items)):
|
||||
text_edit_action = QtGui.QAction("Text edit", menu)
|
||||
text_edit_action = QtWidgets.QAction("Text edit", menu)
|
||||
text_edit_action.setIcon(get_icon("edit.svg"))
|
||||
text_edit_action.triggered.connect(self.textEditActionSlot)
|
||||
menu.addAction(text_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, ShapeItem) or isinstance(item, LineItem), items)):
|
||||
style_action = QtGui.QAction("Style", menu)
|
||||
style_action = QtWidgets.QAction("Style", menu)
|
||||
style_action.setIcon(get_icon("node_conception.svg"))
|
||||
style_action.triggered.connect(self.styleActionSlot)
|
||||
menu.addAction(style_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, LabelItem), items)) and False in list(map(lambda item: item.parentItem() is None, items)):
|
||||
# action only for port labels
|
||||
reset_label_position_action = QtGui.QAction("Reset position", menu)
|
||||
reset_label_position_action = QtWidgets.QAction("Reset position", menu)
|
||||
reset_label_position_action.setIcon(get_icon("reset.svg"))
|
||||
reset_label_position_action.triggered.connect(self.resetLabelPositionActionSlot)
|
||||
menu.addAction(reset_label_position_action)
|
||||
@@ -932,41 +942,41 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if True in list(map(lambda item: item.parentItem() is None, items)):
|
||||
|
||||
if len(items) > 1:
|
||||
horizontal_align_action = QtGui.QAction("Align horizontally", menu)
|
||||
horizontal_align_action = QtWidgets.QAction("Align horizontally", menu)
|
||||
horizontal_align_action.setIcon(get_icon("horizontally.svg"))
|
||||
horizontal_align_action.triggered.connect(self.horizontalAlignmentSlot)
|
||||
menu.addAction(horizontal_align_action)
|
||||
|
||||
vertical_align_action = QtGui.QAction("Align vertically", menu)
|
||||
vertical_align_action = QtWidgets.QAction("Align vertically", menu)
|
||||
vertical_align_action.setIcon(get_icon("vertically.svg"))
|
||||
vertical_align_action.triggered.connect(self.verticalAlignmentSlot)
|
||||
menu.addAction(vertical_align_action)
|
||||
|
||||
raise_layer_action = QtGui.QAction("Raise one layer", menu)
|
||||
raise_layer_action = QtWidgets.QAction("Raise one layer", menu)
|
||||
raise_layer_action.setIcon(get_icon("raise_z_value.svg"))
|
||||
raise_layer_action.triggered.connect(self.raiseLayerActionSlot)
|
||||
menu.addAction(raise_layer_action)
|
||||
|
||||
lower_layer_action = QtGui.QAction("Lower one layer", menu)
|
||||
lower_layer_action = QtWidgets.QAction("Lower one layer", menu)
|
||||
lower_layer_action.setIcon(get_icon("lower_z_value.svg"))
|
||||
lower_layer_action.triggered.connect(self.lowerLayerActionSlot)
|
||||
menu.addAction(lower_layer_action)
|
||||
|
||||
if len(items) > 1:
|
||||
lock_action = QtGui.QAction("Lock or unlock items", menu)
|
||||
lock_action = QtWidgets.QAction("Lock or unlock items", menu)
|
||||
lock_action.setIcon(get_icon("lock.svg"))
|
||||
else:
|
||||
item = items[0]
|
||||
if item.flags() & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable:
|
||||
lock_action = QtGui.QAction("Lock item", menu)
|
||||
if item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable:
|
||||
lock_action = QtWidgets.QAction("Lock item", menu)
|
||||
lock_action.setIcon(get_icon("lock.svg"))
|
||||
else:
|
||||
lock_action = QtGui.QAction("Unlock item", menu)
|
||||
lock_action = QtWidgets.QAction("Unlock item", menu)
|
||||
lock_action.setIcon(get_icon("unlock.svg"))
|
||||
lock_action.triggered.connect(self.lockActionSlot)
|
||||
menu.addAction(lock_action)
|
||||
|
||||
delete_action = QtGui.QAction("Delete", menu)
|
||||
delete_action = QtWidgets.QAction("Delete", menu)
|
||||
delete_action.setIcon(get_icon("delete.svg"))
|
||||
delete_action.triggered.connect(self.deleteActionSlot)
|
||||
menu.addAction(delete_action)
|
||||
@@ -1011,6 +1021,26 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "reload") and item.node().initialized():
|
||||
item.node().reload()
|
||||
|
||||
def isolateActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the isolate action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "isolate") and item.node().initialized():
|
||||
item.node().isolate()
|
||||
|
||||
def unisolateActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the unisolate action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "unisolate") and item.node().initialized():
|
||||
item.node().unisolate()
|
||||
|
||||
def configureActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the configure action in the
|
||||
@@ -1033,15 +1063,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
new_hostname, ok = QtWidgets.QInputDialog.getText(self, "Change hostname", "Hostname:", QtWidgets.QLineEdit.EchoMode.Normal, item.node().name())
|
||||
new_hostname, ok = QtWidgets.QInputDialog.getText(self, "Change hostname", "Hostname:", QtWidgets.QLineEdit.Normal, item.node().name())
|
||||
if ok:
|
||||
if not new_hostname.strip():
|
||||
QtWidgets.QMessageBox.critical(self, "Change hostname", "Hostname cannot be blank")
|
||||
continue
|
||||
if hasattr(item.node(), "validateHostname") and not LocalConfig.instance().experimental():
|
||||
if not item.node().validateHostname(new_hostname):
|
||||
QtWidgets.QMessageBox.critical(self, "Change hostname", "Invalid name detected for this node: {}".format(new_hostname))
|
||||
continue
|
||||
item.node().update({"name": new_hostname})
|
||||
|
||||
def changeSymbolActionSlot(self):
|
||||
@@ -1057,7 +1083,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if items:
|
||||
dialog = SymbolSelectionDialog(self, items)
|
||||
dialog.show()
|
||||
dialog.exec()
|
||||
dialog.exec_()
|
||||
|
||||
def showInFileManagerSlot(self):
|
||||
"""
|
||||
@@ -1074,13 +1100,13 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
break
|
||||
|
||||
if os.path.exists(node_dir):
|
||||
log.debug("Open %s in file manager")
|
||||
log.debug(f"Open {node_dir} in file manager")
|
||||
if QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(node_dir)) is False:
|
||||
QtWidgets.QMessageBox.critical(self, "Show in file manager", "Failed to open {}".format(node_dir))
|
||||
break
|
||||
else:
|
||||
reply = QtWidgets.QMessageBox.information(self, "Show in file manager", "The device directory is located in {} on {}\n\nCopy path to clipboard?".format(node_dir, node.compute().name()), QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
reply = QtWidgets.QMessageBox.information(self, "Show in file manager", "The device directory is located in {} on {}\n\nCopy path to clipboard?".format(node_dir, node.compute().name()), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
QtWidgets.QApplication.clipboard().setText(node_dir)
|
||||
break
|
||||
|
||||
@@ -1137,7 +1163,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
delay = self._main_window.settings()["delay_console_all"]
|
||||
counter = 0
|
||||
for name in sorted(nodes.keys(), key=str.casefold):
|
||||
for name in sorted(nodes.keys()):
|
||||
node = nodes[name]
|
||||
callback = qpartial(self.consoleToNode, node)
|
||||
self._main_window.run_later(counter, callback)
|
||||
@@ -1155,10 +1181,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
proceed = QtWidgets.QMessageBox.question(self,
|
||||
"Console to all nodes",
|
||||
"You are about to open console windows to {} nodes. Are you sure?".format(nb_items),
|
||||
QtWidgets.QMessageBox.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No)
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
if proceed == QtWidgets.QMessageBox.StandardButton.No:
|
||||
if proceed == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
self.consoleFromItems(items)
|
||||
|
||||
@@ -1216,7 +1242,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
delay = self._main_window.settings()["delay_console_all"]
|
||||
counter = 0
|
||||
for name in sorted(nodes.keys(), key=str.casefold):
|
||||
for name in sorted(nodes.keys()):
|
||||
node = nodes[name]
|
||||
callback = qpartial(self.consoleToNode, node, aux=True)
|
||||
self._main_window.run_later(counter, callback)
|
||||
@@ -1255,7 +1281,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self,
|
||||
"Import {}".format(os.path.basename(config_file)),
|
||||
self._import_config_directory,
|
||||
"All files (*);;Config files (*.cfg)",
|
||||
"All files (*.*);;Config files (*.cfg)",
|
||||
"Config files (*.cfg)")
|
||||
if not path:
|
||||
continue
|
||||
@@ -1284,7 +1310,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
continue
|
||||
dialog = FileEditorDialog(item.node(), config_file, parent=self)
|
||||
dialog.show()
|
||||
dialog.exec()
|
||||
dialog.exec_()
|
||||
|
||||
def exportConfigActionSlot(self):
|
||||
"""
|
||||
@@ -1302,7 +1328,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
for item in items:
|
||||
for config_file in item.node().configFiles():
|
||||
path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_config_directory, item.node().name() + "_" + os.path.basename(config_file)), "All files (*);;Config files (*.cfg)")
|
||||
path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_config_directory, item.node().name() + "_" + os.path.basename(config_file)), "All files (*.*);;Config files (*.cfg)")
|
||||
if not path:
|
||||
continue
|
||||
self._export_config_directory = os.path.dirname(path)
|
||||
@@ -1322,7 +1348,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if isinstance(item, NodeItem):
|
||||
dialog = NodeInfoDialog(item.node(), parent=self)
|
||||
dialog.show()
|
||||
dialog.exec()
|
||||
dialog.exec_()
|
||||
|
||||
def bringToFrontSlot(self):
|
||||
"""
|
||||
@@ -1363,7 +1389,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if idlepcs and idlepcs[0] != "0x0":
|
||||
dialog = IdlePCDialog(router, idlepcs, parent=self)
|
||||
dialog.show()
|
||||
dialog.exec()
|
||||
dialog.exec_()
|
||||
else:
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC", "Sorry no Idle-PC values could be computed, please check again with Cisco IOS in a different state")
|
||||
|
||||
@@ -1446,7 +1472,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if items:
|
||||
style_dialog = StyleEditorDialog(self._main_window, items)
|
||||
style_dialog.show()
|
||||
style_dialog.exec()
|
||||
style_dialog.exec_()
|
||||
|
||||
def textEditActionSlot(self):
|
||||
"""
|
||||
@@ -1461,7 +1487,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if items:
|
||||
text_edit_dialog = TextEditorDialog(self._main_window, items)
|
||||
text_edit_dialog.show()
|
||||
text_edit_dialog.exec()
|
||||
text_edit_dialog.exec_()
|
||||
|
||||
def resetLabelPositionActionSlot(self):
|
||||
"""
|
||||
@@ -1519,7 +1545,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if item.parentItem() is None:
|
||||
if not (item.flags() & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable):
|
||||
if not (item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable):
|
||||
log.error("Cannot move object to a upper layer because it is locked")
|
||||
continue
|
||||
item.setZValue(item.zValue() + 1)
|
||||
@@ -1534,7 +1560,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if item.parentItem() is None:
|
||||
if not (item.flags() & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable):
|
||||
if not (item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable):
|
||||
log.error("Cannot move object to a lower layer because it is locked")
|
||||
continue
|
||||
item.setZValue(item.zValue() - 1)
|
||||
@@ -1580,8 +1606,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
question = "Do you want to permanently delete {}?".format(selected_nodes[0].name())
|
||||
reply = QtWidgets.QMessageBox.question(self, "Delete", question,
|
||||
QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem):
|
||||
@@ -1617,7 +1643,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
Ask the server to create a node using this template
|
||||
"""
|
||||
pos = self.mapToScene(pos.toPoint())
|
||||
pos = self.mapToScene(pos)
|
||||
return TemplateManager().instance().createNodeFromTemplateId(self._topology.project(), template_id, pos.x(), pos.y())
|
||||
|
||||
def createNodeItem(self, node, symbol, x, y):
|
||||
@@ -1648,15 +1674,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
def createDrawingItem(self, type, x, y, z, locked=False, rotation=0, svg=None, drawing_id=None):
|
||||
|
||||
if type == "ellipse":
|
||||
item = EllipseItem(pos=QtCore.QPointF(x, y), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
item = EllipseItem(pos=QtCore.QPoint(x, y), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "rect":
|
||||
item = RectangleItem(pos=QtCore.QPointF(x, y), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
item = RectangleItem(pos=QtCore.QPoint(x, y), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "line":
|
||||
item = LineItem(pos=QtCore.QPointF(x, y), dst=QtCore.QPoint(200, 0), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
item = LineItem(pos=QtCore.QPoint(x, y), dst=QtCore.QPoint(200, 0), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "image":
|
||||
item = ImageItem(pos=QtCore.QPointF(x, y), z=z, rotation=rotation, locked=locked, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
item = ImageItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, locked=locked, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "text":
|
||||
item = TextItem(pos=QtCore.QPointF(x, y), z=z, rotation=rotation, locked=locked, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
item = TextItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, locked=locked, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
|
||||
if drawing_id is None:
|
||||
item.create()
|
||||
|
||||
1191
gns3/http_client.py
58
gns3/http_client_error.py
Normal 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)
|
||||
@@ -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.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
|
||||
@@ -27,12 +27,12 @@ log = logging.getLogger(__name__)
|
||||
class DrawingItem:
|
||||
# Map QT stroke to SVG style
|
||||
QT_DASH_TO_SVG = {
|
||||
QtCore.Qt.PenStyle.SolidLine: "",
|
||||
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"
|
||||
QtCore.Qt.SolidLine: "none",
|
||||
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"
|
||||
}
|
||||
|
||||
show_layer = False
|
||||
@@ -48,7 +48,7 @@ class DrawingItem:
|
||||
self._locked = locked
|
||||
if self._id is None:
|
||||
self._id = str(uuid.uuid4())
|
||||
self.setFlags(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsFocusable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
self._graphics_view = MainWindow.instance().uiGraphicsView
|
||||
@@ -57,7 +57,7 @@ class DrawingItem:
|
||||
self._project = project
|
||||
|
||||
# Store a hash of the SVG to avoid him
|
||||
# to be sent if he doesn't change
|
||||
# to be send if he doesn't change
|
||||
self._hash_svg = None
|
||||
|
||||
if pos:
|
||||
@@ -93,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):
|
||||
@@ -106,14 +106,9 @@ class DrawingItem:
|
||||
"""
|
||||
|
||||
if error:
|
||||
if "doesn't exist" in result.get("message", ""):
|
||||
log.warning("Drawing not found on server, recreating: {}".format(self._id))
|
||||
self._id = None
|
||||
self.create()
|
||||
return True
|
||||
log.error("Error while updating drawing: {}".format(result["message"]))
|
||||
return False
|
||||
self.setPos(QtCore.QPointF(result["x"], result["y"]))
|
||||
self.setPos(QtCore.QPoint(result["x"], result["y"]))
|
||||
self.setZValue(result["z"])
|
||||
self.setLocked(result["locked"])
|
||||
self.setRotation(result["rotation"])
|
||||
@@ -129,19 +124,19 @@ class DrawingItem:
|
||||
"""
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
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 key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() == 0:
|
||||
self.setRotation(359)
|
||||
else:
|
||||
self.setRotation(self.rotation() - 1)
|
||||
return True
|
||||
elif key in (QtCore.Qt.Key.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:
|
||||
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
return True
|
||||
elif modifiers & QtCore.Qt.KeyboardModifier.AltModifier:
|
||||
elif modifiers & QtCore.Qt.AltModifier:
|
||||
self._allow_snap_to_grid = False
|
||||
return True
|
||||
return False
|
||||
@@ -196,9 +191,9 @@ class DrawingItem:
|
||||
"""
|
||||
|
||||
if locked is True:
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
self._locked = locked
|
||||
|
||||
def deleting(self):
|
||||
@@ -219,7 +214,7 @@ class DrawingItem:
|
||||
"""
|
||||
Deletes this drawing.
|
||||
|
||||
:param skip_controller: Do not replicate change on the controller (useful when it's already deleted on controller)
|
||||
:param skip_controller: Do not replicate change on the controller (usefull when it's already deleted on controller)
|
||||
"""
|
||||
|
||||
self.setDeleting()
|
||||
@@ -227,19 +222,17 @@ class DrawingItem:
|
||||
from ..topology import Topology
|
||||
Topology.instance().removeDrawing(self)
|
||||
if self._id and not skip_controller:
|
||||
self._project.delete("/drawings/" + self._id, None)
|
||||
self._project.delete("/drawings/" + self._id, None, body=self.__json__())
|
||||
|
||||
def itemChange(self, change, value):
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._main_window.uiSnapToGridAction.isChecked() \
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self._main_window.uiSnapToGridAction.isChecked() \
|
||||
and self._allow_snap_to_grid:
|
||||
grid_size = self._graphics_view.drawingGridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
value.setX((grid_size * round((value.x() + mid_x) / grid_size)) - mid_x)
|
||||
mid_y = self.boundingRect().height() / 2
|
||||
value.setY((grid_size * round((value.y()+mid_y)/grid_size)) - mid_y)
|
||||
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 change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if not value:
|
||||
self.updateDrawing()
|
||||
return QtWidgets.QGraphicsItem.itemChange(self, change, value)
|
||||
@@ -263,10 +256,10 @@ class DrawingItem:
|
||||
return
|
||||
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.GlobalColor.red)
|
||||
painter.setPen(QtCore.Qt.GlobalColor.red)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
|
||||
painter.setPen(QtCore.Qt.GlobalColor.black)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
@@ -305,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.PenStyle.NoPen)
|
||||
pen.setStyle(QtCore.Qt.NoPen)
|
||||
else:
|
||||
pen.setStyle(QtCore.Qt.PenStyle.SolidLine)
|
||||
pen.setStyle(QtCore.Qt.SolidLine)
|
||||
stroke = svg.get("stroke-dasharray")
|
||||
if stroke:
|
||||
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
|
||||
|
||||
@@ -54,14 +54,14 @@ class EthernetLinkItem(LinkItem):
|
||||
|
||||
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))
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.red, self._link._link_style["width"] + 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
else:
|
||||
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], QtCore.Qt.PenStyle(self._link._link_style["type"]), QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
|
||||
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], self._link._link_style["type"], QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
except:
|
||||
if self._hovered:
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.red, self._pen_width + 1, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.red, self._pen_width + 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
else:
|
||||
self.setPen(QtGui.QPen(QtGui.QColor("#000000"), self._pen_width, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
|
||||
self.setPen(QtGui.QPen(QtGui.QColor("#000000"), self._pen_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
|
||||
# draw a line between nodes
|
||||
path = QtGui.QPainterPath(self.source)
|
||||
@@ -121,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.GlobalColor.yellow
|
||||
shape = QtCore.Qt.PenCapStyle.RoundCap
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._source_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.GlobalColor.green
|
||||
shape = QtCore.Qt.PenCapStyle.RoundCap
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.GlobalColor.red
|
||||
shape = QtCore.Qt.PenCapStyle.SquareCap
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.PenStyle.SolidLine, shape, QtCore.Qt.PenJoinStyle.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
point1 = QtCore.QPointF(self.source + self.edge_offset) + QtCore.QPointF((self.dx * self._source_collision_offset) / self.length, (self.dy * self._source_collision_offset) / self.length)
|
||||
|
||||
# avoid any collision of the status point with the source node
|
||||
@@ -154,27 +154,27 @@ class EthernetLinkItem(LinkItem):
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
source_port_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, not self._source_item.locked())
|
||||
source_port_label.setFlag(source_port_label.ItemIsMovable, not self._source_item.locked())
|
||||
source_port_label.show()
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.PenStyle.NoPen:
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.NoPen:
|
||||
painter.drawPoint(point1)
|
||||
|
||||
if self._link.suspended() or self._destination_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
color = QtCore.Qt.GlobalColor.yellow
|
||||
shape = QtCore.Qt.PenCapStyle.RoundCap
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.GlobalColor.green
|
||||
shape = QtCore.Qt.PenCapStyle.RoundCap
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.GlobalColor.red
|
||||
shape = QtCore.Qt.PenCapStyle.SquareCap
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.PenStyle.SolidLine, shape, QtCore.Qt.PenJoinStyle.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
point2 = QtCore.QPointF(self.destination - self.edge_offset) - QtCore.QPointF((self.dx * self._destination_collision_offset) / self.length, (self.dy * self._destination_collision_offset) / self.length)
|
||||
|
||||
# avoid any collision of the status point with the destination node
|
||||
@@ -197,12 +197,12 @@ class EthernetLinkItem(LinkItem):
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
destination_port_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, not self._destination_item.locked())
|
||||
destination_port_label.setFlag(destination_port_label.ItemIsMovable, not self._destination_item.locked())
|
||||
destination_port_label.show()
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.PenStyle.NoPen:
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.NoPen:
|
||||
painter.drawPoint(point2)
|
||||
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
Graphical representation of an image on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
from ..qt import QtSvgWidgets
|
||||
from ..qt import QtSvg
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from .drawing_item import DrawingItem
|
||||
|
||||
|
||||
class ImageItem(QtSvgWidgets.QGraphicsSvgItem, DrawingItem):
|
||||
class ImageItem(QtSvg.QGraphicsSvgItem, DrawingItem):
|
||||
|
||||
"""
|
||||
Class to insert an image on the scene.
|
||||
|
||||
@@ -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.GraphicsItemFlag.ItemIsMovable | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.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.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 key in (QtCore.Qt.Key_P, QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Plus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() > -360.0:
|
||||
self.setRotation(self.rotation() - 1)
|
||||
elif key in (QtCore.Qt.Key.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:
|
||||
elif key in (QtCore.Qt.Key_M, QtCore.Qt.Key_Minus) and modifiers & QtCore.Qt.AltModifier \
|
||||
or key == QtCore.Qt.Key_Minus and modifiers & QtCore.Qt.AltModifier and modifiers & QtCore.Qt.KeypadModifier:
|
||||
if self.rotation() < 360.0:
|
||||
self.setRotation(self.rotation() + 1)
|
||||
else:
|
||||
@@ -107,11 +107,11 @@ class LabelItem(QtWidgets.QGraphicsTextItem):
|
||||
Edit mode for this note.
|
||||
"""
|
||||
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextEditorInteraction)
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
|
||||
self.setSelected(True)
|
||||
self.setFocus()
|
||||
cursor = self.textCursor()
|
||||
cursor.select(QtGui.QTextCursor.SelectionType.Document)
|
||||
cursor.select(QtGui.QTextCursor.Document)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
@@ -131,12 +131,12 @@ class LabelItem(QtWidgets.QGraphicsTextItem):
|
||||
:param event: QFocusEvent instance
|
||||
"""
|
||||
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False)
|
||||
cursor = self.textCursor()
|
||||
if cursor.hasSelection():
|
||||
cursor.clearSelection()
|
||||
self.setTextCursor(cursor)
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction)
|
||||
self.setTextInteractionFlags(QtCore.Qt.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.GlobalColor.red)
|
||||
painter.setPen(QtCore.Qt.GlobalColor.red)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
|
||||
painter.setPen(QtCore.Qt.GlobalColor.black)
|
||||
painter.setPen(QtCore.Qt.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.GraphicsItemChange.ItemSelectedChange:
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if value == 0:
|
||||
self.item_unselected_signal.emit()
|
||||
return super().itemChange(change, value)
|
||||
|
||||
@@ -44,7 +44,7 @@ class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
|
||||
0,
|
||||
dst.x(),
|
||||
dst.y())
|
||||
pen = QtGui.QPen(QtCore.Qt.GlobalColor.black, 2, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.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.CursorShape.SizeHorCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
|
||||
elif event.pos().x() < self._border:
|
||||
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeHorCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
|
||||
else:
|
||||
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeAllCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
|
||||
# Vertical line
|
||||
else:
|
||||
if event.pos().y() > (self.line().y2() - self._border):
|
||||
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeVerCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
|
||||
elif event.pos().y() < self._border:
|
||||
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeVerCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
|
||||
else:
|
||||
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeAllCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.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.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "right"
|
||||
elif event.pos().x() < (self.line().x1() + self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "left"
|
||||
else:
|
||||
if event.pos().y() > (self.line().y2() - self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "top"
|
||||
elif event.pos().y() < (self.line().y1() + self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "bottom"
|
||||
super().mousePressEvent(event)
|
||||
|
||||
@@ -199,7 +199,7 @@ class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
|
||||
"""
|
||||
|
||||
self.update()
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.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.CursorShape.ArrowCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
@@ -21,7 +21,7 @@ Link items are graphical representation of a link on the QGraphicsScene
|
||||
"""
|
||||
|
||||
import math
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvgWidgets, qslot, sip_is_deleted
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot, sip_is_deleted
|
||||
|
||||
from ..packet_capture import PacketCapture
|
||||
from ..dialogs.filter_dialog import FilterDialog
|
||||
@@ -29,11 +29,11 @@ from ..dialogs.style_editor_dialog_link import StyleEditorDialogLink
|
||||
from ..utils.get_icon import get_icon
|
||||
|
||||
|
||||
class SvgIconItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
class SvgIconItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
def __init__(self, symbol, parent):
|
||||
|
||||
QtSvgWidgets.QGraphicsSvgItem.__init__(self, symbol, parent)
|
||||
QtSvg.QGraphicsSvgItem.__init__(self, symbol, parent)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
|
||||
@@ -56,7 +56,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
"""
|
||||
|
||||
_draw_port_labels = False
|
||||
delete_link_item_signal = QtCore.Signal(str)
|
||||
delete_link_item_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, source_item, source_port, destination_item, destination_port, link=None, adding_flag=False):
|
||||
|
||||
@@ -102,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(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
|
||||
self.setFlag(self.ItemIsFocusable)
|
||||
source_item.addLink(self)
|
||||
destination_item.addLink(self)
|
||||
self.setCustomToolTip()
|
||||
@@ -132,7 +132,7 @@ 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):
|
||||
@@ -142,7 +142,7 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
def _styleActionSlot(self, *args):
|
||||
style_dialog = StyleEditorDialogLink(self, self._main_window)
|
||||
style_dialog.show()
|
||||
style_dialog.exec()
|
||||
style_dialog.exec_()
|
||||
|
||||
def setLinkStyle(self, link_style):
|
||||
self._link._link_style["color"] = link_style["color"]
|
||||
@@ -159,6 +159,12 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
"""
|
||||
self._link.deleteLink()
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset this link
|
||||
"""
|
||||
self._link.resetLink()
|
||||
|
||||
def link(self):
|
||||
"""
|
||||
Returns the link attached to this link item.
|
||||
@@ -239,57 +245,63 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
if not self._link.capturing():
|
||||
# start capture
|
||||
start_capture_action = QtGui.QAction("Start capture", menu)
|
||||
start_capture_action = QtWidgets.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 = QtGui.QAction("Stop capture", menu)
|
||||
stop_capture_action = QtWidgets.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 = QtGui.QAction("Start Wireshark", menu)
|
||||
start_wireshark_action = QtWidgets.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 = QtGui.QAction("Analyze capture", menu)
|
||||
analyze_action = QtWidgets.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 = QtGui.QAction("Packet filters", menu)
|
||||
filter_action = QtWidgets.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 = QtGui.QAction("Suspend", menu)
|
||||
suspend_action = QtWidgets.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 = QtGui.QAction("Resume", menu)
|
||||
resume_action = QtWidgets.QAction("Resume", menu)
|
||||
resume_action.setIcon(get_icon('start.svg'))
|
||||
resume_action.triggered.connect(self._suspendActionSlot)
|
||||
menu.addAction(resume_action)
|
||||
|
||||
# reset
|
||||
reset_action = QtWidgets.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 = QtWidgets.QAction("Style", menu)
|
||||
style_action.setIcon(get_icon("node_conception.svg"))
|
||||
style_action.triggered.connect(self._styleActionSlot)
|
||||
menu.addAction(style_action)
|
||||
|
||||
# delete
|
||||
delete_action = QtGui.QAction("Delete", menu)
|
||||
delete_action = QtWidgets.QAction("Delete", menu)
|
||||
delete_action.setIcon(get_icon('delete.svg'))
|
||||
delete_action.triggered.connect(self._deleteActionSlot)
|
||||
menu.addAction(delete_action)
|
||||
@@ -302,10 +314,10 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
:param: QGraphicsSceneMouseEvent instance
|
||||
"""
|
||||
|
||||
if event.button() == QtCore.Qt.MouseButton.RightButton and self._adding_flag:
|
||||
if event.button() == QtCore.Qt.RightButton and self._adding_flag:
|
||||
# send a escape key to the main window to cancel the link addition
|
||||
from ..main_window import MainWindow
|
||||
key = QtGui.QKeyEvent(QtCore.QEvent.Type.KeyPress, QtCore.Qt.Key.Key_Escape, QtCore.Qt.KeyboardModifier.NoModifier)
|
||||
key = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Escape, QtCore.Qt.NoModifier)
|
||||
QtWidgets.QApplication.sendEvent(MainWindow.instance(), key)
|
||||
return
|
||||
else:
|
||||
@@ -322,9 +334,9 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
# create the contextual menu
|
||||
self.setHovered(True)
|
||||
self.setAcceptHoverEvents(False)
|
||||
menu = QtWidgets.QMenu(parent=self.scene().parent())
|
||||
menu = QtWidgets.QMenu()
|
||||
self.populateLinkContextualMenu(menu)
|
||||
menu.exec(QtGui.QCursor.pos())
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setHovered(False)
|
||||
|
||||
@@ -336,10 +348,18 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
"""
|
||||
|
||||
# On pressing backspace or delete key, the selected link gets deleted
|
||||
if event.key() == QtCore.Qt.Key.Key_Delete or event.key() == QtCore.Qt.Key.Key_Backspace:
|
||||
if event.key() == QtCore.Qt.Key_Delete or event.key() == QtCore.Qt.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
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
import urllib.parse
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvgWidgets
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from ..qt.qimage_svg_renderer import QImageSvgRenderer
|
||||
from ..controller import Controller
|
||||
|
||||
@@ -27,7 +27,7 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LogoItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
class LogoItem(QtSvg.QGraphicsSvgItem):
|
||||
"""
|
||||
Margin for the logo
|
||||
"""
|
||||
@@ -60,8 +60,8 @@ class LogoItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
# set graphical settings for this item
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
from ..main_window import MainWindow
|
||||
@@ -83,7 +83,7 @@ class LogoItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
self.setZValue(-2)
|
||||
|
||||
def eventFilter(self, source, event):
|
||||
if event.type() == QtCore.QEvent.Type.Paint:
|
||||
if event.type() == QtCore.QEvent.Paint:
|
||||
self.updatePosition()
|
||||
return QtWidgets.QWidget.eventFilter(self, source, event)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Graphical representation of a node on the QGraphicsScene.
|
||||
|
||||
from ..qt import sip
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvgWidgets, qslot
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, 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(QtSvgWidgets.QGraphicsSvgItem):
|
||||
class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
"""
|
||||
Node for the scene.
|
||||
@@ -60,7 +60,7 @@ class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
# node label
|
||||
self._node_label = None
|
||||
|
||||
self.setPos(QtCore.QPointF(self._node.x(), self._node.y()))
|
||||
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
|
||||
|
||||
# Temporary symbol during loading
|
||||
renderer = QImageSvgRenderer(":/icons/reload.svg")
|
||||
@@ -74,10 +74,10 @@ class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
self.graphicsEffect().setEnabled(False)
|
||||
|
||||
# set graphical settings for this node
|
||||
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.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
# update z value and locked state
|
||||
@@ -223,7 +223,7 @@ class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
:param base_node_id: base node identifier (integer)
|
||||
"""
|
||||
|
||||
self.setPos(QtCore.QPointF(self._node.x(), self._node.y()))
|
||||
self.setPos(QtCore.QPoint(self._node.x(), self._node.y()))
|
||||
self.setSymbol(self._node.symbol())
|
||||
self.update()
|
||||
|
||||
@@ -384,7 +384,7 @@ class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
self._node_label.setRotation(label_data.get("rotation", 0))
|
||||
|
||||
if self._node.locked():
|
||||
self._node_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self._node_label.setFlag(self.ItemIsMovable, False)
|
||||
|
||||
if label_data["x"] is None:
|
||||
self._centerLabel()
|
||||
@@ -402,7 +402,7 @@ class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
self._selected_port = None
|
||||
menu = QtWidgets.QMenu(parent=self.scene().parent())
|
||||
menu = QtWidgets.QMenu()
|
||||
ports = self._node.ports()
|
||||
if not ports:
|
||||
QtWidgets.QMessageBox.critical(self.scene().parent(), "Link", "No port available, please configure this device")
|
||||
@@ -443,7 +443,7 @@ class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
# add some delay before showing the menu
|
||||
# https://github.com/GNS3/gns3-gui/issues/3169
|
||||
QtCore.QThread.msleep(100)
|
||||
menu.exec(pos)
|
||||
menu.exec_(pos)
|
||||
return self._selected_port
|
||||
|
||||
def selectedPortSlot(self, action):
|
||||
@@ -470,7 +470,7 @@ class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
:param value: value of the change
|
||||
"""
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._main_window.uiSnapToGridAction.isChecked() \
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self._main_window.uiSnapToGridAction.isChecked() \
|
||||
and self._allow_snap_to_grid:
|
||||
grid_size = self._main_window.uiGraphicsView.nodeGridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
@@ -479,7 +479,7 @@ class NodeItem(QtSvgWidgets.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.GraphicsItemChange.ItemSelectedChange:
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if value:
|
||||
self.graphicsEffect().setEnabled(True)
|
||||
else:
|
||||
@@ -487,7 +487,7 @@ class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
self.updateNode()
|
||||
|
||||
# adjust link item positions when this node is moving or has changed.
|
||||
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange or change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange or change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
|
||||
for link in self._links:
|
||||
link.adjust()
|
||||
|
||||
@@ -504,16 +504,16 @@ class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
|
||||
# don't show the selection rectangle
|
||||
if not self._settings["draw_rectangle_selected_item"]:
|
||||
option.state = QtWidgets.QStyle.StateFlag.State_None
|
||||
option.state = QtWidgets.QStyle.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.GlobalColor.red)
|
||||
painter.setPen(QtCore.Qt.GlobalColor.red)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
|
||||
painter.setPen(QtCore.Qt.GlobalColor.black)
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
if self.show_layer:
|
||||
text = str(int(self.zValue())) # Z value
|
||||
elif self._last_error:
|
||||
@@ -540,7 +540,7 @@ class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
:param event: QKeyEvent
|
||||
"""
|
||||
|
||||
if event.modifiers() & QtCore.Qt.KeyboardModifier.AltModifier:
|
||||
if event.modifiers() & QtCore.Qt.AltModifier:
|
||||
self._allow_snap_to_grid = False
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
@@ -566,13 +566,13 @@ class NodeItem(QtSvgWidgets.QGraphicsSvgItem):
|
||||
"""
|
||||
|
||||
if locked is True:
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self.setFlag(self.ItemIsMovable, False)
|
||||
if self._node_label:
|
||||
self._node_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self._node_label.setFlag(self.ItemIsMovable, False)
|
||||
else:
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
|
||||
self.setFlag(self.ItemIsMovable, True)
|
||||
if self._node_label:
|
||||
self._node_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
|
||||
self._node_label.setFlag(self.ItemIsMovable, True)
|
||||
for link in self._links:
|
||||
link.adjust()
|
||||
self._locked = locked
|
||||
|
||||
@@ -52,14 +52,14 @@ class SerialLinkItem(LinkItem):
|
||||
|
||||
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))
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.red, self._link._link_style["width"] + 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
else:
|
||||
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], QtCore.Qt.PenStyle(self._link._link_style["type"]), QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
|
||||
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], self._link._link_style["type"], QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
except:
|
||||
if self._hovered:
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.red, self._pen_width + 1, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
|
||||
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.GlobalColor.darkRed, self._pen_width, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
|
||||
self.setPen(QtGui.QPen(QtCore.Qt.darkRed, self._pen_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
|
||||
|
||||
# get source to destination angle
|
||||
vector_angle = math.atan2(self.dy, self.dx)
|
||||
@@ -122,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.PenCapStyle.RoundCap
|
||||
color = QtCore.Qt.GlobalColor.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.yellow
|
||||
elif self._source_port.status() == Port.started:
|
||||
# port is active
|
||||
shape = QtCore.Qt.PenCapStyle.RoundCap
|
||||
color = QtCore.Qt.GlobalColor.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.green
|
||||
else:
|
||||
shape = QtCore.Qt.PenCapStyle.SquareCap
|
||||
color = QtCore.Qt.GlobalColor.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
color = QtCore.Qt.red
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.PenStyle.SolidLine, shape, QtCore.Qt.PenJoinStyle.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
|
||||
source_port_label = self._source_port.label()
|
||||
if source_port_label is None:
|
||||
@@ -142,28 +142,28 @@ class SerialLinkItem(LinkItem):
|
||||
self._source_port.setLabel(source_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
source_port_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, not self._source_item.locked())
|
||||
source_port_label.setFlag(source_port_label.ItemIsMovable, not self._source_item.locked())
|
||||
source_port_label.show()
|
||||
else:
|
||||
source_port_label.hide()
|
||||
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.PenStyle.NoPen:
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.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.GlobalColor.yellow
|
||||
shape = QtCore.Qt.PenCapStyle.RoundCap
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.GlobalColor.green
|
||||
shape = QtCore.Qt.PenCapStyle.RoundCap
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.GlobalColor.red
|
||||
shape = QtCore.Qt.PenCapStyle.SquareCap
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.PenStyle.SolidLine, shape, QtCore.Qt.PenJoinStyle.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
|
||||
destination_port_label = self._destination_port.label()
|
||||
|
||||
@@ -174,12 +174,12 @@ class SerialLinkItem(LinkItem):
|
||||
self._destination_port.setLabel(destination_port_label)
|
||||
|
||||
if self._draw_port_labels:
|
||||
destination_port_label.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, not self._destination_item.locked())
|
||||
destination_port_label.setFlag(destination_port_label.ItemIsMovable, not self._destination_item.locked())
|
||||
destination_port_label.show()
|
||||
else:
|
||||
destination_port_label.hide()
|
||||
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.PenStyle.NoPen:
|
||||
if self._settings["draw_link_status_points"] and self.pen().style() != QtCore.Qt.NoPen:
|
||||
painter.drawPoint(self.destination_point)
|
||||
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -44,7 +44,7 @@ class ShapeItem(DrawingItem):
|
||||
|
||||
if svg is None:
|
||||
self.setRect(0, 0, width, height)
|
||||
pen = QtGui.QPen(QtCore.Qt.GlobalColor.black, 2, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.setPen(pen)
|
||||
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255)) # default color is white and not transparent
|
||||
self.setBrush(brush)
|
||||
@@ -61,21 +61,21 @@ class ShapeItem(DrawingItem):
|
||||
"""
|
||||
|
||||
self.update()
|
||||
self._originally_movable = bool(self.flags() & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
|
||||
self._originally_movable = self.flags() & QtWidgets.QGraphicsItem.ItemIsMovable
|
||||
if event.pos().x() > (self.rect().right() - self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "right"
|
||||
|
||||
elif event.pos().x() < (self.rect().left() + self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "left"
|
||||
|
||||
elif event.pos().y() < (self.rect().top() + self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "top"
|
||||
|
||||
elif event.pos().y() > (self.rect().bottom() - self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "bottom"
|
||||
QtWidgets.QGraphicsItem.mousePressEvent(self, event)
|
||||
|
||||
@@ -87,7 +87,7 @@ class ShapeItem(DrawingItem):
|
||||
"""
|
||||
|
||||
self.update()
|
||||
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, self._originally_movable)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.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.CursorShape.SizeHorCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
|
||||
elif event.pos().x() < (self.rect().left() + self._border):
|
||||
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeHorCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
|
||||
elif event.pos().y() < (self.rect().top() + self._border):
|
||||
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeVerCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
|
||||
elif event.pos().y() > (self.rect().bottom() - self._border):
|
||||
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeVerCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
|
||||
else:
|
||||
self._graphics_view.setCursor(QtCore.Qt.CursorShape.SizeAllCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
|
||||
|
||||
def hoverLeaveEvent(self, event):
|
||||
"""
|
||||
@@ -169,7 +169,7 @@ class ShapeItem(DrawingItem):
|
||||
|
||||
# locked objects don't need cursors
|
||||
if not self.locked():
|
||||
self._graphics_view.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def setWidthAndHeight(self, width, height):
|
||||
self.setRect(0, 0, width, height)
|
||||
@@ -184,7 +184,7 @@ class ShapeItem(DrawingItem):
|
||||
self.setRect(0, 0, width, height)
|
||||
|
||||
pen = QtGui.QPen()
|
||||
brush = QtGui.QBrush(QtCore.Qt.BrushStyle.SolidPattern)
|
||||
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
|
||||
|
||||
if len(svg):
|
||||
pen = self._penFromSVGElement(svg[0])
|
||||
|
||||
@@ -62,11 +62,11 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
Edit mode for this note.
|
||||
"""
|
||||
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextEditorInteraction)
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
|
||||
self.setSelected(True)
|
||||
self.setFocus()
|
||||
cursor = self.textCursor()
|
||||
cursor.select(QtGui.QTextCursor.SelectionType.Document)
|
||||
cursor.select(QtGui.QTextCursor.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.GraphicsItemFlag.ItemIsFocusable, False)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False)
|
||||
cursor = self.textCursor()
|
||||
if cursor.hasSelection():
|
||||
cursor.clearSelection()
|
||||
self.setTextCursor(cursor)
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction)
|
||||
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
|
||||
if not self.toPlainText():
|
||||
# delete the note if empty
|
||||
self.delete()
|
||||
@@ -132,7 +132,7 @@ class TextItem(QtWidgets.QGraphicsTextItem, DrawingItem):
|
||||
text.set("text-decoration", "underline")
|
||||
text.set("fill", "#" + hex(self.defaultTextColor().rgba())[4:])
|
||||
text.set("fill-opacity", str(self.defaultTextColor().alphaF()))
|
||||
text.text = self.toPlainText() or " "
|
||||
text.text = self.toPlainText()
|
||||
|
||||
svg = ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
return svg
|
||||
|
||||
58
gns3/link.py
@@ -84,21 +84,14 @@ class Link(QtCore.QObject):
|
||||
self._initialized = False
|
||||
self._filters = {}
|
||||
self._suspend = False
|
||||
self._nodes = []
|
||||
|
||||
# Boolean if True we are creating the first instance of this node
|
||||
# if false the node already exist in the topology
|
||||
# use to avoid erasing information when reloading
|
||||
self._creator = False
|
||||
|
||||
# Add the default link style from the topology view settings
|
||||
from .main_window import MainWindow
|
||||
topology_view_settings = MainWindow.instance().uiGraphicsView.settings()
|
||||
self._link_style = {
|
||||
"color": topology_view_settings.get("default_link_color", "#000000"),
|
||||
"width": topology_view_settings.get("default_link_width", 2),
|
||||
"type": topology_view_settings.get("default_link_type", 1)
|
||||
}
|
||||
self._nodes = []
|
||||
self._link_style = {}
|
||||
|
||||
body = self._prepareParams()
|
||||
if self._link_id:
|
||||
@@ -119,21 +112,23 @@ 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.QIODeviceBase.OpenModeFlag.WriteOnly)
|
||||
self._capture_file.open(QtCore.QFile.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.QIODeviceBase.OpenModeFlag.WriteOnly)
|
||||
self._capture_file.open(QtCore.QFile.WriteOnly)
|
||||
|
||||
if self._network_manager is None:
|
||||
self._network_manager = QtNetwork.QNetworkAccessManager(self)
|
||||
self._response_stream = Controller.instance().get("/projects/{project_id}/links/{link_id}/pcap".format(project_id=self.project().id(), link_id=self._link_id),
|
||||
None,
|
||||
showProgress=False,
|
||||
downloadProgressCallback=self._downloadPcapProgress,
|
||||
ignoreErrors=True, # If something is wrong avoid disconnect us from server
|
||||
timeout=None,
|
||||
networkManager=self._network_manager)
|
||||
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
|
||||
@@ -359,12 +354,35 @@ 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)
|
||||
|
||||
@@ -395,7 +413,7 @@ 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)
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 832 B After Width: | Height: | Size: 832 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -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,22 +91,17 @@ class LocalConfig(QtCore.QObject):
|
||||
if sys.platform.startswith("win"):
|
||||
old_config_path = os.path.join(os.path.expandvars("%APPDATA%"), "GNS3", filename)
|
||||
else:
|
||||
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)
|
||||
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:
|
||||
@@ -148,13 +141,8 @@ class LocalConfig(QtCore.QObject):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
path = os.path.join(appdata, "GNS3", version)
|
||||
else:
|
||||
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)
|
||||
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)
|
||||
@@ -200,7 +188,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"):
|
||||
@@ -404,6 +392,14 @@ 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
|
||||
@@ -418,20 +414,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
|
||||
|
||||
@@ -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, DEFAULT_LOCAL_SERVER_HOST
|
||||
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.Signal(str, bool)
|
||||
finished = QtCore.Signal()
|
||||
updated = QtCore.Signal(int)
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
finished = QtCore.pyqtSignal()
|
||||
updated = QtCore.pyqtSignal(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.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No)
|
||||
if proceed == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.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,9 +159,9 @@ 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.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No)
|
||||
if proceed == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
from gns3.utils.macos_ubridge_setuid import macos_ubridge_setuid
|
||||
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
|
||||
macos_ubridge_setuid()
|
||||
@@ -203,12 +172,6 @@ class LocalServer(QtCore.QObject):
|
||||
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.
|
||||
@@ -216,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:
|
||||
@@ -252,17 +210,17 @@ class LocalServer(QtCore.QObject):
|
||||
"""
|
||||
|
||||
if "host" in new_settings and new_settings["host"] is None:
|
||||
new_settings["host"] = DEFAULT_LOCAL_SERVER_HOST
|
||||
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')
|
||||
@@ -278,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):
|
||||
"""
|
||||
@@ -325,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
|
||||
@@ -358,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):
|
||||
@@ -375,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()
|
||||
@@ -476,7 +415,7 @@ 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:
|
||||
@@ -529,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):
|
||||
"""
|
||||
@@ -552,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
|
||||
@@ -581,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.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No)
|
||||
"Local controller",
|
||||
"The local controller cannot be stopped, would you like to kill it?",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
||||
if proceed == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
self._local_server_process.kill()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -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
|
||||
25
gns3/main.py
@@ -36,6 +36,7 @@ import time
|
||||
import locale
|
||||
import argparse
|
||||
import signal
|
||||
import psutil
|
||||
|
||||
try:
|
||||
from gns3.qt import QtCore, QtWidgets
|
||||
@@ -48,6 +49,7 @@ 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__
|
||||
|
||||
@@ -118,9 +120,14 @@ 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)
|
||||
|
||||
@@ -143,7 +150,6 @@ def main():
|
||||
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", "")
|
||||
@@ -187,8 +193,11 @@ def main():
|
||||
if sys.version_info < (3, 9):
|
||||
raise SystemExit("Python 3.9 or higher is required")
|
||||
|
||||
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(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(psutil.__version__) < parse_version("2.2.1"):
|
||||
raise SystemExit("Requirement is psutil version 2.2.1 or higher, got version {}".format(psutil.__version__))
|
||||
|
||||
# check for the correct locale
|
||||
# (UNIX/Linux only)
|
||||
@@ -202,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.Format.IniFormat)
|
||||
QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat)
|
||||
|
||||
if sys.platform.startswith('win') and hasattr(sys, "frozen"):
|
||||
try:
|
||||
@@ -230,12 +239,12 @@ def main():
|
||||
local_config = LocalConfig.instance()
|
||||
|
||||
global app
|
||||
app = Application(sys.argv)
|
||||
app = Application(sys.argv, hdpi=local_config.hdpi())
|
||||
|
||||
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)
|
||||
@@ -273,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
|
||||
@@ -296,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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -69,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.Signal(bool)
|
||||
adding_link_signal = QtCore.pyqtSignal(bool)
|
||||
|
||||
# Signal of settings updates
|
||||
settings_updated_signal = QtCore.Signal()
|
||||
@@ -85,7 +86,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.setupUi(self)
|
||||
self.setUnifiedTitleAndToolBarOnMac(True)
|
||||
|
||||
# These widgets will be disabled when no project is loaded
|
||||
# This widgets will be disabled when you have no project loaded
|
||||
self.disableWhenNoProjectWidgets = [
|
||||
self.uiGraphicsView,
|
||||
self.uiAnnotateMenu,
|
||||
@@ -99,9 +100,27 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiEditProjectAction,
|
||||
self.uiDeleteProjectAction,
|
||||
self.uiImportExportConfigsAction,
|
||||
self.uiLockAllAction
|
||||
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
|
||||
logging.getLogger().addHandler(NotifDialogHandler(self._notif_dialog))
|
||||
@@ -116,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()
|
||||
@@ -154,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.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._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._project_dir = None
|
||||
|
||||
# add recent file actions to the File menu
|
||||
for i in range(0, self._maxrecent_files):
|
||||
action = QtGui.QAction(self.uiFileMenu)
|
||||
action = QtWidgets.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 = QtGui.QAction(self.uiFileMenu)
|
||||
action = QtWidgets.QAction(self.uiFileMenu)
|
||||
action.setVisible(False)
|
||||
action.triggered.connect(self.openRecentProjectSlot)
|
||||
self.recent_project_actions.append(action)
|
||||
@@ -194,12 +211,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
self.setWindowTitle("[*] GNS3")
|
||||
|
||||
# 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?")
|
||||
|
||||
# load initial stuff once the event loop isn't busy
|
||||
self.run_later(0, self.startupLoading)
|
||||
|
||||
@@ -213,6 +224,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)
|
||||
@@ -237,6 +249,7 @@ 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)
|
||||
@@ -321,11 +334,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
style = new_settings.get("style")
|
||||
if style and new_settings["style"] != self._settings["style"]:
|
||||
self._setStyle(style)
|
||||
QtWidgets.QMessageBox.information(
|
||||
self,
|
||||
"Interface style",
|
||||
"Please restart the application to fully apply the {} style.".format(style)
|
||||
)
|
||||
|
||||
self._settings.update(new_settings)
|
||||
# save the settings
|
||||
@@ -335,7 +343,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
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):
|
||||
@@ -413,7 +421,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())
|
||||
@@ -427,7 +435,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):
|
||||
@@ -439,7 +456,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)
|
||||
@@ -458,7 +475,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)
|
||||
@@ -522,10 +539,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)
|
||||
@@ -565,6 +582,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Refresh widgets that should be visible or not
|
||||
"""
|
||||
|
||||
for widget in self.disableWhenControllerNotConnectedWidgets:
|
||||
widget.setEnabled(Controller.instance().connected())
|
||||
|
||||
# No projects
|
||||
if Topology.instance().project() is None:
|
||||
for widget in self.disableWhenNoProjectWidgets:
|
||||
@@ -599,7 +619,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.Option.ShowDirsOnly)
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self, "Export directory", self._export_configs_to_dir, QtWidgets.QFileDialog.ShowDirsOnly)
|
||||
if path:
|
||||
self._export_configs_to_dir = os.path.dirname(path)
|
||||
for module in MODULES:
|
||||
@@ -612,7 +632,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.Option.ShowDirsOnly)
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self, "Import directory", self._import_configs_from_dir, QtWidgets.QFileDialog.ShowDirsOnly)
|
||||
if path:
|
||||
self._import_configs_from_dir = os.path.dirname(path)
|
||||
for module in MODULES:
|
||||
@@ -630,12 +650,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.Format_RGB32)
|
||||
image.fill(QtCore.Qt.GlobalColor.white)
|
||||
image = QtGui.QImage(source.size().toSize(), QtGui.QImage.Format_RGB32)
|
||||
image.fill(QtCore.Qt.white)
|
||||
painter = QtGui.QPainter(image)
|
||||
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True)
|
||||
painter.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing, True)
|
||||
painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
|
||||
painter.setRenderHint(QtGui.QPainter.TextAntialiasing, True)
|
||||
painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True)
|
||||
scene.render(painter, source=source)
|
||||
painter.end()
|
||||
# TODO: quality option
|
||||
@@ -725,7 +745,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
dialog = SnapshotsDialog(self, project)
|
||||
dialog.show()
|
||||
dialog.exec()
|
||||
dialog.exec_()
|
||||
|
||||
def _selectAllActionSlot(self):
|
||||
"""
|
||||
@@ -750,12 +770,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Slot to switch to full screen.
|
||||
"""
|
||||
|
||||
if not self.windowState() & QtCore.Qt.WindowState.WindowFullScreen:
|
||||
if not self.windowState() & QtCore.Qt.WindowFullScreen:
|
||||
# switch to full screen
|
||||
self.setWindowState(self.windowState() | QtCore.Qt.WindowState.WindowFullScreen)
|
||||
self.setWindowState(self.windowState() | QtCore.Qt.WindowFullScreen)
|
||||
else:
|
||||
# switch back to normal
|
||||
self.setWindowState(self.windowState() & ~QtCore.Qt.WindowState.WindowFullScreen)
|
||||
self.setWindowState(self.windowState() & ~QtCore.Qt.WindowFullScreen)
|
||||
|
||||
def _zoomInActionSlot(self):
|
||||
"""
|
||||
@@ -791,7 +811,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.AspectRatioMode.KeepAspectRatio)
|
||||
view.fitInView(bounding_rect, QtCore.Qt.KeepAspectRatio)
|
||||
|
||||
def _showLayersActionSlot(self):
|
||||
"""
|
||||
@@ -834,9 +854,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
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)
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
project = Topology.instance().project()
|
||||
@@ -849,9 +869,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
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)
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
project = Topology.instance().project()
|
||||
@@ -864,9 +884,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
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)
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
project = Topology.instance().project()
|
||||
@@ -879,9 +899,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
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)
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
|
||||
project = Topology.instance().project()
|
||||
@@ -931,7 +951,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:
|
||||
@@ -984,17 +1004,14 @@ 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):
|
||||
for action in self.findChildren(QtWidgets.QAction):
|
||||
shortcut = action.shortcut().toString()
|
||||
if shortcut:
|
||||
shortcuts_text += f"{action.toolTip()}: {shortcut}\n"
|
||||
@@ -1014,7 +1031,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
dialog = AboutDialog(self)
|
||||
dialog.show()
|
||||
dialog.exec()
|
||||
dialog.exec_()
|
||||
|
||||
def _exportDebugInformationSlot(self):
|
||||
"""
|
||||
@@ -1023,7 +1040,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
dialog = ExportDebugDialog(self, Topology.instance().project())
|
||||
dialog.show()
|
||||
dialog.exec()
|
||||
dialog.exec_()
|
||||
|
||||
def _doctorSlot(self):
|
||||
"""
|
||||
@@ -1032,7 +1049,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
dialog = DoctorDialog(self)
|
||||
dialog.show()
|
||||
dialog.exec()
|
||||
dialog.exec_()
|
||||
|
||||
def _academyActionSlot(self):
|
||||
"""
|
||||
@@ -1122,7 +1139,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
dialog = PreferencesDialog(self)
|
||||
#dialog.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["preferences_dialog_geometry"].encode()))
|
||||
dialog.show()
|
||||
dialog.exec()
|
||||
dialog.exec_()
|
||||
#self._settings["preferences_dialog_geometry"] = bytes(dialog.saveGeometry().toBase64()).decode()
|
||||
#self.setSettings(self._settings)
|
||||
|
||||
@@ -1132,6 +1149,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Topology.instance().editReadme()
|
||||
|
||||
def _showReadmeActionSlot(self):
|
||||
"""
|
||||
Slot to show the README file
|
||||
"""
|
||||
Topology.instance().showReadme()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._notif_dialog.resize()
|
||||
super().resizeEvent(event)
|
||||
@@ -1145,10 +1168,10 @@ 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.Key_Escape:
|
||||
if self.uiAddLinkAction.isChecked() and key == QtCore.Qt.Key_Escape:
|
||||
self.uiAddLinkAction.setChecked(False)
|
||||
self._addLinkActionSlot()
|
||||
elif key == QtCore.Qt.Key.Key_C and (event.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier):
|
||||
elif key == QtCore.Qt.Key_C and event.modifiers() & QtCore.Qt.ControlModifier:
|
||||
status_bar_message = self.uiStatusBar.currentMessage()
|
||||
if status_bar_message:
|
||||
QtWidgets.QApplication.clipboard().setText(status_bar_message)
|
||||
@@ -1164,8 +1187,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.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
event.ignore()
|
||||
return
|
||||
|
||||
@@ -1227,8 +1250,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.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.No:
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
@@ -1272,13 +1295,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
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)
|
||||
@@ -1431,9 +1452,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
|
||||
@@ -1446,21 +1475,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)
|
||||
@@ -1471,7 +1500,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:
|
||||
@@ -1480,8 +1509,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.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
Topology.instance().deleteProject()
|
||||
|
||||
def _setStyle(self, style_name):
|
||||
@@ -1496,8 +1525,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
style.setCharcoalStyle()
|
||||
elif style_name == "Classic":
|
||||
style.setClassicStyle()
|
||||
elif style_name == "Dark":
|
||||
style.setDarkStyle()
|
||||
else:
|
||||
style.setLegacyStyle()
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -37,7 +37,7 @@ class CloudWizard(VMWizard, Ui_CloudNodeWizard):
|
||||
|
||||
super().__init__(cloud_nodes, parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(":/symbols/cloud.svg"))
|
||||
self.setPixmap(QtWidgets.QWizard.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
|
||||
|
||||
@@ -38,7 +38,7 @@ class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
|
||||
|
||||
super().__init__(ethernet_hubs, parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(":/symbols/hub.svg"))
|
||||
self.setPixmap(QtWidgets.QWizard.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}
|
||||
|
||||
@@ -38,7 +38,7 @@ class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
|
||||
|
||||
super().__init__(ethernet_switches, parent)
|
||||
|
||||
self.setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(":/symbols/ethernet_switch.svg"))
|
||||
self.setPixmap(QtWidgets.QWizard.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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -49,7 +49,7 @@ class EthernetSwitch(Node):
|
||||
|
||||
info = """Ethernet 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}
|
||||
Console is on port {console} and type is {console_type}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
|
||||
@@ -50,7 +50,7 @@ class FrameRelaySwitch(Node):
|
||||
|
||||
info = """Frame relay 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 Frame relay switch
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
|
||||
@@ -43,7 +43,7 @@ class ATMSwitchConfigurationPage(QtWidgets.QWidget, Ui_atmSwitchConfigPageWidget
|
||||
self.uiMappingTreeWidget.itemSelectionChanged.connect(self._mappingSelectionChangedSlot)
|
||||
|
||||
# enable sorting
|
||||
self.uiMappingTreeWidget.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
self.uiMappingTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiMappingTreeWidget.setSortingEnabled(True)
|
||||
|
||||
def _mappingSelectedSlot(self, item, column):
|
||||
@@ -61,11 +61,11 @@ class ATMSwitchConfigurationPage(QtWidgets.QWidget, Ui_atmSwitchConfigPageWidget
|
||||
match_destination_mapping = mapping.search(destination)
|
||||
|
||||
if match_source_mapping and match_destination_mapping:
|
||||
self.uiVPICheckBox.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
self.uiVPICheckBox.setCheckState(QtCore.Qt.Unchecked)
|
||||
(source_port, source_vpi, source_vci) = match_source_mapping.group(1, 2, 3)
|
||||
(destination_port, destination_vpi, destination_vci) = match_destination_mapping.group(1, 2, 3)
|
||||
else:
|
||||
self.uiVPICheckBox.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
self.uiVPICheckBox.setCheckState(QtCore.Qt.Checked)
|
||||
(source_port, source_vpi) = source.split(':')
|
||||
(destination_port, destination_vpi) = destination.split(':')
|
||||
source_vci = destination_vci = 0
|
||||
@@ -103,7 +103,7 @@ class ATMSwitchConfigurationPage(QtWidgets.QWidget, Ui_atmSwitchConfigPageWidget
|
||||
destination_vpi = self.uiDestinationVPISpinBox.value()
|
||||
destination_vci = self.uiDestinationVCISpinBox.value()
|
||||
|
||||
if self.uiVPICheckBox.checkState() == QtCore.Qt.CheckState.Unchecked:
|
||||
if self.uiVPICheckBox.checkState() == QtCore.Qt.Unchecked:
|
||||
source = "{port}:{vpi}:{vci}".format(port=source_port,
|
||||
vpi=source_vpi,
|
||||
vci=source_vci)
|
||||
|
||||
@@ -364,7 +364,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
if interface["type"] == "ethernet":
|
||||
if not state and interface["special"]:
|
||||
continue
|
||||
if self.uiEthernetListWidget.findItems(interface["name"], QtCore.Qt.MatchFlag.MatchFixedString):
|
||||
if self.uiEthernetListWidget.findItems(interface["name"], QtCore.Qt.MatchFixedString):
|
||||
continue
|
||||
self.uiEthernetComboBox.addItem(interface["name"])
|
||||
index += 1
|
||||
@@ -377,7 +377,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
|
||||
dialog.show()
|
||||
if dialog.exec():
|
||||
if dialog.exec_():
|
||||
new_symbol_path = dialog.getSymbol()
|
||||
self.uiSymbolLineEdit.setText(new_symbol_path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
|
||||
@@ -473,9 +473,13 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
if index != -1:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
|
||||
Controller.instance().getCompute("/network/interfaces", settings["compute_id"],
|
||||
self._getInterfacesFromServerCallback,
|
||||
progressText="Retrieving network interfaces...")
|
||||
Controller.instance().getCompute(
|
||||
"/network/interfaces",
|
||||
settings["compute_id"],
|
||||
self._getInterfacesFromServerCallback,
|
||||
progress_text="Retrieving network interfaces...",
|
||||
wait=True
|
||||
)
|
||||
|
||||
else:
|
||||
self.uiDefaultNameFormatLabel.hide()
|
||||
@@ -490,6 +494,7 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
self._interfaces = self._node.interfaces()
|
||||
self._loadNetworkInterfaces(self._interfaces)
|
||||
|
||||
self.uiUsageTextEdit.setPlainText(settings["usage"])
|
||||
# load the current ports
|
||||
self._ports = []
|
||||
self.uiEthernetListWidget.clear()
|
||||
@@ -560,4 +565,6 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
|
||||
settings["ports_mapping"] = self._ports
|
||||
else:
|
||||
settings["ports_mapping"] = self._ports
|
||||
|
||||
settings["usage"] = self.uiUsageTextEdit.toPlainText()
|
||||
return settings
|
||||
|
||||
@@ -87,7 +87,15 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Console type:", cloud_node["remote_console_type"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", cloud_node["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["compute_id"]).name()])
|
||||
compute_id = cloud_node.get("compute_id")
|
||||
if compute_id:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Compute:", ComputeManager.instance().getCompute(compute_id).name()])
|
||||
else:
|
||||
if Controller.instance().settings()["dynamic_compute_allocation"]:
|
||||
msg = "Dynamically allocated by the controller"
|
||||
else:
|
||||
msg = "Manually chosen"
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Compute:", msg])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -107,7 +115,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
self.uiEditCloudNodePushButton.setEnabled(single_selected)
|
||||
|
||||
if single_selected:
|
||||
key = selection[0].data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
key = selection[0].data(0, QtCore.Qt.UserRole)
|
||||
cloud_node = self._cloud_nodes[key]
|
||||
self._refreshInfo(cloud_node)
|
||||
else:
|
||||
@@ -120,7 +128,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
|
||||
wizard = CloudWizard(self._cloud_nodes, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec():
|
||||
if wizard.exec_():
|
||||
new_cloud_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_cloud_settings["compute_id"], name=new_cloud_settings["name"])
|
||||
self._cloud_nodes[key] = CLOUD_SETTINGS.copy()
|
||||
@@ -130,7 +138,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
item.setText(0, self._cloud_nodes[key]["name"])
|
||||
Controller.instance().getSymbolIcon(self._cloud_nodes[key]["symbol"], qpartial(self._setItemIcon, item))
|
||||
|
||||
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, key)
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
self.uiCloudNodesTreeWidget.setCurrentItem(item)
|
||||
|
||||
@@ -141,11 +149,11 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
|
||||
item = self.uiCloudNodesTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
cloud_node = self._cloud_nodes[key]
|
||||
dialog = ConfigurationDialog(cloud_node["name"], cloud_node, CloudConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec():
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(cloud_node["symbol"], qpartial(self._setItemIcon, item))
|
||||
if cloud_node["name"] != item.text(0):
|
||||
@@ -158,7 +166,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
self._cloud_nodes[new_key] = self._cloud_nodes[key]
|
||||
del self._cloud_nodes[key]
|
||||
item.setText(0, cloud_node["name"])
|
||||
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, new_key)
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(cloud_node)
|
||||
|
||||
def _deleteCloudNodeSlot(self):
|
||||
@@ -168,7 +176,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
|
||||
for item in self.uiCloudNodesTreeWidget.selectedItems():
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
del self._cloud_nodes[key]
|
||||
self.uiCloudNodesTreeWidget.takeTopLevelItem(self.uiCloudNodesTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
@@ -192,12 +200,12 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiCloudNodesTreeWidget)
|
||||
item.setText(0, cloud_node["name"])
|
||||
Controller.instance().getSymbolIcon(cloud_node["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, key)
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
|
||||
if self._items:
|
||||
self.uiCloudNodesTreeWidget.setCurrentItem(self._items[0])
|
||||
self.uiCloudNodesTreeWidget.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
self.uiCloudNodesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiCloudNodesTreeWidget.setMaximumWidth(self.uiCloudNodesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
|
||||
@@ -52,7 +52,7 @@ class EthernetHubConfigurationPage(QtWidgets.QWidget, Ui_ethernetHubConfigPageWi
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
|
||||
dialog.show()
|
||||
if dialog.exec():
|
||||
if dialog.exec_():
|
||||
new_symbol_path = dialog.getSymbol()
|
||||
self.uiSymbolLineEdit.setText(new_symbol_path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
|
||||
|
||||
@@ -83,7 +83,15 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template ID:", ethernet_hub.get("template_id", "none")])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_hub["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["compute_id"]).name()])
|
||||
compute_id = ethernet_hub.get("compute_id")
|
||||
if compute_id:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Compute:", ComputeManager.instance().getCompute(compute_id).name()])
|
||||
else:
|
||||
if Controller.instance().settings()["dynamic_compute_allocation"]:
|
||||
msg = "Dynamically allocated by the controller"
|
||||
else:
|
||||
msg = "Manually chosen"
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Compute:", msg])
|
||||
except KeyError:
|
||||
pass
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Number of ports:", str(len(ethernet_hub["ports_mapping"]))])
|
||||
@@ -104,7 +112,7 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
self.uiEditEthernetHubPushButton.setEnabled(single_selected)
|
||||
|
||||
if single_selected:
|
||||
key = selection[0].data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
key = selection[0].data(0, QtCore.Qt.UserRole)
|
||||
ethernet_hub = self._ethernet_hubs[key]
|
||||
self._refreshInfo(ethernet_hub)
|
||||
else:
|
||||
@@ -117,7 +125,7 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
|
||||
wizard = EthernetHubWizard(self._ethernet_hubs, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec():
|
||||
if wizard.exec_():
|
||||
new_ethernet_hub_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_ethernet_hub_settings["compute_id"], name=new_ethernet_hub_settings["name"])
|
||||
self._ethernet_hubs[key] = ETHERNET_HUB_SETTINGS.copy()
|
||||
@@ -126,7 +134,7 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubsTreeWidget)
|
||||
item.setText(0, self._ethernet_hubs[key]["name"])
|
||||
Controller.instance().getSymbolIcon(self._ethernet_hubs[key]["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, key)
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
self.uiEthernetHubsTreeWidget.setCurrentItem(item)
|
||||
|
||||
@@ -137,11 +145,11 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
|
||||
item = self.uiEthernetHubsTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
ethernet_hub = self._ethernet_hubs[key]
|
||||
dialog = ConfigurationDialog(ethernet_hub["name"], ethernet_hub, EthernetHubConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec():
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(ethernet_hub["symbol"], qpartial(self._setItemIcon, item))
|
||||
if ethernet_hub["name"] != item.text(0):
|
||||
@@ -154,7 +162,7 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
self._ethernet_hubs[new_key] = self._ethernet_hubs[key]
|
||||
del self._ethernet_hubs[key]
|
||||
item.setText(0, ethernet_hub["name"])
|
||||
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, new_key)
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(ethernet_hub)
|
||||
|
||||
def _deleteEthernetHubSlot(self):
|
||||
@@ -164,7 +172,7 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
|
||||
for item in self.uiEthernetHubsTreeWidget.selectedItems():
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
del self._ethernet_hubs[key]
|
||||
self.uiEthernetHubsTreeWidget.takeTopLevelItem(self.uiEthernetHubsTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
@@ -188,12 +196,12 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetHubsTreeWidget)
|
||||
item.setText(0, ethernet_hub["name"])
|
||||
Controller.instance().getSymbolIcon(ethernet_hub["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, key)
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
|
||||
if self._items:
|
||||
self.uiEthernetHubsTreeWidget.setCurrentItem(self._items[0])
|
||||
self.uiEthernetHubsTreeWidget.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
self.uiEthernetHubsTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiEthernetHubsTreeWidget.setMaximumWidth(self.uiEthernetHubsTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
|
||||
@@ -51,7 +51,7 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
self.uiPortTypeComboBox.currentIndexChanged.connect(self._typeSelectionChangedSlot)
|
||||
|
||||
# enable sorting
|
||||
self.uiPortsTreeWidget.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
self.uiPortsTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiPortsTreeWidget.setSortingEnabled(True)
|
||||
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
@@ -64,7 +64,7 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
symbol_path = self.uiSymbolLineEdit.text()
|
||||
dialog = SymbolSelectionDialog(self, symbol=symbol_path)
|
||||
dialog.show()
|
||||
if dialog.exec():
|
||||
if dialog.exec_():
|
||||
new_symbol_path = dialog.getSymbol()
|
||||
self.uiSymbolLineEdit.setText(new_symbol_path)
|
||||
self.uiSymbolLineEdit.setToolTip('<img src="{}"/>'.format(new_symbol_path))
|
||||
@@ -127,14 +127,15 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
if port_type == "qinq":
|
||||
port_ethertype = self.uiPortEtherTypeComboBox.currentText()
|
||||
else:
|
||||
port_ethertype = ""
|
||||
port_ethertype = "0x8100"
|
||||
|
||||
if port in self._ports:
|
||||
# update a given entry in the tree widget
|
||||
item = self.uiPortsTreeWidget.findItems(str(port), QtCore.Qt.MatchFlag.MatchFixedString)[0]
|
||||
item = self.uiPortsTreeWidget.findItems(str(port), QtCore.Qt.MatchFixedString)[0]
|
||||
item.setText(1, str(vlan))
|
||||
item.setText(2, port_type)
|
||||
item.setText(3, port_ethertype)
|
||||
if port_ethertype:
|
||||
item.setText(3, port_ethertype)
|
||||
|
||||
else:
|
||||
# add a new entry in the tree widget
|
||||
@@ -142,7 +143,8 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
item.setText(0, str(port))
|
||||
item.setText(1, str(vlan))
|
||||
item.setText(2, port_type)
|
||||
item.setText(3, port_ethertype)
|
||||
if port_ethertype:
|
||||
item.setText(3, port_ethertype)
|
||||
self.uiPortsTreeWidget.addTopLevelItem(item)
|
||||
|
||||
self._ports[port] = {"name": "Ethernet{}".format(port),
|
||||
@@ -224,7 +226,9 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
|
||||
item.setText(0, str(port_info["port_number"]))
|
||||
item.setText(1, str(port_info.get("vlan", 1)))
|
||||
item.setText(2, port_info.get("type", "access"))
|
||||
item.setText(3, port_info.get("ethertype", ""))
|
||||
port_ethertype = port_info.get("ethertype")
|
||||
if port_ethertype:
|
||||
item.setText(3, port_ethertype)
|
||||
self.uiPortsTreeWidget.addTopLevelItem(item)
|
||||
self._ports[port_info["port_number"]] = port_info
|
||||
|
||||
|
||||
@@ -82,7 +82,15 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template ID:", ethernet_switch.get("template_id", "none")])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ethernet_switch["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["compute_id"]).name()])
|
||||
compute_id = ethernet_switch.get("compute_id")
|
||||
if compute_id:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Compute:", ComputeManager.instance().getCompute(compute_id).name()])
|
||||
else:
|
||||
if Controller.instance().settings()["dynamic_compute_allocation"]:
|
||||
msg = "Dynamically allocated by the controller"
|
||||
else:
|
||||
msg = "Manually chosen"
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Compute:", msg])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -109,7 +117,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
self.uiEditEthernetSwitchPushButton.setEnabled(single_selected)
|
||||
|
||||
if single_selected:
|
||||
key = selection[0].data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
key = selection[0].data(0, QtCore.Qt.UserRole)
|
||||
ethernet_switch = self._ethernet_switches[key]
|
||||
self._refreshInfo(ethernet_switch)
|
||||
else:
|
||||
@@ -122,7 +130,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
|
||||
wizard = EthernetSwitchWizard(self._ethernet_switches, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec():
|
||||
if wizard.exec_():
|
||||
new_ethernet_switch_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=new_ethernet_switch_settings["compute_id"], name=new_ethernet_switch_settings["name"])
|
||||
self._ethernet_switches[key] = ETHERNET_SWITCH_SETTINGS.copy()
|
||||
@@ -132,7 +140,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
item.setText(0, self._ethernet_switches[key]["name"])
|
||||
Controller.instance().getSymbolIcon(self._ethernet_switches[key]["symbol"], qpartial(self._setItemIcon, item))
|
||||
|
||||
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, key)
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
self.uiEthernetSwitchesTreeWidget.setCurrentItem(item)
|
||||
|
||||
@@ -143,11 +151,11 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
|
||||
item = self.uiEthernetSwitchesTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
ethernet_switch = self._ethernet_switches[key]
|
||||
dialog = ConfigurationDialog(ethernet_switch["name"], ethernet_switch, EthernetSwitchConfigurationPage(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec():
|
||||
if dialog.exec_():
|
||||
# update the icon
|
||||
Controller.instance().getSymbolIcon(ethernet_switch["symbol"], qpartial(self._setItemIcon, item))
|
||||
if ethernet_switch["name"] != item.text(0):
|
||||
@@ -160,7 +168,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
self._ethernet_switches[new_key] = self._ethernet_switches[key]
|
||||
del self._ethernet_switches[key]
|
||||
item.setText(0, ethernet_switch["name"])
|
||||
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, new_key)
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(ethernet_switch)
|
||||
|
||||
def _deleteEthernetSwitchSlot(self):
|
||||
@@ -169,7 +177,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
"""
|
||||
for item in self.uiEthernetSwitchesTreeWidget.selectedItems():
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
del self._ethernet_switches[key]
|
||||
self.uiEthernetSwitchesTreeWidget.takeTopLevelItem(self.uiEthernetSwitchesTreeWidget.indexOfTopLevelItem(item))
|
||||
|
||||
@@ -193,12 +201,12 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
item = QtWidgets.QTreeWidgetItem(self.uiEthernetSwitchesTreeWidget)
|
||||
item.setText(0, ethernet_switch["name"])
|
||||
Controller.instance().getSymbolIcon(ethernet_switch["symbol"], qpartial(self._setItemIcon, item))
|
||||
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, key)
|
||||
item.setData(0, QtCore.Qt.UserRole, key)
|
||||
self._items.append(item)
|
||||
|
||||
if self._items:
|
||||
self.uiEthernetSwitchesTreeWidget.setCurrentItem(self._items[0])
|
||||
self.uiEthernetSwitchesTreeWidget.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
self.uiEthernetSwitchesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiEthernetSwitchesTreeWidget.setMaximumWidth(self.uiEthernetSwitchesTreeWidget.sizeHintForColumn(0) + 20)
|
||||
|
||||
def _setItemIcon(self, item, icon):
|
||||
|
||||
@@ -42,7 +42,7 @@ class FrameRelaySwitchConfigurationPage(QtWidgets.QWidget, Ui_frameRelaySwitchCo
|
||||
self.uiMappingTreeWidget.itemSelectionChanged.connect(self._mappingSelectionChangedSlot)
|
||||
|
||||
# enable sorting
|
||||
self.uiMappingTreeWidget.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
self.uiMappingTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.uiMappingTreeWidget.setSortingEnabled(True)
|
||||
|
||||
def _mappingSelectedSlot(self, item, column):
|
||||
|
||||
@@ -34,12 +34,13 @@ BUILTIN_SETTINGS = {
|
||||
|
||||
CLOUD_SETTINGS = {
|
||||
"name": "",
|
||||
"usage": "",
|
||||
"remote_console_host": "127.0.0.1",
|
||||
"remote_console_port": 23,
|
||||
"remote_console_type": "none",
|
||||
"remote_console_http_path": "/",
|
||||
"default_name_format": "Cloud{0}",
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"symbol": "cloud",
|
||||
"category": Node.end_devices,
|
||||
"ports_mapping": [],
|
||||
"node_type": "cloud"
|
||||
@@ -48,7 +49,7 @@ CLOUD_SETTINGS = {
|
||||
ETHERNET_HUB_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Hub{0}",
|
||||
"symbol": ":/symbols/hub.svg",
|
||||
"symbol": "hub",
|
||||
"category": Node.switches,
|
||||
"ports_mapping": [],
|
||||
"node_type": "ethernet_hub"
|
||||
@@ -57,7 +58,7 @@ ETHERNET_HUB_SETTINGS = {
|
||||
ETHERNET_SWITCH_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Switch{0}",
|
||||
"symbol": ":/symbols/ethernet_switch.svg",
|
||||
"symbol": "ethernet_switch",
|
||||
"category": Node.switches,
|
||||
"console_type": "none",
|
||||
"ports_mapping": [],
|
||||
|
||||