1
0
mirror of https://github.com/GNS3/gns3-server synced 2024-11-24 17:28:08 +00:00

Catch docker namespace error

Fix #424
This commit is contained in:
Julien Duponchelle 2016-02-11 15:49:28 +01:00
parent 1995adf838
commit 9b0088728f
No known key found for this signature in database
GPG Key ID: F1E2485547D4595D
6 changed files with 128 additions and 28 deletions

View File

@ -30,7 +30,7 @@ log = logging.getLogger(__name__)
from ..base_manager import BaseManager from ..base_manager import BaseManager
from ..project_manager import ProjectManager from ..project_manager import ProjectManager
from .docker_vm import DockerVM from .docker_vm import DockerVM
from .docker_error import DockerError from .docker_error import *
class Docker(BaseManager): class Docker(BaseManager):
@ -70,10 +70,14 @@ class Docker(BaseManager):
:param data: Dictionnary with the body. Will be transformed to a JSON :param data: Dictionnary with the body. Will be transformed to a JSON
:param params: Parameters added as a query arg :param params: Parameters added as a query arg
""" """
response = yield from self.http_query(method, path, data=data, params=params) response = yield from self.http_query(method, path, data=data, params=params)
body = yield from response.read() body = yield from response.read()
if len(body): if len(body):
if response.headers['CONTENT-TYPE'] == 'application/json':
body = json.loads(body.decode("utf-8")) body = json.loads(body.decode("utf-8"))
else:
body = body.decode("utf-8")
log.debug("Query Docker %s %s params=%s data=%s Response: %s", method, path, params, data, body) log.debug("Query Docker %s %s params=%s data=%s Response: %s", method, path, params, data, body)
return body return body
@ -105,6 +109,11 @@ class Docker(BaseManager):
except ValueError: except ValueError:
pass pass
log.debug("Query Docker %s %s params=%s data=%s Response: %s", method, path, params, data, body) log.debug("Query Docker %s %s params=%s data=%s Response: %s", method, path, params, data, body)
if response.status == 304:
raise DockerHttp304Error("Docker has returned an error: {} {}".format(response.status, body))
elif response.status == 404:
raise DockerHttp404Error("Docker has returned an error: {} {}".format(response.status, body))
else:
raise DockerError("Docker has returned an error: {} {}".format(response.status, body)) raise DockerError("Docker has returned an error: {} {}".format(response.status, body))
return response return response

View File

@ -24,3 +24,11 @@ from ..vm_error import VMError
class DockerError(VMError): class DockerError(VMError):
pass pass
class DockerHttp304Error(DockerError):
pass
class DockerHttp404Error(DockerError):
pass

View File

@ -27,12 +27,15 @@ import aiohttp
import json import json
from ...ubridge.hypervisor import Hypervisor from ...ubridge.hypervisor import Hypervisor
from .docker_error import DockerError from .docker_error import *
from ..base_vm import BaseVM from ..base_vm import BaseVM
from ..adapters.ethernet_adapter import EthernetAdapter from ..adapters.ethernet_adapter import EthernetAdapter
from ..nios.nio_udp import NIOUDP from ..nios.nio_udp import NIOUDP
from ...utils.asyncio.telnet_server import AsyncioTelnetServer from ...utils.asyncio.telnet_server import AsyncioTelnetServer
from ...ubridge.ubridge_error import UbridgeError, UbridgeNamespaceError
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -165,11 +168,23 @@ class DockerVM(BaseVM):
else: else:
result = yield from self.manager.query("POST", "containers/{}/start".format(self._cid)) result = yield from self.manager.query("POST", "containers/{}/start".format(self._cid))
namespace = yield from self._get_namespace()
yield from self._start_ubridge() yield from self._start_ubridge()
for adapter_number in range(0, self.adapters): for adapter_number in range(0, self.adapters):
nio = self._ethernet_adapters[adapter_number].get_nio(0) nio = self._ethernet_adapters[adapter_number].get_nio(0)
with (yield from self.manager.ubridge_lock): with (yield from self.manager.ubridge_lock):
yield from self._add_ubridge_connection(nio, adapter_number) try:
yield from self._add_ubridge_connection(nio, adapter_number, namespace)
except UbridgeNamespaceError:
yield from self.stop()
# The container can crash soon after the start this mean we can not move the interface to the container namespace
logdata = yield from self._get_log()
for line in logdata.split('\n'):
log.error(line)
raise DockerError(logdata)
yield from self._start_console() yield from self._start_console()
@ -258,10 +273,15 @@ class DockerVM(BaseVM):
if self._telnet_server: if self._telnet_server:
self._telnet_server.close() self._telnet_server.close()
self._telnet_server = None self._telnet_server = None
# t=5 number of seconds to wait before killing the container # 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}) yield from self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5})
log.info("Docker container '{name}' [{image}] stopped".format( log.info("Docker container '{name}' [{image}] stopped".format(
name=self._name, image=self._image)) name=self._name, image=self._image))
except DockerHttp304Error:
# Container is already stopped
pass
# Ignore runtime error because when closing the server # Ignore runtime error because when closing the server
except RuntimeError as e: except RuntimeError as e:
log.debug("Docker runtime error when closing: {}".format(str(e))) log.debug("Docker runtime error when closing: {}".format(str(e)))
@ -334,12 +354,13 @@ class DockerVM(BaseVM):
self._closed = True self._closed = True
@asyncio.coroutine @asyncio.coroutine
def _add_ubridge_connection(self, nio, adapter_number): def _add_ubridge_connection(self, nio, adapter_number, namespace):
""" """
Creates a connection in uBridge. Creates a connection in uBridge.
:param nio: NIO instance or None if it's a dummu interface (if an interface is missing in ubridge you can't see it via ifconfig in the container) :param nio: NIO instance or None if it's a dummu interface (if an interface is missing in ubridge you can't see it via ifconfig in the container)
:param adapter_number: adapter number :param adapter_number: adapter number
:param namespace: Container namespace (pid)
""" """
try: try:
adapter = self._ethernet_adapters[adapter_number] adapter = self._ethernet_adapters[adapter_number]
@ -362,11 +383,13 @@ class DockerVM(BaseVM):
'docker create_veth {hostif} {guestif}'.format( 'docker create_veth {hostif} {guestif}'.format(
guestif=adapter.guest_ifc, hostif=adapter.host_ifc)) guestif=adapter.guest_ifc, hostif=adapter.host_ifc))
namespace = yield from self._get_namespace()
log.debug("Move container %s adapter %s to namespace %s", self.name, adapter.guest_ifc, namespace) log.debug("Move container %s adapter %s to namespace %s", self.name, adapter.guest_ifc, namespace)
try:
yield from self._ubridge_hypervisor.send( yield from self._ubridge_hypervisor.send(
'docker move_to_ns {ifc} {ns} eth{adapter}'.format( 'docker move_to_ns {ifc} {ns} eth{adapter}'.format(
ifc=adapter.guest_ifc, ns=namespace, adapter=adapter_number)) ifc=adapter.guest_ifc, ns=namespace, adapter=adapter_number))
except UbridgeError as e:
raise UbridgeNamespaceError(e)
if isinstance(nio, NIOUDP): if isinstance(nio, NIOUDP):
yield from self._ubridge_hypervisor.send( yield from self._ubridge_hypervisor.send(
@ -587,3 +610,14 @@ class DockerVM(BaseVM):
log.info("Docker VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(name=self.name, log.info("Docker VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(name=self.name,
id=self.id, id=self.id,
adapter_number=adapter_number)) adapter_number=adapter_number))
@asyncio.coroutine
def _get_log(self):
"""
Return the log from the container
:returns: string
"""
result = yield from self.manager.query("GET", "containers/{}/logs".format(self._cid), params={"stderr": 1, "stdout": 1})
return result

View File

@ -24,3 +24,10 @@ class UbridgeError(Exception):
def __init__(self, message): def __init__(self, message):
Exception.__init__(self, message) Exception.__init__(self, message)
class UbridgeNamespaceError(Exception):
"""
Raised if ubridge can not move a container to a namespace
"""
pass

View File

@ -31,6 +31,7 @@ def test_query_success(loop):
vm._connected = True vm._connected = True
response = MagicMock() response = MagicMock()
response.status = 200 response.status = 200
response.headers = {'CONTENT-TYPE': 'application/json'}
@asyncio.coroutine @asyncio.coroutine
def read(): def read():

View File

@ -20,10 +20,12 @@ import uuid
import asyncio import asyncio
from tests.utils import asyncio_patch from tests.utils import asyncio_patch
from gns3server.ubridge.ubridge_error import UbridgeNamespaceError
from gns3server.modules.docker.docker_vm import DockerVM from gns3server.modules.docker.docker_vm import DockerVM
from gns3server.modules.docker.docker_error import DockerError from gns3server.modules.docker.docker_error import DockerError
from gns3server.modules.docker import Docker from gns3server.modules.docker import Docker
from unittest.mock import patch, MagicMock, PropertyMock, call from unittest.mock import patch, MagicMock, PropertyMock, call
from gns3server.config import Config from gns3server.config import Config
@ -240,17 +242,42 @@ def test_start(loop, vm, manager, free_console_port):
with asyncio_patch("gns3server.modules.docker.DockerVM._get_container_state", return_value="stopped"): with asyncio_patch("gns3server.modules.docker.DockerVM._get_container_state", return_value="stopped"):
with asyncio_patch("gns3server.modules.docker.Docker.query") as mock_query: with asyncio_patch("gns3server.modules.docker.Docker.query") as mock_query:
with asyncio_patch("gns3server.modules.docker.DockerVM._start_ubridge") as mock_start_ubridge: with asyncio_patch("gns3server.modules.docker.DockerVM._start_ubridge") as mock_start_ubridge:
with asyncio_patch("gns3server.modules.docker.DockerVM._get_namespace", return_value=42) as mock_namespace:
with asyncio_patch("gns3server.modules.docker.DockerVM._add_ubridge_connection") as mock_add_ubridge_connection: with asyncio_patch("gns3server.modules.docker.DockerVM._add_ubridge_connection") as mock_add_ubridge_connection:
with asyncio_patch("gns3server.modules.docker.DockerVM._start_console") as mock_start_console: with asyncio_patch("gns3server.modules.docker.DockerVM._start_console") as mock_start_console:
loop.run_until_complete(asyncio.async(vm.start())) loop.run_until_complete(asyncio.async(vm.start()))
mock_query.assert_called_with("POST", "containers/e90e34656842/start") mock_query.assert_called_with("POST", "containers/e90e34656842/start")
mock_add_ubridge_connection.assert_called_once_with(nio, 0) mock_add_ubridge_connection.assert_called_once_with(nio, 0, 42)
assert mock_start_ubridge.called assert mock_start_ubridge.called
assert mock_start_console.called assert mock_start_console.called
assert vm.status == "started" assert vm.status == "started"
def test_start_namespace_failed(loop, vm, manager, free_console_port):
assert vm.status != "started"
vm.adapters = 1
nio = manager.create_nio(0, {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"})
loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, nio)))
with asyncio_patch("gns3server.modules.docker.DockerVM._get_container_state", return_value="stopped"):
with asyncio_patch("gns3server.modules.docker.Docker.query") as mock_query:
with asyncio_patch("gns3server.modules.docker.DockerVM._start_ubridge") as mock_start_ubridge:
with asyncio_patch("gns3server.modules.docker.DockerVM._get_namespace", return_value=42) as mock_namespace:
with asyncio_patch("gns3server.modules.docker.DockerVM._add_ubridge_connection", side_effect=UbridgeNamespaceError()) as mock_add_ubridge_connection:
with asyncio_patch("gns3server.modules.docker.DockerVM._get_log", return_value='Hello not available') as mock_log:
with pytest.raises(DockerError):
loop.run_until_complete(asyncio.async(vm.start()))
mock_query.assert_any_call("POST", "containers/e90e34656842/start")
mock_add_ubridge_connection.assert_called_once_with(nio, 0, 42)
assert mock_start_ubridge.called
assert vm.status == "stopped"
def test_start_without_nio(loop, vm, manager, free_console_port): def test_start_without_nio(loop, vm, manager, free_console_port):
""" """
If no nio exists we will create one. If no nio exists we will create one.
@ -262,6 +289,7 @@ def test_start_without_nio(loop, vm, manager, free_console_port):
with asyncio_patch("gns3server.modules.docker.DockerVM._get_container_state", return_value="stopped"): with asyncio_patch("gns3server.modules.docker.DockerVM._get_container_state", return_value="stopped"):
with asyncio_patch("gns3server.modules.docker.Docker.query") as mock_query: with asyncio_patch("gns3server.modules.docker.Docker.query") as mock_query:
with asyncio_patch("gns3server.modules.docker.DockerVM._start_ubridge") as mock_start_ubridge: with asyncio_patch("gns3server.modules.docker.DockerVM._start_ubridge") as mock_start_ubridge:
with asyncio_patch("gns3server.modules.docker.DockerVM._get_namespace", return_value=42) as mock_namespace:
with asyncio_patch("gns3server.modules.docker.DockerVM._add_ubridge_connection") as mock_add_ubridge_connection: with asyncio_patch("gns3server.modules.docker.DockerVM._add_ubridge_connection") as mock_add_ubridge_connection:
with asyncio_patch("gns3server.modules.docker.DockerVM._start_console") as mock_start_console: with asyncio_patch("gns3server.modules.docker.DockerVM._start_console") as mock_start_console:
loop.run_until_complete(asyncio.async(vm.start())) loop.run_until_complete(asyncio.async(vm.start()))
@ -401,8 +429,8 @@ def test_add_ubridge_connection(loop, vm):
nio = vm.manager.create_nio(0, nio) nio = vm.manager.create_nio(0, nio)
nio.startPacketCapture("/tmp/capture.pcap") nio.startPacketCapture("/tmp/capture.pcap")
vm._ubridge_hypervisor = MagicMock() vm._ubridge_hypervisor = MagicMock()
with asyncio_patch("gns3server.modules.docker.DockerVM._get_namespace", return_value=42):
loop.run_until_complete(asyncio.async(vm._add_ubridge_connection(nio, 0))) loop.run_until_complete(asyncio.async(vm._add_ubridge_connection(nio, 0, 42)))
calls = [ calls = [
call.send("docker create_veth gns3-veth0ext gns3-veth0int"), call.send("docker create_veth gns3-veth0ext gns3-veth0int"),
@ -421,8 +449,8 @@ def test_add_ubridge_connection_none_nio(loop, vm):
nio = None nio = None
vm._ubridge_hypervisor = MagicMock() vm._ubridge_hypervisor = MagicMock()
with asyncio_patch("gns3server.modules.docker.DockerVM._get_namespace", return_value=42):
loop.run_until_complete(asyncio.async(vm._add_ubridge_connection(nio, 0))) loop.run_until_complete(asyncio.async(vm._add_ubridge_connection(nio, 0, 42)))
calls = [ calls = [
call.send("docker create_veth gns3-veth0ext gns3-veth0int"), call.send("docker create_veth gns3-veth0ext gns3-veth0int"),
@ -440,7 +468,7 @@ def test_add_ubridge_connection_invalid_adapter_number(loop, vm):
"rhost": "127.0.0.1"} "rhost": "127.0.0.1"}
nio = vm.manager.create_nio(0, nio) nio = vm.manager.create_nio(0, nio)
with pytest.raises(DockerError): with pytest.raises(DockerError):
loop.run_until_complete(asyncio.async(vm._add_ubridge_connection(nio, 12))) loop.run_until_complete(asyncio.async(vm._add_ubridge_connection(nio, 12, 42)))
def test_add_ubridge_connection_no_free_interface(loop, vm): def test_add_ubridge_connection_no_free_interface(loop, vm):
@ -456,7 +484,7 @@ def test_add_ubridge_connection_no_free_interface(loop, vm):
interfaces = ["gns3-veth{}ext".format(index) for index in range(128)] interfaces = ["gns3-veth{}ext".format(index) for index in range(128)]
with patch("psutil.net_if_addrs", return_value=interfaces): with patch("psutil.net_if_addrs", return_value=interfaces):
loop.run_until_complete(asyncio.async(vm._add_ubridge_connection(nio, 0))) loop.run_until_complete(asyncio.async(vm._add_ubridge_connection(nio, 0, 42)))
def test_delete_ubridge_connection(loop, vm): def test_delete_ubridge_connection(loop, vm):
@ -467,8 +495,8 @@ def test_delete_ubridge_connection(loop, vm):
"rport": 4343, "rport": 4343,
"rhost": "127.0.0.1"} "rhost": "127.0.0.1"}
nio = vm.manager.create_nio(0, nio) nio = vm.manager.create_nio(0, nio)
with asyncio_patch("gns3server.modules.docker.DockerVM._get_namespace", return_value=42):
loop.run_until_complete(asyncio.async(vm._add_ubridge_connection(nio, 0))) loop.run_until_complete(asyncio.async(vm._add_ubridge_connection(nio, 0, 42)))
loop.run_until_complete(asyncio.async(vm._delete_ubridge_connection(0))) loop.run_until_complete(asyncio.async(vm._delete_ubridge_connection(0)))
calls = [ calls = [
@ -561,3 +589,16 @@ def test_stop_capture(vm, tmpdir, manager, free_console_port, loop):
assert vm._ethernet_adapters[0].get_nio(0).capturing assert vm._ethernet_adapters[0].get_nio(0).capturing
loop.run_until_complete(asyncio.async(vm.stop_capture(0))) loop.run_until_complete(asyncio.async(vm.stop_capture(0)))
assert vm._ethernet_adapters[0].get_nio(0).capturing is False assert vm._ethernet_adapters[0].get_nio(0).capturing is False
def test_get_log(loop, vm):
@asyncio.coroutine
def read():
return b'Hello\nWorld'
mock_query = MagicMock()
mock_query.read = read
with asyncio_patch("gns3server.modules.docker.Docker.http_query", return_value=mock_query) as mock:
images = loop.run_until_complete(asyncio.async(vm._get_log()))
mock.assert_called_with("GET", "containers/e90e34656842/logs", params={"stderr": 1, "stdout": 1}, data={})