mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-05-17 00:46:01 +03:00
829 lines
38 KiB
Python
829 lines
38 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# Copyright (C) 2015 GNS3 Technologies Inc.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import os
|
|
from ..qt import sip
|
|
import shutil
|
|
|
|
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
|
|
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
|
|
from ..template_manager import TemplateManager
|
|
from ..template import Template
|
|
from ..modules import Qemu
|
|
from ..registry.appliance import Appliance, ApplianceError
|
|
from ..registry.registry import Registry
|
|
from ..registry.config import Config
|
|
from ..registry.appliance_to_template import ApplianceToTemplate
|
|
from ..registry.image import Image
|
|
from ..utils import human_filesize
|
|
from ..utils.wait_for_lambda_worker import WaitForLambdaWorker
|
|
from ..utils.progress_dialog import ProgressDialog
|
|
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__)
|
|
|
|
|
|
class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
|
|
|
images_changed_signal = QtCore.Signal()
|
|
versions_changed_signal = QtCore.Signal()
|
|
|
|
def __init__(self, parent, path):
|
|
super().__init__(parent)
|
|
|
|
self.setupUi(self)
|
|
self._refreshing = False
|
|
self._server_check = False
|
|
self._template_created = False
|
|
self._path = path
|
|
|
|
# count how many images are being uploaded
|
|
self._image_uploading_count = 0
|
|
|
|
# symbols loaded from controller
|
|
self._symbols = []
|
|
|
|
# connect slots
|
|
self.images_changed_signal.connect(self._refreshVersions)
|
|
self.versions_changed_signal.connect(self._versionRefreshedSlot)
|
|
self.uiRefreshPushButton.clicked.connect(self.images_changed_signal.emit)
|
|
self.uiDownloadPushButton.clicked.connect(self._downloadPushButtonClickedSlot)
|
|
self.uiImportPushButton.clicked.connect(self._importPushButtonClickedSlot)
|
|
self.uiApplianceVersionTreeWidget.currentItemChanged.connect(self._applianceVersionCurrentItemChangedSlot)
|
|
self.uiCreateVersionPushButton.clicked.connect(self._createVersionPushButtonClickedSlot)
|
|
self.allowCustomFiles.clicked.connect(self._allowCustomFilesChangedSlot)
|
|
|
|
# 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.StandardLocation.DownloadLocation)
|
|
if download_directory != "" and download_directory != os.path.dirname(self._path):
|
|
images_directories.append(download_directory)
|
|
|
|
# registry to search for images
|
|
self._registry = Registry(images_directories)
|
|
self._registry.image_list_changed_signal.connect(self.images_changed_signal.emit)
|
|
|
|
# appliance object
|
|
self._appliance = Appliance(self._registry, self._path)
|
|
self.setWindowTitle("Install {} appliance".format(self._appliance["name"]))
|
|
|
|
# add a custom button to show appliance information
|
|
if self._appliance["registry_version"] < 8:
|
|
# FIXME: show appliance info for v8
|
|
self.setButtonText(QtWidgets.QWizard.WizardButton.CustomButton1, "&Appliance info")
|
|
self.setOption(QtWidgets.QWizard.WizardOption.HaveCustomButton1, True)
|
|
self.customButtonClicked.connect(self._showApplianceInfoSlot)
|
|
|
|
# customize the server selection
|
|
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
|
if hasattr(self, "uiVMRadioButton"):
|
|
self.uiVMRadioButton.toggled.connect(self._vmToggledSlot)
|
|
|
|
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
|
if Controller.instance().isRemote():
|
|
self.uiLocalRadioButton.setText("Install the appliance on the main server")
|
|
else:
|
|
if not path.endswith('.builtin.gns3a'):
|
|
destination = None
|
|
try:
|
|
destination = Config().appliances_dir
|
|
except OSError as e:
|
|
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", "Could not find configuration file: {}".format(e))
|
|
except ValueError as e:
|
|
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", "Invalid configuration file: {}".format(e))
|
|
if destination:
|
|
try:
|
|
os.makedirs(destination, exist_ok=True)
|
|
destination = os.path.join(destination, os.path.basename(path))
|
|
shutil.copy(path, destination)
|
|
except OSError as e:
|
|
QtWidgets.QMessageBox.warning(self.parent(), "Cannot copy {} to {}".format(path, destination), str(e))
|
|
|
|
self.uiServerWizardPage.isComplete = self._uiServerWizardPage_isComplete
|
|
|
|
def initializePage(self, page_id):
|
|
"""
|
|
Initialize wizard pages.
|
|
|
|
:param page_id: page identifier
|
|
"""
|
|
|
|
super().initializePage(page_id)
|
|
|
|
# add symbol
|
|
if self._appliance["category"] == "guest":
|
|
symbol = ":/symbols/computer.svg"
|
|
else:
|
|
symbol = ":/symbols/{}.svg".format(self._appliance["category"])
|
|
self.page(page_id).setPixmap(QtWidgets.QWizard.WizardPixmap.LogoPixmap, QtGui.QPixmap(symbol))
|
|
|
|
if self.page(page_id) == self.uiServerWizardPage:
|
|
|
|
Controller.instance().getSymbols(self._getSymbolsCallback)
|
|
template_type = self._appliance.template_type()
|
|
if not template_type:
|
|
raise ApplianceError("No template type found for appliance {}".format(self._appliance["name"]))
|
|
is_mac = ComputeManager.instance().localPlatform().startswith("darwin")
|
|
is_win = ComputeManager.instance().localPlatform().startswith("win")
|
|
|
|
self.uiRemoteServersComboBox.clear()
|
|
if len(ComputeManager.instance().remoteComputes()) == 0:
|
|
self.uiRemoteRadioButton.setEnabled(False)
|
|
else:
|
|
self.uiRemoteRadioButton.setEnabled(True)
|
|
for compute in ComputeManager.instance().remoteComputes():
|
|
self.uiRemoteServersComboBox.addItem(compute.name(), compute)
|
|
|
|
if not ComputeManager.instance().vmCompute():
|
|
self.uiVMRadioButton.setEnabled(False)
|
|
|
|
if ComputeManager.instance().localPlatform() is None:
|
|
self.uiLocalRadioButton.setEnabled(False)
|
|
elif is_mac or is_win:
|
|
if template_type == "qemu":
|
|
# disallow usage of the local server because Qemu has issues on OSX and Windows
|
|
if not LocalConfig.instance().experimental():
|
|
self.uiLocalRadioButton.setEnabled(False)
|
|
elif template_type != "dynamips":
|
|
self.uiLocalRadioButton.setEnabled(False)
|
|
|
|
if ComputeManager.instance().vmCompute():
|
|
self.uiVMRadioButton.setChecked(True)
|
|
elif ComputeManager.instance().localCompute() and self.uiLocalRadioButton.isEnabled():
|
|
self.uiLocalRadioButton.setChecked(True)
|
|
elif self.uiRemoteRadioButton.isEnabled():
|
|
self.uiRemoteRadioButton.setChecked(True)
|
|
else:
|
|
self.uiRemoteRadioButton.setChecked(False)
|
|
|
|
if is_mac or is_win:
|
|
if not self.uiRemoteRadioButton.isEnabled() and not self.uiVMRadioButton.isEnabled() and not self.uiLocalRadioButton.isEnabled():
|
|
QtWidgets.QMessageBox.warning(self, "GNS3 VM", "The GNS3 VM is not available, please configure the GNS3 VM before adding a new appliance.")
|
|
|
|
elif self.page(page_id) == self.uiFilesWizardPage:
|
|
if Controller.instance().isRemote() or self._compute_id != "local":
|
|
self._registry.getRemoteImageList(self._appliance.template_type(), self._compute_id)
|
|
else:
|
|
self.images_changed_signal.emit()
|
|
|
|
elif self.page(page_id) == self.uiQemuWizardPage:
|
|
if self._appliance.template_properties().get('kvm', 'require') == 'require':
|
|
self._server_check = False
|
|
Qemu.instance().getQemuCapabilitiesFromServer(self._compute_id, qpartial(self._qemuServerCapabilitiesCallback))
|
|
else:
|
|
self._server_check = True
|
|
if self._appliance["registry_version"] >= 8:
|
|
qemu_platform = self._appliance.template_properties()["platform"]
|
|
else:
|
|
qemu_platform = self._appliance.template_properties()["arch"]
|
|
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [qemu_platform])
|
|
|
|
elif self.page(page_id) == self.uiInstructionsPage:
|
|
|
|
installation_instructions = self._appliance.get("installation_instructions", "No installation instructions available")
|
|
self.uiInstructionsTextEdit.setText(installation_instructions.strip())
|
|
|
|
elif self.page(page_id) == self.uiUsageWizardPage:
|
|
# TODO: allow taking these info fields at the version level in v8
|
|
category = self._appliance["category"].replace("_", " ")
|
|
usage = self._appliance.get("usage", "No usage information available")
|
|
if self._appliance["registry_version"] >= 8:
|
|
default_username = self._appliance.get("default_username")
|
|
default_password = self._appliance.get("default_password")
|
|
if default_username and default_password:
|
|
usage += "\n\nDefault username: {}\nDefault password: {}".format(default_username, default_password)
|
|
|
|
usage_info = """
|
|
The template will be available in the {} category.
|
|
|
|
Usage: {}
|
|
""".format(category, usage)
|
|
|
|
self.uiUsageTextEdit.setText(usage_info.strip())
|
|
|
|
def _qemuServerCapabilitiesCallback(self, result, error=None, *args, **kwargs):
|
|
"""
|
|
Check if the server supports KVM or not
|
|
"""
|
|
|
|
if self._appliance["registry_version"] >= 8:
|
|
qemu_platform = self._appliance.template_properties()["platform"]
|
|
else:
|
|
qemu_platform = self._appliance.template_properties()["arch"]
|
|
if error is None and "kvm" in result and qemu_platform in result["kvm"]:
|
|
self._server_check = True
|
|
else:
|
|
if error:
|
|
msg = result["message"]
|
|
else:
|
|
msg = "The selected server does not support KVM. A Linux server or the GNS3 VM running in VMware is required."
|
|
QtWidgets.QMessageBox.critical(self, "KVM support", msg)
|
|
self._server_check = False
|
|
|
|
def _uiServerWizardPage_isComplete(self):
|
|
return self.uiRemoteRadioButton.isEnabled() or self.uiVMRadioButton.isEnabled() or self.uiLocalRadioButton.isEnabled()
|
|
|
|
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.template_type(), self._compute_id)
|
|
|
|
def _showApplianceInfoSlot(self):
|
|
"""
|
|
Shows appliance information.
|
|
"""
|
|
|
|
info = (("Product", "product_name"),
|
|
("Vendor", "vendor_name"),
|
|
("Availability", "availability"),
|
|
("Status", "status"),
|
|
("Maintainer", "maintainer"))
|
|
|
|
if "qemu" in self._appliance:
|
|
qemu_info = (("vCPUs", "qemu/cpus"),
|
|
("RAM", "qemu/ram"),
|
|
("Adapters", "qemu/adapters"),
|
|
("Adapter type", "qemu/adapter_type"),
|
|
("Console type", "qemu/console_type"),
|
|
("Architecture", "qemu/arch"),
|
|
("Console type", "qemu/console_type"),
|
|
("KVM", "qemu/kvm"))
|
|
info = info + qemu_info
|
|
|
|
elif "docker" in self._appliance:
|
|
docker_info = (("Image", "docker/image"),
|
|
("Adapters", "docker/adapters"),
|
|
("Console type", "docker/console_type"))
|
|
info = info + docker_info
|
|
|
|
elif "iou" in self._appliance:
|
|
iou_info = (("RAM", "iou/ram"),
|
|
("NVRAM", "iou/nvram"),
|
|
("Ethernet adapters", "iou/ethernet_adapters"),
|
|
("Serial adapters", "iou/serial_adapters"))
|
|
info = info + iou_info
|
|
|
|
elif "dynamips" in self._appliance:
|
|
dynamips_info = (("Platform", "dynamips/platform"),
|
|
("Chassis", "dynamips/chassis"),
|
|
("Midplane", "dynamips/midplane"),
|
|
("NPE", "dynamips/npe"),
|
|
("RAM", "dynamips/ram"),
|
|
("NVRAM", "dynamips/nvram"),
|
|
("slot0", "dynamips/slot0"),
|
|
("slot1", "dynamips/slot1"),
|
|
("slot2", "dynamips/slot2"),
|
|
("slot3", "dynamips/slot3"),
|
|
("slot4", "dynamips/slot4"),
|
|
("slot5", "dynamips/slot5"),
|
|
("slot6", "dynamips/slot6"),
|
|
("wic0", "dynamips/wic0"),
|
|
("wic1", "dynamips/wic1"),
|
|
("wic2", "dynamips/wic2"))
|
|
info = info + dynamips_info
|
|
|
|
text_info = ""
|
|
for (name, key) in info:
|
|
if "/" in key:
|
|
key, subkey = key.split("/")
|
|
value = self._appliance.get(key, {}).get(subkey, None)
|
|
else:
|
|
value = self._appliance.get(key, None)
|
|
if value is None:
|
|
continue
|
|
text_info += "<span style='font-weight:bold;'>{}</span>: {}<br>".format(name, value)
|
|
|
|
msgbox = QtWidgets.QMessageBox(self)
|
|
msgbox.setWindowTitle("Appliance information")
|
|
msgbox.setStyleSheet("QLabel{min-width: 600px;}") # TODO: resize details box QTextEdit{min-height: 500px;}
|
|
msgbox.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
|
|
msgbox.setText(text_info)
|
|
msgbox.setDetailedText(self._appliance["description"])
|
|
msgbox.exec()
|
|
|
|
@qslot
|
|
def _refreshVersions(self, *args):
|
|
"""
|
|
Refresh the list of files for different versions of the appliance
|
|
"""
|
|
|
|
if self._refreshing:
|
|
return
|
|
self._refreshing = True
|
|
|
|
self.uiFilesWizardPage.setSubTitle("Please select one version of " + self._appliance["product_name"] + " and import the required files. Files are searched in your downloads and GNS3 images directories by default")
|
|
worker = WaitForLambdaWorker(lambda: self._refreshDialogWorker())
|
|
progress_dialog = ProgressDialog(worker, "Add appliance", "Scanning directories for files...", None, busy=True, parent=self)
|
|
progress_dialog.show()
|
|
|
|
@qslot
|
|
def _versionRefreshedSlot(self, *args):
|
|
"""
|
|
Called when we finish to scan the disk for new versions
|
|
"""
|
|
|
|
if self._refreshing or self.currentPage() != self.uiFilesWizardPage:
|
|
return
|
|
self._refreshing = True
|
|
self.uiApplianceVersionTreeWidget.clear()
|
|
|
|
for version in self._appliance["versions"]:
|
|
top = QtWidgets.QTreeWidgetItem(self.uiApplianceVersionTreeWidget, ["{} version {}".format(self._appliance["product_name"], version["name"])])
|
|
size = 0
|
|
status = "Ready to install"
|
|
for image in version["images"].values():
|
|
if image["status"] == "Missing":
|
|
status = "Missing files"
|
|
|
|
size += image.get("filesize", 0)
|
|
image_widget = QtWidgets.QTreeWidgetItem([image["filename"],
|
|
human_filesize(image.get("filesize", 0)),
|
|
image["status"]])
|
|
if image["status"] == "Missing":
|
|
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("red")))
|
|
else:
|
|
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
|
|
image_widget.setToolTip(2, image["path"])
|
|
|
|
# Associated data stored are col 0: version, col 1: image
|
|
image_widget.setData(0, QtCore.Qt.ItemDataRole.UserRole, version)
|
|
image_widget.setData(1, QtCore.Qt.ItemDataRole.UserRole, image)
|
|
image_widget.setData(2, QtCore.Qt.ItemDataRole.UserRole, self._appliance)
|
|
top.addChild(image_widget)
|
|
|
|
font = top.font(0)
|
|
font.setBold(True)
|
|
top.setFont(0, font)
|
|
|
|
expand = True
|
|
if status == "Missing files":
|
|
top.setForeground(2, QtGui.QBrush(QtGui.QColor("red")))
|
|
else:
|
|
expand = False
|
|
top.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
|
|
|
|
top.setData(1, QtCore.Qt.ItemDataRole.DisplayRole, human_filesize(size))
|
|
top.setData(2, QtCore.Qt.ItemDataRole.DisplayRole, status)
|
|
top.setData(0, QtCore.Qt.ItemDataRole.UserRole, version)
|
|
top.setData(2, QtCore.Qt.ItemDataRole.UserRole, self._appliance)
|
|
self.uiApplianceVersionTreeWidget.addTopLevelItem(top)
|
|
if expand:
|
|
top.setExpanded(True)
|
|
|
|
if len(self._appliance["versions"]) > 0:
|
|
for column in range(self.uiApplianceVersionTreeWidget.columnCount()):
|
|
self.uiApplianceVersionTreeWidget.resizeColumnToContents(column)
|
|
|
|
self._refreshing = False
|
|
|
|
def _getSymbolsCallback(self, result, error=False, **kwargs):
|
|
"""
|
|
Callback to retrieve the appliance symbols.
|
|
"""
|
|
|
|
if error:
|
|
log.warning("Cannot load symbols from controller")
|
|
else:
|
|
self._symbols = result
|
|
|
|
def _refreshDialogWorker(self):
|
|
"""
|
|
Scan local directory in order to find images on the disk
|
|
"""
|
|
|
|
# Docker do not have versions
|
|
if "versions" not in self._appliance:
|
|
return
|
|
|
|
for version in self._appliance["versions"]:
|
|
for image in version["images"].values():
|
|
img = self._registry.search_image_file(self._appliance.template_type(),
|
|
image["filename"],
|
|
image.get("md5sum"),
|
|
image.get("filesize"),
|
|
strict_md5_check=not self.allowCustomFiles.isChecked())
|
|
if img:
|
|
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
|
|
self.versions_changed_signal.emit()
|
|
|
|
@qslot
|
|
def _applianceVersionCurrentItemChangedSlot(self, current, previous):
|
|
"""
|
|
Called when user select a different item in the list of appliance files
|
|
"""
|
|
|
|
self.uiDownloadPushButton.hide()
|
|
self.uiImportPushButton.hide()
|
|
|
|
if current is None or sip.isdeleted(current):
|
|
return
|
|
|
|
image = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
|
|
if image is not None:
|
|
if "direct_download_url" in image or "download_url" in image:
|
|
self.uiDownloadPushButton.show()
|
|
self.uiImportPushButton.show()
|
|
|
|
@qslot
|
|
def _downloadPushButtonClickedSlot(self, *args):
|
|
"""
|
|
Called when user wants to download an appliance image.
|
|
The file should be selected first.
|
|
"""
|
|
|
|
if self._refreshing:
|
|
return False
|
|
|
|
current = self.uiApplianceVersionTreeWidget.currentItem()
|
|
|
|
if current is None or sip.isdeleted(current):
|
|
return
|
|
|
|
data = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
|
|
if data is not None:
|
|
if "direct_download_url" in data:
|
|
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["direct_download_url"]))
|
|
if "compression" in data:
|
|
QtWidgets.QMessageBox.warning(self, "Add appliance", "The file is compressed with '{}', it must be uncompressed first".format(data["compression"]))
|
|
else:
|
|
QtWidgets.QMessageBox.warning(self, "Add appliance", "Download will redirect you where the required file can be downloaded, you may have to be registered with the vendor in order to download the file.")
|
|
QtGui.QDesktopServices.openUrl(QtCore.QUrl(data["download_url"]))
|
|
|
|
@qslot
|
|
def _createVersionPushButtonClickedSlot(self, *args):
|
|
"""
|
|
Allow user to create a new version of an appliance
|
|
"""
|
|
|
|
current = self.uiApplianceVersionTreeWidget.currentItem()
|
|
if current is None:
|
|
QtWidgets.QMessageBox.critical(self.parent(), "Base version", "Please select a base version")
|
|
return
|
|
base_version = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
|
|
|
new_version_name, ok = QtWidgets.QInputDialog.getText(self, "Creating a new version", "Create a new version for this appliance.\nPlease share your experience on the GNS3 community if this version works.\n\nVersion name:", QtWidgets.QLineEdit.EchoMode.Normal, base_version.get("name"))
|
|
if ok:
|
|
new_version = {"name": new_version_name}
|
|
new_version["images"] = {}
|
|
|
|
for disk_type in base_version["images"]:
|
|
base_filename = base_version["images"][disk_type]["filename"]
|
|
filename, ok = QtWidgets.QInputDialog.getText(self, "Image", "Disk image filename for {}".format(disk_type), QtWidgets.QLineEdit.EchoMode.Normal, base_filename)
|
|
if not ok:
|
|
filename = base_filename
|
|
new_version["images"][disk_type] = {"filename": filename, "version": new_version_name}
|
|
|
|
try:
|
|
self._appliance.create_new_version(new_version)
|
|
except ApplianceError as e:
|
|
QtWidgets.QMessageBox.critical(self.parent(), "Create new version", str(e))
|
|
return
|
|
self.images_changed_signal.emit()
|
|
|
|
@qslot
|
|
def _importPushButtonClickedSlot(self, *args):
|
|
"""
|
|
Called when user wants to import an appliance images.
|
|
The file should be selected first.
|
|
"""
|
|
|
|
if self._refreshing:
|
|
return False
|
|
|
|
current = self.uiApplianceVersionTreeWidget.currentItem()
|
|
if not current:
|
|
return
|
|
disk = current.data(1, QtCore.Qt.ItemDataRole.UserRole)
|
|
|
|
path, _ = QtWidgets.QFileDialog.getOpenFileName()
|
|
if len(path) == 0:
|
|
return
|
|
|
|
image = Image(self._appliance.template_type(), path, filename=disk["filename"])
|
|
try:
|
|
if "md5sum" in disk and image.md5sum != disk["md5sum"]:
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
"Add appliance",
|
|
"This is not the correct file.\n\n"
|
|
"MD5 checksum\n"
|
|
f"actual:\t{image.md5sum}\n"
|
|
f"expected:\t{disk['md5sum']}\n\n"
|
|
"File size\n"
|
|
f"actual:\t{image.filesize} bytes\n"
|
|
f"expected:\t{disk['filesize']} bytes\n\n"
|
|
"Do you want to accept it at your own risks?",
|
|
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
|
|
)
|
|
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
|
return
|
|
except OSError as e:
|
|
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "Can't access to the image file {}: {}.".format(path, str(e)))
|
|
return
|
|
|
|
image_upload_manger = ImageUploadManager(image, Controller.instance(), self._compute_id, self._imageUploadedCallback, LocalConfig.instance().directFileUpload())
|
|
image_upload_manger.upload()
|
|
|
|
def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
|
|
"""
|
|
Callback for getQemuBinariesFromServer.
|
|
|
|
:param result: server response
|
|
:param error: indicates an error (boolean)
|
|
"""
|
|
|
|
if error:
|
|
QtWidgets.QMessageBox.critical(self, "Qemu binaries", "{}".format(result["message"]))
|
|
else:
|
|
self.uiQemuListComboBox.clear()
|
|
for qemu in result:
|
|
if qemu["version"]:
|
|
self.uiQemuListComboBox.addItem("{path} (v{version})".format(path=qemu["path"], version=qemu["version"]), qemu["path"])
|
|
else:
|
|
self.uiQemuListComboBox.addItem("{path}".format(path=qemu["path"]), qemu["path"])
|
|
if self.uiQemuListComboBox.count() == 1:
|
|
self.next()
|
|
else:
|
|
if self._appliance["registry_version"] >= 8:
|
|
qemu_platform = self._appliance.template_properties()["platform"]
|
|
else:
|
|
qemu_platform = self._appliance.template_properties()["arch"]
|
|
i = self.uiQemuListComboBox.findData(qemu_platform, flags=QtCore.Qt.MatchFlag.MatchEndsWith)
|
|
if i != -1:
|
|
self.uiQemuListComboBox.setCurrentIndex(i)
|
|
|
|
def _install(self, version):
|
|
"""
|
|
Install the appliance in GNS3
|
|
|
|
:params version: appliance version name
|
|
"""
|
|
|
|
if version is None:
|
|
appliance_configuration = self._appliance.copy()
|
|
if self._appliance.template_type() != "docker":
|
|
# only Docker do not have versions
|
|
return False
|
|
else:
|
|
try:
|
|
appliance_configuration = self._appliance.search_images_for_version(version)
|
|
except ApplianceError as e:
|
|
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
|
return False
|
|
|
|
template_manager = TemplateManager().instance()
|
|
while len(appliance_configuration["name"]) == 0 or not template_manager.is_name_available(appliance_configuration["name"]):
|
|
QtWidgets.QMessageBox.warning(self.parent(), "Add template", "The name \"{}\" is already used by another template".format(appliance_configuration["name"]))
|
|
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add template", "New name:", QtWidgets.QLineEdit.EchoMode.Normal, appliance_configuration["name"])
|
|
if not ok:
|
|
return False
|
|
appliance_configuration["name"] = appliance_configuration["name"].strip()
|
|
|
|
if self._appliance["registry_version"] >= 8:
|
|
if "settings" in appliance_configuration:
|
|
for settings in appliance_configuration["settings"]:
|
|
if settings["template_type"] == "qemu":
|
|
settings["template_properties"]["path"] = self.uiQemuListComboBox.currentData()
|
|
elif "qemu" in appliance_configuration:
|
|
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
|
|
|
|
new_template = ApplianceToTemplate().new_template(appliance_configuration, self._compute_id, version, self._symbols, parent=self)
|
|
TemplateManager.instance().createTemplate(Template(new_template), callback=self._templateCreatedCallback)
|
|
return False
|
|
|
|
#worker = WaitForLambdaWorker(lambda: self._create_template(appliance_configuration, self._compute_id), allowed_exceptions=[ConfigException, OSError])
|
|
#progress_dialog = ProgressDialog(worker, "Add template", "Installing a new template...", None, busy=True, parent=self)
|
|
#progress_dialog.show()
|
|
#if progress_dialog.exec():
|
|
# QtWidgets.QMessageBox.information(self.parent(), "Add template", "{} template has been installed!".format(appliance_configuration["name"]))
|
|
# return True
|
|
#return False
|
|
|
|
# worker = WaitForLambdaWorker(lambda: config.save(), allowed_exceptions=[ConfigException, OSError])
|
|
# progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
|
|
# progress_dialog.show()
|
|
# if progress_dialog.exec():
|
|
# QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
|
|
# return True
|
|
|
|
def _templateCreatedCallback(self, result, error=False, **kwargs):
|
|
|
|
if error is True:
|
|
QtWidgets.QMessageBox.critical(self.parent(), "Add template", "The template cannot be created: {}".format(result.get("message", "unknown")))
|
|
return
|
|
|
|
QtWidgets.QMessageBox.information(self.parent(), "Add template", "The appliance has been installed and a template named '{}' has been successfully created!".format(result["name"]))
|
|
self._template_created = True
|
|
self.done(True)
|
|
|
|
def _uploadImages(self, name, version):
|
|
"""
|
|
Upload an image the compute.
|
|
"""
|
|
|
|
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 not Controller.instance().isRemote() and self._compute_id == "local" and image["path"].startswith(ImageManager.instance().getDirectory()):
|
|
log.debug("{} is already on the local server".format(image["path"]))
|
|
return
|
|
image = Image(self._appliance.template_type(), image["path"], filename=image["filename"])
|
|
image_upload_manager = ImageUploadManager(image, Controller.instance(), self._compute_id, self._applianceImageUploadedCallback, LocalConfig.instance().directFileUpload())
|
|
image_upload_manager.upload()
|
|
self._image_uploading_count += 1
|
|
|
|
def _applianceImageUploadedCallback(self, result, error=False, context=None, **kwargs):
|
|
if context is None:
|
|
context = {}
|
|
image_path = context.get("image_path", "unknown")
|
|
if error:
|
|
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
|
|
else:
|
|
log.info("Image '{}' has been successfully uploaded".format(image_path))
|
|
self._image_uploading_count -= 1
|
|
|
|
def nextId(self):
|
|
if self.currentPage() == self.uiServerWizardPage:
|
|
if self._appliance.template_type() == "docker":
|
|
# skip Qemu binary selection and files pages if this is a Docker appliance
|
|
return super().nextId() + 3
|
|
elif self._appliance.template_type() != "qemu":
|
|
# skip the Qemu binary selection page if not a Qemu appliance
|
|
return super().nextId() + 1
|
|
if self.currentPage() == self.uiQemuWizardPage:
|
|
if not self._appliance.get("installation_instructions"):
|
|
# skip the installation instructions page if there are no instructions
|
|
return super().nextId() + 1
|
|
return super().nextId()
|
|
|
|
def validateCurrentPage(self):
|
|
"""
|
|
Validates the settings.
|
|
"""
|
|
|
|
if self.currentPage() == self.uiFilesWizardPage:
|
|
# validate the files page
|
|
|
|
if self._refreshing:
|
|
return False
|
|
current = self.uiApplianceVersionTreeWidget.currentItem()
|
|
if current is None or sip.isdeleted(current):
|
|
return False
|
|
version = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
|
if version is None:
|
|
return False
|
|
appliance = current.data(2, QtCore.Qt.ItemDataRole.UserRole)
|
|
try:
|
|
self._appliance.search_images_for_version(version["name"])
|
|
except ApplianceError as e:
|
|
QtWidgets.QMessageBox.critical(self, "Appliance", "Cannot install {} version {}: {}".format(appliance["name"], version["name"], e))
|
|
return False
|
|
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Would you like to install {} version {}?".format(appliance["name"], version["name"]),
|
|
QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
|
|
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
|
return False
|
|
|
|
self._uploadImages(appliance["name"], version["name"])
|
|
|
|
elif self.currentPage() == self.uiUsageWizardPage:
|
|
# validate the usage page
|
|
|
|
if self._template_created:
|
|
return True
|
|
if self._image_uploading_count > 0:
|
|
QtWidgets.QMessageBox.critical(self, "Add appliance", "Please wait for appliance files to be uploaded")
|
|
return False
|
|
current = self.uiApplianceVersionTreeWidget.currentItem()
|
|
if current:
|
|
version = current.data(0, QtCore.Qt.ItemDataRole.UserRole)
|
|
return self._install(version["name"])
|
|
else:
|
|
return self._install(None)
|
|
|
|
elif self.currentPage() == self.uiServerWizardPage:
|
|
# validate the server page
|
|
|
|
if self.uiRemoteRadioButton.isChecked():
|
|
if len(ComputeManager.instance().remoteComputes()) == 0:
|
|
QtWidgets.QMessageBox.critical(self, "Remote server", "There is no remote servers configured in your preferences")
|
|
return False
|
|
self._compute_id = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex()).id()
|
|
elif hasattr(self, "uiVMRadioButton") and self.uiVMRadioButton.isChecked():
|
|
self._compute_id = "vm"
|
|
else:
|
|
if ComputeManager.instance().localPlatform():
|
|
if (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
|
|
if "qemu" in self._appliance:
|
|
reply = QtWidgets.QMessageBox.question(self, "Appliance", "Qemu on Windows and macOS is not supported by the GNS3 team. Do you want to continue?", QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
|
|
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
|
return False
|
|
self._compute_id = "local"
|
|
|
|
elif self.currentPage() == self.uiQemuWizardPage:
|
|
# validate the Qemu
|
|
if self._server_check is False:
|
|
QtWidgets.QMessageBox.critical(self, "Checking for KVM support", "Please wait for the server to reply...")
|
|
return False
|
|
if self.uiQemuListComboBox.currentIndex() == -1:
|
|
QtWidgets.QMessageBox.critical(self, "Qemu binary", "No compatible Qemu binary selected")
|
|
return False
|
|
return True
|
|
|
|
@qslot
|
|
def _vmToggledSlot(self, checked):
|
|
"""
|
|
Slot for when the VM radio button is toggled.
|
|
|
|
:param checked: either the button is checked or not
|
|
"""
|
|
|
|
if checked:
|
|
self.uiRemoteServersGroupBox.setEnabled(False)
|
|
self.uiRemoteServersGroupBox.hide()
|
|
|
|
@qslot
|
|
def _remoteServerToggledSlot(self, checked):
|
|
"""
|
|
Slot for when the remote server radio button is toggled.
|
|
|
|
:param checked: either the button is checked or not
|
|
"""
|
|
|
|
if checked:
|
|
self.uiRemoteServersGroupBox.setEnabled(True)
|
|
self.uiRemoteServersGroupBox.show()
|
|
|
|
@qslot
|
|
def _localToggledSlot(self, checked):
|
|
"""
|
|
Slot for when the local server radio button is toggled.
|
|
|
|
:param checked: either the button is checked or not
|
|
"""
|
|
if checked:
|
|
self.uiRemoteServersGroupBox.setEnabled(False)
|
|
self.uiRemoteServersGroupBox.hide()
|
|
|
|
@qslot
|
|
def _allowCustomFilesChangedSlot(self, checked):
|
|
"""
|
|
Slot for when user want to upload images which don't match md5
|
|
|
|
:param checked: if allows or doesn't allow custom files
|
|
:return:
|
|
"""
|
|
if checked:
|
|
reply = QtWidgets.QMessageBox.question(self, "Custom files",
|
|
"This option allows files with different MD5 checksums. This feature is only for advanced users and can lead "
|
|
"to unexpected problems. Do you want to proceed?",
|
|
QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No)
|
|
|
|
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
|
self.allowCustomFiles.setChecked(False)
|
|
return False
|