mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-05-17 00:46:01 +03:00
Initial version of an appliance file format
This commit is contained in:
162
gns3/appliance_window.py
Normal file
162
gns3/appliance_window.py
Normal file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import jinja2
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from .utils.get_resource import get_resource
|
||||
from .qt import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets, QtGui
|
||||
from .ui.appliance_window_ui import Ui_ApplianceWindow
|
||||
from .image_manager import ImageManager
|
||||
from .registry.appliance import Appliance
|
||||
from .registry.registry import Registry
|
||||
from .registry.config import Config
|
||||
from .registry.image import Image
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def human_filesize(num):
|
||||
for unit in ['B','KB','MB','GB']:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.1f%s" % (num, unit)
|
||||
num /= 1024.0
|
||||
return "%.1f%s" % (num, 'TB')
|
||||
|
||||
|
||||
class ApplianceWindow(QtWidgets.QWidget, Ui_ApplianceWindow):
|
||||
|
||||
def __init__(self, path, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.setWindowTitle(path)
|
||||
|
||||
self._path = path
|
||||
|
||||
# Call linkClickedSlot() for all non local links
|
||||
self.uiWebView.page().setLinkDelegationPolicy(QtWebKitWidgets.QWebPage.DelegateExternalLinks)
|
||||
self.uiWebView.linkClicked.connect(self._linkClickedSlot)
|
||||
|
||||
# Expose JavaScript objects
|
||||
self.uiWebView.page().mainFrame().javaScriptWindowObjectCleared.connect(self.javaScriptWindowObject)
|
||||
|
||||
# Enable the inspector on right click
|
||||
self.uiWebView.settings().setAttribute(QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
|
||||
|
||||
self._refresh()
|
||||
|
||||
self.show()
|
||||
|
||||
def _refresh(self):
|
||||
renderer = jinja2.Environment(loader=jinja2.FileSystemLoader(get_resource('templates')))
|
||||
renderer.filters['nl2br'] = lambda s: s.replace('\n', '<br />')
|
||||
renderer.filters['human_filesize'] = human_filesize
|
||||
template = renderer.get_template("appliance.html")
|
||||
|
||||
registry = Registry(ImageManager.instance().getDirectory())
|
||||
|
||||
try:
|
||||
self._appliance = Appliance(registry, self._path)
|
||||
except ApplianceError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
||||
|
||||
self.uiWebView.setHtml(template.render(appliance=self._appliance, registry=registry))
|
||||
|
||||
def javaScriptWindowObject(self):
|
||||
frame = self.uiWebView.page().mainFrame()
|
||||
frame.addToJavaScriptWindowObject('gns3', self)
|
||||
|
||||
def _linkClickedSlot(self, url):
|
||||
"""
|
||||
Open in a new browser other url
|
||||
"""
|
||||
QtGui.QDesktopServices.openUrl(url)
|
||||
|
||||
#
|
||||
# Public Javascript methods
|
||||
#
|
||||
@QtCore.pyqtSlot(str)
|
||||
def install(self, version):
|
||||
"""
|
||||
Install an appliance based on appliance version
|
||||
|
||||
:param version: Version to install
|
||||
"""
|
||||
|
||||
try:
|
||||
config = Config()
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
||||
self.close()
|
||||
return
|
||||
|
||||
appliance_configuration = self._appliance.search_images_for_version(version)
|
||||
|
||||
if config.servers == ["local"]:
|
||||
server = "local"
|
||||
else:
|
||||
server_types = {}
|
||||
for server in Config().servers:
|
||||
if server == "local":
|
||||
server_types["Local server"] = server
|
||||
elif server == "vm":
|
||||
server_types["GNS3 VM"] = server
|
||||
else:
|
||||
server_types[server] = server
|
||||
selection, ok = QtWidgets.QInputDialog.getItem(self.parent(), "GNS3 server", "Please select a GNS3 server:", list(server_types.keys()), 0, False)
|
||||
if ok:
|
||||
server = server_types[selection]
|
||||
else:
|
||||
return
|
||||
|
||||
if config.add_appliance(appliance_configuration, server):
|
||||
self.close()
|
||||
try:
|
||||
config.save()
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
||||
return
|
||||
QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} {} installed!".format(self._appliance["name"], version))
|
||||
|
||||
@QtCore.pyqtSlot(str, str)
|
||||
def importAppliance(self, filename, md5sum):
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName()
|
||||
if len(path) == 0:
|
||||
return
|
||||
|
||||
#Do not create temporary file
|
||||
md5 = Image(path, cache=False).md5sum
|
||||
if md5 != md5sum:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Add appliance", "This is not the correct image file.")
|
||||
return
|
||||
|
||||
try:
|
||||
config = Config()
|
||||
#TODO: ASK for VM type
|
||||
os.makedirs(os.path.join(config.images_dir, "QEMU"), exist_ok=True)
|
||||
dst_path = os.path.join(config.images_dir, "QEMU", filename)
|
||||
shutil.copy(path, dst_path)
|
||||
md5 = Image(dst_path).md5sum
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "Add appliance", str(e))
|
||||
return False
|
||||
|
||||
self._refresh()
|
||||
@@ -63,6 +63,7 @@ from .progress import Progress
|
||||
from .licence import checkLicence
|
||||
from .image_manager import ImageManager
|
||||
from .update_manager import UpdateManager
|
||||
from .appliance_window import ApplianceWindow
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -401,8 +402,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
path, _ = QtWidgets.QFileDialog.getOpenFileName(self,
|
||||
"Open project",
|
||||
self.projectsDirPath(),
|
||||
"All files (*.*);;GNS3 project files (*.gns3);;NET files (*.net)",
|
||||
"GNS3 project files (*.gns3)")
|
||||
"All files (*.*);;GNS3 project files (*.gns3);;NET files (*.net);;GNS3 appliance (*.gns3a)",
|
||||
"GNS3 project files (*.gns3)",
|
||||
"GNS3 appliance (*.gns3a)")
|
||||
if path:
|
||||
self._loadPath(path)
|
||||
|
||||
@@ -429,10 +431,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
def _loadPath(self, path):
|
||||
"""Open a file and close the previous project"""
|
||||
|
||||
if path and self.checkForUnsavedChanges():
|
||||
self._open_project_path = path
|
||||
self._project.project_closed_signal.connect(self._projectClosedContinueLoadPath)
|
||||
self._project.close()
|
||||
if path:
|
||||
if path.endswith(".gns3a"):
|
||||
self._appliance_window = ApplianceWindow(path)
|
||||
elif self.checkForUnsavedChanges():
|
||||
self._open_project_path = path
|
||||
self._project.project_closed_signal.connect(self._projectClosedContinueLoadPath)
|
||||
self._project.close()
|
||||
|
||||
def _projectClosedContinueLoadPath(self):
|
||||
|
||||
|
||||
0
gns3/registry/__init__.py
Normal file
0
gns3/registry/__init__.py
Normal file
119
gns3/registry/appliance.py
Normal file
119
gns3/registry/appliance.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import json
|
||||
import copy
|
||||
import collections
|
||||
|
||||
|
||||
class ApplianceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Appliance(collections.Mapping):
|
||||
def __init__(self, registry, path):
|
||||
"""
|
||||
:params registry: Instance of the registry where images are located
|
||||
:params path: Path of the appliance file on disk
|
||||
"""
|
||||
self._registry = registry
|
||||
|
||||
try:
|
||||
with open(path) as f:
|
||||
self._appliance = json.load(f)
|
||||
except (OSError, ValueError) as e:
|
||||
raise ApplianceError("Could not read appliance {}: {}".format(path, str(e)))
|
||||
self._check_config()
|
||||
self._resolve_version()
|
||||
|
||||
|
||||
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"] != 1:
|
||||
raise ApplianceError("Please update GNS3 marketplace in order to install this appliance")
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._appliance.__getitem__(key)
|
||||
|
||||
def __iter__(self):
|
||||
return self._appliance.__iter__()
|
||||
|
||||
def __len__(self):
|
||||
return self._appliance.__len__()
|
||||
|
||||
def _resolve_version(self):
|
||||
"""
|
||||
Replace image field in versions by their the complete information from images
|
||||
"""
|
||||
|
||||
for version in self._appliance["versions"]:
|
||||
for image_type, filename in version["images"].items():
|
||||
for file in self._appliance["images"]:
|
||||
if file["filename"] == filename:
|
||||
version["images"][image_type] = copy.copy(file)
|
||||
|
||||
|
||||
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
|
||||
their disk type and path.
|
||||
|
||||
:param version_name: Version name
|
||||
:returns: Appliance with only require images
|
||||
"""
|
||||
|
||||
found = False
|
||||
appliance = copy.deepcopy(self._appliance)
|
||||
for version in appliance["versions"]:
|
||||
if version["name"] == version_name:
|
||||
appliance["name"] = "{} {}".format(appliance["name"], version["name"])
|
||||
appliance["images"] = []
|
||||
for image_type, image in version["images"].items():
|
||||
image["type"] = image_type
|
||||
image["path"] = self._registry.search_image_file(image["md5sum"])
|
||||
if image["path"] is None:
|
||||
raise ApplianceError("File {} with checksum {} not found for {}".format(image["filename"], image["md5sum"], appliance["name"]))
|
||||
|
||||
appliance["images"].append(image)
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
raise ApplianceError("Version {} not found for {}".format(version_name, appliance["name"]))
|
||||
|
||||
return appliance
|
||||
|
||||
def is_version_installable(self, version):
|
||||
"""
|
||||
Search on disk if a version is available for this appliance
|
||||
|
||||
:params version: Version name
|
||||
:returns: Boolean true if installable
|
||||
"""
|
||||
|
||||
try:
|
||||
self.search_images_for_version(version)
|
||||
return True
|
||||
except ApplianceError:
|
||||
return False
|
||||
|
||||
171
gns3/registry/config.py
Normal file
171
gns3/registry/config.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
class ConfigException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
GNS3 config file
|
||||
"""
|
||||
|
||||
def __init__(self, path=None):
|
||||
"""
|
||||
:params path: Path of the configuration file, otherwise detect it on the system
|
||||
"""
|
||||
|
||||
self.path = path
|
||||
if self.path is None:
|
||||
self.path = self._get_standard_config_file_path()
|
||||
|
||||
with open(self.path) as f:
|
||||
self._config = json.load(f)
|
||||
|
||||
@property
|
||||
def images_dir(self):
|
||||
"""
|
||||
:returns: Location of the images directory on the server
|
||||
"""
|
||||
return self._config["Servers"]["local_server"]["images_path"]
|
||||
|
||||
@property
|
||||
def servers(self):
|
||||
"""
|
||||
:returns: List of server present in the configuration file as strings
|
||||
"""
|
||||
|
||||
servers = ["local"]
|
||||
if "vm" in self._config["Servers"] and self._config["Servers"]["vm"].get("auto_start", False):
|
||||
servers.append("vm")
|
||||
if "remote_servers" in self._config["Servers"]:
|
||||
for server in self._config["Servers"]["remote_servers"]:
|
||||
if "url" in server:
|
||||
servers.append(server["url"])
|
||||
return servers
|
||||
|
||||
def _get_standard_config_file_path(self):
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
filename = "gns3_gui.ini"
|
||||
else:
|
||||
filename = "gns3_gui.conf"
|
||||
|
||||
appname = "GNS3"
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
appdata = os.path.expandvars("%APPDATA%")
|
||||
return os.path.join(appdata, appname, filename)
|
||||
else:
|
||||
home = os.path.expanduser("~")
|
||||
return os.path.join(home, ".config", appname, filename)
|
||||
|
||||
def add_appliance(self, appliance_config, server):
|
||||
"""
|
||||
Add appliance to the user configuration
|
||||
|
||||
:param appliance_config: Dictionary with appliance configuration
|
||||
:param server
|
||||
"""
|
||||
|
||||
new_config = {
|
||||
"server": server,
|
||||
"name": appliance_config["name"]
|
||||
}
|
||||
if appliance_config["category"] == "guest":
|
||||
new_config["category"] = 2
|
||||
elif appliance_config["category"] == "router":
|
||||
new_config["category"] = 0
|
||||
|
||||
if "qemu" in appliance_config:
|
||||
self._add_qemu_config(new_config, appliance_config)
|
||||
return True
|
||||
return False
|
||||
|
||||
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"] = 0
|
||||
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"] = "normal"
|
||||
|
||||
options = appliance_config["qemu"].get("options", "")
|
||||
|
||||
new_config["options"] = options.strip()
|
||||
|
||||
new_config["hda_disk_image"] = appliance_config["qemu"].get("hda_disk_image", "")
|
||||
new_config["hdb_disk_image"] = appliance_config["qemu"].get("hdb_disk_image", "")
|
||||
new_config["hdc_disk_image"] = appliance_config["qemu"].get("hdc_disk_image", "")
|
||||
new_config["hdd_disk_image"] = appliance_config["qemu"].get("hdd_disk_image", "")
|
||||
new_config["cdrom_image"] = appliance_config["qemu"].get("cdrom_image", "")
|
||||
new_config["initrd_image"] = appliance_config["qemu"].get("initrd_image", "")
|
||||
new_config["kernel_command_line"] = appliance_config["qemu"].get("kernel_command_line", "")
|
||||
new_config["kernel_image"] = appliance_config["qemu"].get("kernel_image", "")
|
||||
|
||||
|
||||
new_config["qemu_path"] = "qemu-system-{}".format(appliance_config["qemu"]["arch"])
|
||||
|
||||
if "symbol" in appliance_config:
|
||||
new_config["symbol"] = appliance_config["symbol"]
|
||||
elif appliance_config["category"] == "guest":
|
||||
new_config["symbol"] = ":/symbols/qemu_guest.svg"
|
||||
elif appliance_config["category"] == "router":
|
||||
new_config["symbol"] = ":/symbols/router.svg"
|
||||
elif appliance_config["category"] == "multilayer_switch":
|
||||
new_config["symbol"] = ":/symbols/multilayer_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"
|
||||
|
||||
for image in appliance_config["images"]:
|
||||
new_config[image["type"]] = self._relative_image_path(image["path"])
|
||||
|
||||
if "boot_priority" in appliance_config:
|
||||
new_config["boot_priority"] = appliance_config["boot_priority"]
|
||||
|
||||
# Remove VM with the same Name
|
||||
self._config["Qemu"]["vms"] = [item for item in self._config["Qemu"]["vms"] if item["name"] != new_config["name"]]
|
||||
|
||||
self._config["Qemu"]["vms"].append(new_config)
|
||||
|
||||
def _relative_image_path(self, path):
|
||||
"""
|
||||
:returns: Path relative to image directory if image is inside or full path
|
||||
"""
|
||||
|
||||
if os.path.abspath(os.path.join(os.path.dirname(path), "..")) == self.images_dir:
|
||||
return os.path.basename(path)
|
||||
return path
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save the configuration file
|
||||
"""
|
||||
|
||||
with open(self.path, "w+") as f:
|
||||
json.dump(self._config, f, indent=4)
|
||||
92
gns3/registry/image.py
Normal file
92
gns3/registry/image.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
|
||||
|
||||
class Image:
|
||||
"""
|
||||
An appliance image file.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
:params: path of the image
|
||||
"""
|
||||
|
||||
self.path = path
|
||||
self._md5sum = None
|
||||
self._version = None
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
"""
|
||||
:returns: Image filename
|
||||
"""
|
||||
return os.path.basename(self.path)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""
|
||||
:returns: Get the file version / release
|
||||
"""
|
||||
return self._version
|
||||
|
||||
@version.setter
|
||||
def version(self, version):
|
||||
"""
|
||||
:returns: Set the file version / release
|
||||
"""
|
||||
self._version = version
|
||||
|
||||
@property
|
||||
def md5sum(self, cache=True):
|
||||
"""
|
||||
Compute a md5 hash for file
|
||||
|
||||
:params cache: Cache sum on disk
|
||||
:returns: hexadecimal md5
|
||||
"""
|
||||
|
||||
if os.path.exists(self.path + ".md5sum"):
|
||||
with open(self.path + ".md5sum") as f:
|
||||
self._md5sum = f.read()
|
||||
return self._md5sum
|
||||
|
||||
if self._md5sum is None:
|
||||
m = hashlib.md5()
|
||||
with open(self.path, "rb") as f:
|
||||
while True:
|
||||
buf = f.read(4096)
|
||||
if not buf:
|
||||
break
|
||||
m.update(buf)
|
||||
self._md5sum = m.hexdigest()
|
||||
if cache:
|
||||
with open(self.path + ".md5sum", "w+") as f:
|
||||
f.write(self._md5sum)
|
||||
return self._md5sum
|
||||
|
||||
@property
|
||||
def filesize(self):
|
||||
"""
|
||||
Return image file size
|
||||
"""
|
||||
return os.path.getsize(self.path)
|
||||
67
gns3/registry/registry.py
Normal file
67
gns3/registry/registry.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import copy
|
||||
|
||||
from .image import Image
|
||||
|
||||
|
||||
class RegistryError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Registry:
|
||||
def __init__(self, images_dir):
|
||||
self._images_dir = images_dir
|
||||
|
||||
def list_images(self):
|
||||
"""
|
||||
List image on user computer
|
||||
"""
|
||||
images = []
|
||||
|
||||
directory = os.path.join(self._images_dir, "QEMU")
|
||||
if os.path.exists(directory):
|
||||
for filename in os.listdir(directory):
|
||||
if not filename.endswith(".md5sum") and not filename.startswith("."):
|
||||
path = os.path.join(directory, filename)
|
||||
if os.path.isfile(path):
|
||||
images.append(Image(path))
|
||||
return images
|
||||
|
||||
def search_image_file(self, md5sum):
|
||||
"""
|
||||
Search an image based on its MD5 checksum
|
||||
|
||||
:param md5sum: Hash of the image
|
||||
:returns: Image object or None
|
||||
"""
|
||||
|
||||
directory = os.path.join(self._images_dir, "QEMU")
|
||||
if os.path.exists(directory):
|
||||
for filename in os.listdir(directory):
|
||||
if not filename.endswith(".md5sum") and not filename.startswith("."):
|
||||
path = os.path.join(directory, filename)
|
||||
if os.path.isfile(path):
|
||||
image = Image(path)
|
||||
if image.md5sum == md5sum:
|
||||
return image.path
|
||||
return None
|
||||
|
||||
|
||||
44
gns3/ui/appliance_window.ui
Normal file
44
gns3/ui/appliance_window.ui
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ApplianceWindow</class>
|
||||
<widget class="QWidget" name="ApplianceWindow">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::WindowModal</enum>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWebView" name="uiWebView">
|
||||
<property name="url">
|
||||
<url>
|
||||
<string>about:blank</string>
|
||||
</url>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>QWebView</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>QtWebKitWidgets/QWebView</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
30
gns3/ui/appliance_window_ui.py
Normal file
30
gns3/ui/appliance_window_ui.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/ui/appliance_window.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.5
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
class Ui_ApplianceWindow(object):
|
||||
def setupUi(self, ApplianceWindow):
|
||||
ApplianceWindow.setObjectName("ApplianceWindow")
|
||||
ApplianceWindow.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout(ApplianceWindow)
|
||||
self.horizontalLayout.setContentsMargins(5, 5, 5, 5)
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.uiWebView = QtWebKitWidgets.QWebView(ApplianceWindow)
|
||||
self.uiWebView.setUrl(QtCore.QUrl("about:blank"))
|
||||
self.uiWebView.setObjectName("uiWebView")
|
||||
self.horizontalLayout.addWidget(self.uiWebView)
|
||||
|
||||
self.retranslateUi(ApplianceWindow)
|
||||
QtCore.QMetaObject.connectSlotsByName(ApplianceWindow)
|
||||
|
||||
def retranslateUi(self, ApplianceWindow):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
ApplianceWindow.setWindowTitle(_translate("ApplianceWindow", "Form"))
|
||||
|
||||
from PyQt5 import QtWebKitWidgets
|
||||
@@ -2,4 +2,5 @@ jsonschema>=2.4.0
|
||||
paramiko>=1.15.1
|
||||
raven>=5.2.0
|
||||
rsa>=3.1.4
|
||||
psutil>=2.2.1
|
||||
psutil>=2.2.1
|
||||
Jinja2>=2.7.3
|
||||
|
||||
74
resources/templates/appliance.html
Normal file
74
resources/templates/appliance.html
Normal file
@@ -0,0 +1,74 @@
|
||||
{% extends "layout/default.html" %}
|
||||
{% block body %}
|
||||
<div class="jumbotron">
|
||||
{% if appliance["status"] == "broken" %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
|
||||
<span class="sr-only">Error:</span>
|
||||
This appliance is actually not working
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if appliance["status"] == "experimental" %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
|
||||
<span class="sr-only">Error:</span>
|
||||
This appliance is actually experimental
|
||||
</div>
|
||||
{% endif %}
|
||||
<h1>{{ appliance["name"] }}</h1>
|
||||
Category {{ appliance["category"] }}<br />
|
||||
Product: <a href="{{ appliance["product_url"] }}">{{ appliance["product_name"] }}</a><br />
|
||||
Vendor: <a href="{{ appliance["vendor_url"] }}">{{ appliance["vendor_name"] }}</a><br />
|
||||
Documentation: <a href="{{ appliance["documentation_url"] }}">{{ appliance["documentation_url"] }}</a><br />
|
||||
Status: {{ appliance["status"] }}<br />
|
||||
Maintainer: <a href="mailto:{{ appliance["maintainer_email"] }}">{{ appliance["maintainer"] }}</a>
|
||||
|
||||
<p>{{ appliance["description"] | nl2br }}</p>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% if appliance["usage"] %}
|
||||
<h2>Usage</h2>
|
||||
<p>{{ appliance["usage"] | nl2br }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if "qemu" in appliance %}
|
||||
<h2>Qemu settings</h2>
|
||||
{% for key in appliance["qemu"] %}
|
||||
{{ key }}: {{ appliance["qemu"][key] }}<br />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% for version in appliance["versions"] | reverse %}
|
||||
<h2>{{ appliance["name"] }} {{version["name"]}}</h2>
|
||||
{% if appliance.is_version_installable(version["name"]) %}
|
||||
<button type="button" class="btn btn-primary navbar-btn" onclick="return gns3.install('{{version["name"]}}')">Install</button>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
Can't install this appliance version until you import the missing images
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3>Require files</h3>
|
||||
|
||||
{% set version_id = loop.index %}
|
||||
{% for image in version.images.values() %}
|
||||
<h4>{{image["filename"]}}</h4>
|
||||
File Version: {{image["version"]}}<br />
|
||||
Size: {{ image["filesize"] | human_filesize}}<br />
|
||||
Checksum: {{image["md5sum"]}}<br />
|
||||
Download url: <a href="{{image["download_url"]}}">{{image["download_url"]}}</a><br />
|
||||
{% if "direct_download_url" in image %}
|
||||
Direct download url: <a href="{{image["direct_download_url"]}}">{{image["direct_download_url"]}}</a><br />
|
||||
{% endif %}
|
||||
{% set image_path = registry.search_image_file(image["md5sum"]) %}
|
||||
{% if image_path %}
|
||||
<p style="color: green">File available: {{image_path}}</p>
|
||||
{% else %}
|
||||
<p style="color: red">File not available</p>
|
||||
<button type="button" class="btn btn-primary navbar-btn" onclick="return gns3.importAppliance('{{image["filename"]}}', '{{image["md5sum"]}}')">Import file</button>
|
||||
{% endif %}
|
||||
<hr />
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
23
resources/templates/layout/default.html
Normal file
23
resources/templates/layout/default.html
Normal file
File diff suppressed because one or more lines are too long
3
setup.py
3
setup.py
@@ -54,7 +54,8 @@ setup(
|
||||
"gns3-converter>=1.2.3",
|
||||
"raven>=5.2.0",
|
||||
"rsa>=3.1.4",
|
||||
"psutil>=2.2.1"
|
||||
"psutil>=2.2.1",
|
||||
"Jinja2>=2.7.3"
|
||||
],
|
||||
entry_points={
|
||||
"gui_scripts": [
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import pytest
|
||||
import os
|
||||
import uuid
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
import tempfile
|
||||
import urllib.request
|
||||
import sys
|
||||
sys._called_from_test = True
|
||||
|
||||
@@ -170,9 +171,9 @@ def main_window():
|
||||
"""
|
||||
Get a mocked main window
|
||||
"""
|
||||
window = unittest.mock.MagicMock()
|
||||
window = MagicMock()
|
||||
|
||||
uiGraphicsView = unittest.mock.MagicMock()
|
||||
uiGraphicsView = MagicMock()
|
||||
uiGraphicsView.settings.return_value = {
|
||||
"default_label_font": "TypeWriter,10,-1,5,75,0,0,0,0,0",
|
||||
"default_label_color": "#000000"
|
||||
@@ -182,6 +183,24 @@ def main_window():
|
||||
return window
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def images_dir(tmpdir):
|
||||
os.makedirs(os.path.join(str(tmpdir), "gns3_tests", "QEMU"), exist_ok=True)
|
||||
return os.path.join(str(tmpdir), "gns3_tests")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def linux_microcore_img(images_dir):
|
||||
"""
|
||||
Create a fake image and return the path. The md5sum of the file will be 5d41402abc4b2a76b9719d911017c592
|
||||
"""
|
||||
|
||||
path = os.path.join(images_dir, "QEMU", "linux-microcore-3.4.1.img")
|
||||
with open(path, 'w+') as f:
|
||||
f.write("hello")
|
||||
return path
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""
|
||||
Use to detect in code if we are running from pytest
|
||||
|
||||
47
tests/registry/appliances/arista-veos.json
Normal file
47
tests/registry/appliances/arista-veos.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"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": 1,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 8,
|
||||
"ram": 2048,
|
||||
"arch": "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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
56
tests/registry/appliances/microcore-linux.json
Normal file
56
tests/registry/appliances/microcore-linux.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "Micro Core Linux",
|
||||
"category": "guest",
|
||||
"description": "Micro Core Linux is a smaller variant of Tiny Core without a graphical desktop.\n\nIt's provide a complete Linux system in few MB.",
|
||||
"vendor_name": "Team Tiny Core",
|
||||
"vendor_url": "http://distro.ibiblio.org/tinycorelinux",
|
||||
"documentation_url": "http://wiki.tinycorelinux.net/",
|
||||
"product_name": "Micro Core Linux",
|
||||
"product_url": "http://distro.ibiblio.org/tinycorelinux",
|
||||
"registry_version": 1,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 1,
|
||||
"ram": 32,
|
||||
"arch": "i386",
|
||||
"console_type": "telnet"
|
||||
},
|
||||
|
||||
"images": [
|
||||
{
|
||||
"filename": "linux-microcore-3.4.1.img",
|
||||
"version": "3.4.1",
|
||||
"md5sum": "5d41402abc4b2a76b9719d911017c592",
|
||||
"filesize": 5,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
|
||||
"direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/linux-microcore-3.4.1.img"
|
||||
},
|
||||
{
|
||||
"filename": "linux-microcore-4.0.2-clean.img",
|
||||
"version": "4.0.2",
|
||||
"md5sum": "e13d0d1c0b3999ae2386bba70417930c",
|
||||
"filesize": 26411008,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
|
||||
"direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/linux-microcore-4.0.2-clean.img"
|
||||
}
|
||||
],
|
||||
|
||||
"versions": [
|
||||
{
|
||||
"name": "3.4.1",
|
||||
"images": {
|
||||
"hda_disk_image": "linux-microcore-3.4.1.img"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.0.2",
|
||||
"images": {
|
||||
"hda_disk_image": "linux-microcore-4.0.2-clean.img"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
102
tests/registry/test_appliance.py
Normal file
102
tests/registry/test_appliance.py
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import pytest
|
||||
import json
|
||||
|
||||
from gns3.registry.appliance import Appliance, ApplianceError
|
||||
from gns3.registry.registry import Registry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registry(images_dir):
|
||||
return Registry(images_dir)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def microcore_appliance(registry):
|
||||
"""
|
||||
An instance of microcore Appliance object
|
||||
"""
|
||||
return Appliance(registry, "tests/registry/appliances/microcore-linux.json")
|
||||
|
||||
|
||||
def test_check_config(tmpdir, registry):
|
||||
|
||||
test_path = str(tmpdir / "test.json")
|
||||
|
||||
with open(test_path, "w+") as f:
|
||||
f.write("")
|
||||
|
||||
with pytest.raises(ApplianceError):
|
||||
Appliance(registry, "jkhj")
|
||||
|
||||
with pytest.raises(ApplianceError):
|
||||
Appliance(registry, test_path)
|
||||
|
||||
with open(test_path, "w+") as f:
|
||||
f.write("{}")
|
||||
|
||||
with pytest.raises(ApplianceError):
|
||||
Appliance(registry, test_path)
|
||||
|
||||
with open(test_path, "w+") as f:
|
||||
f.write('{"registry_version": 2}')
|
||||
|
||||
with pytest.raises(ApplianceError):
|
||||
Appliance(registry, test_path)
|
||||
|
||||
Appliance(registry, "tests/registry/appliances/microcore-linux.json")
|
||||
|
||||
|
||||
def test_resolve_version(tmpdir):
|
||||
|
||||
with open("tests/registry/appliances/microcore-linux.json") as f:
|
||||
config = json.load(f)
|
||||
|
||||
new_config = Appliance(registry, "tests/registry/appliances/microcore-linux.json")
|
||||
assert new_config["versions"][0]["images"] == {"hda_disk_image": config["images"][0]}
|
||||
|
||||
|
||||
def test_search_images_for_version(linux_microcore_img, microcore_appliance):
|
||||
|
||||
detected = microcore_appliance.search_images_for_version("3.4.1")
|
||||
assert detected["name"] == "Micro Core Linux 3.4.1"
|
||||
assert detected["images"][0]["type"] == "hda_disk_image"
|
||||
assert detected["images"][0]["path"] == linux_microcore_img
|
||||
|
||||
|
||||
def test_search_images_for_version_unknow_version(microcore_appliance):
|
||||
|
||||
with pytest.raises(ApplianceError):
|
||||
detected = microcore_appliance.search_images_for_version("42")
|
||||
|
||||
|
||||
def test_search_images_for_version_missing_file(microcore_appliance):
|
||||
|
||||
with pytest.raises(ApplianceError):
|
||||
detected = microcore_appliance.search_images_for_version("4.0.2")
|
||||
|
||||
|
||||
def test_is_version_installable(linux_microcore_img, microcore_appliance):
|
||||
|
||||
assert microcore_appliance.is_version_installable("3.4.1")
|
||||
assert not microcore_appliance.is_version_installable("4.0.2")
|
||||
|
||||
|
||||
|
||||
272
tests/registry/test_config.py
Normal file
272
tests/registry/test_config.py
Normal file
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from gns3.registry.config import Config, ConfigException
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def empty_config(tmpdir):
|
||||
config = {
|
||||
"Servers": {
|
||||
"local_server": {
|
||||
"allow_console_from_anywhere": False,
|
||||
"auto_start": False,
|
||||
"console_end_port_range": 5000,
|
||||
"console_start_port_range": 2001,
|
||||
"host": "127.0.0.1",
|
||||
"images_path": str(tmpdir),
|
||||
"path": "",
|
||||
"port": 8000,
|
||||
"projects_path": str(tmpdir),
|
||||
"report_errors": False,
|
||||
"udp_end_port_range": 20000,
|
||||
"udp_start_port_range": 10000
|
||||
}
|
||||
},
|
||||
"Dynamips": {
|
||||
"allocate_aux_console_ports": False,
|
||||
"dynamips_path": "/Applications/GNS3.app/Contents/Resources/dynamips",
|
||||
"ghost_ios_support": True,
|
||||
"mmap_support": True,
|
||||
"routers": [
|
||||
{
|
||||
}
|
||||
],
|
||||
"sparse_memory_support": True,
|
||||
"use_local_server": True
|
||||
},
|
||||
"IOU": {
|
||||
"appliances": [
|
||||
{
|
||||
}
|
||||
],
|
||||
"iourc_path": "/Users/noplay/code/gns3/gns3-vagrant/images/iou/iourc.txt",
|
||||
"iouyap_path": "",
|
||||
"license_check": True,
|
||||
"use_local_server": False
|
||||
},
|
||||
"Qemu": {
|
||||
"use_local_server": True,
|
||||
"vms": [
|
||||
]
|
||||
}
|
||||
}
|
||||
path = str(tmpdir / "config")
|
||||
with open(path, "w+") as f:
|
||||
json.dump(config, f)
|
||||
return Config(path)
|
||||
|
||||
|
||||
def test_list_servers(empty_config):
|
||||
assert empty_config.servers == ["local"]
|
||||
|
||||
|
||||
def test_list_servers_vm_enable(tmpdir):
|
||||
config = {
|
||||
"Servers": {
|
||||
"vm": {
|
||||
"auto_start": True,
|
||||
}
|
||||
}
|
||||
}
|
||||
path = str(tmpdir / "config")
|
||||
with open(path, "w+") as f:
|
||||
json.dump(config, f)
|
||||
config = Config(path)
|
||||
assert config.servers == ["local", "vm"]
|
||||
|
||||
|
||||
def test_list_servers_remote_servers(tmpdir):
|
||||
config = {
|
||||
"Servers": {
|
||||
"remote_servers": [
|
||||
{
|
||||
"url": "http://darkside.moon:4242"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
path = str(tmpdir / "config")
|
||||
with open(path, "w+") as f:
|
||||
json.dump(config, f)
|
||||
config = Config(path)
|
||||
assert config.servers == ["local", "http://darkside.moon:4242"]
|
||||
|
||||
|
||||
def test_add_appliance_guest(empty_config, linux_microcore_img):
|
||||
with open("tests/registry/appliances/microcore-linux.json") as f:
|
||||
config = json.load(f)
|
||||
config["images"] = [
|
||||
{
|
||||
"type": "hda_disk_image",
|
||||
"path": linux_microcore_img
|
||||
}
|
||||
]
|
||||
empty_config.add_appliance(config, "local")
|
||||
assert empty_config._config["Qemu"]["vms"][0] == {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 1,
|
||||
"category": 2,
|
||||
"cpu_throttling": 0,
|
||||
"console_type": "telnet",
|
||||
"symbol": ":/symbols/qemu_guest.svg",
|
||||
"hda_disk_image": linux_microcore_img,
|
||||
"hdb_disk_image": "",
|
||||
"hdc_disk_image": "",
|
||||
"hdd_disk_image": "",
|
||||
"cdrom_image": "",
|
||||
"initrd_image": "",
|
||||
"kernel_command_line": "",
|
||||
"kernel_image": "",
|
||||
"legacy_networking": False,
|
||||
"name": "Micro Core Linux",
|
||||
"options": "",
|
||||
"process_priority": "normal",
|
||||
"qemu_path": "qemu-system-i386",
|
||||
"ram": 32,
|
||||
"server": "local"
|
||||
}
|
||||
|
||||
|
||||
def test_add_appliance_with_symbol(empty_config, linux_microcore_img):
|
||||
with open("tests/registry/appliances/microcore-linux.json") as f:
|
||||
config = json.load(f)
|
||||
config["images"] = [
|
||||
{
|
||||
"type": "hda_disk_image",
|
||||
"path": linux_microcore_img
|
||||
}
|
||||
]
|
||||
config["symbol"] = ":/symbols/asa.svg"
|
||||
empty_config.add_appliance(config, "local")
|
||||
assert empty_config._config["Qemu"]["vms"][0]["symbol"] == ":/symbols/asa.svg"
|
||||
|
||||
|
||||
def test_add_appliance_with_boot_priority(empty_config, linux_microcore_img):
|
||||
with open("tests/registry/appliances/microcore-linux.json") as f:
|
||||
config = json.load(f)
|
||||
config["images"] = [
|
||||
{
|
||||
"type": "hda_disk_image",
|
||||
"path": linux_microcore_img
|
||||
}
|
||||
]
|
||||
config["boot_priority"] = "dc"
|
||||
empty_config.add_appliance(config, "local")
|
||||
assert empty_config._config["Qemu"]["vms"][0]["boot_priority"] == "dc"
|
||||
|
||||
|
||||
def test_add_appliance_router_two_disk(empty_config):
|
||||
with open("tests/registry/appliances/arista-veos.json") as f:
|
||||
config = json.load(f)
|
||||
|
||||
|
||||
config["images"] = [
|
||||
{
|
||||
"type": "hda_disk_image",
|
||||
"path": "/a"
|
||||
},
|
||||
{
|
||||
"type": "hdb_disk_image",
|
||||
"path": "/b"
|
||||
}
|
||||
]
|
||||
|
||||
empty_config.add_appliance(config, "local")
|
||||
|
||||
assert empty_config._config["Qemu"]["vms"][0] == {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 8,
|
||||
"category": 0,
|
||||
"cpu_throttling": 0,
|
||||
"symbol": ":/symbols/router.svg",
|
||||
"hda_disk_image": "/a",
|
||||
"hdb_disk_image": "/b",
|
||||
"hdc_disk_image": "",
|
||||
"hdd_disk_image": "",
|
||||
"cdrom_image": "",
|
||||
"initrd_image": "",
|
||||
"kernel_command_line": "",
|
||||
"kernel_image": "",
|
||||
"legacy_networking": False,
|
||||
"name": "Arista vEOS",
|
||||
"options": "",
|
||||
"process_priority": "normal",
|
||||
"qemu_path": "qemu-system-x86_64",
|
||||
"ram": 2048,
|
||||
"console_type": "telnet",
|
||||
"server": "local"
|
||||
}
|
||||
|
||||
|
||||
def test_add_appliance_uniq(empty_config, linux_microcore_img):
|
||||
with open("tests/registry/appliances/microcore-linux.json") as f:
|
||||
config = json.load(f)
|
||||
|
||||
config["images"] = [
|
||||
{
|
||||
"type": "hda_disk_image",
|
||||
"path": linux_microcore_img
|
||||
}
|
||||
]
|
||||
empty_config.add_appliance(config, "local")
|
||||
|
||||
config["qemu"]["adapters"] = 2
|
||||
empty_config.add_appliance(config, "local")
|
||||
|
||||
assert len(empty_config._config["Qemu"]["vms"]) == 1
|
||||
assert empty_config._config["Qemu"]["vms"][0]["adapters"] == 2
|
||||
|
||||
|
||||
def test_add_appliance_path_relative_to_images_dir(empty_config, tmpdir, linux_microcore_img):
|
||||
with open("tests/registry/appliances/microcore-linux.json") as f:
|
||||
config = json.load(f)
|
||||
|
||||
config["images"] = [
|
||||
{
|
||||
"type": "hda_disk_image",
|
||||
"path": str(tmpdir / "QEMU" / "linux-microcore-3.4.1.img")
|
||||
}
|
||||
]
|
||||
|
||||
empty_config.add_appliance(config, "local")
|
||||
assert empty_config._config["Qemu"]["vms"][0]["hda_disk_image"] == "linux-microcore-3.4.1.img"
|
||||
|
||||
|
||||
def test_save(empty_config, linux_microcore_img):
|
||||
|
||||
with open("tests/registry/appliances/microcore-linux.json") as f:
|
||||
config = json.load(f)
|
||||
|
||||
config["images"] = [
|
||||
{
|
||||
"type": "hda_disk_image",
|
||||
"path": linux_microcore_img
|
||||
}
|
||||
]
|
||||
|
||||
empty_config.add_appliance(config, "local")
|
||||
empty_config.save()
|
||||
with open(empty_config.path) as f:
|
||||
assert "Micro Core" in f.read()
|
||||
49
tests/registry/test_image.py
Normal file
49
tests/registry/test_image.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from gns3.registry.image import Image
|
||||
|
||||
|
||||
def test_filename(linux_microcore_img):
|
||||
image = Image(linux_microcore_img)
|
||||
assert image.filename == "linux-microcore-3.4.1.img"
|
||||
|
||||
|
||||
def test_md5sum(linux_microcore_img):
|
||||
image = Image(linux_microcore_img)
|
||||
assert image.md5sum == "5d41402abc4b2a76b9719d911017c592"
|
||||
assert os.path.exists(linux_microcore_img + ".md5sum")
|
||||
assert open(linux_microcore_img + ".md5sum").read() == "5d41402abc4b2a76b9719d911017c592"
|
||||
|
||||
|
||||
def test_filesize(linux_microcore_img):
|
||||
image = Image(linux_microcore_img)
|
||||
assert image.filesize == 5
|
||||
|
||||
|
||||
def test_md5sum_from_cache(tmpdir):
|
||||
path = str(tmpdir / "test.img")
|
||||
open(path, "w+").close()
|
||||
|
||||
with open(path + ".md5sum", "w+") as f:
|
||||
f.write("56f46611dfa80d0eead602cbb3f6dcee")
|
||||
|
||||
image = Image(path)
|
||||
assert image.md5sum == "56f46611dfa80d0eead602cbb3f6dcee"
|
||||
58
tests/registry/test_registry.py
Normal file
58
tests/registry/test_registry.py
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
from gns3.registry.registry import Registry, RegistryError
|
||||
|
||||
|
||||
def test_search_image_file(tmpdir):
|
||||
|
||||
os.makedirs(str(tmpdir / "QEMU"))
|
||||
with open(str(tmpdir / "QEMU" / "a"), "w+") as f:
|
||||
f.write("ALPHA")
|
||||
with open(str(tmpdir / "QEMU" / "b"), "w+") as f:
|
||||
f.write("BETA")
|
||||
|
||||
registry = Registry(str(tmpdir))
|
||||
image = registry.search_image_file("36b84f8e3fba5bf993e3ba352d62d146")
|
||||
assert image == str(tmpdir / "QEMU" / "b")
|
||||
|
||||
assert registry.search_image_file("00000000000000000000000000000000") is None
|
||||
|
||||
|
||||
def test_list_images(tmpdir):
|
||||
os.makedirs(str(tmpdir / "QEMU"))
|
||||
with open(str(tmpdir / "QEMU" / ".DS_Store"), "w+") as f:
|
||||
f.write("garbage")
|
||||
with open(str(tmpdir / "QEMU" / "a"), "w+") as f:
|
||||
f.write("ALPHA")
|
||||
with open(str(tmpdir / "QEMU" / "a.md5sum"), "w+") as f:
|
||||
f.write("e13d0d1c0b3999ae2386bba70417930c")
|
||||
with open(str(tmpdir / "QEMU" / "b"), "w+") as f:
|
||||
f.write("BETA")
|
||||
|
||||
|
||||
registry = Registry(str(tmpdir))
|
||||
images = registry.list_images()
|
||||
assert len(images) == 2
|
||||
|
||||
Reference in New Issue
Block a user