Merge branch '2.0' into 2.1

This commit is contained in:
Julien Duponchelle committed 2017-03-30 10:16:16 +02:00
commit 4f1b738ef5
275 files changed
+5384 -272

No files matched your search

+6 -2
View File
@@ -28,7 +28,7 @@ import platform
from gns3server.utils.interfaces import interfaces
from ..compute.port_manager import PortManager
from ..utils.asyncio import wait_run_in_executor
from ..utils.asyncio import wait_run_in_executor, locked_coroutine
from ..utils.asyncio.telnet_server import AsyncioTelnetServer
from ..ubridge.hypervisor import Hypervisor
from ..ubridge.ubridge_error import UbridgeError
@@ -519,12 +519,16 @@ class BaseNode:
except UbridgeError as e:
raise UbridgeError("{}: {}".format(e, self._ubridge_hypervisor.read_stdout()))
@asyncio.coroutine
@locked_coroutine
def _start_ubridge(self):
"""
Starts uBridge (handles connections to and from this node).
"""
# Prevent us to start multiple ubridge
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
return
if self.ubridge_path is None:
raise NodeError("uBridge is not available, path doesn't exist, or you just installed GNS3 and need to restart your user session to refresh user permissions.")
+74 -17
View File
@@ -24,14 +24,14 @@ import asyncio
import logging
import aiohttp
import json
import sys
from gns3server.utils import parse_version
from gns3server.utils.asyncio import locked_coroutine
from gns3server.compute.base_manager import BaseManager
from gns3server.compute.docker.docker_vm import DockerVM
from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error
log = logging.getLogger(__name__)
from ..base_manager import BaseManager
from .docker_vm import DockerVM
from .docker_error import *
DOCKER_MINIMUM_API_VERSION = "1.21"
@@ -46,30 +46,38 @@ class Docker(BaseManager):
self._connected = False
# Allow locking during ubridge operations
self.ubridge_lock = asyncio.Lock()
self._connector = None
self._session = None
@asyncio.coroutine
def connector(self):
if not self._connected or self._connector.closed:
if not sys.platform.startswith("linux"):
raise DockerError("Docker is supported only on Linux")
def _check_connection(self):
if not self._connected:
try:
self._connector = aiohttp.connector.UnixConnector(self._server_url, conn_timeout=2)
self._connected = True
connector = self.connector()
version = yield from self.query("GET", "version")
except (aiohttp.errors.ClientOSError, FileNotFoundError):
self._connected = False
raise DockerError("Can't connect to docker daemon")
if parse_version(version["ApiVersion"]) < parse_version(DOCKER_MINIMUM_API_VERSION):
raise DockerError("Docker API version is {}. GNS3 requires a minimum API version of {}".format(version["ApiVersion"], DOCKER_MINIMUM_API_VERSION))
def connector(self):
if self._connector is None or self._connector.closed:
if not sys.platform.startswith("linux"):
raise DockerError("Docker is supported only on Linux")
try:
self._connector = aiohttp.connector.UnixConnector(self._server_url, conn_timeout=2, limit=None)
except (aiohttp.errors.ClientOSError, FileNotFoundError):
raise DockerError("Can't connect to docker daemon")
return self._connector
@asyncio.coroutine
def unload(self):
yield from super().unload()
if self._connected:
self._connector.close()
if self._connector and not self._connector.closed:
yield from self._connector.close()
@asyncio.coroutine
def query(self, method, path, data={}, params={}):
@@ -84,7 +92,7 @@ class Docker(BaseManager):
response = yield from self.http_query(method, path, data=data, params=params)
body = yield from response.read()
if len(body):
if body and len(body):
if response.headers['CONTENT-TYPE'] == 'application/json':
body = json.loads(body.decode("utf-8"))
else:
@@ -93,7 +101,7 @@ class Docker(BaseManager):
return body
@asyncio.coroutine
def http_query(self, method, path, data={}, params={}):
def http_query(self, method, path, data={}, params={}, timeout=300):
"""
Make a query to the docker daemon
@@ -101,18 +109,28 @@ class Docker(BaseManager):
:param path: Endpoint in API
:param data: Dictionnary with the body. Will be transformed to a JSON
:param params: Parameters added as a query arg
:param timeout: Timeout
:returns: HTTP response
"""
data = json.dumps(data)
url = "http://docker/" + path
if timeout is None:
timeout = 60 * 60 * 24 * 31 # One month timeout
try:
response = yield from aiohttp.request(
if path != "version": # version is use by check connection
yield from self._check_connection()
if self._session is None or self._session.closed:
connector = self.connector()
self._session = aiohttp.ClientSession(connector=connector)
response = yield from self._session.request(
method,
url,
connector=(yield from self.connector()),
params=params,
data=data,
headers={"content-type": "application/json", },
timeout=timeout
)
except (aiohttp.ClientResponseError, aiohttp.ClientOSError) as e:
raise DockerError("Docker has returned an error: {}".format(str(e)))
@@ -143,11 +161,50 @@ class Docker(BaseManager):
url = "http://docker/" + path
connection = yield from aiohttp.ws_connect(url,
connector=(yield from self.connector()),
connector=self.connector(),
origin="http://docker",
autoping=True)
return connection
@locked_coroutine
def pull_image(self, image, progress_callback=None):
"""
Pull image from docker repository
:params image: Image name
:params progress_callback: A function that receive a log message about image download progress
"""
try:
yield from self.query("GET", "images/{}/json".format(image))
return # We already have the image skip the download
except DockerHttp404Error:
pass
if progress_callback:
progress_callback("Pull {} from docker hub".format(image))
response = yield from self.http_query("POST", "images/create", params={"fromImage": image}, timeout=None)
# The pull api will stream status via an HTTP JSON stream
content = ""
while True:
chunk = yield from response.content.read(1024)
if not chunk:
break
content += chunk.decode("utf-8")
try:
while True:
content = content.lstrip(" \r\n\t")
answer, index = json.JSONDecoder().raw_decode(content)
if "progress" in answer and progress_callback:
progress_callback("Pulling image {}:{}: {}".format(image, answer["id"], answer["progress"]))
content = content[index:]
except ValueError: # Partial JSON
pass
response.close()
if progress_callback:
progress_callback("Success pulling image {}".format(image))
@asyncio.coroutine
def list_images(self):
"""Gets Docker image list.
+3 -21
View File
@@ -24,7 +24,6 @@ import shutil
import psutil
import shlex
import aiohttp
import json
import os
from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer
@@ -767,26 +766,9 @@ class DockerVM(BaseNode):
"""
Pull image from docker repository
"""
log.info("Pull %s from docker hub", image)
response = yield from self.manager.http_query("POST", "images/create", params={"fromImage": image})
# The pull api will stream status via an HTTP JSON stream
content = ""
while True:
chunk = yield from response.content.read(1024)
if not chunk:
break
content += chunk.decode("utf-8")
try:
while True:
content = content.lstrip(" \r\n\t")
answer, index = json.JSONDecoder().raw_decode(content)
if "progress" in answer:
self.project.emit("log.info", {"message": "Pulling image {}:{}: {}".format(self._image, answer["id"], answer["progress"])})
content = content[index:]
except ValueError: # Partial JSON
pass
self.project.emit("log.info", {"message": "Success pulling image {}".format(self._image)})
def callback(msg):
self.project.emit("log.info", {"message": msg})
yield from self.manager.pull_image(image, progress_callback=callback)
@asyncio.coroutine
def _start_ubridge_capture(self, adapter_number, output_file):
@@ -88,7 +88,7 @@ class EthernetSwitch(Device):
:param ports: ports info
"""
if ports != self._ports:
if len(self._nios) > 0:
if len(self._nios) > 0 and len(ports) != len(self._ports):
raise NodeError("Can't modify a switch already connected.")
port_number = 0
@@ -99,6 +99,13 @@ class EthernetSwitch(Device):
self._ports = ports
@asyncio.coroutine
def update_port_settings(self):
for port_settings in self._ports:
port_number = port_settings["port_number"]
if port_number in self._nios and self._nios[port_number] is not None:
yield from self.set_port_settings(port_number, port_settings)
@asyncio.coroutine
def create(self):
@@ -239,10 +239,10 @@ class FrameRelaySwitch(Device):
"""
if port1 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port1))
return
if port2 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port2))
return
nio1 = self._nios[port1]
nio2 = self._nios[port2]
+1 -1
View File
@@ -566,7 +566,7 @@ class IOUVM(BaseNode):
self._terminate_process_iou()
if returncode != 0:
if returncode == 11:
if returncode == -11:
message = "{} process has stopped, return code: {}. This could be an issue with the image using a different image can fix the issue.\n{}".format(process_name, returncode, self.read_iou_stdout())
else:
message = "{} process has stopped, return code: {}\n{}".format(process_name, returncode, self.read_iou_stdout())
+1 -1
View File
@@ -74,7 +74,7 @@ class Project:
try:
if os.path.exists(self.tmp_working_directory()):
shutil.rmtree(self.tmp_working_directory())
except OSError:
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not clean project directory: {}".format(e))
log.info("Project {id} with path '{path}' created".format(path=self._path, id=self._id))
@@ -1014,7 +1014,9 @@ class VirtualBoxVM(BaseNode):
if self.ubridge:
yield from self._ubridge_send("bridge delete {name}".format(name="VBOX-{}-{}".format(self._id, adapter_number)))
yield from self._control_vm("setlinkstate{} off".format(adapter_number + 1))
vm_state = yield from self._get_vm_state()
if vm_state == "running":
yield from self._control_vm("setlinkstate{} off".format(adapter_number + 1))
else:
vm_state = yield from self._get_vm_state()
if vm_state == "running":
+1 -1
View File
@@ -396,7 +396,7 @@ class VMware(BaseManager):
try:
stdout_data, _ = yield from asyncio.wait_for(process.communicate(), timeout=timeout)
except asyncio.TimeoutError:
raise VMwareError("vmrun has timed out after {} seconds!\nTry to run {} in a terminal to see more informations.\nAnd make sure GNS3 and VMware run under the same user.".format(timeout, command_string))
raise VMwareError("vmrun has timed out after {} seconds!\nTry to run {} in a terminal to see more informations.\n\nMake sure GNS3 and VMware run under the same user and whitelist vmrun.exe in your antivirus.".format(timeout, command_string))
if process.returncode:
# vmrun print errors on stdout
+9
View File
@@ -50,6 +50,7 @@ class Config:
# Monitor configuration files for changes
self._watched_files = {}
self._watch_callback = []
if sys.platform.startswith("win"):
@@ -116,6 +117,12 @@ class Config:
self.clear()
self._watch_config_file()
def listen_for_config_changes(self, callback):
"""
Call the callback when the configuration file change
"""
self._watch_callback.append(callback)
@property
def profile(self):
"""
@@ -143,6 +150,8 @@ class Config:
self.read_config()
for section in self._override_config:
self.set_section_config(section, self._override_config[section])
for callback in self._watch_callback:
callback()
def reload(self):
"""
+39 -19
View File
@@ -16,7 +16,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import json
import uuid
import socket
import shutil
import asyncio
@@ -55,7 +57,7 @@ class Controller:
# Store settings shared by the different GUI will be replace by dedicated API later
self._settings = None
self._local_server = None
self._config_file = os.path.join(Config.instance().config_dir, "gns3_controller.conf")
log.info("Load controller configuration file {}".format(self._config_file))
@@ -75,6 +77,7 @@ class Controller:
log.info("Start controller")
self.load_base_files()
server_config = Config.instance().get_section_config("Server")
Config.instance().listen_for_config_changes(self._update_config)
host = server_config.get("host", "localhost")
# If console_host is 0.0.0.0 client will use the ip they use
@@ -88,15 +91,19 @@ class Controller:
name = "Main server"
computes = yield from self._load_controller_settings()
yield from self.add_compute(compute_id="local",
name=name,
protocol=server_config.get("protocol", "http"),
host=host,
console_host=console_host,
port=server_config.getint("port", 3080),
user=server_config.get("user", ""),
password=server_config.get("password", ""),
force=True)
try:
self._local_server = yield from self.add_compute(compute_id="local",
name=name,
protocol=server_config.get("protocol", "http"),
host=host,
console_host=console_host,
port=server_config.getint("port", 3080),
user=server_config.get("user", ""),
password=server_config.get("password", ""),
force=True)
except aiohttp.web_exceptions.HTTPConflict as e:
log.fatal("Can't acces to the local server, make sure anything else is not running on the same port")
sys.exit(1)
for c in computes:
try:
yield from self.add_compute(**c)
@@ -106,6 +113,16 @@ class Controller:
yield from self.gns3vm.auto_start_vm()
yield from self._project_auto_open()
def _update_config(self):
"""
Call this when the server configuration file
change
"""
if self._local_server:
server_config = Config.instance().get_section_config("Server")
self._local_server.user = server_config.get("user")
self._local_server.password = server_config.get("password")
@asyncio.coroutine
def stop(self):
log.info("Stop controller")
@@ -239,14 +256,17 @@ class Controller:
data = json.load(f)
server_settings = data.get("Servers", {})
for remote in server_settings.get("remote_servers", []):
yield from self.add_compute(
host=remote.get("host", "localhost"),
port=remote.get("port", 3080),
protocol=remote.get("protocol", "http"),
name=remote.get("url"),
user=remote.get("user"),
password=remote.get("password")
)
try:
yield from self.add_compute(
host=remote.get("host", "localhost"),
port=remote.get("port", 3080),
protocol=remote.get("protocol", "http"),
name=remote.get("url"),
user=remote.get("user"),
password=remote.get("password")
)
except aiohttp.web.HTTPConflict:
pass # if the server is broken we skip it
if "vm" in server_settings:
vmname = None
vm_settings = server_settings["vm"]
@@ -282,12 +302,12 @@ class Controller:
"""
Store settings shared by the different GUI will be replace by dedicated API later. Dictionnary
"""
assert self._settings is not None
return self._settings
@settings.setter
def settings(self, val):
self._settings = val
self._settings["modification_uuid"] = str(uuid.uuid4()) # We add a modification id to the settings it's help the gui to detect changes
self.save()
self.notification.emit("settings.updated", val)
+4 -2
View File
@@ -128,7 +128,7 @@ class Compute:
self._user = user.strip()
if password:
self._password = password.strip()
self._auth = aiohttp.BasicAuth(self._user, self._password)
self._auth = aiohttp.BasicAuth(self._user, self._password, "utf-8")
else:
self._password = None
self._auth = aiohttp.BasicAuth(self._user, "")
@@ -397,6 +397,8 @@ class Compute:
raise aiohttp.web.HTTPConflict(text="Invalid auth for server {}".format(self._id))
except aiohttp.web.HTTPServiceUnavailable:
raise aiohttp.web.HTTPConflict(text="The server {} is unavailable".format(self._id))
except ValueError:
raise aiohttp.web.HTTPConflict(text="Invalid server url for server {}".format(self._id))
if "version" not in response.json:
self._http_session.close()
@@ -502,7 +504,7 @@ class Compute:
response = yield from self._session().request(method, url, headers=headers, data=data, auth=self._auth, chunked=chunked, timeout=timeout)
except asyncio.TimeoutError as e:
raise ComputeError("Timeout error when connecting to {}".format(url))
except (aiohttp.errors.ClientOSError, aiohttp.errors.ClientRequestError, aiohttp.ClientResponseError) as e:
except (aiohttp.errors.ClientOSError, aiohttp.errors.ClientRequestError, aiohttp.errors.ServerDisconnectedError, aiohttp.ClientResponseError, ValueError) as e:
raise ComputeError(str(e))
body = yield from response.read()
if body and not raw:
+2
View File
@@ -191,3 +191,5 @@ def _export_images(project, image, z):
arcname = os.path.join("images", directory, os.path.basename(image))
z.write(path, arcname)
break
else:
raise aiohttp.web.HTTPConflict(text="Topology could not be exported because the image {} is not available. If you use multiple server, we need a copy of the image on the main server.".format(path))
+4 -1
View File
@@ -75,7 +75,10 @@ def import_project(controller, project_id, stream, location=None, name=None, kee
else:
projects_path = controller.projects_directory()
path = os.path.join(projects_path, project_name)
os.makedirs(path, exist_ok=True)
try:
os.makedirs(path, exist_ok=True)
except UnicodeEncodeError as e:
raise aiohttp.web.HTTPConflict(text="The project name contain non supported or invalid characters")
myzip.extractall(path)
topology = load_topology(os.path.join(path, "project.gns3"))
+2 -1
View File
@@ -18,6 +18,7 @@
import os
import re
import uuid
import html
import asyncio
import aiohttp
@@ -83,7 +84,7 @@ class Link:
"x": -10,
"y": -10,
"rotation": 0,
"text": "{}/{}".format(adapter_number, port_number),
"text": html.escape("{}/{}".format(adapter_number, port_number)),
"style": "font-size: 10; font-style: Verdana"
}
+2 -1
View File
@@ -17,6 +17,7 @@
import aiohttp
import asyncio
import html
import copy
import uuid
import os
@@ -238,7 +239,7 @@ class Node:
self._label = {
"y": round(self._height / 2 + 10) * -1,
"text": self._name,
"text": html.escape(self._name),
"style": style,
"x": None, # None: mean the client should center it
"rotation": 0
+1 -1
View File
@@ -66,7 +66,7 @@ class Port:
# If port name format has change we use the port name as the short name (1.X behavior)
if self._short_name:
return self._short_name
elif not self._name.startswith(self.long_name_type()):
elif not self._name.startswith("{}{}".format(self.long_name_type(), self._interface_number)):
return self._name
return self.short_name_type + "{}/{}".format(self._interface_number, self._port_number)
+6 -2
View File
@@ -637,7 +637,7 @@ class Project:
for node in topology.get("nodes", []):
compute = self.controller.get_compute(node.pop("compute_id"))
name = node.pop("name")
node_id = node.pop("node_id")
node_id = node.pop("node_id", str(uuid.uuid4()))
yield from self.add_node(compute, name, node_id, dump=False, **node)
for link_data in topology.get("links", []):
link = yield from self.add_link(link_id=link_data["link_id"])
@@ -663,7 +663,10 @@ class Project:
pass
self._status = "closed"
self._loading = False
raise e
if isinstance(e, ComputeError):
raise aiohttp.web.HTTPConflict(text=str(e))
else:
raise e
try:
os.remove(path + ".backup")
except OSError:
@@ -699,6 +702,7 @@ class Project:
if self._status == "closed":
yield from self.open()
self.dump()
try:
with tempfile.TemporaryDirectory() as tmpdir:
zipstream = yield from export_project(self, tmpdir, keep_compute_id=True, allow_all_nodes=True)
+11 -5
View File
@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import html
import json
import copy
import uuid
@@ -285,7 +286,10 @@ def _convert_1_3_later(topo, topo_path):
for old_node in topo.get("nodes", []):
node = {}
node["console"] = old_node["properties"].get("console", None)
node["compute_id"] = server_id_to_compute_id[old_node["server_id"]]
try:
node["compute_id"] = server_id_to_compute_id[old_node["server_id"]]
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"])
@@ -376,9 +380,11 @@ def _convert_1_3_later(topo, topo_path):
node["symbol"] = ":/symbols/router.svg"
elif old_node["type"] == "Cloud":
old_node["ports"] = _create_cloud(node, old_node, ":/symbols/cloud.svg")
symbol = old_node.get("symbol", ":/symbols/cloud.svg")
old_node["ports"] = _create_cloud(node, old_node, symbol)
elif old_node["type"] == "Host":
old_node["ports"] = _create_cloud(node, old_node, ":/symbols/computer.svg")
symbol = old_node.get("symbol", ":/symbols/computer.svg")
old_node["ports"] = _create_cloud(node, old_node, symbol)
else:
raise NotImplementedError("Conversion of {} is not supported".format(old_node["type"]))
@@ -463,7 +469,7 @@ def _convert_1_3_later(topo, topo_path):
size=int(font_info[1]),
weight=weight,
style=style,
text=note["text"]
text=html.escape(note["text"])
)
new_note = {
"drawing_id": str(uuid.uuid4()),
@@ -550,7 +556,7 @@ def _convert_label(label):
"""
style = qt_font_to_style(label.get("font"), label.get("color"))
return {
"text": label["text"],
"text": html.escape(label["text"]),
"rotation": 0,
"style": style,
"x": int(label["x"]),
+1 -1
View File
@@ -54,7 +54,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "sync+https://5b834577b69a4b77b112f020412237b5:b648d289253740daad5c08a939ceffd7@sentry.io/38482"
DSN = "sync+https://7c028290d17b4035916285b304d42311:ddf752e704c7423cacab93f8e34f713c@sentry.io/38482"
if hasattr(sys, "frozen"):
cacert = get_resource("cacert.pem")
if cacert is not None and os.path.isfile(cacert):
@@ -54,10 +54,10 @@ class EthernetSwitchHandler:
# Use the Dynamips Ethernet switch to simulate this node
dynamips_manager = Dynamips.instance()
node = yield from dynamips_manager.create_node(request.json.pop("name"),
request.match_info["project_id"],
request.json.get("node_id"),
node_type="ethernet_switch",
ports=request.json.get("ports_mapping"))
request.match_info["project_id"],
request.json.get("node_id"),
node_type="ethernet_switch",
ports=request.json.get("ports_mapping"))
# On Linux, use the generic switch
# builtin_manager = Builtin.instance()
@@ -114,6 +114,7 @@ class EthernetSwitchHandler:
yield from node.set_name(request.json["name"])
if "ports_mapping" in request.json:
node.ports_mapping = request.json["ports_mapping"]
yield from node.update_port_settings()
# builtin_manager = Builtin.instance()
# node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
@@ -22,6 +22,7 @@ from gns3server.web.route import Route
from gns3server.schemas.nio import NIO_SCHEMA
from gns3server.schemas.node import NODE_CAPTURE_SCHEMA
from gns3server.compute.virtualbox import VirtualBox
from gns3server.compute.virtualbox.virtualbox_error import VirtualBoxError
from gns3server.compute.project_manager import ProjectManager
from gns3server.schemas.virtualbox import (
@@ -116,9 +117,15 @@ class VirtualBoxHandler:
name = request.json.pop("name")
vmname = request.json.pop("vmname", None)
if name != vm.name:
oldname = vm.name
vm.name = name
if vm.linked_clone:
yield from vm.set_vmname(vm.name)
try:
yield from vm.set_vmname(vm.name)
except VirtualBoxError as e: # In case of error we rollback (we can't change the name when running)
vm.name = oldname
vm.updated()
raise e
if "adapters" in request.json:
adapters = int(request.json.pop("adapters"))
@@ -103,7 +103,15 @@ class ServerHandler:
description="Retrieve gui settings from the server. Temporary will we removed in later release")
def read_settings(request, response):
response.json(Controller.instance().settings)
settings = None
while True:
# The init of the server could take some times
# we ensure settings are loaded before returning them
settings = Controller.instance().settings
if settings is not None:
break
yield from asyncio.sleep(0.5)
response.json(settings)
@Route.post(
r"/settings",
@@ -113,6 +121,8 @@ class ServerHandler:
})
def write_settings(request, response):
controller = Controller.instance()
if controller.settings is None: # Server is not loaded ignore settings update to prevent buggy client sync issue
return
controller.settings = request.json
try:
controller.save()
@@ -49,7 +49,7 @@ class SymbolHandler:
controller = Controller.instance()
try:
yield from response.file(controller.symbols.get_path(request.match_info["symbol_id"]))
except (KeyError, FileNotFoundError):
except (KeyError, FileNotFoundError, PermissionError):
response.set_status(404)
@Route.post(
+1 -5
View File
@@ -26,9 +26,7 @@ import datetime
import sys
import locale
import argparse
import shutil
import psutil
import asyncio
from gns3server.web.web_server import WebServer
@@ -107,7 +105,6 @@ def parse_arguments(argv):
parser.add_argument("--daemon", action="store_true", help="start as a daemon")
parser.add_argument("--pid", help="store process pid")
parser.add_argument("--profile", help="Settings profile (blank will use default settings files)")
parser.add_argument("--discovery", action="store_true", help="Make server discoverable on the network")
args = parser.parse_args(argv)
if args.config:
@@ -128,8 +125,7 @@ def parse_arguments(argv):
"allow": config.getboolean("allow_remote_console", False),
"quiet": config.getboolean("quiet", False),
"debug": config.getboolean("debug", False),
"logfile": config.getboolean("logfile", ""),
"server_discovery": config.getboolean("server_discovery", False)
"logfile": config.getboolean("logfile", "")
}
parser.set_defaults(**defaults)
+5 -2
View File
@@ -174,7 +174,7 @@ class Hypervisor(UBridgeHypervisor):
except (OSError, PermissionError, subprocess.SubprocessError) as e:
ubridge_stdout = self.read_stdout()
log.error("Could not start ubridge: {}\n{}".format(e, ubridge_stdout))
raise UBridgeHypervisor("Could not start ubridge: {}\n{}".format(e, ubridge_stdout))
raise UbridgeError("Could not start ubridge: {}\n{}".format(e, ubridge_stdout))
def _termination_callback(self, returncode):
"""
@@ -201,7 +201,10 @@ class Hypervisor(UBridgeHypervisor):
except asyncio.TimeoutError:
if self._process.returncode is None:
log.warn("uBridge process {} is still running... killing it".format(self._process.pid))
self._process.kill()
try:
self._process.kill()
except ProcessLookupError:
pass
if self._stdout_file and os.access(self._stdout_file, os.W_OK):
try:
+1 -1
View File
@@ -58,7 +58,7 @@ def list_images(type):
if filename.endswith(".md5sum") or filename.startswith("."):
continue
elif ((filename.endswith(".image") or filename.endswith(".bin")) and type == "dynamips") \
or (filename.endswith(".bin") and type == "iou") \
or ((filename.endswith(".bin") or filename.startswith("i86bi")) and type == "iou") \
or (not filename.endswith(".bin") and not filename.endswith(".image") and type == "qemu"):
files.add(filename)
+2 -2
View File
@@ -99,8 +99,8 @@ def get_size(data, default_width=0, default_height=0):
root = tree.getroot()
try:
width = _svg_convert_size(root.attrib["width"])
height = _svg_convert_size(root.attrib["height"])
width = _svg_convert_size(root.attrib.get("width", "0"))
height = _svg_convert_size(root.attrib.get("height", "0"))
except IndexError:
raise ValueError("Invalid SVG file")
+1 -2
View File
@@ -21,7 +21,6 @@ import urllib
import asyncio
import aiohttp
import logging
import urllib
import traceback
import jsonschema
@@ -114,7 +113,7 @@ class Route(object):
return
if "AUTHORIZATION" in request.headers:
if request.headers["AUTHORIZATION"] == aiohttp.helpers.BasicAuth(user, password).encode():
if request.headers["AUTHORIZATION"] == aiohttp.helpers.BasicAuth(user, password, "utf-8").encode():
return
log.error("Invalid auth. Username should %s", user)
+6 -66
View File
@@ -22,11 +22,7 @@ Set up and run the server.
import os
import sys
import signal
import socket
import json
import ipaddress
import asyncio
import threading
import aiohttp
import aiohttp_cors
import functools
@@ -38,7 +34,6 @@ from ..config import Config
from ..compute import MODULES
from ..compute.port_manager import PortManager
from ..controller import Controller
from ..version import __version__
# do not delete this import
@@ -191,63 +186,12 @@ class WebServer:
atexit.register(close_asyncio_loop)
def _udp_server_discovery(self):
@asyncio.coroutine
def _on_startup(self, *args):
"""
UDP multicast and broadcast server discovery (Linux only)
Called when the HTTP server start
"""
import ctypes
uint32_t = ctypes.c_uint32
in_addr_t = uint32_t
class in_addr(ctypes.Structure):
_fields_ = [('s_addr', in_addr_t)]
class in_pktinfo(ctypes.Structure):
_fields_ = [('ipi_ifindex', ctypes.c_int),
('ipi_spec_dst', in_addr),
('ipi_addr', in_addr)]
IP_PKTINFO = 8
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
membership = socket.inet_aton("239.42.42.1") + socket.inet_aton("0.0.0.0")
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, membership)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_IP, IP_PKTINFO, 1)
try:
sock.bind(("", self._port))
except OSError as e:
log.error("UDP server discovery could not bind on port {}: {}".format(self._port, e))
return
log.info("UDP server discovery started on port {}".format(self._port))
while self._loop.is_running():
try:
data, ancdata, _, address = sock.recvmsg(255, socket.CMSG_LEN(255))
except OSError as e:
log.error("Error while receiving UDP server discovery request: {}".format(e))
continue
cmsg_level, cmsg_type, cmsg_data = ancdata[0]
if cmsg_level == socket.SOL_IP and cmsg_type == IP_PKTINFO:
pktinfo = in_pktinfo.from_buffer_copy(cmsg_data)
request_address = ipaddress.IPv4Address(memoryview(pktinfo.ipi_addr).tobytes())
receiving_interface = socket.if_indextoname(pktinfo.ipi_ifindex)
log.debug("UDP server discovery request received on {} using {}".format(receiving_interface,
request_address))
local_address = ipaddress.IPv4Address(memoryview(pktinfo.ipi_spec_dst).tobytes())
if self._host != "0.0.0.0" and self._host != str(local_address):
log.debug("Ignoring UDP discovery request received on {} instead of {}".format(local_address,
self._host))
continue
server_info = {"version": __version__,
"ip": str(local_address),
"port": self._port}
data = json.dumps(server_info)
sock.sendto(data.encode(), address)
log.debug("Sent server info to {}:{} {}".format(address[0], address[1], data))
time.sleep(1) # this is to prevent too many request to slow down the server
log.debug("UDP server discovery stopped")
yield from Controller.instance().start()
def run(self):
"""
@@ -288,8 +232,9 @@ class WebServer:
for key, val in os.environ.items():
log.debug("ENV %s=%s", key, val)
self._loop.run_until_complete(Controller.instance().start())
self._app = aiohttp.web.Application()
# Background task started with the server
self._app.on_startup.append(self._on_startup)
# Allow CORS for this domains
cors = aiohttp_cors.setup(self._app, defaults={
@@ -322,11 +267,6 @@ class WebServer:
if server_config.getboolean("shell"):
asyncio.async(self.start_shell())
if sys.platform.startswith("linux") and server_config.getboolean("server_discovery"):
# UDP discovery is only supported on Linux
udp_server_discovery = threading.Thread(target=self._udp_server_discovery, daemon=True)
udp_server_discovery.start()
try:
self._loop.run_forever()
except TypeError as e: