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"):