Initial version of an appliance file format

This commit is contained in:
Julien Duponchelle
2015-09-09 11:36:06 +02:00
parent bd0fabd1f6
commit fc5bf2dc4b
20 changed files with 1403 additions and 11 deletions

162
gns3/appliance_window.py Normal file
View 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()

View File

@@ -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):

View File

119
gns3/registry/appliance.py Normal file
View 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
View 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
View 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
View 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

View 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>

View 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

View File

@@ -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

View 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 %}

File diff suppressed because one or more lines are too long

View File

@@ -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": [

View File

@@ -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

View 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"
}
}
]
}

View 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"
}
}
]
}

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

View 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()

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

View 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