From aad69e9650142bb62985ab25d3603d38ee3eb484 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 3 Mar 2016 16:02:27 +0100 Subject: [PATCH] Create a /server API for register servers This allow to push to the controller information about the connection to a server. --- gns3server/controller/__init__.py | 16 ++++ gns3server/controller/server.py | 63 +++++++++++++++ gns3server/handlers/api/server_handler.py | 33 ++++++-- gns3server/run.py | 4 +- gns3server/schemas/server.py | 89 +++++++++++++++++++++ gns3server/{server.py => web/web_server.py} | 18 ++--- tests/conftest.py | 11 ++- tests/controller/test_controller.py | 18 +++-- tests/controller/test_server.py | 48 +++++++++++ tests/handlers/api/test_server.py | 36 +++++++++ 10 files changed, 311 insertions(+), 25 deletions(-) create mode 100644 gns3server/controller/server.py create mode 100644 gns3server/schemas/server.py rename gns3server/{server.py => web/web_server.py} (96%) create mode 100644 tests/controller/test_server.py create mode 100644 tests/handlers/api/test_server.py diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 2c81fd95..c7b6dfcc 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -22,6 +22,9 @@ from ..config import Config class Controller: """The controller manage multiple gns3 servers""" + def __init__(self): + self._servers = {} + def isEnabled(self): """ :returns: True if current instance is the controller @@ -29,6 +32,19 @@ class Controller: """ return Config.instance().get_section_config("Server").getboolean("controller") + def addServer(self, server): + """ + Add a server to the dictionnary of servers controlled by GNS3 + """ + self._servers[server.id] = server + + @property + def servers(self): + """ + :returns: The dictionnary of servers managed by GNS3 + """ + return self._servers + @staticmethod def instance(): """ diff --git a/gns3server/controller/server.py b/gns3server/controller/server.py new file mode 100644 index 00000000..fef33c3d --- /dev/null +++ b/gns3server/controller/server.py @@ -0,0 +1,63 @@ +#!/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 . + + +class Server: + """ + A GNS3 server. + """ + + def __init__(self, server_id, protocol="http", host="localhost", port=8000, user=None, password=None): + self._id = server_id + self._protocol = protocol + self._host = host + self._port = port + self._user = user + self._password = password + self._connected = False + # The remote server version + self._version = None + + @property + def id(self): + """ + :returns: Server identifier (string) + """ + return self._id + + @property + def host(self): + """ + :returns: Server host (string) + """ + return self._host + + def __json__(self): + return { + "server_id": self._id, + "protocol": self._protocol, + "host": self._host, + "port": self._port, + "user": self._user, + "connected": self._connected, + "version": self._version + } + + def __eq__(self, other): + if not isinstance(other, Server): + return False + return other._id == self._id diff --git a/gns3server/handlers/api/server_handler.py b/gns3server/handlers/api/server_handler.py index 105129f2..019a841e 100644 --- a/gns3server/handlers/api/server_handler.py +++ b/gns3server/handlers/api/server_handler.py @@ -15,18 +15,41 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import asyncio +from aiohttp.web import HTTPForbidden + from ...web.route import Route from ...config import Config from ...modules.project_manager import ProjectManager -from aiohttp.web import HTTPForbidden +from ...schemas.server import SERVER_CREATE_SCHEMA, SERVER_OBJECT_SCHEMA +from ...controller import Controller +from ...controller.server import Server + -import asyncio import logging - log = logging.getLogger(__name__) class ServerHandler: + """API entry points for server management.""" + + @classmethod + @Route.post( + r"/servers", + description="Register a server", + status_codes={ + 201: "Server added" + }, + controller=True, + input=SERVER_CREATE_SCHEMA, + output=SERVER_OBJECT_SCHEMA) + def create(request, response): + + server = Server(request.json.pop("server_id"), **request.json) + Controller.instance().addServer(server) + + response.set_status(201) + response.json(server) @classmethod @Route.post( @@ -60,7 +83,7 @@ class ServerHandler: continue # then shutdown the server itself - from gns3server.server import Server - server = Server.instance() + from gns3server.web.web_server import WebServer + server = WebServer.instance() asyncio.async(server.shutdown_server()) response.set_status(201) diff --git a/gns3server/run.py b/gns3server/run.py index 4e86e356..1c140b01 100644 --- a/gns3server/run.py +++ b/gns3server/run.py @@ -28,7 +28,7 @@ import locale import argparse import asyncio -from gns3server.server import Server +from gns3server.web.web_server import WebServer from gns3server.web.logger import init_logger from gns3server.version import __version__ from gns3server.config import Config @@ -233,7 +233,7 @@ def run(): host = server_config["host"] port = int(server_config["port"]) - server = Server.instance(host, port) + server = WebServer.instance(host, port) try: server.run() except OSError as e: diff --git a/gns3server/schemas/server.py b/gns3server/schemas/server.py new file mode 100644 index 00000000..8d3c88be --- /dev/null +++ b/gns3server/schemas/server.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 . + + +SERVER_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to register a GNS3 server instance", + "type": "object", + "properties": { + "server_id": { + "description": "Server identifier", + "type": "string" + }, + "protocol": { + "description": "Server protocol", + "enum": ["http", "https"] + }, + "host": { + "description": "Server host", + "type": "string" + }, + "port": { + "description": "Server port", + "type": "integer" + }, + "user": { + "description": "User for auth", + "type": "string" + }, + "password": { + "description": "Password for auth", + "type": "string" + } + }, + "additionalProperties": False, + "required": ["server_id", "protocol", "host", "port"] +} + +SERVER_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to a GNS3 server object instance", + "type": "object", + "properties": { + "server_id": { + "description": "Server identifier", + "type": "string" + }, + "protocol": { + "description": "Server protocol", + "enum": ["http", "https"] + }, + "host": { + "description": "Server host", + "type": "string" + }, + "port": { + "description": "Server port", + "type": "integer" + }, + "user": { + "description": "User for auth", + "type": "string" + }, + "connected": { + "description": "True if controller is connected to the server", + "type": "boolean" + }, + "version": { + "description": "Version of the GNS3 remote server", + "type": ["string", "null"] + } + }, + "additionalProperties": False, + "required": ["server_id", "protocol", "host", "port"] +} diff --git a/gns3server/server.py b/gns3server/web/web_server.py similarity index 96% rename from gns3server/server.py rename to gns3server/web/web_server.py index c7474c81..88188f1c 100644 --- a/gns3server/server.py +++ b/gns3server/web/web_server.py @@ -29,11 +29,11 @@ import types import time import atexit -from .web.route import Route -from .web.request_handler import RequestHandler -from .config import Config -from .modules import MODULES -from .modules.port_manager import PortManager +from .route import Route +from .request_handler import RequestHandler +from ..config import Config +from ..modules import MODULES +from ..modules.port_manager import PortManager # do not delete this import import gns3server.handlers @@ -42,7 +42,7 @@ import logging log = logging.getLogger(__name__) -class Server: +class WebServer: def __init__(self, host, port): @@ -61,11 +61,11 @@ class Server: :returns: instance of Server """ - if not hasattr(Server, "_instance") or Server._instance is None: + if not hasattr(WebServer, "_instance") or WebServer._instance is None: assert host is not None assert port is not None - Server._instance = Server(host, port) - return Server._instance + WebServer._instance = WebServer(host, port) + return WebServer._instance @asyncio.coroutine def _run_application(self, handler, ssl_context=None): diff --git a/tests/conftest.py b/tests/conftest.py index c8bf4c04..ac8d1ec6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,6 +40,7 @@ from gns3server.handlers import * from gns3server.modules import MODULES from gns3server.modules.port_manager import PortManager from gns3server.modules.project_manager import ProjectManager +from gns3server.controller import Controller from tests.handlers.api.base import Query @@ -136,8 +137,14 @@ def ethernet_device(): return sorted(psutil.net_if_addrs().keys())[0] +@pytest.fixture +def controller(): + Controller._instance = None + return Controller.instance() + + @pytest.yield_fixture(autouse=True) -def run_around_tests(monkeypatch, port_manager): +def run_around_tests(monkeypatch, port_manager, controller): """ This setup a temporay project file environnement around tests """ @@ -151,7 +158,7 @@ def run_around_tests(monkeypatch, port_manager): config.set("Server", "project_directory", os.path.join(tmppath, 'projects')) config.set("Server", "images_path", os.path.join(tmppath, 'images')) config.set("Server", "auth", False) - config.set("Server", "controller", False) + config.set("Server", "controller", True) # Prevent executions of the VM if we forgot to mock something config.set("VirtualBox", "vboxmanage_path", tmppath) diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index 5ef9e375..76accce1 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -18,19 +18,23 @@ import pytest from gns3server.controller import Controller +from gns3server.controller.server import Server from gns3server.config import Config -@pytest.fixture -def controller(): - Controller._instance = None - return Controller.instance() - - - def test_isEnabled(controller): Config.instance().set("Server", "controller", False) assert not controller.isEnabled() Config.instance().set("Server", "controller", True) assert controller.isEnabled() + +def test_addServer(controller): + server1 = Server("test1") + + controller.addServer(server1) + assert len(controller.servers) == 1 + controller.addServer(Server("test1")) + assert len(controller.servers) == 1 + controller.addServer(Server("test2")) + assert len(controller.servers) == 2 diff --git a/tests/controller/test_server.py b/tests/controller/test_server.py new file mode 100644 index 00000000..ad2093f8 --- /dev/null +++ b/tests/controller/test_server.py @@ -0,0 +1,48 @@ +#!/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.server import Server + + +@pytest.fixture +def server(): + return Server("my_server_id", protocol="https", host="example.com", port=84, user="test", password="secure") + + +def test_init(server): + assert server.id == "my_server_id" + + +def test_json(server): + assert server.__json__() == { + "server_id": "my_server_id", + "protocol": "https", + "host": "example.com", + "port": 84, + "user": "test", + "connected": False, + "version": None + } + + +def test__eq__(server): + assert server != 1 + assert server == server + assert server == Server("my_server_id") + assert server != Server("test") diff --git a/tests/handlers/api/test_server.py b/tests/handlers/api/test_server.py new file mode 100644 index 00000000..845920e7 --- /dev/null +++ b/tests/handlers/api/test_server.py @@ -0,0 +1,36 @@ +#!/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 . + + +def test_server_create(server, controller): + + params = { + "server_id": "my_server_id", + "protocol": "http", + "host": "example.com", + "port": 84, + "user": "julien", + "password": "secure" + } + response = server.post("/servers", params) + assert response.status == 201 + assert response.route == "/servers" + assert response.json["user"] == "julien" + assert "password" not in response.json + + assert len(controller.servers) == 1 + assert controller.servers["my_server_id"].host == "example.com"