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

API for creating a VM

This commit is contained in:
Julien Duponchelle 2016-03-10 21:51:29 +01:00
parent 65de1b7b5e
commit 4326d412f9
No known key found for this signature in database
GPG Key ID: F1E2485547D4595D
14 changed files with 405 additions and 48 deletions

View File

@ -19,6 +19,8 @@ import asyncio
import aiohttp import aiohttp
from ..config import Config from ..config import Config
from .project import Project
from .hypervisor import Hypervisor
class Controller: class Controller:
@ -35,12 +37,17 @@ class Controller:
""" """
return Config.instance().get_section_config("Server").getboolean("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 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: if hypervisor_id not in self._hypervisors:
self._hypervisors[hypervisor.id] = hypervisor hypervisor = Hypervisor(hypervisor_id=hypervisor_id, **kwargs)
self._hypervisors[hypervisor_id] = hypervisor
return self._hypervisors[hypervisor_id]
@property @property
def hypervisors(self): def hypervisors(self):
@ -49,19 +56,33 @@ class Controller:
""" """
return self._hypervisors 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 @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 self._projects[project.id] = project
for hypervisor in self._hypervisors.values(): for hypervisor in self._hypervisors.values():
yield from project.addHypervisor(hypervisor) yield from project.addHypervisor(hypervisor)
return self._projects[project.id]
return self._projects[project_id]
def getProject(self, project_id): def getProject(self, project_id):
""" """
Return a server or raise a 404 Return a project or raise a 404
""" """
try: try:
return self._projects[project_id] return self._projects[project_id]

View File

@ -81,7 +81,7 @@ class Hypervisor:
} }
@asyncio.coroutine @asyncio.coroutine
def _httpQuery(self, method, path, data=None): def httpQuery(self, method, path, data=None):
with aiohttp.Timeout(10): with aiohttp.Timeout(10):
with aiohttp.ClientSession() as session: with aiohttp.ClientSession() as session:
url = "{}://{}:{}/v2/hypervisor{}".format(self._protocol, self._host, self._port, path) url = "{}://{}:{}/v2/hypervisor{}".format(self._protocol, self._host, self._port, path)
@ -98,4 +98,4 @@ class Hypervisor:
@asyncio.coroutine @asyncio.coroutine
def post(self, path, data={}): def post(self, path, data={}):
yield from self._httpQuery("POST", path, data) yield from self.httpQuery("POST", path, data)

View File

@ -18,6 +18,8 @@
import asyncio import asyncio
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from .vm import VM
class Project: class Project:
""" """
@ -42,6 +44,7 @@ class Project:
self._path = path self._path = path
self._temporary = temporary self._temporary = temporary
self._hypervisors = set() self._hypervisors = set()
self._vms = {}
@property @property
def name(self): def name(self):
@ -64,6 +67,20 @@ class Project:
self._hypervisors.add(hypervisor) self._hypervisors.add(hypervisor)
yield from hypervisor.post("/projects", self) 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 @asyncio.coroutine
def close(self): def close(self):
for hypervisor in self._hypervisors: for hypervisor in self._hypervisors:

View File

@ -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 <http://www.gnu.org/licenses/>.
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
}

View File

@ -18,3 +18,5 @@
from .hypervisor_handler import HypervisorHandler from .hypervisor_handler import HypervisorHandler
from .project_handler import ProjectHandler from .project_handler import ProjectHandler
from .version_handler import VersionHandler from .version_handler import VersionHandler
from .vm_handler import VMHandler

View File

@ -44,9 +44,7 @@ class HypervisorHandler:
output=HYPERVISOR_OBJECT_SCHEMA) output=HYPERVISOR_OBJECT_SCHEMA)
def create(request, response): def create(request, response):
hypervisor = Hypervisor(request.json.pop("hypervisor_id"), **request.json) hypervisor = yield from Controller.instance().addHypervisor(**request.json)
Controller.instance().addHypervisor(hypervisor)
response.set_status(201) response.set_status(201)
response.json(hypervisor) response.json(hypervisor)

View File

@ -22,7 +22,6 @@ import asyncio
from ....web.route import Route from ....web.route import Route
from ....schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA from ....schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA
from ....controller import Controller from ....controller import Controller
from ....controller.project import Project
import logging import logging
@ -44,11 +43,11 @@ class ProjectHandler:
def create_project(request, response): def create_project(request, response):
controller = Controller.instance() controller = Controller.instance()
project = Project(name=request.json.get("name"), project = yield from controller.addProject(
path=request.json.get("path"), name=request.json.get("name"),
project_id=request.json.get("project_id"), path=request.json.get("path"),
temporary=request.json.get("temporary", False)) project_id=request.json.get("project_id"),
yield from controller.addProject(project) temporary=request.json.get("temporary", False))
response.set_status(201) response.set_status(201)
response.json(project) response.json(project)

View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

@ -62,3 +62,48 @@ VM_CAPTURE_SCHEMA = {
"additionalProperties": False, "additionalProperties": False,
"required": ["capture_file_name"] "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"]
}

View File

@ -34,35 +34,40 @@ def test_isEnabled(controller):
assert controller.isEnabled() assert controller.isEnabled()
def test_addHypervisor(controller): def test_addHypervisor(controller, async_run):
hypervisor1 = Hypervisor("test1") async_run(controller.addHypervisor("test1"))
controller.addHypervisor(hypervisor1)
assert len(controller.hypervisors) == 1 assert len(controller.hypervisors) == 1
controller.addHypervisor(Hypervisor("test1")) async_run(controller.addHypervisor("test1"))
assert len(controller.hypervisors) == 1 assert len(controller.hypervisors) == 1
controller.addHypervisor(Hypervisor("test2")) async_run(controller.addHypervisor("test2"))
assert len(controller.hypervisors) == 2 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): def test_addProject(controller, async_run):
uuid1 = str(uuid.uuid4()) uuid1 = str(uuid.uuid4())
project1 = Project(project_id=uuid1)
uuid2 = str(uuid.uuid4()) uuid2 = str(uuid.uuid4())
async_run(controller.addProject(project1)) async_run(controller.addProject(project_id=uuid1))
assert len(controller.projects) == 1 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 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 assert len(controller.projects) == 2
def test_removeProject(controller, async_run): def test_removeProject(controller, async_run):
uuid1 = str(uuid.uuid4()) 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 assert len(controller.projects) == 1
controller.removeProject(project1) controller.removeProject(project1)
@ -71,21 +76,19 @@ def test_removeProject(controller, async_run):
def test_addProject_with_hypervisor(controller, async_run): def test_addProject_with_hypervisor(controller, async_run):
uuid1 = str(uuid.uuid4()) uuid1 = str(uuid.uuid4())
project1 = Project(project_id=uuid1)
hypervisor = Hypervisor("test1") hypervisor = Hypervisor("test1")
hypervisor.post = MagicMock() 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) hypervisor.post.assert_called_with("/projects", project1)
def test_getProject(controller, async_run): def test_getProject(controller, async_run):
uuid1 = str(uuid.uuid4()) 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 assert controller.getProject(uuid1) == project
with pytest.raises(aiohttp.web.HTTPNotFound): with pytest.raises(aiohttp.web.HTTPNotFound):
assert controller.getProject("dsdssd") assert controller.getProject("dsdssd")

View File

@ -16,6 +16,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from unittest.mock import MagicMock
from gns3server.controller.project import Project from gns3server.controller.project import Project
@ -31,3 +33,10 @@ def test_affect_uuid():
def test_json(tmpdir): def test_json(tmpdir):
p = Project() p = Project()
assert p.__json__() == {"name": p.name, "project_id": p.id, "temporary": False, "path": None} 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'})

View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (C) 2015 GNS3 Technologies Inc. # Copyright (C) 2015 GNS3 Technologies Inc.
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
@ -42,12 +42,10 @@ def project(http_controller):
def test_create_project_with_path(http_controller, tmpdir): 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"})
response = http_controller.post("/projects", {"name": "test", "path": str(tmpdir), "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"}) assert response.status == 201
assert response.status == 201 assert response.json["name"] == "test"
assert response.json["name"] == "test" assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f"
assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f"
assert mock.called
def test_create_project_without_dir(http_controller): def test_create_project_without_dir(http_controller):

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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"