New base server.

This commit is contained in:
Jeremy committed 2015-01-13 17:05:26 -07:00
1 parent 5e665cc67a
commit 61344a1669
51 files changed
+2632 -1835

No files matched your search

+103 -275
View File
@@ -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)