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.
219 lines
8.5 KiB
219 lines
8.5 KiB
"""
|
|
Core shepherd module, tying together main service functionality.
|
|
"""
|
|
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
import toml
|
|
import logging
|
|
import click
|
|
from copy import deepcopy
|
|
|
|
from . import scheduler
|
|
from . import config
|
|
from . import plugin
|
|
from . import control
|
|
|
|
|
|
# Future implementations of checking config differences should be done on
|
|
# the hash of the nested conf dict, so comments shouldn't affect this.
|
|
|
|
# save old config to somewhere in the shepherd root dir - probably need to
|
|
# implement a TOML writer in the config module.
|
|
|
|
# later on, there's going to be an issue with a new config being applied
|
|
# remotely, then the system restarting, and an old edit in /boot being
|
|
# applied over the top...
|
|
# Fix this by saving the working config to /boot when new config applied
|
|
# remotely.
|
|
|
|
|
|
# Relative pathnames here are all relative to "root_dir"
|
|
def define_core_config(confdef):
|
|
"""
|
|
Defines the config definition by populating the ConfigDefinition passed in ``confdef`` - the same pattern plugins use
|
|
"""
|
|
confdef.add_def("name", config.StringDef(
|
|
helptext="Identifying name for this device"))
|
|
|
|
confdef.add_def("hostname",
|
|
config.StringDef(default="", optional=True,
|
|
helptext="If set, changes the system hostname"))
|
|
confdef.add_def("plugin_dir",
|
|
config.StringDef(default="~/shepherd-plugins", optional=True,
|
|
helptext="Optional directory for Shepherd to look for plugins in."))
|
|
confdef.add_def("root_dir",
|
|
config.StringDef(default="~/shepherd", optional=True,
|
|
helptext="Operating directory for shepherd to place working files."))
|
|
confdef.add_def("custom_config_path",
|
|
config.StringDef(optional=True,
|
|
helptext="Path to custom config layer TOML file."))
|
|
confdef.add_def("generated_config_path",
|
|
config.StringDef(default="shepherd-generated.toml", optional=True,
|
|
helptext="Path to custom file Shepherd will generate to show compiled config that was used and any errors in validation."))
|
|
|
|
confdef.add_def("control_server", config.StringDef())
|
|
confdef.add_def("control_api_key", config.StringDef())
|
|
|
|
|
|
def resolve_core_conf_paths(core_conf):
|
|
"""
|
|
Set the cwd to ``root_dir`` and resolve other core config paths relative to that.
|
|
Also expands out any "~" user characters.
|
|
"""
|
|
core_conf["root_dir"] = str(Path(core_conf["root_dir"]).expanduser().resolve())
|
|
os.chdir(core_conf["root_dir"])
|
|
core_conf["plugin_dir"] = str(Path(core_conf["plugin_dir"]).expanduser().resolve())
|
|
core_conf["custom_config_path"] = str(
|
|
Path(core_conf["custom_config_path"]).expanduser().resolve())
|
|
core_conf["generated_config_path"] = str(
|
|
Path(core_conf["generated_config_path"]).expanduser().resolve())
|
|
|
|
|
|
def load_config_layer(confman, config_source):
|
|
"""
|
|
Load a config layer, find the necessary plugin classes, then validate it.
|
|
"""
|
|
# Load in config layer
|
|
confman.load_overlay(config_source)
|
|
|
|
# Get the core config so we can find the plugin directory
|
|
core_config = confman.get_config_bundle("shepherd")
|
|
plugin_dir = core_config["plugin_dir"]
|
|
|
|
# List other table names to get plugins we need to load
|
|
plugin_names = confman.get_bundle_names()
|
|
plugin_names.remove("shepherd")
|
|
|
|
# Load plugins to get their conf defs
|
|
plugin_classes = plugin.find_plugins(plugin_names, plugin_dir)
|
|
for plugin_name, plugin_class in plugin_classes.items():
|
|
new_conf_def = config.ConfDefinition()
|
|
plugin_class.define_config(new_conf_def)
|
|
confman.add_confdef(plugin_name, new_conf_def)
|
|
|
|
# Get plugin configs
|
|
plugin_configs = confman.get_config_bundles(plugin_classes.keys())
|
|
return (core_config, plugin_classes, plugin_configs)
|
|
|
|
|
|
def compile_config(default_config_path):
|
|
"""
|
|
Run through the process of assembling the various config layers, falling back to working
|
|
ones where necessary. Also gathers needed plugin classes in the process.
|
|
"""
|
|
|
|
# Create core confdef and populate it
|
|
core_confdef = config.ConfDefinition()
|
|
define_core_config(core_confdef)
|
|
|
|
confman = config.ConfigManager()
|
|
confman.add_confdef("shepherd", core_confdef)
|
|
|
|
# Default config. This must validate to continue.
|
|
try:
|
|
core_conf, plugin_classes, plugin_configs = load_config_layer(
|
|
confman, Path(default_config_path).expanduser())
|
|
logging.info(F"Loaded default config from {default_config_path}")
|
|
except:
|
|
logging.error(F"Failed to load default config from {default_config_path}")
|
|
raise
|
|
|
|
# Resolve and freeze local install paths that shouldn't be changed or affect loading custom config
|
|
confman.freeze_value("shepherd", "root_dir")
|
|
confman.freeze_value("shepherd", "plugin_dir")
|
|
confman.freeze_value("shepherd", "custom_config_path")
|
|
confman.freeze_value("shepherd", "generated_config_path")
|
|
resolve_core_conf_paths(core_conf)
|
|
|
|
# Pull out custom config path and save current good root_config
|
|
custom_config_path = core_conf["custom_config_path"]
|
|
saved_root_config = deepcopy(confman.root_config)
|
|
|
|
# Custom config layer. If this fails, maintain default config but continue on to Control layer
|
|
try:
|
|
core_conf, plugin_classes, plugin_configs = load_config_layer(
|
|
confman, custom_config_path)
|
|
logging.info(F"Loaded custom config from {custom_config_path}")
|
|
except Exception as e:
|
|
logging.error(
|
|
F"Failed to load custom config from {custom_config_path}. Falling back to default config.", exc_info=e)
|
|
confman.root_config = saved_root_config
|
|
|
|
# Freeze Shepherd Control related config.
|
|
confman.freeze_value("shepherd", "control_server")
|
|
confman.freeze_value("shepherd", "control_api_key")
|
|
resolve_core_conf_paths(core_conf)
|
|
|
|
# Save current good root_config
|
|
saved_root_config = deepcopy(confman.root_config)
|
|
|
|
# Shepherd Control config layer. If this fails, maintain current local config.
|
|
try:
|
|
control_config = control.get_config(core_conf["root_dir"])
|
|
try:
|
|
core_conf, plugin_classes, plugin_configs = load_config_layer(
|
|
confman, control_config)
|
|
logging.info(F"Loaded cached Shepherd Control config")
|
|
except Exception as e:
|
|
logging.error(
|
|
F"Failed to load cached Shepherd Control config. Falling back to local config.", exc_info=e)
|
|
confman.root_config = saved_root_config
|
|
except:
|
|
logging.warning("No cached Shepherd Control config available.")
|
|
|
|
confman.dump_to_file(core_conf["generated_config_path"])
|
|
|
|
return core_conf, plugin_classes, plugin_configs
|
|
|
|
|
|
@click.group(invoke_without_command = True)
|
|
#help="Path to default config TOML file"
|
|
|
|
@click.argument('default_config', default="shepherd-default.toml", type=click.Path())
|
|
@click.pass_context
|
|
def cli(ctx, default_config):
|
|
"""
|
|
Core service. Expects the default config to be set as an argument.
|
|
"""
|
|
#argparser = argparse.ArgumentParser(description="Keep track of a mob "
|
|
# "of roaming Pis")
|
|
#argparser.add_argument("configfile", nargs='?', metavar="configfile",
|
|
# help="Path to configfile", default="shepherd.toml")
|
|
#argparser.add_argument(
|
|
# '-e', '--noedit', help="Disable the editable config temporarily", action="store_true", default=False)
|
|
#argparser.add_argument("-t", "--test", help="Test and interface function of the from 'plugin:function'",
|
|
# default=None)
|
|
|
|
#args = argparser.parse_args()
|
|
|
|
core_conf, plugin_classes, plugin_configs = compile_config(default_config)
|
|
|
|
if args.test is None:
|
|
control.init_control(core_conf, plugin_configs)
|
|
|
|
scheduler.init_scheduler(core_conf)
|
|
plugin.init_plugins(plugin_classes, plugin_configs, core_conf)
|
|
scheduler.restore_jobs()
|
|
|
|
print(str(datetime.now()))
|
|
|
|
if ctx.invoked_subcommand is not None:
|
|
return
|
|
|
|
print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C'))
|
|
try:
|
|
scheduler.start()
|
|
except (KeyboardInterrupt, SystemExit):
|
|
pass
|
|
|
|
@click.argument('plugin_function')
|
|
@cli.command()
|
|
def test():
|
|
if args.test is not None:
|
|
(test_plugin, test_func) = args.test.split(':')
|
|
func = getattr(shepherd.plugin.plugin_functions[test_plugin], test_func)
|
|
print(func())
|
|
return |