mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
242 lines
7.3 KiB
Python
242 lines
7.3 KiB
Python
"""Utils to configure Bigchain.
|
|
|
|
By calling `file_config`, the global configuration (stored in
|
|
`bigchain.config`) will be updated with the values contained in the
|
|
configuration file.
|
|
|
|
Note that there is a precedence in reading configuration values:
|
|
- local config file;
|
|
- environment vars;
|
|
- default config file (contained in ``bigchaindb.__init__``).
|
|
"""
|
|
|
|
|
|
import os
|
|
import copy
|
|
import json
|
|
import logging
|
|
import collections
|
|
|
|
from pkg_resources import iter_entry_points, ResolutionError
|
|
|
|
import bigchaindb
|
|
from bigchaindb.consensus import AbstractConsensusRules
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
CONFIG_DEFAULT_PATH = os.environ.setdefault(
|
|
'BIGCHAINDB_CONFIG_PATH',
|
|
os.path.join(os.path.expanduser('~'), '.bigchaindb'),
|
|
)
|
|
|
|
CONFIG_PREFIX = 'BIGCHAINDB'
|
|
CONFIG_SEP = '_'
|
|
|
|
|
|
def map_leafs(func, mapping):
|
|
"""Map a function to the leafs of a mapping."""
|
|
|
|
def _inner(mapping, path=None):
|
|
if path is None:
|
|
path = []
|
|
|
|
for key, val in mapping.items():
|
|
if isinstance(val, collections.Mapping):
|
|
_inner(val, path + [key])
|
|
else:
|
|
mapping[key] = func(val, path=path+[key])
|
|
|
|
return mapping
|
|
|
|
return _inner(copy.deepcopy(mapping))
|
|
|
|
|
|
def get_casted_value(original, to_convert):
|
|
try:
|
|
return original(to_convert)
|
|
except TypeError:
|
|
return type(original)(to_convert)
|
|
|
|
|
|
# Thanks Alex <3
|
|
# http://stackoverflow.com/a/3233356/597097
|
|
def update(d, u):
|
|
"""Recursively update a mapping."""
|
|
for k, v in u.items():
|
|
if isinstance(v, collections.Mapping):
|
|
r = update(d.get(k, {}), v)
|
|
d[k] = r
|
|
else:
|
|
d[k] = u[k]
|
|
return d
|
|
|
|
|
|
def file_config(filename=None):
|
|
"""Returns the values found in a configuration file.
|
|
|
|
Args:
|
|
filename (str): the JSON file with the configuration. Defaults to ``None``.
|
|
If ``None``, the HOME of the current user and the string ``.bigchaindb`` will be used.
|
|
|
|
Note:
|
|
The function merges the values in ``filename`` with the **default configuration**,
|
|
so any update made to ``bigchaindb.config`` will be lost.
|
|
"""
|
|
if not filename:
|
|
filename = CONFIG_DEFAULT_PATH
|
|
|
|
with open(filename) as f:
|
|
config = json.load(f)
|
|
|
|
logger.info('Configuration loaded from `{}`'.format(filename))
|
|
|
|
return config
|
|
|
|
|
|
def env_config(config):
|
|
"""Return a new configuration with the values found in the environment.
|
|
|
|
The function recursively iterates over the config, checking if there is
|
|
a matching env variable. If an env variable is found, the func updates
|
|
the configuration with that value.
|
|
|
|
The name of the env variable is built combining a prefix (``BIGCHAINDB``)
|
|
with the path to the value. If the ``config`` in input is:
|
|
``{'database': {'host': 'localhost'}}``
|
|
this function will try to read the env variable ``BIGCHAINDB_DATABASE_HOST``.
|
|
"""
|
|
|
|
def load_from_env(value, path):
|
|
var_name = CONFIG_SEP.join([CONFIG_PREFIX] + list(map(lambda s: s.upper(), path)))
|
|
return os.environ.get(var_name, value)
|
|
|
|
return map_leafs(load_from_env, config)
|
|
|
|
|
|
def update_types(config, reference, list_sep=':'):
|
|
"""Return a new configuration where all the values types
|
|
are aligned with the ones in the default configuration"""
|
|
|
|
def _coerce(current, value):
|
|
# Coerce a value to the `current` type.
|
|
try:
|
|
# First we try to apply current to the value, since it
|
|
# might be a function
|
|
return current(value)
|
|
except TypeError:
|
|
# Then we check if current is a list AND if the value
|
|
# is a string.
|
|
if isinstance(current, list) and isinstance(value, str):
|
|
# If so, we use the colon as the separator
|
|
return value.split(list_sep)
|
|
|
|
try:
|
|
# If we are here, we should try to apply the type
|
|
# of `current` to the value
|
|
return type(current)(value)
|
|
except TypeError:
|
|
# Worst case scenario we return the value itself.
|
|
return value
|
|
|
|
def _update_type(value, path):
|
|
current = reference
|
|
|
|
for elem in path:
|
|
try:
|
|
current = current[elem]
|
|
except KeyError:
|
|
return value
|
|
|
|
return _coerce(current, value)
|
|
|
|
return map_leafs(_update_type, config)
|
|
|
|
|
|
def dict_config(config):
|
|
"""Merge the provided configuration with the default one.
|
|
|
|
Args:
|
|
newconfig (dict): a dictionary with the configuration to load.
|
|
|
|
Note:
|
|
The function merges ``newconfig`` with the **default configuration**, so any
|
|
update made to ``bigchaindb.config`` will be lost.
|
|
"""
|
|
bigchaindb.config = copy.deepcopy(bigchaindb._config)
|
|
update(bigchaindb.config, update_types(config, bigchaindb.config))
|
|
bigchaindb.config['CONFIGURED'] = True
|
|
|
|
|
|
def write_config(config, filename=None):
|
|
"""Write the provided configuration to a specific location.
|
|
|
|
Args:
|
|
newconfig (dict): a dictionary with the configuration to load.
|
|
filename (str): the name of the file that will store the new configuration. Defaults to ``None``.
|
|
If ``None``, the HOME of the current user and the string ``.bigchaindb`` will be used.
|
|
"""
|
|
if not filename:
|
|
filename = CONFIG_DEFAULT_PATH
|
|
|
|
with open(filename, 'w') as f:
|
|
json.dump(config, f)
|
|
|
|
|
|
def autoconfigure(filename=None, config=None, force=False):
|
|
"""Run ``file_config`` and ``env_config`` if the module has not
|
|
been initialized."""
|
|
|
|
if not force and bigchaindb.config.get('CONFIGURED'):
|
|
logger.debug('System already configured, skipping autoconfiguration')
|
|
return
|
|
|
|
newconfig = env_config(bigchaindb.config)
|
|
|
|
if config:
|
|
newconfig = update(newconfig, config)
|
|
|
|
try:
|
|
# import pdb; pdb.set_trace()
|
|
newconfig = update(newconfig, file_config(filename=filename))
|
|
except FileNotFoundError as e:
|
|
logger.warning('Cannot find config file `%s`.' % e.filename)
|
|
|
|
dict_config(newconfig)
|
|
return newconfig
|
|
|
|
|
|
def load_consensus_plugin(name=None):
|
|
"""Find and load the chosen consensus plugin.
|
|
|
|
Args:
|
|
name (string): the name of the entry_point, as advertised in the
|
|
setup.py of the providing package.
|
|
|
|
Returns:
|
|
an uninstantiated subclass of ``bigchaindb.consensus.AbstractConsensusRules``
|
|
"""
|
|
if not name:
|
|
name = bigchaindb.config.get('consensus_plugin', 'default')
|
|
|
|
# TODO: This will return the first plugin with group `bigchaindb.consensus`
|
|
# and name `name` in the active WorkingSet.
|
|
# We should probably support Requirements specs in the config, e.g.
|
|
# consensus_plugin: 'my-plugin-package==0.0.1;default'
|
|
plugin = None
|
|
for entry_point in iter_entry_points('bigchaindb.consensus', name):
|
|
plugin = entry_point.load()
|
|
|
|
# No matching entry_point found
|
|
if not plugin:
|
|
raise ResolutionError(
|
|
'No plugin found in group `bigchaindb.consensus` with name `{}`'.
|
|
format(name))
|
|
|
|
# Is this strictness desireable?
|
|
# It will probably reduce developer headaches in the wild.
|
|
if not issubclass(plugin, (AbstractConsensusRules)):
|
|
raise TypeError("object of type '{}' does not implement `bigchaindb."
|
|
"consensus.AbstractConsensusRules`".format(type(plugin)))
|
|
|
|
return plugin
|