From bbc1505274da3f86c3c75acb242f4b2dca2404f1 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 29 Aug 2016 15:53:10 +0200 Subject: [PATCH] Return what is supported by a compute node Ref https://github.com/GNS3/gns3-gui/issues/1448 --- gns3server/compute/base_manager.py | 8 ++++ gns3server/compute/builtin/__init__.py | 7 ++++ gns3server/compute/dynamips/__init__.py | 7 ++++ gns3server/controller/__init__.py | 18 +++++---- gns3server/controller/compute.py | 28 ++++++------- .../controller/gns3vm/vmware_gns3_vm.py | 2 +- gns3server/handlers/api/compute/__init__.py | 1 + .../api/compute/capabilities_handler.py | 40 +++++++++++++++++++ gns3server/schemas/capabilities.py | 37 +++++++++++++++++ gns3server/schemas/compute.py | 6 +-- gns3server/schemas/node.py | 36 +++++++++-------- tests/controller/test_compute.py | 19 ++++----- tests/controller/test_controller.py | 8 +++- .../handlers/api/compute/test_capabilities.py | 31 ++++++++++++++ tests/handlers/api/controller/test_compute.py | 30 ++++++++------ 15 files changed, 210 insertions(+), 68 deletions(-) create mode 100644 gns3server/handlers/api/compute/capabilities_handler.py create mode 100644 gns3server/schemas/capabilities.py create mode 100644 tests/handlers/api/compute/test_capabilities.py diff --git a/gns3server/compute/base_manager.py b/gns3server/compute/base_manager.py index bbce4088..392e595d 100644 --- a/gns3server/compute/base_manager.py +++ b/gns3server/compute/base_manager.py @@ -58,6 +58,14 @@ class BaseManager: self._port_manager = None self._config = Config.instance() + @classmethod + def node_types(cls): + """ + :returns: Array of supported node type on this computer + """ + # By default we transform DockerVM => docker but you can override this (see builtins) + return [cls._NODE_CLASS.__name__.rstrip('VM').lower()] + @property def nodes(self): """ diff --git a/gns3server/compute/builtin/__init__.py b/gns3server/compute/builtin/__init__.py index 8123b201..af1019a8 100644 --- a/gns3server/compute/builtin/__init__.py +++ b/gns3server/compute/builtin/__init__.py @@ -34,3 +34,10 @@ class Builtin(BaseManager): def __init__(self): super().__init__() + + @classmethod + def node_types(cls): + """ + :returns: List of node type supported by this class and computer + """ + return ['cloud', 'nat', 'ethernet_hub', 'ethernet_switch'] diff --git a/gns3server/compute/dynamips/__init__.py b/gns3server/compute/dynamips/__init__.py index 1be5ddae..2e9d2d60 100644 --- a/gns3server/compute/dynamips/__init__.py +++ b/gns3server/compute/dynamips/__init__.py @@ -116,6 +116,13 @@ class Dynamips(BaseManager): self._dynamips_path = None self._dynamips_ids = {} + @classmethod + def node_types(cls): + """ + :returns: List of node type supported by this class and computer + """ + return ['dynamips', 'frame_relay_switch', 'atm_switch'] + def get_dynamips_id(self, project_id): """ :param project_id: UUID of the project diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 3046b1d4..01d13499 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -18,6 +18,7 @@ import os import sys import json +import socket import asyncio import aiohttp @@ -64,13 +65,14 @@ class Controller: log.info("Start controller") yield from self.load() server_config = Config.instance().get_section_config("Server") - self._computes["local"] = Compute(compute_id="local", - controller=self, - protocol=server_config.get("protocol", "http"), - host=server_config.get("host", "localhost"), - port=server_config.getint("port", 3080), - user=server_config.get("user", ""), - password=server_config.get("password", "")) + yield from self.add_compute(compute_id="local", + name=socket.gethostname(), + protocol=server_config.get("protocol", "http"), + host=server_config.get("host", "localhost"), + port=server_config.getint("port", 3080), + user=server_config.get("user", ""), + password=server_config.get("password", ""), + force=True) yield from self.gns3vm.auto_start_vm() @asyncio.coroutine @@ -228,8 +230,8 @@ class Controller: compute = Compute(compute_id=compute_id, controller=self, name=name, **kwargs) self._computes[compute.id] = compute self.save() + yield from compute.connect() self.notification.emit("compute.created", compute.__json__()) - return compute else: self.notification.emit("compute.updated", self._computes[compute_id].__json__()) diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index e59c8102..2ef1ff6d 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -90,9 +90,12 @@ class Compute: self._connected = False self._controller = controller self._set_auth(user, password) - self._version = None self._cpu_usage_percent = None self._memory_usage_percent = None + self._capabilities = { + "version": None, + "node_types": [] + } self.name = name # Websocket for notifications self._ws = None @@ -155,13 +158,6 @@ class Compute: yield from self._ws.close() self._ws = None - @property - def version(self): - """ - :returns: Version of compute node (string or None if not connected) - """ - return self._version - @property def name(self): """ @@ -173,8 +169,6 @@ class Compute: def name(self, name): if name is not None: self._name = name - elif self._id == "local": - self._name = socket.gethostname() else: if self._user: user = self._user @@ -284,7 +278,8 @@ class Compute: "user": self._user, "connected": self._connected, "cpu_usage_percent": self._cpu_usage_percent, - "memory_usage_percent": self._memory_usage_percent + "memory_usage_percent": self._memory_usage_percent, + "capabilities": self._capabilities } @asyncio.coroutine @@ -336,24 +331,27 @@ class Compute: @asyncio.coroutine def http_query(self, method, path, data=None, **kwargs): if not self._connected: - yield from self._connect() + yield from self.connect() if not self._connected: raise aiohttp.web.HTTPConflict(text="The server {} is not a GNS3 server".format(self._id)) response = yield from self._run_http_query(method, path, data=data, **kwargs) return response @asyncio.coroutine - def _connect(self): + def connect(self): """ Check if remote server is accessible """ if not self._connected: - response = yield from self._run_http_query("GET", "/version") + try: + response = yield from self._run_http_query("GET", "/capabilities") + except aiohttp.errors.ClientOSError: + return if "version" not in response.json: self._http_session.close() raise aiohttp.web.HTTPConflict(text="The server {} is not a GNS3 server".format(self._id)) - self._version = response.json["version"] + self._capabilities = response.json if parse_version(__version__)[:2] != parse_version(response.json["version"])[:2]: self._http_session.close() raise aiohttp.web.HTTPConflict(text="The server {} versions are not compatible {} != {}".format(self._id, __version__, response.json["version"])) diff --git a/gns3server/controller/gns3vm/vmware_gns3_vm.py b/gns3server/controller/gns3vm/vmware_gns3_vm.py index 2de100c3..8cc17da5 100644 --- a/gns3server/controller/gns3vm/vmware_gns3_vm.py +++ b/gns3server/controller/gns3vm/vmware_gns3_vm.py @@ -136,7 +136,7 @@ class VMwareGNS3VM(BaseGNS3VM): raise GNS3VMError("No VMX path configured, can't stop the VM") try: yield from self._execute("stop", [self._vmx_path, "soft"]) - except VMwareError as e: + except GNS3VMError as e: log.warning("Error when stopping the VM: {}".format(str(e))) log.info("GNS3 VM has been stopped") self.running = False diff --git a/gns3server/handlers/api/compute/__init__.py b/gns3server/handlers/api/compute/__init__.py index 86cefe32..a0ce10b2 100644 --- a/gns3server/handlers/api/compute/__init__.py +++ b/gns3server/handlers/api/compute/__init__.py @@ -18,6 +18,7 @@ import sys import os +from .capabilities_handler import CapabilitiesHandler from .network_handler import NetworkHandler from .project_handler import ProjectHandler from .dynamips_vm_handler import DynamipsVMHandler diff --git a/gns3server/handlers/api/compute/capabilities_handler.py b/gns3server/handlers/api/compute/capabilities_handler.py new file mode 100644 index 00000000..38bf2e99 --- /dev/null +++ b/gns3server/handlers/api/compute/capabilities_handler.py @@ -0,0 +1,40 @@ +# -*- 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 gns3server.web.route import Route +from gns3server.config import Config +from gns3server.schemas.capabilities import CAPABILITIES_SCHEMA +from gns3server.version import __version__ +from gns3server.compute import MODULES +from aiohttp.web import HTTPConflict + + +class CapabilitiesHandler: + @Route.get( + r"/capabilities", + description="Retrieve the capabilities of the server", + output=CAPABILITIES_SCHEMA) + def get(request, response): + + node_types = [] + for module in MODULES: + node_types.extend(module.node_types()) + + response.json({ + "version": __version__, + "node_types": node_types + }) diff --git a/gns3server/schemas/capabilities.py b/gns3server/schemas/capabilities.py new file mode 100644 index 00000000..dddbda0b --- /dev/null +++ b/gns3server/schemas/capabilities.py @@ -0,0 +1,37 @@ +# -*- 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 .node import NODE_TYPE_SCHEMA + + +CAPABILITIES_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Get what a server support", + "type": "object", + "required": ["version", "node_types"], + "properties": { + "version": { + "description": "Version number", + "type": ["string", "null"], + }, + "node_types": { + "type": "array", + "items": NODE_TYPE_SCHEMA + } + }, + "additionalProperties": False +} diff --git a/gns3server/schemas/compute.py b/gns3server/schemas/compute.py index cf0d7ff5..7d0d65ab 100644 --- a/gns3server/schemas/compute.py +++ b/gns3server/schemas/compute.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from .capabilities import CAPABILITIES_SCHEMA COMPUTE_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", @@ -102,10 +103,7 @@ COMPUTE_OBJECT_SCHEMA = { "maximum": 100, "minimum": 0 }, - "version": { - "description": "Version of the GNS3 remote compute server", - "type": ["string", "null"] - } + "capabilities": CAPABILITIES_SCHEMA }, "additionalProperties": False, "required": ["compute_id", "protocol", "host", "port", "name"] diff --git a/gns3server/schemas/node.py b/gns3server/schemas/node.py index cd4a29ec..32b1e657 100644 --- a/gns3server/schemas/node.py +++ b/gns3server/schemas/node.py @@ -17,6 +17,25 @@ from .label import LABEL_OBJECT_SCHEMA +NODE_TYPE_SCHEMA = { + "description": "Type of node", + "enum": [ + "cloud", + "nat", + "ethernet_hub", + "ethernet_switch", + "frame_relay_switch", + "atm_switch", + "docker", + "dynamips", + "vpcs", + "virtualbox", + "vmware", + "iou", + "qemu" + ] +} + NODE_LIST_IMAGES_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "List of binary images", @@ -87,22 +106,7 @@ NODE_OBJECT_SCHEMA = { "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}$" }, - "node_type": { - "description": "Type of node", - "enum": ["cloud", - "nat", - "ethernet_hub", - "ethernet_switch", - "frame_relay_switch", - "atm_switch", - "docker", - "dynamips", - "vpcs", - "virtualbox", - "vmware", - "iou", - "qemu"] - }, + "node_type": NODE_TYPE_SCHEMA, "node_directory": { "description": "Working directory of the node. Read only", "type": ["null", "string"] diff --git a/tests/controller/test_compute.py b/tests/controller/test_compute.py index 7ae60758..349cd77d 100644 --- a/tests/controller/test_compute.py +++ b/tests/controller/test_compute.py @@ -48,9 +48,6 @@ def test_host_ip(controller): def test_name(): c = Compute("my_compute_id", protocol="https", host="example.com", port=84, controller=MagicMock(), name=None) assert c.name == "https://example.com:84" - with patch("gns3server.config.Config.get_section_config", return_value={"local": True}): - c = Compute("local", protocol="https", host="example.com", port=84, controller=MagicMock(), name=None) - assert c.name == socket.gethostname() c = Compute("world", protocol="https", host="example.com", port=84, controller=MagicMock(), name="hello") assert c.name == "hello" c = Compute("world", protocol="https", host="example.com", port=84, controller=MagicMock(), user="azertyuiopqsdfghjklkm") @@ -88,10 +85,10 @@ def test_compute_httpQueryNotConnected(compute, controller, async_run): response.status = 200 with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: async_run(compute.post("/projects", {"a": "b"})) - mock.assert_any_call("GET", "https://example.com:84/v2/compute/version", headers={'content-type': 'application/json'}, data=None, auth=None, chunked=False) + mock.assert_any_call("GET", "https://example.com:84/v2/compute/capabilities", headers={'content-type': 'application/json'}, data=None, auth=None, chunked=False) mock.assert_any_call("POST", "https://example.com:84/v2/compute/projects", data='{"a": "b"}', headers={'content-type': 'application/json'}, auth=None, chunked=False) assert compute._connected - assert compute.version == __version__ + assert compute._capabilities["version"] == __version__ controller.notification.emit.assert_called_with("compute.updated", compute.__json__()) @@ -103,7 +100,7 @@ def test_compute_httpQueryNotConnectedInvalidVersion(compute, async_run): with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: with pytest.raises(aiohttp.web.HTTPConflict): async_run(compute.post("/projects", {"a": "b"})) - mock.assert_any_call("GET", "https://example.com:84/v2/compute/version", headers={'content-type': 'application/json'}, data=None, auth=None, chunked=False) + mock.assert_any_call("GET", "https://example.com:84/v2/compute/capabilities", headers={'content-type': 'application/json'}, data=None, auth=None, chunked=False) def test_compute_httpQueryNotConnectedNonGNS3Server(compute, async_run): @@ -114,7 +111,7 @@ def test_compute_httpQueryNotConnectedNonGNS3Server(compute, async_run): with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: with pytest.raises(aiohttp.web.HTTPConflict): async_run(compute.post("/projects", {"a": "b"})) - mock.assert_any_call("GET", "https://example.com:84/v2/compute/version", headers={'content-type': 'application/json'}, data=None, auth=None, chunked=False) + mock.assert_any_call("GET", "https://example.com:84/v2/compute/capabilities", headers={'content-type': 'application/json'}, data=None, auth=None, chunked=False) def test_compute_httpQueryNotConnectedNonGNS3Server2(compute, async_run): @@ -125,7 +122,7 @@ def test_compute_httpQueryNotConnectedNonGNS3Server2(compute, async_run): with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: with pytest.raises(aiohttp.web.HTTPConflict): async_run(compute.post("/projects", {"a": "b"})) - mock.assert_any_call("GET", "https://example.com:84/v2/compute/version", headers={'content-type': 'application/json'}, data=None, auth=None, chunked=False) + mock.assert_any_call("GET", "https://example.com:84/v2/compute/capabilities", headers={'content-type': 'application/json'}, data=None, auth=None, chunked=False) def test_compute_httpQueryError(compute, async_run): @@ -234,7 +231,11 @@ def test_json(compute): "user": "test", "cpu_usage_percent": None, "memory_usage_percent": None, - "connected": True + "connected": True, + "capabilities": { + "version": None, + "node_types": [] + } } assert compute.__json__(topology_dump=True) == { "compute_id": "my_compute_id", diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index 10684c4c..7109b4db 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -71,7 +71,11 @@ def test_load(controller, controller_config_path, async_run): "user": "admin", "name": "http://admin@localhost:8000", "cpu_usage_percent": None, - "memory_usage_percent": None + "memory_usage_percent": None, + "capabilities": { + "version": None, + "node_types": [] + } } assert controller.gns3vm.settings["vmname"] == "Test VM" @@ -135,7 +139,7 @@ def test_isEnabled(controller): assert controller.is_enabled() -def test_addCompute(controller, controller_config_path, async_run): +def test_add_compute(controller, controller_config_path, async_run): controller._notification = MagicMock() c = async_run(controller.add_compute(compute_id="test1")) controller._notification.emit.assert_called_with("compute.created", c.__json__()) diff --git a/tests/handlers/api/compute/test_capabilities.py b/tests/handlers/api/compute/test_capabilities.py new file mode 100644 index 00000000..c76c0cc1 --- /dev/null +++ b/tests/handlers/api/compute/test_capabilities.py @@ -0,0 +1,31 @@ +# -*- 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 /version endpoint +It's also used for unittest the HTTP implementation. +""" + +from gns3server.config import Config + +from gns3server.version import __version__ + + +def test_get(http_compute): + response = http_compute.get('/capabilities', example=True) + assert response.status == 200 + assert response.json == {'node_types': ['cloud', 'nat', 'ethernet_hub', 'ethernet_switch', 'vpcs', 'virtualbox', 'dynamips', 'frame_relay_switch', 'atm_switch', 'qemu', 'vmware', 'docker', 'iou'], 'version': __version__} diff --git a/tests/handlers/api/controller/test_compute.py b/tests/handlers/api/controller/test_compute.py index bc5b1da1..029a4dbb 100644 --- a/tests/handlers/api/controller/test_compute.py +++ b/tests/handlers/api/controller/test_compute.py @@ -23,7 +23,7 @@ def test_compute_create_without_id(http_controller, controller): params = { "protocol": "http", - "host": "example.com", + "host": "localhost", "port": 84, "user": "julien", "password": "secure" @@ -36,7 +36,7 @@ def test_compute_create_without_id(http_controller, controller): assert "password" not in response.json assert len(controller.computes) == 1 - assert controller.computes[response.json["compute_id"]].host == "example.com" + assert controller.computes[response.json["compute_id"]].host == "localhost" def test_compute_create_with_id(http_controller, controller): @@ -44,7 +44,7 @@ def test_compute_create_with_id(http_controller, controller): params = { "compute_id": "my_compute_id", "protocol": "http", - "host": "example.com", + "host": "localhost", "port": 84, "user": "julien", "password": "secure" @@ -56,7 +56,7 @@ def test_compute_create_with_id(http_controller, controller): assert "password" not in response.json assert len(controller.computes) == 1 - assert controller.computes["my_compute_id"].host == "example.com" + assert controller.computes["my_compute_id"].host == "localhost" def test_compute_get(http_controller, controller): @@ -64,7 +64,7 @@ def test_compute_get(http_controller, controller): params = { "compute_id": "my_compute_id", "protocol": "http", - "host": "example.com", + "host": "localhost", "port": 84, "user": "julien", "password": "secure" @@ -82,7 +82,7 @@ def test_compute_update(http_controller, controller): params = { "compute_id": "my_compute_id", "protocol": "http", - "host": "example.com", + "host": "localhost", "port": 84, "user": "julien", "password": "secure" @@ -106,7 +106,7 @@ def test_compute_list(http_controller, controller): params = { "compute_id": "my_compute_id", "protocol": "http", - "host": "example.com", + "host": "localhost", "port": 84, "user": "julien", "password": "secure", @@ -124,13 +124,17 @@ def test_compute_list(http_controller, controller): assert compute == { 'compute_id': 'my_compute_id', 'connected': False, - 'host': 'example.com', + 'host': 'localhost', 'port': 84, 'protocol': 'http', 'user': 'julien', 'name': 'My super server', 'cpu_usage_percent': None, - 'memory_usage_percent': None + 'memory_usage_percent': None, + 'capabilities': { + 'version': None, + 'node_types': [] + } } @@ -139,7 +143,7 @@ def test_compute_delete(http_controller, controller): params = { "compute_id": "my_compute_id", "protocol": "http", - "host": "example.com", + "host": "localhost", "port": 84, "user": "julien", "password": "secure" @@ -162,7 +166,7 @@ def test_compute_list_images(http_controller, controller): params = { "compute_id": "my_compute", "protocol": "http", - "host": "example.com", + "host": "localhost", "port": 84, "user": "julien", "password": "secure" @@ -181,7 +185,7 @@ def test_compute_list_vms(http_controller, controller): params = { "compute_id": "my_compute", "protocol": "http", - "host": "example.com", + "host": "localhost", "port": 84, "user": "julien", "password": "secure" @@ -200,7 +204,7 @@ def test_compute_create_img(http_controller, controller): params = { "compute_id": "my_compute", "protocol": "http", - "host": "example.com", + "host": "localhost", "port": 84, "user": "julien", "password": "secure"