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.
171 lines
5.3 KiB
171 lines
5.3 KiB
from sys import argv, executable
|
|
from os import environ, getcwd, sep as psep
|
|
from os.path import dirname, abspath, join as joinpath, \
|
|
expanduser, splitdrive, isfile
|
|
|
|
from time import sleep
|
|
|
|
from copy import deepcopy
|
|
|
|
from apscheduler.triggers.cron import CronTrigger
|
|
from apscheduler.triggers.interval import IntervalTrigger
|
|
|
|
import logging
|
|
import pprint
|
|
|
|
from zasd.logging import *
|
|
from zasd.util import *
|
|
|
|
#
|
|
# Constants
|
|
|
|
CONFIG_BASENAME = 'zasd.conf.py'
|
|
CONFIG_FILENAMES = [CONFIG_BASENAME, '.' + CONFIG_BASENAME]
|
|
|
|
# Default configuration
|
|
DEFAULT_CONFIG = {
|
|
'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': {
|
|
'disabled': False,
|
|
'filesystems': ['tank'],
|
|
'recursive': True,
|
|
'tag': 'zasd',
|
|
'trigger': IntervalTrigger(hours=12),
|
|
'priority': 1,
|
|
'keep': 14
|
|
},
|
|
|
|
'schedules': [{}]
|
|
}
|
|
|
|
# Load configuration
|
|
def load_config():
|
|
global logger
|
|
|
|
if len(argv) > 1:
|
|
# Configuration pathname given as first argument
|
|
if isfile(argv[1]):
|
|
config_pathname = argv[1]
|
|
else:
|
|
logger.warning('Could not find configuration file %s', argv[1])
|
|
return _warn_load_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:
|
|
logger.warning('Unable to find a config file at:')
|
|
for pathname in config_pathnames:
|
|
logger.warning(' ' + pathname)
|
|
|
|
return _warn_load_default()
|
|
|
|
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 = dict()
|
|
|
|
# Execute configuration file
|
|
exec(config_source, global_scope, local_scope)
|
|
|
|
# Merge configuration with default configuration
|
|
config = merge_configs(DEFAULT_CONFIG, local_scope)
|
|
|
|
logger.debug('Loaded configuration')
|
|
|
|
if config['log_level'] <= logging.DEBUG:
|
|
logger.debug('')
|
|
for line in pprint.pformat(config).split('\n'):
|
|
logging.debug(config['tab_size'] * ' ' + line)
|
|
logger.debug('')
|
|
|
|
return config
|
|
|
|
def _warn_load_default():
|
|
global DEFAULT_CONFIG
|
|
|
|
logger.warning('')
|
|
logger.warning('Waiting 10 seconds before loading default configuration...')
|
|
logger.warning('')
|
|
sleep(10)
|
|
logger.warning('Loading default configuration')
|
|
logger.warning('')
|
|
return deepcopy(DEFAULT_CONFIG)
|
|
|
|
def merge_configs(base, diff, path=[]):
|
|
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])):
|
|
logger.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
|
|
|