diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py
index 989bc9e3..7c13d7fc 100644
--- a/gns3server/controller/__init__.py
+++ b/gns3server/controller/__init__.py
@@ -19,6 +19,8 @@ import asyncio
import aiohttp
from ..config import Config
+from .project import Project
+from .hypervisor import Hypervisor
class Controller:
@@ -35,12 +37,17 @@ class Controller:
"""
return Config.instance().get_section_config("Server").getboolean("controller")
- def addHypervisor(self, hypervisor):
+ @asyncio.coroutine
+ def addHypervisor(self, hypervisor_id, **kwargs):
"""
Add a server to the dictionnary of hypervisors controlled by GNS3
+
+ :param kwargs: See the documentation of Hypervisor
"""
- if hypervisor.id not in self._hypervisors:
- self._hypervisors[hypervisor.id] = hypervisor
+ if hypervisor_id not in self._hypervisors:
+ hypervisor = Hypervisor(hypervisor_id=hypervisor_id, **kwargs)
+ self._hypervisors[hypervisor_id] = hypervisor
+ return self._hypervisors[hypervisor_id]
@property
def hypervisors(self):
@@ -49,19 +56,33 @@ class Controller:
"""
return self._hypervisors
+ def getHypervisor(self, hypervisor_id):
+ """
+ Return an hypervisor or raise a 404
+ """
+ try:
+ return self._hypervisors[hypervisor_id]
+ except KeyError:
+ raise aiohttp.web.HTTPNotFound(text="Hypervisor ID {} doesn't exist".format(hypervisor_id))
+
@asyncio.coroutine
- def addProject(self, project):
+ def addProject(self, project_id=None, **kwargs):
"""
- Add a server to the dictionnary of projects controlled by GNS3
+ Create a project or return an existing project
+
+ :param kwargs: See the documentation of Project
"""
- if project.id not in self._projects:
+ if project_id not in self._projects:
+ project = Project(project_id=project_id, **kwargs)
self._projects[project.id] = project
for hypervisor in self._hypervisors.values():
yield from project.addHypervisor(hypervisor)
+ return self._projects[project.id]
+ return self._projects[project_id]
def getProject(self, project_id):
"""
- Return a server or raise a 404
+ Return a project or raise a 404
"""
try:
return self._projects[project_id]
diff --git a/gns3server/controller/hypervisor.py b/gns3server/controller/hypervisor.py
index db9c1968..54bd8863 100644
--- a/gns3server/controller/hypervisor.py
+++ b/gns3server/controller/hypervisor.py
@@ -81,7 +81,7 @@ class Hypervisor:
}
@asyncio.coroutine
- def _httpQuery(self, method, path, data=None):
+ def httpQuery(self, method, path, data=None):
with aiohttp.Timeout(10):
with aiohttp.ClientSession() as session:
url = "{}://{}:{}/v2/hypervisor{}".format(self._protocol, self._host, self._port, path)
@@ -98,4 +98,4 @@ class Hypervisor:
@asyncio.coroutine
def post(self, path, data={}):
- yield from self._httpQuery("POST", path, data)
+ yield from self.httpQuery("POST", path, data)
diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py
index f8f26dc1..49832be1 100644
--- a/gns3server/controller/project.py
+++ b/gns3server/controller/project.py
@@ -18,6 +18,8 @@
import asyncio
from uuid import UUID, uuid4
+from .vm import VM
+
class Project:
"""
@@ -42,6 +44,7 @@ class Project:
self._path = path
self._temporary = temporary
self._hypervisors = set()
+ self._vms = {}
@property
def name(self):
@@ -64,6 +67,20 @@ class Project:
self._hypervisors.add(hypervisor)
yield from hypervisor.post("/projects", self)
+ @asyncio.coroutine
+ def addVM(self, hypervisor, vm_id, **kwargs):
+ """
+ Create a vm or return an existing vm
+
+ :param kwargs: See the documentation of VM
+ """
+ if vm_id not in self._vms:
+ vm = VM(self, hypervisor, vm_id=vm_id, **kwargs)
+ yield from vm.create()
+ self._vms[vm.id] = vm
+ return vm
+ return self._vms[vm_id]
+
@asyncio.coroutine
def close(self):
for hypervisor in self._hypervisors:
diff --git a/gns3server/controller/vm.py b/gns3server/controller/vm.py
new file mode 100644
index 00000000..a693fd2c
--- /dev/null
+++ b/gns3server/controller/vm.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 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 .
+
+
+import asyncio
+import uuid
+
+
+class VM:
+ def __init__(self, project, hypervisor, vm_id=None, vm_type=None, name=None, console=None, console_type="telnet", **kwargs):
+ """
+ :param project: Project of the VM
+ :param hypervisor: Hypervisor server where the server will run
+ :param vm_id: UUID of the vm. Integer id
+ :param vm_type: Type of emulator
+ :param name: Name of the vm
+ :param console: TCP port of the console
+ :param console_type: Type of the console (telnet, vnc, serial..)
+ :param kwargs: Emulator specific properties of the VM
+ """
+
+ if vm_id is None:
+ self._id = str(uuid.uuid4())
+ else:
+ self._id = vm_id
+
+ self._name = name
+ self._project = project
+ self._hypervisor = hypervisor
+ self._vm_type = vm_type
+ self._console = console
+ self._console_type = console_type
+ self._properties = kwargs
+
+ @property
+ def id(self):
+ return self._id
+
+ @property
+ def vm_type(self):
+ return self._vm_type
+
+ @property
+ def console(self):
+ return self._console
+
+ @property
+ def console_type(self):
+ return self._console_type
+
+ @property
+ def properties(self):
+ return self._properties
+
+ @property
+ def project(self):
+ return self._project
+
+ @asyncio.coroutine
+ def create(self):
+ data = self._properties
+ data["vm_id"] = self._id
+ data["console"] = self._console
+ data["console_type"] = self._console_type
+ yield from self._hypervisor.post("/projects/{}/{}/vms".format(self._project.id, self._vm_type), data=data)
+
+ def __json__(self):
+ return {
+ "hypervisor_id": self._hypervisor.id,
+ "vm_id": self._id,
+ "vm_type": self._vm_type,
+ "name": self._name,
+ "console": self._console,
+ "console_type": self._console_type,
+ "properties": self._properties
+ }
+
diff --git a/gns3server/handlers/api/controller/__init__.py b/gns3server/handlers/api/controller/__init__.py
index a195ab7f..99ecd923 100644
--- a/gns3server/handlers/api/controller/__init__.py
+++ b/gns3server/handlers/api/controller/__init__.py
@@ -18,3 +18,5 @@
from .hypervisor_handler import HypervisorHandler
from .project_handler import ProjectHandler
from .version_handler import VersionHandler
+from .vm_handler import VMHandler
+
diff --git a/gns3server/handlers/api/controller/hypervisor_handler.py b/gns3server/handlers/api/controller/hypervisor_handler.py
index 452bfb38..6250286f 100644
--- a/gns3server/handlers/api/controller/hypervisor_handler.py
+++ b/gns3server/handlers/api/controller/hypervisor_handler.py
@@ -44,9 +44,7 @@ class HypervisorHandler:
output=HYPERVISOR_OBJECT_SCHEMA)
def create(request, response):
- hypervisor = Hypervisor(request.json.pop("hypervisor_id"), **request.json)
- Controller.instance().addHypervisor(hypervisor)
-
+ hypervisor = yield from Controller.instance().addHypervisor(**request.json)
response.set_status(201)
response.json(hypervisor)
diff --git a/gns3server/handlers/api/controller/project_handler.py b/gns3server/handlers/api/controller/project_handler.py
index 7b526c7d..19521f14 100644
--- a/gns3server/handlers/api/controller/project_handler.py
+++ b/gns3server/handlers/api/controller/project_handler.py
@@ -22,7 +22,6 @@ import asyncio
from ....web.route import Route
from ....schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA
from ....controller import Controller
-from ....controller.project import Project
import logging
@@ -44,11 +43,11 @@ class ProjectHandler:
def create_project(request, response):
controller = Controller.instance()
- project = Project(name=request.json.get("name"),
- path=request.json.get("path"),
- project_id=request.json.get("project_id"),
- temporary=request.json.get("temporary", False))
- yield from controller.addProject(project)
+ project = yield from controller.addProject(
+ name=request.json.get("name"),
+ path=request.json.get("path"),
+ project_id=request.json.get("project_id"),
+ temporary=request.json.get("temporary", False))
response.set_status(201)
response.json(project)
diff --git a/gns3server/handlers/api/controller/vm_handler.py b/gns3server/handlers/api/controller/vm_handler.py
new file mode 100644
index 00000000..75696d79
--- /dev/null
+++ b/gns3server/handlers/api/controller/vm_handler.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 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 ....web.route import Route
+from ....schemas.vm import VM_OBJECT_SCHEMA
+from ....controller.project import Project
+from ....controller import Controller
+
+
+class VMHandler:
+ """
+ API entry point for VM
+ """
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/vms",
+ parameters={
+ "project_id": "UUID for the project"
+ },
+ status_codes={
+ 201: "Instance created",
+ 400: "Invalid request"
+ },
+ description="Create a new VM instance",
+ input=VM_OBJECT_SCHEMA,
+ output=VM_OBJECT_SCHEMA)
+ def create(request, response):
+
+ controller = Controller.instance()
+ hypervisor = controller.getHypervisor(request.json.pop("hypervisor_id"))
+ project = controller.getProject(request.match_info["project_id"])
+ vm = yield from project.addVM(hypervisor, request.json.pop("vm_id", None), **request.json)
+ response.set_status(201)
+ response.json(vm)
+
diff --git a/gns3server/schemas/vm.py b/gns3server/schemas/vm.py
index a1623a31..37b5133d 100644
--- a/gns3server/schemas/vm.py
+++ b/gns3server/schemas/vm.py
@@ -62,3 +62,48 @@ VM_CAPTURE_SCHEMA = {
"additionalProperties": False,
"required": ["capture_file_name"]
}
+
+
+VM_OBJECT_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "A VM object",
+ "type": "object",
+ "properties": {
+ "hypervisor_id": {
+ "description": "Server identifier",
+ "type": "string"
+ },
+ "vm_id": {
+ "description": "VM identifier",
+ "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}$"
+ },
+ "vm_type": {
+ "description": "Type of VM",
+ "enum": ["docker", "dynamips", "vpcs", "virtualbox", "vmware", "iou"]
+ },
+ "name": {
+ "description": "VM name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "console": {
+ "description": "Console TCP port",
+ "minimum": 1,
+ "maximum": 65535,
+ "type": ["integer", "null"]
+ },
+ "console_type": {
+ "description": "Console type",
+ "enum": ["serial", "vnc", "telnet"]
+ },
+ "properties": {
+ "description": "Properties specific to an emulator",
+ "type": "object"
+ }
+ },
+ "additionalProperties": False,
+ "required": ["name", "vm_type", "hypervisor_id"]
+}
diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py
index 7f78ea8a..0a7660eb 100644
--- a/tests/controller/test_controller.py
+++ b/tests/controller/test_controller.py
@@ -34,35 +34,40 @@ def test_isEnabled(controller):
assert controller.isEnabled()
-def test_addHypervisor(controller):
- hypervisor1 = Hypervisor("test1")
-
- controller.addHypervisor(hypervisor1)
+def test_addHypervisor(controller, async_run):
+ async_run(controller.addHypervisor("test1"))
assert len(controller.hypervisors) == 1
- controller.addHypervisor(Hypervisor("test1"))
+ async_run(controller.addHypervisor("test1"))
assert len(controller.hypervisors) == 1
- controller.addHypervisor(Hypervisor("test2"))
+ async_run(controller.addHypervisor("test2"))
assert len(controller.hypervisors) == 2
+def test_getHypervisor(controller, async_run):
+
+ hypervisor = async_run(controller.addHypervisor("test1"))
+
+ assert controller.getHypervisor("test1") == hypervisor
+ with pytest.raises(aiohttp.web.HTTPNotFound):
+ assert controller.getHypervisor("dsdssd")
+
+
def test_addProject(controller, async_run):
uuid1 = str(uuid.uuid4())
- project1 = Project(project_id=uuid1)
uuid2 = str(uuid.uuid4())
- async_run(controller.addProject(project1))
+ async_run(controller.addProject(project_id=uuid1))
assert len(controller.projects) == 1
- async_run(controller.addProject(Project(project_id=uuid1)))
+ async_run(controller.addProject(project_id=uuid1))
assert len(controller.projects) == 1
- async_run(controller.addProject(Project(project_id=uuid2)))
+ async_run(controller.addProject(project_id=uuid2))
assert len(controller.projects) == 2
def test_removeProject(controller, async_run):
uuid1 = str(uuid.uuid4())
- project1 = Project(project_id=uuid1)
- async_run(controller.addProject(project1))
+ project1 = async_run(controller.addProject(project_id=uuid1))
assert len(controller.projects) == 1
controller.removeProject(project1)
@@ -71,21 +76,19 @@ def test_removeProject(controller, async_run):
def test_addProject_with_hypervisor(controller, async_run):
uuid1 = str(uuid.uuid4())
- project1 = Project(project_id=uuid1)
hypervisor = Hypervisor("test1")
hypervisor.post = MagicMock()
- controller.addHypervisor(hypervisor)
+ controller._hypervisors = {"test1": hypervisor }
- async_run(controller.addProject(project1))
+ project1 = async_run(controller.addProject(project_id=uuid1))
hypervisor.post.assert_called_with("/projects", project1)
def test_getProject(controller, async_run):
uuid1 = str(uuid.uuid4())
- project = Project(project_id=uuid1)
- async_run(controller.addProject(project))
+ project = async_run(controller.addProject(project_id=uuid1))
assert controller.getProject(uuid1) == project
with pytest.raises(aiohttp.web.HTTPNotFound):
assert controller.getProject("dsdssd")
diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py
index 2ab6e466..3c4d479b 100644
--- a/tests/controller/test_project.py
+++ b/tests/controller/test_project.py
@@ -16,6 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+from unittest.mock import MagicMock
+
from gns3server.controller.project import Project
@@ -31,3 +33,10 @@ def test_affect_uuid():
def test_json(tmpdir):
p = Project()
assert p.__json__() == {"name": p.name, "project_id": p.id, "temporary": False, "path": None}
+
+
+def test_addVM(async_run):
+ hypervisor = MagicMock()
+ project = Project()
+ vm = async_run(project.addVM(hypervisor, None, name="test", vm_type="vpcs", startup_config="test.cfg"))
+ hypervisor.post.assert_called_with('/projects/{}/vpcs/vms'.format(project.id), data={'console': None, 'vm_id': vm.id, 'console_type': 'telnet', 'startup_config': 'test.cfg'})
diff --git a/tests/controller/test_vm.py b/tests/controller/test_vm.py
new file mode 100644
index 00000000..1639407c
--- /dev/null
+++ b/tests/controller/test_vm.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 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 .
+
+import pytest
+import uuid
+from unittest.mock import MagicMock
+
+
+from gns3server.controller.vm import VM
+from gns3server.controller.project import Project
+
+
+@pytest.fixture
+def hypervisor():
+ s = MagicMock()
+ s.id = "http://test.com:42"
+ return s
+
+
+@pytest.fixture
+def vm(hypervisor):
+ project = Project(str(uuid.uuid4()))
+ vm = VM(project, hypervisor,
+ name="demo",
+ vm_id=str(uuid.uuid4()),
+ vm_type="vpcs",
+ console_type="vnc")
+ return vm
+
+
+def test_json(vm, hypervisor):
+ assert vm.__json__() == {
+ "hypervisor_id": hypervisor.id,
+ "vm_id": vm.id,
+ "vm_type": vm.vm_type,
+ "name": "demo",
+ "console": vm.console,
+ "console_type": vm.console_type,
+ "properties": vm.properties
+ }
+
+
+def test_init_without_uuid(project, hypervisor):
+ vm = VM(project, hypervisor,
+ vm_type="vpcs",
+ console_type="vnc")
+ assert vm.id is not None
+
+
+def test_create(vm, hypervisor, project, async_run):
+ async_run(vm.create())
+ data = {
+ "console": None,
+ "console_type": "vnc",
+ "vm_id": vm.id
+ }
+ hypervisor.post.assert_called_with("/projects/{}/vpcs/vms".format(vm.project.id), data=data)
diff --git a/tests/handlers/api/controller/test_project.py b/tests/handlers/api/controller/test_project.py
index e3a49028..e41d93f9 100644
--- a/tests/handlers/api/controller/test_project.py
+++ b/tests/handlers/api/controller/test_project.py
@@ -1,13 +1,13 @@
# -*- 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,
+ #
+ # 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.
@@ -42,12 +42,10 @@ def project(http_controller):
def test_create_project_with_path(http_controller, tmpdir):
- with asyncio_patch("gns3server.controller.Controller.addProject") as mock:
- response = http_controller.post("/projects", {"name": "test", "path": str(tmpdir), "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"})
- assert response.status == 201
- assert response.json["name"] == "test"
- assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f"
- assert mock.called
+ response = http_controller.post("/projects", {"name": "test", "path": str(tmpdir), "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"})
+ assert response.status == 201
+ assert response.json["name"] == "test"
+ assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f"
def test_create_project_without_dir(http_controller):
diff --git a/tests/handlers/api/controller/test_vm.py b/tests/handlers/api/controller/test_vm.py
new file mode 100644
index 00000000..414f391b
--- /dev/null
+++ b/tests/handlers/api/controller/test_vm.py
@@ -0,0 +1,53 @@
+# -*- 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 .
+
+"""
+This test suite check /project endpoint
+"""
+
+import uuid
+import os
+import asyncio
+import aiohttp
+import pytest
+
+
+from unittest.mock import patch, MagicMock, PropertyMock
+from tests.utils import asyncio_patch
+
+from gns3server.handlers.api.controller.project_handler import ProjectHandler
+from gns3server.controller import Controller
+
+
+@pytest.fixture
+def hypervisor(http_controller, async_run):
+ hypervisor = MagicMock()
+ hypervisor.id = "example.com"
+ Controller.instance()._hypervisors = {"example.com": hypervisor}
+ return hypervisor
+
+
+@pytest.fixture
+def project(http_controller, async_run):
+ return async_run(Controller.instance().addProject())
+
+
+def test_create_vm(http_controller, tmpdir, project, hypervisor):
+ response = http_controller.post("/projects/{}/vms".format(project.id), {"name": "test", "vm_type": "vpcs", "hypervisor_id": "example.com"}, example=True)
+ assert response.status == 201
+ assert response.json["name"] == "test"
+