Merge branch '2.0' into 2.1

This commit is contained in:
Julien Duponchelle committed 2017-04-26 17:22:14 +02:00
commit 273a711459
37 files changed
+435 -70

No files matched your search

+9 -4
View File
@@ -20,10 +20,10 @@ Docker server module.
"""
import sys
import json
import asyncio
import logging
import aiohttp
import json
from gns3server.utils import parse_version
from gns3server.utils.asyncio import locked_coroutine
from gns3server.compute.base_manager import BaseManager
@@ -33,7 +33,7 @@ from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Err
log = logging.getLogger(__name__)
DOCKER_MINIMUM_API_VERSION = "1.21"
DOCKER_MINIMUM_API_VERSION = "1.25"
class Docker(BaseManager):
@@ -113,7 +113,7 @@ class Docker(BaseManager):
:returns: HTTP response
"""
data = json.dumps(data)
url = "http://docker/" + path
url = "http://docker/v" + DOCKER_MINIMUM_API_VERSION + "/" + path
if timeout is None:
timeout = 60 * 60 * 24 * 31 # One month timeout
@@ -134,6 +134,8 @@ class Docker(BaseManager):
)
except (aiohttp.ClientResponseError, aiohttp.ClientOSError) as e:
raise DockerError("Docker has returned an error: {}".format(str(e)))
except (asyncio.TimeoutError):
raise DockerError("Docker timeout " + method + " " + path)
if response.status >= 300:
body = yield from response.read()
try:
@@ -187,7 +189,10 @@ class Docker(BaseManager):
# The pull api will stream status via an HTTP JSON stream
content = ""
while True:
chunk = yield from response.content.read(1024)
try:
chunk = yield from response.content.read(1024)
except aiohttp.errors.ServerDisconnectedError:
break
if not chunk:
break
content += chunk.decode("utf-8")
+3
View File
@@ -361,6 +361,7 @@ class DockerVM(BaseNode):
try:
yield from self._add_ubridge_connection(nio, adapter_number)
except UbridgeNamespaceError:
log.error("Container {} failed to start", self.name)
yield from self.stop()
# The container can crash soon after the start, this means we can not move the interface to the container namespace
@@ -517,6 +518,8 @@ class DockerVM(BaseNode):
state = yield from self._get_container_state()
if state == "running":
return True
if self.status == "started": # The container crashed we need to clean
yield from self.stop()
return False
@asyncio.coroutine
+1 -1
View File
@@ -1379,7 +1379,7 @@ class QemuVM(BaseNode):
# special case, sata controller doesn't exist in Qemu
options.extend(["-device", 'ahci,id=ahci{},bus=pci.{}'.format(disk_index, disk_index)])
options.extend(["-drive", 'file={},if=none,id=drive-sata-disk{},index={},media=disk'.format(disk, disk_index, disk_index)])
options.extend(["-device", 'ide-drive,drive=drive-sata-disk{},bus=ahci{}.0'.format(disk_index, disk_index)])
options.extend(["-device", 'ide-drive,drive=drive-sata-disk{},bus=ahci{}.0,id=drive-sata-disk{}'.format(disk_index, disk_index, disk_index)])
else:
options.extend(["-drive", 'file={},if={},index={},media=disk'.format(disk, interface, disk_index)])
+3 -2
View File
@@ -588,8 +588,9 @@ class VMware(BaseManager):
for vm_settings in vm_entries.values():
if "displayname" in vm_settings and "config" in vm_settings:
log.debug('Found VM named "{}" with VMX file "{}"'.format(vm_settings["displayname"], vm_settings["config"]))
vmware_vms.append({"vmname": vm_settings["displayname"], "vmx_path": vm_settings["config"]})
if os.path.exists(vm_settings["config"]):
log.debug('Found VM named "{}" with VMX file "{}"'.format(vm_settings["displayname"], vm_settings["config"]))
vmware_vms.append({"vmname": vm_settings["displayname"], "vmx_path": vm_settings["config"]})
return vmware_vms
def _get_vms_from_directory(self, directory):
+12 -5
View File
@@ -35,6 +35,7 @@ from ..version import __version__
from .topology import load_topology
from .gns3vm import GNS3VM
from ..utils.get_resource import get_resource
from .gns3vm.gns3_vm_error import GNS3VMError
import logging
log = logging.getLogger(__name__)
@@ -159,10 +160,13 @@ class Controller:
for c in computes:
try:
yield from self.add_compute(**c)
except aiohttp.web_exceptions.HTTPConflict:
except (aiohttp.web_exceptions.HTTPConflict):
pass # Skip not available servers at loading
yield from self.load_projects()
yield from self.gns3vm.auto_start_vm()
try:
yield from self.gns3vm.auto_start_vm()
except GNS3VMError as e:
log.warn(str(e))
yield from self._project_auto_open()
def _update_config(self):
@@ -215,9 +219,12 @@ class Controller:
"password": c.password,
"compute_id": c.id
})
os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
with open(self._config_file, 'w+') as f:
json.dump(data, f, indent=4)
try:
os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
with open(self._config_file, 'w+') as f:
json.dump(data, f, indent=4)
except OSError as e:
log.error("Can't write the configuration {}: {}".format(self._config_file, str(e)))
@asyncio.coroutine
def _load_controller_settings(self):
+8 -4
View File
@@ -18,6 +18,7 @@
import sys
import copy
import asyncio
import aiohttp
from ...utils.asyncio import locked_coroutine
from .vmware_gns3_vm import VMwareGNS3VM
@@ -242,10 +243,13 @@ class GNS3VM:
yield from self.start()
except GNS3VMError as e:
# User will receive the error later when they will try to use the node
yield from self._controller.add_compute(compute_id="vm",
name="GNS3 VM ({})".format(self.current_engine().vmname),
host=None,
force=True)
try:
yield from self._controller.add_compute(compute_id="vm",
name="GNS3 VM ({})".format(self.current_engine().vmname),
host=None,
force=True)
except aiohttp.web.HTTPConflict:
pass
log.error("Can't start the GNS3 VM: {}", str(e))
@asyncio.coroutine
+1 -1
View File
@@ -674,7 +674,7 @@ class Project:
self.dump()
# We catch all error to be able to rollback the .gns3 to the previous state
except Exception as e:
for compute in self._project_created_on_compute:
for compute in list(self._project_created_on_compute):
try:
yield from compute.post("/projects/{}/close".format(self._id))
# We don't care if a compute is down at this step
+5 -2
View File
@@ -291,8 +291,11 @@ def _convert_1_3_later(topo, topo_path):
except KeyError:
node["compute_id"] = "local"
node["console_type"] = old_node["properties"].get("console_type", "telnet")
node["name"] = old_node["label"]["text"]
node["label"] = _convert_label(old_node["label"])
if "label" in old_node:
node["name"] = old_node["label"]["text"]
node["label"] = _convert_label(old_node["label"])
else:
node["name"] = old_node["properties"]["name"]
node["node_id"] = old_node.get("vm_id", str(uuid.uuid4()))
node["symbol"] = old_node.get("symbol", None)
+1 -1
View File
@@ -54,7 +54,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "sync+https://7c028290d17b4035916285b304d42311:ddf752e704c7423cacab93f8e34f713c@sentry.io/38482"
DSN = "sync+https://19cca90b55874be5862caf9b507fbd7b:1c0897efd092467a874e89b2e4803b29@sentry.io/38482"
if hasattr(sys, "frozen"):
cacert = get_resource("cacert.pem")
if cacert is not None and os.path.isfile(cacert):
@@ -20,6 +20,7 @@ import aiohttp
from gns3server.web.route import Route
from gns3server.controller import Controller
from gns3server.utils import force_unix_path
from gns3server.schemas.node import (
NODE_OBJECT_SCHEMA,
@@ -337,7 +338,7 @@ class NodeHandler:
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
path = request.match_info["path"]
path = os.path.normpath(path)
path = force_unix_path(path)
# Raise error if user try to escape
if path[0] == ".":
+230
View File
@@ -0,0 +1,230 @@
# -*- 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/>.
from aiohttp.web import HTTPConflict
from ...web.route import Route
from ...schemas.nio import NIO_SCHEMA
from ...schemas.vpcs import VPCS_CREATE_SCHEMA
from ...schemas.vpcs import VPCS_UPDATE_SCHEMA
from ...schemas.vpcs import VPCS_OBJECT_SCHEMA
from ...modules.vpcs import VPCS
class VPCSHandler:
"""
API entry points for VPCS.
"""
@classmethod
@Route.post(
r"/projects/{project_id}/vpcs/vms",
parameters={
"project_id": "UUID for the project"
},
status_codes={
201: "Instance created",
400: "Invalid request",
409: "Conflict"
},
description="Create a new VPCS instance",
input=VPCS_CREATE_SCHEMA,
output=VPCS_OBJECT_SCHEMA)
def create(request, response):
vpcs = VPCS.instance()
vm = yield from vpcs.create_vm(request.json["name"],
request.match_info["project_id"],
request.json.get("vm_id"),
console=request.json.get("console"),
startup_script=request.json.get("startup_script"))
response.set_status(201)
response.json(vm)
@classmethod
@Route.get(
r"/projects/{project_id}/vpcs/vms/{vm_id}",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance"
},
status_codes={
200: "Success",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Get a VPCS instance",
output=VPCS_OBJECT_SCHEMA)
def show(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
response.json(vm)
@classmethod
@Route.put(
r"/projects/{project_id}/vpcs/vms/{vm_id}",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance"
},
status_codes={
200: "Instance updated",
400: "Invalid request",
404: "Instance doesn't exist",
409: "Conflict"
},
description="Update a VPCS instance",
input=VPCS_UPDATE_SCHEMA,
output=VPCS_OBJECT_SCHEMA)
def update(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
vm.name = request.json.get("name", vm.name)
vm.console = request.json.get("console", vm.console)
vm.startup_script = request.json.get("startup_script", vm.startup_script)
response.json(vm)
@classmethod
@Route.delete(
r"/projects/{project_id}/vpcs/vms/{vm_id}",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance"
},
status_codes={
204: "Instance deleted",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Delete a VPCS instance")
def delete(request, response):
yield from VPCS.instance().delete_vm(request.match_info["vm_id"])
response.set_status(204)
@classmethod
@Route.post(
r"/projects/{project_id}/vpcs/vms/{vm_id}/start",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance"
},
status_codes={
204: "Instance started",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Start a VPCS instance",
output=VPCS_OBJECT_SCHEMA)
def start(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
yield from vm.start()
response.json(vm)
@classmethod
@Route.post(
r"/projects/{project_id}/vpcs/vms/{vm_id}/stop",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance"
},
status_codes={
204: "Instance stopped",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Stop a VPCS instance")
def stop(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
yield from vm.stop()
response.set_status(204)
@classmethod
@Route.post(
r"/projects/{project_id}/vpcs/vms/{vm_id}/reload",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance",
},
status_codes={
204: "Instance reloaded",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Reload a VPCS instance")
def reload(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
yield from vm.reload()
response.set_status(204)
@Route.post(
r"/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance",
"adapter_number": "Network adapter where the nio is located",
"port_number": "Port where the nio should be added"
},
status_codes={
201: "NIO created",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Add a NIO to a VPCS instance",
input=NIO_SCHEMA,
output=NIO_SCHEMA)
def create_nio(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
nio_type = request.json["type"]
if nio_type not in ("nio_udp", "nio_tap"):
raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type))
nio = vpcs_manager.create_nio(vm.vpcs_path(), request.json)
vm.port_add_nio_binding(int(request.match_info["port_number"]), nio)
response.set_status(201)
response.json(nio)
@classmethod
@Route.delete(
r"/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
parameters={
"project_id": "UUID for the project",
"vm_id": "UUID for the instance",
"adapter_number": "Network adapter where the nio is located",
"port_number": "Port from where the nio should be removed"
},
status_codes={
204: "NIO deleted",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Remove a NIO from a VPCS instance")
def delete_nio(request, response):
vpcs_manager = VPCS.instance()
vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
vm.port_remove_nio_binding(int(request.match_info["port_number"]))
response.set_status(204)
+1 -1
View File
@@ -163,7 +163,7 @@ def pid_lock(path):
pid = int(f.read())
try:
os.kill(pid, 0) # If the proces is not running kill return an error
except OSError:
except (OSError, SystemError):
pid = None
except OSError as e:
log.critical("Can't open pid file %s: %s", pid, str(e))
+2 -1
View File
@@ -15,6 +15,7 @@
# 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 copy
ATM_SWITCH_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
@@ -81,5 +82,5 @@ ATM_SWITCH_OBJECT_SCHEMA = {
"required": ["name", "node_id", "project_id"]
}
ATM_SWITCH_UPDATE_SCHEMA = ATM_SWITCH_OBJECT_SCHEMA
ATM_SWITCH_UPDATE_SCHEMA = copy.deepcopy(ATM_SWITCH_OBJECT_SCHEMA)
del ATM_SWITCH_UPDATE_SCHEMA["required"]
+2 -1
View File
@@ -15,6 +15,7 @@
# 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 copy
from .port import PORT_OBJECT_SCHEMA
HOST_INTERFACE_SCHEMA = {
@@ -136,5 +137,5 @@ CLOUD_OBJECT_SCHEMA = {
"required": ["name", "node_id", "project_id", "ports_mapping"]
}
CLOUD_UPDATE_SCHEMA = CLOUD_OBJECT_SCHEMA
CLOUD_UPDATE_SCHEMA = copy.deepcopy(CLOUD_OBJECT_SCHEMA)
del CLOUD_UPDATE_SCHEMA["required"]
+3 -2
View File
@@ -15,6 +15,7 @@
# 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 copy
from .capabilities import CAPABILITIES_SCHEMA
COMPUTE_CREATE_SCHEMA = {
@@ -52,10 +53,10 @@ COMPUTE_CREATE_SCHEMA = {
}
},
"additionalProperties": False,
"required": ["compute_id", "protocol", "host", "port"]
"required": ["protocol", "host", "port"]
}
COMPUTE_UPDATE_SCHEMA = COMPUTE_CREATE_SCHEMA
COMPUTE_UPDATE_SCHEMA = copy.deepcopy(COMPUTE_CREATE_SCHEMA)
del COMPUTE_UPDATE_SCHEMA["required"]
COMPUTE_OBJECT_SCHEMA = {
+2 -1
View File
@@ -15,6 +15,7 @@
# 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 copy
ETHERNET_HUB_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
@@ -129,5 +130,5 @@ ETHERNET_HUB_OBJECT_SCHEMA = {
"required": ["name", "node_id", "project_id", "ports_mapping"]
}
ETHERNET_HUB_UPDATE_SCHEMA = ETHERNET_HUB_OBJECT_SCHEMA
ETHERNET_HUB_UPDATE_SCHEMA = copy.deepcopy(ETHERNET_HUB_OBJECT_SCHEMA)
del ETHERNET_HUB_UPDATE_SCHEMA["required"]
+2 -1
View File
@@ -15,6 +15,7 @@
# 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 copy
ETHERNET_SWITCH_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
@@ -153,5 +154,5 @@ ETHERNET_SWITCH_OBJECT_SCHEMA = {
"required": ["name", "node_id", "project_id"]
}
ETHERNET_SWITCH_UPDATE_SCHEMA = ETHERNET_SWITCH_OBJECT_SCHEMA
ETHERNET_SWITCH_UPDATE_SCHEMA = copy.deepcopy(ETHERNET_SWITCH_OBJECT_SCHEMA)
del ETHERNET_SWITCH_UPDATE_SCHEMA["required"]
+2 -1
View File
@@ -15,6 +15,7 @@
# 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 copy
FRAME_RELAY_SWITCH_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
@@ -81,5 +82,5 @@ FRAME_RELAY_SWITCH_OBJECT_SCHEMA = {
"required": ["name", "node_id", "project_id"]
}
FRAME_RELAY_SWITCH_UPDATE_SCHEMA = FRAME_RELAY_SWITCH_OBJECT_SCHEMA
FRAME_RELAY_SWITCH_UPDATE_SCHEMA = copy.deepcopy(FRAME_RELAY_SWITCH_OBJECT_SCHEMA)
del FRAME_RELAY_SWITCH_UPDATE_SCHEMA["required"]
+2 -1
View File
@@ -15,6 +15,7 @@
# 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 copy
from .label import LABEL_OBJECT_SCHEMA
NODE_TYPE_SCHEMA = {
@@ -234,5 +235,5 @@ NODE_OBJECT_SCHEMA = {
}
NODE_CREATE_SCHEMA = NODE_OBJECT_SCHEMA
NODE_UPDATE_SCHEMA = NODE_OBJECT_SCHEMA
NODE_UPDATE_SCHEMA = copy.deepcopy(NODE_OBJECT_SCHEMA)
del NODE_UPDATE_SCHEMA["required"]
+2 -2
View File
@@ -5,7 +5,7 @@
</h3>
<ul>
<li><a href="https://gns3.com">Website</a></li>
<li><a href="http://api.gns3.net">API documentation</a></li>
<li><a href="http://docs.gns3.com">Documentation</a></li>
</ul>
<p>If you are looking for uploading the IOU. You can since 1.4 upload them directly from the client see: <a href="https://gns3.com/support/docs/how-to-configure-non-native-io-3">this documentation</a>.</p>
<p>If you are looking for uploading the IOU. You can since 1.4 upload them directly from the client see: <a href="http://docs.gns3.com/1PKfYwR78QP_Z3jqxBQ1pdy6SsqM27qhvdCvSmIizRh4">this documentation</a>.</p>
{% endblock %}
+1 -1
View File
@@ -199,7 +199,7 @@ class Hypervisor(UBridgeHypervisor):
try:
yield from wait_for_process_termination(self._process, timeout=3)
except asyncio.TimeoutError:
if self._process.returncode is None:
if self._process and self._process.returncode is None:
log.warn("uBridge process {} is still running... killing it".format(self._process.pid))
try:
self._process.kill()
+1 -1
View File
@@ -79,7 +79,7 @@ def list_images(type):
images.append({
"filename": filename,
"path": path,
"path": force_unix_path(path),
"md5sum": md5sum(os.path.join(root, filename)),
"filesize": os.stat(os.path.join(root, filename)).st_size})
except OSError as e:
+6 -2
View File
@@ -17,7 +17,7 @@
import io
import struct
from xml.etree.ElementTree import ElementTree
from xml.etree.ElementTree import ElementTree, ParseError
def get_size(data, default_width=0, default_height=0):
@@ -95,7 +95,11 @@ def get_size(data, default_width=0, default_height=0):
filetype = "svg"
fhandle = io.BytesIO(data)
tree = ElementTree()
tree.parse(fhandle)
try:
tree.parse(fhandle)
except ParseError:
raise ValueError("Invalid SVG file")
root = tree.getroot()
try: