PyLink/conf.py

154 lines
6.1 KiB
Python

"""
conf.py - PyLink configuration core.
This module is used to access the configuration of the current PyLink instance.
It provides simple checks for validating and loading YAML-format configurations from arbitrary files.
"""
try:
import yaml
except ImportError:
raise ImportError("PyLink requires PyYAML to function; please install it and try again.")
import logging
import os.path
import sys
from collections import defaultdict
from . import world
__all__ = ['ConfigurationError', 'conf', 'confname', 'validate', 'load_conf',
'get_database_name']
class ConfigurationError(RuntimeError):
"""Error when config conditions aren't met."""
conf = {'bot':
{
'nick': 'PyLink',
'user': 'pylink',
'realname': 'PyLink Service Client',
'serverdesc': 'Unconfigured PyLink'
},
'logging':
{
'console': 'INFO'
},
'servers':
# Wildcard defaultdict! This means that
# any network name you try will work and return
# this basic template:
defaultdict(lambda: {'ip': '0.0.0.0',
'port': 7000,
'recvpass': "unconfigured",
'sendpass': "unconfigured",
'protocol': "null",
'hostname': "pylink.unconfigured",
'sid': "000",
'maxnicklen': 20,
'sidrange': '0##'
})
}
conf['pylink'] = conf['bot']
confname = 'unconfigured'
def validate(condition, errmsg):
"""Raises ConfigurationError with errmsg unless the given condition is met."""
if not condition:
raise ConfigurationError(errmsg)
def _log(level, text, *args, logger=None, **kwargs):
if logger:
logger.log(level, text, *args, **kwargs)
else:
world._log_queue.append((level, text))
def _validate_conf(conf, logger=None):
"""Validates a parsed configuration dict."""
validate(isinstance(conf, dict),
"Invalid configuration given: should be type dict, not %s."
% type(conf).__name__)
if 'pylink' in conf and 'bot' in conf:
_log(logging.WARNING, "Since PyLink 1.2, the 'pylink:' and 'bot:' configuration sections have been condensed "
"into one. You should merge any options under these sections into one 'pylink:' block.", logger=logger)
new_block = conf['bot'].copy()
new_block.update(conf['pylink'])
conf['bot'] = conf['pylink'] = new_block
elif 'pylink' in conf:
conf['bot'] = conf['pylink']
elif 'bot' in conf:
conf['pylink'] = conf['bot']
# TODO: add a migration warning in the next release.
for section in ('pylink', 'servers', 'login', 'logging'):
validate(conf.get(section), "Missing %r section in config." % section)
# Make sure at least one form of authentication is valid.
# Also we'll warn them that login:user/login:password is deprecated
if conf['login'].get('password') or conf['login'].get('user'):
_log(logging.WARNING, "The 'login:user' and 'login:password' options are deprecated since PyLink 1.1. "
"Please switch to the new 'login:accounts' format as outlined in the example config.", logger=logger)
old_login_valid = isinstance(conf['login'].get('password'), str) and isinstance(conf['login'].get('user'), str)
newlogins = conf['login'].get('accounts', {})
validate(old_login_valid or newlogins, "No accounts were set, aborting!")
for account, block in newlogins.items():
validate(isinstance(account, str), "Bad username format %s" % account)
validate(isinstance(block.get('password'), str), "Bad password %s for account %s" % (block.get('password'), account))
validate(conf['login'].get('password') != "changeme", "You have not set the login details correctly!")
if newlogins and not old_login_valid:
validate(conf.get('permissions'), "New-style accounts enabled but no permissions block was found. You will not be able to administrate your PyLink instance!")
if conf['logging'].get('stdout'):
_log(logging.WARNING, 'The log:stdout option is deprecated since PyLink 1.2 in favour of '
'(a more correctly named) log:console. Please update your '
'configuration accordingly!', logger=logger)
return conf
def load_conf(filename, errors_fatal=True, logger=None):
"""Loads a PyLink configuration file from the filename given."""
global confname, conf, fname
# Note: store globally the last loaded conf filename, for REHASH in coremods/control.
fname = filename
# For the internal config name, strip off any .yml extensions and absolute paths
confname = os.path.splitext(os.path.basename(filename))[0]
try:
with open(filename, 'r') as f:
conf = yaml.safe_load(f)
conf = _validate_conf(conf, logger=logger)
except Exception as e:
e = 'Failed to load config from %r: %s: %s' % (filename, type(e).__name__, e)
if logger: # Prefer using the Python logger when available
logger.exception(e)
else: # Otherwise, fall back to a print() call.
print('ERROR: %s' % e, file=sys.stderr)
if errors_fatal:
sys.exit(1)
raise
else:
return conf
def get_database_name(dbname):
"""
Returns a database filename with the given base DB name appropriate for the
current PyLink instance.
This returns '<dbname>.db' if the running config name is PyLink's default
(pylink.yml), and '<dbname>-<config name>.db' for anything else. For example,
if this is called from an instance running as 'pylink testing.yml', it
would return '<dbname>-testing.db'."""
if confname != 'pylink':
dbname += '-%s' % confname
dbname += '.db'
return dbname