Basic support to create new template from appliance.

This commit is contained in:
grossmj
2019-01-13 15:23:14 +07:00
parent f48f4eacd2
commit b6e5a588bf
6 changed files with 226 additions and 293 deletions

View File

@@ -17,10 +17,14 @@
import os
import sip
import urllib
import shutil
from ssl import CertificateError
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
from ..template_manager import TemplateManager
from ..template import Template
from ..modules import Qemu
from ..registry.appliance import Appliance, ApplianceError
from ..registry.registry import Registry
@@ -525,12 +529,6 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
:params version: appliance version name
"""
try:
config = Config()
except (OSError, ValueError) as e:
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
return False
if version is None:
appliance_configuration = self._appliance.copy()
if not "docker" in appliance_configuration:
@@ -543,9 +541,10 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
return False
while len(appliance_configuration["name"]) == 0 or not config.is_name_available(appliance_configuration["name"]):
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "The name \"{}\" is already used by another appliance".format(appliance_configuration["name"]))
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add appliance", "New name:", QtWidgets.QLineEdit.Normal, appliance_configuration["name"])
template_manager = TemplateManager().instance()
while len(appliance_configuration["name"]) == 0 or not template_manager.is_name_available(appliance_configuration["name"]):
QtWidgets.QMessageBox.warning(self.parent(), "Add template", "The name \"{}\" is already used by another template".format(appliance_configuration["name"]))
appliance_configuration["name"], ok = QtWidgets.QInputDialog.getText(self.parent(), "Add template", "New name:", QtWidgets.QLineEdit.Normal, appliance_configuration["name"])
if not ok:
return False
appliance_configuration["name"] = appliance_configuration["name"].strip()
@@ -553,18 +552,194 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
if "qemu" in appliance_configuration:
appliance_configuration["qemu"]["path"] = self.uiQemuListComboBox.currentData()
worker = WaitForLambdaWorker(lambda: config.add_appliance(appliance_configuration, self._compute_id, self._symbols), allowed_exceptions=[ConfigException, OSError])
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
progress_dialog.show()
if not progress_dialog.exec_():
return False
self._create_template(appliance_configuration, self._compute_id)
return True
worker = WaitForLambdaWorker(lambda: config.save(), allowed_exceptions=[ConfigException, OSError])
progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
progress_dialog.show()
if progress_dialog.exec_():
QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
return True
#worker = WaitForLambdaWorker(lambda: self._create_template(appliance_configuration, self._compute_id), allowed_exceptions=[ConfigException, OSError])
#progress_dialog = ProgressDialog(worker, "Add template", "Installing a new template...", None, busy=True, parent=self)
#progress_dialog.show()
#if progress_dialog.exec_():
# QtWidgets.QMessageBox.information(self.parent(), "Add template", "{} template has been installed!".format(appliance_configuration["name"]))
# return True
#return False
# worker = WaitForLambdaWorker(lambda: config.save(), allowed_exceptions=[ConfigException, OSError])
# progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
# progress_dialog.show()
# if progress_dialog.exec_():
# QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
# return True
def _create_template(self, appliance_config, server):
"""
Creates a new template from an appliance.
:param appliance_config: Dictionary with appliance configuration
:param server
"""
new_template = {
"compute_id": server,
"name": appliance_config["name"]
}
if "usage" in appliance_config:
new_template["usage"] = appliance_config["usage"]
if appliance_config["category"] == "multilayer_switch":
new_template["category"] = "switch"
else:
new_template["category"] = appliance_config["category"]
if "symbol" in appliance_config:
new_template["symbol"] = self._set_symbol(appliance_config["symbol"], self._symbols)
if new_template.get("symbol") is None:
if appliance_config["category"] == "guest":
if "docker" in appliance_config:
new_template["symbol"] = ":/symbols/docker_guest.svg"
else:
new_template["symbol"] = ":/symbols/qemu_guest.svg"
elif appliance_config["category"] == "router":
new_template["symbol"] = ":/symbols/router.svg"
elif appliance_config["category"] == "switch":
new_template["symbol"] = ":/symbols/ethernet_switch.svg"
elif appliance_config["category"] == "multilayer_switch":
new_template["symbol"] = ":/symbols/multilayer_switch.svg"
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)
else:
raise ConfigException("{} no configuration found for known emulators".format(new_template["name"]))
TemplateManager.instance().createTemplate(Template(new_template))
def _add_qemu_config(self, new_config, appliance_config):
new_config.update(appliance_config["qemu"])
# the following properties are not valid for a template
new_config.pop("kvm")
new_config.pop("path")
new_config.pop("arch")
options = appliance_config["qemu"].get("options", "")
if "-nographic" not in options:
options += " -nographic"
if appliance_config["qemu"].get("kvm", "allow") == "disable" and "-no-kvm" not in options:
options += " -no-kvm"
new_config["options"] = options.strip()
for image in appliance_config["images"]:
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"]
else:
new_config["qemu_path"] = "qemu-system-{}".format(appliance_config["qemu"]["arch"])
if "first_port_name" in appliance_config:
new_config["first_port_name"] = appliance_config["first_port_name"]
if "port_name_format" in appliance_config:
new_config["port_name_format"] = appliance_config["port_name_format"]
if "port_segment_size" in appliance_config:
new_config["port_segment_size"] = appliance_config["port_segment_size"]
if "custom_adapters" in appliance_config:
new_config["custom_adapters"] = appliance_config["custom_adapters"]
if "linked_clone" in appliance_config:
new_config["linked_clone"] = appliance_config["linked_clone"]
def _add_docker_config(self, new_config, appliance_config):
new_config.update(appliance_config["docker"])
if "custom_adapters" in appliance_config:
new_config["custom_adapters"] = appliance_config["custom_adapters"]
def _add_dynamips_config(self, new_config, appliance_config):
new_config.update(appliance_config["dynamips"])
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):
new_config.update(appliance_config["iou"])
for image in appliance_config["images"]:
if "path" not in image:
raise ConfigException("Disk image is missing")
new_config[image["type"]] = self._relative_image_path("IOU", image["path"])
new_config["path"] = new_config["image"]
def _relative_image_path(self, image_dir_type, path):
"""
:param image_dir_type: Type of image directory
:param filename: Filename at the end of the process
:param path: Full path to the file
:returns: Path relative to image directory.
Copy the image to the directory if not already in the directory
"""
images_dir = os.path.join(Config().images_dir, image_dir_type)
path = os.path.abspath(path)
if os.path.commonprefix([images_dir, path]) == images_dir:
return path.replace(images_dir, '').strip('/\\')
return os.path.basename(path)
def _set_symbol(self, symbol, controller_symbols):
"""
Check if exists on controller or download symbol from the web if needed
"""
# GNS3 builtin symbol
if symbol.startswith(":/symbols/"):
return symbol
path = os.path.join(self.symbols_dir, symbol)
if os.path.exists(path):
return os.path.basename(path)
is_symbol_on_controller = len([s for s in controller_symbols
if s['symbol_id'] == symbol]) > 0
if is_symbol_on_controller:
cached = Controller.instance().getStaticCachedPath(symbol)
if os.path.exists(cached):
try:
shutil.copy(cached, path)
except IOError as e:
log.warning("Cannot copy cached symbol from `{}` to `{}` due `{}`".format(
cached, path, str(e)
))
return symbol
url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol)
try:
urllib.request.urlretrieve(url, path)
return os.path.basename(path)
except (OSError, CertificateError):
return None
def _uploadImages(self, version):
"""
@@ -711,8 +886,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
reply = QtWidgets.QMessageBox.question(self, "Custom files",
"This option allows files with different MD5 checksums. This feature is only for advanced users and can lead "
"to unexpected problems. Are you sure you would like to enable it?",
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No
)
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
self.allowCustomFiles.setChecked(False)

View File

@@ -52,7 +52,6 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
self.setOption(QtWidgets.QWizard.HaveCustomButton1, True)
self.customButtonClicked.connect(self._downloadAppliancesSlot)
self.button(QtWidgets.QWizard.CustomButton1).hide()
self.uiFilterLineEdit.textChanged.connect(self._filterTextChangedSlot)
ApplianceManager.instance().appliances_changed_signal.connect(self._appliancesChangedSlot)
@@ -81,11 +80,10 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
QtWidgets.QMessageBox.information(self, "Appliances", "Appliances are up-to-date!")
def _filterTextChangedSlot(self, text):
self._get_appliances_from_server(appliance_filter=text)
def _setItemIcon(self, item, icon):
item.setIcon(0, icon)
item.setIcon(0, icon)
def _get_tooltip_text(self, appliance):
"""
@@ -188,13 +186,14 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
item.setText(3, appliance["vendor_name"])
item.setData(0, QtCore.Qt.UserRole, appliance)
item.setSizeHint(0, QtCore.QSize(32, 32))
#item.setSizeHint(0, QtCore.QSize(32, 32))
item.setToolTip(0, self._get_tooltip_text(appliance))
Controller.instance().getSymbolIcon(appliance.get("symbol"), qpartial(self._setItemIcon, item),
fallback=":/symbols/" + appliance["category"] + ".svg")
self.uiAppliancesTreeWidget.resizeColumnToContents(0)
self.uiAppliancesTreeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiAppliancesTreeWidget.resizeColumnToContents(0)
def initializePage(self, page_id):
"""

View File

@@ -18,12 +18,7 @@
import json
import os
import urllib
import shutil
from ssl import CertificateError
from gns3.controller import Controller
from ..local_config import LocalConfig
from ..local_server_config import LocalServerConfig
from ..settings import LOCAL_SERVER_SETTINGS
@@ -93,265 +88,6 @@ class Config:
servers.append(server["url"])
return servers
def is_name_available(self, name):
"""
:param name: Appliance name
:returns: True if name is not already used
"""
appliance_names = []
if "Qemu" in self._config:
appliance_names.extend(self._config["Qemu"].get("vms", []))
if "IOU" in self._config:
appliance_names.extend(self._config["IOU"].get("devices", []))
if "Dynamips" in self._config:
appliance_names.extend(self._config["Dynamips"].get("routers", []))
if "Docker" in self._config:
appliance_names.extend(self._config["Docker"].get("containers", []))
for item in appliance_names:
if item["name"] == name:
return False
return True
def add_appliance(self, appliance_config, server, controller_symbols=None):
"""
Add appliance to the user configuration
:param appliance_config: Dictionary with appliance configuration
:param server
:param controller_symbols: Symbols located on controller
"""
if controller_symbols is None:
controller_symbols = []
new_config = {
"compute_id": server,
"name": appliance_config["name"]
}
if "usage" in appliance_config:
new_config["usage"] = appliance_config["usage"]
if appliance_config["category"] == "guest":
new_config["category"] = 2
elif appliance_config["category"] == "router":
new_config["category"] = 0
elif appliance_config["category"] == "firewall":
new_config["category"] = 3
elif appliance_config["category"] == "switch":
new_config["category"] = 1
elif appliance_config["category"] == "multilayer_switch":
new_config["category"] = 1
if "symbol" in appliance_config:
new_config["symbol"] = self._set_symbol(appliance_config["symbol"], controller_symbols)
if new_config.get("symbol") is None:
if appliance_config["category"] == "guest":
if "docker" in appliance_config:
new_config["symbol"] = ":/symbols/docker_guest.svg"
else:
new_config["symbol"] = ":/symbols/qemu_guest.svg"
elif appliance_config["category"] == "router":
new_config["symbol"] = ":/symbols/router.svg"
elif appliance_config["category"] == "switch":
new_config["symbol"] = ":/symbols/ethernet_switch.svg"
elif appliance_config["category"] == "multilayer_switch":
new_config["symbol"] = ":/symbols/multilayer_switch.svg"
elif appliance_config["category"] == "firewall":
new_config["symbol"] = ":/symbols/firewall.svg"
# Raise error if VM already exists
if not self.is_name_available(new_config["name"]):
raise ConfigException("{} already exists".format(new_config["name"]))
if "qemu" in appliance_config:
self._add_qemu_config(new_config, appliance_config)
return
if "iou" in appliance_config:
self._add_iou_config(new_config, appliance_config)
return
if "dynamips" in appliance_config:
self._add_dynamips_config(new_config, appliance_config)
return
if "docker" in appliance_config:
self._add_docker_config(new_config, appliance_config)
return
raise ConfigException("{} no configuration found for known emulators".format(new_config["name"]))
def _add_docker_config(self, new_config, appliance_config):
new_config["adapters"] = appliance_config["docker"]["adapters"]
new_config["image"] = appliance_config["docker"]["image"]
new_config["environment"] = appliance_config["docker"].get("environment", "")
new_config["start_command"] = appliance_config["docker"].get("start_command", "")
new_config["console_type"] = appliance_config["docker"].get("console_type", "telnet")
new_config["console_http_port"] = appliance_config["docker"].get("console_http_port", 80)
new_config["console_http_path"] = appliance_config["docker"].get("console_http_path", "/")
new_config["extra_hosts"] = appliance_config["docker"].get("extra_hosts", "")
self._config["Docker"]["containers"].append(new_config)
def _add_dynamips_config(self, new_config, appliance_config):
new_config["auto_delete_disks"] = True
new_config["disk0"] = 0
new_config["disk1"] = 0
new_config["exec_area"] = 64
new_config["idlemax"] = 500
new_config["idlesleep"] = 30
new_config["system_id"] = "FTX0945W0MY"
new_config["sparsemem"] = True
new_config["private_config"] = ""
new_config["mac_addr"] = ""
new_config["iomem"] = 5
new_config["mmap"] = True
for key, value in appliance_config["dynamips"].items():
new_config[key] = value
for image in appliance_config["images"]:
new_config[image["type"]] = self._relative_image_path("IOS", image["path"])
new_config["idlepc"] = image.get("idlepc", "")
log.debug("Add appliance Dynamips: %s", str(new_config))
self._config["Dynamips"].setdefault("routers", [])
self._config["Dynamips"]["routers"].append(new_config)
def _add_iou_config(self, new_config, appliance_config):
new_config["ethernet_adapters"] = appliance_config["iou"]["ethernet_adapters"]
new_config["serial_adapters"] = appliance_config["iou"]["serial_adapters"]
new_config["startup_config"] = appliance_config["iou"]["startup_config"]
new_config["private_config"] = ""
new_config["l1_keepalives"] = False
new_config["use_default_iou_values"] = True
new_config["nvram"] = appliance_config["iou"]["nvram"]
new_config["ram"] = appliance_config["iou"]["ram"]
new_config["console_type"] = appliance_config["iou"].get("console_type", "telnet")
for image in appliance_config["images"]:
if "path" not in image:
raise ConfigException("Disk image is missing")
new_config[image["type"]] = self._relative_image_path("IOU", image["path"])
new_config["path"] = new_config["image"]
log.debug("Add appliance IOU: %s", str(new_config))
self._config["IOU"].setdefault("devices", [])
self._config["IOU"]["devices"].append(new_config)
def _add_qemu_config(self, new_config, appliance_config):
new_config["adapter_type"] = appliance_config["qemu"]["adapter_type"]
new_config["adapters"] = appliance_config["qemu"]["adapters"]
new_config["cpu_throttling"] = appliance_config["qemu"].get("cpu_throttling", 0)
new_config["cpus"] = appliance_config["qemu"].get("cpus", 1)
new_config["ram"] = appliance_config["qemu"]["ram"]
new_config["console_type"] = appliance_config["qemu"]["console_type"]
new_config["legacy_networking"] = False
new_config["process_priority"] = appliance_config["qemu"].get("process_priority", "normal")
options = appliance_config["qemu"].get("options", "")
if "-nographic" not in options:
options += " -nographic"
if appliance_config["qemu"].get("kvm", "allow") == "disable" and "-no-kvm" not in options:
options += " -no-kvm"
new_config["options"] = options.strip()
for image in appliance_config["images"]:
if image.get("path"):
new_config[image["type"]] = self._relative_image_path("QEMU", image["path"])
new_config.setdefault("hda_disk_image", "")
new_config.setdefault("hdb_disk_image", "")
new_config.setdefault("hdc_disk_image", "")
new_config.setdefault("hdd_disk_image", "")
new_config.setdefault("cdrom_image", "")
new_config.setdefault("bios_image", "")
new_config.setdefault("initrd", "")
new_config.setdefault("kernel_image", "")
new_config["hda_disk_interface"] = appliance_config["qemu"].get("hda_disk_interface", "ide")
new_config["hdb_disk_interface"] = appliance_config["qemu"].get("hdb_disk_interface", "ide")
new_config["hdc_disk_interface"] = appliance_config["qemu"].get("hdc_disk_interface", "ide")
new_config["hdd_disk_interface"] = appliance_config["qemu"].get("hdd_disk_interface", "ide")
new_config["kernel_command_line"] = appliance_config["qemu"].get("kernel_command_line", "")
if "path" in appliance_config["qemu"]:
new_config["qemu_path"] = appliance_config["qemu"]["path"]
else:
new_config["qemu_path"] = "qemu-system-{}".format(appliance_config["qemu"]["arch"])
if "boot_priority" in appliance_config["qemu"]:
new_config["boot_priority"] = appliance_config["qemu"]["boot_priority"]
if "first_port_name" in appliance_config:
new_config["first_port_name"] = appliance_config["first_port_name"]
if "port_name_format" in appliance_config:
new_config["port_name_format"] = appliance_config["port_name_format"]
if "port_segment_size" in appliance_config:
new_config["port_segment_size"] = appliance_config["port_segment_size"]
if "custom_adapters" in appliance_config:
new_config["custom_adapters"] = appliance_config["custom_adapters"]
if "linked_clone" in appliance_config:
new_config["linked_clone"] = appliance_config["linked_clone"]
log.debug("Add appliance QEMU: %s", str(new_config))
self._config["Qemu"].setdefault("vms", [])
self._config["Qemu"]["vms"].append(new_config)
def _set_symbol(self, symbol, controller_symbols):
"""
Check if exists on controller or download symbol from the web if needed
"""
# GNS3 builtin symbol
if symbol.startswith(":/symbols/"):
return symbol
path = os.path.join(self.symbols_dir, symbol)
if os.path.exists(path):
return os.path.basename(path)
is_symbol_on_controller = len([s for s in controller_symbols
if s['symbol_id'] == symbol]) > 0
if is_symbol_on_controller:
cached = Controller.instance().getStaticCachedPath(symbol)
if os.path.exists(cached):
try:
shutil.copy(cached, path)
except IOError as e:
log.warning("Cannot copy cached symbol from `{}` to `{}` due `{}`".format(
cached, path, str(e)
))
return symbol
url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol)
try:
urllib.request.urlretrieve(url, path)
return os.path.basename(path)
except (OSError, CertificateError):
return None
def _relative_image_path(self, image_dir_type, path):
"""
:param image_dir_type: Type of image directory
:param filename: Filename at the end of the processus
:param path: Full path to the file
:returns: Path relative to image directory.
Copy the image to the directory if not already in the directory
"""
images_dir = os.path.join(self.images_dir, image_dir_type)
path = os.path.abspath(path)
if os.path.commonprefix([images_dir, path]) == images_dir:
return path.replace(images_dir, '').strip('/\\')
return os.path.basename(path)
def save(self):
"""
Save the configuration file

View File

@@ -60,6 +60,16 @@ class TemplateManager(QtCore.QObject):
self._templates = {}
self.templates_changed_signal.emit()
def createTemplate(self, template):
"""
Creates a template on the controller.
:param template: template object.
"""
log.debug("Create template '{}' (ID={})".format(template.name(), template.id()))
self._controller.post("/templates", self.templateDataReceivedCallback, body=template.__json__())
def deleteTemplate(self, template_id):
"""
Deletes a template on the controller.
@@ -114,8 +124,7 @@ class TemplateManager(QtCore.QObject):
# Create the new templates
for template in templates:
if template.id() not in self._templates:
log.debug("Create template '{}' (ID={})".format(template.name(), template.id()))
self._controller.post("/templates", self.templateDataReceivedCallback, body=template.__json__())
self.createTemplate(template)
def templateDataReceivedCallback(self, result, error=False, **kwargs):
"""
@@ -215,6 +224,17 @@ class TemplateManager(QtCore.QObject):
log.error("Error while creating node from template: {}".format(result["message"]))
return
def is_name_available(self, name):
"""
:param name: Template name
:returns: True if name is not already used
"""
for template in self._templates.values():
if template.name() == name:
return False
return True
@staticmethod
def instance():
"""

View File

@@ -79,6 +79,9 @@
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="headerShowSortIndicator" stdset="0">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Appliance name</string>

View File

@@ -40,6 +40,7 @@ class Ui_NewTemplateWizard(object):
self.uiAppliancesTreeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.uiAppliancesTreeWidget.setRootIsDecorated(False)
self.uiAppliancesTreeWidget.setObjectName("uiAppliancesTreeWidget")
self.uiAppliancesTreeWidget.header().setSortIndicatorShown(True)
self.verticalLayout_2.addWidget(self.uiAppliancesTreeWidget)
NewTemplateWizard.addPage(self.uiApplianceFromServerWizardPage)