mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-05-28 22:40:30 +03:00
Compare commits
181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf2802b15a | ||
|
|
b162c55078 | ||
|
|
bb42b0ed0b | ||
|
|
e108b5194d | ||
|
|
e9ef8735be | ||
|
|
e8e90bb16a | ||
|
|
49f77930f4 | ||
|
|
4f32619ed8 | ||
|
|
20740748c1 | ||
|
|
e11ce27f7b | ||
|
|
2987bcf91a | ||
|
|
5e97bc0f86 | ||
|
|
e3a3de5df7 | ||
|
|
e1693ce113 | ||
|
|
522091d219 | ||
|
|
1446748934 | ||
|
|
3f0ce380e8 | ||
|
|
bd71383354 | ||
|
|
9649895378 | ||
|
|
8579ffa20a | ||
|
|
3206743329 | ||
|
|
29c87b6e96 | ||
|
|
93b2721d6a | ||
|
|
1ff369683f | ||
|
|
7c56a2467c | ||
|
|
59ef34c17d | ||
|
|
d1fae54049 | ||
|
|
49bd61f769 | ||
|
|
9a4faddd10 | ||
|
|
7654681a94 | ||
|
|
9bfecde957 | ||
|
|
705cbf8bb9 | ||
|
|
ab6e0ce496 | ||
|
|
8042c9eb6f | ||
|
|
ad3c8a09db | ||
|
|
ea4a7f201e | ||
|
|
28c82b8718 | ||
|
|
6a4dd59e81 | ||
|
|
7418c190a8 | ||
|
|
737e32f5c3 | ||
|
|
cfc09d2c14 | ||
|
|
f6ab5cae16 | ||
|
|
39ec7eb8ea | ||
|
|
64c579d43c | ||
|
|
98cc82e6fd | ||
|
|
4b795112b4 | ||
|
|
0e186afaf1 | ||
|
|
b218f7fdf8 | ||
|
|
f1cb6d66f3 | ||
|
|
29758f1b4f | ||
|
|
445dcf3e3b | ||
|
|
f623f28509 | ||
|
|
9d02d57162 | ||
|
|
df03f50e3d | ||
|
|
3b72a66ca5 | ||
|
|
f8aee44442 | ||
|
|
6dd4d1700e | ||
|
|
9b674669db | ||
|
|
53073d458f | ||
|
|
957d89d450 | ||
|
|
db02cbdb2f | ||
|
|
0c9f70152f | ||
|
|
9016975958 | ||
|
|
bf295060fd | ||
|
|
c22ec9f8bd | ||
|
|
bb49cadc6a | ||
|
|
131a49160c | ||
|
|
225b829eae | ||
|
|
e5ef6180b1 | ||
|
|
6e7947eea3 | ||
|
|
3922d370a8 | ||
|
|
833b9d00c9 | ||
|
|
377b8dfcaf | ||
|
|
e68937475f | ||
|
|
6f418f0853 | ||
|
|
8e59927ada | ||
|
|
1012686053 | ||
|
|
672bd850ad | ||
|
|
5db5e1f9fe | ||
|
|
ca94c71bf2 | ||
|
|
76264c55ce | ||
|
|
fd243c42a8 | ||
|
|
a6521ef9e4 | ||
|
|
9fa833762c | ||
|
|
ca0c6468b5 | ||
|
|
15f6945a94 | ||
|
|
645deb8c79 | ||
|
|
428f12a2b3 | ||
|
|
9ad5760ee6 | ||
|
|
82fc4fb3c9 | ||
|
|
df42147d92 | ||
|
|
da5520aa90 | ||
|
|
491c66a315 | ||
|
|
e5c81da700 | ||
|
|
65fad1b4f4 | ||
|
|
34661908d9 | ||
|
|
aee5ffa17f | ||
|
|
e9e8be42b5 | ||
|
|
ae0d928383 | ||
|
|
8db3c1be42 | ||
|
|
f50da3ebd7 | ||
|
|
75b52fc9a4 | ||
|
|
1952da5876 | ||
|
|
1f620026d4 | ||
|
|
1d293618e5 | ||
|
|
2622549ce6 | ||
|
|
900bd1c0b4 | ||
|
|
0b3dbb2843 | ||
|
|
ef4f6b2b27 | ||
|
|
e9806345ca | ||
|
|
ee23e32c75 | ||
|
|
fbeacdcb2a | ||
|
|
b3937c7b94 | ||
|
|
f2711732db | ||
|
|
148ac4b072 | ||
|
|
65eeb79b26 | ||
|
|
537304ce08 | ||
|
|
f22df5f016 | ||
|
|
8dfc8b7714 | ||
|
|
8c6fa9433f | ||
|
|
63837578c5 | ||
|
|
b719703dbe | ||
|
|
084d14c17e | ||
|
|
8c0fca1dd7 | ||
|
|
863d05c923 | ||
|
|
3ebaac8a2c | ||
|
|
16878c9dfa | ||
|
|
45da18bb7c | ||
|
|
7a6d06ea0c | ||
|
|
d371042647 | ||
|
|
0321c11c34 | ||
|
|
522df41a57 | ||
|
|
afccdf5b9e | ||
|
|
b2cd24b511 | ||
|
|
6d131a05f1 | ||
|
|
35e6156c6c | ||
|
|
96d8de4da8 | ||
|
|
6b5a6f3dfe | ||
|
|
8f82eac321 | ||
|
|
e03ed64f59 | ||
|
|
3d702aabd0 | ||
|
|
f5e63c2321 | ||
|
|
1047eb916a | ||
|
|
5dc7d0fbda | ||
|
|
2609be98b6 | ||
|
|
6286e596c0 | ||
|
|
3c546086ed | ||
|
|
f4b2c1c5b9 | ||
|
|
e578ecdd8a | ||
|
|
da8adbaa18 | ||
|
|
6d1333f5fe | ||
|
|
92c858dd07 | ||
|
|
0c7a12f68c | ||
|
|
a4d08cce8c | ||
|
|
e0dd7a66e1 | ||
|
|
23be668c97 | ||
|
|
68d0278140 | ||
|
|
d8e4c1de4d | ||
|
|
a5aa9bfb7a | ||
|
|
3e0273848f | ||
|
|
ec374f173c | ||
|
|
b8abdc79dc | ||
|
|
43744eab7e | ||
|
|
e16f700e49 | ||
|
|
925d57b2f8 | ||
|
|
eceaea1317 | ||
|
|
4326785dfc | ||
|
|
3920c28bde | ||
|
|
b34f51e4b0 | ||
|
|
ef45b2e0f1 | ||
|
|
545a9f53a8 | ||
|
|
83d9367860 | ||
|
|
2131f07e5f | ||
|
|
cf3e716e63 | ||
|
|
c79f14bcab | ||
|
|
acd044a88a | ||
|
|
f26c638350 | ||
|
|
4ea24e622b | ||
|
|
ab854752d9 | ||
|
|
5cee045a65 | ||
|
|
37cd82fb44 |
16
.github/workflows/add-new-issues-to-project.yml
vendored
Normal file
16
.github/workflows/add-new-issues-to-project.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Add new issues to GNS3 project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.4.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/GNS3/projects/3
|
||||
github-token: ${{ secrets.ADD_NEW_ISSUES_TO_PROJECT }}
|
||||
76
.github/workflows/codeql.yml
vendored
Normal file
76
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '27 6 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -63,3 +63,4 @@ __pycache__
|
||||
|
||||
# Virtualenv
|
||||
env
|
||||
venv
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"scanSettings": {
|
||||
"configMode": "AUTO",
|
||||
"configExternalURL": "",
|
||||
"projectToken" : ""
|
||||
"projectToken" : "",
|
||||
"baseBranches": ["master", "2.2", "3.0"]
|
||||
},
|
||||
"checkRunSettings": {
|
||||
"vulnerableCheckRunConclusionLevel": "failure"
|
||||
|
||||
130
CHANGELOG
130
CHANGELOG
@@ -1,5 +1,135 @@
|
||||
# Change Log
|
||||
|
||||
## 2.2.41 12/07/2023
|
||||
|
||||
* Use alternative method to set the correct permissions for uBridge on macOS
|
||||
* Remove sending stats to GA
|
||||
* Catch urllib3 exceptions when sending crash report. Ref https://github.com/GNS3/gns3-gui/issues/3483
|
||||
* Backport UEFI boot mode support for Qemu VMs
|
||||
* Add debug for dropEvent. Ref https://github.com/GNS3/gns3-server/issues/2242
|
||||
|
||||
## 2.2.40.1 10/06/2023
|
||||
|
||||
* No changes
|
||||
|
||||
## 2.2.40 06/06/2023
|
||||
|
||||
* Change log messages for Websocket errors
|
||||
* Do not proceed if an appliance symbol cannot be downloaded. Ref #3466
|
||||
* Delete a node or link from topology summary view using Delete key. Ref #3445
|
||||
* Fix "Start the capture visualization program" checkbox works only one (first) time for a given link. Fixes #3442
|
||||
* Let the selected link style applied when editing a link. Fixes #3460
|
||||
* Fix hovered color shown in style editing dialog. Fixes #3460
|
||||
|
||||
## 2.2.39 08/05/2023
|
||||
|
||||
* Fix nodes are not snapped to the grid at the moment of creation
|
||||
* Upgrade distro and aiohttp dependencies
|
||||
|
||||
## 2.2.38 28/02/2023
|
||||
|
||||
* Add long description content type in setup.py
|
||||
* Automatically add new issues to GNS3 project
|
||||
* Development 2.2.38.dev1
|
||||
|
||||
## 2.2.37 25/01/2023
|
||||
|
||||
* Upgrade to PyQt5 v5.15.7
|
||||
* Changed Windows Terminal telnet console profile from OS X to windows ref: issue #3193
|
||||
|
||||
## 2.2.36 04/01/2023
|
||||
|
||||
* Add Trusted Platform Module (TPM) support for Qemu VMs
|
||||
* Add "on_close" setting to appliance schema. Fixes https://github.com/GNS3/gns3-server/issues/2148
|
||||
* Add default 'ide' disk interface when manually creating Qemu VM template. Fixes #3360
|
||||
* Fix zoom factor is multiplied when loading projects. Fixes #3408
|
||||
* Remove deprecated PuTTY option in preferences. Ref https://github.com/GNS3/gns3-gui/discussions/3415
|
||||
|
||||
|
||||
## 2.2.35.1 10/11/2022
|
||||
|
||||
* Re-release Web-Ui v2.2.35
|
||||
|
||||
## 2.2.35 08/11/2022
|
||||
|
||||
* Fix "variables": [] in project file leads to unlimited increase of empty name/value pairs in GUI. Fixes #3397
|
||||
* Make version PEP 440 compliant
|
||||
* Support for Python 3.11
|
||||
* Upgrade PyQt to 5.15.7 and pywin32 to v305
|
||||
* Allow for more dependency versions at patch level
|
||||
* Replace deprecated distro.linux_distribution() call
|
||||
* Add a fix for the CVE-2007-4559
|
||||
|
||||
## 2.2.34 28/08/2022
|
||||
|
||||
* Upgrade dev dependencies
|
||||
* Implement new option (Delete All) to contextual menu in "Console" dock. Fixes #3325
|
||||
* Fix 2560x1440 resolution for Docker container
|
||||
|
||||
## 2.2.33.1 21/06/2022
|
||||
|
||||
* Match GNS3 server version
|
||||
|
||||
## 2.2.33 20/06/2022
|
||||
|
||||
* Upgrade sentry-sdk and psutil
|
||||
* Check that node names for Qemu and Docker are valid
|
||||
* Backport reset all console connections. Fixes #2072
|
||||
* Add more video resolutions to Docker containers using VNC. Fixes #3329
|
||||
* Add python_requires=">=3.4" in setup.py. Fixes #3326
|
||||
* Only allow post release corrective versions of GUI and server to interact
|
||||
* Allow minor versions of GUI and server to interact
|
||||
* Update VirtViewer path. Fixes #3334
|
||||
|
||||
## 2.2.32 27/04/2022
|
||||
|
||||
* Use public DSNs for Sentry
|
||||
* Fix exception when doubleclick on NAT node. Fixes #3312
|
||||
* Fix "Apply" button in the "Preferences" dialog stays gray when templates/nodes are opened by double-click. Fixes #3307
|
||||
* Add 'reset docks' in the view menu. Ref #3317
|
||||
|
||||
## 2.2.31 26/02/2022
|
||||
|
||||
* Install setuptools v59.6.0 when using Python 3.6
|
||||
|
||||
## 2.2.30 25/02/2022
|
||||
|
||||
* Set setuptools to v60.6.0
|
||||
* Upgrade to pywin32 v303. Ref #3290
|
||||
* Fix int() call. Ref #3283
|
||||
* Fix QPoint() as unexpected type 'float'. Fixes #3283
|
||||
* Fix painter.drawRect() has unexpected type 'float'. Fixes #3282
|
||||
* Fix SpinBox.setValue() requires integer. Fixes #3281
|
||||
|
||||
## 2.2.29 08/01/2022
|
||||
|
||||
* Clear cache when opening symbol selection dialog. Fixes #3256
|
||||
* Fix @ in username issue with HTTP authentication. Fixes #3275
|
||||
* Use '//' operator instead of int()
|
||||
* Fix create drawing item calls since mapToScene() returns a QPointF https://doc.qt.io/qt-5/qgraphicsview.html#mapToScene-4
|
||||
* Fixed QPoint called with floats
|
||||
|
||||
## 2.2.28 15/12/2021
|
||||
|
||||
* Fixed drawLine called with float arguments
|
||||
* Fixed dead VIX API link
|
||||
|
||||
## 2.2.27 12/11/2021
|
||||
|
||||
* Fix symbols in "Symbol selection" dialog are not placed in alphabetical order. Fixes #3245
|
||||
* Fix links duplicates in topology summary. Fixes #3251
|
||||
* chore : use --no-cache-dir flag to pip in dockerfiles to save space
|
||||
|
||||
## 2.2.26 08/10/2021
|
||||
|
||||
* Upgrade embedded Python to version 3.7 in Windows package
|
||||
* Upgrade Visual C++ Redistributable for Visual Studio 2019 in Windows package
|
||||
* Fix SSL support in Windows package
|
||||
* Open "template configuration" dialog with double click on template name in "Preferences". Fixes #3239
|
||||
* Only show "virtio" network adapter when legacy node is enabled. Fixes https://github.com/GNS3/gns3-gui/issues/1969
|
||||
* Double-click on a template opens "template configuration" dialog. Fixes #3236
|
||||
* Fix "Custom symbols" can't be unfolded after using "Filter" field. Fixes #3231
|
||||
|
||||
## 2.2.25 14/09/2021
|
||||
|
||||
* Fix menu disabled for modal dialogs on macOS. Fixes #3007
|
||||
|
||||
@@ -8,7 +8,7 @@ RUN apt-get clean
|
||||
|
||||
ADD dev-requirements.txt /dev-requirements.txt
|
||||
ADD requirements.txt /requirements.txt
|
||||
RUN pip3 install -r /dev-requirements.txt
|
||||
RUN pip3 install --no-cache-dir -r /dev-requirements.txt
|
||||
|
||||
ADD . /src
|
||||
WORKDIR /src
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
include README.rst
|
||||
include README.md
|
||||
include AUTHORS
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
|
||||
62
README.md
Normal file
62
README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
GNS3-gui
|
||||
========
|
||||
|
||||
[](https://github.com/GNS3/gns3-gui/actions?query=workflow%3Atesting)
|
||||
|
||||
[](https://pypi.python.org/pypi/gns3-gui)
|
||||
|
||||
[](https://snyk.io/test/github/GNS3/gns3-gui)
|
||||
|
||||
GNS3 GUI repository.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Please see <https://docs.gns3.com/>
|
||||
|
||||
Software dependencies
|
||||
---------------------
|
||||
|
||||
PyQt5 which is either part of the Linux distribution or installable from
|
||||
PyPi. The other Python dependencies are automatically installed during
|
||||
the GNS3 GUI installation and are listed
|
||||
[here](https://github.com/GNS3/gns3-gui/blob/master/requirements.txt)
|
||||
|
||||
For connecting to nodes using Telnet, a Telnet client is required. On
|
||||
Linux that's a terminal emulator like xterm, gnome-terminal, konsole
|
||||
plus the telnet program. For connecting to nodes with a GUI, a VNC
|
||||
client is required, optionally a SPICE client can be used for Qemu
|
||||
nodes.
|
||||
|
||||
For using packet captures within GNS3, Wireshark should be installed.
|
||||
It's recommended, but if you don't need that functionality you can go
|
||||
without it.
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
If you want to update the interface, modify the .ui files using QT
|
||||
tools. And:
|
||||
|
||||
``` {.bash}
|
||||
cd scripts
|
||||
python build_pyqt.py
|
||||
```
|
||||
|
||||
### Debug
|
||||
|
||||
If you want to see the full logs in the internal shell you can type:
|
||||
|
||||
``` {.bash}
|
||||
debug 2
|
||||
```
|
||||
|
||||
Or start the app with --debug flag.
|
||||
|
||||
Due to the fact PyQT intercept you can use a web debugger for inspecting
|
||||
stuff: <https://github.com/Kozea/wdb>
|
||||
|
||||
Security issues
|
||||
---------------
|
||||
|
||||
Please contact us at <security@gns3.net>
|
||||
60
README.rst
60
README.rst
@@ -1,60 +0,0 @@
|
||||
GNS3-gui
|
||||
========
|
||||
|
||||
.. image:: https://github.com/GNS3/gns3-gui/workflows/testing/badge.svg
|
||||
:target: https://github.com/GNS3/gns3-gui/actions?query=workflow%3Atesting
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/gns3-gui.svg
|
||||
:target: https://pypi.python.org/pypi/gns3-gui
|
||||
|
||||
.. image:: https://snyk.io/test/github/GNS3/gns3-gui/badge.svg
|
||||
:target: https://snyk.io/test/github/GNS3/gns3-gui
|
||||
|
||||
|
||||
GNS3 GUI repository.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Please see https://docs.gns3.com/
|
||||
|
||||
Software dependencies
|
||||
---------------------
|
||||
|
||||
PyQt5 which is either part of the Linux distribution or installable from PyPi. The other Python dependencies are automatically installed during the GNS3 GUI installation and are listed `here <https://github.com/GNS3/gns3-gui/blob/master/requirements.txt>`_
|
||||
|
||||
For connecting to nodes using Telnet, a Telnet client is required. On Linux that's a terminal emulator like xterm, gnome-terminal, konsole plus the telnet program. For connecting to nodes with a GUI, a VNC client is required, optionally a SPICE client can be used for Qemu nodes.
|
||||
|
||||
For using packet captures within GNS3, Wireshark should be installed. It's recommended, but if you don't need that functionality you can go without it.
|
||||
|
||||
Development
|
||||
-------------
|
||||
|
||||
If you want to update the interface, modify the .ui files using QT tools. And:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
cd scripts
|
||||
python build_pyqt.py
|
||||
|
||||
Debug
|
||||
"""""
|
||||
|
||||
If you want to see the full logs in the internal shell you can type:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
debug 2
|
||||
|
||||
|
||||
Or start the app with --debug flag.
|
||||
|
||||
Due to the fact PyQT intercept you can use a web debugger for inspecting stuff:
|
||||
https://github.com/Kozea/wdb
|
||||
|
||||
Security issues
|
||||
----------------
|
||||
|
||||
Please contact us at security@gns3.net
|
||||
|
||||
|
||||
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please use GitHub's report a vulnerability feature. More information can be found in https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability
|
||||
@@ -1,15 +1,16 @@
|
||||
version: '{build}-{branch}'
|
||||
|
||||
image: Visual Studio 2017
|
||||
image: Visual Studio 2022
|
||||
|
||||
platform: x64
|
||||
|
||||
environment:
|
||||
PYTHON: "C:\\Python36-x64"
|
||||
PYTHON: "C:\\Python37-x64"
|
||||
DISTUTILS_USE_SDK: "1"
|
||||
|
||||
install:
|
||||
- cinst nmap
|
||||
- "%PYTHON%\\python.exe -m pip install -U pip setuptools" # upgrade pip & setuptools first
|
||||
- "%PYTHON%\\python.exe -m pip install -r dev-requirements.txt"
|
||||
- "%PYTHON%\\python.exe -m pip install -r win-requirements.txt"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
-rrequirements.txt
|
||||
|
||||
pytest==6.2.4
|
||||
flake8==3.9.2
|
||||
pytest-timeout==1.4.2
|
||||
pytest==7.2.0; python_version >= '3.7'
|
||||
pytest==7.0.1; python_version < '3.7' # v7.0.1 is the last version to support Python 3.6
|
||||
flake8==5.0.4
|
||||
pytest-timeout==2.1.0
|
||||
|
||||
@@ -22,7 +22,7 @@ import inspect
|
||||
import datetime
|
||||
import platform
|
||||
|
||||
from .qt import QtCore
|
||||
from .qt import QtCore, QtWidgets
|
||||
from .topology import Topology
|
||||
from .version import __version__
|
||||
from .console_cmd import ConsoleCmd
|
||||
@@ -109,6 +109,29 @@ class ConsoleView(PyCutExt, ConsoleCmd):
|
||||
self.stdout = sys.stdout
|
||||
self._topology = Topology.instance()
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
"""
|
||||
Handles all context menu events.
|
||||
|
||||
:param event: QContextMenuEvent instance
|
||||
"""
|
||||
|
||||
menu = self.createStandardContextMenu()
|
||||
delete_all_action = QtWidgets.QAction("Delete All", menu)
|
||||
delete_all_action.triggered.connect(self._deleteAllActionSlot)
|
||||
menu.addAction(delete_all_action)
|
||||
menu.exec_(event.globalPos());
|
||||
|
||||
def _deleteAllActionSlot(self):
|
||||
"""
|
||||
Delete all action slot
|
||||
"""
|
||||
|
||||
self.clear()
|
||||
self.write(self.prompt)
|
||||
self.lines = []
|
||||
self._clearLine()
|
||||
|
||||
def _writeMessageSlot(self, message, level):
|
||||
"""
|
||||
Write a message in the console.
|
||||
|
||||
@@ -449,7 +449,7 @@ class Controller(QtCore.QObject):
|
||||
@qslot
|
||||
def _websocket_error(self, error):
|
||||
if self._notification_stream:
|
||||
log.error("Websocket notification stream error: {}".format(self._notification_stream.errorString()))
|
||||
log.error("Websocket controller notification stream error: {}".format(self._notification_stream.errorString()))
|
||||
self._notification_stream = None
|
||||
self._startListenNotifications()
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import os
|
||||
import platform
|
||||
import struct
|
||||
import distro
|
||||
import urllib3
|
||||
|
||||
try:
|
||||
import sentry_sdk
|
||||
@@ -51,7 +52,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "https://ca0e3be7dada465495fc33615f6143b8:4ddf20c171744e138fd327929c5e15f2@o19455.ingest.sentry.io/38506"
|
||||
DSN = "https://8e73913aea164a31a72e3a07fc455e26@o19455.ingest.sentry.io/38506"
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
@@ -75,18 +76,22 @@ class CrashReport:
|
||||
# Don't send log records as events.
|
||||
sentry_logging = LoggingIntegration(level=logging.INFO, event_level=None)
|
||||
|
||||
sentry_sdk.init(dsn=CrashReport.DSN,
|
||||
release=__version__,
|
||||
ca_certs=cacert,
|
||||
default_integrations=False,
|
||||
integrations=[sentry_logging])
|
||||
try:
|
||||
sentry_sdk.init(dsn=CrashReport.DSN,
|
||||
release=__version__,
|
||||
ca_certs=cacert,
|
||||
default_integrations=False,
|
||||
integrations=[sentry_logging])
|
||||
except urllib3.exceptions.HTTPError as e:
|
||||
log.error("Crash report could not be sent: {}".format(e))
|
||||
return
|
||||
|
||||
tags = {
|
||||
"os:name": platform.system(),
|
||||
"os:release": platform.release(),
|
||||
"os:win_32": " ".join(platform.win32_ver()),
|
||||
"os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]),
|
||||
"os:linux": " ".join(distro.linux_distribution()),
|
||||
"os:linux": distro.name(pretty=True),
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -46,19 +46,11 @@ class EditProjectDialog(QtWidgets.QDialog, Ui_EditProjectDialog):
|
||||
self.uiNewVarButton.clicked.connect(self.onAddNewVariable)
|
||||
self.uiGlobalVariablesGrid.addWidget(self.uiNewVarButton, 0, 3, QtCore.Qt.AlignRight)
|
||||
|
||||
self._variables = self.setUpVariables()
|
||||
self._variables = self._project.variables()
|
||||
if not self._variables:
|
||||
self._variables = [{"name": "", "value": ""}]
|
||||
self.updateGlobalVariables()
|
||||
|
||||
def setUpVariables(self):
|
||||
new_variable = {"name": "", "value": ""}
|
||||
variables = self._project.variables()
|
||||
|
||||
if variables is not None:
|
||||
variables.append(new_variable)
|
||||
else:
|
||||
variables = [new_variable]
|
||||
return variables
|
||||
|
||||
def updateGlobalVariables(self):
|
||||
while True:
|
||||
item = self.uiGlobalVariablesGrid.takeAt(1)
|
||||
|
||||
@@ -131,6 +131,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
|
||||
QtWidgets.QLineEdit: "textChanged",
|
||||
QtWidgets.QPlainTextEdit: "textChanged",
|
||||
# QtWidgets.QTreeWidget: "itemChanged",
|
||||
QtWidgets.QTreeWidget: "itemDoubleClicked",
|
||||
QtWidgets.QComboBox: "currentIndexChanged",
|
||||
QtWidgets.QSpinBox: "valueChanged",
|
||||
QtWidgets.QAbstractButton: "pressed"
|
||||
|
||||
@@ -127,7 +127,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
from gns3.modules import VMware
|
||||
settings = VMware.instance().settings()
|
||||
if not os.path.exists(settings["vmrun_path"]):
|
||||
QtWidgets.QMessageBox.critical(self, "VMware", "VMware vmrun tool could not be found, VMware or the VIX API (required for VMware player) is probably not installed. You can download it from https://www.vmware.com/support/developer/vix-api/. After installation you need to restart GNS3.")
|
||||
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()
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
self._border_color.green(),
|
||||
self._border_color.blue(),
|
||||
self._border_color.alpha()))
|
||||
self.uiRotationSpinBox.setValue(first_item.rotation())
|
||||
self.uiRotationSpinBox.setValue(int(first_item.rotation()))
|
||||
self.uiBorderWidthSpinBox.setValue(pen.width())
|
||||
index = self.uiBorderStyleComboBox.findData(pen.style())
|
||||
if index != -1:
|
||||
|
||||
@@ -58,7 +58,9 @@ class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
self.uiRotationLabel.hide()
|
||||
self.uiRotationSpinBox.hide()
|
||||
|
||||
link.setHovered(False) # make sure we use the right style
|
||||
pen = link.pen()
|
||||
link.setHovered(True)
|
||||
|
||||
self._border_color = pen.color()
|
||||
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
|
||||
@@ -102,6 +104,7 @@ class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
# Store values
|
||||
self._link.setLinkStyle(new_link_style)
|
||||
self._link.setHovered(False) # allow to see the new style
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
|
||||
@@ -67,6 +67,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
self._symbol_items = []
|
||||
self._parents = {}
|
||||
|
||||
Controller.instance().clearStaticCache() # TODO: use etag to know when to refresh the cache
|
||||
Controller.instance().get("/symbols", self._listSymbolsCallback)
|
||||
|
||||
def _listSymbolsCallback(self, result, error=False, **kwargs):
|
||||
@@ -108,6 +109,9 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
item.setIcon(0, icon)
|
||||
|
||||
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
|
||||
|
||||
for parent in self._parents.values():
|
||||
parent.sortChildren(0, QtCore.Qt.AscendingOrder)
|
||||
self.adjustSize()
|
||||
|
||||
def _searchTextChangedSlot(self, text):
|
||||
@@ -119,13 +123,13 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
|
||||
"""
|
||||
text = self.uiSearchLineEdit.text()
|
||||
for item in self._symbol_items:
|
||||
if not item.data(0, QtCore.Qt.UserRole).builtin():
|
||||
item.setHidden(True)
|
||||
# if not item.data(0, QtCore.Qt.UserRole).builtin():
|
||||
# item.setHidden(True)
|
||||
# else:
|
||||
if not text.strip() or text.strip().lower() in item.text(0).lower():
|
||||
item.setHidden(False)
|
||||
else:
|
||||
if len(text.strip()) == 0 or text.strip().lower() in item.text(0).lower():
|
||||
item.setHidden(False)
|
||||
else:
|
||||
item.setHidden(True)
|
||||
item.setHidden(True)
|
||||
|
||||
def _customSymbolToggledSlot(self, checked):
|
||||
"""
|
||||
|
||||
@@ -44,7 +44,7 @@ class TextEditorDialog(QtWidgets.QDialog, Ui_TextEditorDialog):
|
||||
# use the first item in the list as the model
|
||||
first_item = items[0]
|
||||
self._setColor(first_item.defaultTextColor())
|
||||
self.uiRotationSpinBox.setValue(first_item.rotation())
|
||||
self.uiRotationSpinBox.setValue(int(first_item.rotation()))
|
||||
self.uiPlainTextEdit.setPlainText(first_item.toPlainText())
|
||||
self.uiPlainTextEdit.setFont(first_item.font())
|
||||
|
||||
|
||||
@@ -121,8 +121,6 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
def setZoom(self, zoom):
|
||||
"""
|
||||
Sets zoom of the Graphics View
|
||||
:param zoom:
|
||||
:return:
|
||||
"""
|
||||
if zoom:
|
||||
factor = zoom / 100.
|
||||
@@ -193,6 +191,9 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# clear all objects on the scene
|
||||
self.scene().clear()
|
||||
|
||||
# reset zoom / scale
|
||||
self.resetTransform()
|
||||
|
||||
|
||||
def _loadSettings(self):
|
||||
"""
|
||||
@@ -326,6 +327,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# connect the signals that let the graphics view knows about events such as
|
||||
# a new link creation or deletion.
|
||||
if self._topology.addLink(link):
|
||||
source_node.addLink(link)
|
||||
destination_node.addLink(link)
|
||||
link.add_link_signal.connect(self.addLinkSlot)
|
||||
link.delete_link_signal.connect(self.deleteLinkSlot)
|
||||
|
||||
@@ -471,7 +474,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
#QtWidgets.QApplication.sendEvent(self, context_event)
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_note:
|
||||
pos = self.mapToScene(event.pos())
|
||||
note = self.createDrawingItem("text", pos.x(), pos.y(), 2)
|
||||
note = self.createDrawingItem("text", int(pos.x()), int(pos.y()), 2)
|
||||
pos_x = note.pos().x()
|
||||
pos_y = note.pos().y() - (note.boundingRect().height() / 2)
|
||||
note.setPos(pos_x, pos_y)
|
||||
@@ -481,19 +484,19 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_note = False
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_rectangle:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("rect", pos.x(), pos.y(), 1)
|
||||
self.createDrawingItem("rect", int(pos.x()), int(pos.y()), 1)
|
||||
self._main_window.uiDrawRectangleAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_rectangle = False
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_ellipse:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("ellipse", pos.x(), pos.y(), 1)
|
||||
self.createDrawingItem("ellipse", int(pos.x()), int(pos.y()), 1)
|
||||
self._main_window.uiDrawEllipseAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_ellipse = False
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_line:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("line", pos.x(), pos.y(), 1)
|
||||
self.createDrawingItem("line", int(pos.x()), int(pos.y()), 1)
|
||||
self._main_window.uiDrawLineAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_line = False
|
||||
@@ -657,7 +660,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if not self._adding_link:
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
item.setSelected(True)
|
||||
if item.node().status() == Node.stopped or item.node().consoleType() == "none":
|
||||
if item.node().status() == Node.stopped or item.node().consoleType() == "none" or item.node().consoleType() is None:
|
||||
self.configureSlot()
|
||||
return
|
||||
else:
|
||||
@@ -710,6 +713,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
:param event: QDropEvent instance
|
||||
"""
|
||||
|
||||
log.debug("Drop event received with mime data: {}".format(event.mimeData().formats()))
|
||||
# check if what has been dropped is handled by this view
|
||||
if event.mimeData().hasFormat("application/x-gns3-template"):
|
||||
template_id = event.mimeData().data("application/x-gns3-template").data().decode()
|
||||
@@ -721,8 +725,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
integer, ok = QtWidgets.QInputDialog.getInt(self, "Nodes", "Number of nodes:", 2, 1, 100, 1)
|
||||
if ok:
|
||||
for node_number in range(integer):
|
||||
x = event.pos().x() - (150 / 2) + (node_number % max_nodes_per_line) * offset
|
||||
y = event.pos().y() - (70 / 2) + (node_number // max_nodes_per_line) * offset
|
||||
x = event.pos().x() - (150 // 2) + (node_number % max_nodes_per_line) * offset
|
||||
y = event.pos().y() - (70 // 2) + (node_number // max_nodes_per_line) * offset
|
||||
if self.createNodeFromTemplateId(template_id, QtCore.QPoint(x, y)) is False:
|
||||
event.ignore()
|
||||
break
|
||||
@@ -1409,7 +1413,15 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
type = "rect"
|
||||
else:
|
||||
type = "image"
|
||||
self.createDrawingItem(type, item.pos().x() + 20, item.pos().y() + 20, item.zValue(), rotation=item.rotation(), svg=item.toSvg())
|
||||
|
||||
self.createDrawingItem(
|
||||
type,
|
||||
int(item.pos().x()) + 20,
|
||||
int(item.pos().y()) + 20,
|
||||
item.zValue(),
|
||||
rotation=item.rotation(),
|
||||
svg=item.toSvg()
|
||||
)
|
||||
elif isinstance(item, NodeItem):
|
||||
item.node().duplicate(item.pos().x() + 20, item.pos().y() + 20, item.zValue())
|
||||
|
||||
@@ -1661,11 +1673,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
x = left
|
||||
while x < rect.right():
|
||||
painter.drawLine(x, rect.top(), x, rect.bottom())
|
||||
painter.drawLine(x, int(rect.top()), x, int(rect.bottom()))
|
||||
x += grid
|
||||
y = top
|
||||
while y < rect.bottom():
|
||||
painter.drawLine(rect.left(), y, rect.right(), y)
|
||||
painter.drawLine(int(rect.left()), y, int(rect.right()), y)
|
||||
y += grid
|
||||
painter.restore()
|
||||
|
||||
|
||||
@@ -404,17 +404,12 @@ class HTTPClient(QtCore.QObject):
|
||||
self._query_waiting_connections = []
|
||||
return
|
||||
|
||||
if params["version"].split("-")[0] != __version__.split("-")[0]:
|
||||
if params["version"].split("+")[0] != __version__.split("+")[0]:
|
||||
msg = "Client version {} is not the same as server (controller) version {}".format(__version__, params["version"])
|
||||
# Stable release
|
||||
if __version_info__[3] == 0:
|
||||
log.error(msg)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
callback({"message": msg}, error=True, server=server)
|
||||
return
|
||||
# We don't allow different major version to interact even with dev build
|
||||
elif parse_version(__version__)[:2] != parse_version(params["version"])[:2]:
|
||||
# We don't allow different versions to interact even with dev build
|
||||
# (excepting post release corrections e.g 2.2.32.1, occassionally done when fixing a packaging problem)
|
||||
# TODO: we should probably follow this standard starting with v3.0: https://semver.org/
|
||||
if parse_version(__version__)[:3] != parse_version(params["version"])[:3]:
|
||||
log.error(msg)
|
||||
for request, callback in self._query_waiting_connections:
|
||||
if callback is not None:
|
||||
@@ -554,14 +549,13 @@ class HTTPClient(QtCore.QObject):
|
||||
query_string = self._paramsToQueryString(params)
|
||||
|
||||
log.debug("{method} {protocol}://{host}:{port}{prefix}{path} {body}{query_string}".format(method=method, protocol=self._protocol, host=host, port=self._port, path=path, body=body, prefix=prefix, query_string=query_string))
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{path}{query_string}".format(protocol=self._protocol, host=host, port=self._port, path=path, prefix=prefix, query_string=query_string))
|
||||
|
||||
if self._user:
|
||||
url = QtCore.QUrl("{protocol}://{user}@{host}:{port}{prefix}{path}{query_string}".format(protocol=self._protocol, user=self._user, host=host, port=self._port, path=path, prefix=prefix, query_string=query_string))
|
||||
else:
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{path}{query_string}".format(protocol=self._protocol, host=host, port=self._port, path=path, prefix=prefix, query_string=query_string))
|
||||
url.setUserName(self._user)
|
||||
|
||||
request = self._request(url)
|
||||
|
||||
request = self._addAuth(request)
|
||||
|
||||
request.setRawHeader(b"User-Agent", "GNS3 QT Client v{version}".format(version=__version__).encode())
|
||||
|
||||
# By default QT doesn't support GET with body even if it's in the RFC that's why we need to use sendCustomRequest
|
||||
@@ -759,10 +753,10 @@ class HTTPClient(QtCore.QObject):
|
||||
host = self._getHostForQuery()
|
||||
|
||||
log.debug("{method} {protocol}://{host}:{port}{prefix}{endpoint}".format(method=method, protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
|
||||
|
||||
if self._user:
|
||||
url = QtCore.QUrl("{protocol}://{user}@{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, user=self._user, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
|
||||
else:
|
||||
url = QtCore.QUrl("{protocol}://{host}:{port}{prefix}{endpoint}".format(protocol=self._protocol, host=host, port=self._port, prefix=prefix, endpoint=endpoint))
|
||||
url.setUserName(self._user)
|
||||
|
||||
request = self._request(url)
|
||||
request = self._addAuth(request)
|
||||
|
||||
@@ -213,7 +213,7 @@ class DrawingItem:
|
||||
|
||||
def itemChange(self, change, value):
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self._main_window.uiSnapToGridAction.isChecked():
|
||||
grid_size = self._graphics_view.drawingGridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
value.setX((grid_size * round((value.x() + mid_x) / grid_size)) - mid_x)
|
||||
@@ -246,7 +246,7 @@ class DrawingItem:
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
@@ -165,7 +165,7 @@ class LabelItem(QtWidgets.QGraphicsTextItem):
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x(), center.y()), zval)
|
||||
|
||||
@@ -320,13 +320,13 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
if not sip_is_deleted(self):
|
||||
# create the contextual menu
|
||||
self.setHovered(True)
|
||||
self.setAcceptHoverEvents(False)
|
||||
menu = QtWidgets.QMenu()
|
||||
self.populateLinkContextualMenu(menu)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
self.setAcceptHoverEvents(True)
|
||||
self._hovered = False
|
||||
self.adjust()
|
||||
self.setHovered(False)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
|
||||
@@ -108,6 +108,9 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
if node.initialized():
|
||||
self.createdSlot(node.id())
|
||||
|
||||
if self._main_window.uiSnapToGridAction.isChecked():
|
||||
self.setPos(QtCore.QPointF(self._node.x() + 0.1, self._node.y()))
|
||||
|
||||
def updateNode(self):
|
||||
"""
|
||||
Sync change to the node
|
||||
@@ -466,7 +469,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
:param value: value of the change
|
||||
"""
|
||||
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isActive() and self._main_window.uiSnapToGridAction.isChecked():
|
||||
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self._main_window.uiSnapToGridAction.isChecked():
|
||||
grid_size = self._main_window.uiGraphicsView.nodeGridSize()
|
||||
mid_x = self.boundingRect().width() / 2
|
||||
value.setX((grid_size * round((value.x() + mid_x) / grid_size)) - mid_x)
|
||||
@@ -507,7 +510,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
center = self.mapFromItem(self, brect.width() / 2.0, brect.height() / 2.0)
|
||||
painter.setBrush(QtCore.Qt.red)
|
||||
painter.setPen(QtCore.Qt.red)
|
||||
painter.drawRect((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20)
|
||||
painter.drawRect(QtCore.QRectF((brect.width() / 2.0) - 10, (brect.height() / 2.0) - 10, 20, 20))
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
if self.show_layer:
|
||||
text = str(int(self.zValue())) # Z value
|
||||
|
||||
@@ -91,8 +91,6 @@ class Link(QtCore.QObject):
|
||||
|
||||
self._nodes = []
|
||||
self._link_style = {}
|
||||
self._source_node.addLink(self)
|
||||
self._destination_node.addLink(self)
|
||||
|
||||
body = self._prepareParams()
|
||||
if self._link_id:
|
||||
|
||||
@@ -193,7 +193,11 @@ class LocalServer(QtCore.QObject):
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
|
||||
from gns3.utils.macos_ubridge_setuid import macos_ubridge_setuid
|
||||
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
|
||||
macos_ubridge_setuid()
|
||||
else:
|
||||
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set root permissions to uBridge {}: {}".format(path, str(e)))
|
||||
return False
|
||||
|
||||
@@ -220,10 +220,14 @@ def main():
|
||||
if not options.debug:
|
||||
try:
|
||||
# hide the console
|
||||
# win32console.AllocConsole()
|
||||
console_window = win32console.GetConsoleWindow()
|
||||
win32gui.ShowWindow(console_window, win32con.SW_HIDE)
|
||||
if console_window:
|
||||
win32gui.ShowWindow(console_window, win32con.SW_HIDE)
|
||||
else:
|
||||
log.warning("Could not get the console window")
|
||||
except win32console.error as e:
|
||||
print("warning: could not allocate console: {}".format(e))
|
||||
log.warning("Could not allocate console: {}".format(e))
|
||||
|
||||
local_config = LocalConfig.instance()
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ from .topology import Topology
|
||||
from .http_client import HTTPClient
|
||||
from .progress import Progress
|
||||
from .update_manager import UpdateManager
|
||||
from .utils.analytics import AnalyticsClient
|
||||
from .dialogs.appliance_wizard import ApplianceWizard
|
||||
from .dialogs.new_template_wizard import NewTemplateWizard
|
||||
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
|
||||
@@ -133,7 +132,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self._local_config_timer = QtCore.QTimer(self)
|
||||
self._local_config_timer.timeout.connect(local_config.checkConfigChanged)
|
||||
self._local_config_timer.start(1000) # milliseconds
|
||||
self._analytics_client = AnalyticsClient()
|
||||
self._template_manager = TemplateManager().instance()
|
||||
self._appliance_manager = ApplianceManager().instance()
|
||||
|
||||
@@ -234,6 +232,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiShowGridAction.triggered.connect(self._showGridActionSlot)
|
||||
self.uiSnapToGridAction.triggered.connect(self._snapToGridActionSlot)
|
||||
self.uiLockAllAction.triggered.connect(self._lockActionSlot)
|
||||
self.uiResetDocksAction.triggered.connect(self._resetDocksSlot)
|
||||
|
||||
# tool menu connections
|
||||
self.uiWebUIAction.triggered.connect(self._openWebInterfaceActionSlot)
|
||||
@@ -245,6 +244,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiReloadAllAction.triggered.connect(self._reloadAllActionSlot)
|
||||
self.uiAuxConsoleAllAction.triggered.connect(self._auxConsoleAllActionSlot)
|
||||
self.uiConsoleAllAction.triggered.connect(self._consoleAllActionSlot)
|
||||
self.uiResetConsoleAllAction.triggered.connect(self._consoleResetAllActionSlot)
|
||||
|
||||
# device menu is contextual and is build on-the-fly
|
||||
self.uiDeviceMenu.aboutToShow.connect(self._deviceMenuActionSlot)
|
||||
@@ -366,12 +366,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
item.updateNode()
|
||||
item.update()
|
||||
|
||||
def analyticsClient(self):
|
||||
def _resetDocksSlot(self):
|
||||
"""
|
||||
Return the analytics client
|
||||
Reset the dock widgets.
|
||||
"""
|
||||
|
||||
return self._analytics_client
|
||||
self.uiTopologySummaryDockWidget.setFloating(False)
|
||||
self.uiComputeSummaryDockWidget.setFloating(False)
|
||||
self.uiConsoleDockWidget.setFloating(False)
|
||||
self.uiNodesDockWidget.setFloating(False)
|
||||
|
||||
def _newProjectActionSlot(self):
|
||||
"""
|
||||
@@ -859,6 +862,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
if project is not None:
|
||||
project.reload_all_nodes()
|
||||
|
||||
def _consoleResetAllActionSlot(self):
|
||||
"""
|
||||
Slot called when reset all console connections.
|
||||
"""
|
||||
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.reset_console_all_nodes()
|
||||
|
||||
def _deviceMenuActionSlot(self):
|
||||
"""
|
||||
Slot to contextually show the device menu.
|
||||
@@ -1123,8 +1135,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
progress.setCancelButtonText("Force quit")
|
||||
|
||||
log.debug("Close the Main Window")
|
||||
self._analytics_client.sendScreenView("Main Window", session_start=False)
|
||||
|
||||
self._finish_application_closing(close_windows=False)
|
||||
event.accept()
|
||||
self.uiConsoleTextEdit.closeIO()
|
||||
@@ -1213,7 +1223,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Controller.instance().connected_signal.connect(self._controllerConnectedSlot)
|
||||
Controller.instance().project_list_updated_signal.connect(self.updateRecentProjectActions)
|
||||
|
||||
self._analytics_client.sendScreenView("Main Window")
|
||||
self.uiGraphicsView.setEnabled(False)
|
||||
|
||||
# show the setup wizard
|
||||
|
||||
@@ -52,6 +52,7 @@ class CloudPreferencesPage(QtWidgets.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
self.uiEditCloudNodePushButton.clicked.connect(self._editCloudNodeSlot)
|
||||
self.uiDeleteCloudNodePushButton.clicked.connect(self._deleteCloudNodeSlot)
|
||||
self.uiCloudNodesTreeWidget.itemSelectionChanged.connect(self._cloudNodeChangedSlot)
|
||||
self.uiCloudNodesTreeWidget.itemDoubleClicked.connect(self._editCloudNodeSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -53,6 +53,8 @@ class EthernetHubPreferencesPage(QtWidgets.QWidget, Ui_EthernetHubPreferencesPag
|
||||
self.uiEditEthernetHubPushButton.clicked.connect(self._editEthernetHubSlot)
|
||||
self.uiDeleteEthernetHubPushButton.clicked.connect(self._deleteEthernetHubSlot)
|
||||
self.uiEthernetHubsTreeWidget.itemSelectionChanged.connect(self._ethernetHubChangedSlot)
|
||||
self.uiEthernetHubsTreeWidget.itemDoubleClicked.connect(self._editEthernetHubSlot)
|
||||
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -53,6 +53,7 @@ class EthernetSwitchPreferencesPage(QtWidgets.QWidget, Ui_EthernetSwitchPreferen
|
||||
self.uiEditEthernetSwitchPushButton.clicked.connect(self._editEthernetSwitchSlot)
|
||||
self.uiDeleteEthernetSwitchPushButton.clicked.connect(self._deleteEthernetSwitchSlot)
|
||||
self.uiEthernetSwitchesTreeWidget.itemSelectionChanged.connect(self._ethernetSwitchChangedSlot)
|
||||
self.uiEthernetSwitchesTreeWidget.itemDoubleClicked.connect(self._editEthernetSwitchSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -116,6 +116,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():
|
||||
"""
|
||||
|
||||
@@ -52,6 +52,7 @@ class DockerVMPreferencesPage(QtWidgets.QWidget, Ui_DockerVMPreferencesPageWidge
|
||||
self.uiEditDockerVMPushButton.clicked.connect(self._dockerImageEditSlot)
|
||||
self.uiDeleteDockerVMPushButton.clicked.connect(self._dockerImageDeleteSlot)
|
||||
self.uiDockerVMsTreeWidget.itemSelectionChanged.connect(self._dockerImageChangedSlot)
|
||||
self.uiDockerVMsTreeWidget.itemDoubleClicked.connect(self._dockerImageEditSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -175,11 +175,26 @@
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QComboBox" name="uiConsoleResolutionComboBox">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>2560x1440</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1920x1080</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1680x1050</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1440x900</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1366x768</string>
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/docker/ui/docker_vm_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.11.3
|
||||
# Created by: PyQt5 UI code generator 5.15.7
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_dockerVMConfigPageWidget(object):
|
||||
def setupUi(self, dockerVMConfigPageWidget):
|
||||
dockerVMConfigPageWidget.setObjectName("dockerVMConfigPageWidget")
|
||||
@@ -100,6 +103,9 @@ class Ui_dockerVMConfigPageWidget(object):
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.uiConsoleResolutionComboBox.addItem("")
|
||||
self.gridLayout.addWidget(self.uiConsoleResolutionComboBox, 8, 1, 1, 1)
|
||||
self.label = QtWidgets.QLabel(self.tab)
|
||||
self.label.setObjectName("label")
|
||||
@@ -190,13 +196,16 @@ class Ui_dockerVMConfigPageWidget(object):
|
||||
self.uiConsoleTypeComboBox.setItemText(4, _translate("dockerVMConfigPageWidget", "none"))
|
||||
self.uiConsoleAutoStartCheckBox.setText(_translate("dockerVMConfigPageWidget", "Auto start console"))
|
||||
self.uiConsoleResolutionLabel.setText(_translate("dockerVMConfigPageWidget", "VNC console resolution:"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(0, _translate("dockerVMConfigPageWidget", "1920x1080"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(1, _translate("dockerVMConfigPageWidget", "1366x768"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(2, _translate("dockerVMConfigPageWidget", "1280x1024"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(3, _translate("dockerVMConfigPageWidget", "1280x800"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(4, _translate("dockerVMConfigPageWidget", "1024x768"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(5, _translate("dockerVMConfigPageWidget", "800x600"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(6, _translate("dockerVMConfigPageWidget", "640x480"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(0, _translate("dockerVMConfigPageWidget", "2560x1440"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(1, _translate("dockerVMConfigPageWidget", "1920x1080"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(2, _translate("dockerVMConfigPageWidget", "1680x1050"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(3, _translate("dockerVMConfigPageWidget", "1440x900"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(4, _translate("dockerVMConfigPageWidget", "1366x768"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(5, _translate("dockerVMConfigPageWidget", "1280x1024"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(6, _translate("dockerVMConfigPageWidget", "1280x800"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(7, _translate("dockerVMConfigPageWidget", "1024x768"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(8, _translate("dockerVMConfigPageWidget", "800x600"))
|
||||
self.uiConsoleResolutionComboBox.setItemText(9, _translate("dockerVMConfigPageWidget", "640x480"))
|
||||
self.label.setText(_translate("dockerVMConfigPageWidget", "HTTP port in the container:"))
|
||||
self.label_2.setText(_translate("dockerVMConfigPageWidget", "HTTP path:"))
|
||||
self.uiEnvironmentLabel.setText(_translate("dockerVMConfigPageWidget", "Environment variables:\n"
|
||||
@@ -217,4 +226,3 @@ class Ui_dockerVMConfigPageWidget(object):
|
||||
self.uiExtraVolumeTextEdit.setPlaceholderText(_translate("dockerVMConfigPageWidget", "e.g. /etc/sysctl.d"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab_2), _translate("dockerVMConfigPageWidget", "Advanced"))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab_3), _translate("dockerVMConfigPageWidget", "Usage"))
|
||||
|
||||
|
||||
@@ -310,8 +310,8 @@ class Router(Node):
|
||||
|
||||
# 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.
|
||||
if re.search(r"""^[\-\w]+$""", hostname) and len(hostname) <= 63:
|
||||
# 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
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ class IOSRouterPreferencesPage(QtWidgets.QWidget, Ui_IOSRouterPreferencesPageWid
|
||||
self.uiDeleteIOSRouterPushButton.clicked.connect(self._iosRouterDeleteSlot)
|
||||
self.uiIOSRoutersTreeWidget.itemSelectionChanged.connect(self._iosRouterChangedSlot)
|
||||
self.uiDecompressIOSPushButton.clicked.connect(self._decompressIOSSlot)
|
||||
self.uiIOSRoutersTreeWidget.itemDoubleClicked.connect(self._iosRouterEditSlot)
|
||||
|
||||
def _iosRouterChangedSlot(self):
|
||||
"""
|
||||
|
||||
@@ -131,8 +131,8 @@ class IOUDevice(Node):
|
||||
|
||||
# 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.
|
||||
if re.search(r"""^[\-\w]+$""", hostname) and len(hostname) <= 63:
|
||||
# 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
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ class IOUDevicePreferencesPage(QtWidgets.QWidget, Ui_IOUDevicePreferencesPageWid
|
||||
self.uiEditIOUDevicePushButton.clicked.connect(self._iouDeviceEditSlot)
|
||||
self.uiDeleteIOUDevicePushButton.clicked.connect(self._iouDeviceDeleteSlot)
|
||||
self.uiIOUDevicesTreeWidget.itemSelectionChanged.connect(self._iouDeviceChangedSlot)
|
||||
self.uiIOUDevicesTreeWidget.itemDoubleClicked.connect(self._iouDeviceEditSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -157,6 +157,7 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
|
||||
|
||||
if self.uiHdaDiskImageLineEdit.text().strip():
|
||||
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text().strip()
|
||||
settings["hda_disk_interface"] = "ide"
|
||||
|
||||
if self.uiLegacyASACheckBox.isChecked():
|
||||
# special settings for legacy ASA VM
|
||||
|
||||
@@ -102,6 +102,7 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
|
||||
# Supported NIC models: e1000, e1000-82544gc, e1000-82545em, e1000e, i82550, i82551, i82557a, i82557b, i82557c, i82558a
|
||||
# i82558b, i82559a, i82559b, i82559c, i82559er, i82562, i82801, ne2k_pci, pcnet, rocker, rtl8139, virtio-net-pci, vmxnet3
|
||||
# This list can be retrieved using "qemu-system-x86_64 -nic model=?" or "qemu-system-x86_64 -device help"
|
||||
self._legacy_devices = ("e1000", "i82551", "i82557b", "i82559er", "ne2k_pci", "pcnet", "rtl8139", "virtio")
|
||||
self._qemu_network_devices = OrderedDict([("e1000", "Intel Gigabit Ethernet"),
|
||||
("e1000-82544gc", "Intel 82544GC Gigabit Ethernet"),
|
||||
@@ -154,6 +155,9 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
for device_name, device_description in self._qemu_network_devices.items():
|
||||
if legacy_networking and device_name not in self._legacy_devices:
|
||||
continue
|
||||
# special case for virtio legacy networking
|
||||
if not legacy_networking and device_name == "virtio":
|
||||
continue
|
||||
self.uiAdapterTypesComboBox.addItem("{} ({})".format(device_description, device_name), device_name)
|
||||
|
||||
@staticmethod
|
||||
@@ -421,7 +425,9 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
if nic in self._legacy_devices:
|
||||
network_devices[nic] = desc
|
||||
else:
|
||||
network_devices = self._qemu_network_devices
|
||||
network_devices = self._qemu_network_devices.copy()
|
||||
# special case for virtio legacy networking
|
||||
network_devices.pop("virtio")
|
||||
|
||||
dialog = CustomAdaptersConfigurationDialog(ports, self._custom_adapters, default_adapter, network_devices, base_mac_address, parent=self)
|
||||
dialog.show()
|
||||
@@ -572,6 +578,8 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
self.uiProcessPriorityComboBox.setCurrentIndex(index)
|
||||
self.uiQemuOptionsLineEdit.setText(settings["options"])
|
||||
self.uiUsageTextEdit.setPlainText(settings["usage"])
|
||||
self.uiTPMCheckBox.setChecked(settings["tpm"])
|
||||
self.uiUEFICheckBox.setChecked(settings["uefi"])
|
||||
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
@@ -686,4 +694,6 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
settings["process_priority"] = self.uiProcessPriorityComboBox.currentText().lower()
|
||||
settings["options"] = self.uiQemuOptionsLineEdit.text()
|
||||
settings["usage"] = self.uiUsageTextEdit.toPlainText()
|
||||
settings["tpm"] = self.uiTPMCheckBox.isChecked()
|
||||
settings["uefi"] = self.uiUEFICheckBox.isChecked()
|
||||
return settings
|
||||
|
||||
@@ -54,6 +54,7 @@ class QemuVMPreferencesPage(QtWidgets.QWidget, Ui_QemuVMPreferencesPageWidget):
|
||||
self.uiEditQemuVMPushButton.clicked.connect(self._qemuVMEditSlot)
|
||||
self.uiDeleteQemuVMPushButton.clicked.connect(self._qemuVMDeleteSlot)
|
||||
self.uiQemuVMsTreeWidget.itemSelectionChanged.connect(self._qemuVMChangedSlot)
|
||||
self.uiQemuVMsTreeWidget.itemDoubleClicked.connect(self._qemuVMEditSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
QEMU VM implementation.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from gns3.node import Node
|
||||
from .settings import QEMU_VM_SETTINGS
|
||||
|
||||
@@ -72,6 +74,8 @@ class QemuVM(Node):
|
||||
"mac_address": QEMU_VM_SETTINGS["mac_address"],
|
||||
"legacy_networking": QEMU_VM_SETTINGS["legacy_networking"],
|
||||
"replicate_network_connection_state": QEMU_VM_SETTINGS["replicate_network_connection_state"],
|
||||
"tpm": QEMU_VM_SETTINGS["tpm"],
|
||||
"uefi": QEMU_VM_SETTINGS["uefi"],
|
||||
"create_config_disk": QEMU_VM_SETTINGS["create_config_disk"],
|
||||
"platform": QEMU_VM_SETTINGS["platform"],
|
||||
"on_close": QEMU_VM_SETTINGS["on_close"],
|
||||
@@ -163,6 +167,18 @@ class QemuVM(Node):
|
||||
from .pages.qemu_vm_configuration_page import QemuVMConfigurationPage
|
||||
return QemuVMConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def validateHostname(hostname):
|
||||
"""
|
||||
Checks if the hostname is valid.
|
||||
|
||||
:param hostname: hostname to check
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return QemuVM.isValidRfc1123Hostname(hostname)
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
|
||||
@@ -57,6 +57,8 @@ QEMU_VM_SETTINGS = {
|
||||
"mac_address": "",
|
||||
"legacy_networking": False,
|
||||
"replicate_network_connection_state": True,
|
||||
"tpm": False,
|
||||
"uefi": False,
|
||||
"create_config_disk": False,
|
||||
"on_close": "power_off",
|
||||
"platform": "",
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>941</width>
|
||||
<height>939</height>
|
||||
<width>478</width>
|
||||
<height>579</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -867,13 +867,6 @@
|
||||
<string>Additional settings</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiQemuOptionsLabel">
|
||||
<property name="text">
|
||||
<string>Options:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="uiQemuOptionsLineEdit">
|
||||
<property name="toolTip">
|
||||
@@ -890,7 +883,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiBaseVMCheckBox">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
@@ -900,10 +893,33 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiTPMCheckBox">
|
||||
<property name="text">
|
||||
<string>Enable Trusted Platform Module (TPM)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiQemuOptionsLabel">
|
||||
<property name="text">
|
||||
<string>Options:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiUEFICheckBox">
|
||||
<property name="text">
|
||||
<string>Enable UEFI boot mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<zorder>uiQemuOptionsLineEdit</zorder>
|
||||
<zorder>uiQemuOptionsLabel</zorder>
|
||||
<zorder>uiBaseVMCheckBox</zorder>
|
||||
<zorder>uiTPMCheckBox</zorder>
|
||||
<zorder>uiUEFICheckBox</zorder>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.13.2
|
||||
# Created by: PyQt5 UI code generator 5.15.9
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
@@ -13,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_QemuVMConfigPageWidget(object):
|
||||
def setupUi(self, QemuVMConfigPageWidget):
|
||||
QemuVMConfigPageWidget.setObjectName("QemuVMConfigPageWidget")
|
||||
QemuVMConfigPageWidget.resize(941, 939)
|
||||
QemuVMConfigPageWidget.resize(478, 579)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(QemuVMConfigPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiQemutabWidget = QtWidgets.QTabWidget(QemuVMConfigPageWidget)
|
||||
@@ -427,19 +428,27 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.groupBox.setObjectName("groupBox")
|
||||
self.gridLayout_3 = QtWidgets.QGridLayout(self.groupBox)
|
||||
self.gridLayout_3.setObjectName("gridLayout_3")
|
||||
self.uiQemuOptionsLabel = QtWidgets.QLabel(self.groupBox)
|
||||
self.uiQemuOptionsLabel.setObjectName("uiQemuOptionsLabel")
|
||||
self.gridLayout_3.addWidget(self.uiQemuOptionsLabel, 0, 0, 1, 1)
|
||||
self.uiQemuOptionsLineEdit = QtWidgets.QLineEdit(self.groupBox)
|
||||
self.uiQemuOptionsLineEdit.setObjectName("uiQemuOptionsLineEdit")
|
||||
self.gridLayout_3.addWidget(self.uiQemuOptionsLineEdit, 0, 1, 1, 1)
|
||||
self.uiBaseVMCheckBox = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.uiBaseVMCheckBox.setEnabled(True)
|
||||
self.uiBaseVMCheckBox.setObjectName("uiBaseVMCheckBox")
|
||||
self.gridLayout_3.addWidget(self.uiBaseVMCheckBox, 1, 0, 1, 2)
|
||||
self.gridLayout_3.addWidget(self.uiBaseVMCheckBox, 3, 0, 1, 2)
|
||||
self.uiTPMCheckBox = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.uiTPMCheckBox.setObjectName("uiTPMCheckBox")
|
||||
self.gridLayout_3.addWidget(self.uiTPMCheckBox, 1, 0, 1, 2)
|
||||
self.uiQemuOptionsLabel = QtWidgets.QLabel(self.groupBox)
|
||||
self.uiQemuOptionsLabel.setObjectName("uiQemuOptionsLabel")
|
||||
self.gridLayout_3.addWidget(self.uiQemuOptionsLabel, 0, 0, 1, 1)
|
||||
self.uiUEFICheckBox = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.uiUEFICheckBox.setObjectName("uiUEFICheckBox")
|
||||
self.gridLayout_3.addWidget(self.uiUEFICheckBox, 2, 0, 1, 2)
|
||||
self.uiQemuOptionsLineEdit.raise_()
|
||||
self.uiQemuOptionsLabel.raise_()
|
||||
self.uiBaseVMCheckBox.raise_()
|
||||
self.uiTPMCheckBox.raise_()
|
||||
self.uiUEFICheckBox.raise_()
|
||||
self.verticalLayout_2.addWidget(self.groupBox)
|
||||
spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout_2.addItem(spacerItem4)
|
||||
@@ -544,7 +553,6 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiProcessPriorityComboBox.setItemText(4, _translate("QemuVMConfigPageWidget", "Low"))
|
||||
self.uiProcessPriorityComboBox.setItemText(5, _translate("QemuVMConfigPageWidget", "Very low"))
|
||||
self.groupBox.setTitle(_translate("QemuVMConfigPageWidget", "Additional settings"))
|
||||
self.uiQemuOptionsLabel.setText(_translate("QemuVMConfigPageWidget", "Options:"))
|
||||
self.uiQemuOptionsLineEdit.setToolTip(_translate("QemuVMConfigPageWidget", "<html><head/><body><p>Variable replacements:</p>\n"
|
||||
"<ul>\n"
|
||||
"<li>%vm-name% =VM name</li>\n"
|
||||
@@ -556,5 +564,8 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
"</ul>\n"
|
||||
"</body></html>"))
|
||||
self.uiBaseVMCheckBox.setText(_translate("QemuVMConfigPageWidget", "Use as a linked base VM"))
|
||||
self.uiTPMCheckBox.setText(_translate("QemuVMConfigPageWidget", "Enable Trusted Platform Module (TPM)"))
|
||||
self.uiQemuOptionsLabel.setText(_translate("QemuVMConfigPageWidget", "Options:"))
|
||||
self.uiUEFICheckBox.setText(_translate("QemuVMConfigPageWidget", "Enable UEFI boot mode"))
|
||||
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.uiAdvancedSettingsTab), _translate("QemuVMConfigPageWidget", "Advanced"))
|
||||
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.uiUsageTab), _translate("QemuVMConfigPageWidget", "Usage"))
|
||||
|
||||
@@ -54,6 +54,7 @@ class VirtualBoxVMPreferencesPage(QtWidgets.QWidget, Ui_VirtualBoxVMPreferencesP
|
||||
self.uiEditVirtualBoxVMPushButton.clicked.connect(self._vboxVMEditSlot)
|
||||
self.uiDeleteVirtualBoxVMPushButton.clicked.connect(self._vboxVMDeleteSlot)
|
||||
self.uiVirtualBoxVMsTreeWidget.itemSelectionChanged.connect(self._vboxVMChangedSlot)
|
||||
self.uiVirtualBoxVMsTreeWidget.itemDoubleClicked.connect(self._vboxVMEditSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -53,6 +53,7 @@ class VMwareVMPreferencesPage(QtWidgets.QWidget, Ui_VMwareVMPreferencesPageWidge
|
||||
self.uiEditVMwareVMPushButton.clicked.connect(self._vmwareVMEditSlot)
|
||||
self.uiDeleteVMwareVMPushButton.clicked.connect(self._vmwareVMDeleteSlot)
|
||||
self.uiVMwareVMsTreeWidget.itemSelectionChanged.connect(self._vmwareVMChangedSlot)
|
||||
self.uiVMwareVMsTreeWidget.itemDoubleClicked.connect(self._vmwareVMEditSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
@@ -53,6 +53,7 @@ class VPCSNodePreferencesPage(QtWidgets.QWidget, Ui_VPCSNodePageWidget):
|
||||
self.uiEditVPCSPushButton.clicked.connect(self._editVPCSSlot)
|
||||
self.uiDeleteVPCSPushButton.clicked.connect(self._deleteVPCSSlot)
|
||||
self.uiVPCSTreeWidget.itemSelectionChanged.connect(self._vpcsChangedSlot)
|
||||
self.uiVPCSTreeWidget.itemDoubleClicked.connect(self._editVPCSSlot)
|
||||
|
||||
def _createSectionItem(self, name):
|
||||
"""
|
||||
|
||||
37
gns3/node.py
37
gns3/node.py
@@ -17,6 +17,7 @@
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
from gns3.controller import Controller
|
||||
from gns3.ports.ethernet_port import EthernetPort
|
||||
@@ -650,10 +651,11 @@ class Node(BaseNode):
|
||||
|
||||
if not console_type:
|
||||
console_type = self.consoleType()
|
||||
if console_type == "vnc":
|
||||
return general_settings["vnc_console_command"]
|
||||
elif console_type.startswith("spice"):
|
||||
return general_settings["spice_console_command"]
|
||||
if console_type:
|
||||
if console_type == "vnc":
|
||||
return general_settings["vnc_console_command"]
|
||||
elif console_type.startswith("spice"):
|
||||
return general_settings["spice_console_command"]
|
||||
return general_settings["telnet_console_command"]
|
||||
|
||||
def consoleType(self):
|
||||
@@ -863,6 +865,33 @@ class Node(BaseNode):
|
||||
if error and "message" in result:
|
||||
log.error("Error while import config: {}".format(result["message"]))
|
||||
|
||||
@staticmethod
|
||||
def isValidRfc1123Hostname(hostname):
|
||||
"""
|
||||
Validate a hostname according to RFC 1123
|
||||
|
||||
Each element of the hostname must be from 1 to 63 characters long
|
||||
and the entire hostname, including the dots, can be at most 253
|
||||
characters long. Valid characters for hostnames are ASCII
|
||||
letters from a to z, the digits from 0 to 9, and the hyphen (-).
|
||||
A hostname may not start with a hyphen.
|
||||
"""
|
||||
|
||||
if hostname[-1] == ".":
|
||||
hostname = hostname[:-1] # strip exactly one dot from the right, if present
|
||||
|
||||
if len(hostname) > 253:
|
||||
return False
|
||||
|
||||
labels = hostname.split(".")
|
||||
|
||||
# the TLD must be not all-numeric
|
||||
if re.match(r"[0-9]+$", labels[-1]):
|
||||
return False
|
||||
|
||||
allowed = re.compile(r"(?!-)[a-zA-Z0-9-]{1,63}(?<!-)$")
|
||||
return all(allowed.match(label) for label in labels)
|
||||
|
||||
@staticmethod
|
||||
def onCloseOptions():
|
||||
"""
|
||||
|
||||
@@ -151,6 +151,22 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
|
||||
self._showContextualMenu(event.globalPos())
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
"""
|
||||
Handles all mouse double click events.
|
||||
|
||||
:param event: QMouseEvent instance
|
||||
"""
|
||||
|
||||
item = self.itemAt(event.pos())
|
||||
if item:
|
||||
template = TemplateManager.instance().getTemplate(item.data(0, QtCore.Qt.UserRole))
|
||||
if template:
|
||||
configuration_page = TEMPLATE_TYPE_TO_CONFIGURATION_PAGE.get(template.template_type())
|
||||
if not template.builtin() and configuration_page:
|
||||
self._configurationSlot(template, configuration_page)
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""
|
||||
Handles all mouse move events.
|
||||
@@ -204,14 +220,14 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
|
||||
menu.exec_(pos)
|
||||
|
||||
def _configurationSlot(self, template, configuration_page, source):
|
||||
def _configurationSlot(self, template, configuration_page, source=None):
|
||||
|
||||
dialog = ConfigurationDialog(template.name(), template.settings(), configuration_page(), parent=self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
TemplateManager.instance().updateTemplate(template)
|
||||
|
||||
def _deleteSlot(self, template, source):
|
||||
def _deleteSlot(self, template, source=None):
|
||||
|
||||
reply = QtWidgets.QMessageBox.question(self, "Template", "Delete {} template?".format(template.name()),
|
||||
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
|
||||
|
||||
@@ -112,15 +112,20 @@ class PacketCapture:
|
||||
"""
|
||||
Starts the packet capture reader.
|
||||
"""
|
||||
|
||||
self._startPacketCommand(link, self.settings()["packet_capture_reader_command"])
|
||||
|
||||
def stopPacketCaptureReader(self, link):
|
||||
"""
|
||||
Stop the packet capture reader
|
||||
"""
|
||||
if link in self._tail_process and self._tail_process[link].poll() is None:
|
||||
|
||||
if link in self._tail_process:
|
||||
log.debug("Stopping packet capture reader for link {}".format(link.link_id()))
|
||||
self._tail_process[link].kill()
|
||||
try:
|
||||
self._tail_process[link].kill()
|
||||
except (PermissionError, OSError):
|
||||
pass
|
||||
del self._tail_process[link]
|
||||
|
||||
def startPacketCaptureAnalyzer(self, link):
|
||||
@@ -183,14 +188,14 @@ class PacketCapture:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Packet capture", "Can't create packet capture file {}: {}".format(capture_file_path, str(e)))
|
||||
return
|
||||
|
||||
if link in self._tail_process and self._tail_process[link].poll() is None:
|
||||
if link in self._tail_process:
|
||||
try:
|
||||
self._tail_process[link].kill()
|
||||
except (PermissionError, OSError):
|
||||
# Sometimes we have condition on windows where the process is in the process to quit
|
||||
pass
|
||||
del self._tail_process[link]
|
||||
if link in self._capture_reader_process and self._capture_reader_process[link].poll() is None:
|
||||
if link in self._capture_reader_process:
|
||||
try:
|
||||
self._capture_reader_process[link].kill()
|
||||
except (PermissionError, OSError):
|
||||
|
||||
@@ -301,7 +301,6 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
self.uiImagesPathLineEdit.setText(local_server["images_path"])
|
||||
self.uiConfigsPathLineEdit.setText(local_server["configs_path"])
|
||||
self.uiAppliancesPathLineEdit.setText(local_server["appliances_path"])
|
||||
self.uiStatsCheckBox.setChecked(settings["send_stats"])
|
||||
self.uiOverlayNotificationsCheckBox.setChecked(settings["overlay_notifications"])
|
||||
self.uiCrashReportCheckBox.setChecked(local_server["report_errors"])
|
||||
self.uiCheckForUpdateCheckBox.setChecked(settings["check_for_update"])
|
||||
@@ -411,7 +410,6 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
"vnc_console_command": self.uiVNCConsoleCommandLineEdit.text(),
|
||||
"spice_console_command": self.uiSPICEConsoleCommandLineEdit.text(),
|
||||
"delay_console_all": self.uiDelayConsoleAllSpinBox.value(),
|
||||
"send_stats": self.uiStatsCheckBox.isChecked(),
|
||||
"multi_profiles": self.uiMultiProfilesCheckBox.isChecked(),
|
||||
"direct_file_upload": self.uiDirectFileUpload.isChecked()
|
||||
}
|
||||
|
||||
@@ -365,6 +365,15 @@ class Project(QtCore.QObject):
|
||||
|
||||
Controller.instance().post("/projects/{project_id}/nodes/reload".format(project_id=self._id), None, body={}, timeout=None)
|
||||
|
||||
def reset_console_all_nodes(self):
|
||||
"""Reset console for all nodes belonging to this project"""
|
||||
|
||||
# Don't do anything if the project doesn't exist on the server
|
||||
if self._id is None:
|
||||
return
|
||||
|
||||
Controller.instance().post("/projects/{project_id}/nodes/console/reset".format(project_id=self._id), None, body={}, timeout=None)
|
||||
|
||||
def get(self, path, callback, **kwargs):
|
||||
"""
|
||||
HTTP GET on the remote server
|
||||
@@ -645,7 +654,7 @@ class Project(QtCore.QObject):
|
||||
@qslot
|
||||
def _websocket_error(self, error):
|
||||
if self._notification_stream:
|
||||
log.error(self._notification_stream.errorString())
|
||||
log.error("Websocket project notification stream error: {}".format(self._notification_stream.errorString()))
|
||||
self._notification_stream = None
|
||||
self._startListenNotifications()
|
||||
|
||||
|
||||
@@ -195,54 +195,6 @@ if hasattr(sys, '_called_from_test'):
|
||||
QtCore.pyqtSignal = FakeQtSignal
|
||||
|
||||
|
||||
class StatsQtWidgetsQWizard(QtWidgets.QWizard):
|
||||
"""
|
||||
Send stats from all the QWizard
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
from ..utils.analytics import AnalyticsClient
|
||||
name = self.__class__.__name__
|
||||
name = re.sub(r"([A-Z])", r" \1", name).strip()
|
||||
AnalyticsClient.instance().sendScreenView(name)
|
||||
|
||||
QtWidgets.QWizard = StatsQtWidgetsQWizard
|
||||
|
||||
|
||||
class StatsQtWidgetsQMainWindow(QtWidgets.QMainWindow):
|
||||
"""
|
||||
Send stats from all the QMainWindow
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
from ..utils.analytics import AnalyticsClient
|
||||
name = self.__class__.__name__
|
||||
name = re.sub(r"([A-Z])", r" \1", name).strip()
|
||||
AnalyticsClient.instance().sendScreenView(name)
|
||||
|
||||
QtWidgets.QMainWindow = StatsQtWidgetsQMainWindow
|
||||
|
||||
|
||||
class StatsQtWidgetsQDialog(QtWidgets.QDialog):
|
||||
"""
|
||||
Send stats from all the QWizard
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
from ..utils.analytics import AnalyticsClient
|
||||
name = self.__class__.__name__
|
||||
name = re.sub(r"([A-Z])", r" \1", name).strip()
|
||||
AnalyticsClient.instance().sendScreenView(name)
|
||||
|
||||
QtWidgets.QDialog = StatsQtWidgetsQDialog
|
||||
|
||||
|
||||
def qpartial(func, *args, **kwargs):
|
||||
"""
|
||||
A functools partial that you can use on qobject. If the targeted qobject is
|
||||
|
||||
@@ -59,7 +59,7 @@ class Appliance(collections.abc.Mapping):
|
||||
"""
|
||||
if "registry_version" not in self._appliance:
|
||||
raise ApplianceError("Invalid appliance configuration please report the issue on https://github.com/GNS3/gns3-registry")
|
||||
if self._appliance["registry_version"] > 6:
|
||||
if self._appliance["registry_version"] > 7:
|
||||
raise ApplianceError("Please update GNS3 in order to install this appliance")
|
||||
|
||||
with open(get_resource(os.path.join("schemas", "appliance.json"))) as f:
|
||||
|
||||
@@ -197,7 +197,8 @@ class ApplianceToTemplate:
|
||||
|
||||
url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol_id)
|
||||
try:
|
||||
self._downloadApplianceSymbol(url, path)
|
||||
if not self._downloadApplianceSymbol(url, path):
|
||||
return None
|
||||
controller.clearStaticCache()
|
||||
if controller.isRemote():
|
||||
controller.uploadSymbol(symbol_id, path)
|
||||
@@ -230,5 +231,7 @@ class ApplianceToTemplate:
|
||||
log.debug("Error while saving appliance symbol to '{}': {}".format(path, e))
|
||||
raise
|
||||
log.debug("Appliance symbol downloaded and saved to '{}'".format(path))
|
||||
return True
|
||||
else:
|
||||
log.warning("Error when downloading appliance symbol from '{}': {}".format(url, reply.errorString()))
|
||||
log.error("Error when downloading appliance symbol from '{}': {}".format(url, reply.errorString()))
|
||||
return False
|
||||
|
||||
@@ -350,6 +350,18 @@
|
||||
"maximum": 100,
|
||||
"title": "Throttle the CPU"
|
||||
},
|
||||
"tpm": {
|
||||
"type": "boolean",
|
||||
"title": "Enable the Trusted Platform Module (TPM)"
|
||||
},
|
||||
"uefi": {
|
||||
"type": "boolean",
|
||||
"title": "Enable the UEFI boot mode"
|
||||
},
|
||||
"on_close": {
|
||||
"title": "Action to execute on the VM is closed",
|
||||
"enum": ["power_off", "shutdown_signal", "save_vm_state"]
|
||||
},
|
||||
"process_priority": {
|
||||
"title": "Process priority for QEMU",
|
||||
"enum": ["realtime",
|
||||
|
||||
@@ -56,7 +56,6 @@ if sys.platform.startswith("win"):
|
||||
program_files_x86 = program_files = os.environ["PROGRAMFILES"]
|
||||
|
||||
PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {'Putty (normal standalone version)': 'putty_standalone.exe -telnet %h %p -loghost "%d"',
|
||||
'Putty (custom deprecated version)': 'putty.exe -telnet %h %p -wt "%d" -gns3 5 -skin 4',
|
||||
'MobaXterm': r'"{}\Mobatek\MobaXterm Personal Edition\MobaXterm.exe" -newtab "telnet %h %p"'.format(program_files_x86),
|
||||
'Royal TS V3': r'{}\code4ward.net\Royal TS V3\RTS3App.exe /connectadhoc:%h /adhoctype:terminal /p:IsTelnetConnection="true" /p:ConnectionType="telnet;Telnet Connection" /p:Port="%p" /p:Name="%d"'.format(program_files),
|
||||
'Royal TS V5': r'"{}\Royal TS V5\RoyalTS.exe" /protocol:terminal /using:adhoc /uri:"%h" /property:Port="%p" /property:IsTelnetConnection="true" /property:Name="%d"'.format(program_files_x86),
|
||||
@@ -65,6 +64,7 @@ if sys.platform.startswith("win"):
|
||||
'SecureCRT (personal profile)': r'"{}\AppData\Local\VanDyke Software\SecureCRT\SecureCRT.exe" /T /N "%d" /TELNET %h %p'.format(userprofile),
|
||||
'TeraTerm Pro': r'"{}\teraterm\ttermpro.exe" /W="%d" /M="ttstart.macro" /T=1 %h %p'.format(program_files_x86),
|
||||
'Telnet': 'telnet %h %p',
|
||||
'Windows Terminal': 'wt.exe -w 1 new-tab --title %d telnet %h %p',
|
||||
'Xshell 4': r'"{}\NetSarang\Xshell 4\xshell.exe" -url telnet://%h:%p'.format(program_files_x86),
|
||||
'Xshell 5': r'"{}\NetSarang\Xshell 5\xshell.exe" -url telnet://%h:%p -newtab %d'.format(program_files_x86),
|
||||
'ZOC 6': r'"{}\ZOC6\zoc.exe" "/TELNET:%h:%p" /TABBED "/TITLE:%d"'.format(program_files_x86)}
|
||||
@@ -141,7 +141,6 @@ elif sys.platform.startswith("darwin"):
|
||||
r""" -e 'end tell'""",
|
||||
'Royal TSX': "open 'rtsx://telnet%3A%2F%2F%h:%p'",
|
||||
'SecureCRT': '/Applications/SecureCRT.app/Contents/MacOS/SecureCRT /N "%d" /T /TELNET %h %p',
|
||||
'Windows Terminal': 'wt.exe -w 1 new-tab --title %d telnet %h %p',
|
||||
'ZOC 6': '/Applications/zoc6.app/Contents/MacOS/zoc6 "/TELNET:%h:%p" /TABBED "/TITLE:%d"',
|
||||
'ZOC 7': '/Applications/zoc7.app/Contents/MacOS/zoc7 "/TELNET:%h:%p" /TABBED "/TITLE:%d"',
|
||||
'ZOC 8': '/Applications/zoc8.app/Contents/MacOS/zoc8 "/TELNET:%h:%p" /TABBED "/TITLE:%d"'
|
||||
@@ -213,7 +212,7 @@ else:
|
||||
if sys.platform.startswith("win"):
|
||||
# Windows
|
||||
PRECONFIGURED_SPICE_CONSOLE_COMMANDS = {
|
||||
'Remote Viewer': r'"{}\VirtViewer v7.0-256\bin\remote-viewer.exe" spice://%h:%p'.format(program_files),
|
||||
'Remote Viewer': r'"{}\VirtViewer v11.0-256\bin\remote-viewer.exe" spice://%h:%p'.format(program_files),
|
||||
}
|
||||
|
||||
# default Windows SPICE console command
|
||||
@@ -285,7 +284,6 @@ GENERAL_SETTINGS = {
|
||||
"check_for_update": True,
|
||||
"overlay_notifications": True,
|
||||
"experimental_features": False,
|
||||
"send_stats": True,
|
||||
"stats_visitor_id": str(uuid.uuid4()), # An anonymous id for stats
|
||||
"last_check_for_update": 0,
|
||||
"telnet_console_command": DEFAULT_TELNET_CONSOLE_COMMAND,
|
||||
|
||||
@@ -415,3 +415,22 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
|
||||
for link in self._topology.links():
|
||||
if link.suspended():
|
||||
link.toggleSuspend()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles key press events
|
||||
"""
|
||||
|
||||
from .main_window import MainWindow
|
||||
view = MainWindow.instance().uiGraphicsView
|
||||
# only deleting a link or node is supported for now
|
||||
if event.key() == QtCore.Qt.Key_Delete:
|
||||
current_item = self.currentItem()
|
||||
if isinstance(current_item, TopologyNodeItem):
|
||||
current_item.node().delete()
|
||||
else:
|
||||
link = current_item.data(0, QtCore.Qt.UserRole)
|
||||
for item in view.scene().items():
|
||||
if isinstance(item, LinkItem) and item.link() == link:
|
||||
item.delete()
|
||||
super().keyPressEvent(event)
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>941</width>
|
||||
<height>910</height>
|
||||
<width>512</width>
|
||||
<height>652</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -223,7 +223,16 @@
|
||||
<string>Binary images</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -373,7 +382,16 @@
|
||||
<string>Console applications</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -474,7 +492,16 @@
|
||||
<string>VNC</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -943,7 +970,16 @@
|
||||
<string>Miscellaneous</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -966,16 +1002,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="uiStatsCheckBox">
|
||||
<property name="text">
|
||||
<string>Send anonymous usage statistics</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="uiOverlayNotificationsCheckBox">
|
||||
<property name="text">
|
||||
@@ -1098,7 +1124,6 @@
|
||||
<tabstop>uiDefaultNoteColorPushButton</tabstop>
|
||||
<tabstop>uiCheckForUpdateCheckBox</tabstop>
|
||||
<tabstop>uiCrashReportCheckBox</tabstop>
|
||||
<tabstop>uiStatsCheckBox</tabstop>
|
||||
<tabstop>uiOverlayNotificationsCheckBox</tabstop>
|
||||
<tabstop>uiExperimentalFeaturesCheckBox</tabstop>
|
||||
<tabstop>uiHdpiCheckBox</tabstop>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/general_preferences_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.13.2
|
||||
# Created by: PyQt5 UI code generator 5.15.6
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
@@ -13,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_GeneralPreferencesPageWidget(object):
|
||||
def setupUi(self, GeneralPreferencesPageWidget):
|
||||
GeneralPreferencesPageWidget.setObjectName("GeneralPreferencesPageWidget")
|
||||
GeneralPreferencesPageWidget.resize(941, 910)
|
||||
GeneralPreferencesPageWidget.resize(512, 652)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(GeneralPreferencesPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiMiscTabWidget = QtWidgets.QTabWidget(GeneralPreferencesPageWidget)
|
||||
@@ -446,10 +447,6 @@ class Ui_GeneralPreferencesPageWidget(object):
|
||||
self.uiCrashReportCheckBox.setChecked(True)
|
||||
self.uiCrashReportCheckBox.setObjectName("uiCrashReportCheckBox")
|
||||
self.verticalLayout_2.addWidget(self.uiCrashReportCheckBox)
|
||||
self.uiStatsCheckBox = QtWidgets.QCheckBox(self.uiMiscTab)
|
||||
self.uiStatsCheckBox.setChecked(True)
|
||||
self.uiStatsCheckBox.setObjectName("uiStatsCheckBox")
|
||||
self.verticalLayout_2.addWidget(self.uiStatsCheckBox)
|
||||
self.uiOverlayNotificationsCheckBox = QtWidgets.QCheckBox(self.uiMiscTab)
|
||||
self.uiOverlayNotificationsCheckBox.setObjectName("uiOverlayNotificationsCheckBox")
|
||||
self.verticalLayout_2.addWidget(self.uiOverlayNotificationsCheckBox)
|
||||
@@ -520,8 +517,7 @@ class Ui_GeneralPreferencesPageWidget(object):
|
||||
GeneralPreferencesPageWidget.setTabOrder(self.uiDefaultNoteFontPushButton, self.uiDefaultNoteColorPushButton)
|
||||
GeneralPreferencesPageWidget.setTabOrder(self.uiDefaultNoteColorPushButton, self.uiCheckForUpdateCheckBox)
|
||||
GeneralPreferencesPageWidget.setTabOrder(self.uiCheckForUpdateCheckBox, self.uiCrashReportCheckBox)
|
||||
GeneralPreferencesPageWidget.setTabOrder(self.uiCrashReportCheckBox, self.uiStatsCheckBox)
|
||||
GeneralPreferencesPageWidget.setTabOrder(self.uiStatsCheckBox, self.uiOverlayNotificationsCheckBox)
|
||||
GeneralPreferencesPageWidget.setTabOrder(self.uiCrashReportCheckBox, self.uiOverlayNotificationsCheckBox)
|
||||
GeneralPreferencesPageWidget.setTabOrder(self.uiOverlayNotificationsCheckBox, self.uiExperimentalFeaturesCheckBox)
|
||||
GeneralPreferencesPageWidget.setTabOrder(self.uiExperimentalFeaturesCheckBox, self.uiHdpiCheckBox)
|
||||
GeneralPreferencesPageWidget.setTabOrder(self.uiHdpiCheckBox, self.uiMultiProfilesCheckBox)
|
||||
@@ -601,7 +597,6 @@ class Ui_GeneralPreferencesPageWidget(object):
|
||||
self.uiMiscTabWidget.setTabText(self.uiMiscTabWidget.indexOf(self.uiSceneTab), _translate("GeneralPreferencesPageWidget", "Topology view"))
|
||||
self.uiCheckForUpdateCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Automatically check for update"))
|
||||
self.uiCrashReportCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Send anonymous crash reports"))
|
||||
self.uiStatsCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Send anonymous usage statistics"))
|
||||
self.uiOverlayNotificationsCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Display error, warning and info in an overlay popup"))
|
||||
self.uiExperimentalFeaturesCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Enable experimental features (dangerous, restart required)"))
|
||||
self.uiHdpiCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Enable HDPI mode (this may crash on Linux, restart required)"))
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>986</width>
|
||||
<height>716</height>
|
||||
<height>719</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="contextMenuPolicy">
|
||||
@@ -38,7 +38,16 @@ background-none;
|
||||
</property>
|
||||
<widget class="QWidget" name="uiCentralWidget">
|
||||
<layout class="QGridLayout">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
@@ -62,7 +71,7 @@ background-none;
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>986</width>
|
||||
<height>42</height>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="uiEditMenu">
|
||||
@@ -129,6 +138,7 @@ background-none;
|
||||
<addaction name="uiShowPortNamesAction"/>
|
||||
<addaction name="uiLockAllAction"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="uiResetDocksAction"/>
|
||||
<addaction name="uiDocksMenu"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="uiControlMenu">
|
||||
@@ -141,6 +151,7 @@ background-none;
|
||||
<addaction name="uiReloadAllAction"/>
|
||||
<addaction name="uiAuxConsoleAllAction"/>
|
||||
<addaction name="uiConsoleAllAction"/>
|
||||
<addaction name="uiResetConsoleAllAction"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="uiAnnotateMenu">
|
||||
<property name="title">
|
||||
@@ -228,7 +239,16 @@ background-none;
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -372,7 +392,16 @@ background-none;
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -438,7 +467,16 @@ background-none;
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QGridLayout">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
@@ -482,7 +520,16 @@ background-none;
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
@@ -1243,6 +1290,16 @@ background-none;
|
||||
<string>New template</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="uiResetDocksAction">
|
||||
<property name="text">
|
||||
<string>Reset docks</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="uiResetConsoleAllAction">
|
||||
<property name="text">
|
||||
<string>Reset all console connections</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/main_window.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.13.2
|
||||
# Created by: PyQt5 UI code generator 5.14.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -48,7 +48,7 @@ class Ui_MainWindow(object):
|
||||
self.gridlayout.addWidget(self.uiGraphicsView, 0, 0, 1, 1)
|
||||
MainWindow.setCentralWidget(self.uiCentralWidget)
|
||||
self.uiMenuBar = QtWidgets.QMenuBar(MainWindow)
|
||||
self.uiMenuBar.setGeometry(QtCore.QRect(0, 0, 986, 42))
|
||||
self.uiMenuBar.setGeometry(QtCore.QRect(0, 0, 986, 22))
|
||||
self.uiMenuBar.setObjectName("uiMenuBar")
|
||||
self.uiEditMenu = QtWidgets.QMenu(self.uiMenuBar)
|
||||
self.uiEditMenu.setObjectName("uiEditMenu")
|
||||
@@ -448,6 +448,13 @@ class Ui_MainWindow(object):
|
||||
self.uiNewTemplateAction = QtWidgets.QAction(MainWindow)
|
||||
self.uiNewTemplateAction.setIcon(icon)
|
||||
self.uiNewTemplateAction.setObjectName("uiNewTemplateAction")
|
||||
self.uiResetDocksAction = QtWidgets.QAction(MainWindow)
|
||||
self.uiResetDocksAction.setObjectName("uiResetDocksAction")
|
||||
self.uiEditReadmeAction = QtWidgets.QAction(MainWindow)
|
||||
self.uiEditReadmeAction.setIcon(icon31)
|
||||
self.uiEditReadmeAction.setObjectName("uiEditReadmeAction")
|
||||
self.uiResetConsoleAllAction = QtWidgets.QAction(MainWindow)
|
||||
self.uiResetConsoleAllAction.setObjectName("uiResetConsoleAllAction")
|
||||
self.uiEditMenu.addAction(self.uiSelectAllAction)
|
||||
self.uiEditMenu.addAction(self.uiSelectNoneAction)
|
||||
self.uiEditMenu.addSeparator()
|
||||
@@ -488,6 +495,7 @@ class Ui_MainWindow(object):
|
||||
self.uiViewMenu.addAction(self.uiShowPortNamesAction)
|
||||
self.uiViewMenu.addAction(self.uiLockAllAction)
|
||||
self.uiViewMenu.addSeparator()
|
||||
self.uiViewMenu.addAction(self.uiResetDocksAction)
|
||||
self.uiViewMenu.addAction(self.uiDocksMenu.menuAction())
|
||||
self.uiControlMenu.addAction(self.uiStartAllAction)
|
||||
self.uiControlMenu.addAction(self.uiSuspendAllAction)
|
||||
@@ -495,6 +503,7 @@ class Ui_MainWindow(object):
|
||||
self.uiControlMenu.addAction(self.uiReloadAllAction)
|
||||
self.uiControlMenu.addAction(self.uiAuxConsoleAllAction)
|
||||
self.uiControlMenu.addAction(self.uiConsoleAllAction)
|
||||
self.uiControlMenu.addAction(self.uiResetConsoleAllAction)
|
||||
self.uiAnnotateMenu.addAction(self.uiAddNoteAction)
|
||||
self.uiAnnotateMenu.addAction(self.uiInsertImageAction)
|
||||
self.uiAnnotateMenu.addAction(self.uiDrawRectangleAction)
|
||||
@@ -711,6 +720,10 @@ class Ui_MainWindow(object):
|
||||
self.uiLockAllAction.setToolTip(_translate("MainWindow", "Lock or unlock all items"))
|
||||
self.uiWebUIAction.setText(_translate("MainWindow", "Web UI - beta"))
|
||||
self.uiNewTemplateAction.setText(_translate("MainWindow", "New template"))
|
||||
self.uiResetDocksAction.setText(_translate("MainWindow", "Reset docks"))
|
||||
self.uiEditReadmeAction.setText(_translate("MainWindow", "Edit readme"))
|
||||
self.uiEditReadmeAction.setToolTip(_translate("MainWindow", "Edit readme"))
|
||||
self.uiResetConsoleAllAction.setText(_translate("MainWindow", "Reset all console connections"))
|
||||
from ..compute_summary_view import ComputeSummaryView
|
||||
from ..console_view import ConsoleView
|
||||
from ..graphics_view import GraphicsView
|
||||
|
||||
@@ -239,4 +239,22 @@ class UpdateManager(QtCore.QObject):
|
||||
for member in members:
|
||||
# Path separator is always / even on windows
|
||||
member.name = member.name.split("/", 1)[1]
|
||||
tar.extractall(path=self._package_directory, members=members)
|
||||
def is_within_directory(directory, target):
|
||||
|
||||
abs_directory = os.path.abspath(directory)
|
||||
abs_target = os.path.abspath(target)
|
||||
|
||||
prefix = os.path.commonprefix([abs_directory, abs_target])
|
||||
|
||||
return prefix == abs_directory
|
||||
|
||||
def safe_extract(tar, path=".", members=None, *, numeric_owner=False):
|
||||
|
||||
for member in tar.getmembers():
|
||||
member_path = os.path.join(path, member.name)
|
||||
if not is_within_directory(path, member_path):
|
||||
raise Exception("Attempted Path Traversal in Tar File")
|
||||
|
||||
tar.extractall(path, members, numeric_owner=numeric_owner)
|
||||
|
||||
safe_extract(tar, path=self._package_directory, members=members)
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import platform
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from urllib.parse import quote
|
||||
from ..version import __version__
|
||||
from ..qt import QtCore, QtNetwork, QtWidgets
|
||||
from ..local_config import LocalConfig
|
||||
from ..settings import GENERAL_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AnalyticsClient(QtCore.QObject):
|
||||
"""
|
||||
Google analytics client to send events.
|
||||
"""
|
||||
|
||||
_property_id = "UA-55817127-3"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._visitor_id = None
|
||||
self._manager = QtNetwork.QNetworkAccessManager(self)
|
||||
|
||||
def finished(network_reply):
|
||||
try:
|
||||
error = network_reply.error()
|
||||
except TypeError:
|
||||
# For unknow reason sometimes error is transform to a signal
|
||||
# we receive few crash report about that, but we are not able
|
||||
# to reproduce. We suspect the problem happen when the
|
||||
# application is closing.
|
||||
#
|
||||
# https://github.com/GNS3/gns3-gui/issues/2011
|
||||
return
|
||||
if error != QtNetwork.QNetworkReply.NoError:
|
||||
log.debug("Error when pushing to Google Analytics %s", network_reply.errorString())
|
||||
|
||||
self._manager.finished.connect(finished)
|
||||
|
||||
#
|
||||
# We need to build a user agent for Universal Analytics in order to
|
||||
# let analytics guess the OS
|
||||
# this could break by analytics at anytime :(
|
||||
if sys.platform.startswith("darwin"):
|
||||
self._user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X {release}) AppleWebKit/537.36 (KHTML, like Gecko) GNS3/{version}".format(release=platform.mac_ver()[0].replace(".", "_"), version=__version__)
|
||||
elif sys.platform.startswith("win"):
|
||||
self._user_agent = "Mozilla/5.0 (Windows NT {release}) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 GNS3/{version}".format(release=platform.release(), version=__version__)
|
||||
else:
|
||||
self._user_agent = "Mozilla/5.0 (X11; Linux {arch}) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 GNS3/{version}".format(arch=platform.machine(), version=__version__)
|
||||
self._rate_limit = {}
|
||||
|
||||
def sendScreenView(self, screen, session_start=None):
|
||||
"""
|
||||
:params session_start: True session start, None during session, False session stop
|
||||
"""
|
||||
|
||||
if session_start is not False and screen in self._rate_limit:
|
||||
if self._rate_limit[screen] + 60 * 1 > datetime.utcnow().timestamp():
|
||||
log.debug("Ignore call %s to Google Analytics because of rate limiting", screen)
|
||||
return
|
||||
|
||||
self._rate_limit[screen] = datetime.utcnow().timestamp()
|
||||
|
||||
settings = LocalConfig.instance().loadSectionSettings("MainWindow", GENERAL_SETTINGS)
|
||||
if settings["send_stats"] is False:
|
||||
log.debug("Stats is turn off ignore call %s", screen)
|
||||
return
|
||||
|
||||
body = "v=1" # Version
|
||||
body += "&tid={}".format(self._property_id) # Tracking ID / Property ID
|
||||
body += "&cid={}".format(settings["stats_visitor_id"]) # Anonymous Client ID
|
||||
body += "&aip=1" # Anonymize IP
|
||||
body += "&t=screenview" # Screenview hit type
|
||||
body += "&an=GNS3" # App name
|
||||
body += "&av={}".format(quote(__version__)) # App version.
|
||||
body += "&ua={}".format(quote(self._user_agent)) # User agent
|
||||
body += "&cd={}".format(quote(screen)) # Category
|
||||
body += "&ds=gns3-gui" # Data source
|
||||
if session_start is True:
|
||||
body += "&sc=start" # Session start
|
||||
elif session_start is False:
|
||||
body += "&sc=end" # Session end
|
||||
|
||||
screen = QtWidgets.QApplication.desktop().screenGeometry()
|
||||
body += "&sr={}x{}".format(screen.width(), screen.height()) # Screen resolution
|
||||
|
||||
locale = QtCore.QLocale.system().name().lower()
|
||||
if locale:
|
||||
body += "&ul={}".format(locale) # User language
|
||||
|
||||
# TODO: HTTPS when possible because it's broken for the moment with Qt on OSX:
|
||||
# https://bugreports.qt.io/browse/QTBUG-45487
|
||||
if sys.platform.startswith("darwin"):
|
||||
url = QtCore.QUrl('http://www.google-analytics.com/collect')
|
||||
else:
|
||||
url = QtCore.QUrl('https://www.google-analytics.com/collect')
|
||||
request_qt = QtNetwork.QNetworkRequest(url)
|
||||
request_qt.setRawHeader(b"Content-Type", b"application/x-www-form-urlencoded")
|
||||
request_qt.setRawHeader(b"User-Agent", self._user_agent.encode())
|
||||
self._manager.post(request_qt, body.encode())
|
||||
|
||||
log.debug("Send stats to Google Analytics: %s", body)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of AnalyticsClient.
|
||||
:returns: instance of AnalyticsClient
|
||||
"""
|
||||
|
||||
if not hasattr(AnalyticsClient, '_instance') or AnalyticsClient._instance is None:
|
||||
AnalyticsClient._instance = AnalyticsClient()
|
||||
return AnalyticsClient._instance
|
||||
38
gns3/utils/authorize_ubridge.py
Normal file
38
gns3/utils/authorize_ubridge.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2023 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/>.
|
||||
|
||||
# This script is intended to be built as a small executable for macOS to set the correct permissions on uBridge
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
|
||||
def authorize_ubridge():
|
||||
|
||||
path = shutil.which("ubridge", path=os.path.dirname(sys.executable))
|
||||
if path is None:
|
||||
raise SystemExit("Could not find ubridge executable at {}".format(path))
|
||||
try:
|
||||
shutil.chown(path, "root", "admin")
|
||||
os.chmod(path, 0o4750)
|
||||
except OSError as e:
|
||||
raise SystemExit("Could not authorize {}: {}".format(path, str(e)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
authorize_ubridge()
|
||||
64
gns3/utils/macos_ubridge_setuid.py
Normal file
64
gns3/utils/macos_ubridge_setuid.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2023 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 shutil
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def macos_ubridge_setuid():
|
||||
|
||||
# AuthorizationExecuteWithPrivileges() has been deprecated since macOS 10.7 but it still works
|
||||
# and much simpler than using SMJobBless() which requires a separate helper tool
|
||||
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
from ctypes import byref
|
||||
|
||||
authorize_ubridge = shutil.which("authorize_ubridge", path=os.path.dirname(sys.executable))
|
||||
if authorize_ubridge is None:
|
||||
raise OSError("Could not find the authorize_ubridge executable")
|
||||
|
||||
# https://developer.apple.com/documentation/security
|
||||
sec = ctypes.cdll.LoadLibrary(ctypes.util.find_library("Security"))
|
||||
|
||||
try:
|
||||
sec.AuthorizationCreate
|
||||
except AttributeError:
|
||||
raise OSError("macOS security library does not support AuthorizationCreate")
|
||||
|
||||
kAuthorizationFlagDefaults = 0
|
||||
auth = ctypes.c_void_p()
|
||||
r_auth = byref(auth)
|
||||
err = sec.AuthorizationCreate(None, None, kAuthorizationFlagDefaults, r_auth)
|
||||
if err:
|
||||
raise OSError("Could not create authorization: {}".format(err))
|
||||
|
||||
exe = [authorize_ubridge]
|
||||
log.info("Executing '{}' with privileges".format(exe))
|
||||
args = (ctypes.c_char_p * len(exe))()
|
||||
for i, arg in enumerate(exe[1:]):
|
||||
args[i] = arg.encode('utf8')
|
||||
io = ctypes.c_void_p()
|
||||
err = sec.AuthorizationExecuteWithPrivileges(auth, exe[0].encode('utf8'), 0, args, byref(io))
|
||||
if err:
|
||||
raise OSError("Could not authorize uBridge: {}".format(err))
|
||||
else:
|
||||
log.info("Successfully authorized uBridge")
|
||||
@@ -15,7 +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 shlex
|
||||
import subprocess
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
# or negative for a release candidate or beta (after the base version
|
||||
# number has been incremented)
|
||||
|
||||
__version__ = "2.2.25"
|
||||
__version_info__ = (2, 2, 25, 0)
|
||||
__version__ = "2.2.41"
|
||||
__version_info__ = (2, 2, 41, 0)
|
||||
|
||||
if "dev" in __version__:
|
||||
try:
|
||||
@@ -32,6 +32,6 @@ if "dev" in __version__:
|
||||
import subprocess
|
||||
if os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")):
|
||||
r = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip("\n")
|
||||
__version__ += "-" + r
|
||||
__version__ += "+" + r
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
-rrequirements.txt
|
||||
|
||||
PyQt5==5.15.4
|
||||
PyQt5==5.15.7
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
jsonschema==3.2.0
|
||||
sentry-sdk==1.3.1
|
||||
psutil==5.8.0
|
||||
distro==1.6.0
|
||||
setuptools
|
||||
jsonschema>=4.17.3,<4.18; python_version >= '3.7'
|
||||
jsonschema==3.2.0; python_version < '3.7' # v3.2.0 is the last version to support Python 3.6
|
||||
sentry-sdk==1.17.0,<1.18
|
||||
psutil==5.9.4
|
||||
distro>=1.8.0
|
||||
setuptools>=60.8.1; python_version >= '3.7'
|
||||
setuptools==59.6.0; python_version < '3.7' # v59.6.0 is the last version to support Python 3.6
|
||||
|
||||
5
setup.py
5
setup.py
@@ -66,7 +66,8 @@ setup(
|
||||
author="Jeremy Grossmann",
|
||||
author_email="package-maintainer@gns3.net",
|
||||
description="GNS3 graphical interface for the GNS3 server.",
|
||||
long_description=open("README.rst", "r").read(),
|
||||
long_description=open("README.md", "r").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
install_requires=open("requirements.txt", "r").read().splitlines(),
|
||||
entry_points={
|
||||
"gui_scripts": [
|
||||
@@ -78,6 +79,7 @@ setup(
|
||||
include_package_data=True,
|
||||
package_data={"gns3": ["configs/*.txt", "schemas/*.json"]},
|
||||
platforms="any",
|
||||
python_requires=">=3.4",
|
||||
setup_requires=["setuptools>=17.1"],
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -98,6 +100,7 @@ setup(
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -61,6 +61,7 @@ def test_toSvg_negative_y(project, controller):
|
||||
|
||||
def test_fromSvg(project, controller):
|
||||
line = LineItem(project=project)
|
||||
line._main_window.uiSnapToGridAction.isChecked = lambda: False
|
||||
line.setPos(50, 84)
|
||||
line.fromSvg('<svg height="150" width="250"><line x1="0" y1="0" x2="250" y2="150" stroke-width="5" stroke="#0000ff" stroke-dasharray="5, 25, 25" /></svg>')
|
||||
assert line.line().x1() == 0
|
||||
@@ -75,6 +76,7 @@ def test_fromSvg(project, controller):
|
||||
|
||||
def test_fromSvg_top_direction(project, controller):
|
||||
line = LineItem(project=project)
|
||||
line._main_window.uiSnapToGridAction.isChecked = lambda: False
|
||||
line.setPos(50, 84)
|
||||
line.fromSvg('<svg height="150" width="250"><line x1="0" y1="150" x2="250" y2="0" stroke-width="5" stroke="#0000ff" stroke-dasharray="5, 25, 25" /></svg>')
|
||||
assert line.line().x1() == 0
|
||||
|
||||
@@ -311,7 +311,7 @@ def test_callbackConnect_major_version_invalid(http_client):
|
||||
|
||||
def test_callbackConnect_minor_version_invalid(http_client):
|
||||
|
||||
new_version = "{}.{}.{}".format(__version_info__[0], __version_info__[1], __version_info__[2] + 1)
|
||||
new_version = "{}.{}.{}".format(__version_info__[0], __version_info__[1] + 1, 0)
|
||||
params = {
|
||||
"local": True,
|
||||
"version": new_version
|
||||
@@ -319,14 +319,23 @@ def test_callbackConnect_minor_version_invalid(http_client):
|
||||
mock = unittest.mock.MagicMock()
|
||||
|
||||
http_client._query_waiting_connections.append((None, mock))
|
||||
# Stable release
|
||||
if __version_info__[3] == 0:
|
||||
http_client._callbackConnect(params)
|
||||
assert http_client._connected is False
|
||||
mock.assert_called_with({"message": "Client version {} is not the same as server (controller) version {}".format(__version__, new_version)}, error=True, server=None)
|
||||
else:
|
||||
http_client._callbackConnect(params)
|
||||
assert http_client._connected is True
|
||||
http_client._callbackConnect(params)
|
||||
assert http_client._connected is False
|
||||
mock.assert_called_with({"message": "Client version {} is not the same as server (controller) version {}".format(__version__, new_version)}, error=True, server=None)
|
||||
|
||||
|
||||
def test_callbackConnect_patch_version(http_client):
|
||||
|
||||
new_version = "{}.{}.{}.{}".format(__version_info__[0], __version_info__[1], __version_info__[2], 42)
|
||||
params = {
|
||||
"local": True,
|
||||
"version": new_version
|
||||
}
|
||||
mock = unittest.mock.MagicMock()
|
||||
|
||||
http_client._query_waiting_connections.append((None, mock))
|
||||
http_client._callbackConnect(params)
|
||||
assert http_client._connected is True
|
||||
|
||||
|
||||
def test_callbackConnect_non_gns3_server(http_client):
|
||||
|
||||
@@ -104,9 +104,6 @@ def test_create_link(devices, project, controller):
|
||||
assert link._link_id is not None
|
||||
assert not devices[0].ports()[0].isFree()
|
||||
|
||||
assert link in devices[0].links()
|
||||
assert link in devices[1].links()
|
||||
|
||||
assert link.getNodePort(devices[0]) == devices[0].ports()[0]
|
||||
assert link.getNodePort(devices[1]) == devices[1].ports()[0]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-rrequirements.txt
|
||||
|
||||
PyQt5==5.15.4 # pyup: ignore
|
||||
pywin32==301 # pyup: ignore
|
||||
PyQt5==5.15.7 # pyup: ignore
|
||||
pywin32==305 # pyup: ignore
|
||||
|
||||
Reference in New Issue
Block a user