You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
185 lines
5.6 KiB
185 lines
5.6 KiB
''' Configuration subsystem '''
|
|
|
|
from typing import MutableMapping, Any
|
|
|
|
from sys import argv, executable, stdout
|
|
from os import environ, getcwd, sep as psep
|
|
from os.path import dirname, abspath, join as joinpath, \
|
|
expanduser, splitdrive, isfile
|
|
|
|
from copy import deepcopy
|
|
|
|
import logging
|
|
import pprint
|
|
|
|
import asyncio
|
|
|
|
from apscheduler.triggers.cron import CronTrigger # type: ignore
|
|
from apscheduler.triggers.interval import IntervalTrigger # type: ignore
|
|
|
|
from zasd.log import log
|
|
from zasd.util import ifenv, uniq, find_file
|
|
|
|
import zasd.config as _config
|
|
|
|
#
|
|
# Constants
|
|
|
|
_CONFIG_BASENAME = 'zasd.conf.py'
|
|
_CONFIG_FILENAMES = [_CONFIG_BASENAME, '.' + _CONFIG_BASENAME]
|
|
|
|
_WARNING_WAIT = 3
|
|
#_WARNING_WAIT = 10
|
|
|
|
# Default configuration
|
|
_DEFAULT_CONFIG = dict(
|
|
zfs_path = '/usr/local/bin/zfs',
|
|
fswatch_path = '/usr/local/bin/fswatch',
|
|
|
|
tab_size = 2,
|
|
|
|
log_level = logging.INFO,
|
|
log_format = '%(asctime)s %(name)s [%(levelname)-8s]: %(message)s',
|
|
log_date_format = '%a, %d %b %Y, %H:%M:%S',
|
|
|
|
separator = ':',
|
|
destroy_trigger = CronTrigger.from_crontab('* * * * *'),
|
|
|
|
# Defaults will take a snapshot every 12 hours
|
|
# and keep them for a week, so if the config file
|
|
# should disappear for some reason, there will
|
|
# at least be _some_ recoverable snapshots
|
|
defaults = dict(
|
|
disabled = False,
|
|
filesystems = ['tank'],
|
|
recursive = True,
|
|
tag = 'zasd',
|
|
trigger = IntervalTrigger(hours=12),
|
|
if_modified = False,
|
|
priority = 1,
|
|
keep = 14
|
|
),
|
|
|
|
schedules = [{}]
|
|
)
|
|
|
|
async def load_config():
|
|
''' Load configuration file '''
|
|
|
|
if len(argv) > 1:
|
|
# Configuration pathname given as first argument
|
|
if isfile(argv[1]):
|
|
config_pathname = argv[1]
|
|
else:
|
|
log.warning('Could not find configuration file %s', argv[1])
|
|
return await _warn_default()
|
|
else:
|
|
# No configuration pathname given; attempt to find it:
|
|
|
|
# Get root of system partition
|
|
sys_root_path = environ.get('SystemDrive', splitdrive(executable)[0] + psep)
|
|
|
|
# Get system configuration directory
|
|
sys_conf_path = joinpath(
|
|
*ifenv(
|
|
'SystemRoot',
|
|
lambda path: [path, 'System32', 'drivers', 'etc'],
|
|
lambda: [sys_root_path, 'etc']))
|
|
|
|
# Get user home directory
|
|
user_home_path = expanduser('~')
|
|
|
|
# Get path of this Python file
|
|
if '__file__' in globals():
|
|
script_path = dirname(abspath(__file__))
|
|
else:
|
|
script_path = dirname(abspath(argv[0]))
|
|
|
|
# Build list of configuration file pathnames to search
|
|
config_paths = uniq([
|
|
getcwd(),
|
|
user_home_path,
|
|
joinpath(user_home_path, '.config'),
|
|
joinpath(user_home_path, '.local', 'share'),
|
|
sys_conf_path,
|
|
script_path])
|
|
config_pathnames = list(joinpath(p, f) for p in config_paths for f in _CONFIG_FILENAMES)
|
|
|
|
# Attempt to find a config file
|
|
config_pathname = find_file(config_pathnames)
|
|
|
|
if config_pathname is None:
|
|
log.warning('Unable to find a config file at:')
|
|
for pathname in config_pathnames:
|
|
log.warning(' %s', pathname)
|
|
|
|
await _warn_default()
|
|
return
|
|
|
|
with open(config_pathname, 'rt', encoding='utf-8') as f:
|
|
config_source = f.read()
|
|
|
|
# Create configuration file scopes
|
|
|
|
global_scope = dict(
|
|
CRITICAL = logging.CRITICAL,
|
|
ERROR = logging.ERROR,
|
|
WARNING = logging.WARNING,
|
|
INFO = logging.INFO,
|
|
DEBUG = logging.DEBUG,
|
|
NOTSET = logging.NOTSET,
|
|
|
|
crontab = CronTrigger.from_crontab,
|
|
cron = CronTrigger.from_crontab,
|
|
|
|
interval = IntervalTrigger,
|
|
every = IntervalTrigger)
|
|
|
|
local_scope: MutableMapping[str, Any] = dict()
|
|
|
|
# Execute configuration file
|
|
exec(config_source, global_scope, local_scope)
|
|
|
|
# Merge configuration with default configuration
|
|
_config.change(_merge_configs(_DEFAULT_CONFIG, local_scope))
|
|
|
|
log.debug('Loaded configuration')
|
|
|
|
if _config.get('log_level') <= logging.DEBUG:
|
|
log.debug('')
|
|
for line in pprint.pformat(_config).split('\n'):
|
|
logging.debug(_config.get('tab_size') * ' ' + line)
|
|
log.debug('')
|
|
|
|
async def _warn_default():
|
|
log.warning('')
|
|
log.warning(
|
|
'Waiting %s seconds before loading default configuration...',
|
|
_WARNING_WAIT)
|
|
log.warning('')
|
|
await asyncio.sleep(_WARNING_WAIT)
|
|
stdout.write('\r')
|
|
log.warning('Loading default configuration')
|
|
log.warning('')
|
|
_config.change(deepcopy(_DEFAULT_CONFIG))
|
|
|
|
# pylint: disable=dangerous-default-value
|
|
def _merge_configs(base, diff, path=[]):
|
|
''' Deep merge configuration dictionaries '''
|
|
|
|
base = base if len(path) == 0 else deepcopy(base)
|
|
for key, value in diff.items():
|
|
if not key in base:
|
|
base[key] = value
|
|
elif not isinstance(value, type(base[key])):
|
|
log.error('Cannot merge diff type %s with base %s type at %s.%s',
|
|
type(value), type(base[key]), '.'.join(path), key)
|
|
return None
|
|
elif isinstance(value, dict):
|
|
merged = _merge_configs(base[key], value, path + [key])
|
|
if merged is None:
|
|
return None
|
|
base[key] = merged
|
|
else:
|
|
base[key] = value
|
|
return base
|
|
|