""" Configuration package x/84. """
# std imports
from __future__ import print_function
import logging.config
import ConfigParser
import warnings
import inspect
import getpass
import socket
import os
#: Singleton representing configuration after load
CFG = None
# pylint: disable=R0915,R0912,W0603
# Too many statements
# Too many branches
# Using the global statement
[docs]def init(lookup_bbs, lookup_log):
"""
Initialize global 'CFG' variable, a singleton to contain bbs settings.
Each variable (``lookup_bbs``, ``lookup_log``) is tuple lookup path of
in-order preferences for .ini files. If none are found, defaults are
initialized, and the last item of each tuple is created.
"""
log = logging.getLogger(__name__)
def write_cfg(cfg, filepath):
""" Write Config to filepath. """
if not os.path.exists(os.path.dirname(os.path.expanduser(filepath))):
dir_name = os.path.dirname(os.path.expanduser(filepath))
print('Creating folder {0}'.format(dir_name))
os.mkdir(dir_name)
print('Saving {0}'.format(filepath))
cfg.write(open(os.path.expanduser(filepath), 'w'))
# exploit last argument, presumed to be within a folder
# writable by our process, and where the ini is wanted
# -- engine.py specifys a default of: ~/.x84/somefile.ini
loaded = False
cfg_logfile = lookup_log[-1]
for cfg_logfile in lookup_log:
cfg_logfile = os.path.expanduser(cfg_logfile)
# load-only defaults,
if os.path.exists(cfg_logfile):
print('loading {0}'.format((cfg_logfile)))
logging.config.fileConfig(cfg_logfile)
loaded = True
break
if not loaded:
cfg_log = init_log_ini()
dir_name = os.path.dirname(cfg_logfile)
if not os.path.isdir(dir_name):
try:
os.makedirs(dir_name)
except OSError as err:
log.warn(err)
try:
write_cfg(cfg_log, cfg_logfile)
log.info('Saved %s', cfg_logfile)
except IOError as err:
log.error(err)
logging.config.fileConfig(cfg_logfile)
loaded = False
cfg_bbs = ConfigParser.SafeConfigParser()
cfg_bbsfile = lookup_bbs[-1]
for cfg_bbsfile in lookup_bbs:
cfg_bbsfile = os.path.expanduser(cfg_bbsfile)
# load defaults,
if os.path.exists(cfg_bbsfile):
cfg_bbs.read(cfg_bbsfile)
log.info('loaded %s', cfg_bbsfile)
loaded = True
break
if not loaded:
cfg_bbs = init_bbs_ini()
dir_name = os.path.dirname(cfg_bbsfile)
if not os.path.isdir(dir_name):
try:
os.makedirs(dir_name)
except OSError as err:
log.warn(err)
try:
write_cfg(cfg_bbs, cfg_bbsfile)
log.info('Saved %s', cfg_bbsfile)
except IOError as err:
log.error(err)
global CFG
CFG = cfg_bbs
[docs]def init_bbs_ini():
""" Returns ConfigParser instance of bbs system defaults. """
# ### How this should have been written ...
# ###
# ### each module should provide a declaration, probably based on a
# ### collections.namedtuple that simply states the various fields,
# ### a help description, and its default values. Then, this program
# ### simply "walks" the module path, importing everybody and finding
# ### this declaration to build up the "default" configuration file.
# ###
# ### at least, in this way, the module defines its configuration scheme
# ### where it is used.
# wouldn't it be nice if we could use comments in the file .. ?
# in such cases, it might be better to use jinja2 or something
cfg_bbs = ConfigParser.SafeConfigParser()
cfg_bbs.add_section('system')
cfg_bbs.set('system', 'bbsname', 'x/84')
cfg_bbs.set('system', 'sysop', '')
cfg_bbs.set('system', 'software', 'x/84')
# use module-level 'default' folder
cfg_bbs.set('system', 'scriptpath', os.path.abspath(
os.path.join(os.path.dirname(__file__), os.path.pardir, 'default')))
cfg_bbs.set('system', 'datapath', os.path.expanduser(os.path.join(
os.path.join('~', '.x84', 'data'))))
cfg_bbs.set('system', 'timeout', '1984')
try:
# pylint: disable=W0612
# Unused variable 'bcrypt'
import bcrypt # NOQA
except ImportError:
cfg_bbs.set('system', 'password_digest', 'internal')
else:
cfg_bbs.set('system', 'password_digest', 'bcrypt')
cfg_bbs.set('system', 'mail_addr',
'%s@%s' % (getpass.getuser(), socket.gethostname()))
cfg_bbs.set('system', 'mail_smtphost', 'localhost')
# one *Could* change 'ansi' termcaps to 'ansi-bbs', for SynchTerm,
# but how do we identify that 'ansi-bbs' TERM is available on this
# system? hmm .. lets offer the reverse, anything beginning with
# 'ansi' can changed to any other value; so we could be
# unidirectional: a value of 'ansi' will translate ansi-bbs -> ansi,
# and a value of 'ansi-bbs' will translate ansi -> ansi-bbs.
cfg_bbs.set('system', 'termcap-ansi', 'ansi')
# change 'unknown' termcaps to 'ansi': for dumb terminals
cfg_bbs.set('system', 'termcap-unknown', 'ansi')
# could be information leak to sensitive sysops
cfg_bbs.set('system', 'show_traceback', 'yes')
# store passwords in uppercase, facebook and mystic bbs does this ..
cfg_bbs.set('system', 'pass_ucase', 'no')
# default encoding for the showart function on UTF-8 capable terminals
cfg_bbs.set('system', 'art_utf8_codec', 'cp437')
cfg_bbs.add_section('telnet')
cfg_bbs.set('telnet', 'enabled', 'yes')
cfg_bbs.set('telnet', 'addr', '127.0.0.1')
cfg_bbs.set('telnet', 'port', '6023')
cfg_bbs.add_section('ssh')
try:
# pylint: disable=W0612
# Unused variable 'x84'
import x84.ssh # noqa
cfg_bbs.set('ssh', 'enabled', 'yes')
except ImportError:
cfg_bbs.set('ssh', 'enabled', 'no')
cfg_bbs.set('ssh', 'addr', '127.0.0.1')
cfg_bbs.set('ssh', 'port', '6022')
cfg_bbs.set('ssh', 'hostkey', os.path.expanduser(
os.path.join('~', '.x84', 'ssh_host_rsa_key')))
cfg_bbs.set('ssh', 'hostkeybits', '2048')
cfg_bbs.add_section('sftp')
cfg_bbs.set('sftp', 'enabled', 'no')
cfg_bbs.set('sftp', 'root', os.path.expanduser(
os.path.join('~', 'x84-sftp_root')))
try:
os.makedirs(
os.path.join(cfg_bbs.get('sftp', 'root'), "__uploads__"))
except OSError:
pass
cfg_bbs.set('sftp', 'uploads_filemode', '644')
# rlogin only works on port 513
cfg_bbs.add_section('rlogin')
cfg_bbs.set('rlogin', 'enabled', 'no')
cfg_bbs.set('rlogin', 'addr', '127.0.0.1')
cfg_bbs.set('rlogin', 'port', '513')
# web
cfg_bbs.add_section('web')
cfg_bbs.set('web', 'enabled', 'no')
cfg_bbs.set('web', 'port', '443')
cfg_bbs.set('web', 'cert', os.path.expanduser(
os.path.join('~', '.x84', 'ssl.cer')))
cfg_bbs.set('web', 'key', os.path.expanduser(
os.path.join('~', '.x84', 'ssl.key')))
cfg_bbs.set('web', 'chain', os.path.expanduser(
os.path.join('~', '.x84', 'ca.cer')))
cfg_bbs.set('web', 'modules', 'msgserve')
# default path if cmd argument is not absolute,
cfg_bbs.add_section('door')
cfg_bbs.set('door', 'path', '/usr/local/bin:/usr/games')
cfg_bbs.add_section('matrix')
cfg_bbs.set('matrix', 'newcmds', 'new, apply')
cfg_bbs.set('matrix', 'byecmds', 'exit, logoff, bye, quit')
cfg_bbs.set('matrix', 'anoncmds', 'anonymous')
cfg_bbs.set('matrix', 'script', 'matrix')
cfg_bbs.set('matrix', 'script_telnet', 'matrix')
cfg_bbs.set('matrix', 'script_ssh', 'matrix_ssh')
cfg_bbs.set('matrix', 'script_sftp', 'matrix_sftp')
cfg_bbs.set('matrix', 'topscript', 'top')
cfg_bbs.set('matrix', 'enable_anonymous', 'no')
cfg_bbs.set('matrix', 'enable_pwreset', 'yes')
cfg_bbs.add_section('session')
cfg_bbs.set('session', 'tap_input', 'no')
cfg_bbs.set('session', 'tap_output', 'no')
cfg_bbs.set('session', 'tap_events', 'no')
cfg_bbs.set('session', 'tap_db', 'no')
cfg_bbs.set('session', 'default_encoding', 'utf8')
cfg_bbs.add_section('irc')
cfg_bbs.set('irc', 'server', 'efnet.portlane.se')
cfg_bbs.set('irc', 'port', '6667')
cfg_bbs.set('irc', 'channel', '#1984')
cfg_bbs.set('irc', 'enable_privnotice', 'yes')
cfg_bbs.set('irc', 'maxnick', '9')
cfg_bbs.set('irc', 'ssl', 'no')
cfg_bbs.add_section('shroo-ms')
cfg_bbs.set('shroo-ms', 'enabled', 'no')
cfg_bbs.set('shroo-ms', 'idkey', '')
cfg_bbs.set('shroo-ms', 'restkey', '')
# new user account script
cfg_bbs.add_section('nua')
cfg_bbs.set('nua', 'script', 'nua')
cfg_bbs.set('nua', 'min_user', '3')
cfg_bbs.set('nua', 'min_pass', '4')
cfg_bbs.set('nua', 'max_user', '11')
cfg_bbs.set('nua', 'max_pass', '16')
cfg_bbs.set('nua', 'max_email', '30')
cfg_bbs.set('nua', 'max_location', '24')
cfg_bbs.set('nua', 'allow_apply', 'yes')
invalid_handles = u', '.join((
cfg_bbs.get('matrix', 'byecmds'),
cfg_bbs.get('matrix', 'newcmds'),
'anonymous', 'sysop',))
cfg_bbs.set('nua', 'invalid_handles', invalid_handles)
cfg_bbs.set('nua', 'handle_validation', '^[A-Za-z0-9]{3,11}$')
cfg_bbs.add_section('msg')
cfg_bbs.set('msg', 'max_subject', '40')
# by default, anybody can make up a new tag. otherwise, only
# those of the groups specified may.
cfg_bbs.set('msg', 'moderated_tags', 'no')
cfg_bbs.set('msg', 'tag_moderators', 'sysop, moderator')
return cfg_bbs
[docs]def init_log_ini():
""" Return ConfigParser instance of logger defaults. """
cfg_log = ConfigParser.RawConfigParser()
cfg_log.add_section('formatters')
cfg_log.set('formatters', 'keys', 'default')
cfg_log.add_section('formatter_default')
# for multiprocessing/threads, use: %(processName)s %(threadName) !
cfg_log.set('formatter_default', 'format',
u'%(asctime)s %(levelname)-6s '
u'%(filename)10s:%(lineno)-3s %(message)s')
cfg_log.set('formatter_default', 'class', 'logging.Formatter')
cfg_log.set('formatter_default', 'datefmt', '%a-%m-%d %I:%M%p')
cfg_log.add_section('handlers')
cfg_log.set('handlers', 'keys', 'console, rotate_daily')
cfg_log.add_section('handler_console')
cfg_log.set('handler_console', 'class', 'logging.StreamHandler')
cfg_log.set('handler_console', 'formatter', 'default')
cfg_log.set('handler_console', 'args', 'tuple()')
cfg_log.add_section('handler_rotate_daily')
cfg_log.set('handler_rotate_daily', 'class',
'logging.handlers.TimedRotatingFileHandler')
cfg_log.set('handler_rotate_daily', 'level', 'INFO')
cfg_log.set('handler_rotate_daily', 'suffix', '%Y%m%d')
cfg_log.set('handler_rotate_daily', 'encoding', 'utf8')
cfg_log.set('handler_rotate_daily', 'formatter', 'default')
daily_log = os.path.join(os.path.expanduser(
os.path.join('~', '.x84', 'daily.log')))
cfg_log.set('handler_rotate_daily', 'args',
'("' + daily_log + '", "midnight", 1, 60)')
cfg_log.add_section('loggers')
cfg_log.set('loggers', 'keys',
'root, sqlitedict, paramiko, xmodem, requests, irc')
cfg_log.add_section('logger_root')
cfg_log.set('logger_root', 'level', 'INFO')
cfg_log.set('logger_root', 'formatter', 'default')
cfg_log.set('logger_root', 'handlers', 'console, rotate_daily')
# squelch sqlitedict's info, its rather long
cfg_log.add_section('logger_sqlitedict')
cfg_log.set('logger_sqlitedict', 'level', 'WARN')
cfg_log.set('logger_sqlitedict', 'formatter', 'default')
cfg_log.set('logger_sqlitedict', 'handlers', 'console, rotate_daily')
cfg_log.set('logger_sqlitedict', 'qualname', 'sqlitedict')
# squelch paramiko.transport info, also too verbose
cfg_log.add_section('logger_paramiko')
cfg_log.set('logger_paramiko', 'level', 'WARN')
cfg_log.set('logger_paramiko', 'formatter', 'default')
cfg_log.set('logger_paramiko', 'handlers', 'console, rotate_daily')
cfg_log.set('logger_paramiko', 'qualname', 'paramiko.transport')
# squelch xmodem's debug, too verbose
cfg_log.add_section('logger_xmodem')
cfg_log.set('logger_xmodem', 'level', 'INFO')
cfg_log.set('logger_xmodem', 'formatter', 'default')
cfg_log.set('logger_xmodem', 'handlers', 'console, rotate_daily')
cfg_log.set('logger_xmodem', 'qualname', 'xmodem')
# squelch requests to warn, too verbose
cfg_log.add_section('logger_requests')
cfg_log.set('logger_requests', 'level', 'WARN')
cfg_log.set('logger_requests', 'formatter', 'default')
cfg_log.set('logger_requests', 'handlers', 'console, rotate_daily')
cfg_log.set('logger_requests', 'qualname', 'requests')
# squelch irc debug, privacy-invasive
cfg_log.add_section('logger_irc')
cfg_log.set('logger_irc', 'level', 'INFO')
cfg_log.set('logger_irc', 'formatter', 'default')
cfg_log.set('logger_irc', 'handlers', 'console, rotate_daily')
cfg_log.set('logger_irc', 'qualname', 'irc.client')
return cfg_log
[docs]def get_ini(section=None, key=None, getter='get', split=False, splitsep=','):
"""
Get an ini configuration of ``section`` and ``key``.
If the option does not exist, an empty list, string, or False
is returned -- return type decided by the given arguments.
The ``getter`` method is 'get' by default, returning a string.
For booleans, use ``getter='get_boolean'``.
To return a list, use ``split=True``.
"""
assert section is not None, section
assert key is not None, key
if CFG is None:
# when building documentation, 'get_ini' at module-level
# imports is not really an error. However, if you're importing
# a module that calls get_ini before the config system is
# initialized, then you're going to get an empty value! warning!!
stack = inspect.stack()
caller_mod, caller_func = stack[2][1], stack[2][3]
warnings.warn('ini system not (yet) initialized, '
'caller = {0}:{1}'.format(caller_mod, caller_func))
elif CFG.has_option(section, key):
getter = getattr(CFG, getter)
value = getter(section, key)
if split and hasattr(value, 'split'):
return [_value.strip() for _value in value.split(splitsep)]
return value
if getter == 'getboolean':
return False
if split:
return []
return u''