diff --git a/bigchaindb/__init__.py b/bigchaindb/__init__.py index 9283913f..414397f6 100644 --- a/bigchaindb/__init__.py +++ b/bigchaindb/__init__.py @@ -2,7 +2,6 @@ import os import copy - def e(key, default=None, conv=None): '''Get the environment variable `key`, fallback to `default` if nothing is found. @@ -24,6 +23,9 @@ def e(key, default=None, conv=None): config = { + 'server': { + 'bind': e('BIGCHAIN_SERVER_BIND', default='0.0.0.0:5000'), + }, 'database': { 'host': e('BIGCHAIN_DATABASE_HOST', default='localhost'), 'port': e('BIGCHAIN_DATABASE_PORT', default=28015), @@ -49,3 +51,4 @@ config = { # for more info. _config = copy.deepcopy(config) from bigchaindb.core import Bigchain # noqa + diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 35b2de2f..c1be6761 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -55,6 +55,10 @@ def run_configure(args, skip_if_exists=False): conf['keypair']['private'], conf['keypair']['public'] = crypto.generate_key_pair() if not args.yes: + for key in ('host', 'port'): + val = conf['server'][key] + conf['server'][key] = input('API Server {}? (default `{}`): '.format(key, val)) or val + for key in ('host', 'port', 'name'): val = conf['database'][key] conf['database'][key] = input('Database {}? (default `{}`): '.format(key, val)) or val diff --git a/bigchaindb/processes.py b/bigchaindb/processes.py index 661cd9db..20b8df8d 100644 --- a/bigchaindb/processes.py +++ b/bigchaindb/processes.py @@ -3,6 +3,7 @@ import multiprocessing as mp import rethinkdb as r +import bigchaindb from bigchaindb import Bigchain from bigchaindb.voter import Voter from bigchaindb.block import Block @@ -11,6 +12,18 @@ from bigchaindb.web import server logger = logging.getLogger(__name__) +BANNER = """ +**************************************************************************** +* * +* Initialization complete. BigchainDB is ready and waiting for events. * +* You can send events through the API documented at: * +* - http://docs.bigchaindb.apiary.io/ * +* * +* Listening to client connections on: {:<15} * +* * +**************************************************************************** +""" + class Processes(object): @@ -68,8 +81,8 @@ class Processes(object): block = Block(self.q_new_transaction) # start the web api - webapi = server.create_app() - p_webapi = mp.Process(name='webapi', target=webapi.run, kwargs={'host': 'localhost'}) + app_server = server.create_server(bigchaindb.config['server']) + p_webapi = mp.Process(name='webapi', target=app_server.run) p_webapi.start() # initialize the processes @@ -92,5 +105,4 @@ class Processes(object): # start message block.initialized.wait() p_voter.initialized.wait() - logger.info('Initialization complete. BigchainDB ready and waiting for events.') - logger.info('You can send events through the API documented at http://docs.bigchaindb.apiary.io/') + logger.info(BANNER.format(bigchaindb.config['server']['bind'])) diff --git a/bigchaindb/web/server.py b/bigchaindb/web/server.py index 0872fae5..208f2458 100644 --- a/bigchaindb/web/server.py +++ b/bigchaindb/web/server.py @@ -1,16 +1,56 @@ -"""This module contains basic functions to instantiate the BigchainDB API. """ +"""This module contains basic functions to instantiate the BigchainDB API. + +The application is implemented in Flask and runs using Gunicorn. +""" + +import copy +import multiprocessing from flask import Flask from bigchaindb import Bigchain from bigchaindb.web import views +import gunicorn.app.base + + +class StandaloneApplication(gunicorn.app.base.BaseApplication): + """Run a **wsgi** app wrapping it in a Gunicorn Base Application. + + Adapted from: + - http://docs.gunicorn.org/en/latest/custom.html + """ + + def __init__(self, app, options=None): + '''Initialize a new standalone application. + + Args: + app: A wsgi Python application. + options (dict): the configuration. + + ''' + self.options = options or {} + self.application = app + super(StandaloneApplication, self).__init__() + + def load_config(self): + config = dict((key, value) for key, value in self.options.items() + if key in self.cfg.settings and value is not None) + + for key, value in config.items(): + # not sure if we need the `key.lower` here, will just keep + # keep it for now. + self.cfg.set(key.lower(), value) + + def load(self): + return self.application def create_app(debug=False): """Return an instance of the Flask application. Args: - debug (bool): a flag to activate the debug mode for the app (default: False). + debug (bool): a flag to activate the debug mode for the app + (default: False). """ app = Flask(__name__) @@ -18,3 +58,26 @@ def create_app(debug=False): app.config['bigchain'] = Bigchain() app.register_blueprint(views.basic_views, url_prefix='/api/v1') return app + + +def create_server(settings): + """Wrap and return an application ready to be run. + + Args: + settings (dict): a dictionary containing the settings, more info + here http://docs.gunicorn.org/en/latest/settings.html + + Return: + an initialized instance of the application. + """ + + settings = copy.deepcopy(settings) + + if not settings.get('workers'): + settings['workers'] = (multiprocessing.cpu_count() * 2) + 1 + + debug = settings.pop('debug', False) + app = create_app(debug) + standalone = StandaloneApplication(app, settings) + return standalone + diff --git a/setup.py b/setup.py index 26670814..0fd5f4b8 100644 --- a/setup.py +++ b/setup.py @@ -79,6 +79,7 @@ setup( 'bitcoin==1.1.42', 'flask==0.10.1', 'requests==2.9', + 'gunicorn~=19.0', ], setup_requires=['pytest-runner'], tests_require=tests_require, diff --git a/tests/web/test_server.py b/tests/web/test_server.py new file mode 100644 index 00000000..874567d7 --- /dev/null +++ b/tests/web/test_server.py @@ -0,0 +1,13 @@ +import copy + + +def test_settings(monkeypatch): + import bigchaindb + from bigchaindb.web import server + + s = server.create_server(bigchaindb.config['server']) + + # for whatever reason the value is wrapped in a list + # needs further investigation + assert s.cfg.bind[0] == bigchaindb.config['server']['bind'] +