Compare commits

...

91 Commits

Author SHA1 Message Date
grossmj
0946dff3a0 Release v2.2.0b2 2019-05-29 17:16:59 +07:00
grossmj
d7d96b10e5 Merging 2.1 into 2.2 branch 2019-05-29 16:50:36 +07:00
grossmj
0c0b2d5cb3 Development on 2.1.21dev1 2019-05-29 16:37:43 +07:00
grossmj
450fbc9af3 Release v2.1.20 2019-05-29 15:44:25 +07:00
grossmj
469ee8fab8 Fix KeyError: 'endpoint' issue. Fixes #2802 2019-05-28 23:17:55 +07:00
grossmj
6ccfcaf76e Development on 2.1.20dev1 2019-05-28 16:33:43 +07:00
grossmj
520e857874 Release v2.1.19 2019-05-28 15:23:35 +07:00
grossmj
012c7b4241 Fix wrong aligment of symbols in saved/exported projects. Fixes #2800 2019-05-27 16:33:51 +07:00
grossmj
1d71cd5bf0 Replace urllib.request by Qt implementation for local server synchronous check. Fixes #2793 2019-05-27 16:03:55 +07:00
grossmj
17d1a7f4ed Support snapshots for portable projects. Fixes https://github.com/GNS3/gns3-gui/issues/2792 2019-05-27 15:35:47 +07:00
grossmj
0cd5c08c6b Fix event notification problem for projects and how snapshots are restored. 2019-05-27 15:24:36 +07:00
grossmj
20ac503fe9 Do not close the nodes dock widget when creating project. 2019-05-26 15:20:53 +07:00
grossmj
5f737c2c7c Fix no scan for images on remote controller. Fixes #2799 2019-05-26 15:12:22 +07:00
grossmj
eb1a37be36 Remove problematic tests. 2019-05-25 17:49:33 +07:00
grossmj
07c64b5432 Use QNetworkAccessManager to download custom appliance symbols. 2019-05-25 16:25:02 +07:00
grossmj
ce981d1c49 Experimental auto upgrade should not be available for "frozen" app. Fixes #2797 2019-05-25 15:17:23 +07:00
grossmj
32a9f2556e Use QNetworkAccessManager for synchronous local server check. Ref #2793
Remove unused code.
2019-05-25 15:04:37 +07:00
grossmj
1dc3c13df2 Don't allow link labels to be moved for locked nodes. Fixes #2794 2019-05-23 15:10:40 +07:00
grossmj
6a6e86b325 Merge 2.1 into 2.2 branch 2019-05-23 14:51:53 +07:00
grossmj
d96277882a Set grid's minimum to 5. Fixes #2795 2019-05-23 14:41:53 +07:00
grossmj
ecec917752 Development on 2.1.19dev1 2019-05-23 14:37:37 +07:00
grossmj
ea9c1a8ee1 Release v2.1.18 2019-05-22 16:13:28 +07:00
grossmj
cfbb09fb57 Fix error in HTTPConnection.request for Python3.6. Fixes #2793 2019-05-22 16:05:34 +07:00
grossmj
dc8aa1fb92 Catch more OSError/PermissionError when checking md5 on remote images. Fixes #2582 2019-05-22 15:23:56 +07:00
grossmj
786cc8aa65 Fix exception when grid size is 0. Fixes #2790 2019-05-22 15:13:16 +07:00
grossmj
4a353e08e3 Catch PermissionError when scanning local image directories. Fixes #2791 2019-05-22 14:55:07 +07:00
grossmj
1371921586 Development on 2.2.0dev12 2019-05-21 19:16:19 +07:00
grossmj
cd8696a714 Release v2.2.0b1 2019-05-21 15:26:54 +07:00
grossmj
17799719d6 Merge branch '2.1' into 2.2 2019-05-21 15:16:19 +07:00
grossmj
2a59013604 Revert "Make sure the latest PyQt5 version 5.12.x is used on Windows." Ref #2778 2019-05-20 12:01:16 +07:00
grossmj
1c46299dd9 Change behavior when an IOU license is verified. Fixes https://github.com/GNS3/gns3-server/issues/1555 2019-05-20 10:51:25 +07:00
grossmj
628d7cb909 Fix cannot load new profile. Fixes #2784 2019-05-19 16:33:33 +07:00
grossmj
b23c92c0fb Fix Docker extra volumes support 2019-05-19 14:26:03 +07:00
Jeremy Grossmann
49ce5a9f38 Merge pull request #2775 from kazkansouh/2.2-docker-volumes
Custom persistent docker volumes
2019-05-18 20:17:20 +07:00
Jeremy Grossmann
4575ea9f6d Merge pull request #2789 from GNS3/update-dockerfile
Update Dockerfile to Ubuntu 18.04
2019-05-18 20:10:31 +07:00
grossmj
fd6a00df6a Update Dockerfile to Ubuntu 18.04 2019-05-18 20:03:04 +07:00
grossmj
58ab4b424a Fix remote packet capture when controller is also remote. Fixes #2785 2019-05-18 17:33:34 +07:00
grossmj
1ea1abf582 Set console type to "none" by default for Ethernet switches and add a warning if trying to use "telnet". Fixes https://github.com/GNS3/gns3-gui/issues/2776 2019-05-18 14:28:20 +07:00
grossmj
e8caab74f4 Bump version to 2.2.0dev11 2019-05-18 14:11:07 +07:00
grossmj
9fce393fd1 Add tooltip for symbol theme support in general preferences. Fixes #2770 2019-05-18 14:01:08 +07:00
grossmj
827c11ae97 Merge remote-tracking branch 'origin/2.2' into 2.2 2019-05-18 13:45:57 +07:00
grossmj
eb370d5672 Merge 2.1 branch into 2.2 2019-05-18 13:45:39 +07:00
grossmj
7732aaf9a5 Release v2.1.17 2019-05-17 15:10:28 +07:00
Karim Kanso
63161eb760 Minor ui tweak to align extra hosts text edit look and feel with extra volume text edit. 2019-04-22 13:03:23 +01:00
Karim Kanso
5dba814d1b Support for persistent docker volumes to be configured via ui (requires corresponding commit on gns3-server) 2019-04-22 11:09:28 +01:00
Jeremy Grossmann
aecdc71f3a Merge pull request #2772 from GNS3/pyup-update-pytest-4.4.0-to-4.4.1
Update pytest to 4.4.1
2019-04-16 20:04:42 +07:00
pyup-bot
3209c1d0e6 Update pytest from 4.4.0 to 4.4.1 2019-04-16 03:33:58 +02:00
grossmj
2b3fb53ef2 Release v2.2.0a5 2019-04-15 17:05:20 +07:00
grossmj
cbbbece0e5 Revert "Drop old Qemu support (Windows and macOS) and legacy ASA support." Ref https://github.com/GNS3/gns3-server/issues/1579
This reverts commit 3e47267e35.
2019-04-15 15:56:01 +07:00
grossmj
56d742b19f Merge 2.1 into 2.2 2019-04-15 15:54:08 +07:00
grossmj
1f566a31cf Development on 2.1.17dev1 2019-04-15 12:41:41 +07:00
grossmj
10d75e15da Release v2.1.16 2019-04-15 12:00:18 +07:00
grossmj
17def7e00a Do not make NPF or NPCAP service mandatory to start the local server on Windows. 2019-04-15 10:33:27 +07:00
grossmj
106afd0987 Do not try to upload a local image that is already installed on the local server. 2019-04-15 00:29:43 +07:00
grossmj
bba9c5e1d8 Back to the major.minor version for config files. Ref https://github.com/GNS3/gns3-gui/issues/2756 2019-04-14 21:31:41 +07:00
grossmj
ae8e8013d4 Some adjustments with compute WebSocket handling. Ref https://github.com/GNS3/gns3-server/issues/1564 2019-04-14 16:48:12 +07:00
grossmj
3a5f1d60f9 Fix AttributeError: 'GraphicsView' object has no attribute '_import_config_dir'. Fixes #2768 2019-04-13 18:45:16 +07:00
grossmj
3f6eb61382 Do not try to lock a SvgIconItem. Fixes #2766 2019-04-13 18:40:54 +07:00
grossmj
32bfff381d Merge 2.1 into 2.2 2019-04-13 18:11:41 +07:00
grossmj
f68a8ea829 Fix OverflowError error with progress dialog. Fixes #2767 2019-04-13 17:38:43 +07:00
grossmj
50066b2f12 More fixes for stuck progress window. Fixes #2765 2019-04-13 17:10:24 +07:00
grossmj
21a99d4376 Fix adding multiple devices - stuck progress window. Fixes #2765 2019-04-13 17:04:23 +07:00
grossmj
f97d3041b8 Make sure the latest PyQt5 version 5.12.x is used on Windows. 2019-04-13 15:43:23 +07:00
grossmj
31d6a065b0 Show a warning when a config export is not supported. Ref #2762 2019-04-11 15:32:22 +07:00
grossmj
20bf63dbbf Prevent locked nodes to be deleted. Fixes https://github.com/GNS3/gns3-gui/issues/2764 2019-04-10 15:43:52 +07:00
grossmj
1c3e0ef640 Fix default telnet console command. 2019-04-09 21:18:55 +07:00
grossmj
5b58d3ab6d Bump version to 2.2.0dev10 2019-04-09 19:20:21 +07:00
grossmj
554c9205f3 Add PuTTY 0.71 and mark GNS3 PuTTY as deprecated. Fixes #2758 2019-04-09 19:19:56 +07:00
grossmj
543a8e7c33 Fix bug with IOS platform detection. Fixes #2760 2019-04-09 16:24:07 +07:00
grossmj
69ef35c674 Development on 2.2.0dev9 2019-04-05 22:01:35 +08:00
grossmj
45102a07b6 Release v2.2.0a4 2019-04-05 19:10:04 +08:00
grossmj
f0b8b22e8a Use the full version number for path to config files. Ref https://github.com/GNS3/gns3-gui/issues/2756 2019-04-05 18:44:31 +08:00
grossmj
d94f5a2d8c Fix error message when shutting down GUI without a started server. 2019-04-01 21:08:17 +07:00
grossmj
a768661c05 Fix remote packet capture and make sure packet capture is stopped when deleting an NIO. Fixes https://github.com/GNS3/gns3-gui/issues/2753 2019-04-01 19:47:32 +07:00
grossmj
4657b005b6 Restore migrate old settings. 2019-04-01 16:20:26 +07:00
grossmj
e71da830b0 Merge remote-tracking branch 'origin/2.2' into 2.2 2019-04-01 15:53:59 +07:00
grossmj
ebf2563200 Store config files in version specific location 2019-04-01 15:53:39 +07:00
Jeremy Grossmann
e8eaa00244 Merge pull request #2755 from GNS3/pyup-update-pytest-4.3.1-to-4.4.0
Update pytest to 4.4.0
2019-04-01 12:00:43 +07:00
pyup-bot
d750e7a427 Update pytest from 4.3.1 to 4.4.0 2019-04-01 06:53:29 +02:00
grossmj
bfc8adc904 Fix error messages on closing GNS3 application. Fixes https://github.com/GNS3/gns3-gui/issues/2750 2019-03-30 17:20:15 +07:00
grossmj
4de38ea590 Fix bug when list of files for an appliance is not displayed. 2019-03-30 15:44:30 +07:00
ziajka
cc0c6d0a7a Update 'local' to 'bundled' in server & gui, Fixes: #1561 2019-03-27 11:56:32 +01:00
grossmj
d1d0810233 Development on 2.2.0dev8 2019-03-25 23:44:19 +08:00
grossmj
ee3c758bb7 Release v2.2.0a3 2019-03-25 19:35:22 +08:00
grossmj
8f077456b1 Development on 2.1.16dev1 2019-03-21 13:56:11 +08:00
grossmj
a29f3e35c0 Release v2.1.15 2019-03-21 11:41:44 +08:00
grossmj
b12cb5c939 Fix bug when changing symbol. Fixes #2740 2019-03-20 15:15:29 +08:00
grossmj
ba646f5efa Fix image upload tests, second try. 2019-03-18 17:34:26 +07:00
grossmj
edafc29cdc Fix image upload tests. 2019-03-18 17:19:23 +07:00
grossmj
5aa67d18c0 Fix issue when images are not uploaded from appliance wizard. Ref https://github.com/GNS3/gns3-gui/issues/2738 2019-03-18 15:33:37 +07:00
grossmj
8067aaadd4 Development on 2.2.0dev7 2019-03-14 23:27:11 +07:00
61 changed files with 806 additions and 396 deletions

View File

@@ -1,5 +1,94 @@
# Change Log
## 2.2.0b2 29/05/2019
* Fix KeyError: 'endpoint' issue. Fixes #2802
* Fix wrong aligment of symbols in saved/exported projects. Fixes #2800
* Replace urllib.request by Qt implementation for local server synchronous check. Fixes #2793
* Support snapshots for portable projects. Fixes https://github.com/GNS3/gns3-gui/issues/2792
* Fix event notification problem for projects and how snapshots are restored.
* Do not close the nodes dock widget when creating project.
* Fix no scan for images on remote controller. Fixes #2799
* Use QNetworkAccessManager to download custom appliance symbols.
* Experimental auto upgrade should not be available for "frozen" app. Fixes #2797
* Don't allow link labels to be moved for locked nodes. Fixes #2794
* Catch more OSError/PermissionError when checking md5 on remote images. Fixes #2582
* Fix exception when grid size is 0. Fixes #2790
* Catch PermissionError when scanning local image directories. Fixes #2791
## 2.1.20 29/05/2019
* Fix KeyError: 'endpoint' issue. Fixes #2802
## 2.1.19 28/05/2019
* Fix wrong aligment of symbols in saved/exported projects. Fixes #2800
* Replace urllib.request by Qt implementation for local server synchronous check. Fixes #2793
* Set grid's minimum to 5. Fixes #2795
## 2.1.18 22/05/2019
* Fix error in HTTPConnection.request for Python3.6. Fixes #2793
* Catch more OSError/PermissionError when checking md5 on remote images. Fixes #2582
* Fix exception when grid size is 0. Fixes #2790
* Catch PermissionError when scanning local image directories. Fixes #2791
* Revert "Make sure the latest PyQt5 version 5.12.x is used on Windows." Ref #2778
## 2.2.0b1 21/05/2019
* Change behavior when an IOU license is verified. Fixes https://github.com/GNS3/gns3-server/issues/1555
* Fix cannot load new profile. Fixes #2784
* Fix remote packet capture when controller is also remote. Fixes #2785
* Set console type to "none" by default for Ethernet switches and add a warning if trying to use "telnet". Fixes https://github.com/GNS3/gns3-gui/issues/2776
* Add tooltip for symbol theme support in general preferences. Fixes #2770
* Support for persistent docker volumes
## 2.1.17 17/05/2019
* No changes.
## 2.2.0a5 15/04/2019
* Revert "Drop old Qemu support (Windows and macOS) and legacy ASA support." Ref https://github.com/GNS3/gns3-server/issues/1579
* Do not make NPF or NPCAP service mandatory to start the local server on Windows.
* Do not try to upload a local image that is already installed on the local server.
* Back to the major.minor version for config files. Ref https://github.com/GNS3/gns3-gui/issues/2756
* Some adjustments with compute WebSocket handling. Ref https://github.com/GNS3/gns3-server/issues/1564
* Fix AttributeError: 'GraphicsView' object has no attribute '_import_config_dir'. Fixes #2768
* Do not try to lock a SvgIconItem. Fixes #2766
* Prevent locked nodes to be deleted. Fixes https://github.com/GNS3/gns3-gui/issues/2764
* Add PuTTY 0.71 and mark GNS3 PuTTY as deprecated. Fixes #2758
* Fix bug with IOS platform detection. Fixes #2760
## 2.1.16 15/04/2019
* Do not make NPF or NPCAP service mandatory to start the local server on Windows.
* Fix OverflowError error with progress dialog. Fixes #2767
* More fixes for stuck progress window. Fixes #2765
* Fix adding multiple devices - stuck progress window. Fixes #2765
* Make sure the latest PyQt5 version 5.12.x is used on Windows.
* Show a warning when a config export is not supported. Ref #2762
## 2.1.15 21/03/2019
* No changes on the GUI.
## 2.2.0a4 05/04/2019
* Use the full version number for path to config files. Ref https://github.com/GNS3/gns3-gui/issues/2756
* Fix error message when shutting down GUI without a started server.
* Fix remote packet capture and make sure packet capture is stopped when deleting an NIO. Fixes https://github.com/GNS3/gns3-gui/issues/2753
* Store config files in version specific location
* Update pytest from 4.3.1 to 4.4.0
* Fix error messages on closing GNS3 application. Fixes https://github.com/GNS3/gns3-gui/issues/2750
* Fix bug when list of files for an appliance is not displayed.
* Update 'local' to 'bundled' in server & gui, Fixes: #1561
## 2.2.0a3 25/03/2019
* Fix bug when changing symbol. Fixes #2740
* Fix issue when images are not uploaded from appliance wizard. Ref https://github.com/GNS3/gns3-gui/issues/2738
## 2.2.0a2 14/03/2019
* Try to handle stacked widget layout differently. Ref #2605

View File

@@ -1,5 +1,5 @@
# Run tests inside a container
FROM ubuntu:17.10
FROM ubuntu:18.04
MAINTAINER GNS3 Team

View File

@@ -1,6 +1,6 @@
-rrequirements.txt
pep8==1.7.0
pytest==4.3.1
pytest==4.4.1
pytest-pythonpath==0.7.3 # useful for running tests outside tox
pytest-timeout==1.3.3

View File

@@ -46,6 +46,7 @@ class Controller(QtCore.QObject):
super().__init__()
self._connected = False
self._connecting = False
self._notification_stream = None
self._version = None
self._cache_directory = tempfile.TemporaryDirectory(suffix="-gns3")
self._http_client = None
@@ -53,8 +54,7 @@ class Controller(QtCore.QObject):
self._error_dialog = None
self._display_error = True
self._projects = []
self._controller_websocket = QtWebSockets.QWebSocket()
self._project_websocket = QtWebSockets.QWebSocket()
self._websocket = QtWebSockets.QWebSocket()
# If we do multiple call in order to download the same symbol we queue them
self._static_asset_download_queue = {}
@@ -246,12 +246,6 @@ class Controller(QtCore.QObject):
if self._http_client:
return self._http_client.createHTTPQuery(method, path, *args, **kwargs)
def getSynchronous(self, endpoint, timeout=2):
return self._http_client.getSynchronous(endpoint, timeout)
def connectProjectWebSocket(self, path, *args):
return self._http_client.connectWebSocket(self._project_websocket, path)
@staticmethod
def instance():
"""
@@ -426,7 +420,7 @@ class Controller(QtCore.QObject):
ignoreErrors=True)
else:
self._notification_stream = self._http_client.connectWebSocket(self._controller_websocket, "/notifications/ws")
self._notification_stream = self._http_client.connectWebSocket(self._websocket, "/notifications/ws")
self._notification_stream.textMessageReceived.connect(self._websocket_event_received)
self._notification_stream.error.connect(self._websocket_error)
@@ -449,7 +443,7 @@ class Controller(QtCore.QObject):
@qslot
def _websocket_error(self, error):
if self._notification_stream:
log.error(self._notification_stream.errorString())
log.error("Websocket notification stream error: {}".format(self._notification_stream.errorString()))
self._notification_stream = None
self._startListenNotifications()

View File

@@ -52,7 +52,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "https://cc555ea871c3443b8ef31f6bf2bb0d7a:5b6b0f2388bd4838a5c9187aae03fcad@sentry.io/38506"
DSN = "https://490d29e0583d4af5a05bea9d722c477d:e35f247c273d46fc8244681ad89bd5de@sentry.io/38506"
if hasattr(sys, "frozen"):
cacert = get_resource("cacert.pem")
if cacert is not None and os.path.isfile(cacert):

View File

@@ -36,6 +36,7 @@ from ..compute_manager import ComputeManager
from ..controller import Controller
from ..local_config import LocalConfig
from ..image_upload_manager import ImageUploadManager
from ..image_manager import ImageManager
import logging
log = logging.getLogger(__name__)
@@ -76,6 +77,12 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
# directories where to search for images
images_directories = list()
for emulator in ("QEMU", "IOU", "DYNAMIPS"):
emulator_images_dir = ImageManager.instance().getDirectoryForType(emulator)
if os.path.exists(emulator_images_dir):
images_directories.append(emulator_images_dir)
images_directories.append(os.path.dirname(self._path))
download_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation)
if download_directory != "" and download_directory != os.path.dirname(self._path):
@@ -190,7 +197,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "The GNS3 VM is not available, please configure the GNS3 VM before adding a new appliance.")
elif self.page(page_id) == self.uiFilesWizardPage:
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
if Controller.instance().isRemote() or self._compute_id != "local":
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
else:
self.images_changed_signal.emit()
elif self.page(page_id) == self.uiQemuWizardPage:
if self._appliance['qemu'].get('kvm', 'require') == 'require':
@@ -221,8 +231,15 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
def _uiServerWizardPage_isComplete(self):
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
def _imageUploadedCallback(self, result, error=False, **kwargs):
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
def _imageUploadedCallback(self, result, error=False, context=None, **kwargs):
if context is None:
context = {}
image_path = context.get("image_path", "unknown")
if error:
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
else:
log.info("Image '{}' has been successfully uploaded".format(image_path))
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
def _showApplianceInfoSlot(self):
"""
@@ -339,6 +356,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("red")))
else:
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
image_widget.setToolTip(2, image["path"])
# Associated data stored are col 0: version, col 1: image
image_widget.setData(0, QtCore.Qt.UserRole, version)
@@ -398,9 +416,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
image.get("filesize"),
strict_md5_check=not self.allowCustomFiles.isChecked())
if img:
image["status"] = "Found"
if img.location == "local":
image["status"] = "Found locally"
else:
compute = ComputeManager.instance().getCompute(self._compute_id)
image["status"] = "Found on {}".format(compute.name())
image["md5sum"] = img.md5sum
image["filesize"] = img.filesize
image["path"] = img.path
else:
image["status"] = "Missing"
self._refreshing = False
@@ -495,9 +518,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
return
image_upload_manger = ImageUploadManager(
image, Controller.instance(), self._compute_id,
self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger = ImageUploadManager(image, Controller.instance(), self._compute_id, self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger.upload()
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
@@ -533,7 +554,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if version is None:
appliance_configuration = self._appliance.copy()
if not "docker" in appliance_configuration:
if "docker" not in appliance_configuration:
# only Docker do not have version
return False
else:
@@ -554,7 +575,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if "qemu" in appliance_configuration:
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, self._symbols)
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, self._symbols, parent=self)
TemplateManager.instance().createTemplate(Template(new_template), callback=self._templateCreatedCallback)
return False
@@ -583,24 +604,35 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
self._template_created = True
self.done(True)
def _uploadImages(self, version):
def _uploadImages(self, name, version):
"""
Upload an image the compute.
"""
appliance_configuration = self._appliance.search_images_for_version(version)
try:
appliance_configuration = self._appliance.search_images_for_version(version)
except ApplianceError as e:
QtWidgets.QMessageBox.critical(self, "Appliance","Cannot install {} version {}: {}".format(name, version, e))
return
for image in appliance_configuration["images"]:
if image["location"] == "local":
if self._compute_id == "local" and image["path"].startswith(ImageManager.instance().getDirectory()):
log.debug("{} is already on the local server".format(image["path"]))
return
image = Image(self._appliance.emulator(), image["path"], filename=image["filename"])
image_upload_manger = ImageUploadManager(
image, Controller.instance(), self._compute_id,
self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manger.upload()
image_upload_manager = ImageUploadManager(image, Controller.instance(), self._compute_id, self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
image_upload_manager.upload()
self._image_uploading_count += 1
def _applianceImageUploadedCallback(self, result, error=False, **kwargs):
self._image_uploading_count -= 1
def _applianceImageUploadedCallback(self, result, error=False, context=None, **kwargs):
if context is None:
context = {}
image_path = context.get("image_path", "unknown")
if error:
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
else:
log.info("Image '{}' has been successfully uploaded".format(image_path))
self._image_uploading_count -= 1
def nextId(self):
if self.currentPage() == self.uiServerWizardPage:
@@ -638,7 +670,8 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return False
self._uploadImages(version["name"])
self._uploadImages(appliance["name"], version["name"])
elif self.currentPage() == self.uiUsageWizardPage:
# validate the usage page

View File

@@ -152,18 +152,18 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
"""
self.uiAppliancesTreeWidget.clear()
parent_routers = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_routers.setText(0, "Routers")
parent_routers.setFlags(parent_routers.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_switches = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_switches.setText(0, "Switches")
parent_switches.setFlags(parent_switches.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_guests = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_guests.setText(0, "Guests")
parent_guests.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_firewalls = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_firewalls.setText(0, "Firewalls")
parent_firewalls.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_switches = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_switches.setText(0, "Switches")
parent_switches.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_routers = QtWidgets.QTreeWidgetItem(self.uiAppliancesTreeWidget)
parent_routers.setText(0, "Routers")
parent_routers.setFlags(parent_guests.flags() & ~QtCore.Qt.ItemIsSelectable)
parent_firewalls.setFlags(parent_firewalls.flags() & ~QtCore.Qt.ItemIsSelectable)
self.uiAppliancesTreeWidget.expandAll()
for appliance in ApplianceManager.instance().appliances():
@@ -268,7 +268,7 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
super().done(result)
if result:
ApplianceManager.instance().appliances_changed_signal.disconnect(self._appliancesChangedSlot)
#ApplianceManager.instance().appliances_changed_signal.disconnect(self._appliancesChangedSlot)
from gns3.main_window import MainWindow
if self.currentPage() == self.uiApplianceFromServerWizardPage:
items = self.uiAppliancesTreeWidget.selectedItems()

View File

@@ -22,6 +22,7 @@ import shutil
from gns3.qt import QtWidgets
from gns3.local_config import LocalConfig
from gns3.ui.profile_select_dialog_ui import Ui_ProfileSelectDialog
from gns3.version import __version_info__
import logging
log = logging.getLogger(__name__)
@@ -39,8 +40,8 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
self._main.hide()
parent = self._main
super().__init__(parent)
self.setupUi(self)
self.setupUi(self)
self.uiNewPushButton.clicked.connect(self._newPushButtonSlot)
self.uiDeletePushButton.clicked.connect(self._deletePushButtonSlot)
@@ -48,12 +49,13 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
screen = QtWidgets.QApplication.desktop().screenGeometry()
self.move(screen.center() - self.rect().center())
version = "{}.{}".format(__version_info__[0], __version_info__[1])
if sys.platform.startswith("win"):
appdata = os.path.expandvars("%APPDATA%")
path = os.path.join(appdata, "GNS3")
path = os.path.join(appdata, "GNS3", version)
else:
home = os.path.expanduser("~")
path = os.path.join(home, ".config", "GNS3")
path = os.path.join(home, ".config", "GNS3", version)
self.profiles_path = os.path.join(path, "profiles")
self.uiShowAtStartupCheckBox.setChecked(LocalConfig.instance().multiProfiles())
@@ -65,9 +67,9 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
try:
if os.path.exists(self.profiles_path):
for profil in sorted(os.listdir(self.profiles_path)):
if not profil.startswith("."):
self.uiProfileSelectComboBox.addItem(profil)
for profile in sorted(os.listdir(self.profiles_path)):
if not profile.startswith("."):
self.uiProfileSelectComboBox.addItem(profile)
except OSError:
pass
@@ -79,7 +81,7 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
super().accept()
def _newPushButtonSlot(self):
profile, ok = QtWidgets.QInputDialog.getText(self.parent(), "New profile", "Profile name:")
profile, ok = QtWidgets.QInputDialog.getText(self, "New profile", "Profile name:")
if ok:
self.uiProfileSelectComboBox.addItem(profile)
self.uiProfileSelectComboBox.setCurrentText(profile)
@@ -88,13 +90,13 @@ class ProfileSelectDialog(QtWidgets.QDialog, Ui_ProfileSelectDialog):
def _deletePushButtonSlot(self):
profile = self.uiProfileSelectComboBox.currentText()
if profile == "default":
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", "You can't delete the default profile")
QtWidgets.QMessageBox.critical(self, "Delete profile", "The default profile cannot be deleted")
else:
try:
shutil.rmtree(os.path.join(self.profiles_path, profile))
self._refresh()
except (OSError, PermissionError) as e:
QtWidgets.QMessageBox.critical(self.parentWidget(), "Delete profile", str(e))
QtWidgets.QMessageBox.critical(self, "Cannot delete profile", str(e))
if __name__ == '__main__':

View File

@@ -119,8 +119,12 @@ class ExportProjectWizard(QtWidgets.QWizard, Ui_ExportProjectWizard):
include_images = "yes"
else:
include_images = "no"
if self.uiIncludeSnapshotsCheckBox.isChecked():
include_snapshots = "yes"
else:
include_snapshots = "no"
compression = self.uiCompressionComboBox.currentData()
export_worker = ExportProjectWorker(self._project, self._path, include_images, compression)
export_worker = ExportProjectWorker(self._project, self._path, include_images, include_snapshots, compression)
progress_dialog = ProgressDialog(export_worker, "Exporting project", "Exporting portable project files...", "Cancel", parent=self, create_thread=False)
progress_dialog.show()
progress_dialog.exec_()

View File

@@ -161,6 +161,8 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
"""
symbol_path = self.getSymbol()
if not symbol_path:
return False
for item in self._items:
item.setSymbol(symbol_path)
return True
@@ -169,7 +171,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
if self.uiSymbolTreeWidget.isEnabled():
current = self.uiSymbolTreeWidget.currentItem()
if current:
if current and current.parent():
return current.data(0, QtCore.Qt.UserRole).id()
else:
return os.path.basename(self.uiSymbolLineEdit.text())

View File

@@ -49,7 +49,7 @@ from .compute_manager import ComputeManager
from .utils.get_icon import get_icon
# link items
from .items.link_item import LinkItem
from .items.link_item import LinkItem, SvgIconItem
from .items.ethernet_link_item import EthernetLinkItem
from .items.serial_link_item import SerialLinkItem
@@ -1199,12 +1199,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
path, _ = QtWidgets.QFileDialog.getOpenFileName(self,
"Import {}".format(os.path.basename(config_file)),
self._import_config_dir,
self._import_config_directory,
"All files (*.*);;Config files (*.cfg)",
"Config files (*.cfg)")
if not path:
continue
self._import_config_dir = os.path.dirname(path)
self._import_config_directory = os.path.dirname(path)
item.node().importFile(config_file, path)
def editConfigActionSlot(self):
@@ -1485,7 +1485,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
"""
for item in self.scene().selectedItems():
if not isinstance(item, LinkItem) and not isinstance(item, LabelItem):
if not isinstance(item, LinkItem) and not isinstance(item, LabelItem) and not isinstance(item, SvgIconItem):
if item.locked() is True:
item.setLocked(False)
else:
@@ -1503,7 +1503,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
selected_nodes = []
for item in self.scene().selectedItems():
if isinstance(item, NodeItem):
selected_nodes.append(item.node())
node = item.node()
if node.locked():
QtWidgets.QMessageBox.critical(self, "Delete", "Cannot delete node '{}' because it is locked".format(node.name()))
return
selected_nodes.append(node)
if selected_nodes:
if len(selected_nodes) > 1:
question = "Do you want to permanently delete these {} nodes?".format(len(selected_nodes))
@@ -1598,10 +1602,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
def drawBackground(self, painter, rect):
super().drawBackground(painter, rect)
if self._main_window.uiShowGridAction.isChecked():
grids = [(self.drawingGridSize(),QtGui.QColor(208, 208, 208)),
(self.nodeGridSize(),QtGui.QColor(190, 190, 190))]
grids = [(self.drawingGridSize(), QtGui.QColor(208, 208, 208)),
(self.nodeGridSize(), QtGui.QColor(190, 190, 190))]
painter.save()
for (grid,colour) in grids:
for (grid, colour) in grids:
if not grid:
continue
painter.setPen(QtGui.QPen(colour))
left = int(rect.left()) - (int(rect.left()) % grid)

View File

@@ -207,16 +207,16 @@ class HTTPClient(QtCore.QObject):
Called when a query upload progress
"""
if not sip_is_deleted(HTTPClient._progress_callback):
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(total))
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
def _notify_progress_download(self, query_id, sent, total):
"""
Called when a query download progress
"""
if not sip_is_deleted(HTTPClient._progress_callback):
# abs() for maxium because sometimes the system send negative
# abs() for maximum because sometimes the system send negative
# values
HTTPClient._progress_callback.progress_signal.emit(query_id, str(sent), str(abs(total)))
HTTPClient._progress_callback.progress_signal.emit(query_id, str(abs(sent)), str(abs(total)))
@classmethod
def setProgressCallback(cls, progress_callback):
@@ -470,7 +470,9 @@ class HTTPClient(QtCore.QObject):
"""
host = self._getHostForQuery()
request = websocket.request()
request.setUrl(QtCore.QUrl("ws://{host}:{port}{prefix}{path}".format(host=host, port=self._port, path=path, prefix=prefix)))
ws_url = "ws://{host}:{port}{prefix}{path}".format(host=host, port=self._port, path=path, prefix=prefix)
log.debug("Connecting to WebSocket endpoint: {}".format(ws_url))
request.setUrl(QtCore.QUrl(ws_url))
self._addAuth(request)
websocket.open(request)
return websocket
@@ -725,46 +727,54 @@ class HTTPClient(QtCore.QObject):
e = HttpBadRequest(body)
raise e
def getSynchronous(self, endpoint, timeout=2):
def getSynchronous(self, method, endpoint, prefix="/v2", timeout=2):
"""
Synchronous check if a server is running
:returns: Tuple (Status code, json of anwser). Status 0 is a non HTTP error
:returns: Tuple (Status code, json of answer). Status 0 is a non HTTP error
"""
try:
url = "{protocol}://{host}:{port}/v2/{endpoint}".format(protocol=self._protocol, host=self._host, port=self._port, endpoint=endpoint)
if self._user is not None and len(self._user) > 0:
log.debug("Synchronous get {} with user '{}'".format(url, self._user))
auth_handler = urllib.request.HTTPBasicAuthHandler()
auth_handler.add_password(realm="GNS3 server",
uri=url,
user=self._user,
passwd=self._password)
opener = urllib.request.build_opener(auth_handler)
urllib.request.install_opener(opener)
else:
log.debug("Synchronous get {} (no authentication)".format(url))
response = urllib.request.urlopen(url, timeout=timeout)
content_type = response.getheader("CONTENT-TYPE")
if response.status == 200:
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))
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))
request = self._request(url)
request = self._addAuth(request)
request.setRawHeader(b"User-Agent", "GNS3 QT Client v{version}".format(version=__version__).encode())
try:
response = self._network_manager.sendCustomRequest(request, method.encode())
except SystemError as e:
log.error("Can't send query: {}".format(str(e)))
return
loop = QtCore.QEventLoop()
response.finished.connect(loop.quit)
if timeout is not None:
QtCore.QTimer.singleShot(timeout * 1000, qpartial(self._timeoutSlot, response, timeout))
if not loop.isRunning():
loop.exec_()
status = response.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
if response.error() != QtNetwork.QNetworkReply.NoError:
log.debug("Error while connecting to local server {}".format(response.errorString()))
return status, None
else:
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
if status == 200:
if content_type == "application/json":
content = response.read()
content = bytes(response.readAll())
json_data = json.loads(content.decode("utf-8"))
return response.status, json_data
return status, json_data
else:
return response.status, None
except http.client.InvalidURL as e:
log.warning("Invalid local server url: {}".format(e))
return 0, None
except urllib.error.URLError:
# Connection refused. It's a normal behavior if server is not started
return 0, None
except urllib.error.HTTPError as e:
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
return e.code, None
except (OSError, http.client.BadStatusLine, ValueError) as e:
log.debug("Error during get on {}:{}: {}".format(self.host(), self.port(), e))
return status, None
return 0, None
@classmethod

View File

@@ -176,7 +176,7 @@ class ImageManager:
if node_type == 'DYNAMIPS':
return os.path.join(self.getDirectory(), 'IOS')
else:
return os.path.join(self.getDirectory(), node_type)
return os.path.join(self.getDirectory(), node_type.upper())
@staticmethod
def instance():

View File

@@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import pathlib
import urllib.parse
@@ -37,6 +38,9 @@ class ImageUploadManager(object):
self._controller = controller
def upload(self):
if not os.path.exists(self._image.path):
log.error("Image '{}' could not be found".format(self._image.path))
return
if self._directFileUpload:
# first obtain endpoint and know when target request
self._controller.getEndpoint(self._getComputePath(), self._compute_id, self._onLoadEndpointCallback, showProgress=False)
@@ -71,19 +75,16 @@ class ImageUploadManager(object):
self._callback(result, error, **kwargs)
def _fileUploadToCompute(self, endpoint):
log.info("Uploading file to compute: {}".format(endpoint))
log.debug("Uploading image '{}' to compute".format(self._image.path))
parse_results = urllib.parse.urlparse(endpoint)
network_manager = self._controller.getHttpClient().getNetworkManager()
client = HTTPClient.fromUrl(endpoint, network_manager=network_manager)
# We don't retry connection as in case of fail we try direct file upload
client.setMaxRetryConnection(0)
client.createHTTPQuery(
'POST', parse_results.path, self._checkIfSuccessfulCallback, body=pathlib.Path(self._image.path),
progressText="Uploading {}".format(self._image.filename), timeout=None, prefix="")
client.createHTTPQuery('POST', parse_results.path, self._checkIfSuccessfulCallback, body=pathlib.Path(self._image.path),
context={"image_path": self._image.path}, progressText="Uploading {}".format(self._image.filename), timeout=None, prefix="")
def _fileUploadToController(self):
log.info("Uploading file to controller: {}".format(self._getComputePath()))
self._controller.postCompute(
self._getComputePath(), self._compute_id, self._callback, body=pathlib.Path(self._image.path),
progressText="Uploading {}".format(self._image.filename), timeout=None)
log.debug("Uploading image '{}' to controller".format(self._image.path))
self._controller.postCompute(self._getComputePath(), self._compute_id, self._callback, body=pathlib.Path(self._image.path),
context={"image_path": self._image.path}, progressText="Uploading {}".format(self._image.filename), timeout=None)

View File

@@ -147,6 +147,7 @@ class EthernetLinkItem(LinkItem):
self._source_port.setLabel(source_port_label)
if self._draw_port_labels:
source_port_label.setFlag(source_port_label.ItemIsMovable, not self._source_item.locked())
source_port_label.show()
else:
source_port_label.hide()
@@ -189,6 +190,7 @@ class EthernetLinkItem(LinkItem):
self._destination_port.setLabel(destination_port_label)
if self._draw_port_labels:
destination_port_label.setFlag(destination_port_label.ItemIsMovable, not self._destination_item.locked())
destination_port_label.show()
else:
destination_port_label.hide()

View File

@@ -103,22 +103,11 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
from ..main_window import MainWindow
self._main_window = MainWindow.instance()
if self._main_window.uiSnapToGridAction.isChecked():
self._snapToGrid()
self._settings = self._main_window.uiGraphicsView.settings()
if node.initialized():
self.createdSlot(node.id())
def _snapToGrid(self):
grid_size = self._main_window.uiGraphicsView.nodeGridSize()
mid_x = self.boundingRect().width() / 2
x = (grid_size * round((self.x() + mid_x) / grid_size)) - mid_x
mid_y = self.boundingRect().height() / 2
y = (grid_size * round((self.y() + mid_y) / grid_size)) - mid_y
self.setPos(x, y)
def updateNode(self):
"""
Sync change to the node

View File

@@ -136,6 +136,7 @@ class SerialLinkItem(LinkItem):
self._source_port.setLabel(source_port_label)
if self._draw_port_labels:
source_port_label.setFlag(source_port_label.ItemIsMovable, not self._source_item.locked())
source_port_label.show()
else:
source_port_label.hide()
@@ -167,6 +168,7 @@ class SerialLinkItem(LinkItem):
self._destination_port.setLabel(destination_port_label)
if self._draw_port_labels:
destination_port_label.setFlag(destination_port_label.ItemIsMovable, not self._destination_item.locked())
destination_port_label.show()
else:
destination_port_label.hide()

View File

@@ -79,6 +79,7 @@ class Link(QtCore.QObject):
self._deleting = False
self._capture_file_path = None
self._capture_file = None
self._capture_compute_id = None
self._initialized = False
self._filters = {}
self._suspend = False
@@ -103,24 +104,28 @@ class Link(QtCore.QObject):
Controller.instance().post("/projects/{project_id}/links".format(project_id=source_node.project().id()), self._linkCreatedCallback, body=body)
def _parseResponse(self, result):
self._capturing = result.get("capturing", False)
# If the controller is remote the capture path should be rewrite to something local
self._capturing = result.get("capturing", False)
if self._capturing:
if Controller.instance().isRemote():
if self._capture_file_path is None and result.get("capture_file_path", None) is not None:
self._capture_compute_id = result.get("capture_compute_id", None)
self._capture_file_path = result.get("capture_file_path", None)
if Controller.instance().isRemote() or (self._capture_compute_id and self._capture_compute_id != "local"):
# We need to stream the pcap file content if the controller or compute is remote
if Controller.instance().isRemote() or self._capture_file_path is None:
self._capture_file = QtCore.QTemporaryFile()
self._capture_file.open(QtCore.QFile.WriteOnly)
self._capture_file.setAutoRemove(True)
self._capture_file_path = self._capture_file.fileName()
Controller.instance().get("/projects/{project_id}/links/{link_id}/pcap".format(project_id=self.project().id(),link_id=self._link_id),
None,
showProgress=False,
downloadProgressCallback=self._downloadPcapProgress,
ignoreErrors=True, # If something is wrong avoid disconnect us from server
timeout=None)
else:
self._capture_file_path = result["capture_file_path"]
else:
self._capture_file = QtCore.QFile(self._capture_file_path)
self._capture_file.open(QtCore.QFile.WriteOnly)
Controller.instance().get("/projects/{project_id}/links/{link_id}/pcap".format(project_id=self.project().id(), link_id=self._link_id),
None,
showProgress=False,
downloadProgressCallback=self._downloadPcapProgress,
ignoreErrors=True, # If something is wrong avoid disconnect us from server
timeout=None)
log.debug("Capturing packets to '{}'".format(self._capture_file_path))
if "nodes" in result:
self._nodes = result["nodes"]
@@ -353,7 +358,7 @@ class Link(QtCore.QObject):
if error:
log.error("Error while starting capture on link: {}".format(result["message"]))
return
self._parseResponse(result)
#self._parseResponse(result)
def _downloadPcapProgress(self, content, server=None, context={}, **kwargs):
"""
@@ -366,15 +371,16 @@ class Link(QtCore.QObject):
self._capture_file.flush()
def stopCapture(self):
if Controller.instance().isRemote():
if Controller.instance().isRemote() or (self._capture_compute_id and self._capture_compute_id != "local"):
if self._capture_file:
self._capture_file.close()
self._capture_file = None
if self._capture_file_path and os.path.exists(self._capture_file_path):
try:
os.remove(self._capture_file_path)
except OSError as e:
log.error("Cannot remove file {}: {}".format(self._capture_file_path, e))
# if self._capture_file_path and os.path.exists(self._capture_file_path):
# try:
# os.remove(self._capture_file_path)
# except OSError as e:
# log.error("Cannot remove file {}: {}".format(self._capture_file_path, e))
self._capture_file_path = None
Controller.instance().post("/projects/{project_id}/links/{link_id}/stop_capture".format(project_id=self.project().id(),
link_id=self._link_id),
@@ -384,7 +390,7 @@ class Link(QtCore.QObject):
if error:
log.error("Error while stopping capture on link: {}".format(result["message"]))
return
self._parseResponse(result)
#self._parseResponse(result)
def get(self, path, callback, **kwargs):
"""

View File

@@ -24,8 +24,10 @@ import copy
import psutil
from .qt import QtCore, QtWidgets
from .version import __version__
from .version import __version__, __version_info__
from .utils import parse_version
from .local_server_config import LocalServerConfig
from .settings import LOCAL_SERVER_SETTINGS
import logging
log = logging.getLogger(__name__)
@@ -87,8 +89,25 @@ class LocalConfig(QtCore.QObject):
try:
# create the config file if it doesn't exist
os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
with open(self._config_file, "w", encoding="utf-8") as f:
json.dump({"version": __version__, "type": "settings"}, f)
if sys.platform.startswith("win"):
old_config_path = os.path.join(os.path.expandvars("%APPDATA%"), "GNS3", filename)
else:
old_config_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3", filename)
# TODO: migrate versioned config file from a previous version of GNS3 (for instance 2.2 -> 2.3) + support profiles
if os.path.exists(old_config_path):
# migrate post version 2.2.0 configuration file
shutil.copyfile(old_config_path, self._config_file)
# reset the local server path and ubridge path
settings = LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)
settings["path"] = ""
settings["ubridge_path"] = ""
LocalServerConfig.instance().saveSettings("Server", settings)
else:
# create a new config
with open(self._config_file, "w", encoding="utf-8") as f:
json.dump({"version": __version__, "type": "settings"}, f)
except OSError as e:
log.error("Could not create the config file {}: {}".format(self._config_file, e))
@@ -114,17 +133,18 @@ class LocalConfig(QtCore.QObject):
self._config_file = None
self._resetLoadConfig()
def configDirectory(self):
"""
Get the configuration directory
"""
version = "{}.{}".format(__version_info__[0], __version_info__[1])
if sys.platform.startswith("win"):
appdata = os.path.expandvars("%APPDATA%")
path = os.path.join(appdata, "GNS3")
path = os.path.join(appdata, "GNS3", version)
else:
home = os.path.expanduser("~")
path = os.path.join(home, ".config", "GNS3")
path = os.path.join(home, ".config", "GNS3", version)
if self._profile is not None:
path = os.path.join(path, "profiles", self._profile)
@@ -146,8 +166,9 @@ class LocalConfig(QtCore.QObject):
# In < 1.4 on Mac the config was in a gns3.net directory
# We have move to same location as Linux
if sys.platform.startswith("darwin"):
version = "{}.{}".format(__version_info__[0], __version_info__[1])
old_path = os.path.join(os.path.expanduser("~"), ".config", "gns3.net")
new_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3")
new_path = os.path.join(os.path.expanduser("~"), ".config", "GNS3", version)
if os.path.exists(old_path) and not os.path.exists(new_path):
try:
shutil.copytree(old_path, new_path)
@@ -156,7 +177,7 @@ class LocalConfig(QtCore.QObject):
def _migrateOldConfig(self):
"""
Migrate pre 1.4 config
Migrate config from a previous version.
"""
# Display an error if settings come from a more recent version of GNS3
@@ -165,7 +186,7 @@ class LocalConfig(QtCore.QObject):
if "version" in self._settings:
if parse_version(self._settings["version"])[:2] > parse_version(__version__)[:2]:
app = QtWidgets.QApplication(sys.argv) # We need to create an application because settings are loaded before Qt init
error_message = "Your settings are for version {} of GNS3. You cannot use a previous version of GNS3 without risking losing data. If you want to reset delete the settings in {}".format(self._settings["version"], self.configDirectory())
error_message = "Settings are for version {} of GNS3. It is not possible to use a previous version of GNS3 without risking losing data. Delete the settings in '{}' to start GNS3".format(self._settings["version"], self.configDirectory())
QtWidgets.QMessageBox.critical(False, "Version error", error_message)
# Exit immediately not clean but we want to avoid any side effect that could corrupt the file
QtCore.QTimer.singleShot(0, app.quit)
@@ -174,21 +195,21 @@ class LocalConfig(QtCore.QObject):
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("1.4.0alpha1"):
servers = self._settings.get("Servers", {})
servers = self._settings.get("Servers", {})
if "LocalServer" in self._settings:
if "LocalServer" in self._settings:
servers["local_server"] = copy.copy(self._settings["LocalServer"])
# We migrate the server binary for OSX due to the change from py2app to CX freeze
# We migrate the server binary for OSX due to the change from py2app to CX freeze
if servers["local_server"]["path"] == "/Applications/GNS3.app/Contents/Resources/server/Contents/MacOS/gns3server":
servers["local_server"]["path"] = "gns3server"
if "RemoteServers" in self._settings:
if "RemoteServers" in self._settings:
servers["remote_servers"] = copy.copy(self._settings["RemoteServers"])
self._settings["Servers"] = servers
self._settings["Servers"] = servers
if "GUI" in self._settings:
if "GUI" in self._settings:
main_window = self._settings.get("MainWindow", {})
main_window["hide_getting_started_dialog"] = self._settings["GUI"].get("hide_getting_started_dialog", False)
self._settings["MainWindow"] = main_window
@@ -201,7 +222,7 @@ class LocalConfig(QtCore.QObject):
if self._settings["MainWindow"].get("telnet_console_command") not in PRECONFIGURED_TELNET_CONSOLE_COMMANDS.values():
self._settings["MainWindow"]["telnet_console_command"] = DEFAULT_TELNET_CONSOLE_COMMAND
# Migrate 1.X to 2.0
# Migrate 1.X to 2.0
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
if "Qemu" in self._settings:
# The internet VM is replaced by the nat Node
@@ -212,7 +233,7 @@ class LocalConfig(QtCore.QObject):
vms.append(vm)
self._settings["Qemu"]["vms"] = vms
# Starting with 2.0.0dev5 IOU licence is stored in the settings
# Starting with 2.0.0dev5 IOU licence is stored in the settings
if "version" not in self._settings or parse_version(self._settings["version"]) < parse_version("2.0.0"):
if "IOU" in self._settings and "iourc_path" in self._settings["IOU"] and "iourc_content" not in self._settings["IOU"]:
try:

View File

@@ -36,7 +36,6 @@ from gns3.local_config import LocalConfig
from gns3.local_server_config import LocalServerConfig
from gns3.utils.wait_for_connection_worker import WaitForConnectionWorker
from gns3.utils.progress_dialog import ProgressDialog
from gns3.utils.http import getSynchronous
from gns3.utils.sudo import sudo
from gns3.http_client import HTTPClient
from gns3.controller import Controller
@@ -371,11 +370,9 @@ class LocalServer(QtCore.QObject):
if sys.platform.startswith('win'):
if not self._checkWindowsService("npf") and not self._checkWindowsService("npcap"):
QtWidgets.QMessageBox.critical(self.parent(), "Error", "The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
return False
log.warning("The NPF or NPCAP service is not installed, please install Winpcap or Npcap and reboot.")
self._port = self._settings["port"]
# check the local server path
local_server_path = self.localServerPath()
if not local_server_path:
@@ -516,9 +513,7 @@ class LocalServer(QtCore.QObject):
:returns: boolean
"""
status, json_data = getSynchronous(self._settings["protocol"], self._settings["host"], self._port, "version",
timeout=2, user=self._settings["user"], password=self._settings["password"])
status, json_data = HTTPClient(self._settings).getSynchronous("GET", "/version", timeout=2)
if status == 401: # Auth issue that need to be solved later
return True
elif json_data is None:

View File

@@ -42,7 +42,7 @@ from .dialogs.edit_project_dialog import EditProjectDialog
from .dialogs.setup_wizard import SetupWizard
from .settings import GENERAL_SETTINGS
from .items.node_item import NodeItem
from .items.link_item import LinkItem
from .items.link_item import LinkItem, SvgIconItem
from .items.shape_item import ShapeItem
from .items.label_item import LabelItem
from .topology import Topology
@@ -329,7 +329,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def _openWebInterfaceActionSlot(self):
if Controller.instance().connected():
base_url = Controller.instance().httpClient().fullUrl()
webui_url = "{}/static/web-ui/local".format(base_url)
webui_url = "{}/static/web-ui/bundled".format(base_url)
QtGui.QDesktopServices.openUrl(QtCore.QUrl(webui_url))
def _showGridActionSlot(self):
@@ -364,7 +364,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
for item in self.uiGraphicsView.items():
if not isinstance(item, LinkItem) and not isinstance(item, LabelItem):
if not isinstance(item, LinkItem) and not isinstance(item, LabelItem) and not isinstance(item, SvgIconItem):
if self.uiLockAllAction.isChecked() and not item.locked():
item.setLocked(True)
elif not self.uiLockAllAction.isChecked() and item.locked():
@@ -392,9 +392,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._project_dialog = ProjectDialog(self)
self._project_dialog.show()
create_new_project = self._project_dialog.exec_()
# Close the device dock so it repopulates. Done in case switching between cloud and local.
self.uiNodesDockWidget.setVisible(False)
self.uiNodesDockWidget.setWindowTitle("")
if create_new_project:
Topology.instance().createLoadProject(self._project_dialog.getProjectSettings())
@@ -1130,6 +1127,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._settings["state"] = bytes(self.saveState().toBase64()).decode()
self.setSettings(self._settings)
Controller.instance().stopListenNotifications()
server = LocalServer.instance()
server.stopLocalServer(wait=True)

View File

@@ -38,7 +38,7 @@ class EthernetSwitch(Node):
# this is an always-on node
self.setStatus(Node.started)
self._always_on = True
self.settings().update({"ports_mapping": [], "console_type": "telnet"})
self.settings().update({"ports_mapping": [], "console_type": "none"})
def info(self):
"""

View File

@@ -59,7 +59,7 @@ ETHERNET_SWITCH_SETTINGS = {
"default_name_format": "Switch{0}",
"symbol": ":/symbols/ethernet_switch.svg",
"category": Node.switches,
"console_type": "telnet",
"console_type": "none",
"ports_mapping": [],
"node_type": "ethernet_switch"
}

View File

@@ -51,7 +51,8 @@ class DockerVM(Node):
"console_resolution": DOCKER_CONTAINER_SETTINGS["console_resolution"],
"console_http_port": DOCKER_CONTAINER_SETTINGS["console_http_port"],
"console_http_path": DOCKER_CONTAINER_SETTINGS["console_http_path"],
"extra_hosts": DOCKER_CONTAINER_SETTINGS["extra_hosts"]}
"extra_hosts": DOCKER_CONTAINER_SETTINGS["extra_hosts"],
"extra_volumes": DOCKER_CONTAINER_SETTINGS["extra_volumes"]}
self.settings().update(docker_vm_settings)

View File

@@ -103,7 +103,8 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
self.uiConsoleResolutionComboBox.setCurrentIndex(self.uiConsoleResolutionComboBox.findText(settings["console_resolution"]))
self.uiConsoleHttpPortSpinBox.setValue(settings["console_http_port"])
self.uiHttpConsolePathLineEdit.setText(settings["console_http_path"])
self.uiExtraHostsTextEdit.setText(settings["extra_hosts"])
self.uiExtraHostsTextEdit.setPlainText(settings["extra_hosts"])
self.uiExtraVolumeTextEdit.setPlainText("\n".join(settings["extra_volumes"]))
if not group:
self.uiNameLineEdit.setText(settings["name"])
@@ -175,6 +176,8 @@ class DockerVMConfigurationPage(QtWidgets.QWidget, Ui_dockerVMConfigPageWidget):
settings["console_http_port"] = self.uiConsoleHttpPortSpinBox.value()
settings["console_http_path"] = self.uiHttpConsolePathLineEdit.text()
settings["extra_hosts"] = self.uiExtraHostsTextEdit.toPlainText()
# only tidy input here, validation is performed server side
settings["extra_volumes"] = [ y for x in self.uiExtraVolumeTextEdit.toPlainText().split("\n") for y in [ x.strip() ] if y ]
if not group:
adapters = self.uiAdapterSpinBox.value()

View File

@@ -95,6 +95,9 @@ class DockerVMPreferencesPage(QtWidgets.QWidget, Ui_DockerVMPreferencesPageWidge
if docker_container["extra_hosts"]:
QtWidgets.QTreeWidgetItem(section_item, ["Extra hosts:", str(docker_container["extra_hosts"])])
if docker_container["extra_volumes"]:
QtWidgets.QTreeWidgetItem(section_item, ["Extra volumes:", "\n".join(docker_container["extra_volumes"])])
self.uiDockerVMInfoTreeWidget.expandAll()
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(0)
self.uiDockerVMInfoTreeWidget.resizeColumnToContents(1)

View File

@@ -43,5 +43,6 @@ DOCKER_CONTAINER_SETTINGS = {
"console_http_port": 80,
"console_http_path": "/",
"extra_hosts": "",
"extra_volumes": [],
"node_type": "docker"
}

View File

@@ -297,9 +297,34 @@ one per line)</string>
</widget>
</item>
<item row="0" column="1">
<widget class="QTextEdit" name="uiExtraHostsTextEdit"/>
<widget class="QPlainTextEdit" name="uiExtraHostsTextEdit">
<property name="placeholderText">
<string>e.g. router:192.168.0.1</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="uiExtraVolumeLabel">
<property name="text">
<string>Additional directories to
make persistent that are
not included in the image
VOLUMES config. One
directory per line.</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPlainTextEdit" name="uiExtraVolumeTextEdit">
<property name="plainText">
<string/>
</property>
<property name="placeholderText">
<string>e.g. /etc/sysctl.d</string>
</property>
</widget>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>

View File

@@ -143,11 +143,18 @@ class Ui_dockerVMConfigPageWidget(object):
self.uiExtraHostsLabel.setWordWrap(True)
self.uiExtraHostsLabel.setObjectName("uiExtraHostsLabel")
self.gridLayout_2.addWidget(self.uiExtraHostsLabel, 0, 0, 1, 1)
self.uiExtraHostsTextEdit = QtWidgets.QTextEdit(self.tab_2)
self.uiExtraHostsTextEdit = QtWidgets.QPlainTextEdit(self.tab_2)
self.uiExtraHostsTextEdit.setObjectName("uiExtraHostsTextEdit")
self.gridLayout_2.addWidget(self.uiExtraHostsTextEdit, 0, 1, 1, 1)
self.uiExtraVolumeLabel = QtWidgets.QLabel(self.tab_2)
self.uiExtraVolumeLabel.setObjectName("uiExtraVolumeLabel")
self.gridLayout_2.addWidget(self.uiExtraVolumeLabel, 1, 0, 1, 1)
self.uiExtraVolumeTextEdit = QtWidgets.QPlainTextEdit(self.tab_2)
self.uiExtraVolumeTextEdit.setPlainText("")
self.uiExtraVolumeTextEdit.setObjectName("uiExtraVolumeTextEdit")
self.gridLayout_2.addWidget(self.uiExtraVolumeTextEdit, 1, 1, 1, 1)
spacerItem = QtWidgets.QSpacerItem(20, 388, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout_2.addItem(spacerItem, 1, 1, 1, 1)
self.gridLayout_2.addItem(spacerItem, 2, 1, 1, 1)
self.uiTabWidget.addTab(self.tab_2, "")
self.tab_3 = QtWidgets.QWidget()
self.tab_3.setObjectName("tab_3")
@@ -201,6 +208,13 @@ class Ui_dockerVMConfigPageWidget(object):
"to the /etc/hosts file.\n"
"(hostname:IP\n"
"one per line)"))
self.uiExtraHostsTextEdit.setPlaceholderText(_translate("dockerVMConfigPageWidget", "e.g. router:192.168.0.1"))
self.uiExtraVolumeLabel.setText(_translate("dockerVMConfigPageWidget", "Additional directories to\n"
"make persistent that are\n"
"not included in the image\n"
"VOLUMES config. One\n"
"directory per line."))
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"))

View File

@@ -123,7 +123,7 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
# try to guess the platform
image = os.path.basename(self.uiIOSImageLineEdit.text())
match = re.match(r"^(c[0-9]+)p?\\-\w+", image.lower())
match = re.match(r"^(c[0-9]+)p?-\w+", image.lower())
if not match:
QtWidgets.QMessageBox.warning(self, "IOS image", "Could not detect the platform, make sure this is a valid IOS image!")
return

View File

@@ -111,7 +111,7 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
# try to guess the platform
image = os.path.basename(path)
match = re.match(r"^(c[0-9]+)\\-\w+", image)
match = re.match(r"^(c[0-9]+)p?-\w+", image)
if not match:
QtWidgets.QMessageBox.warning(self, "IOS image", "Could not detect the platform, make sure this is a valid IOS image!")
return

View File

@@ -14,6 +14,13 @@
<string>IOS on UNIX</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>IOU licence (iourc file):</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>

View File

@@ -1,21 +1,22 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/iou/ui/iou_preferences_page.ui'
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/iou/ui/iou_preferences_page.ui'
#
# Created by: PyQt5 UI code generator 5.7.1
# Created by: PyQt5 UI code generator 5.9
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_IOUPreferencesPageWidget(object):
def setupUi(self, IOUPreferencesPageWidget):
IOUPreferencesPageWidget.setObjectName("IOUPreferencesPageWidget")
IOUPreferencesPageWidget.resize(490, 532)
self.vboxlayout = QtWidgets.QVBoxLayout(IOUPreferencesPageWidget)
self.vboxlayout.setObjectName("vboxlayout")
self.label = QtWidgets.QLabel(IOUPreferencesPageWidget)
self.label.setObjectName("label")
self.vboxlayout.addWidget(self.label)
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.IOULicenceTextEdit = QtWidgets.QPlainTextEdit(IOUPreferencesPageWidget)
@@ -47,7 +48,9 @@ class Ui_IOUPreferencesPageWidget(object):
def retranslateUi(self, IOUPreferencesPageWidget):
_translate = QtCore.QCoreApplication.translate
IOUPreferencesPageWidget.setWindowTitle(_translate("IOUPreferencesPageWidget", "IOS on UNIX"))
self.label.setText(_translate("IOUPreferencesPageWidget", "IOU licence (iourc file):"))
self.IOULicenceTextEdit.setToolTip(_translate("IOUPreferencesPageWidget", "A license is required to run IOU. Copy & paste the content of your iourc file here or use the browse button to select a file. The license will be pushed to remote servers."))
self.uiIOURCPathToolButton.setText(_translate("IOUPreferencesPageWidget", "&Browse..."))
self.uiLicensecheckBox.setText(_translate("IOUPreferencesPageWidget", "Check for a valid IOU license key"))
self.uiRestoreDefaultsPushButton.setText(_translate("IOUPreferencesPageWidget", "Restore defaults"))

View File

@@ -152,9 +152,14 @@ class Module(QtCore.QObject):
:param directory: destination directory path
"""
node_names_cannot_export = []
for node in self._nodes:
if hasattr(node, "initialized") and node.initialized():
node.exportConfigsToDirectory(directory)
if not node.exportConfigsToDirectory(directory):
node_names_cannot_export.append(node.name())
if node_names_cannot_export:
log.warning("Config export is not supported by the following nodes: {}".format(" ".join(node_names_cannot_export)))
def importConfigs(self, directory):
"""

View File

@@ -48,9 +48,13 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
# Mandatory fields
self.uiNameWizardPage.registerField("vm_name*", self.uiNameLineEdit)
self.uiDiskWizardPage.registerField("hda_disk_image*", self.uiHdaDiskImageLineEdit)
self.uiInitrdKernelImageWizardPage.registerField("initrd*", self.uiInitrdImageLineEdit)
self.uiInitrdKernelImageWizardPage.registerField("kernel_image*", self.uiKernelImageLineEdit)
# Fill image combo boxes
self.addImageSelector(self.uiHdaDiskExistingImageRadioButton, self.uiHdaDiskImageListComboBox, self.uiHdaDiskImageLineEdit, self.uiHdaDiskImageToolButton, QemuVMConfigurationPage.getDiskImage, create_image_wizard=QemuImageWizard, create_button=self.uiHdaDiskImageCreateToolButton, image_suffix="-hda")
self.addImageSelector(self.uiLinuxExistingImageRadioButton, self.uiInitrdImageListComboBox, self.uiInitrdImageLineEdit, self.uiInitrdImageToolButton, QemuVMConfigurationPage.getDiskImage)
self.addImageSelector(self.uiLinuxExistingImageRadioButton, self.uiKernelImageListComboBox, self.uiKernelImageLineEdit, self.uiKernelImageToolButton, QemuVMConfigurationPage.getDiskImage)
def validateCurrentPage(self):
"""
@@ -61,7 +65,11 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
return False
if self.currentPage() == self.uiNameWizardPage:
self.uiRamSpinBox.setValue(512)
if self.uiLegacyASACheckBox.isChecked():
QtWidgets.QMessageBox.warning(self, "Legacy ASA VM", "Running ASA (with initrd/kernel) is not recommended and will not work on Windows 10, please use ASAv instead")
self.uiRamSpinBox.setValue(1024)
else:
self.uiRamSpinBox.setValue(256)
if self.currentPage() == self.uiBinaryMemoryWizardPage:
if not self.uiQemuListComboBox.count():
@@ -81,7 +89,7 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
if self.uiLocalRadioButton.isChecked() and not ComputeManager.instance().localPlatform().startswith("linux"):
QtWidgets.QMessageBox.warning(self, "QEMU on Windows or Mac", "The recommended way to run QEMU on Windows and OSX is to use the GNS3 VM")
if self.page(page_id) == self.uiDiskWizardPage:
if self.page(page_id) in [self.uiDiskWizardPage, self.uiInitrdKernelImageWizardPage]:
self.loadImagesList("/qemu/images")
elif self.page(page_id) == self.uiBinaryMemoryWizardPage:
try:
@@ -109,7 +117,9 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
is_64bit = sys.maxsize > 2 ** 32
if ComputeManager.instance().localPlatform().startswith("win") and self.uiLocalRadioButton.isChecked():
if is_64bit:
if self.uiLegacyASACheckBox.isChecked():
search_string = r"qemu-0.13.0\qemu-system-i386w.exe"
elif is_64bit:
# default is qemu-system-x86_64w.exe on Windows 64-bit with a remote server
search_string = "x86_64w.exe"
else:
@@ -144,8 +154,44 @@ class QemuVMWizard(VMWithImagesWizard, Ui_QemuVMWizard):
"compute_id": self._compute_id,
"category": Node.end_devices,
"hda_disk_image": self.uiHdaDiskImageLineEdit.text(),
"console_type": console_type,
"options": ""
"console_type": console_type
}
if self.uiLegacyASACheckBox.isChecked():
# special settings for legacy ASA VM
settings["adapters"] = 4
settings["initrd"] = self.uiInitrdImageLineEdit.text()
settings["kernel_image"] = self.uiKernelImageLineEdit.text()
settings["kernel_command_line"] = "ide_generic.probe_mask=0x01 ide_core.chs=0.0:980,16,32 auto nousb console=ttyS0,9600 bigphysarea=65536 ide1=noprobe no-hlt -net nic"
settings["options"] = "-no-kvm -icount auto -hdachs 980,16,32"
if not sys.platform.startswith("darwin"):
settings["cpu_throttling"] = 80 # limit to 80% CPU usage
settings["process_priority"] = "low"
settings["symbol"] = ":/symbols/asa.svg"
settings["category"] = Node.security_devices
if "options" not in settings:
settings["options"] = ""
if self._compute_id == "local" and (sys.platform.startswith("win") and qemu_path.endswith(r"qemu-0.11.0\qemu.exe")) or \
(sys.platform.startswith("darwin") and "GNS3.app" in qemu_path):
settings["options"] += " -vga none -vnc none"
settings["legacy_networking"] = True
else:
settings["options"] += " -nographic"
settings["options"] = settings["options"].strip()
return settings
def nextId(self):
"""
Wizard rules!
"""
current_id = self.currentId()
if self.page(current_id) == self.uiDiskWizardPage:
if self.uiLegacyASACheckBox.isChecked():
return self.uiDiskWizardPage.nextId()
return -1
elif self.page(current_id) == self.uiInitrdKernelImageWizardPage:
return -1
return QtWidgets.QWizard.nextId(self)

View File

@@ -106,6 +106,13 @@
<item row="0" column="1">
<widget class="QLineEdit" name="uiNameLineEdit"/>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="uiLegacyASACheckBox">
<property name="text">
<string>This is a legacy ASA VM</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWizardPage" name="uiBinaryMemoryWizardPage">
@@ -305,6 +312,114 @@
</item>
</layout>
</widget>
<widget class="QWizardPage" name="uiInitrdKernelImageWizardPage">
<property name="title">
<string>Linux boot specific settings</string>
</property>
<property name="subTitle">
<string>Please choose a initrd and a kernel image.</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QRadioButton" name="uiLinuxExistingImageRadioButton">
<property name="text">
<string>Existing image</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="uiNewImageRadioButton_4">
<property name="text">
<string>New Image</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QFormLayout" name="formLayout_2">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QComboBox" name="uiInitrdImageListComboBox"/>
</item>
<item>
<widget class="QLineEdit" name="uiInitrdImageLineEdit"/>
</item>
<item>
<widget class="QToolButton" name="uiInitrdImageToolButton">
<property name="text">
<string>&amp;Browse...</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextOnly</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="uiKernelImageLabel">
<property name="text">
<string>Kernel image (vmlinuz):</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_14">
<item>
<widget class="QComboBox" name="uiKernelImageListComboBox"/>
</item>
<item>
<widget class="QLineEdit" name="uiKernelImageLineEdit"/>
</item>
<item>
<widget class="QToolButton" name="uiKernelImageToolButton">
<property name="text">
<string>&amp;Browse...</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextOnly</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="uiInitrdLabel">
<property name="text">
<string>Initial RAM disk (initrd):</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<tabstops>
<tabstop>uiNameLineEdit</tabstop>

View File

@@ -60,6 +60,9 @@ class Ui_QemuVMWizard(object):
self.uiNameLineEdit = QtWidgets.QLineEdit(self.uiNameWizardPage)
self.uiNameLineEdit.setObjectName("uiNameLineEdit")
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 1, 1, 1)
self.uiLegacyASACheckBox = QtWidgets.QCheckBox(self.uiNameWizardPage)
self.uiLegacyASACheckBox.setObjectName("uiLegacyASACheckBox")
self.gridLayout.addWidget(self.uiLegacyASACheckBox, 1, 0, 1, 2)
QemuVMWizard.addPage(self.uiNameWizardPage)
self.uiBinaryMemoryWizardPage = QtWidgets.QWizardPage()
self.uiBinaryMemoryWizardPage.setObjectName("uiBinaryMemoryWizardPage")
@@ -146,6 +149,60 @@ class Ui_QemuVMWizard(object):
self.horizontalLayout_8.addWidget(self.uiHdaDiskImageCreateToolButton)
self.gridLayout_3.addLayout(self.horizontalLayout_8, 1, 0, 1, 1)
QemuVMWizard.addPage(self.uiDiskWizardPage)
self.uiInitrdKernelImageWizardPage = QtWidgets.QWizardPage()
self.uiInitrdKernelImageWizardPage.setObjectName("uiInitrdKernelImageWizardPage")
self.gridLayout_4 = QtWidgets.QGridLayout(self.uiInitrdKernelImageWizardPage)
self.gridLayout_4.setObjectName("gridLayout_4")
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
self.uiLinuxExistingImageRadioButton = QtWidgets.QRadioButton(self.uiInitrdKernelImageWizardPage)
self.uiLinuxExistingImageRadioButton.setChecked(True)
self.uiLinuxExistingImageRadioButton.setObjectName("uiLinuxExistingImageRadioButton")
self.horizontalLayout_7.addWidget(self.uiLinuxExistingImageRadioButton)
self.uiNewImageRadioButton_4 = QtWidgets.QRadioButton(self.uiInitrdKernelImageWizardPage)
self.uiNewImageRadioButton_4.setChecked(False)
self.uiNewImageRadioButton_4.setObjectName("uiNewImageRadioButton_4")
self.horizontalLayout_7.addWidget(self.uiNewImageRadioButton_4)
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_7.addItem(spacerItem2)
self.gridLayout_4.addLayout(self.horizontalLayout_7, 0, 0, 1, 1)
self.formLayout_2 = QtWidgets.QFormLayout()
self.formLayout_2.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)
self.formLayout_2.setObjectName("formLayout_2")
self.horizontalLayout_11 = QtWidgets.QHBoxLayout()
self.horizontalLayout_11.setObjectName("horizontalLayout_11")
self.uiInitrdImageListComboBox = QtWidgets.QComboBox(self.uiInitrdKernelImageWizardPage)
self.uiInitrdImageListComboBox.setObjectName("uiInitrdImageListComboBox")
self.horizontalLayout_11.addWidget(self.uiInitrdImageListComboBox)
self.uiInitrdImageLineEdit = QtWidgets.QLineEdit(self.uiInitrdKernelImageWizardPage)
self.uiInitrdImageLineEdit.setObjectName("uiInitrdImageLineEdit")
self.horizontalLayout_11.addWidget(self.uiInitrdImageLineEdit)
self.uiInitrdImageToolButton = QtWidgets.QToolButton(self.uiInitrdKernelImageWizardPage)
self.uiInitrdImageToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
self.uiInitrdImageToolButton.setObjectName("uiInitrdImageToolButton")
self.horizontalLayout_11.addWidget(self.uiInitrdImageToolButton)
self.formLayout_2.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_11)
self.uiKernelImageLabel = QtWidgets.QLabel(self.uiInitrdKernelImageWizardPage)
self.uiKernelImageLabel.setObjectName("uiKernelImageLabel")
self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.uiKernelImageLabel)
self.horizontalLayout_14 = QtWidgets.QHBoxLayout()
self.horizontalLayout_14.setObjectName("horizontalLayout_14")
self.uiKernelImageListComboBox = QtWidgets.QComboBox(self.uiInitrdKernelImageWizardPage)
self.uiKernelImageListComboBox.setObjectName("uiKernelImageListComboBox")
self.horizontalLayout_14.addWidget(self.uiKernelImageListComboBox)
self.uiKernelImageLineEdit = QtWidgets.QLineEdit(self.uiInitrdKernelImageWizardPage)
self.uiKernelImageLineEdit.setObjectName("uiKernelImageLineEdit")
self.horizontalLayout_14.addWidget(self.uiKernelImageLineEdit)
self.uiKernelImageToolButton = QtWidgets.QToolButton(self.uiInitrdKernelImageWizardPage)
self.uiKernelImageToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
self.uiKernelImageToolButton.setObjectName("uiKernelImageToolButton")
self.horizontalLayout_14.addWidget(self.uiKernelImageToolButton)
self.formLayout_2.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_14)
self.uiInitrdLabel = QtWidgets.QLabel(self.uiInitrdKernelImageWizardPage)
self.uiInitrdLabel.setObjectName("uiInitrdLabel")
self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.uiInitrdLabel)
self.gridLayout_4.addLayout(self.formLayout_2, 1, 0, 1, 1)
QemuVMWizard.addPage(self.uiInitrdKernelImageWizardPage)
self.retranslateUi(QemuVMWizard)
QtCore.QMetaObject.connectSlotsByName(QemuVMWizard)
@@ -168,6 +225,7 @@ class Ui_QemuVMWizard(object):
self.uiNameWizardPage.setTitle(_translate("QemuVMWizard", "QEMU VM name"))
self.uiNameWizardPage.setSubTitle(_translate("QemuVMWizard", "Please choose a descriptive name for your new QEMU virtual machine."))
self.uiNameLabel.setText(_translate("QemuVMWizard", "Name:"))
self.uiLegacyASACheckBox.setText(_translate("QemuVMWizard", "This is a legacy ASA VM"))
self.uiBinaryMemoryWizardPage.setTitle(_translate("QemuVMWizard", "QEMU binary and memory"))
self.uiBinaryMemoryWizardPage.setSubTitle(_translate("QemuVMWizard", "Please check the Qemu binary is correctly set and the virtual machine has enough memory to work."))
self.uiQemuListLabel.setText(_translate("QemuVMWizard", "Qemu binary:"))
@@ -188,4 +246,12 @@ class Ui_QemuVMWizard(object):
self.uiHdaDiskImageLabel.setText(_translate("QemuVMWizard", "Disk image (hda):"))
self.uiHdaDiskImageToolButton.setText(_translate("QemuVMWizard", "&Browse..."))
self.uiHdaDiskImageCreateToolButton.setText(_translate("QemuVMWizard", "&Create"))
self.uiInitrdKernelImageWizardPage.setTitle(_translate("QemuVMWizard", "Linux boot specific settings"))
self.uiInitrdKernelImageWizardPage.setSubTitle(_translate("QemuVMWizard", "Please choose a initrd and a kernel image."))
self.uiLinuxExistingImageRadioButton.setText(_translate("QemuVMWizard", "Existing image"))
self.uiNewImageRadioButton_4.setText(_translate("QemuVMWizard", "New Image"))
self.uiInitrdImageToolButton.setText(_translate("QemuVMWizard", "&Browse..."))
self.uiKernelImageLabel.setText(_translate("QemuVMWizard", "Kernel image (vmlinuz):"))
self.uiKernelImageToolButton.setText(_translate("QemuVMWizard", "&Browse..."))
self.uiInitrdLabel.setText(_translate("QemuVMWizard", "Initial RAM disk (initrd):"))

View File

@@ -72,7 +72,7 @@ class TraceNGNode(Node):
self._last_destination = destination
params = {"destination": destination}
log.debug("{} is starting".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, body=params, timeout=None, progressText="{} is starting".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, body=params, timeout=None, showProgress=False)
def info(self):
"""

View File

@@ -223,7 +223,7 @@ class Node(BaseNode):
return
log.debug("{} is starting".format(self.name()))
self.post("/start", self._startCallback, timeout=None, progressText="{} is starting".format(self.name()))
self.post("/start", self._startCallback, timeout=None, showProgress=False)
def _startCallback(self, result, error=False, **kwargs):
"""
@@ -249,7 +249,7 @@ class Node(BaseNode):
return
log.debug("{} is stopping".format(self.name()))
self.post("/stop", self._stopCallback, timeout=None, progressText="{} is stopping".format(self.name()))
self.post("/stop", self._stopCallback, timeout=None, showProgress=False)
def _stopCallback(self, result, error=False, **kwargs):
"""
@@ -279,7 +279,7 @@ class Node(BaseNode):
return
log.debug("{} is being suspended".format(self.name()))
self.post("/suspend", self._suspendCallback, timeout=None, progressText="{} is suspending".format(self.name()))
self.post("/suspend", self._suspendCallback, timeout=None, showProgress=False)
def _suspendCallback(self, result, error=False, **kwargs):
"""
@@ -301,7 +301,7 @@ class Node(BaseNode):
"""
log.debug("{} is being reloaded".format(self.name()))
self.post("/reload", self._reloadCallback, timeout=None, progressText="{} is reloading".format(self.name()))
self.post("/reload", self._reloadCallback, timeout=None, showProgress=False)
def _reloadCallback(self, result, error=False, **kwargs):
"""
@@ -455,7 +455,7 @@ class Node(BaseNode):
if not skip_controller:
for link in self.links():
link.setDeleting()
self.controllerHttpDelete("/nodes/{node_id}".format(node_id=self._node_id), self._deleteCallback)
self.controllerHttpDelete("/nodes/{node_id}".format(node_id=self._node_id), self._deleteCallback, showProgress=False)
else:
self.deleted_signal.emit()
self._module.removeNode(self)
@@ -485,7 +485,7 @@ class Node(BaseNode):
"y": int(y),
"z": int(z)}
self.post("/duplicate", self._duplicateCallback, body=params, timeout=None, progressText="{} is being duplicated".format(self.name()))
self.post("/duplicate", self._duplicateCallback, body=params, timeout=None, showProgress=False)
def _duplicateCallback(self, result, error=False, **kwargs):
"""
@@ -775,12 +775,13 @@ class Node(BaseNode):
"""
if not hasattr(self, "configFiles"):
return
return False
for file in self.configFiles():
self.get("/files/{file}".format(file=file),
self._exportConfigsToDirectoryCallback,
context={"directory": directory, "file": file},
raw=True)
return True
def _exportConfigsToDirectoryCallback(self, result, error=False, raw_body=None, context={}, **kwargs):
"""

View File

@@ -189,7 +189,10 @@ class Progress(QtCore.QObject):
# Due to Qt limitations for large numbers (above 32bit int) we calculate "progress" ourselves
current, maximum = self._normalize(query['current'], query['maximum'])
progress_dialog.setMaximum(maximum)
progress_dialog.setValue(current)
try:
progress_dialog.setValue(current)
except OverflowError:
progress_dialog.setValue(100)
if text and query["maximum"] > 1000:
text += "\n{} / {}".format(human_filesize(query["current"]), human_filesize(query["maximum"]))

View File

@@ -17,14 +17,12 @@
import os
import json
from .qt import QtCore, qpartial, QtWidgets, QtNetwork, qslot
from .qt import QtCore, qpartial, QtNetwork, QtWebSockets, qslot
from gns3.controller import Controller
from gns3.compute_manager import ComputeManager
from gns3.topology import Topology
from gns3.local_config import LocalConfig
from gns3.settings import GRAPHICS_VIEW_SETTINGS
from gns3.template_manager import TemplateManager
from gns3.utils import parse_version
import logging
@@ -49,7 +47,6 @@ class Project(QtCore.QObject):
# Called when project is fully loaded
project_loaded_signal = QtCore.Signal()
def __init__(self):
self._id = None
@@ -84,6 +81,7 @@ class Project(QtCore.QObject):
# Due to bug in Qt on some version we need a dedicated network manager
self._notification_network_manager = QtNetwork.QNetworkAccessManager()
self._notification_stream = None
self._websocket = QtWebSockets.QWebSocket()
super().__init__()
@@ -485,7 +483,8 @@ class Project(QtCore.QObject):
if self._closed:
self._closed = False
self._closing = False
self._startListenNotifications()
if not self._notification_stream:
self._startListenNotifications()
self.project_updated_signal.emit()
self.project_loaded_signal.emit()
@@ -537,7 +536,8 @@ class Project(QtCore.QObject):
if self._closed:
self._closed = False
self._closing = False
self._startListenNotifications()
if not self._notification_stream:
self._startListenNotifications()
self.project_updated_signal.emit()
self.get("/nodes", self._listNodesCallback)
@@ -609,7 +609,7 @@ class Project(QtCore.QObject):
log.debug("Stop listening for notifications from project %s", self._id)
stream = self._notification_stream
self._notification_stream = None
stream.abort()
stream.close()
def _startListenNotifications(self):
if not Controller.instance().connected():
@@ -627,7 +627,7 @@ class Project(QtCore.QObject):
else:
path = "/projects/{project_id}/notifications/ws".format(project_id=self._id)
self._notification_stream = Controller.instance().connectProjectWebSocket(path)
self._notification_stream = Controller.instance().httpClient().connectWebSocket(self._websocket, path)
self._notification_stream.textMessageReceived.connect(self._websocket_event_received)
self._notification_stream.error.connect(self._websocket_error)
@@ -700,7 +700,7 @@ class Project(QtCore.QObject):
elif result["action"] == "project.updated":
self._projectUpdatedCallback(result["event"])
elif result["action"] == "snapshot.restored":
Topology.instance().createLoadProject({"project_id": result["event"]["project_id"]})
Topology.instance().restoreSnapshot(result["event"]["project_id"])
elif result["action"] == "log.error":
log.error(result["event"]["message"])
elif result["action"] == "log.warning":

View File

@@ -150,9 +150,7 @@ class Appliance(collections.Mapping):
for image_type, image in version["images"].items():
image["type"] = image_type
img = self._registry.search_image_file(
self.emulator(), image["filename"], image.get("md5sum"), image.get("filesize")
)
img = self._registry.search_image_file(self.emulator(), image["filename"], image.get("md5sum"), image.get("filesize"))
if img is None:
if "md5sum" in image:
raise ApplianceError("File {} with checksum {} not found for {}".format(image["filename"], image["md5sum"], appliance["name"]))

View File

@@ -17,10 +17,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import urllib.request
import shutil
from ssl import CertificateError
from ..qt import QtCore, QtWidgets, QtNetwork
from ..controller import Controller
from .config import Config, ConfigException
@@ -33,7 +33,7 @@ class ApplianceToTemplate:
Appliance installation.
"""
def new_template(self, appliance_config, server, controller_symbols=None):
def new_template(self, appliance_config, server, controller_symbols=None, parent=None):
"""
Creates a new template from an appliance.
@@ -41,6 +41,7 @@ class ApplianceToTemplate:
:param server
"""
self._parent = parent
new_template = {
"compute_id": server,
"name": appliance_config["name"]
@@ -195,7 +196,7 @@ class ApplianceToTemplate:
url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol_id)
try:
urllib.request.urlretrieve(url, path)
self._downloadApplianceSymbol(url, path)
controller = Controller.instance()
controller.clearStaticCache()
if controller.isRemote():
@@ -203,3 +204,31 @@ class ApplianceToTemplate:
return os.path.basename(path)
except (OSError, CertificateError):
return None
def _downloadApplianceSymbol(self, url, path, timeout=30):
"""
Download an appliance symbol in a synchronous way.
"""
network_manager = QtNetwork.QNetworkAccessManager()
request = QtNetwork.QNetworkRequest(QtCore.QUrl(url))
request.setRawHeader(b'User-Agent', b'GNS3 symbol downloader')
reply = network_manager.get(request)
progress_dialog = QtWidgets.QProgressDialog("Downloading '{}' appliance symbol...".format(os.path.basename(path)), "Cancel", 0, 0, self._parent)
progress_dialog.setMinimumDuration(0)
reply.finished.connect(progress_dialog.close)
QtCore.QTimer.singleShot(timeout * 1000, progress_dialog.close)
log.debug("Downloading appliance symbol from '{}'".format(url))
progress_dialog.show()
progress_dialog.exec_()
status = reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
if reply.error() == QtNetwork.QNetworkReply.NoError and status == 200:
try:
with open(path, 'wb+') as f:
f.write(reply.readAll())
except OSError as e:
log.debug("Error while saving appliance symbol to '{}': {}".format(path, e))
raise
log.debug("Appliance symbol downloaded and saved to '{}'".format(path))
else:
log.warning("Error when downloading appliance symbol from '{}': {}".format(url, reply.errorString()))

View File

@@ -38,7 +38,7 @@ class Image:
self._location = "local"
self._emulator = emulator
self.path = path
self._path = path
if filename is None:
self._filename = os.path.basename(self.path)
else:
@@ -66,6 +66,13 @@ class Image:
"""
return self._filename
@property
def path(self):
"""
:returns: Image path
"""
return self._path
@property
def version(self):
"""
@@ -90,12 +97,12 @@ class Image:
"""
if self._md5sum is None:
from_cache = Image._cache.get(self.path)
from_cache = Image._cache.get(self._path)
if from_cache:
self._md5sum = from_cache
return self._md5sum
md5_file = self.path + ".md5sum"
md5_file = self._path + ".md5sum"
if os.path.exists(md5_file):
try:
with open(md5_file) as f:
@@ -105,20 +112,20 @@ class Image:
log.debug("Could not read '{}': {}".format(md5_file, e))
try:
if not os.path.isfile(self.path):
if not os.path.isfile(self._path):
return None
m = hashlib.md5()
with open(self._path, "rb") as f:
while True:
buf = f.read(4096)
if not buf:
break
m.update(buf)
except (OSError, PermissionError) as e:
log.debug("Cannot access '{}': {}".format(self.path, e))
log.debug("Cannot access '{}': {}".format(self._path, e))
return None
m = hashlib.md5()
with open(self.path, "rb") as f:
while True:
buf = f.read(4096)
if not buf:
break
m.update(buf)
self._md5sum = m.hexdigest()
Image._cache[self.path] = self._md5sum
Image._cache[self._path] = self._md5sum
return self._md5sum
@md5sum.setter
@@ -133,7 +140,7 @@ class Image:
if self._filesize is not None:
return self._filesize
try:
self._filesize = os.path.getsize(self.path)
self._filesize = os.path.getsize(self._path)
return self._filesize
except OSError:
return 0

View File

@@ -87,12 +87,12 @@ class Registry(QtCore.QObject):
return remote_image
for directory in self._images_dirs:
log.debug("Search images %s (%s) in %s", filename, md5sum, directory)
log.debug("Search image {} (MD5={} SIZE={}) in '{}'".format(filename, md5sum, size, directory))
if os.path.exists(directory):
for file in os.listdir(directory):
if not file.endswith(".md5sum") and not file.startswith("."):
path = os.path.join(directory, file)
try:
try:
for file in os.listdir(directory):
if not file.endswith(".md5sum") and not file.startswith("."):
path = os.path.join(directory, file)
if os.path.isfile(path):
if md5sum is None or strict_md5_check is False:
if filename == os.path.basename(path):
@@ -104,9 +104,9 @@ class Registry(QtCore.QObject):
if size is None or (file_size - 10 < size and file_size + 10 > size):
image = Image(emulator, path)
if image.md5sum == md5sum:
log.debug("Found images %s (%s) in %s", filename, md5sum, image.path)
log.debug("Found image {} (MD5={}) in {}".format(filename, md5sum, image.path))
return image
except (OSError, PermissionError) as e:
log.error("Cannot scan {}: {}".format(path, e))
except (OSError, PermissionError) as e:
log.error("Cannot scan {}: {}".format(path, e))
return None

View File

@@ -166,6 +166,13 @@
"extra_hosts": {
"description": "Hosts which will be written to /etc/hosts into container" ,
"type": "string"
},
"extra_volumes": {
"description": "Additional directories to make persistent that are not included in the images VOLUME directive" ,
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [

View File

@@ -55,7 +55,8 @@ if sys.platform.startswith("win"):
# windows 32-bit
program_files_x86 = program_files = os.environ["PROGRAMFILES"]
PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {'Putty (included with GNS3)': 'putty.exe -telnet %h %p -wt "%d" -gns3 5 -skin 4',
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': 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),
'SuperPutty': r'SuperPutty.exe -telnet "%h -P %p -wt \"%d\""',
@@ -75,7 +76,7 @@ if sys.platform.startswith("win"):
DEFAULT_DELAY_CONSOLE_ALL = 1500
else:
PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Solar-Putty (included with GNS3 downloaded from gns3.com)"] = 'Solar-PuTTY.exe --telnet --hostname %h --port %p --name "%d"'
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Putty (included with GNS3)"]
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Putty (normal standalone version)"]
elif sys.platform.startswith("darwin"):
# Mac OS X

View File

@@ -213,6 +213,7 @@ class TemplateManager(QtCore.QObject):
self._controller.post("/projects/{project_id}/templates/{template_id}".format(project_id=project.id(), template_id=template_id),
self._createNodeFromTemplateCallback,
params,
showProgress=False,
timeout=None)
return True

View File

@@ -109,14 +109,14 @@ class Topology(QtCore.QObject):
return self._project
def setProject(self, project):
def setProject(self, project, snapshot=False):
"""
Set current project
:param project: Project instance
"""
if self._project:
if self._project and snapshot is False:
# Assert to detect when we create a new project object for the same project
assert project is None or (project != self._project and project.id != self._project.id)
self._project.stopListenNotifications()
@@ -134,7 +134,6 @@ class Topology(QtCore.QObject):
self.project_changed_signal.emit()
def _projectUpdatedSlot(self):
if not self._project or not self._project.filesDir() or not self._project.filename():
return
@@ -193,6 +192,7 @@ class Topology(QtCore.QObject):
"""
Create load a project based on settings, not on the .gns3
"""
self.setProject(None)
from .project import Project
project = Project()
@@ -215,6 +215,17 @@ class Topology(QtCore.QObject):
self._main_window.uiStatusBar.showMessage("Project created", 2000)
return project
def restoreSnapshot(self, project_id):
"""
Restore a snapshot for a given project.
"""
assert self._project.id() == project_id
project = self._project
self.setProject(project, snapshot=True)
project.load()
self._main_window.uiStatusBar.showMessage("Snapshot restored", 2000)
def loadProject(self, path):
"""
Loads a project into GNS3.

View File

@@ -67,13 +67,13 @@
<item row="4" column="1">
<widget class="QSpinBox" name="uiNodeGridSizeSpinBox">
<property name="minimum">
<number>10</number>
<number>5</number>
</property>
<property name="maximum">
<number>150</number>
</property>
<property name="singleStep">
<number>10</number>
<number>5</number>
</property>
<property name="value">
<number>75</number>
@@ -83,13 +83,13 @@
<item row="5" column="1">
<widget class="QSpinBox" name="uiDrawingGridSizeSpinBox">
<property name="minimum">
<number>10</number>
<number>5</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="singleStep">
<number>10</number>
<number>5</number>
</property>
<property name="value">
<number>25</number>

View File

@@ -37,16 +37,16 @@ class Ui_EditProjectDialog(object):
self.uiSceneWidthSpinBox.setObjectName("uiSceneWidthSpinBox")
self.uiGeneralGrid.addWidget(self.uiSceneWidthSpinBox, 2, 1, 1, 1)
self.uiNodeGridSizeSpinBox = QtWidgets.QSpinBox(self.uiGeneralTab)
self.uiNodeGridSizeSpinBox.setMinimum(10)
self.uiNodeGridSizeSpinBox.setMinimum(5)
self.uiNodeGridSizeSpinBox.setMaximum(150)
self.uiNodeGridSizeSpinBox.setSingleStep(10)
self.uiNodeGridSizeSpinBox.setSingleStep(5)
self.uiNodeGridSizeSpinBox.setProperty("value", 75)
self.uiNodeGridSizeSpinBox.setObjectName("uiNodeGridSizeSpinBox")
self.uiGeneralGrid.addWidget(self.uiNodeGridSizeSpinBox, 4, 1, 1, 1)
self.uiDrawingGridSizeSpinBox = QtWidgets.QSpinBox(self.uiGeneralTab)
self.uiDrawingGridSizeSpinBox.setMinimum(10)
self.uiDrawingGridSizeSpinBox.setMinimum(5)
self.uiDrawingGridSizeSpinBox.setMaximum(100)
self.uiDrawingGridSizeSpinBox.setSingleStep(10)
self.uiDrawingGridSizeSpinBox.setSingleStep(5)
self.uiDrawingGridSizeSpinBox.setProperty("value", 25)
self.uiDrawingGridSizeSpinBox.setObjectName("uiDrawingGridSizeSpinBox")
self.uiGeneralGrid.addWidget(self.uiDrawingGridSizeSpinBox, 5, 1, 1, 1)

View File

@@ -61,14 +61,24 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="1" column="1" colspan="2">
<widget class="QComboBox" name="uiCompressionComboBox"/>
</item>
<item row="2" column="0" colspan="3">
<widget class="QCheckBox" name="uiIncludeImagesCheckBox">
<property name="text">
<string>Include base images</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="uiIncludeSnapshotsCheckBox">
<property name="text">
<string>Include snapshots</string>
</property>
</widget>
</item>
<item row="4" column="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -81,9 +91,6 @@
</property>
</spacer>
</item>
<item row="1" column="1" colspan="2">
<widget class="QComboBox" name="uiCompressionComboBox"/>
</item>
</layout>
</widget>
<widget class="QWizardPage" name="uiProjectReadmeWizardPage">

View File

@@ -38,14 +38,17 @@ class Ui_ExportProjectWizard(object):
self.uiCompressionLabel = QtWidgets.QLabel(self.uiExportOptionsWizardPage)
self.uiCompressionLabel.setObjectName("uiCompressionLabel")
self.gridLayout.addWidget(self.uiCompressionLabel, 1, 0, 1, 1)
self.uiIncludeImagesCheckBox = QtWidgets.QCheckBox(self.uiExportOptionsWizardPage)
self.uiIncludeImagesCheckBox.setObjectName("uiIncludeImagesCheckBox")
self.gridLayout.addWidget(self.uiIncludeImagesCheckBox, 2, 0, 1, 2)
spacerItem = QtWidgets.QSpacerItem(20, 247, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem, 3, 1, 1, 1)
self.uiCompressionComboBox = QtWidgets.QComboBox(self.uiExportOptionsWizardPage)
self.uiCompressionComboBox.setObjectName("uiCompressionComboBox")
self.gridLayout.addWidget(self.uiCompressionComboBox, 1, 1, 1, 2)
self.uiIncludeImagesCheckBox = QtWidgets.QCheckBox(self.uiExportOptionsWizardPage)
self.uiIncludeImagesCheckBox.setObjectName("uiIncludeImagesCheckBox")
self.gridLayout.addWidget(self.uiIncludeImagesCheckBox, 2, 0, 1, 3)
self.uiIncludeSnapshotsCheckBox = QtWidgets.QCheckBox(self.uiExportOptionsWizardPage)
self.uiIncludeSnapshotsCheckBox.setObjectName("uiIncludeSnapshotsCheckBox")
self.gridLayout.addWidget(self.uiIncludeSnapshotsCheckBox, 3, 0, 1, 2)
spacerItem = QtWidgets.QSpacerItem(20, 247, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem, 4, 2, 1, 1)
ExportProjectWizard.addPage(self.uiExportOptionsWizardPage)
self.uiProjectReadmeWizardPage = QtWidgets.QWizardPage()
self.uiProjectReadmeWizardPage.setObjectName("uiProjectReadmeWizardPage")
@@ -68,6 +71,7 @@ class Ui_ExportProjectWizard(object):
self.uiPathBrowserToolButton.setText(_translate("ExportProjectWizard", "Browse..."))
self.uiCompressionLabel.setText(_translate("ExportProjectWizard", "Compression:"))
self.uiIncludeImagesCheckBox.setText(_translate("ExportProjectWizard", "Include base images"))
self.uiIncludeSnapshotsCheckBox.setText(_translate("ExportProjectWizard", "Include snapshots"))
self.uiProjectReadmeWizardPage.setTitle(_translate("ExportProjectWizard", "Readme file"))
self.uiProjectReadmeWizardPage.setSubTitle(_translate("ExportProjectWizard", "Write a summary of the project."))
self.uiReadmeTextEdit.setHtml(_translate("ExportProjectWizard", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"

View File

@@ -136,11 +136,15 @@
<item>
<widget class="QGroupBox" name="uiSymbolThemeGroupBox">
<property name="title">
<string>Symbol theme</string>
<string>Symbol theme for new templates</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QComboBox" name="uiSymbolThemeComboBox"/>
<widget class="QComboBox" name="uiSymbolThemeComboBox">
<property name="toolTip">
<string>Symbol theme support only works when adding a new template using the recommended method in the template wizard.</string>
</property>
</widget>
</item>
</layout>
</widget>
@@ -876,13 +880,13 @@
</sizepolicy>
</property>
<property name="minimum">
<number>10</number>
<number>5</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="singleStep">
<number>10</number>
<number>5</number>
</property>
<property name="value">
<number>25</number>
@@ -898,13 +902,13 @@
</sizepolicy>
</property>
<property name="minimum">
<number>10</number>
<number>5</number>
</property>
<property name="maximum">
<number>150</number>
</property>
<property name="singleStep">
<number>10</number>
<number>5</number>
</property>
<property name="value">
<number>75</number>

View File

@@ -396,9 +396,9 @@ class Ui_GeneralPreferencesPageWidget(object):
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiDrawingGridSizeSpinBox.sizePolicy().hasHeightForWidth())
self.uiDrawingGridSizeSpinBox.setSizePolicy(sizePolicy)
self.uiDrawingGridSizeSpinBox.setMinimum(10)
self.uiDrawingGridSizeSpinBox.setMinimum(5)
self.uiDrawingGridSizeSpinBox.setMaximum(100)
self.uiDrawingGridSizeSpinBox.setSingleStep(10)
self.uiDrawingGridSizeSpinBox.setSingleStep(5)
self.uiDrawingGridSizeSpinBox.setProperty("value", 25)
self.uiDrawingGridSizeSpinBox.setObjectName("uiDrawingGridSizeSpinBox")
self.gridLayout_3.addWidget(self.uiDrawingGridSizeSpinBox, 3, 1, 1, 1)
@@ -408,9 +408,9 @@ class Ui_GeneralPreferencesPageWidget(object):
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiNodeGridSizeSpinBox.sizePolicy().hasHeightForWidth())
self.uiNodeGridSizeSpinBox.setSizePolicy(sizePolicy)
self.uiNodeGridSizeSpinBox.setMinimum(10)
self.uiNodeGridSizeSpinBox.setMinimum(5)
self.uiNodeGridSizeSpinBox.setMaximum(150)
self.uiNodeGridSizeSpinBox.setSingleStep(10)
self.uiNodeGridSizeSpinBox.setSingleStep(5)
self.uiNodeGridSizeSpinBox.setProperty("value", 75)
self.uiNodeGridSizeSpinBox.setObjectName("uiNodeGridSizeSpinBox")
self.gridLayout_3.addWidget(self.uiNodeGridSizeSpinBox, 2, 1, 1, 1)
@@ -488,7 +488,8 @@ class Ui_GeneralPreferencesPageWidget(object):
self.label_3.setText(_translate("GeneralPreferencesPageWidget", "My custom appliances:"))
self.uiAppliancesPathToolButton.setText(_translate("GeneralPreferencesPageWidget", "Browse..."))
self.uiStyleGroupBox.setTitle(_translate("GeneralPreferencesPageWidget", "Interface style"))
self.uiSymbolThemeGroupBox.setTitle(_translate("GeneralPreferencesPageWidget", "Symbol theme"))
self.uiSymbolThemeGroupBox.setTitle(_translate("GeneralPreferencesPageWidget", "Symbol theme for new templates"))
self.uiSymbolThemeComboBox.setToolTip(_translate("GeneralPreferencesPageWidget", "Symbol theme support only works when adding a new template using the recommended method in the template wizard."))
self.uiConfigurationFileGroupBox.setTitle(_translate("GeneralPreferencesPageWidget", "Configuration file"))
self.uiImportConfigurationFilePushButton.setText(_translate("GeneralPreferencesPageWidget", "&Import"))
self.uiExportConfigurationFilePushButton.setText(_translate("GeneralPreferencesPageWidget", "&Export"))

View File

@@ -92,7 +92,7 @@ class UpdateManager(QtCore.QObject):
self._silent = silent
self._parent = parent
if hasattr(sys, "frozen") and LocalConfig.instance().experimental():
if not hasattr(sys, "frozen") and LocalConfig.instance().experimental():
url = 'https://pypi.org/pypi/gns3-gui/json'
self._get(url, self._pypiReplySlot)
else:

View File

@@ -28,16 +28,17 @@ class ExportProjectWorker(QtCore.QObject):
finished = QtCore.pyqtSignal()
updated = QtCore.pyqtSignal(int)
def __init__(self, project, path, include_images, compression):
def __init__(self, project, path, include_images, include_snapshots, compression):
super().__init__()
self._project = project
self._include_images = include_images
self._include_snapshots = include_snapshots
self._path = path
self._compression = compression
def run(self):
if self._project:
self._project.get("/export?include_images={}&compression={}".format(self._include_images, self._compression),
self._project.get("/export?include_images={}&include_snapshots={}&compression={}".format(self._include_images, self._include_snapshots, self._compression),
self._exportReceived,
downloadProgressCallback=self._downloadFileProgress,
timeout=None)

View File

@@ -1,74 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import ssl
import http
import json
import base64
import urllib.request
import logging
log = logging.getLogger(__name__)
def getSynchronous(protocol, host, port, endpoint, timeout=2, user=None, password=None):
"""
:returns: Tuple (Status code, json of anwser). Status 0 is a non HTTP error
"""
try:
url = "{protocol}://{host}:{port}/v2/{endpoint}".format(protocol=protocol, host=host, port=port, endpoint=endpoint)
request = urllib.request.Request(url)
if user is not None and len(user) > 0:
log.debug("Synchronous get {} with user '{}'".format(url, user))
base64string = base64.encodebytes('{}:{}'.format(user, password).encode()).replace(b'\n', b'')
request.add_header("Authorization", "Basic {}".format(base64string.decode()))
else:
log.debug("Synchronous get {} (no authentication)".format(url))
if sys.version_info >= (3, 5):
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
response = urllib.request.urlopen(request, timeout=timeout, context=ctx)
else:
response = urllib.request.urlopen(request, timeout=timeout)
content_type = response.getheader("CONTENT-TYPE")
if response.status == 200:
if content_type == "application/json":
content = response.read()
json_data = json.loads(content.decode("utf-8"))
return response.status, json_data
else:
return response.status, None
except http.client.InvalidURL as e:
log.warning("Invalid local server url: {}".format(e))
return 0, None
except http.client.UnknownProtocol:
log.warning("Unknown server running on {}:{}".format(host, port))
return 0, None
except urllib.error.URLError:
# Connection refused. It's a normal behavior if server is not started
return 0, None
except urllib.error.HTTPError as e:
log.debug("Error during get on {}:{}: {}".format(host, port, e))
return e.code, None
except (OSError, http.client.BadStatusLine, ValueError, SystemError) as e:
log.debug("Error during get on {}:{}: {}".format(host, port, e))
return 0, None

View File

@@ -23,7 +23,7 @@
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "2.2.0a2"
__version__ = "2.2.0b2"
__version_info__ = (2, 2, 0, -99)
# If it's a git checkout try to add the commit

View File

@@ -18,7 +18,6 @@
import pytest
import json
import os
from unittest.mock import patch
from gns3.registry.config import Config
from gns3.settings import LOCAL_SERVER_SETTINGS
@@ -209,47 +208,6 @@ def test_add_appliance_with_symbol_from_symbols_dir(empty_config, linux_microcor
assert new_template["symbol"] == "linux_guest.svg"
def test_add_appliance_with_symbol_from_web(empty_config, linux_microcore_img, symbols_dir):
with open("tests/registry/appliances/microcore-linux.gns3a", encoding="utf-8") as f:
config = json.load(f)
config["images"] = [
{
"type": "hda_disk_image",
"filename": "linux-microcore-3.4.1.img",
"path": linux_microcore_img
}
]
config["symbol"] = "linux_guest.svg"
symbol_path = os.path.join(symbols_dir, "linux_guest.svg")
with patch("urllib.request.urlretrieve") as mock:
new_template = ApplianceToTemplate().new_template(config, "local")
mock.assert_called_with("https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/linux_guest.svg", symbol_path)
assert new_template["symbol"] == "linux_guest.svg"
def test_add_appliance_with_symbol_from_web_error(empty_config, linux_microcore_img, symbols_dir):
with open("tests/registry/appliances/microcore-linux.gns3a", encoding="utf-8") as f:
config = json.load(f)
config["images"] = [
{
"type": "hda_disk_image",
"filename": "linux-microcore-3.4.1.img",
"path": linux_microcore_img
}
]
config["symbol"] = "linux_guest.svg"
symbol_path = os.path.join(symbols_dir, "linux_guest.svg")
with patch("urllib.request.urlretrieve") as mock:
mock.side_effect = OSError
new_template = ApplianceToTemplate().new_template(config, "local")
mock.assert_called_with("https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/linux_guest.svg", symbol_path)
# In case of error we fallback on default symbol
assert new_template["symbol"] == ":/symbols/qemu_guest.svg"
def test_add_appliance_with_port_name_format(linux_microcore_img):
with open("tests/registry/appliances/microcore-linux.gns3a", encoding="utf-8") as f:
config = json.load(f)

View File

@@ -15,9 +15,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import pytest
import unittest.mock
import pathlib
import tempfile
from gns3.registry.image import Image
from gns3.image_upload_manager import ImageUploadManager
@@ -25,7 +27,8 @@ from gns3.image_upload_manager import ImageUploadManager
@pytest.fixture
def image():
return Image('QEMU', 'test.img')
(fd, path) = tempfile.mkstemp(suffix=".img")
return Image('QEMU', path)
@pytest.fixture
@@ -38,10 +41,12 @@ def callback():
def test_direct_file_upload(image, controller, callback):
manager = ImageUploadManager(image, controller, 'compute_id', callback, directFileUpload=True)
manager.upload()
controller.getEndpoint.assert_called_with(
'/QEMU/images/test.img',
'/QEMU/images/{}'.format(image.filename),
'compute_id',
manager._onLoadEndpointCallback,
showProgress=False
@@ -50,8 +55,8 @@ def test_direct_file_upload(image, controller, callback):
with unittest.mock.patch('gns3.image_upload_manager.HTTPClient') as client:
manager._onLoadEndpointCallback(dict(endpoint='/endpoint'))
client.fromUrl.return_value.createHTTPQuery.assert_called_with(
'POST', '/endpoint', manager._checkIfSuccessfulCallback, body=pathlib.Path('test.img'),
prefix="", progressText='Uploading test.img', timeout=None
'POST', '/endpoint', manager._checkIfSuccessfulCallback, body=pathlib.Path(image.path),
prefix="", context={'image_path': image.path}, progressText='Uploading {}'.format(image.filename), timeout=None
)
manager._checkIfSuccessfulCallback({})
@@ -62,11 +67,12 @@ def test_direct_file_upload_fallback_to_controller(image, controller, callback):
manager = ImageUploadManager(image, controller, callback, directFileUpload=True)
manager._checkIfSuccessfulCallback({}, error=True, connection_error=True)
controller.postCompute.assert_called_with(
'/QEMU/images/test.img',
'/QEMU/images/{}'.format(image.filename),
callback,
None,
body=pathlib.Path('test.img'),
progressText='Uploading test.img',
body=pathlib.Path(image.path),
context={'image_path': image.path},
progressText='Uploading {}'.format(image.filename),
timeout=None
)
@@ -75,10 +81,12 @@ def test_upload_via_controller(image, controller, callback):
manager = ImageUploadManager(image, controller, callback, directFileUpload=False)
manager.upload()
controller.postCompute.assert_called_with(
'/QEMU/images/test.img',
'/QEMU/images/{}'.format(image.filename),
callback,
None,
body=pathlib.Path('test.img'),
progressText='Uploading test.img',
body=pathlib.Path(image.path),
context={'image_path': image.path},
progressText='Uploading {}'.format(image.filename),
timeout=None
)
)