mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-04 14:31:13 +02:00
New base server.
This commit is contained in:
1 parent
5e665cc67a
commit
61344a1669
51 files changed
+2632
-1835
No files matched your search
+103
-275
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2013 GNS3 Technologies Inc.
|
||||
# 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
|
||||
@@ -19,51 +19,36 @@
|
||||
Set up and run the server.
|
||||
"""
|
||||
|
||||
import zmq
|
||||
from zmq.eventloop import ioloop, zmqstream
|
||||
ioloop.install()
|
||||
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import sys
|
||||
import signal
|
||||
import errno
|
||||
import socket
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
import tornado.autoreload
|
||||
import pkg_resources
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import ipaddress
|
||||
import base64
|
||||
import uuid
|
||||
import functools
|
||||
import types
|
||||
import time
|
||||
|
||||
from pkg_resources import parse_version
|
||||
from .web.route import Route
|
||||
from .config import Config
|
||||
from .handlers.jsonrpc_websocket import JSONRPCWebSocket
|
||||
from .handlers.version_handler import VersionHandler
|
||||
from .handlers.file_upload_handler import FileUploadHandler
|
||||
from .handlers.auth_handler import LoginHandler
|
||||
from .builtins.server_version import server_version
|
||||
from .builtins.interfaces import interfaces
|
||||
from .modules import MODULES
|
||||
|
||||
#FIXME: have something generic to automatically import handlers so the routes can be found
|
||||
from .handlers.version_handler import VersionHandler
|
||||
from .handlers.vpcs_handler import VPCSHandler
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Server(object):
|
||||
class Server:
|
||||
|
||||
# built-in handlers
|
||||
handlers = [(r"/version", VersionHandler),
|
||||
(r"/upload", FileUploadHandler),
|
||||
(r"/login", LoginHandler)]
|
||||
|
||||
def __init__(self, host, port, ipc, console_bind_to_any):
|
||||
def __init__(self, host, port, console_bind_to_any):
|
||||
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._router = None
|
||||
self._stream = None
|
||||
self._loop = None
|
||||
self._start_time = time.time()
|
||||
|
||||
if console_bind_to_any:
|
||||
if ipaddress.ip_address(self._host).version == 6:
|
||||
@@ -73,263 +58,106 @@ class Server(object):
|
||||
else:
|
||||
self._console_host = self._host
|
||||
|
||||
if ipc:
|
||||
self._zmq_port = 0 # this forces to use IPC for communications with the ZeroMQ server
|
||||
#TODO: server config file support, to be reviewed
|
||||
# # get the projects and temp directories from the configuration file (passed to the modules)
|
||||
# config = Config.instance()
|
||||
# server_config = config.get_default_section()
|
||||
# # default projects directory is "~/GNS3/projects"
|
||||
# self._projects_dir = os.path.expandvars(os.path.expanduser(server_config.get("projects_directory", "~/GNS3/projects")))
|
||||
# self._temp_dir = server_config.get("temporary_directory", tempfile.gettempdir())
|
||||
#
|
||||
# try:
|
||||
# os.makedirs(self._projects_dir)
|
||||
# log.info("projects directory '{}' created".format(self._projects_dir))
|
||||
# except FileExistsError:
|
||||
# pass
|
||||
# except OSError as e:
|
||||
# log.error("could not create the projects directory {}: {}".format(self._projects_dir, e))
|
||||
|
||||
@asyncio.coroutine
|
||||
def _run_application(self, app):
|
||||
|
||||
server = yield from self._loop.create_server(app.make_handler(), self._host, self._port)
|
||||
return server
|
||||
|
||||
def _stop_application(self):
|
||||
"""
|
||||
Cleanup the modules (shutdown running emulators etc.)
|
||||
"""
|
||||
|
||||
#TODO: clean everything from here
|
||||
self._loop.stop()
|
||||
|
||||
def _signal_handling(self):
|
||||
|
||||
def signal_handler(signame):
|
||||
log.warning("server has got signal {}, exiting...".format(signame))
|
||||
self._stop_application()
|
||||
|
||||
signals = ["SIGTERM", "SIGINT"]
|
||||
if sys.platform.startswith("win"):
|
||||
signals.extend(["SIGBREAK"])
|
||||
else:
|
||||
# communication between the ZeroMQ server and the modules (ZeroMQ dealers)
|
||||
# is IPv4 and local (127.0.0.1)
|
||||
try:
|
||||
# let the OS find an unused port for the ZeroMQ server
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
self._zmq_port = sock.getsockname()[1]
|
||||
except OSError as e:
|
||||
log.critical("server cannot listen to {}: {}".format(self._host, e))
|
||||
raise SystemExit
|
||||
self._ipc = ipc
|
||||
self._modules = []
|
||||
signals.extend(["SIGHUP", "SIGQUIT"])
|
||||
|
||||
# get the projects and temp directories from the configuration file (passed to the modules)
|
||||
config = Config.instance()
|
||||
server_config = config.get_default_section()
|
||||
# default projects directory is "~/GNS3/projects"
|
||||
self._projects_dir = os.path.expandvars(os.path.expanduser(server_config.get("projects_directory", "~/GNS3/projects")))
|
||||
self._temp_dir = server_config.get("temporary_directory", tempfile.gettempdir())
|
||||
for signal_name in signals:
|
||||
callback = functools.partial(signal_handler, signal_name)
|
||||
if sys.platform.startswith("win"):
|
||||
# add_signal_handler() is not yet supported on Windows
|
||||
signal.signal(getattr(signal, signal_name), callback)
|
||||
else:
|
||||
self._loop.add_signal_handler(getattr(signal, signal_name), callback)
|
||||
|
||||
try:
|
||||
os.makedirs(self._projects_dir)
|
||||
log.info("projects directory '{}' created".format(self._projects_dir))
|
||||
except FileExistsError:
|
||||
pass
|
||||
except OSError as e:
|
||||
log.error("could not create the projects directory {}: {}".format(self._projects_dir, e))
|
||||
def _reload_hook(self):
|
||||
|
||||
def load_modules(self):
|
||||
"""
|
||||
Loads the modules.
|
||||
"""
|
||||
def reload():
|
||||
|
||||
#=======================================================================
|
||||
# cwd = os.path.dirname(os.path.abspath(__file__))
|
||||
# module_path = os.path.join(cwd, 'modules')
|
||||
# log.info("loading modules from {}".format(module_path))
|
||||
# module_manager = ModuleManager([module_path])
|
||||
# module_manager.load_modules()
|
||||
# for module in module_manager.get_all_modules():
|
||||
# instance = module_manager.activate_module(module,
|
||||
# "127.0.0.1", # ZeroMQ server address
|
||||
# self._zmq_port, # ZeroMQ server port
|
||||
# projects_dir=self._projects_dir,
|
||||
# temp_dir=self._temp_dir)
|
||||
# if not instance:
|
||||
# continue
|
||||
# self._modules.append(instance)
|
||||
# destinations = instance.destinations()
|
||||
# for destination in destinations:
|
||||
# JSONRPCWebSocket.register_destination(destination, module.name)
|
||||
# instance.start() # starts the new process
|
||||
#=======================================================================
|
||||
log.info("reloading")
|
||||
self._stop_application()
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
|
||||
# special built-in to return the server version
|
||||
JSONRPCWebSocket.register_destination("builtin.version", server_version)
|
||||
# special built-in to return the available interfaces on this host
|
||||
JSONRPCWebSocket.register_destination("builtin.interfaces", interfaces)
|
||||
|
||||
for module in MODULES:
|
||||
instance = module(module.__name__.lower(),
|
||||
"127.0.0.1", # ZeroMQ server address
|
||||
self._zmq_port, # ZeroMQ server port
|
||||
host=self._host, # server host address
|
||||
console_host=self._console_host,
|
||||
projects_dir=self._projects_dir,
|
||||
temp_dir=self._temp_dir)
|
||||
|
||||
self._modules.append(instance)
|
||||
destinations = instance.destinations()
|
||||
for destination in destinations:
|
||||
JSONRPCWebSocket.register_destination(destination, instance.name)
|
||||
instance.start() # starts the new process
|
||||
# code extracted from tornado
|
||||
for module in sys.modules.values():
|
||||
# Some modules play games with sys.modules (e.g. email/__init__.py
|
||||
# in the standard library), and occasionally this can cause strange
|
||||
# failures in getattr. Just ignore anything that's not an ordinary
|
||||
# module.
|
||||
if not isinstance(module, types.ModuleType):
|
||||
continue
|
||||
path = getattr(module, "__file__", None)
|
||||
if not path:
|
||||
continue
|
||||
if path.endswith(".pyc") or path.endswith(".pyo"):
|
||||
path = path[:-1]
|
||||
modified = os.stat(path).st_mtime
|
||||
if modified > self._start_time:
|
||||
log.debug("file {} has been modified".format(path))
|
||||
reload()
|
||||
self._loop.call_later(1, self._reload_hook)
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Starts the Tornado web server and ZeroMQ server.
|
||||
Starts the server.
|
||||
"""
|
||||
|
||||
settings = {
|
||||
"debug":True,
|
||||
"cookie_secret": base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes),
|
||||
"login_url": "/login",
|
||||
}
|
||||
#TODO: SSL support for Rackspace cloud integration (here or with nginx for instance).
|
||||
self._loop = asyncio.get_event_loop()
|
||||
app = aiohttp.web.Application()
|
||||
for method, route, handler in Route.get_routes():
|
||||
log.debug("adding route: {} {}".format(method, route))
|
||||
app.router.add_route(method, route, handler)
|
||||
for module in MODULES:
|
||||
log.debug("loading module {}".format(module.__name__))
|
||||
module.instance()
|
||||
|
||||
ssl_options = {}
|
||||
log.info("starting server on {}:{}".format(self._host, self._port))
|
||||
self._loop.run_until_complete(self._run_application(app))
|
||||
self._signal_handling()
|
||||
|
||||
#FIXME: remove it in production
|
||||
self._loop.call_later(1, self._reload_hook)
|
||||
try:
|
||||
cloud_config = Config.instance().get_section_config("CLOUD_SERVER")
|
||||
|
||||
cloud_settings = {
|
||||
|
||||
"required_user" : cloud_config['WEB_USERNAME'],
|
||||
"required_pass" : cloud_config['WEB_PASSWORD'],
|
||||
}
|
||||
|
||||
settings.update(cloud_settings)
|
||||
|
||||
if cloud_config["SSL_ENABLED"] == "yes":
|
||||
ssl_options = {
|
||||
"certfile" : cloud_config["SSL_CRT"],
|
||||
"keyfile" : cloud_config["SSL_KEY"],
|
||||
}
|
||||
|
||||
log.info("Certs found - starting in SSL mode")
|
||||
except KeyError:
|
||||
log.info("Missing cloud.conf - disabling HTTP auth and SSL")
|
||||
|
||||
router = self._create_zmq_router()
|
||||
# Add our JSON-RPC Websocket handler to Tornado
|
||||
self.handlers.extend([(r"/", JSONRPCWebSocket, dict(zmq_router=router))])
|
||||
if hasattr(sys, "frozen"):
|
||||
templates_dir = "templates"
|
||||
else:
|
||||
templates_dir = pkg_resources.resource_filename("gns3server", "templates")
|
||||
tornado_app = tornado.web.Application(self.handlers,
|
||||
template_path=templates_dir,
|
||||
**settings) # FIXME: debug mode!
|
||||
|
||||
try:
|
||||
user_log = logging.getLogger('user_facing')
|
||||
user_log.info("Starting server on {}:{} (Tornado v{}, PyZMQ v{}, ZMQ v{})".format(
|
||||
self._host, self._port, tornado.version, zmq.__version__, zmq.zmq_version()))
|
||||
|
||||
kwargs = {"address": self._host}
|
||||
|
||||
if ssl_options:
|
||||
kwargs["ssl_options"] = ssl_options
|
||||
|
||||
if parse_version(tornado.version) >= parse_version("3.1"):
|
||||
kwargs["max_buffer_size"] = 524288000 # 500 MB file upload limit
|
||||
|
||||
tornado_app.listen(self._port, **kwargs)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EADDRINUSE: # socket already in use
|
||||
logging.critical("socket in use for {}:{}".format(self._host, self._port))
|
||||
self._cleanup(graceful=False)
|
||||
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
self._stream = zmqstream.ZMQStream(router, ioloop)
|
||||
self._stream.on_recv_stream(JSONRPCWebSocket.dispatch_message)
|
||||
tornado.autoreload.add_reload_hook(self._reload_callback)
|
||||
|
||||
def signal_handler(signum=None, frame=None):
|
||||
try:
|
||||
log.warning("Server got signal {}, exiting...".format(signum))
|
||||
self._cleanup(signum)
|
||||
except RuntimeError:
|
||||
# to ignore logging exception: RuntimeError: reentrant call inside <_io.BufferedWriter name='<stderr>'>
|
||||
pass
|
||||
|
||||
signals = [signal.SIGTERM, signal.SIGINT]
|
||||
if not sys.platform.startswith("win"):
|
||||
signals.extend([signal.SIGHUP, signal.SIGQUIT])
|
||||
else:
|
||||
signals.extend([signal.SIGBREAK])
|
||||
for sig in signals:
|
||||
signal.signal(sig, signal_handler)
|
||||
|
||||
try:
|
||||
ioloop.start()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
self._loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
log.info("\nExiting...")
|
||||
self._cleanup()
|
||||
|
||||
def _create_zmq_router(self):
|
||||
"""
|
||||
Creates the ZeroMQ router socket to send
|
||||
requests to modules.
|
||||
|
||||
:returns: ZeroMQ router socket
|
||||
"""
|
||||
|
||||
context = zmq.Context()
|
||||
context.linger = 0
|
||||
self._router = context.socket(zmq.ROUTER)
|
||||
if self._ipc:
|
||||
try:
|
||||
self._router.bind("ipc:///tmp/gns3.ipc")
|
||||
except zmq.error.ZMQError as e:
|
||||
log.critical("Could not start ZeroMQ server on ipc:///tmp/gns3.ipc, reason: {}".format(e))
|
||||
self._cleanup(graceful=False)
|
||||
raise SystemExit
|
||||
log.info("ZeroMQ server listening to ipc:///tmp/gns3.ipc")
|
||||
else:
|
||||
try:
|
||||
self._router.bind("tcp://127.0.0.1:{}".format(self._zmq_port))
|
||||
except zmq.error.ZMQError as e:
|
||||
log.critical("Could not start ZeroMQ server on 127.0.0.1:{}, reason: {}".format(self._zmq_port, e))
|
||||
self._cleanup(graceful=False)
|
||||
raise SystemExit
|
||||
log.info("ZeroMQ server listening to 127.0.0.1:{}".format(self._zmq_port))
|
||||
return self._router
|
||||
|
||||
def stop_module(self, module):
|
||||
"""
|
||||
Stop a given module.
|
||||
|
||||
:param module: module name
|
||||
"""
|
||||
|
||||
if not self._router.closed:
|
||||
self._router.send_string(module, zmq.SNDMORE)
|
||||
self._router.send_string("stop")
|
||||
|
||||
def _reload_callback(self):
|
||||
"""
|
||||
Callback for the Tornado reload hook.
|
||||
"""
|
||||
|
||||
for module in self._modules:
|
||||
if module.is_alive():
|
||||
module.terminate()
|
||||
module.join(timeout=1)
|
||||
|
||||
def _shutdown(self):
|
||||
"""
|
||||
Shutdowns the I/O loop and the ZeroMQ stream & socket.
|
||||
"""
|
||||
|
||||
if self._stream and not self._stream.closed:
|
||||
# close the ZeroMQ stream
|
||||
self._stream.close()
|
||||
|
||||
if self._router and not self._router.closed:
|
||||
# close the ZeroMQ router socket
|
||||
self._router.close()
|
||||
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
ioloop.stop()
|
||||
|
||||
def _cleanup(self, signum=None, graceful=True):
|
||||
"""
|
||||
Shutdowns any running module processes
|
||||
and adds a callback to stop the event loop & ZeroMQ
|
||||
|
||||
:param signum: signal number (if called by a signal handler)
|
||||
:param graceful: gracefully stop the modules
|
||||
"""
|
||||
|
||||
# terminate all modules
|
||||
for module in self._modules:
|
||||
if module.is_alive() and graceful:
|
||||
log.info("stopping {}".format(module.name))
|
||||
self.stop_module(module.name)
|
||||
module.join(timeout=3)
|
||||
if module.is_alive():
|
||||
# just kill the module if it is still alive.
|
||||
log.info("terminating {}".format(module.name))
|
||||
module.terminate()
|
||||
module.join(timeout=1)
|
||||
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
if signum:
|
||||
ioloop.add_callback_from_signal(self._shutdown)
|
||||
else:
|
||||
ioloop.add_callback(self._shutdown)
|
||||
Reference in new issue
Block a user