diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py new file mode 100644 index 00000000..37ea0ad9 --- /dev/null +++ b/gns3server/controller/link.py @@ -0,0 +1,51 @@ +#!/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 uuid +import asyncio + + +class Link: + def __init__(self): + self._id = str(uuid.uuid4()) + self._vms = [] + + @asyncio.coroutine + def addVM(self, vm, adapter_number, port_number): + """ + Add a VM to the link + """ + self._vms.append({ + "vm": vm, + "adapter_number": adapter_number, + "port_number": port_number + }) + + @property + def id(self): + return self._id + + def __json__(self): + res = [] + for side in self._vms: + res.append({ + "vm_id": side["vm"].id, + "adapter_number": side["adapter_number"], + "port_number": side["port_number"] + }) + return {"vms": res, "link_id": self._id} + diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 49832be1..71f74e72 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -16,9 +16,11 @@ # along with this program. If not, see . import asyncio +import aiohttp from uuid import UUID, uuid4 from .vm import VM +from .link import Link class Project: @@ -45,6 +47,7 @@ class Project: self._temporary = temporary self._hypervisors = set() self._vms = {} + self._links = {} @property def name(self): @@ -81,6 +84,33 @@ class Project: return vm return self._vms[vm_id] + def getVM(self, vm_id): + """ + Return the VM or raise a 404 if the VM is unknown + """ + try: + return self._vms[vm_id] + except KeyError: + raise aiohttp.web.HTTPNotFound(text="VM ID {} doesn't exist".format(vm_id)) + + @asyncio.coroutine + def addLink(self): + """ + Create a link. By default the link is empty + """ + link = Link() + self._links[link.id] = link + return link + + def getLink(self, link_id): + """ + Return the Link or raise a 404 if the VM is unknown + """ + try: + return self._links[link_id] + except KeyError: + raise aiohttp.web.HTTPNotFound(text="Link ID {} doesn't exist".format(link_id)) + @asyncio.coroutine def close(self): for hypervisor in self._hypervisors: diff --git a/gns3server/handlers/api/controller/__init__.py b/gns3server/handlers/api/controller/__init__.py index 5eac84b5..9ddd4f7b 100644 --- a/gns3server/handlers/api/controller/__init__.py +++ b/gns3server/handlers/api/controller/__init__.py @@ -19,3 +19,4 @@ from .hypervisor_handler import HypervisorHandler from .project_handler import ProjectHandler from .version_handler import VersionHandler from .vm_handler import VMHandler +from .link_handler import LinkHandler diff --git a/gns3server/handlers/api/controller/link_handler.py b/gns3server/handlers/api/controller/link_handler.py new file mode 100644 index 00000000..e22ad677 --- /dev/null +++ b/gns3server/handlers/api/controller/link_handler.py @@ -0,0 +1,52 @@ +# -*- 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.link import LINK_OBJECT_SCHEMA +from ....controller.project import Project +from ....controller import Controller + + +class LinkHandler: + """ + API entry point for Link + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/links", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Link created", + 400: "Invalid request" + }, + description="Create a new link instance", + input=LINK_OBJECT_SCHEMA, + output=LINK_OBJECT_SCHEMA) + def create(request, response): + + controller = Controller.instance() + project = controller.getProject(request.match_info["project_id"]) + link = yield from project.addLink() + for vm in request.json["vms"]: + yield from link.addVM(project.getVM(vm["vm_id"]), + vm["adapter_number"], + vm["port_number"]) + response.set_status(201) + response.json(link) diff --git a/gns3server/schemas/link.py b/gns3server/schemas/link.py new file mode 100644 index 00000000..cd71fb4a --- /dev/null +++ b/gns3server/schemas/link.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# +# 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 . + + +LINK_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "A link object", + "type": "object", + "properties": { + "link_id": { + "description": "Link 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}$" + }, + "vms": { + "description": "List of the VMS", + "type": "array", + "items": { + "type": "object", + "properties": { + "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}$" + }, + "adapter_number": { + "description": "Adapter number", + "type": "integer" + }, + "port_number": { + "description": "Port number", + "type": "integer" + } + }, + "required": ["vm_id", "adapter_number", "port_number"], + "additionalProperties": False + } + } + }, + "required": ["vms"], + "additionalProperties": False +} diff --git a/tests/controller/test_link.py b/tests/controller/test_link.py new file mode 100644 index 00000000..9cdcfc75 --- /dev/null +++ b/tests/controller/test_link.py @@ -0,0 +1,72 @@ +#!/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 + +from gns3server.controller.link import Link +from gns3server.controller.vm import VM +from gns3server.controller.hypervisor import Hypervisor +from gns3server.controller.project import Project + + +@pytest.fixture +def project(): + return Project() + + +@pytest.fixture +def hypervisor(): + return Hypervisor("example.com") + + +def test_addVM(async_run, project, hypervisor): + vm1 = VM(project, hypervisor) + + link = Link() + async_run(link.addVM(vm1, 0, 4)) + assert link._vms == [ + { + "vm": vm1, + "adapter_number": 0, + "port_number": 4 + } + ] + + +def test_json(async_run, project, hypervisor): + vm1 = VM(project, hypervisor) + vm2 = VM(project, hypervisor) + + link = Link() + async_run(link.addVM(vm1, 0, 4)) + async_run(link.addVM(vm2, 1, 3)) + assert link.__json__() == { + "link_id": link.id, + "vms": [ + { + "vm_id": vm1.id, + "adapter_number": 0, + "port_number": 4 + }, + { + "vm_id": vm2.id, + "adapter_number": 1, + "port_number": 3 + } + ] + } + diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 5d602f92..10adb62e 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 . +import pytest +import aiohttp from unittest.mock import MagicMock @@ -45,3 +47,34 @@ def test_addVM(async_run): 'console_type': 'telnet', 'startup_config': 'test.cfg', 'name': 'test'}) + + +def test_getVM(async_run): + hypervisor = MagicMock() + project = Project() + vm = async_run(project.addVM(hypervisor, None, name="test", vm_type="vpcs", properties={"startup_config": "test.cfg"})) + assert project.getVM(vm.id) == vm + + with pytest.raises(aiohttp.web_exceptions.HTTPNotFound): + project.getVM("test") + + +def test_addLink(async_run): + hypervisor = MagicMock() + project = Project() + vm1 = async_run(project.addVM(hypervisor, None, name="test1", vm_type="vpcs", properties={"startup_config": "test.cfg"})) + vm2 = async_run(project.addVM(hypervisor, None, name="test2", vm_type="vpcs", properties={"startup_config": "test.cfg"})) + link = async_run(project.addLink()) + async_run(link.addVM(vm1, 3, 1)) + async_run(link.addVM(vm2, 4, 2)) + assert len(link._vms) == 2 + + +def test_getLink(async_run): + hypervisor = MagicMock() + project = Project() + link = async_run(project.addLink()) + assert project.getLink(link.id) == link + + with pytest.raises(aiohttp.web_exceptions.HTTPNotFound): + project.getLink("test") diff --git a/tests/handlers/api/controller/test_link.py b/tests/handlers/api/controller/test_link.py new file mode 100644 index 00000000..343a32de --- /dev/null +++ b/tests/handlers/api/controller/test_link.py @@ -0,0 +1,70 @@ +# -*- 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 +from gns3server.controller.vm import VM + + +@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_link(http_controller, tmpdir, project, hypervisor, async_run): + vm1 = async_run(project.addVM(hypervisor, None)) + vm2 = async_run(project.addVM(hypervisor, None)) + + response = http_controller.post("/projects/{}/links".format(project.id), { + "vms": [ + { + "vm_id": vm1.id, + "adapter_number": 0, + "port_number": 3 + }, + { + "vm_id": vm2.id, + "adapter_number": 2, + "port_number": 4 + } + ] + }, example=True) + assert response.status == 201 + assert response.json["link_id"] is not None + assert len(response.json["vms"]) == 2