Compare commits

..

1 Commits
3.0 ... master

Author SHA1 Message Date
Jeremy Grossmann
5f34eb18d1 Merge pull request #3829 from GNS3/release/v2.2.59
Release v2.2.59
2026-05-08 20:06:01 +08:00
297 changed files with 129809 additions and 113516 deletions

2
AUTHORS Normal file
View File

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

266
CHANGELOG
View File

@@ -1,13 +1,5 @@
# Change Log
## 3.1.0a2 12/05/2026
* Release v2.2.59
* 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.59 08/05/2026
* Remove psutil version check
@@ -15,18 +7,6 @@
* Add --title to remote-viewer console commands. Fixes #3783
* Fix deleting drawings. Ref #3810
## 3.1.0a1 25/04/2026
* Fix bug when an image checksum matches with another image filename
* Refresh Ui & resources files
* Copy GUI configuration from previous minor version
* Change default disk names when creating Qemu disks
* Fix QMenu parents
* Fix UltraVNC preconfigured command
* Fix bug when dragging scene
* Implement Dark Style
* Properly close image after uploading. Fixes #3736
## 2.2.58.1 12/04/2026
* Fix callback issues in found in v2.2.58
@@ -69,15 +49,6 @@
* Display a warning if a SVG image format isn't supported
* Fix error in profile selection window after PyQt6 migration
## 3.0.6 28/01/2026
* Fixing tab name in MobaXterm
* PyQt6 migration
* Add XDG Config Home support
* Support for Python 3.14
* Clicking the "console connect to all nodes" opens all consoles in name order with case-insensitively
## 2.2.56 21/01/2026
* Fixing tab name in MobaXterm
@@ -90,21 +61,6 @@
* Support for Python 3.14
* Clicking the "console connect to all nodes" opens all consoles in name order with case-insensitively
## 3.0.5 14/05/2025
* Merge remote-tracking branch 'origin/2.2' into 3.0
* Merge remote-tracking branch 'origin/2.2' into 3.0
* Development on 2.2.55.dev1
* Release v2.2.54
* Development on 2.2.54.dev1
* Replace "Docker hub" by "Docker repository" because it is possible to use different repositories
* Upgrade dependencies
* Fix bring console in front when clicking on "Open all consoles". Fixes #3706
* Upgrade dependencies
* Allow password greater than 8 characters. Ref #3714
* Development on 3.0.5.dev1
* Add -F arg to wmctrl. Ref #3706
## 2.2.54 21/04/2025
* Replace "Docker hub" by "Docker repository" because it is possible to use different repositories
@@ -112,48 +68,12 @@
* Fix bring console in front when clicking on "Open all consoles". Fixes #3706
* Add -F arg to wmctrl. Ref #3706
## 3.0.4 25/02/2025
* Upgrade dependencies
* Fix auto idle-pc for IOS templates
* Add user info and password change for logged-in user. Fixes #3698
## 3.0.3 22/01/2025
* Set minimum duration for progress dialog when uploading. Ref https://github.com/GNS3/gns3-gui/issues/3682
* Add logs when uploading images to the controller
* Option to disable SSL certificate verification for future connections. Fixes https://github.com/GNS3/gns3-gui/issues/3694
* Fix packet capture when connected to a controller with SSL. Fixes https://github.com/GNS3/gns3-gui/issues/3696
* Update status after importing an image when installing a new appliance. Fixes #3691
* Update file browser filters to find IOU images without extension. Fixes #3692
* Upgrade dependencies
## 2.2.53 21/01/2025
* Update file browser filters for all files and IOU images
* Upgrade dependencies
* Fix Linux Mint default terminal configuration
## 3.0.2 03/01/2025
* Add button to create templates based on images that are not used by any yet.
* Add "prune" images button in image management dialog.
* Use the controller image endpoint to install appliances
* Drop Python 3.8
* Add image info tooltip in image management dialog.
* Upgrade dependencies
* Apply grid color via css property
## 3.0.1 27/12/2024
* Fix issue when image is already on the local server. Fixes https://github.com/GNS3/gns3-gui/issues/3678
* Fix image uploading when image name differs with the image name recorded in the appliance. Fixes https://github.com/GNS3/gns3-gui/issues/3682
* Fix Linux Mint default terminal configuration
## 3.0.0 20/12/2024
* Change title of QMessageBox
## 2.2.52 02/12/2024
* Add iol extension filter. Ref #3664
@@ -161,16 +81,6 @@
* 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
@@ -187,10 +97,6 @@
* 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
@@ -210,11 +116,6 @@
* 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
@@ -226,12 +127,6 @@
* Add NAT symbols
* Only show log message if event has "message"
## 3.0.0b2 07/04/2024
* Enable local controller support on Linux
* Support for custom Qemu path in templates and nodes
* Round CPUs value in Docker templates and VMs. Ref https://github.com/GNS3/gns3-gui/issues/3572
## 2.2.46 26/02/2024
* Add GNS3 console command "env" to show what environment variables are used. Ref https://github.com/GNS3/gns3-server/issues/2306
@@ -252,21 +147,6 @@
* 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
@@ -279,77 +159,6 @@
* 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
@@ -415,20 +224,6 @@
* 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
@@ -443,67 +238,6 @@
* 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 Normal file
View File

@@ -0,0 +1,504 @@
GNU Public License (GPL)
------------------------
GNS3 is released under the GPLv3 (see LICENSE) with the additional
exemption that compiling, linking, and/or using OpenSSL is allowed.
GNS3 trademark
--------------
"GNS3" is a trademark of GNS3 Technologies, Inc.
Windows Driver Kit
------------------
The Windows binary distribution includes devcon.exe, a Microsoft(R)
Windows Driver Kit (WDK) sample in object code form which is
redistributed under the terms of the WDK License terms.
With respect to binaries built using the Microsoft(R) Windows
Driver Kit (WDK), GPLv3 does not extend to any WDK Distributable Code.
All WDK Distributable Code is considered by the licensors of GNS3
to constitute, or be equivalent to, "System Libraries" as defined in
section 1 of GPLv3.
OpenSSL License
---------------
The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
the OpenSSL License and the original SSLeay license apply to the toolkit.
See below for the actual license texts. Actually both licenses are BSD-style
Open Source licenses. In case of any license issues related to OpenSSL
please contact openssl-core@openssl.org.
/* ====================================================================
* Copyright (c) 1998-2003 The OpenSSL Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. All advertising materials mentioning features or use of this
* software must display the following acknowledgment:
* "This product includes software developed by the OpenSSL Project
* for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
*
* 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For written permission, please contact
* openssl-core@openssl.org.
*
* 5. Products derived from this software may not be called "OpenSSL"
* nor may "OpenSSL" appear in their names without prior written
* permission of the OpenSSL Project.
*
* 6. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by the OpenSSL Project
* for use in the OpenSSL Toolkit (http://www.openssl.org/)"
*
* THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
* EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
* ====================================================================
*
* This product includes cryptographic software written by Eric Young
* (eay@cryptsoft.com). This product includes software written by Tim
* Hudson (tjh@cryptsoft.com).
*
*/
Original SSLeay License
-----------------------
/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
* All rights reserved.
*
* This package is an SSL implementation written
* by Eric Young (eay@cryptsoft.com).
* The implementation was written so as to conform with Netscapes SSL.
*
* This library is free for commercial and non-commercial use as long as
* the following conditions are aheared to. The following conditions
* apply to all code found in this distribution, be it the RC4, RSA,
* lhash, DES, etc., code; not just the SSL code. The SSL documentation
* included with this distribution is covered by the same copyright terms
* except that the holder is Tim Hudson (tjh@cryptsoft.com).
*
* Copyright remains Eric Young's, and as such any Copyright notices in
* the code are not to be removed.
* If this package is used in a product, Eric Young should be given attribution
* as the author of the parts of the library used.
* This can be in the form of a textual message at program startup or
* in documentation (online or textual) provided with the package.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* "This product includes cryptographic software written by
* Eric Young (eay@cryptsoft.com)"
* The word 'cryptographic' can be left out if the rouines from the library
* being used are not cryptographic related :-).
* 4. If you include any Windows specific code (or a derivative thereof) from
* the apps directory (application code) you must include an acknowledgement:
* "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
*
* THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* The licence and distribution terms for any publically available version or
* derivative of this code cannot be changed. i.e. this code cannot simply be
* copied and put under another distribution licence
* [including the GNU Public Licence.]
*/
=====================================================================================================
Several fantastic pieces of free and open-source software have really get GNS3 to where it is today.
A few require that we include their license agreements within our software.
=====================================================================================================
License notice for Qt
---------------------
http://doc.qt.io/qt-4.8/gpl.html
License notice for PyQt
-----------------------
http://www.gnu.org/licenses/gpl.html
License notice for jsonschema
-----------------------------
https://github.com/Julian/jsonschema/blob/master/COPYING
Copyright (c) 2013 Julian Berman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
License notice for aiohttp
--------------------------
https://github.com/KeepSafe/aiohttp/blob/master/LICENSE.txt
Copyright (c) 2013, 2014, 2015 Nikolay Kim and Andrew Svetlov
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
License notice for Jinja
------------------------
https://github.com/KeepSafe/aiohttp/blob/master/LICENSE.txt
Copyright (c) 2009 by the Jinja Team, see AUTHORS for more details.
Some rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
License notice for raven
------------------------
https://github.com/getsentry/raven-python/blob/master/LICENSE
Copyright (c) 2015 Functional Software, Inc and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
License notice for pywin32
--------------------------
https://github.com/SublimeText/Pywin32/blob/master/License.txt
Unless stated in the specfic source file, this work is
Copyright (c) 1996-2008, Greg Stein and Mark Hammond.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the distribution.
Neither names of Greg Stein, Mark Hammond nor the name of contributors may be used
to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
License notice for Winpcap
--------------------------
https://www.winpcap.org/misc/copyright.htm
Copyright (c) 1999 - 2005 NetGroup, Politecnico di Torino (Italy).
Copyright (c) 2005 - 2010 CACE Technologies, Davis (California).
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
License notice for cpulimit
---------------------------
https://github.com/opsengine/cpulimit/blob/master/LICENSE
Copyright (C) 2005-2012, by: Angelo Marletta <angelo dot marletta at gmail dot com>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
License notice for Cygwin
-------------------------
https://cygwin.com/licensing.html
License notice for SuperPutty
-----------------------------
https://github.com/jimradford/superputty/blob/master/License.txt
Copyright (c) 2009 Jim Radford http://www.jimradford.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
License notice for Putty
------------------------
http://www.chiark.greenend.org.uk/~sgtatham/putty/licence.html
PuTTY is copyright 1997-2015 Simon Tatham.
Portions copyright Robert de Bath, Joris van Rantwijk, Delian Delchev, Andreas Schultz, Jeroen Massar,
Wez Furlong, Nicolas Barry, Justin Bradford, Ben Harris, Malcolm Smith, Ahmad Khalifa, Markus Kuhn,
Colin Watson, Christopher Staite, and CORE SDI S.A.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
License notice for iouyap
-------------------------
https://github.com/GNS3/iouyap/blob/master/LICENSE
License notice for Dynamips
---------------------------
https://github.com/GNS3/dynamips/blob/master/COPYING
License notice for Qemu
-----------------------
http://wiki.qemu.org/License
License notice for VPCS
-----------------------
Copyright (c) 2007-2013, Paul Meng (mirnshi@gmail.com)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
License notice for Python
-------------------------
https://www.python.org/download/releases/3.4.2/license/
License notice for BusyBox
---------------------------
BusyBox is distributed under version 2 of the General Public License
https://busybox.net/license.html
Source code is available here:
https://github.com/GNS3/busybox
Licence notice for zipstream
-----------------------------
zipstream is distributed under version 3 of the General Public License
https://github.com/allanlei/python-zipstream/blob/master/LICENSE
Source code is available here:
https://pypi.python.org/pypi/zipstream
Licence notice for aiohttp_cors
-------------------------------
Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>.
Licensed under the Apache License, Version 2.0, see LICENSE file for details.
https://github.com/aio-libs/aiohttp_cors

View File

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

View File

@@ -1,14 +1,18 @@
GNS3-gui
========
[![GitHub Actions tests](https://github.com/GNS3/gns3-gui/workflows/testing/badge.svg)](https://github.com/GNS3/gns3-gui/actions?query=workflow%3Atesting)
[![Latest PyPi version](https://img.shields.io/pypi/v/gns3-gui.svg)](https://pypi.python.org/pypi/gns3-gui)
[![Snyk scanning](https://snyk.io/test/github/GNS3/gns3-gui/badge.svg)](https://snyk.io/test/github/GNS3/gns3-gui)
[![image](https://github.com/GNS3/gns3-gui/workflows/testing/badge.svg)](https://github.com/GNS3/gns3-gui/actions?query=workflow%3Atesting)
[![image](https://img.shields.io/pypi/v/gns3-gui.svg)](https://pypi.python.org/pypi/gns3-gui)
[![image](https://snyk.io/test/github/GNS3/gns3-gui/badge.svg)](https://snyk.io/test/github/GNS3/gns3-gui)
GNS3 GUI repository.
Installation
------------
Please see the documentation on our [website](https://docs.gns3.com)
Please see <https://docs.gns3.com/>
Software dependencies
---------------------
@@ -16,7 +20,7 @@ Software dependencies
PyQt6 which is either part of the Linux distribution or installable from
PyPi. The other Python dependencies are automatically installed during
the GNS3 GUI installation and are listed
[here](https://github.com/GNS3/gns3-gui/blob/3.0/requirements.txt)
[here](https://github.com/GNS3/gns3-gui/blob/master/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
@@ -39,7 +43,7 @@ cd scripts
python build_pyqt.py
```
### Debugging
### Debug
If you want to see the full logs in the internal shell you can type:

View File

@@ -1,3 +1,5 @@
-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

View File

@@ -19,7 +19,6 @@ 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
@@ -50,17 +49,7 @@ class ApplianceManager(QtCore.QObject):
settings = LocalConfig.instance().loadSectionSettings("MainWindow", GENERAL_SETTINGS)
symbol_theme = settings["symbol_theme"]
if update is True:
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
self._controller.get("/appliances?update=yes&symbol_theme={}".format(symbol_theme), self._listAppliancesCallback, progressText="Downloading appliances from online registry...")
else:
self._controller.get("/appliances?symbol_theme={}".format(symbol_theme), self._listAppliancesCallback)

View File

@@ -20,7 +20,6 @@ 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

View File

@@ -37,7 +37,6 @@ 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
@@ -203,24 +202,6 @@ class Compute:
return self._memory_usage_percent
def setDiskUsagePercent(self, usage):
"""
Sets the compute disk usage.
:returns: disk usage (integer)
"""
self._disk_usage_percent = usage
def diskUsagePercent(self):
"""
Returns the compute disk usage.
:param usage: disk usage (integer)
"""
return self._disk_usage_percent
def capabilities(self):
"""
Returns the compute capabilities

View File

@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .qt import QtCore, QtWidgets
from .qt import QtCore
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 = QtCore.QTimer()
# self._timer.setInterval(1000)
# self._timer.timeout.connect(self._refreshComputesSlot)
# self._timer.start()
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, show_progress=False, timeout=30)
self._controller.get("/computes", self._listComputesCallback, showProgress=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, show_progress=False, timeout=30)
self._controller.get("/computes", self._listComputesCallback, showProgress=False, timeout=30)
def _controllerDisconnectedSlot(self):
"""
Called when disconnected from the controller.
Called when disconnected from a compute.
"""
for compute_id in list(self._computes):
@@ -96,9 +96,8 @@ class ComputeManager(QtCore.QObject):
log.error("Error while getting compute list: {}".format(result["message"]))
return
if result:
for compute in result:
self.computeDataReceivedCallback(compute)
for compute in result:
self.computeDataReceivedCallback(compute)
def computeDataReceivedCallback(self, compute):
"""
@@ -114,17 +113,15 @@ 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"])
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"))
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 new_node:
self.created_signal.emit(compute_id)
@@ -211,20 +208,6 @@ 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
@@ -262,16 +245,10 @@ class ComputeManager(QtCore.QObject):
for compute in computes:
if compute.id() not in self._computes:
log.debug("Create compute %s", compute.id())
params = {"connect": True}
self._controller.post("/computes", callback=self._computeCreatedCallback, body=compute.__json__(), params=params)
self._controller.post("/computes", None, body=compute.__json__())
self._computes[compute.id()] = compute
self.created_signal.emit(compute.id())
def _computeCreatedCallback(self, result, error=False, **kwargs):
if error:
log.error(result.get("message"))
@staticmethod
def reset():
ComputeManager._instance = None

View File

@@ -16,15 +16,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Compute summary view that list all the computes and their status.
Compute summary view that list all the compute, 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__)
@@ -45,16 +43,9 @@ 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.)
@@ -67,20 +58,14 @@ class ComputeItem(QtWidgets.QTreeWidgetItem):
text = self._compute.name()
if self._compute.cpuUsagePercent() is not None:
text = "{} CPU {}%, RAM {}%, DISK {}%".format(text,
self._compute.cpuUsagePercent(),
self._compute.memoryUsagePercent(),
self._compute.diskUsagePercent())
text = "{} CPU {}%, RAM {}%".format(text, self._compute.cpuUsagePercent(), self._compute.memoryUsagePercent())
self.setText(0, text)
if self._compute.connected():
self._status = "connected"
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))))
self.setToolTip(0, "Server {} version {} running on {}".format(self._compute.name(),
self._compute.capabilities().get("version", "n/a"),
self._compute.capabilities().get("platform", "")))
if usage is None or (self._compute.cpuUsagePercent() < 90 and self._compute.memoryUsagePercent() < 90):
self.setIcon(0, QtGui.QIcon(':/icons/led_green.svg'))
else:
@@ -126,44 +111,13 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
def __init__(self, parent):
super().__init__(parent)
self._compute_items = {}
self._computes = {}
ComputeManager.instance().created_signal.connect(self._computeAddedSlot)
ComputeManager.instance().updated_signal.connect(self._computeUpdatedSlot)
ComputeManager.instance().deleted_signal.connect(self._computeRemovedSlot)
for compute in ComputeManager.instance().computes():
self._computeAddedSlot(compute.id())
def 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(parent=self)
connect_action = QtGui.QAction("Connect to server", menu)
connect_action.setIcon(get_icon("start.svg"))
connect_action.triggered.connect(lambda: ComputeManager.instance().connectToCompute(compute.id()))
menu.addAction(connect_action)
menu.exec(pos)
def _computeConnectSlot(self, compute_id):
"""
:param compute_id:
:return:
"""
ComputeManager.instance().connectToCompute(compute_id)
def _computeAddedSlot(self, compute_id):
"""
Called when a compute is added to the list of computes
@@ -174,7 +128,7 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
compute = ComputeManager.instance().getCompute(compute_id)
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
return
self._compute_items[compute_id] = ComputeItem(self, compute)
self._computes[compute_id] = ComputeItem(self, compute)
def _computeUpdatedSlot(self, compute_id):
"""
@@ -183,14 +137,13 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
:params url: URL of the compute
"""
if compute_id in self._compute_items:
if compute_id in self._computes:
compute = ComputeManager.instance().getCompute(compute_id)
# We hide the remote GNS3 VM
if ComputeManager.instance().computeIsTheRemoteGNS3VM(compute):
self._computeRemovedSlot(compute_id)
else:
self._compute_items[compute_id].setCompute(compute)
self._compute_items[compute_id]._refreshStatusSlot()
self._computes[compute_id]._refreshStatusSlot()
else:
self._computeAddedSlot(compute_id)
@@ -201,6 +154,6 @@ class ComputeSummaryView(QtWidgets.QTreeWidget):
:params url: URL of the compute
"""
if compute_id in self._compute_items:
self.takeTopLevelItem(self.indexOfTopLevelItem(self._compute_items[compute_id]))
del self._compute_items[compute_id]
if compute_id in self._computes:
self.takeTopLevelItem(self.indexOfTopLevelItem(self._computes[compute_id]))
del self._computes[compute_id]

View File

@@ -21,14 +21,13 @@ import tempfile
import json
import pathlib
from urllib.parse import urlparse
from .qt import QtCore, QtGui, QtWebSockets, qpartial, qslot
from .qt import QtCore, QtNetwork, QtGui, QtWidgets, QtWebSockets, qpartial, qslot
from .symbol import Symbol
from .local_config import LocalConfig
from .settings import CONTROLLER_SETTINGS
from .local_server_config import LocalServerConfig
from .settings import LOCAL_SERVER_SETTINGS
from gns3.local_config import LocalConfig
from gns3.utils import parse_version
from gns3.http_client import HTTPClient
import logging
log = logging.getLogger(__name__)
@@ -43,7 +42,6 @@ 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):
@@ -58,47 +56,16 @@ 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):
@@ -106,7 +73,8 @@ class Controller(QtCore.QObject):
:returns Boolean: True if the controller is remote
"""
return self._settings["remote"]
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
return not settings["auto_start"]
def connecting(self):
"""
@@ -136,8 +104,10 @@ class Controller(QtCore.QObject):
self._http_client = http_client
if self._http_client:
self._http_client.connected_signal.connect(self._httpClientConnectedSlot)
self._http_client.disconnected_signal.connect(self._httpClientDisconnectedSlot)
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._connectingToServer()
def getHttpClient(self):
@@ -155,14 +125,6 @@ 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
@@ -170,16 +132,46 @@ class Controller(QtCore.QObject):
self._connected = False
self._connecting = True
self.httpClient().connectToServer()
status, json_data = self.httpClient().getSynchronous('GET', '/version', timeout=60)
self._versionGetSlot(json_data, status is None or status >= 300)
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:
@@ -190,16 +182,16 @@ class Controller(QtCore.QObject):
self._startListenNotifications()
def post(self, *args, **kwargs):
return self.request("POST", *args, **kwargs)
return self.createHTTPQuery("POST", *args, **kwargs)
def get(self, *args, **kwargs):
return self.request("GET", *args, **kwargs)
return self.createHTTPQuery("GET", *args, **kwargs)
def put(self, *args, **kwargs):
return self.request("PUT", *args, **kwargs)
return self.createHTTPQuery("PUT", *args, **kwargs)
def delete(self, *args, **kwargs):
return self.request("DELETE", *args, **kwargs)
return self.createHTTPQuery("DELETE", *args, **kwargs)
def getCompute(self, path, compute_id, *args, **kwargs):
"""
@@ -235,13 +227,31 @@ class Controller(QtCore.QObject):
return compute_id
return compute_id
def request(self, method, path, *args, **kwargs):
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):
"""
Forward the query to the HTTP client or controller depending on the path
"""
if self._http_client:
return self._http_client.sendRequest(method, path, *args, **kwargs)
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
@staticmethod
def instance():
@@ -274,10 +284,9 @@ 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.sendRequest("GET", url, qpartial(self._getStaticCallback, url, path), raw=True)
def _getStaticCallback(self, url, path, result, error=False, **kwargs):
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, url, path))
def _getStaticCallback(self, url, path, result, error=False, raw_body=None, **kwargs):
if path not in self._static_asset_download_queue:
return
@@ -293,7 +302,7 @@ class Controller(QtCore.QObject):
return
try:
with open(path, "wb+") as f:
f.write(result)
f.write(raw_body)
except OSError as e:
log.error("Can't write to {}: {}".format(path, str(e)))
return
@@ -357,14 +366,9 @@ class Controller(QtCore.QObject):
def uploadSymbol(self, symbol_id, path):
self.post(
"/symbols/" + symbol_id + "/raw",
qpartial(self._finishSymbolUpload, path),
body=pathlib.Path(path),
progress_text="Uploading {}".format(symbol_id),
timeout=None,
wait=True
)
self.post("/symbols/" + symbol_id + "/raw",
qpartial(self._finishSymbolUpload, path),
body=pathlib.Path(path), progressText="Uploading {}".format(symbol_id), timeout=None)
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
@@ -393,17 +397,6 @@ 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)
@@ -416,36 +409,26 @@ 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().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}")
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))
else:
self._notification_stream = self._http_client.connectWebSocket(self._websocket, "/notifications/ws")
self._notification_stream.textMessageReceived.connect(self._websocket_event_received)
@@ -454,15 +437,7 @@ class Controller(QtCore.QObject):
else:
self._notification_stream.errorOccurred.connect(self._websocket_error)
self._notification_stream.sslErrors.connect(self._sslErrorsSlot)
self._notification_stream.disconnected.connect(self._websocket_disconnected)
url = urlparse(self._notification_stream.requestUrl().toString())
log.info(f"Listening for controller notifications on {url.scheme}://{url.netloc}{url.path}")
def _websocket_disconnected(self):
self._connected = False
self.disconnected_signal.emit()
self.stopListenNotifications()
log.info("Listening for controller notifications on '{}'".format(self._notification_stream.requestUrl().toString()))
def stopListenNotifications(self):
if self._notification_stream:
@@ -470,6 +445,7 @@ 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):
"""
@@ -481,10 +457,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.stopListenNotifications()
self._notification_stream = None
self._startListenNotifications()
@qslot
def _sslErrorsSlot(self, ssl_errors):

View File

@@ -15,12 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
import platform
import struct
import distro
try:
import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
@@ -29,6 +23,12 @@ 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://45b39b114d5c49acf77e6de84648ec3d@o19455.ingest.us.sentry.io/38506"
DSN = "https://dd662ce99d7e4a04714a89939ec523c9@o19455.ingest.us.sentry.io/38506"
_instance = None
def __init__(self):

View File

@@ -16,7 +16,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
from ..qt import sip
import shutil
@@ -30,7 +29,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_size
from ..utils import human_filesize
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
from ..utils.progress_dialog import ProgressDialog
from ..compute_manager import ComputeManager
@@ -53,6 +52,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self.setupUi(self)
self._refreshing = False
self._server_check = False
self._template_created = False
self._path = path
@@ -75,12 +75,6 @@ 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):
@@ -108,6 +102,9 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
# 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")
@@ -141,12 +138,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
# add symbol
if self._appliance["category"] == "guest":
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"
symbol = ":/symbols/computer.svg"
else:
symbol = ":/symbols/{}.svg".format(self._appliance["category"])
self.page(page_id).setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(symbol))
@@ -154,6 +146,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
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)
@@ -162,22 +160,50 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
for compute in ComputeManager.instance().remoteComputes():
self.uiRemoteServersComboBox.addItem(compute.name(), compute)
#if ComputeManager.instance().localPlatform() is None:
# self.uiLocalRadioButton.setEnabled(False)
if not ComputeManager.instance().vmCompute():
self.uiVMRadioButton.setEnabled(False)
if ComputeManager.instance().localCompute() and self.uiLocalRadioButton.isEnabled():
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():
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._registry.getRemoteImageList(self._appliance.template_type(), self._compute_id)
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")
@@ -201,8 +227,27 @@ 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.uiLocalRadioButton.isEnabled()
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
def _imageUploadedCallback(self, result, error=False, context=None, **kwargs):
if context is None:
@@ -212,7 +257,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._registry.getRemoteImageList(self._appliance.template_type(), self._compute_id)
def _showApplianceInfoSlot(self):
"""
@@ -323,13 +368,13 @@ Usage: {}
size += image.get("filesize", 0)
image_widget = QtWidgets.QTreeWidgetItem([image["filename"],
human_size(image.get("filesize", 0)),
human_filesize(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(0, f'{image["status"]} with path: {image["path"]}')
image_widget.setToolTip(2, image["path"])
# Associated data stored are col 0: version, col 1: image
image_widget.setData(0, QtCore.Qt.ItemDataRole.UserRole, version)
@@ -348,7 +393,7 @@ Usage: {}
expand = False
top.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
top.setData(1, QtCore.Qt.ItemDataRole.DisplayRole, human_size(size))
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)
@@ -383,13 +428,11 @@ 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"
@@ -519,14 +562,36 @@ Usage: {}
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.parent())
image_upload_manger = ImageUploadManager(image, Controller.instance(), self._compute_id, self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger.upload()
# refresh the images list
if Controller.instance().isRemote() or self._compute_id != "local":
self._registry.getRemoteImageList()
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.images_changed_signal.emit()
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):
"""
@@ -555,10 +620,33 @@ Usage: {}
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:
@@ -578,24 +666,37 @@ 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 False
return
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 True
return
image = Image(self._appliance.template_type(), image["path"], filename=image["filename"])
image_upload_manager = ImageUploadManager(image, Controller.instance(), self.parent())
if not image_upload_manager.upload():
return False
return True
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
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() + 2
elif not self._appliance.get("installation_instructions"):
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"):
# skip the installation instructions page if there are no instructions
return super().nextId() + 1
return super().nextId()
@@ -627,7 +728,7 @@ Usage: {}
if reply == QtWidgets.QMessageBox.StandardButton.No:
return False
return self._uploadImages(appliance["name"], version["name"])
self._uploadImages(appliance["name"], version["name"])
elif self.currentPage() == self.uiUsageWizardPage:
# validate the usage page
@@ -652,6 +753,8 @@ 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")):
@@ -661,8 +764,28 @@ Usage: {}
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):
"""

View File

@@ -15,11 +15,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import psutil
import platform
import os
import stat
import sys
import struct
from gns3.qt import QtWidgets
@@ -92,6 +92,19 @@ 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"""
@@ -173,9 +186,39 @@ 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)

View File

@@ -34,15 +34,42 @@ 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)
self.uiServerUserLineEdit.setText(self._compute.user())
if self._compute.user():
self.uiEnableAuthenticationCheckBox.setChecked(True)
self.uiServerUserLineEdit.setText(self._compute.user())
else:
self.uiEnableAuthenticationCheckBox.setChecked(False)
self.uiWarningLabel.setVisible(False)
else:
self.uiEnableAuthenticationCheckBox.setChecked(False)
self.uiWarningLabel.setVisible(False)
self._enableAuthenticationSlot(self.uiEnableAuthenticationCheckBox.isChecked())
def _enableAuthenticationSlot(self, state):
"""
Slot to enable or not the authentication.
"""
if self.uiEnableAuthenticationCheckBox.isChecked():
self.uiServerUserLineEdit.setVisible(True)
self.uiServerPasswordLineEdit.setVisible(True)
self.uiServerUserLabel.setVisible(True)
self.uiServerPasswordLabel.setVisible(True)
else:
self.uiServerUserLineEdit.setVisible(False)
self.uiServerPasswordLineEdit.setVisible(False)
self.uiServerUserLabel.setVisible(False)
self.uiServerPasswordLabel.setVisible(False)
def compute(self):
return self._compute
@@ -60,16 +87,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 compute hostname {}".format(host))
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server 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 compute name {}".format(name))
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server name {}".format(name))
return
if port is None or port < 1:
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid compute port {}".format(port))
QtWidgets.QMessageBox.critical(self, "Remote compute", "Invalid remote server port {}".format(port))
return
if not self._compute:
@@ -78,8 +105,12 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
self._compute.setProtocol(protocol)
self._compute.setHost(host)
self._compute.setPort(port)
self._compute.setUser(user)
self._compute.setPassword(password)
if self.uiEnableAuthenticationCheckBox.isChecked():
self._compute.setUser(user)
self._compute.setPassword(password)
else:
self._compute.setUser(None)
self._compute.setPassword(None)
super().accept()

View File

@@ -15,9 +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 gns3.utils import parse_version
from ..qt import QtGui, QtWidgets, QtCore, qslot, qpartial
from ..qt import QtWidgets, QtCore, qslot, qpartial
from ..topology import Topology
from ..ui.edit_project_dialog_ui import Ui_EditProjectDialog
@@ -48,39 +46,11 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
self.uiNewVarButton.clicked.connect(self.onAddNewVariable)
self.uiGlobalVariablesGrid.addWidget(self.uiNewVarButton, 0, 3, QtCore.Qt.AlignmentFlag.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)
@@ -145,12 +115,4 @@ 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")

View File

@@ -64,10 +64,9 @@ class FileEditorDialog(QtWidgets.QDialog, Ui_FileEditorDialog):
def _refreshSlot(self):
self._target.get("/files/" + self._path, self._getCallback)
def _getCallback(self, result, error=False, **kwargs):
def _getCallback(self, result, error=False, raw_body=None, **kwargs):
if not error:
self.uiFileTextEdit.setText(result)
self.uiFileTextEdit.setText(raw_body.decode("utf-8", errors="ignore"))
elif result.get("status") == 404:
if self._default:
self.uiFileTextEdit.setText(self._default)

View File

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

View File

@@ -1,60 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from ..qt import QtWidgets
from ..ui.login_dialog_ui import Ui_LoginDialog
import logging
log = logging.getLogger(__name__)
class LoginDialog(QtWidgets.QDialog, Ui_LoginDialog):
"""
Login dialog.
"""
def __init__(self, parent):
"""
:param parent: parent widget.
"""
super().__init__(parent)
self.setupUi(self)
self._parent = parent
self._username = None
self._password = None
def getUsername(self):
return self._username
def setUsername(self, username):
self.uiUsernameLineEdit.setText(username)
def getPassword(self):
return self._password
def done(self, result):
if result:
self._username = self.uiUsernameLineEdit.text()
self._password = self.uiPasswordLineEdit.text()
super().done(result)

View File

@@ -1,85 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from ..qt import QtCore, QtGui, QtWidgets
from ..ui.password_dialog_ui import Ui_PasswordDialog
import logging
log = logging.getLogger(__name__)
class PasswordDialog(QtWidgets.QDialog, Ui_PasswordDialog):
"""
Password dialog.
"""
def __init__(self, parent):
"""
:param parent: parent widget.
"""
super().__init__(parent)
self.setupUi(self)
self._password = None
self._eye_on_icon = QtGui.QIcon(':/icons/eye-on.svg')
self._eye_off_icon = QtGui.QIcon(':/icons/eye-off.svg')
for line_edit in [self.uiPasswordLineEdit, self.uiConfirmPasswordLineEdit]:
action = line_edit.addAction(self._eye_on_icon, QtWidgets.QLineEdit.TrailingPosition)
button = action.associatedWidgets()[-1]
button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
button.pressed.connect(self.onPressedSlot)
#button.released.connect(self.onReleasedSlot)
def onPressedSlot(self):
button = self.sender()
line_edit = button.parent()
if line_edit.echoMode() == QtWidgets.QLineEdit.Password:
button.setIcon(self._eye_off_icon)
line_edit.setEchoMode(QtWidgets.QLineEdit.Normal)
else:
button.setIcon(self._eye_on_icon)
line_edit.setEchoMode(QtWidgets.QLineEdit.Password)
# def onReleasedSlot(self):
#
# button = self.sender()
# button.setIcon(self._eye_on_icon)
# button.parent().setEchoMode(QtWidgets.QLineEdit.Password)
def getPassword(self):
return self._password
def done(self, result):
if result:
new_password = self.uiPasswordLineEdit.text()
confirm_password = self.uiConfirmPasswordLineEdit.text()
if new_password != confirm_password:
QtWidgets.QMessageBox.critical(self, "Error", "Passwords do not match.")
return
pattern = re.compile(r'^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$')
if not pattern.match(new_password):
QtWidgets.QMessageBox.critical(self, "Error", "Password must be at least 8 characters long and contain at least one digit, one lowercase letter and one uppercase letter.")
return
self._password = new_password
super().done(result)

View File

@@ -21,10 +21,9 @@ Dialog to load module and built-in preference pages.
from ..qt import QtGui, QtCore, QtWidgets
from ..ui.preferences_dialog_ui import Ui_PreferencesDialog
from ..pages.controller_preferences_page import ControllerPreferencesPage
from ..pages.server_preferences_page import ServerPreferencesPage
from ..pages.general_preferences_page import GeneralPreferencesPage
from ..pages.packet_capture_preferences_page import PacketCapturePreferencesPage
from ..pages.user_preferences_page import UserPreferencesPage
from ..pages.gns3_vm_preferences_page import GNS3VMPreferencesPage
from ..modules import MODULES
@@ -86,10 +85,9 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
# load built-in preference pages
pages = [
GeneralPreferencesPage,
ControllerPreferencesPage,
UserPreferencesPage,
#GNS3VMPreferencesPage,
PacketCapturePreferencesPage
ServerPreferencesPage,
GNS3VMPreferencesPage,
PacketCapturePreferencesPage,
]
for page in pages:

View File

@@ -139,24 +139,18 @@ 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},
progress_text="Duplicating project '{}'...".format(name),
timeout=None,
wait=True
)
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)
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},
progress_text="Duplicating project '{}'...".format(name),
timeout=None,
wait=True
)
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)
def _duplicateCallback(self, result, error=False, **kwargs):
if error:

View File

@@ -16,13 +16,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
import datetime
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
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
import logging
log = logging.getLogger(__name__)
@@ -33,8 +34,6 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
Export project wizard.
"""
readme_signal = QtCore.pyqtSignal()
def __init__(self, project, parent):
super().__init__(parent)
@@ -51,40 +50,24 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
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 zstd compression by default
self.uiCompressionComboBox.setCurrentIndex(4)
# set zip compression by default
self.uiCompressionComboBox.setCurrentIndex(1)
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/{}".format(self._readme_filename), self._loadedReadme, raw=True)
self._project.get("/files/README.txt", self._loadedReadme)
def _loadedReadme(self, result, error=False, context={}, **kwargs):
def _loadedReadme(self, result, error=False, raw_body=None, context={}, **kwargs):
if not error:
content = result.decode("utf-8", errors="replace")
self.uiReadmeTextEdit.setPlainText(content)
self.uiReadmeTextEdit.setPlainText(raw_body.decode("utf-8", errors="replace"))
else:
if self._use_markdown:
readme_markdown = "# Project {}\n\nCreated on: {}\n\nAuthor: John Doe <john.doe@example.com>\n\n## Description\n\nNo project description was given".format(self._project.name(), datetime.date.today())
self.uiReadmeTextEdit.setPlainText(readme_markdown)
else:
readme_text = "Project: '{}' created on {}\nAuthor: John Doe <john.doe@example.com>\n\nNo project description was given".format(self._project.name(), datetime.date.today())
self.uiReadmeTextEdit.setPlainText(readme_text)
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):
@@ -92,28 +75,14 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
if len(directory) == 0:
directory = LocalServer.instance().localServerSettings()["projects_path"]
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export project", directory,
"GNS3 Project (*.gns3project *.gns3p)",
"GNS3 Project (*.gns3project *.gns3p)")
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export portable project", directory,
"GNS3 Portable Project (*.gns3project *.gns3p)",
"GNS3 Portable 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
@@ -141,39 +110,15 @@ 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):
"""
@@ -181,11 +126,6 @@ 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"
@@ -197,46 +137,8 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
keep_compute_ids = "yes"
compression = self.uiCompressionComboBox.currentData()
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}"
)
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()
super().done(result)
def _downloadFileProgress(self, content, **kwargs):
"""
Called for each part of the file
"""
try:
with open(self._path, 'ab') as f:
f.write(content)
except OSError as e:
log.error(f"Could not write project file: {e}")
return

View File

@@ -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, raw=True)
self._project.get("/files/README.txt", self._loadedReadme)
def _loadedReadme(self, result, error=False, context={}, **kwargs):
def _loadedReadme(self, result, error=False, raw_body=None, context={}, **kwargs):
if not error:
self.label.setText(result.decode("utf-8"))
self.label.setText(raw_body.decode("utf-8"))
def onValueChange(self, variable, text):
variable["value"] = text

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 GNS3 Technologies Inc.
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,12 +19,14 @@ import sys
import os
import shutil
from gns3.qt import QtCore, QtWidgets, QtNetwork
from gns3.qt import QtCore, QtWidgets, QtGui, QtNetwork, qslot
from gns3.controller import Controller
from gns3.local_server import LocalServer
from gns3.utils.interfaces import interfaces
from ..settings import DEFAULT_CONTROLLER_HOST
from ..settings import DEFAULT_LOCAL_SERVER_HOST
from ..ui.setup_wizard_ui import Ui_SetupWizard
from ..version import __version__
import logging
@@ -43,17 +45,40 @@ 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)
if sys.platform.startswith("darwin"):
# we want to see the cancel button on OSX
self.setOptions(QtWidgets.QWizard.WizardOption.NoDefaultButton)
self.uiLocalServerToolButton.clicked.connect(self._localServerBrowserSlot)
self.uiGNS3VMDownloadLinkUrlLabel.setText("")
self.uiRefreshPushButton.clicked.connect(self._refreshVMListSlot)
self.uiVmwareRadioButton.clicked.connect(self._listVMwareVMsSlot)
self.uiVirtualBoxRadioButton.clicked.connect(self._listVirtualBoxVMsSlot)
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.uiLocalControllerWizardPage.registerField("path*", self.uiLocalServerPathLineEdit)
self.uiLocalServerWizardPage.registerField("path*", self.uiLocalServerPathLineEdit)
# load all available addresses
for address in QtNetwork.QNetworkInterface.allAddresses():
@@ -70,11 +95,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"):
# only support local controller on Linux
self.uiLocalControllerRadioButton.setChecked(True)
else:
self.uiLocalControllerRadioButton.setEnabled(False)
self.uiRemoteControllerRadioButton.setChecked(True)
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)
def _localServerBrowserSlot(self):
"""
@@ -83,7 +108,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
filter = ""
if sys.platform.startswith("win"):
filter = "Executable (*.exe);;All files (*)"
filter = "Executable (*.exe);;All files (*.*)"
server_path = shutil.which("gns3server")
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select the local server", server_path, filter)
if not path:
@@ -91,6 +116,36 @@ 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.
@@ -106,6 +161,13 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
dialog.uiTreeWidget.setCurrentItem(child_pane)
return dialog.uiStackedWidget.currentWidget()
def _getSettingsCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
log.error("Error while get gettings: {}".format(result["message"]))
return
self._gns3_vm_settings = result
def initializePage(self, page_id):
"""
Initialize Wizard pages.
@@ -114,16 +176,35 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
super().initializePage(page_id)
if self.page(page_id) == self.uiControllerWizardPage:
if self.page(page_id) == self.uiServerWizardPage:
Controller.instance().setDisplayError(False)
elif self.page(page_id) == self.uiLocalControllerWizardPage:
Controller.instance().get("/gns3vm", self._getSettingsCallback)
elif self.page(page_id) == self.uiVMWizardPage:
if self._GNS3VMSettings()["engine"] == "vmware":
self.uiVmwareRadioButton.setChecked(True)
self._listVMwareVMsSlot()
elif self._GNS3VMSettings()["engine"] == "virtualbox":
self.uiVirtualBoxRadioButton.setChecked(True)
self._listVirtualBoxVMsSlot()
self.uiCPUSpinBox.setValue(self._GNS3VMSettings()["vcpus"])
self.uiRAMSpinBox.setValue(self._GNS3VMSettings()["ram"])
elif self.page(page_id) == self.uiLocalServerWizardPage:
local_server_settings = LocalServer.instance().localServerSettings()
self.uiLocalServerPathLineEdit.setText(local_server_settings["path"])
index = self.uiLocalServerHostComboBox.findData(local_server_settings["host"])
if index != -1:
self.uiLocalServerHostComboBox.setCurrentIndex(index)
else:
index = self.uiLocalServerHostComboBox.findText(DEFAULT_CONTROLLER_HOST)
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)
if index != -1:
self.uiLocalServerHostComboBox.setCurrentIndex(index)
@@ -132,28 +213,61 @@ 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_CONTROLLER_HOST)
self.uiRemoteMainServerHostLineEdit.setText(DEFAULT_LOCAL_SERVER_HOST)
self.uiRemoteMainServerAuthCheckBox.setChecked(False)
self.uiRemoteMainServerUserLineEdit.setText("")
self.uiRemoteMainServerPasswordLineEdit.setText("")
else:
self.uiRemoteMainServerHostLineEdit.setText(local_server_settings["host"])
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["username"])
self.uiRemoteMainServerAuthCheckBox.setChecked(local_server_settings["auth"])
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["user"])
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.uiLocalControllerRadioButton.isChecked():
if self.uiLocalRadioButton.isChecked():
local_server_settings = LocalServer.instance().localServerSettings()
self._addSummaryEntry("Type:", "Local")
self._addSummaryEntry("Server type:", "Local")
self._addSummaryEntry("Path:", local_server_settings["path"])
self._addSummaryEntry("Host:", local_server_settings["host"])
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
elif self.uiRemoteControllerRadioButton.isChecked():
local_server_settings = LocalServer.instance().localServerSettings()
self._addSummaryEntry("Type:", "Remote")
self._addSummaryEntry("Server type:", "Remote")
self._addSummaryEntry("Host:", local_server_settings["host"])
self._addSummaryEntry("Port:", str(local_server_settings["port"]))
self._addSummaryEntry("User:", local_server_settings["username"])
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)
def _saveSettingsCallback(self, result, error=False, **kwargs):
if error:
@@ -175,45 +289,112 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
Controller.instance().setDisplayError(True)
if self.currentPage() == self.uiLocalControllerWizardPage:
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
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()
if self.uiVmwareRadioButton.isChecked():
vm_settings["engine"] = "vmware"
elif self.uiVirtualBoxRadioButton.isChecked():
vm_settings["engine"] = "virtualbox"
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"]))
# 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.")
return False
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"]))
elif self.currentPage() == self.uiLocalServerWizardPage:
local_server_settings = LocalServer.instance().localServerSettings()
local_server_settings["auto_start"] = True
local_server_settings["path"] = self.uiLocalServerPathLineEdit.text().strip()
local_server_settings["host"] = self.uiLocalServerHostComboBox.itemData(self.uiLocalServerHostComboBox.currentIndex())
local_server_settings["port"] = self.uiLocalServerPortSpinBox.value()
if not os.path.isfile(local_server_settings["path"]):
QtWidgets.QMessageBox.critical(self, "Local server", "Could not find local server {}".format(local_server_settings["path"]))
return False
if not os.access(local_server_settings["path"], os.X_OK):
QtWidgets.QMessageBox.critical(self, "Local server", "{} is not an executable".format(local_server_settings["path"]))
return False
LocalServer.instance().updateLocalServerSettings(local_controller_settings)
# start and connect to the controller if required
LocalServer.instance().updateLocalServerSettings(local_server_settings)
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)
remote_controller_settings = Controller.instance().settings()
remote_controller_settings["auto_start"] = False
remote_controller_settings["remote"] = True
remote_controller_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
remote_controller_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
remote_controller_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText().lower()
remote_controller_settings["accept_insecure_ssl_certificate"] = False
remote_controller_settings["username"] = self.uiRemoteMainServerUserLineEdit.text()
remote_controller_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
Controller.instance().setSettings(remote_controller_settings)
Controller.instance().connect()
return Controller.instance().connected()
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
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.
@@ -238,17 +419,20 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
"""
current_id = self.currentId()
if self.page(current_id) == self.uiLocalControllerWizardPage and self.uiLocalControllerRadioButton.isChecked():
if self.page(current_id) == self.uiLocalServerStatusWizardPage and not self.uiVMRadioButton.isChecked():
return self._pageId(self.uiSummaryWizardPage)
if self.page(current_id) == self.uiControllerWizardPage and self.uiRemoteControllerRadioButton.isChecked():
if self.page(current_id) == self.uiServerWizardPage and self.uiRemoteControllerRadioButton.isChecked():
return self._pageId(self.uiRemoteControllerWizardPage)
if self.page(current_id) == self.uiVMWizardPage:
return self._pageId(self.uiSummaryWizardPage)
return QtWidgets.QWizard.nextId(self)
def _pageId(self, page):
"""
Return id of the page
"""
for id in self.pageIds():
if self.page(id) == page:
return id

View File

@@ -1,71 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from gns3.qt import QtCore, QtGui, QtWidgets
from gns3.ui.show_readme_dialog_ui import Ui_ShowReadmeDialog
from gns3.utils import parse_version
import logging
log = logging.getLogger(__name__)
class ShowReadmeDialog(QtWidgets.QDialog, Ui_ShowReadmeDialog):
def __init__(self, project, path, content=None, parent=None):
if parent is None:
from gns3.main_window import MainWindow
parent = MainWindow.instance()
super().__init__(parent)
self.setupUi(self)
self._project = project
self._path = path
self.setWindowTitle(project.name() + " " + os.path.basename(path))
self.uiRefreshButton.pressed.connect(self._refreshSlot)
# QTextDocument before Qt version 5.14 doesn't support Markdown
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.14.0") or parse_version(QtCore.PYQT_VERSION_STR) < parse_version("5.14.0"):
self._markdown = False
else:
self._markdown = True
self._document = QtGui.QTextDocument()
self.uiTextBrowser.setDocument(self._document)
if content:
if self._markdown:
self._document.setMarkdown(content)
else:
self._document.setPlainText(content)
else:
self._refreshSlot()
def _refreshSlot(self):
self._project.get("/files/" + self._path, self._getCallback, raw=True)
def _getCallback(self, result, error=False, **kwargs):
if not error:
content = result.decode("utf-8", errors="ignore")
if self._markdown:
self._document.setMarkdown(content)
else:
self._document.setPlainText(content)

View File

@@ -85,14 +85,11 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
snapshot_name, ok = QtWidgets.QInputDialog.getText(self, "Snapshot", "Snapshot name:", QtWidgets.QLineEdit.EchoMode.Normal, "Unnamed")
if ok and snapshot_name and self._project:
Controller.instance().post(
"/projects/{}/snapshots".format(self._project.id()),
self._createSnapshotsCallback,
{"name": snapshot_name},
progress_text="Creation of snapshot '{}' in progress...".format(snapshot_name),
timeout=None,
wait=True
)
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)
def _createSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):
if error:
@@ -142,13 +139,8 @@ class SnapshotsDialog(QtWidgets.QDialog, Ui_SnapshotsDialog):
if reply == QtWidgets.QMessageBox.StandardButton.Cancel:
return
Controller.instance().post(
"/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id),
self._restoreSnapshotsCallback,
progress_text="Restoring snapshot...",
timeout=None,
wait=True
)
Controller.instance().post("/projects/{}/snapshots/{}/restore".format(self._project.id(), snapshot_id),
self._restoreSnapshotsCallback, progressText="Restoring snapshot...", timeout=None)
def _restoreSnapshotsCallback(self, result, error=False, server=None, context={}, **kwargs):

View File

@@ -22,7 +22,6 @@ 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
@@ -50,8 +49,7 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
self.uiBorderStyleComboBox.addItem("Dot", QtCore.Qt.PenStyle.DotLine)
self.uiBorderStyleComboBox.addItem("Dash Dot", QtCore.Qt.PenStyle.DashDotLine)
self.uiBorderStyleComboBox.addItem("Dash Dot Dot", QtCore.Qt.PenStyle.DashDotDotLine)
if True not in list(map(lambda item: isinstance(item, LineItem), items)):
self.uiBorderStyleComboBox.addItem("No border", QtCore.Qt.PenStyle.NoPen)
self.uiBorderStyleComboBox.addItem("No border", QtCore.Qt.PenStyle.NoPen)
# use the first item in the list as the model
first_item = items[0]

View File

@@ -191,14 +191,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
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),
progress_text="Uploading {}".format(symbol_id),
timeout=None,
wait=True
)
Controller.instance().post("/symbols/" + symbol_id + "/raw", qpartial(self._finishSymbolUpload, path), body=pathlib.Path(path), progressText="Uploading {}".format(symbol_id), timeout=None)
def _finishSymbolUpload(self, path, result, error=False, **kwargs):
if error:

View File

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

View File

@@ -37,7 +37,6 @@ 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)
@@ -103,8 +102,6 @@ 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)

View File

@@ -100,6 +100,8 @@ 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)
@@ -830,18 +832,6 @@ class GraphicsView(QtWidgets.QGraphicsView):
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 = QtGui.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 = QtGui.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)
@@ -1021,26 +1011,6 @@ 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
@@ -1068,6 +1038,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
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):
@@ -1100,7 +1074,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
break
if os.path.exists(node_dir):
log.debug(f"Open {node_dir} in file manager")
log.debug("Open %s 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,58 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2021 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class HttpClientError(Exception):
def __init__(self, message: str):
super().__init__()
self._message = message
def __repr__(self):
return self._message
def __str__(self):
return self._message
class HttpClientNotFoundError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)
class HttpClientCancelledRequestError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)
class HttpClientBadRequestError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)
class HttpClientUnauthorizedError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)
class HttpClientForbiddenError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)
class HttpClientTimeoutError(HttpClientError):
def __init__(self, message: str):
super().__init__(message)

View File

@@ -20,10 +20,11 @@ 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
@@ -68,7 +69,7 @@ class ImageManager:
"""
if (server and server != "local") or Controller.instance().isRemote():
return self._uploadImageToRemoteServer(source_path, node_type, parent)
return self._uploadImageToRemoteServer(source_path, server, node_type)
else:
destination_directory = self.getDirectoryForType(node_type)
destination_path = os.path.join(destination_directory, os.path.basename(source_path))
@@ -114,45 +115,27 @@ class ImageManager:
source_path = destination_path
return source_path
def _uploadImageToRemoteServer(self, path, node_type, parent):
def _uploadImageToRemoteServer(self, path, server, node_type):
"""
Upload image to remote server
:param path: File path on computer
:param server: The server where the images should be located
:param node_type: Image node_type
:returns path: Final path
"""
if node_type == 'QEMU':
image_type = 'qemu'
upload_endpoint = '/qemu/images'
elif node_type == 'IOU':
image_type = 'iou'
upload_endpoint = '/iou/images'
elif node_type == 'DYNAMIPS':
image_type = 'ios'
upload_endpoint = '/dynamips/images'
else:
raise Exception('Invalid node type')
filename = self._getRelativeImagePath(path, node_type).replace("\\", "/")
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}"
)
Controller.instance().postCompute('{}/{}'.format(upload_endpoint, filename), server, None, body=pathlib.Path(path), progressText="Uploading {}".format(filename), timeout=None)
return filename
def _getRelativeImagePath(self, path, node_type):
@@ -181,7 +164,7 @@ class ImageManager:
:returns: path to the default images directory
"""
return copy.copy(Controller.instance().settings()['images_path'])
return copy.copy(LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)['images_path'])
def getDirectoryForType(self, node_type):
"""

View File

@@ -17,11 +17,9 @@
import os
import pathlib
import urllib.parse
from gns3.http_client_error import HttpClientError, HttpClientCancelledRequestError
from gns3.qt import QtWidgets
from gns3.registry.image import Image
from gns3.controller import Controller
from gns3.http_client import HTTPClient
import logging
log = logging.getLogger(__name__)
@@ -29,42 +27,64 @@ log = logging.getLogger(__name__)
class ImageUploadManager(object):
"""
Manager over the image upload
Manager over the image upload. Encapsulates file uploads to computes or via controller.
"""
def __init__(self, image: Image, controller: Controller, parent: QtWidgets.QWidget):
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
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 False
return self._fileUploadToController()
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()
def _fileUploadToController(self) -> bool:
def _getComputePath(self):
return '/{emulator}/images/{filename}'.format(emulator=self._image.emulator, filename=self._image.filename)
log.info("Uploading image '{}' to controller".format(self._image.filename))
try:
self._controller.post(
f"/images/upload/{self._image.filename}",
callback=None,
body=pathlib.Path(self._image.path),
context={"image_path": self._image.path},
progress_text="Uploading {}".format(self._image.filename),
timeout=None,
wait=True
)
except HttpClientCancelledRequestError:
return False
except HttpClientError as e:
QtWidgets.QMessageBox.critical(
self._parent,
"Image upload to controller",
f"Could not upload image {self._image.filename}: {e}"
)
return False
return True
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)

View File

@@ -27,7 +27,7 @@ log = logging.getLogger(__name__)
class DrawingItem:
# Map QT stroke to SVG style
QT_DASH_TO_SVG = {
QtCore.Qt.PenStyle.SolidLine: "none",
QtCore.Qt.PenStyle.SolidLine: "",
QtCore.Qt.PenStyle.NoPen: None,
QtCore.Qt.PenStyle.DashLine: "25, 25",
QtCore.Qt.PenStyle.DotLine: "5, 25",
@@ -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__(), show_progress=False)
self._project.put("/drawings/" + self._id, self.updateDrawingCallback, body=self.__json__(), showProgress=False)
@qslot
def updateDrawingCallback(self, result, error=False, **kwargs):
@@ -234,8 +234,10 @@ class DrawingItem:
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._main_window.uiSnapToGridAction.isChecked() \
and self._allow_snap_to_grid:
grid_size = self._graphics_view.drawingGridSize()
value.setX(grid_size * round(value.x() / grid_size))
value.setY(grid_size * round(value.y() / grid_size))
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)
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemSelectedChange:
if not value:

View File

@@ -159,12 +159,6 @@ 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.
@@ -288,12 +282,6 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
resume_action.triggered.connect(self._suspendActionSlot)
menu.addAction(resume_action)
# reset
reset_action = QtGui.QAction("Reset", menu)
reset_action.setIcon(get_icon('reload.svg'))
reset_action.triggered.connect(self._resetActionSlot)
menu.addAction(reset_action)
# style
style_action = QtGui.QAction("Style", menu)
style_action.setIcon(get_icon("node_conception.svg"))
@@ -352,14 +340,6 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
self._deleteActionSlot()
return
def _resetActionSlot(self):
"""
Slot to receive events from the reset action in the
contextual menu.
"""
self.reset()
def _deleteActionSlot(self):
"""
Slot to receive events from the delete action in the

View File

@@ -125,19 +125,15 @@ class Link(QtCore.QObject):
else:
self._capture_file = QtCore.QFile(self._capture_file_path)
self._capture_file.open(QtCore.QIODeviceBase.OpenModeFlag.WriteOnly)
if self._network_manager is None:
self._network_manager = QtNetwork.QNetworkAccessManager(self)
self._network_manager.sslErrors.connect(Controller.instance().httpClient().handleSslError)
self._response_stream = Controller.instance().get(
"/projects/{project_id}/links/{link_id}/capture/stream".format(project_id=self.project().id(), link_id=self._link_id),
callback=None,
show_progress=False,
download_progress_callback=self._downloadPcapProgress,
timeout=None,
network_manager=self._network_manager
)
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)
log.debug("Has successfully started capturing packets on link {} to '{}'".format(self._link_id, self._capture_file_path))
else:
self._response_stream = None
@@ -363,35 +359,12 @@ 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}/capture/start".format(project_id=self.project().id(), link_id=self._link_id),
Controller.instance().post("/projects/{project_id}/links/{link_id}/start_capture".format(project_id=self.project().id(), link_id=self._link_id),
self._startCaptureCallback,
body=data)
@@ -422,7 +395,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}/capture/stop".format(project_id=self.project().id(),
Controller.instance().post("/projects/{project_id}/links/{link_id}/stop_capture".format(project_id=self.project().id(),
link_id=self._link_id),
self._stopCaptureCallback)

View File

@@ -26,6 +26,8 @@ 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__)
@@ -47,6 +49,7 @@ class LocalConfig(QtCore.QObject):
super().__init__()
self._profile = None
self._config_file = config_file
self._migrateOldConfigPath()
self._resetLoadConfig()
def _resetLoadConfig(self):
@@ -87,11 +90,25 @@ class LocalConfig(QtCore.QObject):
# create the config file if it doesn't exist
os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
# Try to migrate the configuration file from the previous version if it exists
previous_version = f"{__version_info__[0]}.{int(__version_info__[1]) - 1}"
previous_config_file = os.path.join(self.configDirectory(previous_version), filename)
if os.path.exists(previous_config_file):
shutil.copyfile(previous_config_file, self._config_file)
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)
# 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)
else:
# create a new config
with open(self._config_file, "w", encoding="utf-8") as f:
@@ -102,6 +119,7 @@ class LocalConfig(QtCore.QObject):
user_settings = self._readConfig(self._config_file)
# overwrite system wide settings with user specific ones
self._settings.update(user_settings)
self._migrateOldConfig()
self.writeConfig()
def profile(self):
@@ -120,13 +138,12 @@ class LocalConfig(QtCore.QObject):
self._config_file = None
self._resetLoadConfig()
def configDirectory(self, version=None):
def configDirectory(self):
"""
Get the configuration directory
"""
if not version:
version = "{}.{}".format(__version_info__[0], __version_info__[1])
version = "{}.{}".format(__version_info__[0], __version_info__[1])
if sys.platform.startswith("win"):
appdata = os.path.expandvars("%APPDATA%")
path = os.path.join(appdata, "GNS3", version)
@@ -151,6 +168,93 @@ class LocalConfig(QtCore.QObject):
"""
return os.path.join(self.configDirectory(), "run_as_root")
def _migrateOldConfigPath(self):
"""
Migrate pre 1.4 config path
"""
# In < 1.4 on Mac the config was in a gns3.net directory
# We have move to same location as Linux
if sys.platform.startswith("darwin"):
version = "{}.{}".format(__version_info__[0], __version_info__[1])
old_path = os.path.join(os.path.expanduser("~"), ".config", "gns3.net")
new_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3", version)
if os.path.exists(old_path) and not os.path.exists(new_path):
try:
shutil.copytree(old_path, new_path)
except OSError as e:
log.error("Can't copy the old config: %s", str(e))
def _migrateOldConfig(self):
"""
Migrate config from a previous version.
"""
# Display an error if settings come from a more recent version of GNS3
# patch level version are compatible (ex 1.5.3 and 1.5.2). But if you open
# settings from 1.6.1 with 1.5.1 you will have an error
if "version" in self._settings:
if parse_version(self._settings["version"])[:2] > parse_version(__version__)[:2]:
app = QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
error_message = "Settings are for version {} of GNS3. It is not possible to use a previous version of GNS3 without risking losing data. Delete the settings in '{}' to start GNS3".format(self._settings["version"], self.configDirectory())
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()
sys.exit(1)
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.0alpha1"):
servers = self._settings.get("Servers", {})
if "LocalServer" in self._settings:
servers["local_server"] = copy.copy(self._settings["LocalServer"])
# We migrate the server binary for OSX due to the change from py2app to CX freeze
if servers["local_server"]["path"] == "/Applications/GNS3.app/Contents/Resources/server/Contents/MacOS/gns3server":
servers["local_server"]["path"] = "gns3server"
if "RemoteServers" in self._settings:
servers["remote_servers"] = copy.copy(self._settings["RemoteServers"])
self._settings["Servers"] = servers
if "GUI" in self._settings:
main_window = self._settings.get("MainWindow", {})
main_window["hide_getting_started_dialog"] = self._settings["GUI"].get("hide_getting_started_dialog", False)
self._settings["MainWindow"] = main_window
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.1dev2"):
if sys.platform.startswith("darwin"):
from .settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, DEFAULT_TELNET_CONSOLE_COMMAND
if "MainWindow" in self._settings:
if self._settings["MainWindow"].get("telnet_console_command") not in PRECONFIGURED_TELNET_CONSOLE_COMMANDS.values():
self._settings["MainWindow"]["telnet_console_command"] = DEFAULT_TELNET_CONSOLE_COMMAND
# Migrate 1.X to 2.0
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
if "Qemu" in self._settings:
# The internet VM is replaced by the nat Node
# we remove it from the list of available VM
vms = []
for vm in self._settings["Qemu"].get("vms", []):
if vm.get("hda_disk_image") != "core-linux-6.4-internet-0.1.img":
vms.append(vm)
self._settings["Qemu"]["vms"] = vms
# Starting with 2.0.0dev5 IOU licence is stored in the settings
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
if "IOU" in self._settings and "iourc_path" in self._settings["IOU"] and "iourc_content" not in self._settings["IOU"]:
try:
with open(self._settings["IOU"]["iourc_path"], "r", encoding="utf-8") as f:
self._settings["IOU"]["iourc_content"] = f.read().replace("\r\n", "\n")
del self._settings["IOU"]["iourc_path"]
except OSError as e:
log.warning("Can't import IOU licence {}: {}".format(self._settings["IOU"]["iourc_path"], str(e)))
except UnicodeDecodeError as e:
log.warning("Non ascii characters in iourc file {}, please remove them: {}".format(self._settings["IOU"]["iourc_path"], str(e)))
def _readConfig(self, config_path):
"""
Read the configuration file.
@@ -314,6 +418,20 @@ 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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python
#
# Copyright (C) 2021 GNS3 Technologies Inc.
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -22,6 +22,8 @@ import stat
import shlex
import socket
import shutil
import random
import string
import struct
import psutil
import signal
@@ -29,12 +31,13 @@ import subprocess
from gns3.qt import QtWidgets, QtCore, qslot
from gns3.settings import DEFAULT_CONTROLLER_HOST
from gns3.settings import LOCAL_SERVER_SETTINGS, DEFAULT_LOCAL_SERVER_HOST
from gns3.local_config import LocalConfig
from gns3.http_client import HTTPClient
from gns3.local_server_config import LocalServerConfig
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
from gns3.utils.progress_dialog import ProgressDialog
from gns3.utils.sudo import sudo
from gns3.http_client import HTTPClient
from gns3.controller import Controller
@@ -91,6 +94,13 @@ 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)
@@ -112,6 +122,27 @@ 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.
@@ -172,6 +203,12 @@ 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.
@@ -179,9 +216,14 @@ class LocalServer(QtCore.QObject):
:returns: local server settings (dict)
"""
settings = Controller.instance().settings()
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
self._settings = copy.copy(settings)
# user & password
if settings["auth"] is True and not settings["user"].strip():
settings["user"] = "admin"
settings["password"] = self._passwordGenerate()
# local GNS3 server path
local_server_path = shutil.which(settings["path"].strip())
if local_server_path is None:
@@ -210,17 +252,17 @@ class LocalServer(QtCore.QObject):
"""
if "host" in new_settings and new_settings["host"] is None:
new_settings["host"] = DEFAULT_CONTROLLER_HOST
new_settings["host"] = DEFAULT_LOCAL_SERVER_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"]
Controller.instance().setSettings(self._settings)
LocalServerConfig.instance().saveSettings("Server", self._settings)
# Settings have changed we need to restart the server
if not Controller.instance().connected() or old_settings != self._settings:
if 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')
@@ -236,8 +278,12 @@ class LocalServer(QtCore.QObject):
# If the controller is remote:
else:
self.stopLocalServer(wait=True)
if Controller.instance().isRemote() and not Controller.instance().connected():
Controller.instance().connect()
if self._settings.get("host") is None:
self._http_client = None
else:
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
def shouldLocalServerAutoStart(self):
"""
@@ -279,24 +325,29 @@ class LocalServer(QtCore.QObject):
Try to start the embedded gns3 server.
"""
local_server_already_running = self.isLocalServerRunning()
if local_server_already_running and self._server_started_by_me:
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:
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")
Controller.instance().connect()
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
return True
if local_server_already_running:
if self.isLocalServerRunning():
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 local_server_already_running:
if not self.isLocalServerRunning():
if not self.initLocalServer():
QtWidgets.QMessageBox.critical(self.parent(), "Local server", "Could not start the local server process: {}".format(self._settings["path"]))
return False
@@ -307,14 +358,15 @@ class LocalServer(QtCore.QObject):
if self.parent():
worker = WaitForConnectionWorker(self._settings["host"], self._port)
progress_dialog = ProgressDialog(worker,
"Local controller",
"Starting local controller {} on port {}...".format(self._settings["host"], self._port),
"Local server",
"Connecting to server {} on port {}...".format(self._settings["host"], self._port),
"Cancel", busy=True, parent=self.parent())
progress_dialog.show()
if not progress_dialog.exec():
return False
self._server_started_by_me = True
Controller.instance().connect()
self._http_client = HTTPClient(self._settings)
Controller.instance().setHttpClient(self._http_client)
return True
def initLocalServer(self):
@@ -323,6 +375,15 @@ 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()
@@ -415,7 +476,7 @@ class LocalServer(QtCore.QObject):
pass
except OSError as e:
log.warning("could not delete server log file {}: {}".format(logpath, e))
command += ' --logfile="{}" --pid="{}"'.format(logpath, self._pid_path())
command += ' --log="{}" --pid="{}"'.format(logpath, self._pid_path())
log.debug("Starting local server process with {}".format(command))
try:
@@ -468,8 +529,19 @@ class LocalServer(QtCore.QObject):
:returns: boolean
"""
http_client = HTTPClient(self._settings)
return http_client.checkServerRunning()
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
def stopLocalServer(self, wait=False):
"""
@@ -480,14 +552,13 @@ class LocalServer(QtCore.QObject):
if self.localServerProcessIsRunning():
self._stopping = True
log.debug("Stopping local controller (PID={})".format(self._local_server_process.pid))
log.debug("Stopping local server (PID={})".format(self._local_server_process.pid))
# local server is running, let's stop it
http_client = Controller.instance().httpClient()
if http_client:
http_client.shutdown()
if self._http_client:
self._http_client.shutdown()
if wait:
worker = StopLocalServerWorker(self._local_server_process)
progress_dialog = ProgressDialog(worker, "Local server", "Waiting for the local controller to stop...", None, busy=True, parent=self.parent())
progress_dialog = ProgressDialog(worker, "Local server", "Waiting for the local server to stop...", None, busy=True, parent=self.parent())
progress_dialog.show()
progress_dialog.exec()
if self._local_server_process.returncode is None:
@@ -510,8 +581,8 @@ class LocalServer(QtCore.QObject):
self._local_server_process.wait(timeout=60)
except subprocess.TimeoutExpired:
proceed = QtWidgets.QMessageBox.question(self.parent(),
"Local controller",
"The local controller cannot be stopped, would you like to kill it?",
"Local server",
"The Local server cannot be stopped, would you like to kill it?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No)

163
gns3/local_server_config.py Normal file
View File

@@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import configparser
import logging
log = logging.getLogger(__name__)
class LocalServerConfig:
"""
Local server configuration.
"""
def __init__(self, config_file=None):
appname = "GNS3"
self._config = configparser.RawConfigParser()
if config_file:
self._config_file = config_file
else:
if sys.platform.startswith("win"):
filename = "gns3_server.ini"
else:
filename = "gns3_server.conf"
from .local_config import LocalConfig
if sys.platform.startswith("win"):
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
else:
self._config_file = os.path.join(LocalConfig.instance().configDirectory(), filename)
try:
# create the config file if it doesn't exist
open(self._config_file, "a").close()
except OSError as e:
log.error("Could not create the local server configuration {}: {}".format(self._config_file, e))
self.readConfig()
def setConfigFile(self, path):
"""
Change the location of the server config (use for test)
"""
self._config = configparser.RawConfigParser()
self._config_file = path
self.readConfig()
def readConfig(self):
"""
Read the configuration file.
"""
try:
self._config.read(self._config_file, encoding="utf-8")
except (OSError, configparser.Error, UnicodeEncodeError, UnicodeDecodeError) as e:
log.error("Could not read the local server configuration {}: {}".format(self._config_file, e))
def writeConfig(self):
"""
Write the configuration file.
"""
try:
log.debug("Write configuration file %s", self._config_file)
with open(self._config_file, "w", encoding="utf-8") as fp:
self._config.write(fp)
except (OSError, configparser.Error) as e:
log.error("Could not write the local server configuration {}: {}".format(self._config_file, e))
def loadSettings(self, section, default_settings):
"""
Get all the settings from a given section.
:param section: section name
:param default_settings: setting names and default values (dict)
:returns: settings (dict)
"""
if section not in self._config:
self._config[section] = {}
settings = {}
for name, default in default_settings.items():
if isinstance(default, bool):
settings[name] = self._config[section].getboolean(name, default)
elif isinstance(default, int):
settings[name] = self._config[section].getint(name, default)
elif isinstance(default, float):
settings[name] = self._config[section].getfloat(name, default)
else:
settings[name] = self._config[section].get(name, default)
if settings[name] == "None":
settings[name] = None
# sync with the config file
self.saveSettings(section, settings)
return settings
def saveSettings(self, section, settings):
"""
Save all the settings in a given section.
:param section: section name
:param settings: settings to save (dict)
"""
changed = False
if section not in self._config:
self._config[section] = {}
changed = True
for name, value in settings.items():
if name not in self._config[section] or self._config[section][name] != str(value):
self._config[section][name] = str(value)
changed = True
if changed:
self.writeConfig()
def deleteSetting(self, section, name):
"""
Delete a specific setting in a given section.
:param section: section name
:param name: setting name to delete
"""
if section in self._config and name in self._config[section]:
del self._config[section][name]
self.writeConfig()
@staticmethod
def instance():
"""
Singleton to return only on instance of LocalServerConfig.
:returns: instance of Config
"""
if not hasattr(LocalServerConfig, "_instance"):
LocalServerConfig._instance = LocalServerConfig()
return LocalServerConfig._instance

View File

@@ -48,7 +48,6 @@ 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__
@@ -119,14 +118,9 @@ 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)
@@ -149,6 +143,7 @@ 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", "")

View File

@@ -39,7 +39,6 @@ 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
@@ -86,7 +85,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.setupUi(self)
self.setUnifiedTitleAndToolBarOnMac(True)
# This widgets will be disabled when you have no project loaded
# These widgets will be disabled when no project is loaded
self.disableWhenNoProjectWidgets = [
self.uiGraphicsView,
self.uiAnnotateMenu,
@@ -100,27 +99,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiEditProjectAction,
self.uiDeleteProjectAction,
self.uiImportExportConfigsAction,
self.uiLockAllAction,
self.uiShowReadmeAction
self.uiLockAllAction
]
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))
@@ -135,6 +116,8 @@ 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()
@@ -188,7 +171,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
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):
@@ -215,7 +198,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
supported_image_formats = [fmt.data().decode('utf-8') for fmt in QtGui.QImageReader().supportedImageFormats()]
log.debug("Supported image formats: %s", ", ".join(supported_image_formats))
if "svg" not in supported_image_formats:
log.warning("SVG image format is not supported, is the Qt SVG module installed? (qt6-svg-plugins)")
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)
@@ -230,7 +213,6 @@ 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)
@@ -255,7 +237,6 @@ 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)
@@ -354,7 +335,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def _openWebInterfaceActionSlot(self):
if Controller.instance().connected():
base_url = Controller.instance().httpClient().fullUrl()
webui_url = f"{base_url}/static/web-ui/bundled"
webui_url = "{}/static/web-ui/bundled".format(base_url)
QtGui.QDesktopServices.openUrl(QtCore.QUrl(webui_url))
def _showGridActionSlot(self):
@@ -448,15 +429,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
dialog.show()
dialog.exec()
def _imageManagementActionSlot(self):
"""
Called when user wants to manage images
"""
dialog = ImageDialog(self)
dialog.show()
dialog.exec()
@qslot
def openApplianceActionSlot(self, *args):
"""
@@ -553,7 +525,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
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 project (.gns3p) instead")
QtWidgets.QMessageBox.critical(self, "Open project", "Cannot open a .gns3 file on a remote server, please use a portable project (.gns3p) instead")
return
else:
Topology.instance().loadProject(path)
@@ -593,9 +565,6 @@ 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:
@@ -1015,9 +984,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot to open the setup wizard.
"""
setup_wizard = SetupWizard(self)
setup_wizard.show()
setup_wizard.exec()
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()
def _shortcutsActionSlot(self):
@@ -1160,12 +1132,6 @@ 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)
@@ -1306,11 +1272,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if not self._settings["hide_setup_wizard"]:
self._setupWizardActionSlot()
else:
if Controller.instance().isRemote():
Controller.instance().connect()
else:
# start and connect to the local server if needed
LocalServer.instance().localServerAutoStartIfRequired()
# 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 self._settings["check_for_update"]:
# automatic check for update every week (604800 seconds)
@@ -1463,17 +1431,9 @@ 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
@@ -1486,20 +1446,20 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def _exportProjectActionSlot(self):
"""
Slot called to export a project
Slot called to export a portable project
"""
Topology.instance().exportProject()
def _importProjectActionSlot(self):
"""
Slot called to import a project
Slot called to import a portable project
"""
directory = self._portable_project_dir
if not os.path.exists(directory):
directory = Topology.instance().projectsDirPath()
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open project", directory,
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open portable project", directory,
"All files (*);;GNS3 Portable Project (*.gns3project *.gns3p)",
"GNS3 Portable Project (*.gns3project *.gns3p)")
if path:

View File

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

View File

@@ -19,7 +19,9 @@
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
@@ -51,15 +53,14 @@ class Builtin(Module):
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
# 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")
server_settings = {}
config = LocalServerConfig.instance()
if self._settings["default_nat_interface"]:
# save some settings to the local server config file
server_settings["default_nat_interface"] = self._settings["default_nat_interface"]
config.saveSettings(self.__class__.__name__, server_settings)
else:
config.deleteSetting(self.__class__.__name__, "default_nat_interface")
def _loadSettings(self):
"""

View File

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

View File

@@ -42,7 +42,6 @@ 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"],
@@ -140,8 +139,7 @@ class Cloud(Node):
port_info += " Port {name} {description}\n".format(name=port.name(),
description=port.description())
usage = "\n" + self._settings.get("usage")
return info + port_info + usage
return info + port_info
def configPage(self):
"""

View File

@@ -48,7 +48,7 @@ class CloudWizard(VMWizard, Ui_CloudNodeWizard):
"""
settings = {"name": self.uiNameLineEdit.text(),
"symbol": "cloud",
"symbol": ":/symbols/cloud.svg",
"compute_id": self._compute_id}
return settings

View File

@@ -54,7 +54,7 @@ class EthernetHubWizard(VMWizard, Ui_EthernetHubWizard):
"name": "Ethernet{}".format(port_number)})
settings = {"name": self.uiNameLineEdit.text(),
"symbol": "hub",
"symbol": ":/symbols/hub.svg",
"category": Node.switches,
"compute_id": self._compute_id,
"ports_mapping": ports}

View File

@@ -54,10 +54,10 @@ class EthernetSwitchWizard(VMWizard, Ui_EthernetSwitchWizard):
"name": "Ethernet{}".format(port_number),
"type": "access",
"vlan": 1,
"ethertype": "0x8100"})
"ethertype": ""})
settings = {"name": self.uiNameLineEdit.text(),
"symbol": "ethernet_switch",
"symbol": ":/symbols/ethernet_switch.svg",
"category": Node.switches,
"compute_id": self._compute_id,
"ports_mapping": ports}

View File

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

View File

@@ -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 node ID is {node_id}
Local ID is {id} and server ID is {node_id}
Console is on port {console} and type is {console_type}
""".format(name=self.name(),
id=self.id(),

View File

@@ -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 node ID is {node_id}
Local ID is {id} and server ID is {node_id}
Hardware is Dynamips emulated simple Frame relay switch
""".format(name=self.name(),
id=self.id(),

View File

@@ -473,13 +473,9 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
if index != -1:
self.uiCategoryComboBox.setCurrentIndex(index)
Controller.instance().getCompute(
"/network/interfaces",
settings["compute_id"],
self._getInterfacesFromServerCallback,
progress_text="Retrieving network interfaces...",
wait=True
)
Controller.instance().getCompute("/network/interfaces", settings["compute_id"],
self._getInterfacesFromServerCallback,
progressText="Retrieving network interfaces...")
else:
self.uiDefaultNameFormatLabel.hide()
@@ -494,7 +490,6 @@ 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()
@@ -565,6 +560,4 @@ class CloudConfigurationPage(QtWidgets.QWidget, Ui_cloudConfigPageWidget):
settings["ports_mapping"] = self._ports
else:
settings["ports_mapping"] = self._ports
settings["usage"] = self.uiUsageTextEdit.toPlainText()
return settings

View File

@@ -87,15 +87,7 @@ 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:
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])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(cloud_node["compute_id"]).name()])
except KeyError:
pass

View File

@@ -83,15 +83,7 @@ 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:
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])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_hub["compute_id"]).name()])
except KeyError:
pass
QtWidgets.QTreeWidgetItem(section_item, ["Number of ports:", str(len(ethernet_hub["ports_mapping"]))])

View File

@@ -127,15 +127,14 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
if port_type == "qinq":
port_ethertype = self.uiPortEtherTypeComboBox.currentText()
else:
port_ethertype = "0x8100"
port_ethertype = ""
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.setText(1, str(vlan))
item.setText(2, port_type)
if port_ethertype:
item.setText(3, port_ethertype)
item.setText(3, port_ethertype)
else:
# add a new entry in the tree widget
@@ -143,8 +142,7 @@ class EthernetSwitchConfigurationPage(QtWidgets.QWidget, Ui_ethernetSwitchConfig
item.setText(0, str(port))
item.setText(1, str(vlan))
item.setText(2, port_type)
if port_ethertype:
item.setText(3, port_ethertype)
item.setText(3, port_ethertype)
self.uiPortsTreeWidget.addTopLevelItem(item)
self._ports[port] = {"name": "Ethernet{}".format(port),
@@ -226,9 +224,7 @@ 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"))
port_ethertype = port_info.get("ethertype")
if port_ethertype:
item.setText(3, port_ethertype)
item.setText(3, port_info.get("ethertype", ""))
self.uiPortsTreeWidget.addTopLevelItem(item)
self._ports[port_info["port_number"]] = port_info

View File

@@ -82,15 +82,7 @@ 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:
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])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ethernet_switch["compute_id"]).name()])
except KeyError:
pass

View File

@@ -34,13 +34,12 @@ 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": "cloud",
"symbol": ":/symbols/cloud.svg",
"category": Node.end_devices,
"ports_mapping": [],
"node_type": "cloud"
@@ -49,7 +48,7 @@ CLOUD_SETTINGS = {
ETHERNET_HUB_SETTINGS = {
"name": "",
"default_name_format": "Hub{0}",
"symbol": "hub",
"symbol": ":/symbols/hub.svg",
"category": Node.switches,
"ports_mapping": [],
"node_type": "ethernet_hub"
@@ -58,7 +57,7 @@ ETHERNET_HUB_SETTINGS = {
ETHERNET_SWITCH_SETTINGS = {
"name": "",
"default_name_format": "Switch{0}",
"symbol": "ethernet_switch",
"symbol": ":/symbols/ethernet_switch.svg",
"category": Node.switches,
"console_type": "none",
"ports_mapping": [],

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/atm_switch_configuration_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/builtin_preferences_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1034</width>
<height>575</height>
<width>979</width>
<height>564</height>
</rect>
</property>
<property name="windowTitle">
@@ -555,16 +555,6 @@
<zorder>uiConsoleTypeLabel</zorder>
<zorder>uiConsoleTypeComboBox</zorder>
</widget>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Usage</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QPlainTextEdit" name="uiUsageTextEdit"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_configuration_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
@@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_cloudConfigPageWidget(object):
def setupUi(self, cloudConfigPageWidget):
cloudConfigPageWidget.setObjectName("cloudConfigPageWidget")
cloudConfigPageWidget.resize(1034, 575)
cloudConfigPageWidget.resize(979, 564)
self.verticalLayout = QtWidgets.QVBoxLayout(cloudConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(parent=cloudConfigPageWidget)
@@ -282,14 +282,6 @@ class Ui_cloudConfigPageWidget(object):
self.uiConsoleTypeLabel.raise_()
self.uiConsoleTypeComboBox.raise_()
self.uiTabWidget.addTab(self.MiscTab, "")
self.tab = QtWidgets.QWidget()
self.tab.setObjectName("tab")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.tab)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.uiUsageTextEdit = QtWidgets.QPlainTextEdit(parent=self.tab)
self.uiUsageTextEdit.setObjectName("uiUsageTextEdit")
self.verticalLayout_3.addWidget(self.uiUsageTextEdit)
self.uiTabWidget.addTab(self.tab, "")
self.verticalLayout.addWidget(self.uiTabWidget)
self.retranslateUi(cloudConfigPageWidget)
@@ -344,4 +336,3 @@ class Ui_cloudConfigPageWidget(object):
self.uiConsoleTypeComboBox.setItemText(4, _translate("cloudConfigPageWidget", "https"))
self.uiConsoleTypeComboBox.setItemText(5, _translate("cloudConfigPageWidget", "none"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.MiscTab), _translate("cloudConfigPageWidget", "Misc."))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab), _translate("cloudConfigPageWidget", "Usage"))

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_preferences_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/cloud_wizard.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/ethernet_hub_configuration_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/ethernet_hub_preferences_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/ethernet_hub_wizard.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/ethernet_switch_configuration_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/ethernet_switch_preferences_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/ethernet_switch_wizard.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/builtin/ui/frame_relay_switch_configuration_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -39,7 +39,7 @@ class DockerVMWizard(VMWizard, Ui_DockerVMWizard):
super().__init__(docker_containers, parent)
self._docker_containers = docker_containers
self.setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(":/symbols/docker_guest.svg"))
self.setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(":/icons/docker.png"))
self.uiNewImageRadioButton.setChecked(True)
self._existingImageRadioButtonToggledSlot(False)

View File

@@ -46,17 +46,14 @@ class DockerVM(Node):
"custom_adapters": DOCKER_CONTAINER_SETTINGS["custom_adapters"],
"start_command": DOCKER_CONTAINER_SETTINGS["start_command"],
"environment": DOCKER_CONTAINER_SETTINGS["environment"],
"aux": None,
"console_type": DOCKER_CONTAINER_SETTINGS["console_type"],
"console_auto_start": DOCKER_CONTAINER_SETTINGS["console_auto_start"],
"aux_type": DOCKER_CONTAINER_SETTINGS["aux_type"],
"console_resolution": DOCKER_CONTAINER_SETTINGS["console_resolution"],
"console_http_port": DOCKER_CONTAINER_SETTINGS["console_http_port"],
"console_http_path": DOCKER_CONTAINER_SETTINGS["console_http_path"],
"extra_hosts": DOCKER_CONTAINER_SETTINGS["extra_hosts"],
"extra_volumes": DOCKER_CONTAINER_SETTINGS["extra_volumes"],
"memory": DOCKER_CONTAINER_SETTINGS["memory"],
"cpus": DOCKER_CONTAINER_SETTINGS["cpus"],
}
"extra_volumes": DOCKER_CONTAINER_SETTINGS["extra_volumes"]}
self.settings().update(docker_vm_settings)
@@ -69,10 +66,9 @@ class DockerVM(Node):
info = """Docker container {name} is {state}
Running on server {host} with port {port}
Local ID is {id} and node ID is {node_id}
Local ID is {id} and server ID is {node_id}
Docker image is "{image}"
Console is on port {console} and type is {console_type}
Aux console is on port {aux} and type is {aux_type}
""".format(name=self.name(),
id=self.id(),
node_id=self._node_id,
@@ -81,8 +77,6 @@ class DockerVM(Node):
port=self.compute().port(),
console=self._settings["console"],
console_type=self._settings["console_type"],
aux=self._settings["aux"],
aux_type=self._settings["aux_type"],
image=self._settings["image"])
port_info = ""
@@ -126,6 +120,18 @@ class DockerVM(Node):
from .pages.docker_vm_configuration_page import DockerVMConfigurationPage
return DockerVMConfigurationPage
@staticmethod
def validateHostname(hostname):
"""
Checks if the hostname is valid.
:param hostname: hostname to check
:returns: boolean
"""
return DockerVM.isValidRfc1123Hostname(hostname)
@staticmethod
def defaultSymbol():
"""

View File

@@ -112,14 +112,11 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
self.uiEnvironmentTextEdit.setText(settings["environment"])
self.uiConsoleTypeComboBox.setCurrentIndex(self.uiConsoleTypeComboBox.findText(settings["console_type"]))
self.uiConsoleAutoStartCheckBox.setChecked(settings["console_auto_start"])
self.uiAuxTypeComboBox.setCurrentIndex(self.uiAuxTypeComboBox.findText(settings["aux_type"]))
self.uiConsoleResolutionComboBox.setCurrentIndex(self.uiConsoleResolutionComboBox.findText(settings["console_resolution"]))
self.uiConsoleHttpPortSpinBox.setValue(settings["console_http_port"])
self.uiHttpConsolePathLineEdit.setText(settings["console_http_path"])
self.uiExtraHostsTextEdit.setPlainText(settings["extra_hosts"])
self.uiExtraVolumeTextEdit.setPlainText("\n".join(settings["extra_volumes"]))
self.uiMaxMemorySpinBox.setValue(settings["memory"])
self.uiMaxCPUsDoubleSpinBox.setValue(settings["cpus"])
if not group:
self.uiNameLineEdit.setText(settings["name"])
@@ -194,15 +191,12 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
settings["environment"] = self.uiEnvironmentTextEdit.toPlainText()
settings["console_type"] = self.uiConsoleTypeComboBox.currentText()
settings["console_auto_start"] = self.uiConsoleAutoStartCheckBox.isChecked()
settings["aux_type"] = self.uiAuxTypeComboBox.currentText()
settings["console_resolution"] = self.uiConsoleResolutionComboBox.currentText()
settings["console_http_port"] = self.uiConsoleHttpPortSpinBox.value()
settings["console_http_path"] = self.uiHttpConsolePathLineEdit.text()
settings["extra_hosts"] = self.uiExtraHostsTextEdit.toPlainText()
# only tidy input here, validation is performed server side
settings["extra_volumes"] = [ y for x in self.uiExtraVolumeTextEdit.toPlainText().split("\n") for y in [ x.strip() ] if y ]
settings["memory"] = self.uiMaxMemorySpinBox.value()
settings["cpus"] = round(self.uiMaxCPUsDoubleSpinBox.value(), self.uiMaxCPUsDoubleSpinBox.decimals())
if not group:
adapters = self.uiAdapterSpinBox.value()

View File

@@ -81,32 +81,21 @@ class DockerVMPreferencesPage(QtWidgets.QWidget, Ui_DockerVMPreferencesPageWidge
QtWidgets.QTreeWidgetItem(section_item, ["Template ID:", docker_container.get("template_id", "none")])
QtWidgets.QTreeWidgetItem(section_item, ["Image name:", docker_container["image"]])
try:
compute_id = docker_container.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])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(docker_container["compute_id"]).name()])
except KeyError:
pass
QtWidgets.QTreeWidgetItem(section_item, ["Console type:", str(docker_container["console_type"])])
QtWidgets.QTreeWidgetItem(section_item, ["Auto start console:", "{}".format(docker_container["console_auto_start"])])
QtWidgets.QTreeWidgetItem(section_item, ["Auxiliary console type:", str(docker_container["aux_type"])])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", docker_container["default_name_format"]])
QtWidgets.QTreeWidgetItem(section_item, ["Adapters:", str(docker_container["adapters"])])
if docker_container["cpus"]:
QtWidgets.QTreeWidgetItem(section_item, ["CPUs:", str(docker_container["cpus"])])
if docker_container["memory"]:
QtWidgets.QTreeWidgetItem(section_item, ["Memory:", str(docker_container["memory"]) + " MB"])
if docker_container["start_command"]:
QtWidgets.QTreeWidgetItem(section_item, ["Start command:", str(docker_container["start_command"])])
if docker_container["environment"]:
QtWidgets.QTreeWidgetItem(section_item, ["Environment:", str(docker_container["environment"])])
if docker_container["extra_hosts"]:
QtWidgets.QTreeWidgetItem(section_item, ["Extra hosts:", str(docker_container["extra_hosts"])])
if docker_container["extra_volumes"]:
QtWidgets.QTreeWidgetItem(section_item, ["Extra volumes:", "\n".join(docker_container["extra_volumes"])])

View File

@@ -29,7 +29,7 @@ DOCKER_SETTINGS = {
DOCKER_CONTAINER_SETTINGS = {
"default_name_format": "{name}-{0}",
"usage": "",
"symbol": "docker_guest",
"symbol": ":/symbols/docker_guest.svg",
"category": Node.end_devices,
"start_command": "",
"name": "",
@@ -40,13 +40,10 @@ DOCKER_CONTAINER_SETTINGS = {
"environment": "",
"console_type": "telnet",
"console_auto_start": False,
"aux_type": "none",
"console_resolution": "1024x768",
"console_http_port": 80,
"console_http_path": "/",
"extra_hosts": "",
"extra_volumes": [],
"memory": 0,
"cpus": 0,
"node_type": "docker"
}

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/docker/ui/docker_preferences_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>649</width>
<height>718</height>
<width>504</width>
<height>560</height>
</rect>
</property>
<property name="windowTitle">
@@ -127,50 +127,13 @@
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="uiMaxMemoryLabel">
<property name="text">
<string>Maximum memory:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QSpinBox" name="uiMaxMemorySpinBox">
<property name="suffix">
<string> MB</string>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="singleStep">
<number>32</number>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="uiMaxCPUsLabel">
<property name="text">
<string>Maximum CPUs:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QDoubleSpinBox" name="uiMaxCPUsDoubleSpinBox">
<property name="decimals">
<number>1</number>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="uiConsoleTypeLabel">
<property name="text">
<string>Console type:</string>
</property>
</widget>
</item>
<item row="10" column="1">
<item row="8" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetNoConstraint</enum>
@@ -213,35 +176,14 @@
</item>
</layout>
</item>
<item row="11" column="0">
<widget class="QLabel" name="uiAuxTypeLabel">
<property name="text">
<string>Auxiliary console type:</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QComboBox" name="uiAuxTypeComboBox">
<item>
<property name="text">
<string>telnet</string>
</property>
</item>
<item>
<property name="text">
<string>none</string>
</property>
</item>
</widget>
</item>
<item row="12" column="0">
<item row="9" column="0">
<widget class="QLabel" name="uiConsoleResolutionLabel">
<property name="text">
<string>VNC console resolution:</string>
</property>
</widget>
</item>
<item row="12" column="1">
<item row="9" column="1">
<widget class="QComboBox" name="uiConsoleResolutionComboBox">
<item>
<property name="text">
@@ -295,14 +237,14 @@
</item>
</widget>
</item>
<item row="13" column="0">
<item row="10" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>HTTP port in the container:</string>
</property>
</widget>
</item>
<item row="13" column="1">
<item row="10" column="1">
<widget class="QSpinBox" name="uiConsoleHttpPortSpinBox">
<property name="minimum">
<number>1</number>
@@ -312,17 +254,17 @@
</property>
</widget>
</item>
<item row="14" column="0">
<item row="11" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>HTTP path:</string>
</property>
</widget>
</item>
<item row="14" column="1">
<item row="11" column="1">
<widget class="QLineEdit" name="uiHttpConsolePathLineEdit"/>
</item>
<item row="15" column="0">
<item row="12" column="0">
<widget class="QLabel" name="uiEnvironmentLabel">
<property name="text">
<string>Environment variables:
@@ -336,17 +278,17 @@
</property>
</widget>
</item>
<item row="15" column="1">
<item row="12" column="1">
<widget class="QTextEdit" name="uiEnvironmentTextEdit"/>
</item>
<item row="16" column="0">
<item row="13" column="0">
<widget class="QLabel" name="uiNetworkConfigLabel">
<property name="text">
<string>Network configuration</string>
</property>
</widget>
</item>
<item row="16" column="1">
<item row="13" column="1">
<widget class="QPushButton" name="uiNetworkConfigEditButton">
<property name="text">
<string>Edit</string>

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/docker/ui/docker_vm_configuration_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
@@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_dockerVMConfigPageWidget(object):
def setupUi(self, dockerVMConfigPageWidget):
dockerVMConfigPageWidget.setObjectName("dockerVMConfigPageWidget")
dockerVMConfigPageWidget.resize(649, 718)
dockerVMConfigPageWidget.resize(504, 560)
self.verticalLayout = QtWidgets.QVBoxLayout(dockerVMConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiTabWidget = QtWidgets.QTabWidget(parent=dockerVMConfigPageWidget)
@@ -77,25 +77,9 @@ class Ui_dockerVMConfigPageWidget(object):
self.uiCustomAdaptersConfigurationPushButton = QtWidgets.QPushButton(parent=self.tab)
self.uiCustomAdaptersConfigurationPushButton.setObjectName("uiCustomAdaptersConfigurationPushButton")
self.gridLayout.addWidget(self.uiCustomAdaptersConfigurationPushButton, 7, 1, 1, 1)
self.uiMaxMemoryLabel = QtWidgets.QLabel(parent=self.tab)
self.uiMaxMemoryLabel.setObjectName("uiMaxMemoryLabel")
self.gridLayout.addWidget(self.uiMaxMemoryLabel, 8, 0, 1, 1)
self.uiMaxMemorySpinBox = QtWidgets.QSpinBox(parent=self.tab)
self.uiMaxMemorySpinBox.setMaximum(65535)
self.uiMaxMemorySpinBox.setSingleStep(32)
self.uiMaxMemorySpinBox.setObjectName("uiMaxMemorySpinBox")
self.gridLayout.addWidget(self.uiMaxMemorySpinBox, 8, 1, 1, 1)
self.uiMaxCPUsLabel = QtWidgets.QLabel(parent=self.tab)
self.uiMaxCPUsLabel.setObjectName("uiMaxCPUsLabel")
self.gridLayout.addWidget(self.uiMaxCPUsLabel, 9, 0, 1, 1)
self.uiMaxCPUsDoubleSpinBox = QtWidgets.QDoubleSpinBox(parent=self.tab)
self.uiMaxCPUsDoubleSpinBox.setDecimals(1)
self.uiMaxCPUsDoubleSpinBox.setSingleStep(0.1)
self.uiMaxCPUsDoubleSpinBox.setObjectName("uiMaxCPUsDoubleSpinBox")
self.gridLayout.addWidget(self.uiMaxCPUsDoubleSpinBox, 9, 1, 1, 1)
self.uiConsoleTypeLabel = QtWidgets.QLabel(parent=self.tab)
self.uiConsoleTypeLabel.setObjectName("uiConsoleTypeLabel")
self.gridLayout.addWidget(self.uiConsoleTypeLabel, 10, 0, 1, 1)
self.gridLayout.addWidget(self.uiConsoleTypeLabel, 8, 0, 1, 1)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetNoConstraint)
self.horizontalLayout.setObjectName("horizontalLayout")
@@ -110,18 +94,10 @@ class Ui_dockerVMConfigPageWidget(object):
self.uiConsoleAutoStartCheckBox = QtWidgets.QCheckBox(parent=self.tab)
self.uiConsoleAutoStartCheckBox.setObjectName("uiConsoleAutoStartCheckBox")
self.horizontalLayout.addWidget(self.uiConsoleAutoStartCheckBox)
self.gridLayout.addLayout(self.horizontalLayout, 10, 1, 1, 1)
self.uiAuxTypeLabel = QtWidgets.QLabel(parent=self.tab)
self.uiAuxTypeLabel.setObjectName("uiAuxTypeLabel")
self.gridLayout.addWidget(self.uiAuxTypeLabel, 11, 0, 1, 1)
self.uiAuxTypeComboBox = QtWidgets.QComboBox(parent=self.tab)
self.uiAuxTypeComboBox.setObjectName("uiAuxTypeComboBox")
self.uiAuxTypeComboBox.addItem("")
self.uiAuxTypeComboBox.addItem("")
self.gridLayout.addWidget(self.uiAuxTypeComboBox, 11, 1, 1, 1)
self.gridLayout.addLayout(self.horizontalLayout, 8, 1, 1, 1)
self.uiConsoleResolutionLabel = QtWidgets.QLabel(parent=self.tab)
self.uiConsoleResolutionLabel.setObjectName("uiConsoleResolutionLabel")
self.gridLayout.addWidget(self.uiConsoleResolutionLabel, 12, 0, 1, 1)
self.gridLayout.addWidget(self.uiConsoleResolutionLabel, 9, 0, 1, 1)
self.uiConsoleResolutionComboBox = QtWidgets.QComboBox(parent=self.tab)
self.uiConsoleResolutionComboBox.setObjectName("uiConsoleResolutionComboBox")
self.uiConsoleResolutionComboBox.addItem("")
@@ -134,35 +110,35 @@ class Ui_dockerVMConfigPageWidget(object):
self.uiConsoleResolutionComboBox.addItem("")
self.uiConsoleResolutionComboBox.addItem("")
self.uiConsoleResolutionComboBox.addItem("")
self.gridLayout.addWidget(self.uiConsoleResolutionComboBox, 12, 1, 1, 1)
self.gridLayout.addWidget(self.uiConsoleResolutionComboBox, 9, 1, 1, 1)
self.label = QtWidgets.QLabel(parent=self.tab)
self.label.setObjectName("label")
self.gridLayout.addWidget(self.label, 13, 0, 1, 1)
self.gridLayout.addWidget(self.label, 10, 0, 1, 1)
self.uiConsoleHttpPortSpinBox = QtWidgets.QSpinBox(parent=self.tab)
self.uiConsoleHttpPortSpinBox.setMinimum(1)
self.uiConsoleHttpPortSpinBox.setMaximum(65535)
self.uiConsoleHttpPortSpinBox.setObjectName("uiConsoleHttpPortSpinBox")
self.gridLayout.addWidget(self.uiConsoleHttpPortSpinBox, 13, 1, 1, 1)
self.gridLayout.addWidget(self.uiConsoleHttpPortSpinBox, 10, 1, 1, 1)
self.label_2 = QtWidgets.QLabel(parent=self.tab)
self.label_2.setObjectName("label_2")
self.gridLayout.addWidget(self.label_2, 14, 0, 1, 1)
self.gridLayout.addWidget(self.label_2, 11, 0, 1, 1)
self.uiHttpConsolePathLineEdit = QtWidgets.QLineEdit(parent=self.tab)
self.uiHttpConsolePathLineEdit.setObjectName("uiHttpConsolePathLineEdit")
self.gridLayout.addWidget(self.uiHttpConsolePathLineEdit, 14, 1, 1, 1)
self.gridLayout.addWidget(self.uiHttpConsolePathLineEdit, 11, 1, 1, 1)
self.uiEnvironmentLabel = QtWidgets.QLabel(parent=self.tab)
self.uiEnvironmentLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.uiEnvironmentLabel.setWordWrap(False)
self.uiEnvironmentLabel.setObjectName("uiEnvironmentLabel")
self.gridLayout.addWidget(self.uiEnvironmentLabel, 15, 0, 1, 1)
self.gridLayout.addWidget(self.uiEnvironmentLabel, 12, 0, 1, 1)
self.uiEnvironmentTextEdit = QtWidgets.QTextEdit(parent=self.tab)
self.uiEnvironmentTextEdit.setObjectName("uiEnvironmentTextEdit")
self.gridLayout.addWidget(self.uiEnvironmentTextEdit, 15, 1, 1, 1)
self.gridLayout.addWidget(self.uiEnvironmentTextEdit, 12, 1, 1, 1)
self.uiNetworkConfigLabel = QtWidgets.QLabel(parent=self.tab)
self.uiNetworkConfigLabel.setObjectName("uiNetworkConfigLabel")
self.gridLayout.addWidget(self.uiNetworkConfigLabel, 16, 0, 1, 1)
self.gridLayout.addWidget(self.uiNetworkConfigLabel, 13, 0, 1, 1)
self.uiNetworkConfigEditButton = QtWidgets.QPushButton(parent=self.tab)
self.uiNetworkConfigEditButton.setObjectName("uiNetworkConfigEditButton")
self.gridLayout.addWidget(self.uiNetworkConfigEditButton, 16, 1, 1, 1)
self.gridLayout.addWidget(self.uiNetworkConfigEditButton, 13, 1, 1, 1)
self.uiTabWidget.addTab(self.tab, "")
self.tab_2 = QtWidgets.QWidget()
self.tab_2.setObjectName("tab_2")
@@ -217,9 +193,6 @@ class Ui_dockerVMConfigPageWidget(object):
self.uiMacAddrLabel.setText(_translate("dockerVMConfigPageWidget", "Base MAC:"))
self.uiCustomAdaptersLabel.setText(_translate("dockerVMConfigPageWidget", "Custom adapters:"))
self.uiCustomAdaptersConfigurationPushButton.setText(_translate("dockerVMConfigPageWidget", "&Configure custom adapters"))
self.uiMaxMemoryLabel.setText(_translate("dockerVMConfigPageWidget", "Maximum memory:"))
self.uiMaxMemorySpinBox.setSuffix(_translate("dockerVMConfigPageWidget", " MB"))
self.uiMaxCPUsLabel.setText(_translate("dockerVMConfigPageWidget", "Maximum CPUs:"))
self.uiConsoleTypeLabel.setText(_translate("dockerVMConfigPageWidget", "Console type:"))
self.uiConsoleTypeComboBox.setItemText(0, _translate("dockerVMConfigPageWidget", "telnet"))
self.uiConsoleTypeComboBox.setItemText(1, _translate("dockerVMConfigPageWidget", "vnc"))
@@ -227,9 +200,6 @@ class Ui_dockerVMConfigPageWidget(object):
self.uiConsoleTypeComboBox.setItemText(3, _translate("dockerVMConfigPageWidget", "https"))
self.uiConsoleTypeComboBox.setItemText(4, _translate("dockerVMConfigPageWidget", "none"))
self.uiConsoleAutoStartCheckBox.setText(_translate("dockerVMConfigPageWidget", "Auto start console"))
self.uiAuxTypeLabel.setText(_translate("dockerVMConfigPageWidget", "Auxiliary console type:"))
self.uiAuxTypeComboBox.setItemText(0, _translate("dockerVMConfigPageWidget", "telnet"))
self.uiAuxTypeComboBox.setItemText(1, _translate("dockerVMConfigPageWidget", "none"))
self.uiConsoleResolutionLabel.setText(_translate("dockerVMConfigPageWidget", "VNC console resolution:"))
self.uiConsoleResolutionComboBox.setItemText(0, _translate("dockerVMConfigPageWidget", "2560x1440"))
self.uiConsoleResolutionComboBox.setItemText(1, _translate("dockerVMConfigPageWidget", "1920x1080"))

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/docker/ui/docker_vm_preferences_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/docker/ui/docker_vm_wizard.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -25,6 +25,7 @@ import hashlib
from gns3.local_config import LocalConfig
from gns3.image_manager import ImageManager
from gns3.local_server_config import LocalServerConfig
from gns3.controller import Controller
from gns3.template_manager import TemplateManager
from gns3.template import Template
@@ -144,19 +145,18 @@ class Dynamips(Module):
# save the settings
LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings)
# FIXME: handle server side config
# # save some settings to the local server config file
# server_settings = {
# "allocate_aux_console_ports": self._settings["allocate_aux_console_ports"],
# "ghost_ios_support": self._settings["ghost_ios_support"],
# "sparse_memory_support": self._settings["sparse_memory_support"],
# "mmap_support": self._settings["mmap_support"],
# }
#
# if self._settings["dynamips_path"]:
# server_settings["dynamips_path"] = os.path.normpath(self._settings["dynamips_path"])
# config = LocalServerConfig.instance()
# config.saveSettings(self.__class__.__name__, server_settings)
# save some settings to the local server config file
server_settings = {
"allocate_aux_console_ports": self._settings["allocate_aux_console_ports"],
"ghost_ios_support": self._settings["ghost_ios_support"],
"sparse_memory_support": self._settings["sparse_memory_support"],
"mmap_support": self._settings["mmap_support"],
}
if self._settings["dynamips_path"]:
server_settings["dynamips_path"] = os.path.normpath(self._settings["dynamips_path"])
config = LocalServerConfig.instance()
config.saveSettings(self.__class__.__name__, server_settings)
def updateImageIdlepc(self, image_path, idlepc):
"""

View File

@@ -218,20 +218,15 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
image = self.uiIOSImageLineEdit.text()
platform = self.uiPlatformComboBox.currentText()
ram = self.uiRamSpinBox.value()
Controller.instance().postCompute(
"/dynamips/auto_idlepc",
self._compute_id,
callback=self._computeAutoIdlepcCallback,
body={
"image": image,
"platform": platform,
"ram": ram
},
timeout=None,
progress_text="Finding an Idle-PC value...",
wait=True
)
Controller.instance().postCompute("/auto_idlepc",
self._compute_id,
self._computeAutoIdlepcCallback,
timeout=None,
body={
"image": image,
"platform": platform,
"ram": ram})
self.uiIdlePCFinderPushButton.setEnabled(False)
def _etherSwitchSlot(self, state):
"""
@@ -321,7 +316,7 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
super().initializePage(page_id)
if self.page(page_id) == self.uiIOSImageWizardPage:
self.loadImagesList("ios")
self.loadImagesList("/dynamips/images")
elif self.page(page_id) == self.uiNameWizardPage:
self._prefillPlatform()
self.uiNameLineEdit.setText(self.uiPlatformComboBox.currentText())

View File

@@ -50,6 +50,7 @@ class Router(Node):
router_settings = {"platform": platform,
"usage": "",
"chassis": "",
"image": "",
"image_md5sum": "",
"startup_config": "",
@@ -70,7 +71,6 @@ class Router(Node):
"console_type": "telnet",
"console_auto_start": False,
"aux": None,
"aux_type": "none",
"mac_addr": None,
"system_id": "FTX0945W0MY",
"slot0": None,
@@ -101,14 +101,11 @@ class Router(Node):
"""
log.debug("{} is requesting Idle-PC proposals".format(self.name()))
self.controllerHttpGet(
"/nodes/{node_id}/dynamips/idlepc_proposals".format(node_id=self._node_id),
callback,
context={"router": self},
progress_text="Computing Idle-PC values, please wait...",
timeout=240,
wait=True
)
self.controllerHttpGet("/nodes/{node_id}/dynamips/idlepc_proposals".format(node_id=self._node_id),
callback,
timeout=240,
context={"router": self},
progressText="Computing Idle-PC values, please wait...")
def computeAutoIdlepc(self, callback):
"""
@@ -116,14 +113,11 @@ class Router(Node):
"""
log.debug("{} is requesting Idle-PC proposals".format(self.name()))
self.controllerHttpGet(
"/nodes/{node_id}/dynamips/auto_idlepc".format(node_id=self._node_id),
callback,
context={"router": self},
progress_text="Computing Idle-PC values, please wait...",
timeout=240,
wait=True
)
self.controllerHttpGet("/nodes/{node_id}/dynamips/auto_idlepc".format(node_id=self._node_id),
callback,
timeout=240,
context={"router": self},
progressText="Computing Idle-PC values, please wait...")
def idlepc(self):
"""
@@ -245,11 +239,10 @@ class Router(Node):
info = """Router {name} is {state}
Running on server {host} with port {port}
Local ID is {id} and node ID is {node_id}
Local ID is {id} and server ID is {node_id}
Dynamips ID is {dynamips_id}
Hardware is Dynamips emulated Cisco {platform} {specific_info} with {ram}MB RAM and {nvram}KB NVRAM
Console is on port {console} and type is {console_type}
Auxiliary console is on port {aux} and type is {aux_type}
Console is on port {console} and type is {console_type}, AUX console is on port {aux}
IOS image is "{image_name}"
{idlepc_info}
PCMCIA disks: disk0 is {disk0}MB and disk1 is {disk1}MB
@@ -267,7 +260,6 @@ class Router(Node):
console=self._settings["console"],
console_type=self._settings["console_type"],
aux=self._settings["aux"],
aux_type=self._settings["aux_type"],
image_name=os.path.basename(self._settings["image"]),
idlepc_info=idlepc_info,
disk0=self._settings["disk0"],
@@ -306,6 +298,23 @@ class Router(Node):
from ..pages.ios_router_configuration_page import IOSRouterConfigurationPage
return IOSRouterConfigurationPage
@staticmethod
def validateHostname(hostname):
"""
Checks if the hostname is valid.
:param hostname: hostname to check
:returns: boolean
"""
# IOS names must start with a letter, end with a letter or digit, and
# have as interior characters only letters, digits, and hyphens.
# They must be 63 characters or fewer (ARPANET rules).
if re.search(r"""^(?!-|[0-9])[a-zA-Z0-9-]{1,63}(?<!-)$""", hostname):
return True
return False
@staticmethod
def defaultSymbol():
"""

View File

@@ -24,6 +24,7 @@ import re
from gns3.qt import QtCore, QtGui, QtWidgets
from gns3.local_server import LocalServer
from gns3.local_config import LocalConfig
from gns3.dialogs.node_properties_dialog import ConfigurationError
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.controller import Controller
@@ -374,11 +375,6 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
self.uiConsoleAutoStartCheckBox.setChecked(settings["console_auto_start"])
# load the auxiliary console type
index = self.uiAuxTypeComboBox.findText(settings["aux_type"])
if index != -1:
self.uiAuxTypeComboBox.setCurrentIndex(index)
# load the memories and disks settings
self.uiRamSpinBox.setValue(settings["ram"])
self.uiNvramSpinBox.setValue(settings["nvram"])
@@ -493,6 +489,8 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
name = self.uiNameLineEdit.text()
if not name:
QtWidgets.QMessageBox.critical(self, "Name", "IOS router name cannot be empty!")
elif node and not node.validateHostname(name) and not LocalConfig.instance().experimental():
QtWidgets.QMessageBox.critical(self, "Name", "Invalid name detected for IOS router: {}".format(name))
else:
settings["name"] = name
@@ -582,9 +580,6 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
settings["console_type"] = self.uiConsoleTypeComboBox.currentText().lower()
settings["console_auto_start"] = self.uiConsoleAutoStartCheckBox.isChecked()
# save auxiliary console type
settings["aux_type"] = self.uiAuxTypeComboBox.currentText().lower()
# save the memories and disks settings
settings["ram"] = self.uiRamSpinBox.value()
settings["nvram"] = self.uiNvramSpinBox.value()

View File

@@ -386,15 +386,7 @@ class IOSRouterPreferencesPage(QtWidgets.QWidget, Ui_IOSRouterPreferencesPageWid
QtWidgets.QTreeWidgetItem(section_item, ["Template ID:", ios_router.get("template_id", "none")])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", ios_router["default_name_format"]])
try:
compute_id = ios_router.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])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(ios_router["compute_id"]).name()])
except KeyError:
pass
QtWidgets.QTreeWidgetItem(section_item, ["Platform:", ios_router["platform"]])
@@ -403,7 +395,6 @@ class IOSRouterPreferencesPage(QtWidgets.QWidget, Ui_IOSRouterPreferencesPageWid
QtWidgets.QTreeWidgetItem(section_item, ["Image:", ios_router["image"]])
QtWidgets.QTreeWidgetItem(section_item, ["Console type:", ios_router["console_type"]])
QtWidgets.QTreeWidgetItem(section_item, ["Auto start console:", "{}".format(ios_router["console_auto_start"])])
QtWidgets.QTreeWidgetItem(section_item, ["Auxiliary console type:", ios_router["aux_type"]])
if ios_router["idlepc"]:
QtWidgets.QTreeWidgetItem(section_item, ["Idle-PC:", ios_router["idlepc"]])
if ios_router["startup_config"]:

View File

@@ -34,13 +34,12 @@ IOS_ROUTER_SETTINGS = {
"default_name_format": "R{0}",
"usage": "",
"image": "",
"symbol": "router",
"symbol": ":/symbols/router.svg",
"category": Node.routers,
"startup_config": "",
"private_config": "",
"console_type": "telnet",
"console_auto_start": False,
"aux_type": "none",
"platform": "",
"idlepc": "",
"idlemax": 500,

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/dynamips/ui/dynamips_preferences_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>964</width>
<height>830</height>
<width>980</width>
<height>734</height>
</rect>
</property>
<property name="windowTitle">
@@ -250,7 +250,7 @@
</item>
</layout>
</item>
<item row="14" column="1" colspan="2">
<item row="12" column="1" colspan="2">
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -263,27 +263,6 @@
</property>
</spacer>
</item>
<item row="12" column="2">
<widget class="QComboBox" name="uiAuxTypeComboBox">
<item>
<property name="text">
<string>telnet</string>
</property>
</item>
<item>
<property name="text">
<string>none</string>
</property>
</item>
</widget>
</item>
<item row="12" column="0" colspan="2">
<widget class="QLabel" name="uiAuxTypeLabel">
<property name="text">
<string>Aux console type:</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="uiMemoriesPageWidget">
@@ -291,16 +270,7 @@
<string>Memories and disks</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>10</number>
</property>
<item row="0" column="0">
@@ -478,16 +448,7 @@
<string>Slots</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>10</number>
</property>
<item>
@@ -688,16 +649,7 @@
<string>Advanced</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>10</number>
</property>
<item>
@@ -876,16 +828,7 @@
<string>Environment</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>10</number>
</property>
<item>

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/dynamips/ui/ios_router_configuration_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
@@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_iosRouterConfigPageWidget(object):
def setupUi(self, iosRouterConfigPageWidget):
iosRouterConfigPageWidget.setObjectName("iosRouterConfigPageWidget")
iosRouterConfigPageWidget.resize(964, 830)
iosRouterConfigPageWidget.resize(980, 734)
self.vboxlayout = QtWidgets.QVBoxLayout(iosRouterConfigPageWidget)
self.vboxlayout.setObjectName("vboxlayout")
self.uiTabWidget = QtWidgets.QTabWidget(parent=iosRouterConfigPageWidget)
@@ -144,15 +144,7 @@ class Ui_iosRouterConfigPageWidget(object):
self.horizontalLayout.addWidget(self.uiConsoleAutoStartCheckBox)
self.gridLayout_2.addLayout(self.horizontalLayout, 11, 2, 1, 1)
spacerItem = QtWidgets.QSpacerItem(263, 151, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
self.gridLayout_2.addItem(spacerItem, 14, 1, 1, 2)
self.uiAuxTypeComboBox = QtWidgets.QComboBox(parent=self.uiGeneralPageWidget)
self.uiAuxTypeComboBox.setObjectName("uiAuxTypeComboBox")
self.uiAuxTypeComboBox.addItem("")
self.uiAuxTypeComboBox.addItem("")
self.gridLayout_2.addWidget(self.uiAuxTypeComboBox, 12, 2, 1, 1)
self.uiAuxTypeLabel = QtWidgets.QLabel(parent=self.uiGeneralPageWidget)
self.uiAuxTypeLabel.setObjectName("uiAuxTypeLabel")
self.gridLayout_2.addWidget(self.uiAuxTypeLabel, 12, 0, 1, 2)
self.gridLayout_2.addItem(spacerItem, 12, 1, 1, 2)
self.uiTabWidget.addTab(self.uiGeneralPageWidget, "")
self.uiMemoriesPageWidget = QtWidgets.QWidget()
self.uiMemoriesPageWidget.setObjectName("uiMemoriesPageWidget")
@@ -591,9 +583,6 @@ class Ui_iosRouterConfigPageWidget(object):
self.uiConsoleTypeComboBox.setItemText(0, _translate("iosRouterConfigPageWidget", "telnet"))
self.uiConsoleTypeComboBox.setItemText(1, _translate("iosRouterConfigPageWidget", "none"))
self.uiConsoleAutoStartCheckBox.setText(_translate("iosRouterConfigPageWidget", "Auto start console"))
self.uiAuxTypeComboBox.setItemText(0, _translate("iosRouterConfigPageWidget", "telnet"))
self.uiAuxTypeComboBox.setItemText(1, _translate("iosRouterConfigPageWidget", "none"))
self.uiAuxTypeLabel.setText(_translate("iosRouterConfigPageWidget", "Aux console type:"))
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiGeneralPageWidget), _translate("iosRouterConfigPageWidget", "General"))
self.uiRamLabel.setText(_translate("iosRouterConfigPageWidget", "RAM size:"))
self.uiRamSpinBox.setSuffix(_translate("iosRouterConfigPageWidget", " MiB"))

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/dynamips/ui/ios_router_preferences_page.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -1,6 +1,6 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/dynamips/ui/ios_router_wizard.ui'
#
# Created by: PyQt6 UI code generator 6.10.2
# Created by: PyQt6 UI code generator 6.10.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -92,7 +92,7 @@ class IOUDeviceWizard(VMWithImagesWizard, Ui_IOUDeviceWizard):
if self.page(page_id) == self.uiNameWizardPage:
if not self.uiIOUImageToolButton.isEnabled():
QtWidgets.QMessageBox.warning(self, "IOU image", "You have chosen to use a remote server, please provide the path to an IOU image located on this server!")
self.loadImagesList("iou")
self.loadImagesList("/iou/images")
def getSettings(self):
"""

View File

@@ -73,7 +73,7 @@ class IOUDevice(Node):
info = """Device {name} is {state}
Running on server {host} with port {port}
Local ID is {id} and node ID is {node_id}
Local ID is {id} and server ID is {node_id}
Hardware is Cisco IOU generic device with {memories_info}
Console is on port {console} and type is {console_type}
IOU image is "{image_name}"
@@ -119,6 +119,23 @@ class IOUDevice(Node):
from .pages.iou_device_configuration_page import iouDeviceConfigurationPage
return iouDeviceConfigurationPage
@staticmethod
def validateHostname(hostname):
"""
Checks if the hostname is valid.
:param hostname: hostname to check
:returns: boolean
"""
# IOS names must start with a letter, end with a letter or digit, and
# have as interior characters only letters, digits, and hyphens.
# They must be 63 characters or fewer (ARPANET rules).
if re.search(r"""^(?!-|[0-9])[a-zA-Z0-9-]{1,63}(?<!-)$""", hostname):
return True
return False
@staticmethod
def defaultSymbol():
"""

View File

@@ -23,6 +23,7 @@ import os
from gns3.qt import QtWidgets
from gns3.local_server import LocalServer
from gns3.local_config import LocalConfig
from gns3.dialogs.node_properties_dialog import ConfigurationError
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
from gns3.node import Node
@@ -245,6 +246,8 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
name = self.uiNameLineEdit.text()
if not name:
QtWidgets.QMessageBox.critical(self, "Name", "IOU device name cannot be empty!")
elif node and not node.validateHostname(name) and not LocalConfig.instance().experimental():
QtWidgets.QMessageBox.critical(self, "Name", "Invalid name detected for IOU device: {}".format(name))
else:
settings["name"] = name

View File

@@ -88,15 +88,7 @@ class IOUDevicePreferencesPage(QtWidgets.QWidget, Ui_IOUDevicePreferencesPageWid
QtWidgets.QTreeWidgetItem(section_item, ["Template ID:", iou_device.get("template_id", "none")])
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", iou_device["default_name_format"]])
try:
compute_id = iou_device.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])
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(iou_device["compute_id"]).name()])
except KeyError:
# Compute doesn't exists
pass

View File

@@ -32,7 +32,7 @@ IOU_DEVICE_SETTINGS = {
"default_name_format": "IOU{0}",
"usage": "",
"path": "",
"symbol": "multilayer_switch",
"symbol": ":/symbols/multilayer_switch.svg",
"category": Node.routers,
"startup_config": "",
"private_config": "",

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