diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py
index 68297c25..d2fb9b5f 100644
--- a/gns3server/handlers/__init__.py
+++ b/gns3server/handlers/__init__.py
@@ -24,6 +24,7 @@ from gns3server.handlers.api.dynamips_device_handler import DynamipsDeviceHandle
from gns3server.handlers.api.dynamips_vm_handler import DynamipsVMHandler
from gns3server.handlers.api.qemu_handler import QEMUHandler
from gns3server.handlers.api.virtualbox_handler import VirtualBoxHandler
+from gns3server.handlers.api.docker_handler import DockerHandler
from gns3server.handlers.api.vpcs_handler import VPCSHandler
from gns3server.handlers.api.vmware_handler import VMwareHandler
from gns3server.handlers.api.config_handler import ConfigHandler
diff --git a/gns3server/handlers/api/docker_handler.py b/gns3server/handlers/api/docker_handler.py
new file mode 100644
index 00000000..0f015054
--- /dev/null
+++ b/gns3server/handlers/api/docker_handler.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from aiohttp.web import HTTPConflict
+
+from ...web.route import Route
+from ...modules.docker import Docker
+
+from ...schemas.docker import (
+ DOCKER_CREATE_SCHEMA, DOCKER_UPDATE_SCHEMA, DOCKER_CAPTURE_SCHEMA,
+ DOCKER_OBJECT_SCHEMA
+)
+from ...schemas.nio import NIO_SCHEMA
+
+
+class DockerHandler:
+ """API entry points for Docker."""
+
+ @classmethod
+ @Route.get(
+ r"/docker/images",
+ status_codes={
+ 200: "Success",
+ },
+ description="Get all available Docker images")
+ def show(request, response):
+ docker_manager = Docker.instance()
+ images = yield from docker_manager.list_images()
+ response.json(images)
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/docker/images",
+ parameters={
+ "project_id": "UUID for the project"
+ },
+ status_codes={
+ 201: "Instance created",
+ 400: "Invalid request",
+ 409: "Conflict"
+ },
+ description="Create a new Docker container",
+ input=DOCKER_CREATE_SCHEMA,
+ output=DOCKER_OBJECT_SCHEMA)
+ def create(request, response):
+ docker_manager = Docker.instance()
+ container = yield from docker_manager.create_vm(
+ request.json.pop("name"),
+ request.match_info["project_id"],
+ request.json.get("id"),
+ image=request.json.pop("imagename"),
+ startcmd=request.json.get("startcmd")
+ )
+ # FIXME: DO WE NEED THIS?
+ for name, value in request.json.items():
+ if name != "_vm_id":
+ if hasattr(container, name) and getattr(container, name) != value:
+ setattr(container, name, value)
+
+ response.set_status(201)
+ response.json(container)
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/docker/images/{id}/start",
+ parameters={
+ "project_id": "UUID of the project",
+ "id": "ID of the container"
+ },
+ status_codes={
+ 204: "Instance started",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Start a Docker container",
+ input=DOCKER_CREATE_SCHEMA,
+ output=DOCKER_OBJECT_SCHEMA)
+ def start(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ yield from container.start()
+ response.set_status(204)
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/docker/images/{id}/stop",
+ parameters={
+ "project_id": "UUID of the project",
+ "id": "ID of the container"
+ },
+ status_codes={
+ 204: "Instance stopped",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Stop a Docker container",
+ input=DOCKER_CREATE_SCHEMA,
+ output=DOCKER_OBJECT_SCHEMA)
+ def stop(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ yield from container.stop()
+ response.set_status(204)
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/docker/images/{id}/reload",
+ parameters={
+ "project_id": "UUID of the project",
+ "id": "ID of the container"
+ },
+ status_codes={
+ 204: "Instance restarted",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Restart a Docker container",
+ input=DOCKER_CREATE_SCHEMA,
+ output=DOCKER_OBJECT_SCHEMA)
+ def reload(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ yield from container.restart()
+ response.set_status(204)
+
+ @classmethod
+ @Route.delete(
+ r"/projects/{project_id}/docker/images/{id}",
+ parameters={
+ "id": "ID for the container",
+ "project_id": "UUID for the project"
+ },
+ status_codes={
+ 204: "Instance deleted",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Delete a Docker container")
+ def delete(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ yield from container.remove()
+ response.set_status(204)
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/docker/images/{id}/suspend",
+ parameters={
+ "project_id": "UUID of the project",
+ "id": "ID of the container"
+ },
+ status_codes={
+ 204: "Instance paused",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Pause a Docker container",
+ input=DOCKER_CREATE_SCHEMA,
+ output=DOCKER_OBJECT_SCHEMA)
+ def suspend(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ yield from container.pause()
+ response.set_status(204)
+
+ @Route.post(
+ r"/projects/{project_id}/docker/images/{id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
+ parameters={
+ "project_id": "UUID for the project",
+ "id": "ID of the container",
+ "adapter_number": "Adapter where the nio should be added",
+ "port_number": "Port on the adapter"
+ },
+ status_codes={
+ 201: "NIO created",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Add a NIO to a Docker container",
+ input=NIO_SCHEMA,
+ output=NIO_SCHEMA)
+ def create_nio(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ nio_type = request.json["type"]
+ if nio_type not in ("nio_udp"):
+ raise HTTPConflict(
+ text="NIO of type {} is not supported".format(nio_type))
+ nio = docker_manager.create_nio(
+ int(request.match_info["adapter_number"]), request.json)
+ adapter = container._ethernet_adapters[
+ int(request.match_info["adapter_number"])
+ ]
+ container.adapter_add_nio_binding(
+ int(request.match_info["adapter_number"]), nio)
+ response.set_status(201)
+ response.json(nio)
+
+ @classmethod
+ @Route.delete(
+ r"/projects/{project_id}/docker/images/{id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
+ parameters={
+ "project_id": "UUID for the project",
+ "id": "ID of the container",
+ "adapter_number": "Adapter where the nio should be added",
+ "port_number": "Port on the adapter"
+ },
+ status_codes={
+ 204: "NIO deleted",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Remove a NIO from a Docker container")
+ def delete_nio(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ yield from container.adapter_remove_nio_binding(
+ int(request.match_info["adapter_number"]))
+ response.set_status(204)
diff --git a/gns3server/modules/docker/__init__.py b/gns3server/modules/docker/__init__.py
new file mode 100644
index 00000000..c20a4efb
--- /dev/null
+++ b/gns3server/modules/docker/__init__.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Docker server module.
+"""
+
+import asyncio
+import logging
+import aiohttp
+import docker
+from requests.exceptions import ConnectionError
+
+log = logging.getLogger(__name__)
+
+from ..base_manager import BaseManager
+from ..project_manager import ProjectManager
+from .docker_vm import Container
+from .docker_error import DockerError
+
+
+class Docker(BaseManager):
+
+ _VM_CLASS = Container
+
+ def __init__(self):
+ super().__init__()
+ # FIXME: make configurable and start docker before trying
+ self._server_url = 'unix://var/run/docker.sock'
+ self._client = docker.Client(base_url=self._server_url)
+ self._execute_lock = asyncio.Lock()
+
+ @property
+ def server_url(self):
+ """Returns the Docker server url.
+
+ :returns: url
+ :rtype: string
+ """
+ return self._server_url
+
+ @server_url.setter
+ def server_url(self, value):
+ self._server_url = value
+ self._client = docker.Client(base_url=value)
+
+ @asyncio.coroutine
+ def execute(self, command, kwargs, timeout=60):
+ command = getattr(self._client, command)
+ log.debug("Executing Docker with command: {}".format(command))
+ try:
+ result = command(**kwargs)
+ except Exception as error:
+ raise DockerError("Docker has returned an error: {}".format(error))
+ return result
+
+ @asyncio.coroutine
+ def list_images(self):
+ """Gets Docker image list.
+
+ :returns: list of dicts
+ :rtype: list
+ """
+ images = []
+ try:
+ for image in self._client.images():
+ for tag in image['RepoTags']:
+ images.append({'imagename': tag})
+ return images
+ except ConnectionError as error:
+ raise DockerError(
+ """Docker couldn't list images and returned an error: {}
+Is the Docker service running?""".format(error))
+
+ @asyncio.coroutine
+ def list_containers(self):
+ """Gets Docker container list.
+
+ :returns: list of dicts
+ :rtype: list
+ """
+ return self._client.containers()
+
+ def get_container(self, cid, project_id=None):
+ """Returns a Docker container.
+
+ :param id: Docker container identifier
+ :param project_id: Project identifier
+
+ :returns: Docker container
+ """
+ if project_id:
+ project = ProjectManager.instance().get_project(project_id)
+
+ if cid not in self._vms:
+ raise aiohttp.web.HTTPNotFound(
+ text="Docker container with ID {} doesn't exist".format(cid))
+
+ container = self._vms[cid]
+ if project_id:
+ if container.project.id != project.id:
+ raise aiohttp.web.HTTPNotFound(
+ text="Project ID {} doesn't belong to container {}".format(
+ project_id, container.name))
+ return container
diff --git a/gns3server/modules/docker/docker_error.py b/gns3server/modules/docker/docker_error.py
new file mode 100644
index 00000000..2e03ace5
--- /dev/null
+++ b/gns3server/modules/docker/docker_error.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Custom exceptions for the Docker module.
+"""
+
+from ..vm_error import VMError
+
+
+class DockerError(VMError):
+ pass
diff --git a/gns3server/modules/docker/docker_vm.py b/gns3server/modules/docker/docker_vm.py
new file mode 100644
index 00000000..50e56472
--- /dev/null
+++ b/gns3server/modules/docker/docker_vm.py
@@ -0,0 +1,405 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Docker container instance.
+"""
+
+import asyncio
+import shutil
+import docker
+import netifaces
+
+from docker.utils import create_host_config
+from gns3server.ubridge.hypervisor import Hypervisor
+from pkg_resources import parse_version
+from .docker_error import DockerError
+from ..base_vm import BaseVM
+from ..adapters.ethernet_adapter import EthernetAdapter
+from ..nios.nio_udp import NIOUDP
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Container(BaseVM):
+ """Docker container implementation.
+
+ :param name: Docker container name
+ :param vm_id: Docker VM identifier
+ :param project: Project instance
+ :param manager: Manager instance
+ :param image: Docker image
+ """
+ def __init__(self, name, vm_id, project, manager, image, startcmd=None):
+ self._name = name
+ self._id = vm_id
+ self._project = project
+ self._manager = manager
+ self._image = image
+ self._startcmd = startcmd
+ self._veths = []
+ self._ethernet_adapters = []
+ self._ubridge_hypervisor = None
+ self._temporary_directory = None
+ self._hw_virtualization = False
+
+ log.debug(
+ "{module}: {name} [{image}] initialized.".format(
+ module=self.manager.module_name,
+ name=self.name,
+ image=self._image))
+
+ def __json__(self):
+ return {
+ "name": self._name,
+ "vm_id": self._id,
+ "cid": self._cid,
+ "project_id": self._project.id,
+ "image": self._image,
+ }
+
+ @property
+ def veths(self):
+ """Returns Docker host veth interfaces."""
+ return self._veths
+
+ @asyncio.coroutine
+ def _get_container_state(self):
+ """Returns the container state (e.g. running, paused etc.)
+
+ :returns: state
+ :rtype: str
+ """
+ try:
+ result = yield from self.manager.execute(
+ "inspect_container", {"container": self._cid})
+ result_dict = {state.lower(): value for state, value in result["State"].items()}
+ for state, value in result_dict.items():
+ if value is True:
+ # a container can be both paused and running
+ if state == "paused":
+ return "paused"
+ if state == "running":
+ if "paused" in result_dict and result_dict["paused"] is True:
+ return "paused"
+ return state.lower()
+ return 'exited'
+ except Exception as err:
+ raise DockerError("Could not get container state for {0}: ".format(
+ self._name), str(err))
+
+ @asyncio.coroutine
+ def create(self):
+ """Creates the Docker container."""
+ params = {
+ "name": self._name,
+ "image": self._image,
+ "network_disabled": True,
+ "host_config": create_host_config(
+ privileged=True, cap_add=['ALL'])
+ }
+ if self._startcmd:
+ params.update({'command': self._startcmd})
+
+ result = yield from self.manager.execute("create_container", params)
+ self._cid = result['Id']
+ log.info("Docker container '{name}' [{id}] created".format(
+ name=self._name, id=self._id))
+ return True
+
+ @property
+ def ubridge_path(self):
+ """Returns the uBridge executable path.
+
+ :returns: path to uBridge
+ """
+ path = self._manager.config.get_section_config("Server").get(
+ "ubridge_path", "ubridge")
+ if path == "ubridge":
+ path = shutil.which("ubridge")
+ return path
+
+ @asyncio.coroutine
+ def _start_ubridge(self):
+ """Starts uBridge (handles connections to and from this Docker VM)."""
+ server_config = self._manager.config.get_section_config("Server")
+ server_host = server_config.get("host")
+ self._ubridge_hypervisor = Hypervisor(
+ self._project, self.ubridge_path, self.working_dir, server_host)
+
+ log.info("Starting new uBridge hypervisor {}:{}".format(
+ self._ubridge_hypervisor.host, self._ubridge_hypervisor.port))
+ yield from self._ubridge_hypervisor.start()
+ log.info("Hypervisor {}:{} has successfully started".format(
+ self._ubridge_hypervisor.host, self._ubridge_hypervisor.port))
+ yield from self._ubridge_hypervisor.connect()
+ if parse_version(
+ self._ubridge_hypervisor.version) < parse_version('0.9.1'):
+ raise DockerError(
+ "uBridge version must be >= 0.9.1, detected version is {}".format(
+ self._ubridge_hypervisor.version))
+
+ @asyncio.coroutine
+ def start(self):
+ """Starts this Docker container."""
+
+ state = yield from self._get_container_state()
+ if state == "paused":
+ yield from self.unpause()
+ else:
+ result = yield from self.manager.execute(
+ "start", {"container": self._cid})
+
+ yield from self._start_ubridge()
+ for adapter_number in range(0, self.adapters):
+ nio = self._ethernet_adapters[adapter_number].get_nio(0)
+ if nio:
+ yield from self._add_ubridge_connection(nio, adapter_number)
+
+ log.info("Docker container '{name}' [{image}] started".format(
+ name=self._name, image=self._image))
+
+ def is_running(self):
+ """Checks if the container is running.
+
+ :returns: True or False
+ :rtype: bool
+ """
+ state = yield from self._get_container_state()
+ if state == "running":
+ return True
+ return False
+
+ @asyncio.coroutine
+ def restart(self):
+ """Restarts this Docker container."""
+ result = yield from self.manager.execute(
+ "restart", {"container": self._cid})
+ log.info("Docker container '{name}' [{image}] restarted".format(
+ name=self._name, image=self._image))
+
+ @asyncio.coroutine
+ def stop(self):
+ """Stops this Docker container."""
+
+ if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
+ yield from self._ubridge_hypervisor.stop()
+
+ state = yield from self._get_container_state()
+ if state == "paused":
+ yield from self.unpause()
+ result = yield from self.manager.execute(
+ "kill", {"container": self._cid})
+ log.info("Docker container '{name}' [{image}] stopped".format(
+ name=self._name, image=self._image))
+
+ @asyncio.coroutine
+ def pause(self):
+ """Pauses this Docker container."""
+ result = yield from self.manager.execute(
+ "pause", {"container": self._cid})
+ log.info("Docker container '{name}' [{image}] paused".format(
+ name=self._name, image=self._image))
+
+ @asyncio.coroutine
+ def unpause(self):
+ """Unpauses this Docker container."""
+ result = yield from self.manager.execute(
+ "unpause", {"container": self._cid})
+ state = yield from self._get_container_state()
+ log.info("Docker container '{name}' [{image}] unpaused".format(
+ name=self._name, image=self._image))
+
+ @asyncio.coroutine
+ def remove(self):
+ """Removes this Docker container."""
+ state = yield from self._get_container_state()
+ if state == "paused":
+ yield from self.unpause()
+ if state == "running":
+ yield from self.stop()
+ result = yield from self.manager.execute(
+ "remove_container", {"container": self._cid, "force": True})
+ log.info("Docker container '{name}' [{image}] removed".format(
+ name=self._name, image=self._image))
+
+ @asyncio.coroutine
+ def close(self):
+ """Closes this Docker container."""
+ log.debug("Docker container '{name}' [{id}] is closing".format(
+ name=self.name, id=self._cid))
+ for adapter in self._ethernet_adapters.values():
+ if adapter is not None:
+ for nio in adapter.ports.values():
+ if nio and isinstance(nio, NIOUDP):
+ self.manager.port_manager.release_udp_port(
+ nio.lport, self._project)
+
+ yield from self.remove()
+
+ log.info("Docker container '{name}' [{id}] closed".format(
+ name=self.name, id=self._cid))
+ self._closed = True
+
+ def _add_ubridge_connection(self, nio, adapter_number):
+ """
+ Creates a connection in uBridge.
+
+ :param nio: NIO instance
+ :param adapter_number: adapter number
+ """
+ try:
+ adapter = self._ethernet_adapters[adapter_number]
+ except IndexError:
+ raise DockerError(
+ "Adapter {adapter_number} doesn't exist on Docker container '{name}'".format(
+ name=self.name, adapter_number=adapter_number))
+
+ if nio and isinstance(nio, NIOUDP):
+ ifcs = netifaces.interfaces()
+ for index in range(128):
+ ifcs = netifaces.interfaces()
+ if "gns3-veth{}ext".format(index) not in ifcs:
+ adapter.ifc = "eth{}".format(str(index))
+ adapter.host_ifc = "gns3-veth{}ext".format(str(index))
+ adapter.guest_ifc = "gns3-veth{}int".format(str(index))
+ break
+ if not hasattr(adapter, "ifc"):
+ raise DockerError(
+ "Adapter {adapter_number} couldn't allocate interface on Docker container '{name}'".format(
+ name=self.name, adapter_number=adapter_number))
+
+ yield from self._ubridge_hypervisor.send(
+ 'docker create_veth {hostif} {guestif}'.format(
+ guestif=adapter.guest_ifc, hostif=adapter.host_ifc))
+ self._veths.append(adapter.host_ifc)
+
+ namespace = yield from self.get_namespace()
+ yield from self._ubridge_hypervisor.send(
+ 'docker move_to_ns {ifc} {ns}'.format(
+ ifc=adapter.guest_ifc, ns=namespace))
+
+ yield from self._ubridge_hypervisor.send(
+ 'bridge create bridge{}'.format(adapter_number))
+ yield from self._ubridge_hypervisor.send(
+ 'bridge add_nio_linux_raw bridge{adapter} {ifc}'.format(
+ ifc=adapter.host_ifc, adapter=adapter_number))
+
+ if isinstance(nio, NIOUDP):
+ yield from self._ubridge_hypervisor.send(
+ 'bridge add_nio_udp bridge{adapter} {lport} {rhost} {rport}'.format(
+ adapter=adapter_number, lport=nio.lport, rhost=nio.rhost,
+ rport=nio.rport))
+
+ if nio.capturing:
+ yield from self._ubridge_hypervisor.send(
+ 'bridge start_capture bridge{adapter} "{pcap_file}"'.format(
+ adapter=adapter_number, pcap_file=nio.pcap_output_file))
+
+ yield from self._ubridge_hypervisor.send(
+ 'bridge start bridge{adapter}'.format(adapter=adapter_number))
+
+ def _delete_ubridge_connection(self, adapter_number):
+ """Deletes a connection in uBridge.
+
+ :param adapter_number: adapter number
+ """
+ yield from self._ubridge_hypervisor.send("bridge delete bridge{name}".format(
+ name=adapter_number))
+
+ adapter = self._ethernet_adapters[adapter_number]
+ yield from self._ubridge_hypervisor.send("docker delete_veth {name}".format(
+ name=adapter.host_ifc))
+
+ def adapter_add_nio_binding(self, adapter_number, nio):
+ """Adds an adapter NIO binding.
+
+ :param adapter_number: adapter number
+ :param nio: NIO instance to add to the slot/port
+ """
+ try:
+ adapter = self._ethernet_adapters[adapter_number]
+ except IndexError:
+ raise DockerError(
+ "Adapter {adapter_number} doesn't exist on Docker container '{name}'".format(
+ name=self.name, adapter_number=adapter_number))
+
+ adapter.add_nio(0, nio)
+ log.info(
+ "Docker container '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(
+ name=self.name,
+ id=self._id,
+ nio=nio,
+ adapter_number=adapter_number))
+
+ def adapter_remove_nio_binding(self, adapter_number):
+ """
+ Removes an adapter NIO binding.
+
+ :param adapter_number: adapter number
+
+ :returns: NIO instance
+ """
+ try:
+ adapter = self._ethernet_adapters[adapter_number]
+ except IndexError:
+ raise DockerError(
+ "Adapter {adapter_number} doesn't exist on Docker VM '{name}'".format(
+ name=self.name, adapter_number=adapter_number))
+
+ adapter.remove_nio(0)
+ try:
+ yield from self._delete_ubridge_connection(adapter_number)
+ except:
+ pass
+
+ log.info(
+ "Docker VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format(
+ name=self.name, id=self.id, nio=adapter.host_ifc,
+ adapter_number=adapter_number))
+
+ @property
+ def adapters(self):
+ """Returns the number of Ethernet adapters for this Docker VM.
+
+ :returns: number of adapters
+ :rtype: int
+ """
+ return len(self._ethernet_adapters)
+
+ @adapters.setter
+ def adapters(self, adapters):
+ """Sets the number of Ethernet adapters for this Docker container.
+
+ :param adapters: number of adapters
+ """
+
+ self._ethernet_adapters.clear()
+ for adapter_number in range(0, adapters):
+ self._ethernet_adapters.append(EthernetAdapter())
+
+ log.info(
+ 'Docker container "{name}" [{id}]: number of Ethernet adapters changed to {adapters}'.format(
+ name=self._name,
+ id=self._id,
+ adapters=adapters))
+
+ def get_namespace(self):
+ result = yield from self.manager.execute(
+ "inspect_container", {"container": self._cid})
+ return int(result['State']['Pid'])
diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py
index 0b5fab97..74aff05d 100644
--- a/gns3server/modules/port_manager.py
+++ b/gns3server/modules/port_manager.py
@@ -227,7 +227,6 @@ class PortManager:
:param project: Project instance
"""
-
port = self.find_unused_port(self._udp_port_range[0],
self._udp_port_range[1],
host=self._udp_host,
diff --git a/gns3server/schemas/docker.py b/gns3server/schemas/docker.py
new file mode 100644
index 00000000..1fb13190
--- /dev/null
+++ b/gns3server/schemas/docker.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+DOCKER_CREATE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to create a new Docker container",
+ "type": "object",
+ "properties": {
+ "vm_id": {
+ "description": "Docker VM instance identifier",
+ "oneOf": [
+ {"type": "string",
+ "minLength": 36,
+ "maxLength": 36,
+ "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"},
+ {"type": "integer"} # for legacy projects
+ ]
+ },
+ "name": {
+ "description": "Docker container name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "startcmd": {
+ "description": "Docker CMD entry",
+ "type": "string",
+ "minLength": 1,
+ },
+ "imagename": {
+ "description": "Docker image name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "adapters": {
+ "description": "number of adapters",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 64,
+ },
+ "adapter_type": {
+ "description": "Docker adapter type",
+ "type": "string",
+ "minLength": 1,
+ },
+ "console": {
+ "description": "console name",
+ "type": "string",
+ "minLength": 1,
+ },
+ },
+ "additionalProperties": False,
+}
+
+DOCKER_UPDATE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to update a Docker container",
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "Docker container name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "image": {
+ "description": "Docker image name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "adapters": {
+ "description": "number of adapters",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 64,
+ },
+ "adapter_type": {
+ "description": "Docker adapter type",
+ "type": "string",
+ "minLength": 1,
+ },
+ },
+ "additionalProperties": False,
+}
+
+DOCKER_CAPTURE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to start a packet capture on a Docker container port",
+ "type": "object",
+ "properties": {
+ "capture_file_name": {
+ "description": "Capture file name",
+ "type": "string",
+ "minLength": 1,
+ },
+ },
+ "additionalProperties": False,
+ "required": ["capture_file_name"]
+}
+
+DOCKER_OBJECT_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Docker instance",
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "Docker container name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "vm_id": {
+ "description": "Docker container instance UUID",
+ "type": "string",
+ "minLength": 36,
+ "maxLength": 36,
+ "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
+ },
+ "cid": {
+ "description": "Docker container ID",
+ "type": "string",
+ "minLength": 64,
+ "maxLength": 64,
+ "pattern": "^[a-zA-Z0-9_.-]{64}$"
+ },
+ "project_id": {
+ "description": "Project UUID",
+ "type": "string",
+ "minLength": 36,
+ "maxLength": 36,
+ "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
+ },
+ "image": {
+ "description": "Docker image name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "adapters": {
+ "description": "number of adapters",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 64,
+ },
+ "adapter_type": {
+ "description": "Docker adapter type",
+ "type": "string",
+ "minLength": 1,
+ },
+ },
+ "additionalProperties": False,
+ "required": ["vm_id", "project_id"]
+}
diff --git a/requirements.txt b/requirements.txt
index 636e980b..884281b0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,4 @@ aiohttp>=0.15.1
Jinja2>=2.7.3
raven>=5.2.0
gns3-netifaces==0.10.4.1
+docker-py==1.2.3
diff --git a/setup.py b/setup.py
index 4ca5da87..c6e1f77b 100644
--- a/setup.py
+++ b/setup.py
@@ -42,7 +42,8 @@ dependencies = [
"jsonschema>=2.4.0",
"aiohttp>=0.15.1",
"Jinja2>=2.7.3",
- "raven>=5.2.0"
+ "raven>=5.2.0",
+ "docker-py>=1.2.3"
]
if not sys.platform.startswith("win"):