bigchaindb/bigchaindb/config_utils.py

309 lines
9.6 KiB
Python
Raw Normal View History

# Copyright © 2020 Interplanetary Database Association e.V.,
# BigchainDB and IPDB software contributors.
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0
2016-10-14 14:53:44 +02:00
"""Utils for reading and setting configuration settings.
2016-02-10 19:55:33 +01:00
2016-10-14 14:53:44 +02:00
The value of each BigchainDB Server configuration setting is
determined according to the following rules:
2016-02-10 19:55:33 +01:00
2016-10-17 14:49:21 +02:00
* If it's set by an environment variable, then use that value
* Otherwise, if it's set in a local config file, then use that
2016-10-14 14:53:44 +02:00
value
* Otherwise, use the default value (contained in
2016-10-14 14:53:44 +02:00
``bigchaindb.__init__``)
2016-02-10 19:55:33 +01:00
"""
2016-03-31 11:50:48 +02:00
2016-02-10 19:55:33 +01:00
import os
import copy
import json
import logging
import collections
2017-01-24 10:12:16 +01:00
from functools import lru_cache
from pkg_resources import iter_entry_points, ResolutionError
2016-02-10 19:55:33 +01:00
from bigchaindb.common import exceptions
2016-02-10 19:55:33 +01:00
import bigchaindb
Rebase/feat/586/integrate tx model (#641) * Adjust imports to bigchaindb_common * Adjust get_spent function signature * Adjust block serialization * Fix BigchainApi Test * Fix TestTransactionValidation tests * Fix TestBlockValidation tests * WIP: TestMultipleInputs * Adjust tests to tx-model interface changes - Fix old tests - Fix tests in TestMultipleInputs class * Remove fulfillment message tests * Fix TransactionMalleability tests * Remove Cryptoconditions tests * Remove create_transaction * Remove signing logic * Remove consensus plugin * Fix block_creation pipeline * Fix election pipeline * Replace some util functions with bdb_common ones - timestamp ==> gen_timestamp - serialize. * Implement Block model * Simplify function signatures for vote functions Change parameter interface for the following functions: - has_previous_vote - verify_vote_signature - block_election_status so that they take a block's id and voters instead of a fake block. * Integrate Block and Transaction model * Fix leftover tests and cleanup conftest * Add bigchaindb-common to install_requires * Delete transactions after block is written (#609) * delete transactions after block is written * cleanup transaction_exists * check for duplicate transactions * delete invalid tx from backlog * test duplicate transaction * Remove dead code * Test processes.py * Test invalid tx in on server * Fix tests for core.py * Fix models tests * Test commands main fn * Add final coverage to vote pipeline * Add more tests to voting pipeline * Remove consensus plugin docs and misc * Post rebase fixes * Fix rebase mess * Remove extra blank line * Improve docstring * Remove comment handled in bigchaindb/cryptoconditions#27; see https://github.com/bigchaindb/cryptoconditions/issues/27 * Fix block serialization in block creation * Add signed_ prefix to transfer_tx * Improve docs * Add library documentation page on pipelines * PR feedback for models.py * Impr. readability of get_last_voted_block * Use dict comprehension * Add docker-compose file to build and serve docs locally for development purposes * Change private_key for signing_key * Improve docstrings * Remove consensus docs * Document new consensus module * Create different transactions for the block * Cleanup variable names in block.py * Create different transactions for the block * Cleanup variable names in block.py
2016-09-29 10:29:41 +02:00
from bigchaindb.validation import BaseValidationRules
2016-02-10 19:55:33 +01:00
# TODO: move this to a proper configuration file for logging
logging.getLogger('requests').setLevel(logging.WARNING)
2016-02-10 19:55:33 +01:00
logger = logging.getLogger(__name__)
2016-03-24 01:41:00 +01:00
CONFIG_DEFAULT_PATH = os.environ.setdefault(
'BIGCHAINDB_CONFIG_PATH',
os.path.join(os.path.expanduser('~'), '.bigchaindb'),
)
2016-02-10 19:55:33 +01:00
2016-03-24 01:41:00 +01:00
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))
2016-02-10 19:55:33 +01:00
# Thanks Alex <3
# http://stackoverflow.com/a/3233356/597097
def update(d, u):
"""Recursively update a mapping (i.e. a dict, list, set, or tuple).
Conceptually, d and u are two sets trees (with nodes and edges).
This function goes through all the nodes of u. For each node in u,
if d doesn't have that node yet, then this function adds the node from u,
otherwise this function overwrites the node already in d with u's node.
Args:
d (mapping): The mapping to overwrite and add to.
u (mapping): The mapping to read for changes.
Returns:
mapping: An updated version of d (updated by u).
"""
2016-02-10 19:55:33 +01:00
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 config values found in a configuration file.
2016-02-10 19:55:33 +01:00
Args:
filename (str): the JSON file with the configuration values.
If ``None``, CONFIG_DEFAULT_PATH will be used.
2016-02-10 19:55:33 +01:00
Returns:
dict: The config values in the specified config file (or the
file at CONFIG_DEFAULT_PATH, if filename == None)
2016-02-10 19:55:33 +01:00
"""
logger.debug('On entry into file_config(), filename = {}'.format(filename))
2016-04-26 03:24:56 +02:00
if filename is None:
2016-02-10 19:55:33 +01:00
filename = CONFIG_DEFAULT_PATH
logger.debug('file_config() will try to open `{}`'.format(filename))
with open(filename) as f:
try:
config = json.load(f)
except ValueError as err:
raise exceptions.ConfigurationError(
2016-08-18 20:19:22 +02:00
'Failed to parse the JSON configuration from `{}`, {}'.format(filename, err)
)
2016-02-10 19:55:33 +01:00
logger.info('Configuration loaded from `{}`'.format(filename))
2016-02-10 19:55:33 +01:00
2016-03-24 01:41:00 +01:00
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``.
"""
2016-02-10 19:55:33 +01:00
2016-03-24 01:41:00 +01:00
def load_from_env(value, path):
var_name = CONFIG_SEP.join([CONFIG_PREFIX] + list(map(lambda s: s.upper(), path)))
2016-03-24 01:41:00 +01:00
return os.environ.get(var_name, value)
return map_leafs(load_from_env, config)
2016-04-07 18:06:04 +02:00
def update_types(config, reference, list_sep=':'):
2016-03-24 01:41:00 +01:00
"""Return a new configuration where all the values types
are aligned with the ones in the default configuration
"""
2016-03-24 01:41:00 +01:00
def _coerce(current, value):
# Coerce a value to the `current` type.
2016-03-24 01:41:00 +01:00
try:
# First we try to apply current to the value, since it
# might be a function
return current(value)
2016-03-24 01:41:00 +01:00
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
2016-04-07 18:06:04 +02:00
return value.split(list_sep)
2016-03-24 01:41:00 +01:00
try:
# If we are here, we should try to apply the type
# of `current` to the value
return type(current)(value)
2016-03-24 01:41:00 +01:00
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:
2016-03-24 01:41:00 +01:00
return value
return _coerce(current, value)
2016-03-24 01:41:00 +01:00
return map_leafs(_update_type, config)
2016-04-14 10:55:07 +02:00
def set_config(config):
2016-04-14 09:56:59 +02:00
"""Set bigchaindb.config equal to the default config dict,
then update that with whatever is in the provided config dict,
and then set bigchaindb.config['CONFIGURED'] = True
2016-02-10 19:55:33 +01:00
Args:
2016-04-14 09:56:59 +02:00
config (dict): the config dict to read for changes
to the default config
2016-02-10 19:55:33 +01:00
Note:
2016-04-14 09:56:59 +02:00
Any previous changes made to ``bigchaindb.config`` will be lost.
2016-02-10 19:55:33 +01:00
"""
2016-04-14 09:56:59 +02:00
# Deep copy the default config into bigchaindb.config
2016-02-10 19:55:33 +01:00
bigchaindb.config = copy.deepcopy(bigchaindb._config)
2016-04-14 09:56:59 +02:00
# Update the default config with whatever is in the passed config
2016-04-07 18:06:04 +02:00
update(bigchaindb.config, update_types(config, bigchaindb.config))
2016-02-10 19:55:33 +01:00
bigchaindb.config['CONFIGURED'] = True
def update_config(config):
"""Update bigchaindb.config with whatever is in the provided config dict,
and then set bigchaindb.config['CONFIGURED'] = True
Args:
config (dict): the config dict to read for changes
to the default config
"""
# Update the default config with whatever is in the passed config
update(bigchaindb.config, update_types(config, bigchaindb.config))
bigchaindb.config['CONFIGURED'] = True
2016-03-24 01:41:00 +01:00
def write_config(config, filename=None):
2016-02-10 19:55:33 +01:00
"""Write the provided configuration to a specific location.
Args:
config (dict): a dictionary with the configuration to load.
2016-02-10 19:55:33 +01:00
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, indent=4)
2016-02-10 19:55:33 +01:00
def is_configured():
return bool(bigchaindb.config.get('CONFIGURED'))
2016-03-24 01:41:00 +01:00
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 is_configured():
2016-04-06 16:15:17 +02:00
logger.debug('System already configured, skipping autoconfiguration')
2016-02-10 19:55:33 +01:00
return
2016-03-24 01:41:00 +01:00
# start with the current configuration
newconfig = bigchaindb.config
2016-03-24 01:41:00 +01:00
# update configuration from file
2016-02-10 19:55:33 +01:00
try:
newconfig = update(newconfig, file_config(filename=filename))
except FileNotFoundError as e:
2017-05-18 13:08:46 +02:00
if filename:
raise
else:
logger.info('Cannot find config file `%s`.' % e.filename)
2016-03-24 01:41:00 +01:00
# override configuration with env variables
newconfig = env_config(newconfig)
2016-04-12 16:23:09 +02:00
if config:
newconfig = update(newconfig, config)
set_config(newconfig) # sets bigchaindb.config
2017-01-24 10:12:16 +01:00
2017-01-26 19:36:38 +01:00
2017-01-24 10:12:16 +01:00
@lru_cache()
def load_validation_plugin(name=None):
"""Find and load the chosen validation plugin.
2017-01-24 10:12:16 +01:00
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.validation.AbstractValidationRules``
2017-01-24 10:12:16 +01:00
"""
if not name:
return BaseValidationRules
2017-01-24 10:12:16 +01:00
# TODO: This will return the first plugin with group `bigchaindb.validation`
2017-01-24 10:12:16 +01:00
# and name `name` in the active WorkingSet.
# We should probably support Requirements specs in the config, e.g.
# validation_plugin: 'my-plugin-package==0.0.1;default'
2017-01-24 10:12:16 +01:00
plugin = None
for entry_point in iter_entry_points('bigchaindb.validation', name):
2017-01-24 10:12:16 +01:00
plugin = entry_point.load()
# No matching entry_point found
if not plugin:
raise ResolutionError(
'No plugin found in group `bigchaindb.validation` with name `{}`'.
2017-01-24 10:12:16 +01:00
format(name))
# Is this strictness desireable?
# It will probably reduce developer headaches in the wild.
if not issubclass(plugin, (BaseValidationRules,)):
2017-02-22 14:09:12 +01:00
raise TypeError('object of type "{}" does not implement `bigchaindb.'
'validation.BaseValidationRules`'.format(type(plugin)))
2017-01-24 10:12:16 +01:00
return plugin
2017-07-31 01:58:58 +02:00
def load_events_plugins(names=None):
2017-07-31 01:58:58 +02:00
plugins = []
if names is None:
return plugins
2017-07-31 01:58:58 +02:00
for name in names:
for entry_point in iter_entry_points('bigchaindb.events', name):
2017-07-31 01:58:58 +02:00
plugins.append((name, entry_point.load()))
return plugins