From b72358461ccf82dd5feae5f93d3c48cab6be3f34 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 25 Jun 2023 16:54:12 +0930 Subject: [PATCH 01/17] Add support for appliance version 8 format --- gns3/dialogs/appliance_wizard.py | 39 +- gns3/registry/appliance.py | 95 ++- gns3/registry/appliance_to_template.py | 86 ++- gns3/schemas/appliance.json | 2 +- gns3/schemas/appliance_v8.json | 851 +++++++++++++++++++++++++ 5 files changed, 1006 insertions(+), 67 deletions(-) create mode 100644 gns3/schemas/appliance_v8.json diff --git a/gns3/dialogs/appliance_wizard.py b/gns3/dialogs/appliance_wizard.py index ec15d7194..7f9d72cf7 100644 --- a/gns3/dialogs/appliance_wizard.py +++ b/gns3/dialogs/appliance_wizard.py @@ -96,7 +96,7 @@ def __init__(self, parent, path): # 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) + #self.customButtonClicked.connect(self._showApplianceInfoSlot) # customize the server selection self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot) @@ -144,18 +144,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") - + emulator_type = self._appliance.emulator() + if not emulator_type: + raise ApplianceError("No emulator type found for appliance {}".format(self._appliance["name"])) is_mac = ComputeManager.instance().localPlatform().startswith("darwin") is_win = ComputeManager.instance().localPlatform().startswith("win") @@ -200,12 +191,12 @@ def initializePage(self, page_id): self.images_changed_signal.emit() elif self.page(page_id) == self.uiQemuWizardPage: - if self._appliance['qemu'].get('kvm', 'require') == 'require': + if self._appliance.emulator_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"]]) + Qemu.instance().getQemuBinariesFromServer(self._compute_id, qpartial(self._getQemuBinariesFromServerCallback), [self._appliance.emulator_properties()["arch"]]) 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", ""))) @@ -215,7 +206,7 @@ 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 error is None and "kvm" in result and self._appliance.emulator_properties()["arch"] in result["kvm"]: self._server_check = True else: if error: @@ -554,7 +545,7 @@ 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) + i = self.uiQemuListComboBox.findData(self._appliance.emulator_properties()["arch"], flags=QtCore.Qt.MatchEndsWith) if i != -1: self.uiQemuListComboBox.setCurrentIndex(i) @@ -585,10 +576,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["emulator_type"] == "qemu": + settings["emulator_properties"]["path"] = self.uiQemuListComboBox.currentData() + else: 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, version, self._compute_id, self._symbols, parent=self) TemplateManager.instance().createTemplate(Template(new_template), callback=self._templateCreatedCallback) return False @@ -649,10 +645,10 @@ 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.emulator() == "docker": # skip Qemu binary selection and files pages if this is a Docker appliance return super().nextId() + 2 - elif "qemu" not in self._appliance: + elif self._appliance.emulator() != "qemu": # skip the Qemu binary selection page if not a Qemu appliance return super().nextId() + 1 return super().nextId() @@ -722,7 +718,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 diff --git a/gns3/registry/appliance.py b/gns3/registry/appliance.py index 895a305ba..d956c215d 100644 --- a/gns3/registry/appliance.py +++ b/gns3/registry/appliance.py @@ -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): @@ -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: @@ -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(os.path.join("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(os.path.join("schemas", 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)) @@ -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"]: @@ -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 @@ -142,17 +155,25 @@ 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.get("checksum") + + img = self._registry.search_image_file(self.emulator(), 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"])) image["path"] = img.path image["location"] = img.location - if "md5sum" not in image: + if not checksum: image["md5sum"] = img.md5sum image["filesize"] = img.filesize @@ -187,8 +208,48 @@ def is_version_installable(self, version): return False def emulator(self): - if "qemu" in self._appliance: - return "qemu" - if "iou" in self._appliance: - return "iou" - return "dynamips" + + if self._registry_version >= 8: + emulator_type = None + for settings in self._appliance["settings"]: + if settings["emulator_type"] and not emulator_type: + emulator_type = settings["emulator_type"] + elif settings["emulator_type"] and emulator_type != settings["emulator_type"]: + # we are currently not supporting multiple different emulators in the same appliance + raise ApplianceError("Multiple different emulator types found in appliance") + return emulator_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 emulator_properties(self): + """ + Get emulator 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["emulator_properties"] + # otherwise take the first settings we find + for settings in self._appliance["settings"]: + if settings["emulator_type"]: + return settings["emulator_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 diff --git a/gns3/registry/appliance_to_template.py b/gns3/registry/appliance_to_template.py index 71a4bcb53..6fc4960fd 100644 --- a/gns3/registry/appliance_to_template.py +++ b/gns3/registry/appliance_to_template.py @@ -33,7 +33,7 @@ class ApplianceToTemplate: Appliance installation. """ - def new_template(self, appliance_config, server, controller_symbols=None, parent=None): + def new_template(self, appliance_config, appliance_version, server, controller_symbols=None, parent=None): """ Creates a new template from an appliance. @@ -42,6 +42,7 @@ def new_template(self, appliance_config, server, controller_symbols=None, parent """ self._parent = parent + self._registry_version = appliance_config["registry_version"] new_template = { "compute_id": server, "name": appliance_config["name"] @@ -73,34 +74,62 @@ def new_template(self, appliance_config, server, controller_symbols=None, parent elif appliance_config["category"] == "firewall": new_template["symbol"] = ":/symbols/firewall.svg" - if "qemu" in appliance_config: - new_template["template_type"] = "qemu" - self._add_qemu_config(new_template, appliance_config) - elif "iou" in appliance_config: - new_template["template_type"] = "iou" - self._add_iou_config(new_template, appliance_config) - elif "dynamips" in appliance_config: - new_template["template_type"] = "dynamips" - self._add_dynamips_config(new_template, appliance_config) - elif "docker" in appliance_config: - new_template["template_type"] = "docker" - self._add_docker_config(new_template, appliance_config) + if self._registry_version >= 8: + for version in appliance_config["versions"]: + if version["name"] == appliance_version: + settings = self._get_settings(appliance_config, version.get("settings")) + emulator_type = settings["emulator_type"] + if emulator_type == "qemu": + self._add_qemu_config(new_template, settings["emulator_properties"], appliance_config) + elif emulator_type == "iou": + self._add_iou_config(new_template, settings["emulator_properties"], appliance_config) + elif emulator_type == "dynamips": + self._add_dynamips_config(new_template, settings["emulator_properties"], appliance_config) + elif emulator_type == "docker": + self._add_docker_config(new_template, settings["emulator_properties"], appliance_config) else: - raise ConfigException("{} no configuration found for known emulators".format(new_template["name"])) + if "qemu" in appliance_config: + self._add_qemu_config(new_template, appliance_config["qemu"], appliance_config) + elif "iou" in appliance_config: + self._add_iou_config(new_template, appliance_config["iou"], appliance_config) + elif "dynamips" in appliance_config: + self._add_dynamips_config(new_template, appliance_config["dynamips"], appliance_config) + elif "docker" in appliance_config: + self._add_docker_config(new_template, appliance_config["docker"], appliance_config) + else: + raise ConfigException("{} no configuration found for known emulators".format(new_template["name"])) return new_template - def _add_qemu_config(self, new_config, appliance_config): + def _get_settings(self, appliance_config, settings_name=None): - new_config.update(appliance_config["qemu"]) + # first look for specific settings set if provided + if settings_name: + for settings in appliance_config["settings"]: + if settings.get("name") == settings_name: + return settings + raise ConfigException("Settings {} cannot be found", settings_name) + + # then look for specified default settings ('default' = true) + for settings in appliance_config["settings"]: + if settings.get("default", False): + return settings + + # if no default settings are specified, use the first available settings set + return appliance_config["settings"][0] + + def _add_qemu_config(self, new_config, emulator_properties, appliance_config): + + new_config["template_type"] = "qemu" + new_config.update(emulator_properties) # the following properties are not valid for a template new_config.pop("kvm", None) new_config.pop("path", None) new_config.pop("arch", None) - options = appliance_config["qemu"].get("options", "") - if appliance_config["qemu"].get("kvm", "allow") == "disable" and "-machine accel=tcg" not in options: + options = emulator_properties.get("options", "") + if emulator_properties.get("kvm", "allow") == "disable" and "-machine accel=tcg" not in options: options += " -machine accel=tcg" new_config["options"] = options.strip() @@ -108,10 +137,10 @@ def _add_qemu_config(self, new_config, appliance_config): if image.get("path"): new_config[image["type"]] = self._relative_image_path("QEMU", image["path"]) - if "path" in appliance_config["qemu"]: - new_config["qemu_path"] = appliance_config["qemu"]["path"] + if "path" in emulator_properties: + new_config["qemu_path"] = emulator_properties["path"] else: - new_config["qemu_path"] = "qemu-system-{}".format(appliance_config["qemu"]["arch"]) + new_config["qemu_path"] = "qemu-system-{}".format(emulator_properties["arch"]) if "first_port_name" in appliance_config: new_config["first_port_name"] = appliance_config["first_port_name"] @@ -128,24 +157,27 @@ def _add_qemu_config(self, new_config, appliance_config): if "linked_clone" in appliance_config: new_config["linked_clone"] = appliance_config["linked_clone"] - def _add_docker_config(self, new_config, appliance_config): + def _add_docker_config(self, new_config, emulator_properties, appliance_config): - new_config.update(appliance_config["docker"]) + new_config["template_type"] = "docker" + new_config.update(emulator_properties) if "custom_adapters" in appliance_config: new_config["custom_adapters"] = appliance_config["custom_adapters"] - def _add_dynamips_config(self, new_config, appliance_config): + def _add_dynamips_config(self, new_config, emulator_properties, appliance_config): - new_config.update(appliance_config["dynamips"]) + new_config["template_type"] = "dynamips" + new_config.update(emulator_properties) for image in appliance_config["images"]: new_config[image["type"]] = self._relative_image_path("IOS", image["path"]) new_config["idlepc"] = image.get("idlepc", "") - def _add_iou_config(self, new_config, appliance_config): + def _add_iou_config(self, new_config, emulator_properties, appliance_config): - new_config.update(appliance_config["iou"]) + new_config["template_type"] = "iou" + new_config.update(emulator_properties) for image in appliance_config["images"]: if "path" not in image: raise ConfigException("Disk image is missing") diff --git a/gns3/schemas/appliance.json b/gns3/schemas/appliance.json index 10a6f6628..76a6a2088 100644 --- a/gns3/schemas/appliance.json +++ b/gns3/schemas/appliance.json @@ -86,7 +86,7 @@ "title": "An optional product url on vendor website" }, "registry_version": { - "enum": [1, 2, 3, 4, 5, 6], + "enum": [1, 2, 3, 4, 5, 6, 7], "title": "Version of the registry compatible with this appliance" }, "status": { diff --git a/gns3/schemas/appliance_v8.json b/gns3/schemas/appliance_v8.json new file mode 100644 index 000000000..e2cfadb07 --- /dev/null +++ b/gns3/schemas/appliance_v8.json @@ -0,0 +1,851 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "title": "JSON schema validating a GNS3 appliance", + "definitions": { + "dynamips_slot": { + "enum": [ + "C2600-MB-2FE", + "C2600-MB-1E", + "PA-A1", + "PA-8E", + "C1700-MB-1FE", + "PA-8T", + "PA-2FE-TX", + "PA-FE-TX", + "PA-GE", + "C2600-MB-2E", + "C7200-IO-FE", + "NM-4T", + "C2600-MB-1FE", + "C7200-IO-2FE", + "PA-POS-OC3", + "PA-4T+", + "C1700-MB-WIC1", + "NM-16ESW", + "C7200-IO-GE-E", + "NM-4E", + "GT96100-FE", + "NM-1FE-TX", + "Leopard-2FE", + "NM-1E", + "PA-4E", + "" + ] + }, + "dynamips_wic": { + "enum": [ + "WIC-1ENET", + "WIC-1T", + "WIC-2T", + "" + ] + }, + "docker_properties": { + "type": "object", + "title": "Docker specific properties", + "properties": { + "adapters": { + "type": "integer", + "title": "Number of ethernet adapters" + }, + "image": { + "type": "string", + "title": "Docker image in the Docker Hub" + }, + "start_command": { + "type": "string", + "title": "Command executed when the container start. Empty will use the default" + }, + "environment": { + "type": "string", + "title": "One KEY=VAR environment by line" + }, + "console_type": { + "enum": [ + "telnet", + "vnc", + "http", + "https" + ], + "title": "Type of console connection for the administration of the appliance" + }, + "console_http_port": { + "description": "Internal port in the container of the HTTP server", + "type": "integer" + }, + "console_http_path": { + "description": "Path of the web interface", + "type": "string" + } + }, + "required": [ + "adapters", + "image" + ] + }, + "iou_properties": { + "type": "object", + "title": "IOU specific properties", + "properties": { + "ethernet_adapters": { + "type": "integer", + "title": "Number of ethernet adapters" + }, + "serial_adapters": { + "type": "integer", + "title": "Number of serial adapters" + }, + "nvram": { + "type": "integer", + "title": "Host NVRAM" + }, + "ram": { + "type": "integer", + "title": "Host RAM" + }, + "startup_config": { + "type": "string", + "title": "Config loaded at startup" + } + }, + "required": [ + "ethernet_adapters", + "serial_adapters", + "nvram", + "ram", + "startup_config" + ] + }, + "dynamips_properties": { + "type": "object", + "title": "Dynamips specific properties", + "properties": { + "chassis": { + "title": "Chassis type", + "enum": [ + "1720", + "1721", + "1750", + "1751", + "1760", + "2610", + "2620", + "2610XM", + "2620XM", + "2650XM", + "2621", + "2611XM", + "2621XM", + "2651XM", + "3620", + "3640", + "3660", + "" + ] + }, + "platform": { + "title": "Platform type", + "enum": [ + "c1700", + "c2600", + "c2691", + "c3725", + "c3745", + "c3600", + "c7200" + ] + }, + "ram": { + "title": "Amount of ram", + "type": "integer", + "minimum": 1 + }, + "nvram": { + "title": "Amount of nvram", + "type": "integer", + "minimum": 1 + }, + "startup_config": { + "type": "string", + "title": "Config loaded at startup" + }, + "wic0": { + "$ref": "#/definitions/dynamips_wic" + }, + "wic1": { + "$ref": "#/definitions/dynamips_wic" + }, + "wic2": { + "$ref": "#/definitions/dynamips_wic" + }, + "slot0": { + "$ref": "#/definitions/dynamips_slot" + }, + "slot1": { + "$ref": "#/definitions/dynamips_slot" + }, + "slot2": { + "$ref": "#/definitions/dynamips_slot" + }, + "slot3": { + "$ref": "#/definitions/dynamips_slot" + }, + "slot4": { + "$ref": "#/definitions/dynamips_slot" + }, + "slot5": { + "$ref": "#/definitions/dynamips_slot" + }, + "slot6": { + "$ref": "#/definitions/dynamips_slot" + }, + "midplane": { + "enum": [ + "std", + "vxr" + ] + }, + "npe": { + "enum": [ + "npe-100", + "npe-150", + "npe-175", + "npe-200", + "npe-225", + "npe-300", + "npe-400", + "npe-g2" + ] + } + }, + "required": [ + "platform", + "ram", + "nvram" + ] + }, + "qemu_properties": { + "type": "object", + "title": "Qemu specific properties", + "properties": { + "adapter_type": { + "enum": [ + "e1000", + "i82550", + "i82551", + "i82557a", + "i82557b", + "i82557c", + "i82558a", + "i82558b", + "i82559a", + "i82559b", + "i82559c", + "i82559er", + "i82562", + "i82801", + "ne2k_pci", + "pcnet", + "rtl8139", + "virtio", + "virtio-net-pci", + "vmxnet3" + ], + "title": "Type of network adapter" + }, + "adapters": { + "type": "integer", + "title": "Number of adapters" + }, + "custom_adapters": { + "type": "array", + "title": "Custom adapters", + "items": { + "type": "object", + "properties": { + "adapter_number": { + "title": "Adapter number", + "type": "integer" + }, + "port_name": { + "title": "Custom port name", + "type": "string", + "minimum": 1 + }, + "adapter_type": { + "title": "Custom adapter type", + "type": "string", + "enum": [ + "e1000", + "i82550", + "i82551", + "i82557a", + "i82557b", + "i82557c", + "i82558a", + "i82558b", + "i82559a", + "i82559b", + "i82559c", + "i82559er", + "i82562", + "i82801", + "ne2k_pci", + "pcnet", + "rtl8139", + "virtio", + "virtio-net-pci", + "vmxnet3" + ] + }, + "mac_address": { + "title": "Custom MAC address", + "type": "string", + "minimum": 1, + "pattern": "^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$" + } + }, + "required": [ + "adapter_number" + ] + } + }, + "ram": { + "type": "integer", + "title": "Ram allocated to the appliance (MB)" + }, + "cpus": { + "type": "integer", + "title": "Number of Virtual CPU" + }, + "hda_disk_interface": { + "enum": [ + "ide", + "scsi", + "sd", + "mtd", + "floppy", + "pflash", + "virtio", + "sata" + ], + "title": "Disk interface for the installed hda_disk_image" + }, + "hdb_disk_interface": { + "enum": [ + "ide", + "scsi", + "sd", + "mtd", + "floppy", + "pflash", + "virtio", + "sata" + ], + "title": "Disk interface for the installed hdb_disk_image" + }, + "hdc_disk_interface": { + "enum": [ + "ide", + "scsi", + "sd", + "mtd", + "floppy", + "pflash", + "virtio", + "sata" + ], + "title": "Disk interface for the installed hdc_disk_image" + }, + "hdd_disk_interface": { + "enum": [ + "ide", + "scsi", + "sd", + "mtd", + "floppy", + "pflash", + "virtio", + "sata" + ], + "title": "Disk interface for the installed hdd_disk_image" + }, + "arch": { + "enum": [ + "aarch64", + "alpha", + "arm", + "cris", + "i386", + "lm32", + "m68k", + "microblaze", + "microblazeel", + "mips", + "mips64", + "mips64el", + "mipsel", + "moxie", + "or32", + "ppc", + "ppc64", + "ppcemb", + "s390x", + "sh4", + "sh4eb", + "sparc", + "sparc64", + "tricore", + "unicore32", + "x86_64", + "xtensa", + "xtensaeb" + ], + "title": "Architecture emulated" + }, + "console_type": { + "enum": [ + "telnet", + "vnc", + "spice" + ], + "title": "Type of console connection for the administration of the appliance" + }, + "boot_priority": { + "enum": [ + "d", + "c", + "dc", + "cd", + "n", + "nc", + "nd", + "cn", + "dn" + ], + "title": "Optional define the disk boot priory. Refer to -boot option in qemu manual for more details." + }, + "kernel_command_line": { + "type": "string", + "title": "Command line parameters send to the kernel" + }, + "kvm": { + "title": "KVM requirements", + "enum": [ + "require", + "allow", + "disable" + ] + }, + "options": { + "type": "string", + "title": "Optional additional qemu command line options" + }, + "cpu_throttling": { + "type": "number", + "minimum": 0, + "maximum": 100, + "title": "Throttle the CPU" + }, + "tpm": { + "type": "boolean", + "title": "Enable the Trusted Platform Module (TPM)" + }, + "uefi": { + "type": "boolean", + "title": "Enable the UEFI boot mode" + }, + "on_close": { + "title": "Action to execute on the VM is closed", + "enum": [ + "power_off", + "shutdown_signal", + "save_vm_state" + ] + }, + "process_priority": { + "title": "Process priority for QEMU", + "enum": [ + "realtime", + "very high", + "high", + "normal", + "low", + "very low", + "null" + ] + } + }, + "required": [ + "adapter_type", + "adapters", + "ram", + "arch", + "console_type", + "kvm" + ] + } + }, + "properties": { + "appliance_id": { + "title": "Appliance ID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "name": { + "type": "string", + "title": "Appliance name" + }, + "category": { + "enum": [ + "router", + "multilayer_switch", + "firewall", + "guest" + ], + "title": "Category of the appliance" + }, + "description": { + "type": "string", + "title": "Description of the appliance. Could be a marketing description" + }, + "vendor_name": { + "type": "string", + "title": "Name of the vendor" + }, + "vendor_url": { + "type": "string", + "format": "uri", + "title": "Website of the vendor" + }, + "documentation_url": { + "type": "string", + "format": "uri", + "title": "An optional documentation for using the appliance on vendor website" + }, + "product_name": { + "type": "string", + "title": "Product name" + }, + "product_url": { + "type": "string", + "format": "uri", + "title": "An optional product url on vendor website" + }, + "registry_version": { + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "title": "Version of the registry compatible with this appliance" + }, + "status": { + "enum": [ + "stable", + "experimental", + "broken" + ], + "title": "Document if the appliance is working or not" + }, + "availability": { + "enum": [ + "free", + "with-registration", + "free-to-try", + "service-contract" + ], + "title": "About image availability: can be downloaded directly; download requires a free registration; paid but a trial version (time or feature limited) is available; not available publicly" + }, + "maintainer": { + "type": "string", + "title": "Maintainer name" + }, + "maintainer_email": { + "type": "string", + "format": "email", + "title": "Maintainer email" + }, + "installation_instructions": { + "type": "string", + "title": "Optional installation instructions" + }, + "usage": { + "type": "string", + "title": "How to use the appliance" + }, + "default_username": { + "type": "string", + "title": "Default username for the appliance" + }, + "default_password": { + "type": "string", + "title": "Default password for the appliance" + }, + "symbol": { + "type": "string", + "title": "An optional symbol for the appliance" + }, + "first_port_name": { + "type": "string", + "title": "Optional name of the first networking port example: eth0" + }, + "port_name_format": { + "type": "string", + "title": "Optional formating of the networking port example: eth{0}" + }, + "port_segment_size": { + "type": "integer", + "title": "Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2" + }, + "linked_clone": { + "type": "boolean", + "title": "False if you don't want to use a single image for all nodes" + }, + "settings": { + "type": "array", + "title": "Settings for running the appliance", + "items": { + "type": "object", + "title": "Emulator settings", + "properties": { + "name": { + "type": "string", + "title": "Name of the settings set" + }, + "default": { + "type": "boolean", + "title": "Whether these are the default settings" + }, + "emulator_type": { + "enum": [ + "docker", + "iou", + "dynamips", + "qemu" + ], + "title": "Type of emulator properties" + }, + "emulator_versions": { + "type": "array", + "title": "Versions of the emulator", + "items": { + "type": "string", + "title": "Version of the emulator" + }, + "emulator_properties": { + "type": "object", + "title": "Properties for the emulator", + "oneOf": [ + { + "$ref": "#/definitions/qemu_properties" + }, + { + "$ref": "#/definitions/dynamips_properties" + }, + { + "$ref": "#/definitions/iou_properties" + }, + { + "$ref": "#/definitions/docker_properties" + } + ] + } + }, + "required": [ + "emulator_type", + "emulator_properties" + ] + } + } + }, + "images": { + "type": "array", + "title": "Images for this appliance", + "items": { + "type": "object", + "title": "An image file", + "properties": { + "filename": { + "type": "string", + "title": "Filename" + }, + "version": { + "type": "string", + "title": "Version of the file" + }, + "checksum": { + "type": "string", + "title": "checksum of the file" + }, + "checksum_type": { + "title": "checksum type", + "enum": [ + "md5" + ] + }, + "filesize": { + "type": "integer", + "title": "File size in bytes" + }, + "download_url": { + "type": "string", + "format": "uri", + "title": "Download url where you can download the appliance from a browser" + }, + "direct_download_url": { + "type": "string", + "format": "uri", + "title": "Optional. Non authenticated url to the image file where you can download the image." + }, + "compression": { + "enum": [ + "bzip2", + "gzip", + "lzma", + "xz", + "rar", + "zip", + "7z" + ], + "title": "Optional, compression type of direct download url image." + }, + "compression_target": { + "type": "string", + "title": "Optional, file name of the image file inside the compressed file." + } + }, + "required": [ + "filename", + "version", + "checksum", + "filesize" + ] + } + }, + "versions": { + "type": "array", + "title": "Versions of the appliance", + "items": { + "type": "object", + "title": "A version of the appliance", + "properties": { + "name": { + "type": "string", + "title": "Name of the version" + }, + "settings": { + "type": "string", + "title": "Emulator settings to use to run the version" + }, + "installation_instructions": { + "type": "string", + "title": "Optional installation instructions for the version" + }, + "usage": { + "type": "string", + "title": "Optional instructions about using the version" + }, + "default_username": { + "type": "string", + "title": "Default username for the version" + }, + "default_password": { + "type": "string", + "title": "Default password for the version" + }, + "symbol": { + "type": "string", + "title": "An optional symbol for the version" + }, + "first_port_name": { + "type": "string", + "title": "Optional name of the first networking port example: eth0" + }, + "port_name_format": { + "type": "string", + "title": "Optional formating of the networking port example: eth{0}" + }, + "port_segment_size": { + "type": "integer", + "title": "Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2" + }, + "linked_clone": { + "type": "boolean", + "title": "False if you don't want to use a single image for all nodes" + }, + "idlepc": { + "type": "string", + "pattern": "^0x[0-9a-f]{8}" + }, + "images": { + "type": "object", + "title": "Images used for this version", + "properties": { + "kernel_image": { + "type": "string", + "title": "Kernel image" + }, + "initrd": { + "type": "string", + "title": "Initrd disk image" + }, + "image": { + "type": "string", + "title": "OS image" + }, + "bios_image": { + "type": "string", + "title": "Bios image" + }, + "hda_disk_image": { + "type": "string", + "title": "Hda disk image" + }, + "hdb_disk_image": { + "type": "string", + "title": "Hdc disk image" + }, + "hdc_disk_image": { + "type": "string", + "title": "Hdd disk image" + }, + "hdd_disk_image": { + "type": "string", + "title": "Hdd diskimage" + }, + "cdrom_image": { + "type": "string", + "title": "cdrom image" + } + } + } + }, + "required": [ + "name" + ] + } + }, + "required": [ + "appliance_id", + "name", + "category", + "description", + "vendor_name", + "vendor_url", + "product_name", + "registry_version", + "status", + "maintainer", + "maintainer_email" + ] + } +} \ No newline at end of file From 4b7cf4e5536edeb75214036a84a24f7d5cbf01d8 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 3 Jul 2023 19:12:44 +1000 Subject: [PATCH 02/17] Support legacy "idlepc" field --- gns3/registry/appliance.py | 2 +- gns3/registry/appliance_to_template.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gns3/registry/appliance.py b/gns3/registry/appliance.py index d956c215d..4f7631320 100644 --- a/gns3/registry/appliance.py +++ b/gns3/registry/appliance.py @@ -109,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: diff --git a/gns3/registry/appliance_to_template.py b/gns3/registry/appliance_to_template.py index 6fc4960fd..16cbc7775 100644 --- a/gns3/registry/appliance_to_template.py +++ b/gns3/registry/appliance_to_template.py @@ -172,7 +172,8 @@ def _add_dynamips_config(self, new_config, emulator_properties, appliance_config for image in appliance_config["images"]: new_config[image["type"]] = self._relative_image_path("IOS", image["path"]) - new_config["idlepc"] = image.get("idlepc", "") + if self._registry_version < 8: + new_config["idlepc"] = image.get("idlepc", "") def _add_iou_config(self, new_config, emulator_properties, appliance_config): From 8a5ab6b37475bbf59e215f718cc65603377cc858 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 9 Jul 2023 14:10:15 +1000 Subject: [PATCH 03/17] Update schema for appliance version 8 --- gns3/schemas/appliance.json | 1 - gns3/schemas/appliance_v8.json | 728 ++++++++++++++++++--------------- 2 files changed, 398 insertions(+), 331 deletions(-) diff --git a/gns3/schemas/appliance.json b/gns3/schemas/appliance.json index 76a6a2088..2a9005fc1 100644 --- a/gns3/schemas/appliance.json +++ b/gns3/schemas/appliance.json @@ -400,7 +400,6 @@ "md5sum": { "type": "string", "title": "md5sum of the file", - "type": "string", "pattern": "^[a-f0-9]{32}$" }, "filesize": { diff --git a/gns3/schemas/appliance_v8.json b/gns3/schemas/appliance_v8.json index e2cfadb07..7598052bd 100644 --- a/gns3/schemas/appliance_v8.json +++ b/gns3/schemas/appliance_v8.json @@ -3,55 +3,45 @@ "type": "object", "title": "JSON schema validating a GNS3 appliance", "definitions": { - "dynamips_slot": { - "enum": [ - "C2600-MB-2FE", - "C2600-MB-1E", - "PA-A1", - "PA-8E", - "C1700-MB-1FE", - "PA-8T", - "PA-2FE-TX", - "PA-FE-TX", - "PA-GE", - "C2600-MB-2E", - "C7200-IO-FE", - "NM-4T", - "C2600-MB-1FE", - "C7200-IO-2FE", - "PA-POS-OC3", - "PA-4T+", - "C1700-MB-WIC1", - "NM-16ESW", - "C7200-IO-GE-E", - "NM-4E", - "GT96100-FE", - "NM-1FE-TX", - "Leopard-2FE", - "NM-1E", - "PA-4E", - "" - ] - }, - "dynamips_wic": { + "categories": { "enum": [ - "WIC-1ENET", - "WIC-1T", - "WIC-2T", - "" + "router", + "multilayer_switch", + "firewall", + "guest" ] }, "docker_properties": { "type": "object", - "title": "Docker specific properties", + "title": "Docker template properties", "properties": { - "adapters": { - "type": "integer", - "title": "Number of ethernet adapters" + "name": { + "type": "string", + "title": "Name of the template" + }, + "category": { + "$ref": "#/definitions/categories", + "title": "Category of the template" + }, + "default_name_format": { + "type": "string", + "title": "Default name format" + }, + "usage": { + "type": "string", + "title": "How to use the template" + }, + "symbol": { + "type": "string", + "title": "Symbol of the template" }, "image": { "type": "string", - "title": "Docker image in the Docker Hub" + "title": "Docker image" + }, + "adapters": { + "type": "integer", + "title": "Number of ethernet adapters" }, "start_command": { "type": "string", @@ -68,15 +58,28 @@ "http", "https" ], - "title": "Type of console connection for the administration of the appliance" + "title": "Type of console" }, "console_http_port": { - "description": "Internal port in the container of the HTTP server", + "title": "Internal port in the container of the HTTP server", "type": "integer" }, "console_http_path": { - "description": "Path of the web interface", + "title": "Path of the web interface", + "type": "string" + }, + "console_resolution": { + "title": "Console resolution for VNC, for example 1024x768", + "type": "string", + "pattern": "^[0-9]+x[0-9]+$" + }, + "extra_hosts": { + "title": "Docker extra hosts (added to /etc/hosts)", "type": "string" + }, + "extra_volumes": { + "title": "Additional directories to make persistent", + "type": "array" } }, "required": [ @@ -86,8 +89,28 @@ }, "iou_properties": { "type": "object", - "title": "IOU specific properties", + "title": "IOU template properties", "properties": { + "name": { + "type": "string", + "title": "Name of the template" + }, + "category": { + "$ref": "#/definitions/categories", + "title": "Category of the template" + }, + "default_name_format": { + "type": "string", + "title": "Default name format" + }, + "usage": { + "type": "string", + "title": "How to use the template" + }, + "symbol": { + "type": "string", + "title": "Symbol of the template" + }, "ethernet_adapters": { "type": "integer", "title": "Number of ethernet adapters" @@ -96,14 +119,14 @@ "type": "integer", "title": "Number of serial adapters" }, - "nvram": { - "type": "integer", - "title": "Host NVRAM" - }, "ram": { "type": "integer", "title": "Host RAM" }, + "nvram": { + "type": "integer", + "title": "Host NVRAM" + }, "startup_config": { "type": "string", "title": "Config loaded at startup" @@ -117,10 +140,68 @@ "startup_config" ] }, + "dynamips_slot": { + "enum": [ + "C2600-MB-2FE", + "C2600-MB-1E", + "PA-A1", + "PA-8E", + "C1700-MB-1FE", + "PA-8T", + "PA-2FE-TX", + "PA-FE-TX", + "PA-GE", + "C2600-MB-2E", + "C7200-IO-FE", + "NM-4T", + "C2600-MB-1FE", + "C7200-IO-2FE", + "PA-POS-OC3", + "PA-4T+", + "C1700-MB-WIC1", + "NM-16ESW", + "C7200-IO-GE-E", + "NM-4E", + "GT96100-FE", + "NM-1FE-TX", + "Leopard-2FE", + "NM-1E", + "PA-4E", + "" + ] + }, + "dynamips_wic": { + "enum": [ + "WIC-1ENET", + "WIC-1T", + "WIC-2T", + "" + ] + }, "dynamips_properties": { "type": "object", - "title": "Dynamips specific properties", + "title": "Dynamips template properties", "properties": { + "name": { + "type": "string", + "title": "Name of the template" + }, + "category": { + "$ref": "#/definitions/categories", + "title": "Category of the template" + }, + "default_name_format": { + "type": "string", + "title": "Default name format" + }, + "usage": { + "type": "string", + "title": "How to use the template" + }, + "symbol": { + "type": "string", + "title": "Symbol of the template" + }, "chassis": { "title": "Chassis type", "enum": [ @@ -166,6 +247,10 @@ "type": "integer", "minimum": 1 }, + "idlepc": { + "type": "string", + "pattern": "^0x[0-9a-f]{8}" + }, "startup_config": { "type": "string", "title": "Config loaded at startup" @@ -225,10 +310,42 @@ "nvram" ] }, + "qemu_disk_interfaces": { + "enum": [ + "ide", + "scsi", + "sd", + "mtd", + "floppy", + "pflash", + "virtio", + "sata" + ] + }, "qemu_properties": { "type": "object", - "title": "Qemu specific properties", + "title": "Qemu template properties", "properties": { + "name": { + "type": "string", + "title": "Name of the template" + }, + "category": { + "$ref": "#/definitions/categories", + "title": "Category of the template" + }, + "default_name_format": { + "type": "string", + "title": "Default name format" + }, + "usage": { + "type": "string", + "title": "How to use the template" + }, + "symbol": { + "type": "string", + "title": "Symbol of the template" + }, "adapter_type": { "enum": [ "e1000", @@ -311,6 +428,22 @@ ] } }, + "first_port_name": { + "type": "string", + "title": "Optional name of the first networking port example: eth0" + }, + "port_name_format": { + "type": "string", + "title": "Optional formating of the networking port example: eth{0}" + }, + "port_segment_size": { + "type": "integer", + "title": "Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2" + }, + "linked_clone": { + "type": "boolean", + "title": "False if you don't want to use a single image for all nodes" + }, "ram": { "type": "integer", "title": "Ram allocated to the appliance (MB)" @@ -320,58 +453,22 @@ "title": "Number of Virtual CPU" }, "hda_disk_interface": { - "enum": [ - "ide", - "scsi", - "sd", - "mtd", - "floppy", - "pflash", - "virtio", - "sata" - ], + "$ref": "#/definitions/qemu_disk_interfaces", "title": "Disk interface for the installed hda_disk_image" }, "hdb_disk_interface": { - "enum": [ - "ide", - "scsi", - "sd", - "mtd", - "floppy", - "pflash", - "virtio", - "sata" - ], + "$ref": "#/definitions/qemu_disk_interfaces", "title": "Disk interface for the installed hdb_disk_image" }, "hdc_disk_interface": { - "enum": [ - "ide", - "scsi", - "sd", - "mtd", - "floppy", - "pflash", - "virtio", - "sata" - ], + "$ref": "#/definitions/qemu_disk_interfaces", "title": "Disk interface for the installed hdc_disk_image" }, "hdd_disk_interface": { - "enum": [ - "ide", - "scsi", - "sd", - "mtd", - "floppy", - "pflash", - "virtio", - "sata" - ], + "$ref": "#/definitions/qemu_disk_interfaces", "title": "Disk interface for the installed hdd_disk_image" }, - "arch": { + "platform": { "enum": [ "aarch64", "alpha", @@ -402,13 +499,14 @@ "xtensa", "xtensaeb" ], - "title": "Architecture emulated" + "title": "Platform to emulate" }, "console_type": { "enum": [ "telnet", "vnc", - "spice" + "spice", + "spice+agent" ], "title": "Type of console connection for the administration of the appliance" }, @@ -481,7 +579,7 @@ "adapter_type", "adapters", "ram", - "arch", + "platform", "console_type", "kvm" ] @@ -500,12 +598,7 @@ "title": "Appliance name" }, "category": { - "enum": [ - "router", - "multilayer_switch", - "firewall", - "guest" - ], + "$ref": "#/definitions/categories", "title": "Category of the appliance" }, "description": { @@ -537,16 +630,9 @@ }, "registry_version": { "enum": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, 8 ], - "title": "Version of the registry compatible with this appliance" + "title": "Version of the registry compatible with this appliance (version >=8 introduced breaking changes)" }, "status": { "enum": [ @@ -594,22 +680,6 @@ "type": "string", "title": "An optional symbol for the appliance" }, - "first_port_name": { - "type": "string", - "title": "Optional name of the first networking port example: eth0" - }, - "port_name_format": { - "type": "string", - "title": "Optional formating of the networking port example: eth{0}" - }, - "port_segment_size": { - "type": "integer", - "title": "Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2" - }, - "linked_clone": { - "type": "boolean", - "title": "False if you don't want to use a single image for all nodes" - }, "settings": { "type": "array", "title": "Settings for running the appliance", @@ -625,7 +695,12 @@ "type": "boolean", "title": "Whether these are the default settings" }, - "emulator_type": { + "inherit_default_settings": { + "type": "boolean", + "title": "Whether the default settings should be used", + "default": "true" + }, + "template_type": { "enum": [ "docker", "iou", @@ -634,218 +709,211 @@ ], "title": "Type of emulator properties" }, - "emulator_versions": { - "type": "array", - "title": "Versions of the emulator", - "items": { - "type": "string", - "title": "Version of the emulator" - }, - "emulator_properties": { - "type": "object", - "title": "Properties for the emulator", - "oneOf": [ - { - "$ref": "#/definitions/qemu_properties" - }, - { - "$ref": "#/definitions/dynamips_properties" - }, - { - "$ref": "#/definitions/iou_properties" - }, - { - "$ref": "#/definitions/docker_properties" - } - ] - } - }, - "required": [ - "emulator_type", - "emulator_properties" - ] - } - } - }, - "images": { - "type": "array", - "title": "Images for this appliance", - "items": { - "type": "object", - "title": "An image file", - "properties": { - "filename": { - "type": "string", - "title": "Filename" - }, - "version": { - "type": "string", - "title": "Version of the file" - }, - "checksum": { - "type": "string", - "title": "checksum of the file" - }, - "checksum_type": { - "title": "checksum type", - "enum": [ - "md5" - ] - }, - "filesize": { - "type": "integer", - "title": "File size in bytes" - }, - "download_url": { - "type": "string", - "format": "uri", - "title": "Download url where you can download the appliance from a browser" - }, - "direct_download_url": { - "type": "string", - "format": "uri", - "title": "Optional. Non authenticated url to the image file where you can download the image." - }, - "compression": { - "enum": [ - "bzip2", - "gzip", - "lzma", - "xz", - "rar", - "zip", - "7z" - ], - "title": "Optional, compression type of direct download url image." - }, - "compression_target": { - "type": "string", - "title": "Optional, file name of the image file inside the compressed file." - } - }, - "required": [ - "filename", - "version", - "checksum", - "filesize" - ] - } - }, - "versions": { - "type": "array", - "title": "Versions of the appliance", - "items": { - "type": "object", - "title": "A version of the appliance", - "properties": { - "name": { - "type": "string", - "title": "Name of the version" - }, - "settings": { - "type": "string", - "title": "Emulator settings to use to run the version" - }, - "installation_instructions": { - "type": "string", - "title": "Optional installation instructions for the version" - }, - "usage": { - "type": "string", - "title": "Optional instructions about using the version" - }, - "default_username": { - "type": "string", - "title": "Default username for the version" - }, - "default_password": { - "type": "string", - "title": "Default password for the version" - }, - "symbol": { - "type": "string", - "title": "An optional symbol for the version" - }, - "first_port_name": { - "type": "string", - "title": "Optional name of the first networking port example: eth0" - }, - "port_name_format": { - "type": "string", - "title": "Optional formating of the networking port example: eth{0}" - }, - "port_segment_size": { - "type": "integer", - "title": "Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2" - }, - "linked_clone": { - "type": "boolean", - "title": "False if you don't want to use a single image for all nodes" - }, - "idlepc": { - "type": "string", - "pattern": "^0x[0-9a-f]{8}" - }, - "images": { + "template_properties": { "type": "object", - "title": "Images used for this version", - "properties": { - "kernel_image": { - "type": "string", - "title": "Kernel image" + "title": "Properties for the template", + "oneOf": [ + { + "$ref": "#/definitions/qemu_properties" }, - "initrd": { - "type": "string", - "title": "Initrd disk image" + { + "$ref": "#/definitions/dynamips_properties" }, - "image": { - "type": "string", - "title": "OS image" - }, - "bios_image": { - "type": "string", - "title": "Bios image" - }, - "hda_disk_image": { - "type": "string", - "title": "Hda disk image" + { + "$ref": "#/definitions/iou_properties" }, - "hdb_disk_image": { - "type": "string", - "title": "Hdc disk image" - }, - "hdc_disk_image": { - "type": "string", - "title": "Hdd disk image" - }, - "hdd_disk_image": { - "type": "string", - "title": "Hdd diskimage" - }, - "cdrom_image": { - "type": "string", - "title": "cdrom image" + { + "$ref": "#/definitions/docker_properties" } - } + ] } }, "required": [ - "name" + "template_type", + "template_properties" ] } - }, - "required": [ - "appliance_id", - "name", - "category", - "description", - "vendor_name", - "vendor_url", - "product_name", - "registry_version", - "status", - "maintainer", - "maintainer_email" - ] - } -} \ No newline at end of file + } + }, + "images": { + "type": "array", + "title": "Images for this appliance", + "items": { + "type": "object", + "title": "An image file", + "properties": { + "filename": { + "type": "string", + "title": "Filename" + }, + "version": { + "type": "string", + "title": "Version of the image file" + }, + "md5sum": { + "type": "string", + "title": "MD5 cheksum of the image file", + "pattern": "^[a-f0-9]{32}$" + }, + "checksum": { + "type": "string", + "title": "checksum of the image file" + }, + "checksum_type": { + "title": "checksum type of the image file", + "enum": [ + "md5" + ] + }, + "filesize": { + "type": "integer", + "title": "File size in bytes of the image file" + }, + "download_url": { + "type": "string", + "format": "uri", + "title": "Download URL where you can download the image file from a browser" + }, + "direct_download_url": { + "type": "string", + "format": "uri", + "title": "Optional. Non authenticated URL to the image file where you can download the image directly" + }, + "compression": { + "enum": [ + "bzip2", + "gzip", + "lzma", + "xz", + "rar", + "zip", + "7z" + ], + "title": "Optional, compression type of direct download URL image." + }, + "compression_target": { + "type": "string", + "title": "Optional, file name of the image file inside the compressed file." + } + }, + "anyOf": [ + { + "required": [ + "filename", + "version", + "md5sum", + "filesize" + ] + }, + { + "required": [ + "filename", + "version", + "checksum", + "filesize" + ] + } + ] + } + }, + "versions": { + "type": "array", + "title": "Versions of the appliance", + "items": { + "type": "object", + "title": "A version of the appliance", + "properties": { + "name": { + "type": "string", + "title": "Name of the version" + }, + "settings": { + "type": "string", + "title": "Template settings to use to run the version" + }, + "category": { + "$ref": "#/definitions/categories", + "title": "Category of the version" + }, + "installation_instructions": { + "type": "string", + "title": "Optional installation instructions for the version" + }, + "usage": { + "type": "string", + "title": "Optional instructions about using the version" + }, + "default_username": { + "type": "string", + "title": "Default username for the version" + }, + "default_password": { + "type": "string", + "title": "Default password for the version" + }, + "symbol": { + "type": "string", + "title": "An optional symbol for the version" + }, + "images": { + "type": "object", + "title": "Images used for this version", + "properties": { + "kernel_image": { + "type": "string", + "title": "Kernel image (Qemu only)" + }, + "initrd": { + "type": "string", + "title": "Initrd disk image (Qemu only)" + }, + "image": { + "type": "string", + "title": "OS image (IOU and Dynamips only)" + }, + "bios_image": { + "type": "string", + "title": "Bios image (Qemu only)" + }, + "hda_disk_image": { + "type": "string", + "title": "Hda disk image (Qemu only)" + }, + "hdb_disk_image": { + "type": "string", + "title": "Hdc disk image (Qemu only)" + }, + "hdc_disk_image": { + "type": "string", + "title": "Hdd disk image (Qemu only)" + }, + "hdd_disk_image": { + "type": "string", + "title": "Hdd disk image (Qemu only)" + }, + "cdrom_image": { + "type": "string", + "title": "cdrom image (Qemu only)" + } + } + } + }, + "required": [ + "name" + ] + } + }, + "required": [ + "appliance_id", + "name", + "category", + "description", + "vendor_name", + "vendor_url", + "product_name", + "registry_version", + "status", + "maintainer", + "maintainer_email" + ] +} From 0a43b9e6e9200f3c1ee9d0f57738940d0da3971d Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 9 Jul 2023 20:18:22 +1000 Subject: [PATCH 04/17] Fix tests --- gns3/dialogs/appliance_wizard.py | 2 +- gns3/registry/appliance_to_template.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gns3/dialogs/appliance_wizard.py b/gns3/dialogs/appliance_wizard.py index 7f9d72cf7..d5ba01f14 100644 --- a/gns3/dialogs/appliance_wizard.py +++ b/gns3/dialogs/appliance_wizard.py @@ -584,7 +584,7 @@ def _install(self, version): else: appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData() - new_template = ApplianceToTemplate().new_template(appliance_configuration, version, 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 diff --git a/gns3/registry/appliance_to_template.py b/gns3/registry/appliance_to_template.py index 16cbc7775..122111f10 100644 --- a/gns3/registry/appliance_to_template.py +++ b/gns3/registry/appliance_to_template.py @@ -33,7 +33,7 @@ class ApplianceToTemplate: Appliance installation. """ - def new_template(self, appliance_config, appliance_version, server, controller_symbols=None, parent=None): + def new_template(self, appliance_config, server, appliance_version=None, controller_symbols=None, parent=None): """ Creates a new template from an appliance. @@ -76,7 +76,7 @@ def new_template(self, appliance_config, appliance_version, server, controller_s if self._registry_version >= 8: for version in appliance_config["versions"]: - if version["name"] == appliance_version: + if appliance_version and version["name"] == appliance_version: settings = self._get_settings(appliance_config, version.get("settings")) emulator_type = settings["emulator_type"] if emulator_type == "qemu": From a58451a5526e47932ddb8f88ae0017393e179503 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 11 Jul 2023 17:38:00 +1000 Subject: [PATCH 05/17] Update to support template_type & template_properties --- gns3/dialogs/appliance_wizard.py | 44 ++++++++++++++--------- gns3/registry/appliance.py | 28 +++++++-------- gns3/registry/appliance_to_template.py | 48 ++++++++++++++------------ 3 files changed, 68 insertions(+), 52 deletions(-) diff --git a/gns3/dialogs/appliance_wizard.py b/gns3/dialogs/appliance_wizard.py index d5ba01f14..4f48f90b6 100644 --- a/gns3/dialogs/appliance_wizard.py +++ b/gns3/dialogs/appliance_wizard.py @@ -144,9 +144,9 @@ def initializePage(self, page_id): if self.page(page_id) == self.uiServerWizardPage: Controller.instance().getSymbols(self._getSymbolsCallback) - emulator_type = self._appliance.emulator() - if not emulator_type: - raise ApplianceError("No emulator type found for appliance {}".format(self._appliance["name"])) + 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") @@ -164,11 +164,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(): @@ -186,17 +186,21 @@ 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.emulator_properties().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.emulator_properties()["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.uiUsageWizardPage: self.uiUsageTextEdit.setText("The template will be available in the {} category.\n\n{}".format(self._appliance["category"].replace("_", " "), self._appliance.get("usage", ""))) @@ -206,7 +210,11 @@ 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.emulator_properties()["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: @@ -227,7 +235,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): """ @@ -398,7 +406,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"), @@ -510,7 +518,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", @@ -545,7 +553,11 @@ def _getQemuBinariesFromServerCallback(self, result, error=False, **kwargs): if self.uiQemuListComboBox.count() == 1: self.next() else: - i = self.uiQemuListComboBox.findData(self._appliance.emulator_properties()["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) @@ -628,7 +640,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 @@ -645,10 +657,10 @@ def _applianceImageUploadedCallback(self, result, error=False, context=None, **k def nextId(self): if self.currentPage() == self.uiServerWizardPage: - if self._appliance.emulator() == "docker": + if self._appliance.template_type() == "docker": # skip Qemu binary selection and files pages if this is a Docker appliance return super().nextId() + 2 - elif self._appliance.emulator() != "qemu": + elif self._appliance.template_type() != "qemu": # skip the Qemu binary selection page if not a Qemu appliance return super().nextId() + 1 return super().nextId() diff --git a/gns3/registry/appliance.py b/gns3/registry/appliance.py index 4f7631320..31ea4ea4b 100644 --- a/gns3/registry/appliance.py +++ b/gns3/registry/appliance.py @@ -163,7 +163,7 @@ def search_images_for_version(self, version_name): raise ApplianceError("Checksum type {} is not supported".format(checksum_type)) checksum = image.get("checksum") - img = self._registry.search_image_file(self.emulator(), image["filename"], checksum, image.get("filesize")) + img = self._registry.search_image_file(self.template_type(), image["filename"], checksum, image.get("filesize")) if img is None: if checksum: raise ApplianceError("File {} with checksum {} not found for {}".format(image["filename"], checksum, appliance["name"])) @@ -207,17 +207,17 @@ def is_version_installable(self, version): except ApplianceError: return False - def emulator(self): + def template_type(self): if self._registry_version >= 8: - emulator_type = None + template_type = None for settings in self._appliance["settings"]: - if settings["emulator_type"] and not emulator_type: - emulator_type = settings["emulator_type"] - elif settings["emulator_type"] and emulator_type != settings["emulator_type"]: - # we are currently not supporting multiple different emulators in the same appliance - raise ApplianceError("Multiple different emulator types found in appliance") - return emulator_type + 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") + return template_type else: if "qemu" in self._appliance: return "qemu" @@ -229,20 +229,20 @@ def emulator(self): return "docker" return None - def emulator_properties(self): + def template_properties(self): """ - Get emulator properties + 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["emulator_properties"] + return settings["template_properties"] # otherwise take the first settings we find for settings in self._appliance["settings"]: - if settings["emulator_type"]: - return settings["emulator_properties"] + if settings["template_type"]: + return settings["template_properties"] else: if "qemu" in self._appliance: return self._appliance["qemu"] diff --git a/gns3/registry/appliance_to_template.py b/gns3/registry/appliance_to_template.py index 122111f10..e1596c4eb 100644 --- a/gns3/registry/appliance_to_template.py +++ b/gns3/registry/appliance_to_template.py @@ -78,15 +78,15 @@ def new_template(self, appliance_config, server, appliance_version=None, control for version in appliance_config["versions"]: if appliance_version and version["name"] == appliance_version: settings = self._get_settings(appliance_config, version.get("settings")) - emulator_type = settings["emulator_type"] - if emulator_type == "qemu": - self._add_qemu_config(new_template, settings["emulator_properties"], appliance_config) - elif emulator_type == "iou": - self._add_iou_config(new_template, settings["emulator_properties"], appliance_config) - elif emulator_type == "dynamips": - self._add_dynamips_config(new_template, settings["emulator_properties"], appliance_config) - elif emulator_type == "docker": - self._add_docker_config(new_template, settings["emulator_properties"], appliance_config) + template_type = settings["template_type"] + if template_type == "qemu": + self._add_qemu_config(new_template, settings["template_properties"], appliance_config) + elif template_type == "iou": + self._add_iou_config(new_template, settings["template_properties"], appliance_config) + elif template_type == "dynamips": + self._add_dynamips_config(new_template, settings["template_properties"], appliance_config) + elif template_type == "docker": + self._add_docker_config(new_template, settings["template_properties"], appliance_config) else: if "qemu" in appliance_config: self._add_qemu_config(new_template, appliance_config["qemu"], appliance_config) @@ -118,18 +118,18 @@ def _get_settings(self, appliance_config, settings_name=None): # if no default settings are specified, use the first available settings set return appliance_config["settings"][0] - def _add_qemu_config(self, new_config, emulator_properties, appliance_config): + def _add_qemu_config(self, new_config, template_properties, appliance_config): new_config["template_type"] = "qemu" - new_config.update(emulator_properties) + new_config.update(template_properties) # the following properties are not valid for a template new_config.pop("kvm", None) new_config.pop("path", None) new_config.pop("arch", None) - options = emulator_properties.get("options", "") - if emulator_properties.get("kvm", "allow") == "disable" and "-machine accel=tcg" not in options: + options = template_properties.get("options", "") + if template_properties.get("kvm", "allow") == "disable" and "-machine accel=tcg" not in options: options += " -machine accel=tcg" new_config["options"] = options.strip() @@ -137,10 +137,14 @@ def _add_qemu_config(self, new_config, emulator_properties, appliance_config): if image.get("path"): new_config[image["type"]] = self._relative_image_path("QEMU", image["path"]) - if "path" in emulator_properties: - new_config["qemu_path"] = emulator_properties["path"] + if "path" in template_properties: + new_config["qemu_path"] = template_properties["path"] else: - new_config["qemu_path"] = "qemu-system-{}".format(emulator_properties["arch"]) + if self._registry_version >= 8: + # the "arch" field was replaced by the "platform" field in registry version 8 + new_config["qemu_path"] = "qemu-system-{}".format(template_properties["platform"]) + else: + new_config["qemu_path"] = "qemu-system-{}".format(template_properties["arch"]) if "first_port_name" in appliance_config: new_config["first_port_name"] = appliance_config["first_port_name"] @@ -157,28 +161,28 @@ def _add_qemu_config(self, new_config, emulator_properties, appliance_config): if "linked_clone" in appliance_config: new_config["linked_clone"] = appliance_config["linked_clone"] - def _add_docker_config(self, new_config, emulator_properties, appliance_config): + def _add_docker_config(self, new_config, template_properties, appliance_config): new_config["template_type"] = "docker" - new_config.update(emulator_properties) + new_config.update(template_properties) if "custom_adapters" in appliance_config: new_config["custom_adapters"] = appliance_config["custom_adapters"] - def _add_dynamips_config(self, new_config, emulator_properties, appliance_config): + def _add_dynamips_config(self, new_config, template_properties, appliance_config): new_config["template_type"] = "dynamips" - new_config.update(emulator_properties) + new_config.update(template_properties) for image in appliance_config["images"]: new_config[image["type"]] = self._relative_image_path("IOS", image["path"]) if self._registry_version < 8: new_config["idlepc"] = image.get("idlepc", "") - def _add_iou_config(self, new_config, emulator_properties, appliance_config): + def _add_iou_config(self, new_config, template_properties, appliance_config): new_config["template_type"] = "iou" - new_config.update(emulator_properties) + new_config.update(template_properties) for image in appliance_config["images"]: if "path" not in image: raise ConfigException("Disk image is missing") From 1133ee6e1b90f9587b337a38874860a8a4eb8bac Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 9 Aug 2023 22:15:15 +1000 Subject: [PATCH 06/17] Development on v2.2.43.dev1 --- gns3/version.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gns3/version.py b/gns3/version.py index 10e31e8c6..d997d415c 100644 --- a/gns3/version.py +++ b/gns3/version.py @@ -23,8 +23,9 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.42" -__version_info__ = (2, 2, 42, 0) +__version__ = "2.2.43.dev1" + +__version_info__ = (2, 2, 43, 99) if "dev" in __version__: try: import os From 01deb01e6aa481cfb5d26b6ffc36cb9db4222087 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 11 Aug 2023 18:13:41 +1000 Subject: [PATCH 07/17] Upgrade to PyQt 5.15.9 and pywin32 --- mac-requirements.txt | 2 +- win-requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mac-requirements.txt b/mac-requirements.txt index 449038cdd..c0f718542 100644 --- a/mac-requirements.txt +++ b/mac-requirements.txt @@ -1,3 +1,3 @@ -rrequirements.txt -PyQt5==5.15.7 +PyQt5==5.15.9 diff --git a/win-requirements.txt b/win-requirements.txt index b2ae314d3..4af4a9fc5 100644 --- a/win-requirements.txt +++ b/win-requirements.txt @@ -1,4 +1,4 @@ -rrequirements.txt -PyQt5==5.15.7 # pyup: ignore -pywin32==305 # pyup: ignore +PyQt5==5.15.9 # pyup: ignore +pywin32==306 # pyup: ignore From e2168a3c81e0008a4cb4a0f17a2d23555fb3f4a5 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 12 Aug 2023 17:20:33 +1000 Subject: [PATCH 08/17] Use importlib instead of pkg_resources --- gns3/main.py | 10 ---- .../pages/iou_device_configuration_page.py | 1 + gns3/registry/appliance.py | 2 +- gns3/utils/get_resource.py | 53 +++++++------------ requirements.txt | 1 + 5 files changed, 22 insertions(+), 45 deletions(-) diff --git a/gns3/main.py b/gns3/main.py index f7b21e96b..80ff45578 100644 --- a/gns3/main.py +++ b/gns3/main.py @@ -30,16 +30,6 @@ except Exception as e: print("Fail update installation: {}".format(str(e))) - -# WARNING -# Due to buggy user machines we choose to put this as the first loading modules -# otherwise the egg cache is initialized in his standard location and -# if is not writetable the application crash. It's the user fault -# because one day the user as used sudo to run an egg and break his -# filesystem permissions, but it's a common mistake. -from gns3.utils.get_resource import get_resource - - import datetime import traceback import time diff --git a/gns3/modules/iou/pages/iou_device_configuration_page.py b/gns3/modules/iou/pages/iou_device_configuration_page.py index e22d0629f..4a712056d 100644 --- a/gns3/modules/iou/pages/iou_device_configuration_page.py +++ b/gns3/modules/iou/pages/iou_device_configuration_page.py @@ -57,6 +57,7 @@ def __init__(self): self.uiPrivateConfigToolButton.hide() # location of the base config templates + # FIXME: this does not work self._base_iou_l2_config_template = get_resource(os.path.join("configs", "iou_l2_base_startup-config.txt")) self._base_iou_l3_config_template = get_resource(os.path.join("configs", "iou_l3_base_startup-config.txt")) self._default_configs_dir = LocalServer.instance().localServerSettings()["configs_path"] diff --git a/gns3/registry/appliance.py b/gns3/registry/appliance.py index 895a305ba..c142a362a 100644 --- a/gns3/registry/appliance.py +++ b/gns3/registry/appliance.py @@ -62,7 +62,7 @@ def _check_config(self): if self._appliance["registry_version"] > 7: raise ApplianceError("Please update GNS3 in order to install this appliance") - with open(get_resource(os.path.join("schemas", "appliance.json"))) as f: + with open(get_resource("schemas/appliance.json")) as f: schema = json.load(f) v = jsonschema.Draft4Validator(schema) try: diff --git a/gns3/utils/get_resource.py b/gns3/utils/get_resource.py index d3f80d915..9f17b9b49 100644 --- a/gns3/utils/get_resource.py +++ b/gns3/utils/get_resource.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2014 GNS3 Technologies Inc. +# Copyright (C) 2023 GNS3 Technologies Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -15,50 +15,35 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import sys -import os -import tempfile -import pkg_resources import atexit import logging - -log = logging.getLogger(__name__) +import os +import sys try: - egg_cache_dir = tempfile.mkdtemp() - pkg_resources.set_extraction_path(egg_cache_dir) -except ValueError: - # If the path is already set the module throw an error - pass + import importlib_resources +except ImportError: + from importlib import resources as importlib_resources -@atexit.register -def clean_egg_cache(): - try: - import shutil - shutil.rmtree(egg_cache_dir, ignore_errors=True) - except Exception: - # We don't care if we can not cleanup - pass +from contextlib import ExitStack +resource_manager = ExitStack() +atexit.register(resource_manager.close) + +log = logging.getLogger(__name__) def get_resource(resource_name): + """ + Return a resource in current directory or in frozen package + """ resource_path = None if hasattr(sys, "frozen"): resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), resource_name)) - if sys.platform.startswith("darwin") and not os.path.exists(resource_path): - resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), "lib", resource_name)) - elif not hasattr(sys, "frozen"): - if pkg_resources.resource_exists("gns3", resource_name): - try: - resource_path = pkg_resources.resource_filename("gns3", resource_name) - except pkg_resources.ExtractionError as e: - log.fatal(e) - sys.stderr.write(e) - sys.exit(1) - resource_path = os.path.normpath(resource_path) - else: - resource_path = os.path.dirname(os.path.realpath(__file__)) - resource_path = os.path.join(resource_path, "..", "..", "resources", resource_name) + else: + ref = importlib_resources.files("gns3") / resource_name + path = resource_manager.enter_context(importlib_resources.as_file(ref)) + if os.path.exists(path): + resource_path = os.path.normpath(path) return resource_path diff --git a/requirements.txt b/requirements.txt index 0fde69f5f..102815e55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,6 @@ sentry-sdk==1.29.2,<1.30 psutil==5.9.5 distro>=1.8.0 truststore>=0.7.0; python_version >= '3.10' +importlib-resources>=1.3; python_version <= '3.9' setuptools>=60.8.1; python_version >= '3.7' setuptools==59.6.0; python_version < '3.7' # v59.6.0 is the last version to support Python 3.6 From 9243083321c13031f9a8a367ec006810ac25a0d4 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 12 Aug 2023 17:47:48 +1000 Subject: [PATCH 09/17] Upgrade dependencies --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 102815e55..eda9dd28c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -jsonschema>=4.17.3,<4.18; python_version >= '3.7' +jsonschema>=4.19.0,<4.20; python_version >= '3.7' jsonschema==3.2.0; python_version < '3.7' # v3.2.0 is the last version to support Python 3.6 sentry-sdk==1.29.2,<1.30 psutil==5.9.5 From 2b7840279a52cd9bb96cbeea35b6b481e5250ce6 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 12 Aug 2023 17:51:24 +1000 Subject: [PATCH 10/17] Downgrade jsonschema --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eda9dd28c..8aae72d09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -jsonschema>=4.19.0,<4.20; python_version >= '3.7' +jsonschema>=4.17.3,<4.18; python_version >= '3.7' # v4.17.3 is the last version to support Python 3.7 jsonschema==3.2.0; python_version < '3.7' # v3.2.0 is the last version to support Python 3.6 sentry-sdk==1.29.2,<1.30 psutil==5.9.5 From 4a32ae9736e624606f1803db342552765750bc96 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 14 Aug 2023 12:01:09 +1000 Subject: [PATCH 11/17] Drop "kvm" field. --- gns3/schemas/appliance_v8.json | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/gns3/schemas/appliance_v8.json b/gns3/schemas/appliance_v8.json index 7598052bd..c66c6db3e 100644 --- a/gns3/schemas/appliance_v8.json +++ b/gns3/schemas/appliance_v8.json @@ -528,14 +528,6 @@ "type": "string", "title": "Command line parameters send to the kernel" }, - "kvm": { - "title": "KVM requirements", - "enum": [ - "require", - "allow", - "disable" - ] - }, "options": { "type": "string", "title": "Optional additional qemu command line options" @@ -580,8 +572,7 @@ "adapters", "ram", "platform", - "console_type", - "kvm" + "console_type" ] } }, From fef734bbbe6e2e4aba0353bd6507f1b3161d17a1 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 16 Aug 2023 00:30:02 +1000 Subject: [PATCH 12/17] Finalize appliance v8 support and add tests. --- gns3/dialogs/appliance_wizard.py | 12 +- gns3/registry/appliance.py | 8 +- gns3/registry/appliance_to_template.py | 68 ++++++---- gns3/schemas/appliance_v8.json | 34 ++--- tests/conftest.py | 2 +- .../registry/appliances/arista-veos-v8.gns3a | 50 ++++++++ tests/registry/appliances/arista-veos.gns3a | 4 +- .../appliances/broken-microcore-linux.gns3a | 3 - tests/registry/appliances/cisco-3745-v8.gns3a | 51 ++++++++ tests/registry/appliances/cisco-3745.gns3a | 3 +- .../registry/appliances/cisco-iou-l3-v8.gns3a | 41 ++++++ tests/registry/appliances/cisco-iou-l3.gns3a | 3 +- tests/registry/appliances/empty-vm-v8.gns3a | 115 +++++++++++++++++ tests/registry/appliances/juniper-vsrx.gns3a | 8 +- .../registry/appliances/microcore-linux.gns3a | 3 - .../registry/appliances/openvswitch-v8.gns3a | 24 ++++ tests/registry/appliances/openvswitch.gns3a | 1 + tests/registry/test_appliance.py | 62 ++++++++- tests/registry/test_appliance_to_template.py | 119 +++++++++++++++--- tests/test_http_client.py | 2 +- tests/test_image_manager.py | 2 +- tests/test_local_server.py | 2 +- tests/test_update_manager.py | 6 +- 23 files changed, 526 insertions(+), 97 deletions(-) create mode 100644 tests/registry/appliances/arista-veos-v8.gns3a create mode 100644 tests/registry/appliances/cisco-3745-v8.gns3a create mode 100644 tests/registry/appliances/cisco-iou-l3-v8.gns3a create mode 100644 tests/registry/appliances/empty-vm-v8.gns3a create mode 100644 tests/registry/appliances/openvswitch-v8.gns3a diff --git a/gns3/dialogs/appliance_wizard.py b/gns3/dialogs/appliance_wizard.py index 4f48f90b6..2dd49c189 100644 --- a/gns3/dialogs/appliance_wizard.py +++ b/gns3/dialogs/appliance_wizard.py @@ -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) @@ -591,8 +593,8 @@ def _install(self, version): if self._appliance["registry_version"] >= 8: if "settings" in appliance_configuration: for settings in appliance_configuration["settings"]: - if settings["emulator_type"] == "qemu": - settings["emulator_properties"]["path"] = self.uiQemuListComboBox.currentData() + if settings["template_type"] == "qemu": + settings["template_properties"]["path"] = self.uiQemuListComboBox.currentData() else: appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData() diff --git a/gns3/registry/appliance.py b/gns3/registry/appliance.py index 3ee517bdb..cf3de56ef 100644 --- a/gns3/registry/appliance.py +++ b/gns3/registry/appliance.py @@ -74,7 +74,7 @@ def _check_config(self): else: appliance_file = "appliance.json" - with open(get_resource("schemas/appliance.json")) as f: + with open(get_resource("schemas/{}".format(appliance_file))) as f: schema = json.load(f) v = jsonschema.Draft4Validator(schema) try: @@ -161,7 +161,7 @@ def search_images_for_version(self, version_name): 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.get("checksum") + checksum = image.pop("checksum") img = self._registry.search_image_file(self.template_type(), image["filename"], checksum, image.get("filesize")) if img is None: @@ -173,7 +173,7 @@ def search_images_for_version(self, version_name): image["path"] = img.path image["location"] = img.location - if not checksum: + if "md5sum" not in image: image["md5sum"] = img.md5sum image["filesize"] = img.filesize @@ -217,6 +217,8 @@ def template_type(self): 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: diff --git a/gns3/registry/appliance_to_template.py b/gns3/registry/appliance_to_template.py index 244a5dc00..f3f6e2cb9 100644 --- a/gns3/registry/appliance_to_template.py +++ b/gns3/registry/appliance_to_template.py @@ -76,18 +76,29 @@ def new_template(self, appliance_config, server, appliance_version=None, control new_template["symbol"] = ":/symbols/firewall.svg" if self._registry_version >= 8: - for version in appliance_config["versions"]: - if appliance_version and version["name"] == appliance_version: - settings = self._get_settings(appliance_config, version.get("settings")) - template_type = settings["template_type"] - if template_type == "qemu": - self._add_qemu_config(new_template, settings["template_properties"], appliance_config) - elif template_type == "iou": - self._add_iou_config(new_template, settings["template_properties"], appliance_config) - elif template_type == "dynamips": - self._add_dynamips_config(new_template, settings["template_properties"], appliance_config) - elif template_type == "docker": - self._add_docker_config(new_template, settings["template_properties"], appliance_config) + if appliance_version: + for version in appliance_config["versions"]: + if appliance_version and version["name"] == appliance_version: + # inject "usage", "category" and "symbol" specified at the version + # level into the template properties + usage = version.get("usage") + if usage: + new_template["usage"] = usage + new_template["symbol"] = version.get("symbol", new_template["symbol"]) + new_template["category"] = version.get("category", new_template["category"]) + settings = self._get_settings(appliance_config, version.get("settings")) + template_type = settings["template_type"] + if template_type == "qemu": + self._add_qemu_config(new_template, settings["template_properties"], appliance_config) + elif template_type == "iou": + self._add_iou_config(new_template, settings["template_properties"], appliance_config) + elif template_type == "dynamips": + self._add_dynamips_config(new_template, settings["template_properties"], appliance_config) + else: + # docker appliances have no version + settings = self._get_settings(appliance_config) + if settings["template_type"] == "docker": + self._add_docker_config(new_template, settings["template_properties"], appliance_config) else: if "qemu" in appliance_config: self._add_qemu_config(new_template, appliance_config["qemu"], appliance_config) @@ -104,17 +115,28 @@ def new_template(self, appliance_config, server, appliance_version=None, control def _get_settings(self, appliance_config, settings_name=None): - # first look for specific settings set if provided + default_settings = None + # first look for default settings, if any ('default' = true, first set that has it) + for settings in appliance_config["settings"]: + if settings.get("default", False): + default_settings = settings + break + + # then look for specific settings set if a name is provided if settings_name: for settings in appliance_config["settings"]: if settings.get("name") == settings_name: + if settings.get("inherit_default_properties", True) and \ + default_settings and default_settings["template_type"] == settings["template_type"]: + default_settings["template_properties"].update(settings["template_properties"]) + return default_settings return settings - raise ConfigException("Settings {} cannot be found", settings_name) + raise ConfigException("Settings '{}' cannot be found in the appliance file", settings_name) + elif default_settings: + return default_settings - # then look for specified default settings ('default' = true) - for settings in appliance_config["settings"]: - if settings.get("default", False): - return settings + if not appliance_config.get("settings"): + raise ConfigException("No settings found in the appliance file") # if no default settings are specified, use the first available settings set return appliance_config["settings"][0] @@ -125,14 +147,16 @@ def _add_qemu_config(self, new_config, template_properties, appliance_config): new_config.update(template_properties) # the following properties are not valid for a template - new_config.pop("kvm", None) - new_config.pop("path", None) - new_config.pop("arch", None) + new_config.pop("kvm", None) # To check KVM setting against the server capabilities + new_config.pop("path", None) # Qemu binary selected in previous step + new_config.pop("arch", None) # Used for selecting the Qemu binary options = template_properties.get("options", "") if template_properties.get("kvm", "allow") == "disable" and "-machine accel=tcg" not in options: options += " -machine accel=tcg" - new_config["options"] = options.strip() + options = options.strip() + if options: + new_config["options"] = options for image in appliance_config["images"]: if image.get("path"): diff --git a/gns3/schemas/appliance_v8.json b/gns3/schemas/appliance_v8.json index c66c6db3e..4fb0db7b6 100644 --- a/gns3/schemas/appliance_v8.json +++ b/gns3/schemas/appliance_v8.json @@ -14,6 +14,7 @@ "docker_properties": { "type": "object", "title": "Docker template properties", + "additionalProperties": false, "properties": { "name": { "type": "string", @@ -83,13 +84,13 @@ } }, "required": [ - "adapters", "image" ] }, "iou_properties": { "type": "object", "title": "IOU template properties", + "additionalProperties": false, "properties": { "name": { "type": "string", @@ -131,14 +132,7 @@ "type": "string", "title": "Config loaded at startup" } - }, - "required": [ - "ethernet_adapters", - "serial_adapters", - "nvram", - "ram", - "startup_config" - ] + } }, "dynamips_slot": { "enum": [ @@ -181,6 +175,7 @@ "dynamips_properties": { "type": "object", "title": "Dynamips template properties", + "additionalProperties": false, "properties": { "name": { "type": "string", @@ -305,9 +300,7 @@ } }, "required": [ - "platform", - "ram", - "nvram" + "platform" ] }, "qemu_disk_interfaces": { @@ -325,6 +318,7 @@ "qemu_properties": { "type": "object", "title": "Qemu template properties", + "additionalProperties": false, "properties": { "name": { "type": "string", @@ -566,14 +560,7 @@ "null" ] } - }, - "required": [ - "adapter_type", - "adapters", - "ram", - "platform", - "console_type" - ] + } } }, "properties": { @@ -686,9 +673,9 @@ "type": "boolean", "title": "Whether these are the default settings" }, - "inherit_default_settings": { + "inherit_default_properties": { "type": "boolean", - "title": "Whether the default settings should be used", + "title": "Whether the default properties should be used", "default": "true" }, "template_type": { @@ -905,6 +892,7 @@ "registry_version", "status", "maintainer", - "maintainer_email" + "maintainer_email", + "settings" ] } diff --git a/tests/conftest.py b/tests/conftest.py index ff45f2eae..0b2c28f11 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -172,7 +172,7 @@ def local_server_config(): return LocalServerConfig.instance() -@pytest.yield_fixture(autouse=True) +@pytest.fixture(autouse=True) def run_around_tests(local_config, main_window): """ This setup a temporay environnement around tests diff --git a/tests/registry/appliances/arista-veos-v8.gns3a b/tests/registry/appliances/arista-veos-v8.gns3a new file mode 100644 index 000000000..9a3c4911e --- /dev/null +++ b/tests/registry/appliances/arista-veos-v8.gns3a @@ -0,0 +1,50 @@ +{ + "appliance_id": "1c784362-8aaf-4312-b0f5-4b138cf2e25b", + "name": "Arista vEOS", + "category": "router", + "description": "Arista EOSĀ® is the core of Arista cloud networking solutions for next-generation data centers and cloud networks. Cloud architectures built with Arista EOS scale to tens of thousands of compute and storage nodes with management and provisioning capabilities that work at scale. Through its programmability, EOS enables a set of software applications that deliver workflow automation, high availability, unprecedented network visibility and analytics and rapid integration with a wide range of third-party applications for virtualization, management, automation and orchestration services.\n\nArista Extensible Operating System (EOS) is a fully programmable and highly modular, Linux-based network operation system, using familiar industry standard CLI and runs a single binary software image across the Arista switching family. Architected for resiliency and programmability, EOS has a unique multi-process state sharing architecture that separates state information and packet forwarding from protocol processing and application logic.", + "vendor_name": "Arista", + "vendor_url": "http://www.arista.com/", + "documentation_url": "http://www.arista.com/docs/Manuals/ConfigGuide.pdf", + "product_name": "Arista vEOS", + "product_url": "https://eos.arista.com/", + "registry_version": 8, + "status": "stable", + "maintainer": "GNS3 Team", + "maintainer_email": "developers@gns3.net", + "settings": [ + { + "template_type": "qemu", + "template_properties": { + "adapter_type": "e1000", + "adapters": 8, + "ram": 2048, + "platform": "x86_64", + "console_type": "telnet" + } + } + ], + "images": [ + { + "filename": "Aboot-veos-serial-2.1.0.iso", + "version": "2.1.0", + "md5sum": "2687534f2ff11b998dec0511066457c0", + "download_url": "https://www.arista.com/en/support/software-download" + }, + { + "filename": "vEOS-lab-4.13.8M.vmdk", + "version": "4.13.8M", + "md5sum": "a47145b9e6e7a24171c0850f8755535e", + "download_url": "https://www.arista.com/en/support/software-download" + } + ], + "versions": [ + { + "name": "4.13.8M", + "images": { + "hda_disk_image": "Aboot-veos-serial-2.1.0.iso", + "hdb_disk_image": "vEOS-lab-4.13.8M.vmdk" + } + } + ] +} diff --git a/tests/registry/appliances/arista-veos.gns3a b/tests/registry/appliances/arista-veos.gns3a index c77dbf5c5..018ef2703 100644 --- a/tests/registry/appliances/arista-veos.gns3a +++ b/tests/registry/appliances/arista-veos.gns3a @@ -1,4 +1,5 @@ { + "appliance_id": "1c784362-8aaf-4312-b0f5-4b138cf2e25b", "name": "Arista vEOS", "category": "router", "description": "Arista EOSĀ® is the core of Arista cloud networking solutions for next-generation data centers and cloud networks. Cloud architectures built with Arista EOS scale to tens of thousands of compute and storage nodes with management and provisioning capabilities that work at scale. Through its programmability, EOS enables a set of software applications that deliver workflow automation, high availability, unprecedented network visibility and analytics and rapid integration with a wide range of third-party applications for virtualization, management, automation and orchestration services.\n\nArista Extensible Operating System (EOS) is a fully programmable and highly modular, Linux-based network operation system, using familiar industry standard CLI and runs a single binary software image across the Arista switching family. Architected for resiliency and programmability, EOS has a unique multi-process state sharing architecture that separates state information and packet forwarding from protocol processing and application logic.", @@ -11,7 +12,6 @@ "status": "stable", "maintainer": "GNS3 Team", "maintainer_email": "developers@gns3.net", - "qemu": { "adapter_type": "e1000", "adapters": 8, @@ -19,7 +19,6 @@ "arch": "x86_64", "console_type": "telnet" }, - "images": [ { "filename": "Aboot-veos-serial-2.1.0.iso", @@ -34,7 +33,6 @@ "download_url": "https://www.arista.com/en/support/software-download" } ], - "versions": [ { "name": "4.13.8M", diff --git a/tests/registry/appliances/broken-microcore-linux.gns3a b/tests/registry/appliances/broken-microcore-linux.gns3a index 061ad94c4..a4c61b6d5 100644 --- a/tests/registry/appliances/broken-microcore-linux.gns3a +++ b/tests/registry/appliances/broken-microcore-linux.gns3a @@ -11,7 +11,6 @@ "status": "stable", "maintainer": "GNS3 Team", "maintainer_email": "developers@gns3.net", - "qemu": { "adapter_type": "e1000", "adapters": 1, @@ -19,7 +18,6 @@ "arch": "i386", "console_type": "telnet" }, - "images": [ { "filename": "linux-microcore-3.4.1.img", @@ -30,7 +28,6 @@ "direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/linux-microcore-3.4.1.img" } ], - "versions": [ { "name": "3.4.1", diff --git a/tests/registry/appliances/cisco-3745-v8.gns3a b/tests/registry/appliances/cisco-3745-v8.gns3a new file mode 100644 index 000000000..e3da0805c --- /dev/null +++ b/tests/registry/appliances/cisco-3745-v8.gns3a @@ -0,0 +1,51 @@ +{ + "appliance_id": "96dac9ff-581c-4262-a9f0-5c68890d049e", + "category": "router", + "status": "experimental", + "maintainer": "GNS3 Team", + "name": "Cisco 3745", + "vendor_name": "Cisco", + "product_name": "3745", + "vendor_url": "http://www.cisco.com", + "description": "Cisco 3745 Multiservice Access Router", + "registry_version": 8, + "maintainer_email": "developers@gns3.net", + "documentation_url": "http://www.cisco.com/c/en/us/support/routers/3745-multiservice-access-router/model.html", + "settings": [ + { + "template_type": "dynamips", + "template_properties": { + "chassis": "", + "platform": "c3745", + "ram": 256, + "nvram": 256, + "startup_config": "ios_base_startup-config.txt", + "slot0": "GT96100-FE", + "slot1": "NM-1FE-TX", + "slot2": "NM-4T", + "slot3": "", + "slot4": "", + "wic0": "WIC-1T", + "wic1": "WIC-1T", + "wic2": "WIC-1T", + "idlepc": "0x60aa1da0" + } + } + ], + "versions": [ + { + "name": "124-25d", + "images": { + "image": "c3745-adventerprisek9-mz.124-25d.image" + } + } + ], + "images": [ + { + "filesize": 82053028, + "md5sum": "ddbaf74274822b50fa9670e10c75b08f", + "version": "124-25d", + "filename": "c3745-adventerprisek9-mz.124-25d.image" + } + ] +} diff --git a/tests/registry/appliances/cisco-3745.gns3a b/tests/registry/appliances/cisco-3745.gns3a index aa9e5c813..b3707cec9 100644 --- a/tests/registry/appliances/cisco-3745.gns3a +++ b/tests/registry/appliances/cisco-3745.gns3a @@ -1,4 +1,5 @@ { + "appliance_id": "96dac9ff-581c-4262-a9f0-5c68890d049e", "category": "router", "status": "experimental", "maintainer": "GNS3 Team", @@ -10,7 +11,6 @@ "registry_version": 2, "maintainer_email": "developers@gns3.net", "documentation_url": "http://www.cisco.com/c/en/us/support/routers/3745-multiservice-access-router/model.html", - "dynamips": { "chassis": "", "platform": "c3745", @@ -26,7 +26,6 @@ "wic1": "WIC-1T", "wic2": "WIC-1T" }, - "versions": [ { "images": { diff --git a/tests/registry/appliances/cisco-iou-l3-v8.gns3a b/tests/registry/appliances/cisco-iou-l3-v8.gns3a new file mode 100644 index 000000000..beea98564 --- /dev/null +++ b/tests/registry/appliances/cisco-iou-l3-v8.gns3a @@ -0,0 +1,41 @@ +{ + "appliance_id": "a8f5935d-7229-4b32-8a01-24ef41758f2f", + "category": "router", + "status": "experimental", + "maintainer": "GNS3 Team", + "name": "Cisco IOU L3", + "vendor_name": "Cisco", + "product_name": "Cisco IOU L3", + "vendor_url": "http://www.cisco.com", + "description": "Cisco IOS on UNIX Layer 3 image.", + "registry_version": 8, + "maintainer_email": "developers@gns3.net", + "settings": [ + { + "template_type": "iou", + "template_properties": { + "ethernet_adapters": 2, + "serial_adapters": 2, + "nvram": 128, + "ram": 256, + "startup_config": "iou_l3_base_startup-config.txt" + } + } + ], + "versions": [ + { + "images": { + "image": "i86bi-linux-l3-adventerprisek9-15.4.1T.bin" + }, + "name": "15.4.1T" + } + ], + "images": [ + { + "filesize": 152677848, + "md5sum": "5d41402abc4b2a76b9719d911017c592", + "version": "15.4.1T", + "filename": "i86bi-linux-l3-adventerprisek9-15.4.1T.bin" + } + ] +} diff --git a/tests/registry/appliances/cisco-iou-l3.gns3a b/tests/registry/appliances/cisco-iou-l3.gns3a index 44b6e25c9..bb2cd39c7 100644 --- a/tests/registry/appliances/cisco-iou-l3.gns3a +++ b/tests/registry/appliances/cisco-iou-l3.gns3a @@ -1,4 +1,5 @@ { + "appliance_id": "a8f5935d-7229-4b32-8a01-24ef41758f2f", "category": "router", "status": "experimental", "maintainer": "GNS3 Team", @@ -9,7 +10,6 @@ "description": "Cisco IOS on UNIX Layer 3 image.", "registry_version": 2, "maintainer_email": "developers@gns3.net", - "iou": { "ethernet_adapters": 2, "serial_adapters": 2, @@ -17,7 +17,6 @@ "ram": 256, "startup_config": "iou_l3_base_startup-config.txt" }, - "versions": [ { "images": { diff --git a/tests/registry/appliances/empty-vm-v8.gns3a b/tests/registry/appliances/empty-vm-v8.gns3a new file mode 100644 index 000000000..8e1cc179d --- /dev/null +++ b/tests/registry/appliances/empty-vm-v8.gns3a @@ -0,0 +1,115 @@ +{ + "appliance_id": "1cfdf900-7c30-4cb7-8f03-3f61d2581633", + "name": "Empty VM", + "category": "guest", + "description": "A empty VM with empty hard disks 8G, 30G, 100G & 200G.", + "vendor_name": "GNS3", + "vendor_url": "https://gns3.com", + "documentation_url": "", + "product_name": "QEMU", + "product_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/", + "registry_version": 8, + "status": "experimental", + "maintainer": "GNS3 Team", + "maintainer_email": "developers@gns3.net", + "usage": "Default at first boot the VM will start from the cdrom.", + "settings": [ + { + "default": true, + "template_type": "qemu", + "template_properties": { + "adapter_type": "e1000", + "adapters": 1, + "ram": 1024, + "hda_disk_interface": "sata", + "platform": "x86_64", + "console_type": "vnc", + "boot_priority": "d" + } + }, + { + "name": "i386 settings", + "template_type": "qemu", + "template_properties": { + "platform": "i386", + "adapters": 8 + } + }, + { + "name": "ARM settings", + "inherit_default_properties": false, + "template_type": "qemu", + "template_properties": { + "platform": "arm", + "ram": 512 + } + } + ], + "images": [ + { + "filename": "empty8G.qcow2", + "version": "8G", + "md5sum": "f1d2c25b6990f99bd05b433ab603bdb4", + "filesize": 197120, + "download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/", + "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty8G.qcow2/download" + }, + { + "filename": "empty30G.qcow2", + "version": "30G", + "checksum": "3411a599e822f2ac6be560a26405821a", + "filesize": 197120, + "download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/", + "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty30G.qcow2/download" + }, + { + "filename": "empty100G.qcow2", + "version": "100G", + "checksum": "d08fdec95fffbda3f04e9a00db49295df73ae4a507396e442ba9e4ad5c14ce5a", + "checksum_type": "sha256", + "filesize": 198656, + "download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/", + "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty100G.qcow2/download" + }, + { + "filename": "empty200G.qcow2", + "version": "200G", + "md5sum": "d1686d2f25695dee32eab9a6f4652c7c", + "filesize": 200192, + "download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/", + "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty200G.qcow2/download" + } + ], + "versions": [ + { + "name": "8G", + "images": { + "hda_disk_image": "empty8G.qcow2" + } + }, + { + "name": "30G", + "settings": "i386 settings", + "images": { + "hda_disk_image": "empty30G.qcow2" + } + }, + { + "name": "100G", + "settings": "ARM settings", + "images": { + "hda_disk_image": "empty100G.qcow2" + } + }, + { + "name": "200G", + "settings": "ARM settings", + "usage": "This is how to use this version", + "symbol": "ethernet_switch", + "category": "switch", + "images": { + "hda_disk_image": "empty200G.qcow2" + } + } + ] +} diff --git a/tests/registry/appliances/juniper-vsrx.gns3a b/tests/registry/appliances/juniper-vsrx.gns3a index 705708e47..293a6b96e 100644 --- a/tests/registry/appliances/juniper-vsrx.gns3a +++ b/tests/registry/appliances/juniper-vsrx.gns3a @@ -3,8 +3,6 @@ "vendor_name": "Juniper", "product_name": "vSRX", "name": "vSRX", - - "description": "The vSRX delivers core firewall, networking, advanced security, and automated lifecycle management capabilities for enterprises and service providers. The industry\u2019s fastest virtual security platform, the vSRX offers firewall speeds up to 17 Gbps using only two virtual CPUs, providing scalable, secure protection across private, public, and hybrid clouds.", "maintainer_email": "developers@gns3.net", "documentation_url": "http://www.juniper.net/techpubs/", @@ -13,7 +11,6 @@ "status": "experimental", "vendor_url": "https://www.juniper.net", "registry_version": 1, - "qemu": { "console_type": "telnet", "ram": 2000, @@ -23,8 +20,6 @@ "options": "-smp 2", "kvm": "require" }, - - "images": [ { "version": "12.1X47.4-domestic", @@ -34,7 +29,6 @@ "download_url": "https://www.juniper.net/us/en/dm/free-vsrx-trial/" } ], - "versions": [ { "name": "12.1X47.4-domestic", @@ -43,4 +37,4 @@ } } ] - } +} diff --git a/tests/registry/appliances/microcore-linux.gns3a b/tests/registry/appliances/microcore-linux.gns3a index 096a8bcba..c943b9c27 100644 --- a/tests/registry/appliances/microcore-linux.gns3a +++ b/tests/registry/appliances/microcore-linux.gns3a @@ -12,7 +12,6 @@ "maintainer": "GNS3 Team", "maintainer_email": "developers@gns3.net", "usage": "Just start the appliance", - "qemu": { "adapter_type": "e1000", "adapters": 1, @@ -21,7 +20,6 @@ "console_type": "telnet", "kvm": "allow" }, - "images": [ { "filename": "linux-microcore-3.4.1.img", @@ -40,7 +38,6 @@ "direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/linux-microcore-4.0.2-clean.img" } ], - "versions": [ { "name": "3.4.1", diff --git a/tests/registry/appliances/openvswitch-v8.gns3a b/tests/registry/appliances/openvswitch-v8.gns3a new file mode 100644 index 000000000..ac90eeb44 --- /dev/null +++ b/tests/registry/appliances/openvswitch-v8.gns3a @@ -0,0 +1,24 @@ +{ + "appliance_id": "40c84186-752f-4a71-b6fb-961ba36e8cf7", + "name": "Open vSwitch", + "category": "multilayer_switch", + "description": "Open vSwitch is a production quality, multilayer virtual switch licensed under the open source Apache 2.0 license. It is designed to enable massive network automation through programmatic extension, while still supporting standard management interfaces and protocols (e.g. NetFlow, sFlow, IPFIX, RSPAN, CLI, LACP, 802.1ag). In addition, it is designed to support distribution across multiple physical servers similar to VMware's vNetwork distributed vswitch or Cisco's Nexus 1000V.", + "vendor_name": "Open vSwitch", + "vendor_url": "http://openvswitch.org/", + "documentation_url": "http://openvswitch.org/support/", + "product_name": "Open vSwitch", + "registry_version": 8, + "status": "stable", + "maintainer": "GNS3 Team", + "maintainer_email": "developers@gns3.net", + "usage": "By default all interfaces are connected to the br0", + "settings": [ + { + "template_type": "docker", + "template_properties": { + "adapters": 16, + "image": "gns3/openvswitch:latest" + } + } + ] +} diff --git a/tests/registry/appliances/openvswitch.gns3a b/tests/registry/appliances/openvswitch.gns3a index 73ceaeb4c..15c69b869 100644 --- a/tests/registry/appliances/openvswitch.gns3a +++ b/tests/registry/appliances/openvswitch.gns3a @@ -1,4 +1,5 @@ { + "appliance_id": "40c84186-752f-4a71-b6fb-961ba36e8cf7", "name": "Open vSwitch", "category": "multilayer_switch", "description": "Open vSwitch is a production quality, multilayer virtual switch licensed under the open source Apache 2.0 license. It is designed to enable massive network automation through programmatic extension, while still supporting standard management interfaces and protocols (e.g. NetFlow, sFlow, IPFIX, RSPAN, CLI, LACP, 802.1ag). In addition, it is designed to support distribution across multiple physical servers similar to VMware's vNetwork distributed vswitch or Cisco's Nexus 1000V.", diff --git a/tests/registry/test_appliance.py b/tests/registry/test_appliance.py index 784c1580f..d52a1684f 100644 --- a/tests/registry/test_appliance.py +++ b/tests/registry/test_appliance.py @@ -202,6 +202,62 @@ def test_create_new_version(): os.remove(wrong_appliance_file) -def test_emulator(): - assert Appliance(registry, os.path.abspath("tests/registry/appliances/microcore-linux.gns3a")).emulator() == "qemu" - assert Appliance(registry, os.path.abspath("tests/registry/appliances/cisco-iou-l3.gns3a")).emulator() == "iou" +def test_template_type(): + assert Appliance(registry, os.path.abspath("tests/registry/appliances/microcore-linux.gns3a")).template_type() == "qemu" + assert Appliance(registry, os.path.abspath("tests/registry/appliances/cisco-iou-l3.gns3a")).template_type() == "iou" + + +def test_checksum_in_appliance_format_v8(registry, images_dir): + + path_empty_8g = os.path.join(images_dir, "QEMU", "empty8G.qcow2") + with open(path_empty_8g, 'w+') as f: + f.write("hello") + + appliance = Appliance(registry, os.path.abspath("tests/registry/appliances/empty-vm-v8.gns3a")) + appliance._appliance['versions'][0]['images']['hda_disk_image']['filesize'] = 5 + appliance._appliance['versions'][0]['images']['hda_disk_image']['md5sum'] = "5d41402abc4b2a76b9719d911017c592" + + detected = appliance.search_images_for_version("8G") + assert detected["images"][0]["md5sum"] == "5d41402abc4b2a76b9719d911017c592" + assert detected["images"][0]["filesize"] == 5 + + path_empty_30g = os.path.join(images_dir, "QEMU", "empty30G.qcow2") + os.rename(path_empty_8g, path_empty_30g) + appliance._appliance['versions'][1]['images']['hda_disk_image']['filesize'] = 5 + appliance._appliance['versions'][1]['images']['hda_disk_image']['checksum'] = "5d41402abc4b2a76b9719d911017c592" + detected = appliance.search_images_for_version("30G") + assert detected["images"][0]["md5sum"] == "5d41402abc4b2a76b9719d911017c592" + assert detected["images"][0]["filesize"] == 5 + + with pytest.raises(ApplianceError): + appliance.search_images_for_version("100G") + + +def test_multiple_different_template_types_found(): + + appliance_path = "tests/registry/appliances/empty-vm-v8.gns3a" + wrong_appliance_fp, wrong_appliance_file = tempfile.mkstemp() + + with open(appliance_path, encoding='utf-8') as f: + appliance = json.loads(f.read()) + appliance["settings"][0]["template_type"] = "dynamips" + os.write(wrong_appliance_fp, json.dumps(appliance).encode()) + os.close(wrong_appliance_fp) + + with pytest.raises(ApplianceError): + Appliance(registry, wrong_appliance_file).template_type() + + +def test_appliance_without_settings(): + + appliance_path = "tests/registry/appliances/empty-vm-v8.gns3a" + wrong_appliance_fp, wrong_appliance_file = tempfile.mkstemp() + + with open(appliance_path, encoding='utf-8') as f: + appliance = json.loads(f.read()) + del appliance["settings"] + os.write(wrong_appliance_fp, json.dumps(appliance).encode()) + os.close(wrong_appliance_fp) + + with pytest.raises(ApplianceError): + Appliance(registry, wrong_appliance_file).template_type() diff --git a/tests/registry/test_appliance_to_template.py b/tests/registry/test_appliance_to_template.py index 83ca60ca5..cc21e0c16 100644 --- a/tests/registry/test_appliance_to_template.py +++ b/tests/registry/test_appliance_to_template.py @@ -64,8 +64,15 @@ def empty_config(tmpdir, images_dir, symbols_dir, local_server_config): return Config(path) -def test_add_appliance_iou(iou_l3): - with open("tests/registry/appliances/cisco-iou-l3.gns3a", encoding="utf-8") as f: +@pytest.mark.parametrize( + "appliance_file", + [ + "cisco-iou-l3.gns3a", + "cisco-iou-l3-v8.gns3a" + ] +) +def test_add_appliance_iou(iou_l3, appliance_file): + with open("tests/registry/appliances/{}".format(appliance_file), encoding="utf-8") as f: config = json.load(f) config["images"] = [ { @@ -74,7 +81,7 @@ def test_add_appliance_iou(iou_l3): "path": iou_l3 } ] - new_template = ApplianceToTemplate().new_template(config, "local") + new_template = ApplianceToTemplate().new_template(config, "local", "15.4.1T") assert new_template == { "category": "router", "template_type": "iou", @@ -91,8 +98,15 @@ def test_add_appliance_iou(iou_l3): } -def test_add_appliance_docker(): - with open("tests/registry/appliances/openvswitch.gns3a", encoding="utf-8") as f: +@pytest.mark.parametrize( + "appliance_file", + [ + "openvswitch.gns3a", + "openvswitch-v8.gns3a" + ] +) +def test_add_appliance_docker(appliance_file): + with open("tests/registry/appliances/{}".format(appliance_file), encoding="utf-8") as f: config = json.load(f) new_template = ApplianceToTemplate().new_template(config, "local") @@ -108,8 +122,15 @@ def test_add_appliance_docker(): } -def test_add_appliance_dynamips(cisco_3745): - with open("tests/registry/appliances/cisco-3745.gns3a", encoding="utf-8") as f: +@pytest.mark.parametrize( + "appliance_file", + [ + "cisco-3745.gns3a", + "cisco-3745-v8.gns3a" + ] +) +def test_add_appliance_dynamips(cisco_3745, appliance_file): + with open("tests/registry/appliances/{}".format(appliance_file), encoding="utf-8") as f: config = json.load(f) config["images"] = [ { @@ -120,7 +141,7 @@ def test_add_appliance_dynamips(cisco_3745): } ] - new_template = ApplianceToTemplate().new_template(config, "local") + new_template = ApplianceToTemplate().new_template(config, "local", "124-25d") assert new_template == { "template_type": "dynamips", "category": "router", @@ -166,7 +187,6 @@ def test_add_appliance_guest(linux_microcore_img): "symbol": ":/symbols/qemu_guest.svg", "hda_disk_image": "linux-microcore-3.4.1.img", "name": "Micro Core Linux", - "options": "", "qemu_path": "qemu-system-i386", "usage": "Just start the appliance", "ram": 32, @@ -238,8 +258,15 @@ def test_add_appliance_with_boot_priority(linux_microcore_img): assert new_template["boot_priority"] == "dc" -def test_add_appliance_router_two_disk(images_dir): - with open("tests/registry/appliances/arista-veos.gns3a", encoding="utf-8") as f: +@pytest.mark.parametrize( + "appliance_file", + [ + "arista-veos.gns3a", + "arista-veos-v8.gns3a" + ] +) +def test_add_appliance_router_two_disk(images_dir, appliance_file): + with open("tests/registry/appliances/{}".format(appliance_file), encoding="utf-8") as f: config = json.load(f) config["images"] = [ @@ -255,8 +282,8 @@ def test_add_appliance_router_two_disk(images_dir): } ] - new_template = ApplianceToTemplate().new_template(config, "local") - assert new_template == { + new_template = ApplianceToTemplate().new_template(config, "local", "4.13.8M") + expected_result = { "template_type": "qemu", "adapter_type": "e1000", "adapters": 8, @@ -265,12 +292,76 @@ def test_add_appliance_router_two_disk(images_dir): "hda_disk_image": "a", "hdb_disk_image": "b", "name": "Arista vEOS", - "options": "", "qemu_path": "qemu-system-x86_64", "ram": 2048, "console_type": "telnet", "compute_id": "local" } + if "v8" in appliance_file: + expected_result["platform"] = "x86_64" # platform was added in v8 + assert new_template == expected_result + + +def test_add_appliance_v8_default_properties_inheritance(images_dir): + with open("tests/registry/appliances/empty-vm-v8.gns3a", encoding="utf-8") as f: + config = json.load(f) + + # check that default properties are used + new_template = ApplianceToTemplate().new_template(config, "local", "8G") + expected_result = { + "name": "Empty VM", + "template_type": "qemu", + "symbol": ":/symbols/qemu_guest.svg", + "category": "guest", + "adapter_type": "e1000", + "adapters": 1, + "ram": 1024, + "qemu_path": "qemu-system-x86_64", + "hda_disk_interface": "sata", + "platform": "x86_64", + "console_type": "vnc", + "boot_priority": "d", + "compute_id": "local", + "usage": "Default at first boot the VM will start from the cdrom." + } + assert new_template == expected_result + + # check that specific properties are used along with default properties + new_template = ApplianceToTemplate().new_template(config, "local", "30G") + expected_result.update( + { + "adapters": 8, + "qemu_path": "qemu-system-i386", + "platform": "i386", + } + ) + assert new_template == expected_result + + # check that specific properties are used along without default properties + new_template = ApplianceToTemplate().new_template(config, "local", "100G") + expected_result = { + "name": "Empty VM", + "template_type": "qemu", + "symbol": ":/symbols/qemu_guest.svg", + "category": "guest", + "ram": 512, + "qemu_path": "qemu-system-arm", + "platform": "arm", + "compute_id": "local", + "usage": "Default at first boot the VM will start from the cdrom." + } + assert new_template == expected_result + + # check that specific properties are used with "usage", "symbol" and "category" defined at the version level + new_template = ApplianceToTemplate().new_template(config, "local", "200G") + expected_result.update( + { + "usage": "This is how to use this version", + "symbol": "ethernet_switch", + "category": "switch" + } + ) + assert new_template == expected_result def test_add_appliance_path_relative_to_images_dir(tmpdir, linux_microcore_img): diff --git a/tests/test_http_client.py b/tests/test_http_client.py index c7a0c51bb..93d195ce4 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -47,7 +47,7 @@ def http_client(http_request, network_manager): return HTTPClient({"protocol": "http", "host": "127.0.0.1", "port": "3080"}, network_manager=network_manager) -@pytest.yield_fixture(autouse=True) +@pytest.fixture(autouse=True) def http_request(): mock = unittest.mock.Mock() diff --git a/tests/test_image_manager.py b/tests/test_image_manager.py index a314994f1..f2651daeb 100644 --- a/tests/test_image_manager.py +++ b/tests/test_image_manager.py @@ -34,7 +34,7 @@ def images_dir(tmpdir): return path -@pytest.yield_fixture +@pytest.fixture def image_manager(tmpdir, images_dir): ImageManager._instance = None settings = LOCAL_SERVER_SETTINGS diff --git a/tests/test_local_server.py b/tests/test_local_server.py index 9e2727e73..d308e424a 100644 --- a/tests/test_local_server.py +++ b/tests/test_local_server.py @@ -32,7 +32,7 @@ def local_server_path(tmpdir): return str(tmpdir / "gns3server") -@pytest.yield_fixture +@pytest.fixture def local_server(local_server_path, tmpdir): with open(str(tmpdir / "test.cfg"), "w+") as f: f.write(""" diff --git a/tests/test_update_manager.py b/tests/test_update_manager.py index 5516f3f45..b595e3fa2 100644 --- a/tests/test_update_manager.py +++ b/tests/test_update_manager.py @@ -26,14 +26,14 @@ from gns3 import version -@pytest.yield_fixture +@pytest.fixture def frozen(): sys.frozen = True yield delattr(sys, 'frozen') -@pytest.yield_fixture +@pytest.fixture def devVersion(): old_version_info = version.__version_info__ old_version = version.__version__ @@ -44,7 +44,7 @@ def devVersion(): version.__version__ = old_version -@pytest.yield_fixture +@pytest.fixture def stableVersion(): old_version_info = version.__version_info__ old_version = version.__version__ From 6d855045ef9596bd26aba59f58a0783bf982bd75 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 16 Aug 2023 14:11:55 +1000 Subject: [PATCH 13/17] Show installation instructions when available and fix regression when installing Docker appliance. --- gns3/dialogs/appliance_wizard.py | 34 +++++++++++++++++++++++++++----- gns3/ui/appliance_wizard.ui | 31 ++++++++++++++++++++++++----- gns3/ui/appliance_wizard_ui.py | 25 +++++++++++++++++++---- 3 files changed, 76 insertions(+), 14 deletions(-) diff --git a/gns3/dialogs/appliance_wizard.py b/gns3/dialogs/appliance_wizard.py index 2dd49c189..8835887fb 100644 --- a/gns3/dialogs/appliance_wizard.py +++ b/gns3/dialogs/appliance_wizard.py @@ -204,8 +204,28 @@ def initializePage(self, page_id): 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): """ @@ -572,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: @@ -595,7 +615,7 @@ def _install(self, version): for settings in appliance_configuration["settings"]: if settings["template_type"] == "qemu": settings["template_properties"]["path"] = self.uiQemuListComboBox.currentData() - else: + 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) @@ -661,10 +681,14 @@ def nextId(self): if self.currentPage() == self.uiServerWizardPage: if self._appliance.template_type() == "docker": # skip Qemu binary selection and files pages if this is a Docker appliance - return super().nextId() + 2 + 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): diff --git a/gns3/ui/appliance_wizard.ui b/gns3/ui/appliance_wizard.ui index 09373e8c2..a19d1aa11 100644 --- a/gns3/ui/appliance_wizard.ui +++ b/gns3/ui/appliance_wizard.ui @@ -6,8 +6,8 @@ 0 0 - 900 - 601 + 726 + 428 @@ -219,6 +219,27 @@ + + + Installation instructions + + + Please read the following instructions in order to install your new appliance. + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + Required files @@ -232,12 +253,12 @@ 20 - - 120 - 20 + + 120 + Appliance version and files diff --git a/gns3/ui/appliance_wizard_ui.py b/gns3/ui/appliance_wizard_ui.py index b5a3dca58..ecdbf9cba 100644 --- a/gns3/ui/appliance_wizard_ui.py +++ b/gns3/ui/appliance_wizard_ui.py @@ -2,16 +2,19 @@ # Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/appliance_wizard.ui' # -# Created by: PyQt5 UI code generator 5.9 +# Created by: PyQt5 UI code generator 5.15.9 # -# WARNING! All changes made in this file will be lost! +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + from PyQt5 import QtCore, QtGui, QtWidgets + class Ui_ApplianceWizard(object): def setupUi(self, ApplianceWizard): ApplianceWizard.setObjectName("ApplianceWizard") - ApplianceWizard.resize(900, 601) + ApplianceWizard.resize(726, 428) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -112,6 +115,14 @@ def setupUi(self, ApplianceWizard): self.uiQemuListComboBox.setObjectName("uiQemuListComboBox") self.horizontalLayout_2.addWidget(self.uiQemuListComboBox) ApplianceWizard.addPage(self.uiQemuWizardPage) + self.uiInstructionsPage = QtWidgets.QWizardPage() + self.uiInstructionsPage.setObjectName("uiInstructionsPage") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.uiInstructionsPage) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.uiInstructionsTextEdit = QtWidgets.QTextEdit(self.uiInstructionsPage) + self.uiInstructionsTextEdit.setObjectName("uiInstructionsTextEdit") + self.verticalLayout_3.addWidget(self.uiInstructionsTextEdit) + ApplianceWizard.addPage(self.uiInstructionsPage) self.uiFilesWizardPage = QtWidgets.QWizardPage() self.uiFilesWizardPage.setObjectName("uiFilesWizardPage") self.verticalLayout = QtWidgets.QVBoxLayout(self.uiFilesWizardPage) @@ -174,6 +185,13 @@ def retranslateUi(self, ApplianceWizard): self.uiQemuWizardPage.setTitle(_translate("ApplianceWizard", "Qemu settings")) self.uiQemuWizardPage.setSubTitle(_translate("ApplianceWizard", "Please choose the qemu binary that will be used to run this appliance.")) self.uiQemuListLabel.setText(_translate("ApplianceWizard", "Qemu binary:")) + self.uiInstructionsPage.setTitle(_translate("ApplianceWizard", "Installation instructions")) + self.uiInstructionsPage.setSubTitle(_translate("ApplianceWizard", "Please read the following instructions in order to install your new appliance.")) + self.uiInstructionsTextEdit.setHtml(_translate("ApplianceWizard", "\n" +"\n" +"


")) self.uiFilesWizardPage.setTitle(_translate("ApplianceWizard", "Required files")) self.uiFilesWizardPage.setSubTitle(_translate("ApplianceWizard", "The following files are required to install the appliance")) self.uiApplianceVersionTreeWidget.headerItem().setText(0, _translate("ApplianceWizard", "Appliance version and files")) @@ -191,5 +209,4 @@ def retranslateUi(self, ApplianceWizard): "p, li { white-space: pre-wrap; }\n" "\n" "

The default username/password is admin/admin. A default configuration is present.

")) - from . import resources_rc From 7f6cace0d5fa3491968203529d293b29d99a1ad4 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 15 Sep 2023 16:08:45 +0700 Subject: [PATCH 14/17] Fix generic icon in Wayland. Ref #3501 --- gns3/application.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gns3/application.py b/gns3/application.py index 57bd7f4fc..c0a631957 100644 --- a/gns3/application.py +++ b/gns3/application.py @@ -46,6 +46,9 @@ def __init__(self, argv, hdpi=True): super().__init__(argv) + # this is tell Wayland what is the name of the desktop file (gns3.desktop) + self.setDesktopFileName("gns3") + # this info is necessary for QSettings self.setOrganizationName("GNS3") self.setOrganizationDomain("gns3.net") From 6777961d2901ce73af7092b6e175829468f6b9c2 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 18 Sep 2023 20:23:09 +0700 Subject: [PATCH 15/17] Add KiTTY to preconfigured telnet consoles. Fixes #3507 --- gns3/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gns3/settings.py b/gns3/settings.py index 220157f74..c8df8aca7 100644 --- a/gns3/settings.py +++ b/gns3/settings.py @@ -56,6 +56,7 @@ program_files_x86 = program_files = os.environ["PROGRAMFILES"] PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {'Putty (normal standalone version)': 'putty_standalone.exe -telnet %h %p -loghost "%d"', + 'KiTTY': r'kitty -title "%d" telnet://%h %p', 'MobaXterm': r'"{}\Mobatek\MobaXterm Personal Edition\MobaXterm.exe" -newtab "telnet %h %p"'.format(program_files_x86), 'Royal TS V3': r'{}\code4ward.net\Royal TS V3\RTS3App.exe /connectadhoc:%h /adhoctype:terminal /p:IsTelnetConnection="true" /p:ConnectionType="telnet;Telnet Connection" /p:Port="%p" /p:Name="%d"'.format(program_files), 'Royal TS V5': r'"{}\Royal TS V5\RoyalTS.exe" /protocol:terminal /using:adhoc /uri:"%h" /property:Port="%p" /property:IsTelnetConnection="true" /property:Name="%d"'.format(program_files_x86), From 025276f8a77387a58b4c2203dda11da8a5aae7dc Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 19 Sep 2023 20:07:57 +0700 Subject: [PATCH 16/17] Upgrade sentry-sdk and truststore --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8aae72d09..345b56fa7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ jsonschema>=4.17.3,<4.18; python_version >= '3.7' # v4.17.3 is the last version to support Python 3.7 jsonschema==3.2.0; python_version < '3.7' # v3.2.0 is the last version to support Python 3.6 -sentry-sdk==1.29.2,<1.30 +sentry-sdk==1.31.0,<1.32 psutil==5.9.5 distro>=1.8.0 -truststore>=0.7.0; python_version >= '3.10' +truststore>=0.8.0; python_version >= '3.10' importlib-resources>=1.3; python_version <= '3.9' setuptools>=60.8.1; python_version >= '3.7' setuptools==59.6.0; python_version < '3.7' # v59.6.0 is the last version to support Python 3.6 From 97b777ceead0457ab076ee4832a1731693e89f28 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 19 Sep 2023 20:16:52 +0700 Subject: [PATCH 17/17] Release v2.2.43 --- CHANGELOG | 9 +++++++++ gns3/crash_report.py | 2 +- gns3/version.py | 5 +++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 66766f401..af346bc47 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,14 @@ # Change Log +## 2.2.43 19/09/2023 + +* Add KiTTY to preconfigured telnet consoles. Fixes #3507 +* Fix generic icon in Wayland. Ref #3501 +* Support for appliance format version 8. +* Use importlib instead of pkg_resources +* Upgrade to PyQt 5.15.9 and pywin32 +* Add support for appliance version 8 format + ## 2.2.42 09/08/2023 * Use the system's certificate store for SSL connections diff --git a/gns3/crash_report.py b/gns3/crash_report.py index 63103b358..51ff5692e 100644 --- a/gns3/crash_report.py +++ b/gns3/crash_report.py @@ -50,7 +50,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "https://bae0411a1718612ee8c25cdb12ec7f02@o19455.ingest.sentry.io/38506" + DSN = "https://57454675a266a9d705fd505947a81b5c@o19455.ingest.sentry.io/38506" _instance = None def __init__(self): diff --git a/gns3/version.py b/gns3/version.py index d997d415c..b8c19a66e 100644 --- a/gns3/version.py +++ b/gns3/version.py @@ -23,9 +23,10 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.43.dev1" +__version__ = "2.2.43" + +__version_info__ = (2, 2, 43, 0) -__version_info__ = (2, 2, 43, 99) if "dev" in __version__: try: import os