mirror of
https://github.com/GNS3/gns3-server
synced 2024-11-24 09:18:08 +00:00
parent
1995adf838
commit
9b0088728f
@ -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):
|
||||||
body = json.loads(body.decode("utf-8"))
|
if response.headers['CONTENT-TYPE'] == 'application/json':
|
||||||
|
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,7 +109,12 @@ 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)
|
||||||
raise DockerError("Docker has returned an error: {} {}".format(response.status, 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))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
yield from self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5})
|
try:
|
||||||
log.info("Docker container '{name}' [{image}] stopped".format(
|
yield from self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5})
|
||||||
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
|
||||||
# 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)
|
||||||
yield from self._ubridge_hypervisor.send(
|
try:
|
||||||
'docker move_to_ns {ifc} {ns} eth{adapter}'.format(
|
yield from self._ubridge_hypervisor.send(
|
||||||
ifc=adapter.guest_ifc, ns=namespace, adapter=adapter_number))
|
'docker move_to_ns {ifc} {ns} eth{adapter}'.format(
|
||||||
|
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
|
||||||
|
@ -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
|
||||||
|
@ -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():
|
||||||
|
@ -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._add_ubridge_connection") as mock_add_ubridge_connection:
|
with asyncio_patch("gns3server.modules.docker.DockerVM._get_namespace", return_value=42) as mock_namespace:
|
||||||
with asyncio_patch("gns3server.modules.docker.DockerVM._start_console") as mock_start_console:
|
with asyncio_patch("gns3server.modules.docker.DockerVM._add_ubridge_connection") as mock_add_ubridge_connection:
|
||||||
loop.run_until_complete(asyncio.async(vm.start()))
|
with asyncio_patch("gns3server.modules.docker.DockerVM._start_console") as mock_start_console:
|
||||||
|
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,9 +289,10 @@ 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._add_ubridge_connection") as mock_add_ubridge_connection:
|
with asyncio_patch("gns3server.modules.docker.DockerVM._get_namespace", return_value=42) as mock_namespace:
|
||||||
with asyncio_patch("gns3server.modules.docker.DockerVM._start_console") as mock_start_console:
|
with asyncio_patch("gns3server.modules.docker.DockerVM._add_ubridge_connection") as mock_add_ubridge_connection:
|
||||||
loop.run_until_complete(asyncio.async(vm.start()))
|
with asyncio_patch("gns3server.modules.docker.DockerVM._start_console") as mock_start_console:
|
||||||
|
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")
|
||||||
assert mock_add_ubridge_connection.called
|
assert mock_add_ubridge_connection.called
|
||||||
@ -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={})
|
||||||
|
Loading…
Reference in New Issue
Block a user