diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt index 08616805..b0d5e689 100644 --- a/docs/api/examples/post_vpcs.txt +++ b/docs/api/examples/post_vpcs.txt @@ -8,7 +8,7 @@ POST /vpcs HTTP/1.1 HTTP/1.1 200 CONNECTION: close -CONTENT-LENGTH: 67 +CONTENT-LENGTH: 66 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 @@ -17,5 +17,5 @@ X-ROUTE: /vpcs { "console": 4242, "name": "PC TEST 1", - "vpcs_id": 42 + "vpcs_id": 1 } diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 74d44fc1..abc2cd15 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -16,12 +16,10 @@ # along with this program. If not, see . from ..web.route import Route -from ..modules.vpcs import VPCS - -# schemas from ..schemas.vpcs import VPCS_CREATE_SCHEMA from ..schemas.vpcs import VPCS_OBJECT_SCHEMA from ..schemas.vpcs import VPCS_ADD_NIO_SCHEMA +from ..modules import VPCS class VPCSHandler(object): @@ -40,9 +38,9 @@ class VPCSHandler(object): output=VPCS_OBJECT_SCHEMA) def create(request, response): vpcs = VPCS.instance() - i = yield from vpcs.create(request.json) - response.json({'name': request.json['name'], - "vpcs_id": i, + vm = yield from vpcs.create_vm(request.json['name']) + response.json({'name': vm.name, + "vpcs_id": vm.id, "console": 4242}) @classmethod diff --git a/gns3server/modules/base.py b/gns3server/modules/base.py deleted file mode 100644 index 38761bf3..00000000 --- a/gns3server/modules/base.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- 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 . - -import asyncio - - -#TODO: make this more generic (not json but *args?) -class BaseModule(object): - _instance = None - - @classmethod - def instance(cls): - if cls._instance is None: - cls._instance = cls() - asyncio.async(cls._instance.run()) - return cls._instance - - def __init__(self): - self._queue = asyncio.Queue() - - @classmethod - def destroy(cls): - future = asyncio.Future() - cls._instance._queue.put_nowait((future, None, )) - yield from asyncio.wait([future]) - cls._instance = None - - @asyncio.coroutine - def put(self, json): - - future = asyncio.Future() - self._queue.put_nowait((future, json, )) - yield from asyncio.wait([future]) - return future.result() - - @asyncio.coroutine - def run(self): - - while True: - future, json = yield from self._queue.get() - if json is None: - future.set_result(True) - break - try: - result = yield from self.process(json) - future.set_result(result) - except Exception as e: - future.set_exception(e) - - @asyncio.coroutine - def process(self, json): - raise NotImplementedError diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py new file mode 100644 index 00000000..5e618059 --- /dev/null +++ b/gns3server/modules/base_vm.py @@ -0,0 +1,99 @@ +# -*- 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 . + + +import asyncio +from .vm_error import VMError + + +class BaseVM: + + def __init__(self, name, identifier): + self._queue = asyncio.Queue() + self._name = name + self._id = identifier + self._created = asyncio.Future() + self._worker = asyncio.async(self._run()) + + @property + def id(self): + """ + Returns the unique ID for this VM. + + :returns: id (integer) + """ + + return self._id + + @property + def name(self): + """ + Returns the name for this VM. + + :returns: name (string) + """ + + return self._name + + @asyncio.coroutine + def _execute(self, subcommand, args): + """Called when we receive an event""" + raise NotImplementedError + + @asyncio.coroutine + def _create(self): + """Called when the run loop start""" + raise NotImplementedError + + @asyncio.coroutine + def _run(self, timeout=60): + + try: + yield from self._create() + self._created.set_result(True) + except VMError as e: + self._created.set_exception(e) + return + + while True: + future, subcommand, args = yield from self._queue.get() + try: + try: + yield from asyncio.wait_for(self._execute(subcommand, args), timeout=timeout) + except asyncio.TimeoutError: + raise VMError("{} has timed out after {} seconds!".format(subcommand, timeout)) + future.set_result(True) + except Exception as e: + future.set_exception(e) + + def wait_for_creation(self): + return self._created + + def put(self, *args): + """ + Add to the processing queue of the VM + + :returns: future + """ + + future = asyncio.Future() + try: + args.insert(0, future) + self._queue.put_nowait(args) + except asyncio.qeues.QueueFull: + raise VMError("Queue is full") + return future diff --git a/gns3server/modules/vm_error.py b/gns3server/modules/vm_error.py new file mode 100644 index 00000000..d7b71e14 --- /dev/null +++ b/gns3server/modules/vm_error.py @@ -0,0 +1,20 @@ +# -*- 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 . + + +class VMError(Exception): + pass diff --git a/gns3server/modules/vm_manager.py b/gns3server/modules/vm_manager.py new file mode 100644 index 00000000..7065a084 --- /dev/null +++ b/gns3server/modules/vm_manager.py @@ -0,0 +1,89 @@ +# -*- 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 . + + +import asyncio +import aiohttp + +from .vm_error import VMError + + +class VMManager: + """ + Base class for all VMManager. + Responsible of management of a VM pool + """ + + def __init__(self): + self._vms = {} + + @classmethod + def instance(cls): + """ + Singleton to return only one instance of Manager. + + :returns: instance of Manager + """ + + if not hasattr(cls, "_instance"): + cls._instance = cls() + return cls._instance + + @classmethod + @asyncio.coroutine + def destroy(cls): + cls._instance = None + + def _get_vm_instance(self, vm_id): + """ + Returns a VM instance. + + :param vm_id: VM identifier + + :returns: VM instance + """ + + if vm_id not in self._vms: + raise aiohttp.web.HTTPNotFound(text="ID {} doesn't exist".format(vm_id)) + return self._vms[vm_id] + + @asyncio.coroutine + def create_vm(self, vmname, identifier=None): + if not identifier: + for i in range(1, 1024): + if i not in self._vms: + identifier = i + break + if identifier == 0: + raise VMError("Maximum number of VM instances reached") + else: + if identifier in self._vms: + raise VMError("VM identifier {} is already used by another VM instance".format(identifier)) + vm = self._VM_CLASS(vmname, identifier) + yield from vm.wait_for_creation() + self._vms[vm.id] = vm + return vm + + @asyncio.coroutine + def start_vm(self, vm_id): + vm = self._get_vm_instance(vm_id) + yield from vm.start() + + @asyncio.coroutine + def stop_vm(self, vm_id): + vm = self._get_vm_instance(vm_id) + yield from vm.stop() diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index d544365c..618124d1 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -19,19 +19,12 @@ VPCS server module. """ -import asyncio - -from ..base import BaseModule +from ..vm_manager import VMManager +from .vpcs_device import VPCSDevice -class VPCS(BaseModule): +class VPCS(VMManager): + _VM_CLASS = VPCSDevice - @asyncio.coroutine - def process(self, json): - yield from asyncio.sleep(1) - return 42 - - @asyncio.coroutine - def create(self, json): - i = yield from self.put(json) - return i + def create_vm(self, name): + return super().create_vm(name) diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py new file mode 100644 index 00000000..734344db --- /dev/null +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -0,0 +1,23 @@ +# -*- 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 ..base_vm import BaseVM + + +class VPCSDevice(BaseVM): + pass diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index fb2b3c9a..d5c441ca 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -20,16 +20,8 @@ from tests.utils import asyncio_patch from gns3server import modules -def test_vpcs_create(server): - response = server.post('/vpcs', {'name': 'PC TEST 1'}, example=True) - assert response.status == 200 - assert response.route == '/vpcs' - assert response.json['name'] == 'PC TEST 1' - assert response.json['vpcs_id'] == 42 - - -@asyncio_patch('demoserver.modules.VPCS.create', return_value=84) -def test_vpcs_mock(server, mock): +@asyncio_patch('gns3server.modules.VPCS.create_vm', return_value=84) +def test_vpcs_create(server, mock): response = server.post('/vpcs', {'name': 'PC TEST 1'}, example=False) assert response.status == 200 assert response.route == '/vpcs'