Skip to content

Commit

Permalink
Merge pull request #3490 from GNS3/appliance-v8-support
Browse files Browse the repository at this point in the history
Support for appliance version 8 format
  • Loading branch information
grossmj authored Aug 16, 2023
2 parents 2b78402 + 6d85504 commit 1a739c0
Show file tree
Hide file tree
Showing 26 changed files with 1,645 additions and 140 deletions.
103 changes: 68 additions & 35 deletions gns3/dialogs/appliance_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,11 @@ def __init__(self, parent, path):
self.setWindowTitle("Install {} appliance".format(self._appliance["name"]))

# add a custom button to show appliance information
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Appliance info")
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
self.customButtonClicked.connect(self._showApplianceInfoSlot)
if self._appliance["registry_version"] < 8:
# FIXME: show appliance info for v8
self.setButtonText(QtWidgets.QWizard.CustomButton1, "&Appliance info")
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
self.customButtonClicked.connect(self._showApplianceInfoSlot)

# customize the server selection
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
Expand Down Expand Up @@ -144,18 +146,9 @@ def initializePage(self, page_id):
if self.page(page_id) == self.uiServerWizardPage:

Controller.instance().getSymbols(self._getSymbolsCallback)

if "qemu" in self._appliance:
emulator_type = "qemu"
elif "iou" in self._appliance:
emulator_type = "iou"
elif "docker" in self._appliance:
emulator_type = "docker"
elif "dynamips" in self._appliance:
emulator_type = "dynamips"
else:
QtWidgets.QMessageBox.warning(self, "Appliance", "Could not determine the emulator type")

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")

Expand All @@ -173,11 +166,11 @@ def initializePage(self, page_id):
if ComputeManager.instance().localPlatform() is None:
self.uiLocalRadioButton.setEnabled(False)
elif is_mac or is_win:
if emulator_type == "qemu":
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 emulator_type != "dynamips":
elif template_type != "dynamips":
self.uiLocalRadioButton.setEnabled(False)

if ComputeManager.instance().vmCompute():
Expand All @@ -195,27 +188,55 @@ def initializePage(self, page_id):

elif self.page(page_id) == self.uiFilesWizardPage:
if Controller.instance().isRemote() or self._compute_id != "local":
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
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['qemu'].get('kvm', 'require') == 'require':
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
Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance["qemu"]["arch"]])
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:
self.uiUsageTextEdit.setText("The template will be available in the {} category.\n\n{}".format(self._appliance["category"].replace("_", " "), self._appliance.get("usage", "")))
# 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 error is None and "kvm" in result and self._appliance["qemu"]["arch"] in result["kvm"]:
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:
Expand All @@ -236,7 +257,7 @@ def _imageUploadedCallback(self, result, error=False, context=None, **kwargs):
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)
self._registry.getRemoteImageList(self._appliance.template_type(), self._compute_id)

def _showApplianceInfoSlot(self):
"""
Expand Down Expand Up @@ -407,7 +428,7 @@ def _refreshDialogWorker(self):

for version in self._appliance["versions"]:
for image in version["images"].values():
img = self._registry.search_image_file(self._appliance.emulator(),
img = self._registry.search_image_file(self._appliance.template_type(),
image["filename"],
image.get("md5sum"),
image.get("filesize"),
Expand Down Expand Up @@ -519,7 +540,7 @@ def _importPushButtonClickedSlot(self, *args):
if len(path) == 0:
return

image = Image(self._appliance.emulator(), path, filename=disk["filename"])
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",
Expand Down Expand Up @@ -554,7 +575,11 @@ def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs):
if self.uiQemuListComboBox.count() == 1:
self.next()
else:
i = self.uiQemuListComboBox.findData(self._appliance["qemu"]["arch"], flags=QtCore.Qt.MatchEndsWith)
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.MatchEndsWith)
if i != -1:
self.uiQemuListComboBox.setCurrentIndex(i)

Expand All @@ -567,8 +592,8 @@ def _install(self, version):

if version is None:
appliance_configuration = self._appliance.copy()
if "docker" not in appliance_configuration:
# only Docker do not have version
if self._appliance.template_type() != "docker":
# only Docker do not have versions
return False
else:
try:
Expand All @@ -585,10 +610,15 @@ def _install(self, version):
return False
appliance_configuration["name"] = appliance_configuration["name"].strip()

if "qemu" in appliance_configuration:
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, self._symbols, parent=self)
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

Expand Down Expand Up @@ -632,7 +662,7 @@ def _uploadImages(self, name, version):
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.emulator(), image["path"], filename=image["filename"])
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
Expand All @@ -649,12 +679,16 @@ def _applianceImageUploadedCallback(self, result, error=False, context=None, **k

def nextId(self):
if self.currentPage() == self.uiServerWizardPage:
if "docker" in self._appliance:
if self._appliance.template_type() == "docker":
# skip Qemu binary selection and files pages if this is a Docker appliance
return super().nextId() + 2
elif "qemu" not in self._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):
Expand Down Expand Up @@ -722,7 +756,6 @@ def validateCurrentPage(self):

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
Expand Down
99 changes: 81 additions & 18 deletions gns3/registry/appliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
import os
import collections.abc
import jsonschema
from gns3.utils.get_resource import get_resource


from gns3.utils.get_resource import get_resource
import logging
log = logging.getLogger(__name__)


class ApplianceError(Exception):
Expand All @@ -38,6 +40,7 @@ def __init__(self, registry, path):
:params path: Path of the appliance file on disk or file content
"""
self._registry = registry
self._registry_version = None

if os.path.isabs(path):
try:
Expand All @@ -58,16 +61,25 @@ def _check_config(self):
:param appliance: Sanity check on the appliance configuration
"""
if "registry_version" not in self._appliance:
raise ApplianceError("Invalid appliance configuration please report the issue on https://github.com/GNS3/gns3-registry")
if self._appliance["registry_version"] > 7:
raise ApplianceError("Please update GNS3 in order to install this appliance")
raise ApplianceError("Invalid appliance configuration please report the issue on https://github.com/GNS3/gns3-registry/issues")

self._registry_version = self._appliance["registry_version"]
if self._registry_version > 8:
# we only support registry version 8 and below
raise ApplianceError("Registry version {} is not supported in this version of GNS3".format(self._registry_version))

with open(get_resource("schemas/appliance.json")) as f:
if self._registry_version == 8:
# registry version 8 has a different schema with support for multiple settings sets
appliance_file = "appliance_v8.json"
else:
appliance_file = "appliance.json"

with open(get_resource("schemas/{}".format(appliance_file))) as f:
schema = json.load(f)
v = jsonschema.Draft4Validator(schema)
try:
v.validate(self._appliance)
except jsonschema.ValidationError as e:
except jsonschema.ValidationError:
error = jsonschema.exceptions.best_match(v.iter_errors(self._appliance)).message
raise ApplianceError("Invalid appliance file: {}".format(error))

Expand All @@ -82,10 +94,11 @@ def __len__(self):

def _resolve_version(self):
"""
Replace image field in versions by their the complete information from images
Replace image field in versions by the complete information from images
"""

if "versions" not in self._appliance:
log.debug("No versions found in appliance")
return

for version in self._appliance["versions"]:
Expand All @@ -96,7 +109,7 @@ def _resolve_version(self):
for file in self._appliance["images"]:
file = copy.copy(file)

if "idlepc" in version:
if self._registry_version < 8 and "idlepc" in version:
file["idlepc"] = version["idlepc"]

if "/" in filename:
Expand Down Expand Up @@ -127,7 +140,7 @@ def create_new_version(self, new_version):
def search_images_for_version(self, version_name):
"""
Search on disk the images required by this version.
And keep only the require images in the images fields. Add to the images
And keep only the required images in the images fields. Add to the images
their disk type and path.
:param version_name: Version name
Expand All @@ -142,10 +155,18 @@ def search_images_for_version(self, version_name):
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"))
checksum = image.get("md5sum")
if checksum is None and self._registry_version >= 8:
# registry version >= 8 has the checksum and checksum_type fields
checksum_type = image.get("checksum_type", "md5") # md5 is the default and only supported type
if checksum_type != "md5":
raise ApplianceError("Checksum type {} is not supported".format(checksum_type))
checksum = image.pop("checksum")

img = self._registry.search_image_file(self.template_type(), image["filename"], checksum, 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"]))
if checksum:
raise ApplianceError("File {} with checksum {} not found for {}".format(image["filename"], checksum, appliance["name"]))
else:
raise ApplianceError("File {} not found for {}".format(image["filename"], appliance["name"]))

Expand Down Expand Up @@ -186,9 +207,51 @@ def is_version_installable(self, version):
except ApplianceError:
return False

def emulator(self):
if "qemu" in self._appliance:
return "qemu"
if "iou" in self._appliance:
return "iou"
return "dynamips"
def template_type(self):

if self._registry_version >= 8:
template_type = None
for settings in self._appliance["settings"]:
if settings["template_type"] and not template_type:
template_type = settings["template_type"]
elif settings["template_type"] and template_type != settings["template_type"]:
# we are currently not supporting multiple different template types in the same appliance
raise ApplianceError("Multiple different template types found in appliance")
if not template_type:
raise ApplianceError("No template type found in appliance {}".format(self._appliance["name"]))
return template_type
else:
if "qemu" in self._appliance:
return "qemu"
if "iou" in self._appliance:
return "iou"
if "dynamips" in self._appliance:
return "dynamips"
if "docker" in self._appliance:
return "docker"
return None

def template_properties(self):
"""
Get template properties
"""

if self._registry_version >= 8:
# find the default settings if any
for settings in self._appliance["settings"]:
if settings.get("default", False):
return settings["template_properties"]
# otherwise take the first settings we find
for settings in self._appliance["settings"]:
if settings["template_type"]:
return settings["template_properties"]
else:
if "qemu" in self._appliance:
return self._appliance["qemu"]
if "iou" in self._appliance:
return self._appliance["iou"]
if "dynamips" in self._appliance:
return self._appliance["dynamips"]
if "docker" in self._appliance:
return self._appliance["docker"]
return None
Loading

0 comments on commit 1a739c0

Please sign in to comment.