mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-13 02:15:52 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb5dc36987 | ||
|
|
cc1980ac1c | ||
|
|
8cd66cfc02 | ||
|
|
645031ee15 | ||
|
|
c70b344133 | ||
|
|
e291ec1eb9 | ||
|
|
8e8985c69f | ||
|
|
4eca82174d | ||
|
|
32f5dbb645 | ||
|
|
7aae682f0a | ||
|
|
879e32eb9a | ||
|
|
01481cde1f | ||
|
|
b5deb9d177 | ||
|
|
e1aa13fa73 | ||
|
|
62c51edbae | ||
|
|
a13d063aa1 | ||
|
|
1ef1872f8e | ||
|
|
657698a961 | ||
|
|
081ba31b50 | ||
|
|
aea4ae808f | ||
|
|
ae3515434c | ||
|
|
589c9754e8 | ||
|
|
4ecd3b2015 | ||
|
|
84ee3263ba | ||
|
|
174624121d | ||
|
|
d9a9abf845 | ||
|
|
0b07299472 | ||
|
|
7fb192699b | ||
|
|
bccdfc97d1 | ||
|
|
792c4310eb | ||
|
|
3e21f96bf9 | ||
|
|
5f932fee9f | ||
|
|
ef38ccfefa | ||
|
|
670b6d689a | ||
|
|
fc75a6b8d3 | ||
|
|
647ac87c89 | ||
|
|
817a053dfc | ||
|
|
df4e4bec24 | ||
|
|
60ac6d2dfe | ||
|
|
c0a0a13bdd | ||
|
|
a3044ede77 | ||
|
|
df3baffd9b | ||
|
|
e7b8309a80 | ||
|
|
029344da14 | ||
|
|
d0bab9947e | ||
|
|
e4a6db8ebc | ||
|
|
181a31be32 | ||
|
|
8d368cd0f6 | ||
|
|
41d4ecc4b7 | ||
|
|
4d95e0b51f | ||
|
|
4efdefaf5a |
No files matched your search
@@ -1,5 +1,51 @@
|
||||
# Change Log
|
||||
|
||||
## 2.1.16 15/04/2019
|
||||
|
||||
* Fix broken embedded console for Ethernet switch. Fixes #1574
|
||||
* Remove old unused argument option. Fixes #1569
|
||||
|
||||
## 2.1.15 21/03/2019
|
||||
|
||||
* Fix IOU symlink issue on remote servers.
|
||||
* Fix vcpus configuration for GNS3 VM on VMware. Ref #2738.
|
||||
|
||||
## 2.1.14 27/02/2019
|
||||
|
||||
* Fix issue when setting cpuid.corespersocket for the GNS3 VM. Fixes https://github.com/GNS3/gns3-gui/issues/2723
|
||||
* Bump ACPI Shutdown Timeout to 120 seconds. Ref #1536
|
||||
|
||||
## 2.1.13 26/02/2019
|
||||
|
||||
* Force jsonschema dependency to 2.6.0
|
||||
* Less aggressive connections to uBridge. Ref #1289
|
||||
* Fix topology images (Pictures) disappearing from projects. Fixes #1514.
|
||||
* Reset MAC addresses when duplicating a project. Fixes #1522
|
||||
* Fix API call to create a node from an appliance doesn't return the new node data. Fixes #1527
|
||||
* Detect invalid environment variable and send a warning when creating a Docker node. Ref #2683
|
||||
* Do not export/import symlinks for projects. Fixes #2699
|
||||
* Fix symlink not being created for duplicated IOU devices. Fixes https://github.com/GNS3/gns3-gui/issues/2699
|
||||
* Configure coresPerSocket value in VMX file for the GNS3 VM. Fixes https://github.com/GNS3/gns3-gui/issues/2688
|
||||
* Count logical CPUs to detect if the number of vCPUs is too high when configuring the GNS3 VM. Fixes #2688.
|
||||
* Add explicit error when trying to pull a Docker image from Docker Hub without Internet access. Fixes #1506.
|
||||
* Fixes double display output in GRUB in QEMU v3.1. Fixes #1516.
|
||||
|
||||
## 2.1.12 23/01/2019
|
||||
|
||||
* Tune how to get the size of SVG images. Ref https://github.com/GNS3/gns3-gui/issues/2674.
|
||||
* Automatically create a symbolic link to the IOU image in the IOU working directory. Fixes #1484
|
||||
* Fix link pause/filters only work for the first interface of Docker containers. Fixes #1482
|
||||
* Telnet console resize support for Docker VM.
|
||||
* Fix _fix_permissions() garbles permissions in Docker VM. Ref #1428
|
||||
* Fix "None is not of type 'integer'" when opening project containing a Qemu VM. Fixes #2610.
|
||||
* Only require Xtigervnc or Xvfb+x11vnc for Docker with vnc console. Ref #1438
|
||||
* Support tigervnc in Docker VM. Ref #1438
|
||||
* Update minimum VIX version requirements for VMware. Ref #1415.
|
||||
|
||||
## 2.1.11 28/09/2018
|
||||
|
||||
* Catch some exceptions.
|
||||
|
||||
## 2.1.10 15/09/2018
|
||||
|
||||
* Include locale information and GNS3 VM version in crash reports.
|
||||
@@ -677,7 +723,7 @@
|
||||
* Fix naming of IOU serial interfaces
|
||||
* Improve timeout management
|
||||
* When exporting debug information export GNS3 VM vmx content
|
||||
* /debug for exporting debug informations
|
||||
* /debug for exporting debug information
|
||||
* Raise error if using a non linked clone VM twice
|
||||
* Fix a possible deadlock at exit
|
||||
* Fix import of some old dynamips topologies
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
GET /v2/compute/debug
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Return debug informations about the compute
|
||||
Return debug information about the compute
|
||||
|
||||
Response status codes
|
||||
**********************
|
||||
|
||||
@@ -276,7 +276,7 @@ class BaseManager:
|
||||
destination_dir = destination_node.working_dir
|
||||
try:
|
||||
shutil.rmtree(destination_dir)
|
||||
shutil.copytree(source_node.working_dir, destination_dir)
|
||||
shutil.copytree(source_node.working_dir, destination_dir, symlinks=True, ignore_dangling_symlinks=True)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Can't duplicate node data: {}".format(e))
|
||||
|
||||
|
||||
@@ -200,7 +200,10 @@ class Docker(BaseManager):
|
||||
|
||||
if progress_callback:
|
||||
progress_callback("Pulling '{}' from docker hub".format(image))
|
||||
response = yield from self.http_query("POST", "images/create", params={"fromImage": image}, timeout=None)
|
||||
try:
|
||||
response = yield from self.http_query("POST", "images/create", params={"fromImage": image}, timeout=None)
|
||||
except DockerError as e:
|
||||
raise DockerError("Could not pull the '{}' image from Docker Hub, please check your Internet connection (original error: {})".format(image, e))
|
||||
# The pull api will stream status via an HTTP JSON stream
|
||||
content = ""
|
||||
while True:
|
||||
|
||||
@@ -24,6 +24,7 @@ import shutil
|
||||
import psutil
|
||||
import shlex
|
||||
import aiohttp
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer
|
||||
@@ -82,12 +83,14 @@ class DockerVM(BaseNode):
|
||||
self._ethernet_adapters = []
|
||||
self._temporary_directory = None
|
||||
self._telnet_servers = []
|
||||
self._x11vnc_process = None
|
||||
self._vnc_process = None
|
||||
self._xvfb_process = None
|
||||
self._console_resolution = console_resolution
|
||||
self._console_http_path = console_http_path
|
||||
self._console_http_port = console_http_port
|
||||
self._console_websocket = None
|
||||
self._extra_hosts = extra_hosts
|
||||
self._permissions_fixed = False
|
||||
|
||||
self._volumes = []
|
||||
# Keep a list of created bridge
|
||||
@@ -344,6 +347,9 @@ class DockerVM(BaseNode):
|
||||
if self._environment:
|
||||
for e in self._environment.strip().split("\n"):
|
||||
e = e.strip()
|
||||
if e.split("=")[0] == "":
|
||||
self.project.emit("log.warning", {"message": "{} has invalid environment variable: {}".format(self.name, e)})
|
||||
continue
|
||||
if not e.startswith("GNS3_"):
|
||||
formatted = self._format_env(variables, e)
|
||||
params["Env"].append(formatted)
|
||||
@@ -439,12 +445,14 @@ class DockerVM(BaseNode):
|
||||
|
||||
if self.console_type == "telnet":
|
||||
yield from self._start_console()
|
||||
|
||||
elif self.console_type == "http" or self.console_type == "https":
|
||||
yield from self._start_http()
|
||||
|
||||
if self.allocate_aux:
|
||||
yield from self._start_aux()
|
||||
|
||||
self._permissions_fixed = False
|
||||
self.status = "started"
|
||||
log.info("Docker container '{name}' [{image}] started listen for {console_type} on {console}".format(name=self._name,
|
||||
image=self._image,
|
||||
@@ -460,11 +468,10 @@ class DockerVM(BaseNode):
|
||||
# We can not use the API because docker doesn't expose a websocket api for exec
|
||||
# https://github.com/GNS3/gns3-gui/issues/1039
|
||||
try:
|
||||
process = yield from asyncio.subprocess.create_subprocess_exec(
|
||||
"docker", "exec", "-i", self._cid, "/gns3/bin/busybox", "script", "-qfc", "while true; do TERM=vt100 /gns3/bin/busybox sh; done", "/dev/null",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT,
|
||||
stdin=asyncio.subprocess.PIPE)
|
||||
process = yield from asyncio.subprocess.create_subprocess_exec("docker", "exec", "-i", self._cid, "/gns3/bin/busybox", "script", "-qfc", "while true; do TERM=vt100 /gns3/bin/busybox sh; done", "/dev/null",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT,
|
||||
stdin=asyncio.subprocess.PIPE)
|
||||
except OSError as e:
|
||||
raise DockerError("Could not start auxiliary console process: {}".format(e))
|
||||
server = AsyncioTelnetServer(reader=process.stdout, writer=process.stdin, binary=True, echo=True)
|
||||
@@ -472,7 +479,7 @@ class DockerVM(BaseNode):
|
||||
self._telnet_servers.append((yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.aux)))
|
||||
except OSError as e:
|
||||
raise DockerError("Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.aux, e))
|
||||
log.debug("Docker container '%s' started listen for auxilary telnet on %d", self.name, self.aux)
|
||||
log.debug("Docker container '%s' started listen for auxiliary telnet on %d", self.name, self.aux)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _fix_permissions(self):
|
||||
@@ -482,6 +489,7 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
|
||||
state = yield from self._get_container_state()
|
||||
log.info("Docker container '{name}' fix ownership, state = {state}".format(name=self._name, state=state))
|
||||
if state == "stopped" or state == "exited":
|
||||
# We need to restart it to fix permissions
|
||||
yield from self.manager.query("POST", "containers/{}/start".format(self._cid))
|
||||
@@ -509,6 +517,7 @@ class DockerVM(BaseNode):
|
||||
except OSError as e:
|
||||
raise DockerError("Could not fix permissions for {}: {}".format(volume, e))
|
||||
yield from process.wait()
|
||||
self._permissions_fixed = True
|
||||
|
||||
@asyncio.coroutine
|
||||
def _start_vnc(self):
|
||||
@@ -517,19 +526,46 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
|
||||
self._display = self._get_free_display_port()
|
||||
if shutil.which("Xvfb") is None or shutil.which("x11vnc") is None:
|
||||
raise DockerError("Please install Xvfb and x11vnc before using VNC support")
|
||||
self._xvfb_process = yield from asyncio.create_subprocess_exec("Xvfb", "-nolisten", "tcp", ":{}".format(self._display), "-screen", "0", self._console_resolution + "x16")
|
||||
# We pass a port for TCPV6 due to a crash in X11VNC if not here: https://github.com/GNS3/gns3-server/issues/569
|
||||
self._x11vnc_process = yield from asyncio.create_subprocess_exec("x11vnc", "-forever", "-nopw", "-shared", "-geometry", self._console_resolution, "-display", "WAIT:{}".format(self._display), "-rfbport", str(self.console), "-rfbportv6", str(self.console), "-noncache", "-listen", self._manager.port_manager.console_host)
|
||||
if not (shutil.which("Xtigervnc") or shutil.which("Xvfb") and shutil.which("x11vnc")):
|
||||
raise DockerError("Please install tigervnc-standalone-server (recommended) or Xvfb + x11vnc before using VNC support")
|
||||
|
||||
if shutil.which("Xtigervnc"):
|
||||
with open(os.path.join(self.working_dir, "vnc.log"), "w") as fd:
|
||||
self._vnc_process = yield from asyncio.create_subprocess_exec("Xtigervnc",
|
||||
"-geometry", self._console_resolution,
|
||||
"-depth", "16",
|
||||
"-interface", self._manager.port_manager.console_host,
|
||||
"-rfbport", str(self.console),
|
||||
"-AlwaysShared",
|
||||
"-SecurityTypes", "None",
|
||||
":{}".format(self._display),
|
||||
stdout=fd, stderr=subprocess.STDOUT)
|
||||
else:
|
||||
self._xvfb_process = yield from asyncio.create_subprocess_exec("Xvfb",
|
||||
"-nolisten",
|
||||
"tcp", ":{}".format(self._display),
|
||||
"-screen", "0",
|
||||
self._console_resolution + "x16")
|
||||
|
||||
# We pass a port for TCPV6 due to a crash in X11VNC if not here: https://github.com/GNS3/gns3-server/issues/569
|
||||
with open(os.path.join(self.working_dir, "vnc.log"), "w") as fd:
|
||||
self._vnc_process = yield from asyncio.create_subprocess_exec("x11vnc",
|
||||
"-forever",
|
||||
"-nopw",
|
||||
"-shared",
|
||||
"-geometry", self._console_resolution,
|
||||
"-display", "WAIT:{}".format(self._display),
|
||||
"-rfbport", str(self.console),
|
||||
"-rfbportv6", str(self.console),
|
||||
"-noncache",
|
||||
"-listen", self._manager.port_manager.console_host,
|
||||
stdout=fd, stderr=subprocess.STDOUT)
|
||||
|
||||
x11_socket = os.path.join("/tmp/.X11-unix/", "X{}".format(self._display))
|
||||
yield from wait_for_file_creation(x11_socket)
|
||||
#monitor_process(self._vnc_process, self._vnc_callback)
|
||||
|
||||
#monitor_process(self._xvfb_process, self._xvfb_callback)
|
||||
#monitor_process(self._x11vnc_process, self._x11vnc_callback)
|
||||
|
||||
def _xvfb_callback(self, returncode):
|
||||
def _vnc_callback(self, returncode):
|
||||
"""
|
||||
Called when the process has stopped.
|
||||
|
||||
@@ -537,17 +573,7 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
|
||||
if returncode != 0:
|
||||
self.project.emit("log.error", {"message": "The Xvfb process has stopped, return code: {}.".format(returncode)})
|
||||
|
||||
def _x11vnc_callback(self, returncode):
|
||||
"""
|
||||
Called when the process has stopped.
|
||||
|
||||
:param returncode: Process returncode
|
||||
"""
|
||||
|
||||
if returncode != 0:
|
||||
self.project.emit("log.error", {"message": "The x11vnc process has stopped, return code: {}.".format(returncode)})
|
||||
self.project.emit("log.error", {"message": "The vnc process has stopped, return code: {}.".format(returncode)})
|
||||
|
||||
@asyncio.coroutine
|
||||
def _start_http(self):
|
||||
@@ -570,6 +596,19 @@ class DockerVM(BaseNode):
|
||||
])
|
||||
self._telnet_servers.append((yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console)))
|
||||
|
||||
@asyncio.coroutine
|
||||
def _window_size_changed_callback(self, columns, rows):
|
||||
"""
|
||||
Called when the console window size has been changed.
|
||||
(when naws is enabled in the Telnet server)
|
||||
|
||||
:param columns: number of columns
|
||||
:param rows: number of rows
|
||||
"""
|
||||
|
||||
# resize the container TTY.
|
||||
yield from self._manager.query("POST", "containers/{}/resize?h={}&w={}".format(self._cid, rows, columns))
|
||||
|
||||
@asyncio.coroutine
|
||||
def _start_console(self):
|
||||
"""
|
||||
@@ -592,8 +631,7 @@ class DockerVM(BaseNode):
|
||||
|
||||
output_stream = asyncio.StreamReader()
|
||||
input_stream = InputStream()
|
||||
|
||||
telnet = AsyncioTelnetServer(reader=output_stream, writer=input_stream, echo=True)
|
||||
telnet = AsyncioTelnetServer(reader=output_stream, writer=input_stream, echo=True, naws=True, window_size_changed_callback=self._window_size_changed_callback)
|
||||
try:
|
||||
self._telnet_servers.append((yield from asyncio.start_server(telnet.run, self._manager.port_manager.console_host, self.console)))
|
||||
except OSError as e:
|
||||
@@ -603,7 +641,6 @@ class DockerVM(BaseNode):
|
||||
input_stream.ws = self._console_websocket
|
||||
|
||||
output_stream.feed_data(self.name.encode() + b" console is now available... Press RETURN to get started.\r\n")
|
||||
|
||||
asyncio_ensure_future(self._read_console_output(self._console_websocket, output_stream))
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -678,14 +715,15 @@ class DockerVM(BaseNode):
|
||||
if state == "paused":
|
||||
yield from self.unpause()
|
||||
|
||||
yield from self._fix_permissions()
|
||||
if not self._permissions_fixed:
|
||||
yield from self._fix_permissions()
|
||||
|
||||
state = yield from self._get_container_state()
|
||||
if state != "stopped" or state != "exited":
|
||||
# t=5 number of seconds to wait before killing the container
|
||||
try:
|
||||
yield from self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5})
|
||||
log.info("Docker container '{name}' [{image}] stopped".format(
|
||||
name=self._name, image=self._image))
|
||||
log.info("Docker container '{name}' [{image}] stopped".format(name=self._name, image=self._image))
|
||||
except DockerHttp304Error:
|
||||
# Container is already stopped
|
||||
pass
|
||||
@@ -725,12 +763,13 @@ class DockerVM(BaseNode):
|
||||
if state == "paused" or state == "running":
|
||||
yield from self.stop()
|
||||
if self.console_type == "vnc":
|
||||
if self._x11vnc_process:
|
||||
if self._vnc_process:
|
||||
try:
|
||||
self._x11vnc_process.terminate()
|
||||
yield from self._x11vnc_process.wait()
|
||||
self._vnc_process.terminate()
|
||||
yield from self._vnc_process.wait()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
if self._xvfb_process:
|
||||
try:
|
||||
self._xvfb_process.terminate()
|
||||
yield from self._xvfb_process.wait()
|
||||
|
||||
@@ -517,6 +517,17 @@ class IOUVM(BaseNode):
|
||||
|
||||
if "IOURC" not in os.environ:
|
||||
env["IOURC"] = iourc_path
|
||||
|
||||
# create a symbolic link to the image to avoid IOU error "failed code signing checks"
|
||||
# on newer images, see https://github.com/GNS3/gns3-server/issues/1484
|
||||
try:
|
||||
symlink = os.path.join(self.working_dir, os.path.basename(self.path))
|
||||
if os.path.islink(symlink):
|
||||
os.unlink(symlink)
|
||||
os.symlink(self.path, symlink)
|
||||
except OSError as e:
|
||||
raise IOUError("Could not create symbolic link: {}".format(e))
|
||||
|
||||
command = yield from self._build_command()
|
||||
try:
|
||||
log.info("Starting IOU: {}".format(command))
|
||||
@@ -651,6 +662,13 @@ class IOUVM(BaseNode):
|
||||
pass
|
||||
self._iou_process = None
|
||||
|
||||
try:
|
||||
symlink = os.path.join(self.working_dir, os.path.basename(self.path))
|
||||
if os.path.islink(symlink):
|
||||
os.unlink(symlink)
|
||||
except OSError as e:
|
||||
log.warning("Could not delete symbolic link: {}".format(e))
|
||||
|
||||
self._started = False
|
||||
self.save_configs()
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# This ports are disallowed by Chrome and Firefox to avoid issues, we skip them as well
|
||||
# These ports are disallowed by Chrome and Firefox to avoid issues, we skip them as well
|
||||
BANNED_PORTS = set((1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 77, 79, 87, 95, 101, 102, 103,
|
||||
104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 139, 143, 179, 389, 465, 512, 513, 514, 515, 526,
|
||||
530, 531, 532, 540, 556, 563, 587, 601, 636, 993, 995, 2049, 3659, 4045, 6000, 6665, 6666, 6667,
|
||||
|
||||
@@ -415,7 +415,7 @@ class Project:
|
||||
"""
|
||||
|
||||
files = []
|
||||
for dirpath, dirnames, filenames in os.walk(self.path):
|
||||
for dirpath, dirnames, filenames in os.walk(self.path, followlinks=False):
|
||||
for filename in filenames:
|
||||
if not filename.endswith(".ghost"):
|
||||
path = os.path.relpath(dirpath, self.path)
|
||||
|
||||
@@ -186,7 +186,7 @@ class Qemu(BaseManager):
|
||||
return ""
|
||||
else:
|
||||
try:
|
||||
output = yield from subprocess_check_output(qemu_path, "-version")
|
||||
output = yield from subprocess_check_output(qemu_path, "-version", "-nographic")
|
||||
match = re.search("version\s+([0-9a-z\-\.]+)", output)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
|
||||
@@ -974,7 +974,7 @@ class QemuVM(BaseNode):
|
||||
try:
|
||||
if self.acpi_shutdown:
|
||||
yield from self._control_vm("system_powerdown")
|
||||
yield from gns3server.utils.asyncio.wait_for_process_termination(self._process, timeout=30)
|
||||
yield from gns3server.utils.asyncio.wait_for_process_termination(self._process, timeout=120)
|
||||
else:
|
||||
self._process.terminate()
|
||||
yield from gns3server.utils.asyncio.wait_for_process_termination(self._process, timeout=3)
|
||||
@@ -1601,18 +1601,19 @@ class QemuVM(BaseNode):
|
||||
|
||||
return network_options
|
||||
|
||||
def _graphic(self):
|
||||
@asyncio.coroutine
|
||||
def _disable_graphics(self):
|
||||
"""
|
||||
Adds the correct graphic options depending of the OS
|
||||
Disable graphics depending of the QEMU version
|
||||
"""
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
if any(opt in self._options for opt in ["-display", "-nographic", "-curses", "-sdl" "-spice", "-vnc"]):
|
||||
return []
|
||||
if len(os.environ.get("DISPLAY", "")) > 0:
|
||||
return []
|
||||
if "-nographic" not in self._options:
|
||||
version = yield from self.manager.get_qemu_version(self.qemu_path)
|
||||
if version and parse_version(version) >= parse_version("3.0"):
|
||||
return ["-display", "none"]
|
||||
else:
|
||||
return ["-nographic"]
|
||||
return []
|
||||
|
||||
def _run_with_kvm(self, qemu_path, options):
|
||||
"""
|
||||
@@ -1678,7 +1679,8 @@ class QemuVM(BaseNode):
|
||||
raise QemuError("Console type {} is unknown".format(self._console_type))
|
||||
command.extend(self._monitor_options())
|
||||
command.extend((yield from self._network_options()))
|
||||
command.extend(self._graphic())
|
||||
if self._console_type == "telnet":
|
||||
command.extend((yield from self._disable_graphics()))
|
||||
if additional_options:
|
||||
try:
|
||||
command.extend(shlex.split(additional_options))
|
||||
|
||||
@@ -149,6 +149,7 @@ class VMware(BaseManager):
|
||||
VIX 1.13 was the release for Player 6.
|
||||
VIX 1.14 was the release for Player 7.
|
||||
VIX 1.15 was the release for Workstation Player 12.
|
||||
VIX 1.17 was the release for Workstation Player 14.
|
||||
|
||||
:param player_version: VMware Player major version.
|
||||
"""
|
||||
@@ -162,6 +163,8 @@ class VMware(BaseManager):
|
||||
yield from self.check_vmrun_version(minimum_required_version="1.14.0")
|
||||
elif player_version >= 12:
|
||||
yield from self.check_vmrun_version(minimum_required_version="1.15.0")
|
||||
elif player_version >= 14:
|
||||
yield from self.check_vmrun_version(minimum_required_version="1.17.0")
|
||||
self._host_type = "player"
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -172,6 +175,7 @@ class VMware(BaseManager):
|
||||
VIX 1.13 was the release for Workstation 10.
|
||||
VIX 1.14 was the release for Workstation 11.
|
||||
VIX 1.15 was the release for Workstation Pro 12.
|
||||
VIX 1.17 was the release for Workstation Pro 14.
|
||||
|
||||
:param ws_version: VMware Workstation major version.
|
||||
"""
|
||||
@@ -185,6 +189,8 @@ class VMware(BaseManager):
|
||||
yield from self.check_vmrun_version(minimum_required_version="1.14.0")
|
||||
elif ws_version >= 12:
|
||||
yield from self.check_vmrun_version(minimum_required_version="1.15.0")
|
||||
elif ws_version >= 14:
|
||||
yield from self.check_vmrun_version(minimum_required_version="1.17.0")
|
||||
self._host_type = "ws"
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -315,7 +315,7 @@ class VPCSVM(BaseNode):
|
||||
if sys.platform.startswith("win32"):
|
||||
try:
|
||||
self._process.send_signal(signal.CTRL_BREAK_EVENT)
|
||||
except OSError:
|
||||
except (SystemError, OSError):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
|
||||
@@ -446,9 +446,13 @@ class Compute:
|
||||
self._capabilities = response.json
|
||||
|
||||
if response.json["version"].split("-")[0] != __version__.split("-")[0]:
|
||||
msg = "GNS3 controller version {} is not the same as compute server {} version {}".format(__version__,
|
||||
self._name,
|
||||
response.json["version"])
|
||||
if self._name.startswith("GNS3 VM"):
|
||||
msg = "GNS3 version {} is not the same as the GNS3 VM version {}. Please upgrade the GNS3 VM.".format(__version__,
|
||||
response.json["version"])
|
||||
else:
|
||||
msg = "GNS3 controller version {} is not the same as compute server {} version {}".format(__version__,
|
||||
self._name,
|
||||
response.json["version"])
|
||||
if __version_info__[3] == 0:
|
||||
# Stable release
|
||||
log.error(msg)
|
||||
|
||||
@@ -55,7 +55,7 @@ class Drawing:
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def ressource_filename(self):
|
||||
def resource_filename(self):
|
||||
"""
|
||||
If the svg content has been dump to an external file return is name otherwise None
|
||||
"""
|
||||
|
||||
@@ -31,7 +31,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def export_project(project, temporary_dir, include_images=False, keep_compute_id=False, allow_all_nodes=False):
|
||||
def export_project(project, temporary_dir, include_images=False, keep_compute_id=False, allow_all_nodes=False, reset_mac_addresses=False):
|
||||
"""
|
||||
Export a project to a zip file.
|
||||
|
||||
@@ -42,6 +42,7 @@ def export_project(project, temporary_dir, include_images=False, keep_compute_id
|
||||
:param include images: save OS images to the zip file
|
||||
:param keep_compute_id: If false replace all compute id by local (standard behavior for .gns3project to make it portable)
|
||||
:param allow_all_nodes: Allow all nodes type to be include in the zip even if not portable
|
||||
:param reset_mac_addresses: Reset MAC addresses for every nodes.
|
||||
|
||||
:returns: ZipStream object
|
||||
"""
|
||||
@@ -61,10 +62,10 @@ def export_project(project, temporary_dir, include_images=False, keep_compute_id
|
||||
# First we process the .gns3 in order to be sure we don't have an error
|
||||
for file in os.listdir(project._path):
|
||||
if file.endswith(".gns3"):
|
||||
yield from _patch_project_file(project, os.path.join(project._path, file), zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir)
|
||||
yield from _patch_project_file(project, os.path.join(project._path, file), zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses)
|
||||
|
||||
# Export the local files
|
||||
for root, dirs, files in os.walk(project._path, topdown=True):
|
||||
for root, dirs, files in os.walk(project._path, topdown=True, followlinks=False):
|
||||
files = [f for f in files if _is_exportable(os.path.join(root, f))]
|
||||
for file in files:
|
||||
path = os.path.join(root, file)
|
||||
@@ -125,6 +126,7 @@ def _patch_mtime(path):
|
||||
new_mtime = file_date.replace(year=1980).timestamp()
|
||||
os.utime(path, (st.st_atime, new_mtime))
|
||||
|
||||
|
||||
def _is_exportable(path):
|
||||
"""
|
||||
:returns: True if file should not be included in the final archive
|
||||
@@ -134,6 +136,10 @@ def _is_exportable(path):
|
||||
if path.endswith("snapshots"):
|
||||
return False
|
||||
|
||||
# do not export symlinks
|
||||
if os.path.islink(path):
|
||||
return False
|
||||
|
||||
# do not export directories of snapshots
|
||||
if "{sep}snapshots{sep}".format(sep=os.path.sep) in path:
|
||||
return False
|
||||
@@ -155,7 +161,7 @@ def _is_exportable(path):
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _patch_project_file(project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir):
|
||||
def _patch_project_file(project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses):
|
||||
"""
|
||||
Patch a project file (.gns3) to export a project.
|
||||
The .gns3 file is renamed to project.gns3
|
||||
@@ -188,6 +194,10 @@ def _patch_project_file(project, path, zstream, include_images, keep_compute_id,
|
||||
if "properties" in node and node["node_type"] != "docker":
|
||||
for prop, value in node["properties"].items():
|
||||
|
||||
# reset the MAC address
|
||||
if reset_mac_addresses and prop in ("mac_addr", "mac_address"):
|
||||
node["properties"][prop] = None
|
||||
|
||||
if node["node_type"] == "iou":
|
||||
if not prop == "path":
|
||||
continue
|
||||
|
||||
@@ -72,15 +72,19 @@ class VMwareGNS3VM(BaseGNS3VM):
|
||||
if ram % 4 != 0:
|
||||
raise GNS3VMError("Allocated memory {} for the GNS3 VM must be a multiple of 4".format(ram))
|
||||
|
||||
available_vcpus = psutil.cpu_count(logical=False)
|
||||
available_vcpus = psutil.cpu_count(logical=True)
|
||||
if vcpus > available_vcpus:
|
||||
raise GNS3VMError("You have allocated too many vCPUs for the GNS3 VM! (max available is {} vCPUs)".format(available_vcpus))
|
||||
|
||||
try:
|
||||
pairs = VMware.parse_vmware_file(self._vmx_path)
|
||||
pairs["numvcpus"] = str(vcpus)
|
||||
pairs["memsize"] = str(ram)
|
||||
VMware.write_vmx_file(self._vmx_path, pairs)
|
||||
if vcpus > 1:
|
||||
pairs["numvcpus"] = str(vcpus)
|
||||
cores_per_sockets = int(vcpus / psutil.cpu_count(logical=False))
|
||||
if cores_per_sockets > 1:
|
||||
pairs["cpuid.corespersocket"] = str(cores_per_sockets)
|
||||
pairs["memsize"] = str(ram)
|
||||
VMware.write_vmx_file(self._vmx_path, pairs)
|
||||
log.info("GNS3 VM vCPU count set to {} and RAM amount set to {}".format(vcpus, ram))
|
||||
except OSError as e:
|
||||
raise GNS3VMError('Could not read/write VMware VMX file "{}": {}'.format(self._vmx_path, e))
|
||||
|
||||
@@ -184,9 +184,11 @@ def _move_files_to_compute(compute, project_id, directory, files_path):
|
||||
|
||||
location = os.path.join(directory, files_path)
|
||||
if os.path.exists(location):
|
||||
for (dirpath, dirnames, filenames) in os.walk(location):
|
||||
for (dirpath, dirnames, filenames) in os.walk(location, followlinks=False):
|
||||
for filename in filenames:
|
||||
path = os.path.join(dirpath, filename)
|
||||
if os.path.islink(path):
|
||||
continue
|
||||
dst = os.path.relpath(path, directory)
|
||||
yield from _upload_file(compute, project_id, path, dst)
|
||||
yield from wait_run_in_executor(shutil.rmtree, os.path.join(directory, files_path))
|
||||
@@ -213,9 +215,11 @@ def _import_images(controller, path):
|
||||
|
||||
image_dir = controller.images_path()
|
||||
root = os.path.join(path, "images")
|
||||
for (dirpath, dirnames, filenames) in os.walk(root):
|
||||
for (dirpath, dirnames, filenames) in os.walk(root, followlinks=False):
|
||||
for filename in filenames:
|
||||
path = os.path.join(dirpath, filename)
|
||||
if os.path.islink(path):
|
||||
continue
|
||||
dst = os.path.join(image_dir, os.path.relpath(path, root))
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
shutil.move(path, dst)
|
||||
@@ -91,12 +91,12 @@ class Node:
|
||||
|
||||
# Update node properties with additional elements
|
||||
for prop in kwargs:
|
||||
if prop not in ignore_properties:
|
||||
if prop and prop not in ignore_properties:
|
||||
if hasattr(self, prop):
|
||||
try:
|
||||
setattr(self, prop, kwargs[prop])
|
||||
except AttributeError as e:
|
||||
log.critical("Can't set attribute %s", prop)
|
||||
log.critical("Cannot set attribute '%s'".format(prop))
|
||||
raise e
|
||||
else:
|
||||
if prop not in self.CONTROLLER_ONLY_PROPERTIES and kwargs[prop] is not None and kwargs[prop] != "":
|
||||
@@ -254,7 +254,7 @@ class Node:
|
||||
try:
|
||||
self._width, self._height, filetype = self._project.controller.symbols.get_size(val)
|
||||
except (ValueError, OSError) as e:
|
||||
log.error("Could not write symbol: {}".format(e))
|
||||
log.error("Could not set symbol: {}".format(e))
|
||||
# If symbol is invalid we replace it by the default
|
||||
self.symbol = ":/symbols/computer.svg"
|
||||
if self._label is None:
|
||||
|
||||
@@ -146,7 +146,6 @@ class Project:
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Called when open/close a project. Cleanup internal stuff
|
||||
@@ -704,24 +703,26 @@ class Project:
|
||||
# We don't care if a compute is down at this step
|
||||
except (ComputeError, aiohttp.web.HTTPError, aiohttp.ClientResponseError, TimeoutError):
|
||||
pass
|
||||
self._cleanPictures()
|
||||
self._clean_pictures()
|
||||
self._status = "closed"
|
||||
if not ignore_notification:
|
||||
self.controller.notification.emit("project.closed", self.__json__())
|
||||
|
||||
def _cleanPictures(self):
|
||||
def _clean_pictures(self):
|
||||
"""
|
||||
Delete unused images
|
||||
Delete unused pictures.
|
||||
"""
|
||||
|
||||
# Project have been deleted
|
||||
if not os.path.exists(self.path):
|
||||
# Project have been deleted or is loading or is not opened
|
||||
if not os.path.exists(self.path) or self._loading or self._status != "opened":
|
||||
return
|
||||
try:
|
||||
pictures = set(os.listdir(self.pictures_directory))
|
||||
for drawing in self._drawings.values():
|
||||
try:
|
||||
pictures.remove(drawing.ressource_filename)
|
||||
resource_filename = drawing.resource_filename
|
||||
if resource_filename:
|
||||
pictures.remove(resource_filename)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -733,11 +734,12 @@ class Project:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
for pict in pictures:
|
||||
os.remove(os.path.join(self.pictures_directory, pict))
|
||||
for pic_filename in pictures:
|
||||
path = os.path.join(self.pictures_directory, pic_filename)
|
||||
log.info("Deleting unused picture '{}'".format(path))
|
||||
os.remove(path)
|
||||
except OSError as e:
|
||||
log.warning(str(e))
|
||||
log.warning("Could not delete unused pictures: {}".format(e))
|
||||
|
||||
@asyncio.coroutine
|
||||
def delete(self):
|
||||
@@ -929,13 +931,13 @@ class Project:
|
||||
self.dump()
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
zipstream = yield from export_project(self, tmpdir, keep_compute_id=True, allow_all_nodes=True)
|
||||
zipstream = yield from export_project(self, tmpdir, keep_compute_id=True, allow_all_nodes=True, reset_mac_addresses=True)
|
||||
project_path = os.path.join(tmpdir, "project.gns3p")
|
||||
yield from wait_run_in_executor(self._create_duplicate_project_file, project_path, zipstream)
|
||||
with open(project_path, "rb") as f:
|
||||
project = yield from import_project(self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_id=True)
|
||||
except (ValueError, OSError, UnicodeEncodeError) as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Can not duplicate project: {}".format(str(e)))
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot duplicate project: {}".format(str(e)))
|
||||
|
||||
if previous_status == "closed":
|
||||
yield from self.close()
|
||||
|
||||
@@ -57,7 +57,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "https://71472e2845b2422cab117bdfa8af55ec:da50c8f08b9f44d8b905356741c278df@sentry.io/38482"
|
||||
DSN = "https://6536a51af3154084859112bacb4f2277:72822a229faa463ba6c9338126a6f280@sentry.io/38482"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = get_resource("cacert.pem")
|
||||
if cacert is not None and os.path.isfile(cacert):
|
||||
|
||||
@@ -263,10 +263,11 @@ class DockerHandler:
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
nio = container.ethernet_adapters[int(request.match_info["adapter_number"])].get_nio(0)
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
nio = container.ethernet_adapters[adapter_number].get_nio(0)
|
||||
if "filters" in request.json and nio:
|
||||
nio.filters = request.json["filters"]
|
||||
yield from container.adapter_update_nio_binding(int(request.match_info["port_number"]), nio)
|
||||
yield from container.adapter_update_nio_binding(adapter_number, nio)
|
||||
response.set_status(201)
|
||||
response.json(request.json)
|
||||
|
||||
|
||||
@@ -301,12 +301,9 @@ class IOUHandler:
|
||||
nio = vm.adapters[adapter_number].get_nio(port_number)
|
||||
if "filters" in request.json and nio:
|
||||
nio.filters = request.json["filters"]
|
||||
yield from vm.adapter_update_nio_binding(
|
||||
adapter_number,
|
||||
port_number,
|
||||
nio)
|
||||
yield from vm.adapter_update_nio_binding(adapter_number, port_number, nio)
|
||||
response.set_status(201)
|
||||
response.json(nio)
|
||||
response.json(request.json)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
|
||||
@@ -42,9 +42,9 @@ class ServerHandler:
|
||||
|
||||
@Route.get(
|
||||
r"/debug",
|
||||
description="Return debug informations about the compute",
|
||||
description="Return debug information about the compute",
|
||||
status_codes={
|
||||
201: "Writed"
|
||||
201: "Written"
|
||||
})
|
||||
def debug(request, response):
|
||||
response.content_type = "text/plain"
|
||||
|
||||
@@ -279,12 +279,13 @@ class VMwareHandler:
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
nio = vm.ethernet_adapters[int(request.match_info["adapter_number"])]
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
nio = vm.ethernet_adapters[adapter_number]
|
||||
if "filters" in request.json and nio:
|
||||
nio.filters = request.json["filters"]
|
||||
yield from vm.adapter_update_nio_binding(int(request.match_info["adapter_number"]), nio)
|
||||
response.set_status(201)
|
||||
response.json(request.json)
|
||||
yield from vm.adapter_update_nio_binding(adapter_number, nio)
|
||||
response.set_status(201)
|
||||
response.json(request.json)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/vmware/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
|
||||
@@ -67,8 +67,9 @@ class ApplianceHandler:
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(request.match_info["project_id"])
|
||||
yield from project.add_node_from_appliance(request.match_info["appliance_id"],
|
||||
x=request.json["x"],
|
||||
y=request.json["y"],
|
||||
compute_id=request.json.get("compute_id"))
|
||||
node = yield from project.add_node_from_appliance(request.match_info["appliance_id"],
|
||||
x=request.json["x"],
|
||||
y=request.json["y"],
|
||||
compute_id=request.json.get("compute_id"))
|
||||
response.set_status(201)
|
||||
response.json(node)
|
||||
@@ -59,7 +59,7 @@ class ComputeHandler:
|
||||
|
||||
@Route.put(
|
||||
r"/computes/{compute_id}",
|
||||
description="Get a compute server information",
|
||||
description="Update a compute server",
|
||||
status_codes={
|
||||
200: "Compute server updated",
|
||||
400: "Invalid request",
|
||||
|
||||
@@ -287,9 +287,9 @@ class ProjectHandler:
|
||||
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
datas = yield from export_project(
|
||||
project, tmp_dir,
|
||||
include_images=bool(int(request.query.get("include_images", "0"))))
|
||||
stream = yield from export_project(project,
|
||||
tmp_dir,
|
||||
include_images=bool(int(request.query.get("include_images", "0"))))
|
||||
# We need to do that now because export could failed and raise an HTTP error
|
||||
# that why response start need to be the later possible
|
||||
response.content_type = 'application/gns3project'
|
||||
@@ -297,7 +297,7 @@ class ProjectHandler:
|
||||
response.enable_chunked_encoding()
|
||||
yield from response.prepare(request)
|
||||
|
||||
for data in datas:
|
||||
for data in stream:
|
||||
response.write(data)
|
||||
yield from response.drain()
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class ServerHandler:
|
||||
r"/debug",
|
||||
description="Dump debug information to disk (debug directory in config directory). Work only for local server",
|
||||
status_codes={
|
||||
201: "Writed"
|
||||
201: "Written"
|
||||
})
|
||||
def debug(request, response):
|
||||
|
||||
@@ -156,7 +156,7 @@ class ServerHandler:
|
||||
f.write(ServerHandler._getDebugData())
|
||||
except Exception as e:
|
||||
# If something is wrong we log the info to the log and we hope the log will be include correctly to the debug export
|
||||
log.error("Could not export debug informations {}".format(e), exc_info=1)
|
||||
log.error("Could not export debug information {}".format(e), exc_info=1)
|
||||
|
||||
try:
|
||||
if Controller.instance().gns3vm.engine == "vmware":
|
||||
|
||||
@@ -60,7 +60,7 @@ class SymbolHandler:
|
||||
r"/symbols/{symbol_id:.+}/raw",
|
||||
description="Write the symbol file",
|
||||
status_codes={
|
||||
200: "Symbol returned"
|
||||
200: "Symbol written"
|
||||
},
|
||||
raw=True)
|
||||
def upload(request, response):
|
||||
@@ -78,6 +78,7 @@ class SymbolHandler:
|
||||
f.write(chunk)
|
||||
except (UnicodeEncodeError, OSError) as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Could not write symbol file '{}': {}".format(path, e))
|
||||
|
||||
# Reset the symbol list
|
||||
controller.symbols.list()
|
||||
response.set_status(204)
|
||||
@@ -91,7 +91,6 @@ def parse_arguments(argv):
|
||||
parser.add_argument("--host", help="run on the given host/IP address")
|
||||
parser.add_argument("--port", help="run on the given port", type=int)
|
||||
parser.add_argument("--ssl", action="store_true", help="run in SSL mode")
|
||||
parser.add_argument("--no-ubridge", action="store_false", help="do not use ubridge to handle node connections")
|
||||
parser.add_argument("--config", help="Configuration file")
|
||||
parser.add_argument("--certfile", help="SSL cert file")
|
||||
parser.add_argument("--certkey", help="SSL key file")
|
||||
|
||||
@@ -137,7 +137,7 @@ VM_CREATE_SCHEMA = {
|
||||
},
|
||||
"mac_addr": {
|
||||
"description": "Base MAC address",
|
||||
"type": "string",
|
||||
"type": ["null", "string"],
|
||||
"minLength": 1,
|
||||
"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
|
||||
},
|
||||
@@ -355,7 +355,7 @@ VM_UPDATE_SCHEMA = {
|
||||
},
|
||||
"mac_addr": {
|
||||
"description": "Base MAC address",
|
||||
"type": "string",
|
||||
"type": ["null", "string"],
|
||||
"minLength": 1,
|
||||
"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
|
||||
},
|
||||
@@ -595,7 +595,7 @@ VM_OBJECT_SCHEMA = {
|
||||
},
|
||||
"mac_addr": {
|
||||
"description": "Base MAC address",
|
||||
"type": "string",
|
||||
"type": ["null", "string"]
|
||||
#"minLength": 1,
|
||||
#"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
|
||||
},
|
||||
|
||||
@@ -537,7 +537,7 @@ QEMU_OBJECT_SCHEMA = {
|
||||
"description": "Console TCP port",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"type": "integer"
|
||||
"type": ["integer", "null"]
|
||||
},
|
||||
"console_type": {
|
||||
"description": "Console type",
|
||||
|
||||
@@ -69,7 +69,7 @@ class UBridgeHypervisor:
|
||||
connection_success = False
|
||||
last_exception = None
|
||||
while time.time() - begin < timeout:
|
||||
yield from asyncio.sleep(0.01)
|
||||
yield from asyncio.sleep(0.1)
|
||||
try:
|
||||
self._reader, self._writer = yield from asyncio.open_connection(host, self._port)
|
||||
except OSError as e:
|
||||
@@ -84,6 +84,7 @@ class UBridgeHypervisor:
|
||||
log.info("Connected to uBridge hypervisor after {:.4f} seconds".format(time.time() - begin))
|
||||
|
||||
try:
|
||||
yield from asyncio.sleep(0.1)
|
||||
version = yield from self.send("hypervisor version")
|
||||
self._version = version[0].split("-", 1)[0]
|
||||
except IndexError:
|
||||
@@ -237,7 +238,7 @@ class UBridgeHypervisor:
|
||||
.format(host=self._host, port=self._port, command=command, run=self.is_running()))
|
||||
else:
|
||||
retries += 1
|
||||
yield from asyncio.sleep(0.1)
|
||||
yield from asyncio.sleep(0.5)
|
||||
continue
|
||||
retries = 0
|
||||
buf += chunk.decode("utf-8")
|
||||
|
||||
@@ -198,8 +198,8 @@ class UnstoppableEventLoop(EventLoop):
|
||||
|
||||
|
||||
class ShellConnection(TelnetConnection):
|
||||
def __init__(self, reader, writer, shell, loop):
|
||||
super(ShellConnection, self).__init__(reader, writer)
|
||||
def __init__(self, reader, writer, shell, window_size_changed_callback, loop):
|
||||
super(ShellConnection, self).__init__(reader, writer, window_size_changed_callback)
|
||||
self._shell = shell
|
||||
self._loop = loop
|
||||
self._cli = None
|
||||
@@ -239,9 +239,12 @@ class ShellConnection(TelnetConnection):
|
||||
def disconnected(self):
|
||||
pass
|
||||
|
||||
@asyncio.coroutine
|
||||
def window_size_changed(self, columns, rows):
|
||||
self._size = Size(rows=rows, columns=columns)
|
||||
self._cb.terminal_size_changed()
|
||||
if self._window_size_changed_callback:
|
||||
yield from self._window_size_changed_callback(columns, rows)
|
||||
|
||||
@asyncio.coroutine
|
||||
def feed(self, data):
|
||||
@@ -291,8 +294,8 @@ def create_telnet_shell(shell, loop=None):
|
||||
if loop is None:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def factory(reader, writer):
|
||||
return ShellConnection(reader, writer, shell, loop)
|
||||
def factory(reader, writer, window_size_changed_callback):
|
||||
return ShellConnection(reader, writer, shell, window_size_changed_callback, loop)
|
||||
|
||||
return AsyncioTelnetServer(binary=True, echo=True, naws=True, connection_factory=factory)
|
||||
|
||||
|
||||
@@ -15,7 +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 re
|
||||
|
||||
import asyncio
|
||||
import asyncio.subprocess
|
||||
import struct
|
||||
@@ -62,10 +62,11 @@ READ_SIZE = 1024
|
||||
|
||||
class TelnetConnection(object):
|
||||
"""Default implementation of telnet connection which may but may not be used."""
|
||||
def __init__(self, reader, writer):
|
||||
def __init__(self, reader, writer, window_size_changed_callback=None):
|
||||
self.is_closing = False
|
||||
self._reader = reader
|
||||
self._writer = writer
|
||||
self._window_size_changed_callback = window_size_changed_callback
|
||||
|
||||
@property
|
||||
def reader(self):
|
||||
@@ -85,10 +86,13 @@ class TelnetConnection(object):
|
||||
"""Method called when client is disconnecting"""
|
||||
pass
|
||||
|
||||
@asyncio.coroutine
|
||||
def window_size_changed(self, columns, rows):
|
||||
"""Method called when window size changed, only can occur when
|
||||
`naws` flag is enable in server configuration."""
|
||||
pass
|
||||
|
||||
if self._window_size_changed_callback:
|
||||
yield from self._window_size_changed_callback(columns, rows)
|
||||
|
||||
@asyncio.coroutine
|
||||
def feed(self, data):
|
||||
@@ -116,7 +120,7 @@ class TelnetConnection(object):
|
||||
class AsyncioTelnetServer:
|
||||
MAX_NEGOTIATION_READ = 10
|
||||
|
||||
def __init__(self, reader=None, writer=None, binary=True, echo=False, naws=False, connection_factory=None):
|
||||
def __init__(self, reader=None, writer=None, binary=True, echo=False, naws=False, window_size_changed_callback=None, connection_factory=None):
|
||||
"""
|
||||
Initializes telnet server
|
||||
:param naws when True make a window size negotiation
|
||||
@@ -131,6 +135,7 @@ class AsyncioTelnetServer:
|
||||
self._lock = asyncio.Lock()
|
||||
self._reader_process = None
|
||||
self._current_read = None
|
||||
self._window_size_changed_callback = window_size_changed_callback
|
||||
|
||||
self._binary = binary
|
||||
# If echo is true when the client send data
|
||||
@@ -139,8 +144,8 @@ class AsyncioTelnetServer:
|
||||
self._echo = echo
|
||||
self._naws = naws
|
||||
|
||||
def default_connection_factory(reader, writer):
|
||||
return TelnetConnection(reader, writer)
|
||||
def default_connection_factory(reader, writer, window_size_changed_callback):
|
||||
return TelnetConnection(reader, writer, window_size_changed_callback)
|
||||
|
||||
if connection_factory is None:
|
||||
connection_factory = default_connection_factory
|
||||
@@ -190,7 +195,7 @@ class AsyncioTelnetServer:
|
||||
@asyncio.coroutine
|
||||
def run(self, network_reader, network_writer):
|
||||
# Keep track of connected clients
|
||||
connection = self._connection_factory(network_reader, network_writer)
|
||||
connection = self._connection_factory(network_reader, network_writer, self._window_size_changed_callback)
|
||||
self._connections[network_writer] = connection
|
||||
|
||||
try:
|
||||
@@ -215,7 +220,7 @@ class AsyncioTelnetServer:
|
||||
try:
|
||||
writer.write_eof()
|
||||
yield from writer.drain()
|
||||
except ConnectionError:
|
||||
except (AttributeError, ConnectionError):
|
||||
continue
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -307,6 +312,7 @@ class AsyncioTelnetServer:
|
||||
cmd.append(buffer[location])
|
||||
return op
|
||||
|
||||
@asyncio.coroutine
|
||||
def _negotiate(self, data, connection):
|
||||
""" Performs negotiation commands"""
|
||||
|
||||
@@ -314,7 +320,7 @@ class AsyncioTelnetServer:
|
||||
if command == NAWS:
|
||||
if len(payload) == 4:
|
||||
columns, rows = struct.unpack(str('!HH'), bytes(payload))
|
||||
connection.window_size_changed(columns, rows)
|
||||
yield from connection.window_size_changed(columns, rows)
|
||||
else:
|
||||
log.warning('Wrong number of NAWS bytes')
|
||||
else:
|
||||
@@ -373,7 +379,7 @@ class AsyncioTelnetServer:
|
||||
break
|
||||
|
||||
# SE command is followed by IAC, remove the last two operations from stack
|
||||
self._negotiate(negotiation[0:-2], connection)
|
||||
yield from self._negotiate(negotiation[0:-2], connection)
|
||||
|
||||
# This must be a 3-byte TELNET command
|
||||
else:
|
||||
|
||||
@@ -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 re
|
||||
import io
|
||||
import struct
|
||||
from xml.etree.ElementTree import ElementTree, ParseError
|
||||
@@ -103,13 +104,21 @@ def get_size(data, default_width=0, default_height=0):
|
||||
root = tree.getroot()
|
||||
|
||||
try:
|
||||
width_attr = root.attrib.get("width", "0")
|
||||
height_attr = root.attrib.get("height", "0")
|
||||
width_attr = root.attrib.get("width", "100%")
|
||||
height_attr = root.attrib.get("height", "100%")
|
||||
if width_attr.endswith("%") or height_attr.endswith("%"):
|
||||
# check to viewBox attribute if width or height value is a percentage
|
||||
_, _, width_attr, height_attr = root.attrib.get("viewBox").split()
|
||||
viewbox = root.attrib.get("viewBox")
|
||||
if not viewbox:
|
||||
raise ValueError("Invalid SVG file: missing viewBox attribute")
|
||||
_, _, viewbox_width, viewbox_height = re.split(r'[\s,]+', viewbox)
|
||||
if width_attr.endswith("%"):
|
||||
width = _svg_convert_size(viewbox_width, width_attr)
|
||||
else:
|
||||
width = _svg_convert_size(width_attr)
|
||||
if height_attr.endswith("%"):
|
||||
height = _svg_convert_size(viewbox_height, height_attr)
|
||||
else:
|
||||
height = _svg_convert_size(height_attr)
|
||||
except (AttributeError, IndexError) as e:
|
||||
raise ValueError("Invalid SVG file: {}".format(e))
|
||||
@@ -117,11 +126,12 @@ def get_size(data, default_width=0, default_height=0):
|
||||
return width, height, filetype
|
||||
|
||||
|
||||
def _svg_convert_size(size):
|
||||
def _svg_convert_size(size, percent=None):
|
||||
"""
|
||||
Convert svg size to the px version
|
||||
|
||||
:param size: String with the size
|
||||
:param percent: String with the percentage, None = 100%
|
||||
"""
|
||||
|
||||
# https://www.w3.org/TR/SVG/coords.html#Units
|
||||
@@ -133,8 +143,11 @@ def _svg_convert_size(size):
|
||||
"in": 90,
|
||||
"px": 1
|
||||
}
|
||||
if len(size) > 3:
|
||||
factor = 1.0
|
||||
if len(size) >= 3:
|
||||
if size[-2:] in conversion_table:
|
||||
return round(float(size[:-2]) * conversion_table[size[-2:]])
|
||||
|
||||
return round(float(size))
|
||||
factor = conversion_table[size[-2:]]
|
||||
size = size[:-2]
|
||||
if percent:
|
||||
factor *= float(percent.rstrip("%")) / 100.0
|
||||
return round(float(size) * factor)
|
||||
@@ -23,8 +23,8 @@
|
||||
# or negative for a release candidate or beta (after the base version
|
||||
# number has been incremented)
|
||||
|
||||
__version__ = "2.1.10"
|
||||
__version_info__ = (2, 1, 10, 0)
|
||||
__version__ = "2.1.16"
|
||||
__version_info__ = (2, 1, 16, 0)
|
||||
|
||||
# If it's a git checkout try to add the commit
|
||||
if "dev" in __version__:
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
jsonschema>=2.4.0
|
||||
jsonschema==2.6.0
|
||||
aiohttp>=2.3.3,<2.4.0 # pyup: ignore
|
||||
aiohttp-cors>=0.5.3,<0.6.0 # pyup: ignore
|
||||
yarl>=0.11
|
||||
@@ -8,4 +8,4 @@ psutil>=3.0.0
|
||||
zipstream>=1.1.4
|
||||
typing>=3.5.3.0 # Otherwise yarl fails with python 3.4
|
||||
prompt-toolkit==1.0.15
|
||||
async-timeout<3.0.0 # pyup: ignore; 3.0 drops support for python 3.4
|
||||
async-timeout<3.0.0 # pyup: ignore; 3.0 drops support for python 3.4
|
||||
@@ -971,13 +971,12 @@ def test_mount_binds(vm, tmpdir):
|
||||
|
||||
def test_start_vnc(vm, loop):
|
||||
vm.console_resolution = "1280x1024"
|
||||
with patch("shutil.which", return_value="/bin/x"):
|
||||
with patch("shutil.which", return_value="/bin/Xtigervnc"):
|
||||
with asyncio_patch("gns3server.compute.docker.docker_vm.wait_for_file_creation") as mock_wait:
|
||||
with asyncio_patch("asyncio.create_subprocess_exec") as mock_exec:
|
||||
loop.run_until_complete(asyncio.ensure_future(vm._start_vnc()))
|
||||
assert vm._display is not None
|
||||
mock_exec.assert_any_call("Xvfb", "-nolisten", "tcp", ":{}".format(vm._display), "-screen", "0", "1280x1024x16")
|
||||
mock_exec.assert_any_call("x11vnc", "-forever", "-nopw", "-shared", "-geometry", "1280x1024", "-display", "WAIT:{}".format(vm._display), "-rfbport", str(vm.console), "-rfbportv6", str(vm.console), "-noncache", "-listen", "127.0.0.1")
|
||||
assert mock_exec.call_args[0] == ("Xtigervnc", "-geometry", vm.console_resolution, "-depth", "16", "-interface", "127.0.0.1", "-rfbport", str(vm.console), "-AlwaysShared", "-SecurityTypes", "None", ":{}".format(vm._display))
|
||||
mock_wait.assert_called_with("/tmp/.X11-unix/X{}".format(vm._display))
|
||||
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ def test_is_running(vm, running_subprocess_mock):
|
||||
|
||||
|
||||
def test_start(loop, vm, running_subprocess_mock):
|
||||
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="3.1.0")
|
||||
with asyncio_patch("gns3server.compute.qemu.QemuVM.start_wrap_console"):
|
||||
with asyncio_patch("asyncio.create_subprocess_exec", return_value=running_subprocess_mock) as mock:
|
||||
loop.run_until_complete(asyncio.ensure_future(vm.start()))
|
||||
@@ -130,6 +131,7 @@ def test_stop(loop, vm, running_subprocess_mock):
|
||||
future = asyncio.Future()
|
||||
future.set_result(True)
|
||||
process.wait.return_value = future
|
||||
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="3.1.0")
|
||||
|
||||
with asyncio_patch("gns3server.compute.qemu.QemuVM.start_wrap_console"):
|
||||
with asyncio_patch("asyncio.create_subprocess_exec", return_value=process):
|
||||
@@ -213,6 +215,7 @@ def test_port_remove_nio_binding(vm, loop):
|
||||
|
||||
|
||||
def test_close(vm, port_manager, loop):
|
||||
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="3.1.0")
|
||||
with asyncio_patch("gns3server.compute.qemu.QemuVM.start_wrap_console"):
|
||||
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()):
|
||||
loop.run_until_complete(asyncio.ensure_future(vm.start()))
|
||||
@@ -338,6 +341,7 @@ def test_disk_options(vm, tmpdir, loop, fake_qemu_img_binary):
|
||||
|
||||
def test_cdrom_option(vm, tmpdir, loop, fake_qemu_img_binary):
|
||||
|
||||
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="3.1.0")
|
||||
vm._cdrom_image = str(tmpdir / "test.iso")
|
||||
open(vm._cdrom_image, "w+").close()
|
||||
|
||||
@@ -348,6 +352,7 @@ def test_cdrom_option(vm, tmpdir, loop, fake_qemu_img_binary):
|
||||
|
||||
def test_bios_option(vm, tmpdir, loop, fake_qemu_img_binary):
|
||||
|
||||
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="3.1.0")
|
||||
vm._bios_image = str(tmpdir / "test.img")
|
||||
open(vm._bios_image, "w+").close()
|
||||
|
||||
@@ -454,6 +459,7 @@ def test_control_vm_expect_text(vm, loop, running_subprocess_mock):
|
||||
|
||||
def test_build_command(vm, loop, fake_qemu_binary, port_manager):
|
||||
|
||||
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="3.1.0")
|
||||
os.environ["DISPLAY"] = "0:0"
|
||||
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process:
|
||||
cmd = loop.run_until_complete(asyncio.ensure_future(vm._build_command()))
|
||||
@@ -477,7 +483,9 @@ def test_build_command(vm, loop, fake_qemu_binary, port_manager):
|
||||
"-device",
|
||||
"e1000,mac={},netdev=gns3-0".format(vm._mac_address),
|
||||
"-netdev",
|
||||
"socket,id=gns3-0,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio.rport, nio.lport)
|
||||
"socket,id=gns3-0,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio.rport, nio.lport),
|
||||
"-display",
|
||||
"none"
|
||||
]
|
||||
|
||||
|
||||
@@ -486,6 +494,7 @@ def test_build_command_manual_uuid(vm, loop, fake_qemu_binary, port_manager):
|
||||
If user has set a uuid we keep it
|
||||
"""
|
||||
|
||||
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="3.1.0")
|
||||
vm.options = "-uuid e1c307a4-896f-11e6-81a5-3c07547807cc"
|
||||
os.environ["DISPLAY"] = "0:0"
|
||||
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process:
|
||||
@@ -524,7 +533,8 @@ def test_build_command_kvm(linux_platform, vm, loop, fake_qemu_binary, port_mana
|
||||
"-device",
|
||||
"e1000,mac={},netdev=gns3-0".format(vm._mac_address),
|
||||
"-netdev",
|
||||
"socket,id=gns3-0,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio.rport, nio.lport)
|
||||
"socket,id=gns3-0,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio.rport, nio.lport),
|
||||
"-nographic"
|
||||
]
|
||||
|
||||
|
||||
@@ -560,13 +570,52 @@ def test_build_command_kvm_2_4(linux_platform, vm, loop, fake_qemu_binary, port_
|
||||
"-device",
|
||||
"e1000,mac={},netdev=gns3-0".format(vm._mac_address),
|
||||
"-netdev",
|
||||
"socket,id=gns3-0,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio.rport, nio.lport)
|
||||
"socket,id=gns3-0,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio.rport, nio.lport),
|
||||
"-nographic"
|
||||
]
|
||||
|
||||
|
||||
def test_build_command_kvm_3_1(linux_platform, vm, loop, fake_qemu_binary, port_manager):
|
||||
"""
|
||||
Qemu 2.4 introduce an issue with KVM
|
||||
"""
|
||||
vm._run_with_kvm = MagicMock(return_value=True)
|
||||
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="3.1.0")
|
||||
os.environ["DISPLAY"] = "0:0"
|
||||
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process:
|
||||
cmd = loop.run_until_complete(asyncio.ensure_future(vm._build_command()))
|
||||
nio = vm._local_udp_tunnels[0][0]
|
||||
assert cmd == [
|
||||
fake_qemu_binary,
|
||||
"-name",
|
||||
"test",
|
||||
"-m",
|
||||
"256M",
|
||||
"-smp",
|
||||
"cpus=1",
|
||||
"-enable-kvm",
|
||||
"-machine",
|
||||
"smm=off",
|
||||
"-boot",
|
||||
"order=c",
|
||||
"-uuid",
|
||||
vm.id,
|
||||
"-serial",
|
||||
"telnet:127.0.0.1:{},server,nowait".format(vm._internal_console_port),
|
||||
"-net",
|
||||
"none",
|
||||
"-device",
|
||||
"e1000,mac={},netdev=gns3-0".format(vm._mac_address),
|
||||
"-netdev",
|
||||
"socket,id=gns3-0,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio.rport, nio.lport),
|
||||
"-display",
|
||||
"none"
|
||||
]
|
||||
|
||||
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows")
|
||||
def test_build_command_without_display(vm, loop, fake_qemu_binary):
|
||||
|
||||
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="2.5.0")
|
||||
os.environ["DISPLAY"] = ""
|
||||
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process:
|
||||
cmd = loop.run_until_complete(asyncio.ensure_future(vm._build_command()))
|
||||
@@ -575,6 +624,7 @@ def test_build_command_without_display(vm, loop, fake_qemu_binary):
|
||||
|
||||
def test_build_command_two_adapters(vm, loop, fake_qemu_binary, port_manager):
|
||||
|
||||
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="2.5.0")
|
||||
os.environ["DISPLAY"] = "0:0"
|
||||
vm.adapters = 2
|
||||
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process:
|
||||
@@ -604,7 +654,8 @@ def test_build_command_two_adapters(vm, loop, fake_qemu_binary, port_manager):
|
||||
"-device",
|
||||
"e1000,mac={},netdev=gns3-1".format(int_to_macaddress(macaddress_to_int(vm._mac_address) + 1)),
|
||||
"-netdev",
|
||||
"socket,id=gns3-1,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio2.rport, nio2.lport)
|
||||
"socket,id=gns3-1,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio2.rport, nio2.lport),
|
||||
"-nographic"
|
||||
]
|
||||
|
||||
|
||||
@@ -613,6 +664,7 @@ def test_build_command_two_adapters_mac_address(vm, loop, fake_qemu_binary, port
|
||||
Should support multiple base vmac address
|
||||
"""
|
||||
|
||||
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="2.5.0")
|
||||
vm.adapters = 2
|
||||
vm.mac_address = "00:00:ab:0e:0f:09"
|
||||
mac_0 = vm._mac_address
|
||||
|
||||
@@ -75,14 +75,13 @@ def test_create_node_from_appliance(http_controller, controller, project, comput
|
||||
"name": "test",
|
||||
"symbol": "guest.svg",
|
||||
"default_name_format": "{name}-{0}",
|
||||
"server": "example.com"
|
||||
"compute_id": "example.com"
|
||||
})}
|
||||
with asyncio_patch("gns3server.controller.project.Project.add_node_from_appliance") as mock:
|
||||
with asyncio_patch("gns3server.controller.project.Project.add_node_from_appliance", return_value={"name": "test", "node_type": "qemu", "compute_id": "example.com"}) as mock:
|
||||
response = http_controller.post("/projects/{}/appliances/{}".format(project.id, id), {
|
||||
"x": 42,
|
||||
"y": 12
|
||||
})
|
||||
mock.assert_called_with(id, x=42, y=12, compute_id=None)
|
||||
print(response.body)
|
||||
assert response.route == "/projects/{project_id}/appliances/{appliance_id}"
|
||||
assert response.status == 201
|
||||
Reference in new issue
Block a user