mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-06 07:11:13 +02:00
Merge branch '2.0' into 2.1
This commit is contained in:
275 files changed
+5384
-272
No files matched your search
@@ -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.")
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]),
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user