diff --git a/gns3server/config.py b/gns3server/config.py index 657ac67b..7a91d530 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -157,7 +157,7 @@ class Config(object): @staticmethod def instance(files=None): """ - Singleton to return only on instance of Config. + Singleton to return only one instance of Config. :params files: Array of configuration files (optional) :returns: instance of Config diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 325e8ba4..55dc3c86 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -26,6 +26,7 @@ from gns3server.handlers.api.qemu_handler import QEMUHandler from gns3server.handlers.api.virtualbox_handler import VirtualBoxHandler from gns3server.handlers.api.vpcs_handler import VPCSHandler from gns3server.handlers.api.config_handler import ConfigHandler +from gns3server.handlers.api.server_handler import ServerHandler from gns3server.handlers.upload_handler import UploadHandler if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test"): diff --git a/gns3server/handlers/api/server_handler.py b/gns3server/handlers/api/server_handler.py new file mode 100644 index 00000000..65e8892f --- /dev/null +++ b/gns3server/handlers/api/server_handler.py @@ -0,0 +1,42 @@ +# -*- 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 ...web.route import Route +from ...config import Config +from aiohttp.web import HTTPForbidden +import asyncio + +class ServerHandler: + + @classmethod + @Route.post( + r"/server/shutdown", + description="Shutdown the local server", + status_codes={ + 201: "Server is shutting down", + 403: "Server shutdown refused" + }) + def shutdown(request, response): + + config = Config.instance() + if config.get_section_config("Server").getboolean("local", False) is False: + raise HTTPForbidden(text="You can only stop a local server") + + from gns3server.server import Server + server = Server.instance() + asyncio.async(server.shutdown_server()) + response.set_status(201) diff --git a/gns3server/main.py b/gns3server/main.py index 0d846445..0ef24789 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -173,7 +173,7 @@ def main(): CrashReport.instance() host = server_config["host"] port = int(server_config["port"]) - server = Server(host, port) + server = Server.instance(host, port) try: server.run() except Exception as e: diff --git a/gns3server/server.py b/gns3server/server.py index be677676..aae6ca4a 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -48,9 +48,22 @@ class Server: self._host = host self._port = port self._loop = None + self._handler = None self._start_time = time.time() self._port_manager = PortManager(host) + @staticmethod + def instance(host=None, port=None): + """ + Singleton to return only one instance of Server. + + :returns: instance of Server + """ + + if not hasattr(Server, "_instance") or Server._instance is None: + Server._instance = Server(host, port) + return Server._instance + @asyncio.coroutine def _run_application(self, handler, ssl_context=None): @@ -63,11 +76,14 @@ class Server: return server @asyncio.coroutine - def _stop_application(self): + def shutdown_server(self): """ - Cleanup the modules (shutdown running emulators etc.) + Cleanly shutdown the server. """ + if self._handler: + yield from self._handler.finish_connections() + for module in MODULES: log.debug("Unloading module {}".format(module.__name__)) m = module.instance() @@ -81,13 +97,12 @@ class Server: self._loop.stop() - def _signal_handling(self, handler): + def _signal_handling(self): @asyncio.coroutine def signal_handler(signame): log.warning("Server has got signal {}, exiting...".format(signame)) - yield from handler.finish_connections() - yield from self._stop_application() + yield from self.shutdown_server() signals = ["SIGTERM", "SIGINT"] if sys.platform.startswith("win"): @@ -103,14 +118,13 @@ class Server: else: self._loop.add_signal_handler(getattr(signal, signal_name), callback) - def _reload_hook(self, handler): + def _reload_hook(self): @asyncio.coroutine def reload(): log.info("Reloading") - yield from handler.finish_connections() - yield from self._stop_application() + yield from self.shutdown_server() os.execv(sys.executable, [sys.executable] + sys.argv) # code extracted from tornado @@ -130,7 +144,7 @@ class Server: if modified > self._start_time: log.debug("File {} has been modified".format(path)) asyncio.async(reload()) - self._loop.call_later(1, self._reload_hook, handler) + self._loop.call_later(1, self._reload_hook) def _create_ssl_context(self, server_config): @@ -196,13 +210,13 @@ class Server: m.port_manager = self._port_manager log.info("Starting server on {}:{}".format(self._host, self._port)) - handler = app.make_handler(handler=RequestHandler) - self._loop.run_until_complete(self._run_application(handler, ssl_context)) - self._signal_handling(handler) + self._handler = app.make_handler(handler=RequestHandler) + self._loop.run_until_complete(self._run_application(self._handler, ssl_context)) + self._signal_handling() if server_config.getboolean("live"): log.info("Code live reload is enabled, watching for file changes") - self._loop.call_later(1, self._reload_hook, handler) + self._loop.call_later(1, self._reload_hook) if server_config.getboolean("shell"): asyncio.async(self.start_shell())