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.
shepherd-agent/shepherd/core.py

207 lines
8.0 KiB

"""
Core shepherd module, tying together main service functionality.
"""
import os
from pathlib import Path
from datetime import datetime
import logging
import pkg_resources
import click
from configspec import *
from . import scheduler
from . import plugin
from . import control
@click.group(invoke_without_command=True)
#help="Path to default config TOML file"
@click.argument('default_config_path', required=False, type=click.Path())
@click.pass_context
def cli(ctx, default_config_path):
"""
Core service. Expects the default config to be set as an argument.
"""
version_text = pkg_resources.get_distribution("shepherd")
logging.info(F"Initialising Shepherd Agent {version_text}")
# 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()
confman = ConfigManager()
plugin_classes = compile_config_and_get_plugins(confman, default_config_path)
control.init_control(core_conf, plugin_configs)
scheduler.init_scheduler(core_conf)
plugin.init_plugins(plugin_classes, plugin_configs, core_config)
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
def compile_config_and_get_plugins(confman, 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 = ConfigSpecification()
define_core_config(core_confdef)
confman.add_confspec("shepherd", core_confdef)
# ====Default Local Config Layer====
# This must validate to continue.
default_config_path = Path(default_config_path).expanduser()
try:
plugin_classes = load_config_layer_and_plugins(confman, default_config_path)
logging.info(F"Loaded default config layer from {default_config_path}")
except Exception:
logging.error(F"Failed to load default config from {default_config_path}")
raise
# Resolve and freeze local install paths that shouldn't be changed from default config
core_conf = confman.get_config_bundle("shepherd")
resolve_core_conf_paths(core_conf)
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")
# Pull out custom config path and save current good config
custom_config_path = core_conf["custom_config_path"]
confman.save_fallback()
# ====Custom Local Config Layer====
# If this fails, maintain default config but continue on to Control layer
try:
plugin_classes = load_config_layer_and_plugins(confman, custom_config_path)
logging.info(F"Loaded custom config layer from {custom_config_path}")
except Exception as e:
logging.error(
F"Failed to load custom config layer from {custom_config_path}. Falling back"
" to default config.", exc_info=e)
confman.fallback()
# Freeze Shepherd Control related config.
core_conf = confman.get_config_bundle("shepherd")
resolve_core_conf_paths(core_conf)
confman.freeze_value("shepherd", "control_server")
confman.freeze_value("shepherd", "control_api_key")
# Save current good config
confman.save_fallback()
# ====Control Remote Config Layer====
# If this fails, maintain current local config.
try:
control_config = control.get_config(core_conf["root_dir"])
try:
plugin_classes = load_config_layer_and_plugins(confman, control_config)
logging.info(F"Loaded cached Shepherd Control config layer")
except Exception as e:
logging.error(
F"Failed to load cached Shepherd Control config layer. Falling back"
" to local config.", exc_info=e)
confman.fallback()
except Exception:
logging.warning("No cached Shepherd Control config layer available.")
logging.debug("Compiled config: %s", confman.root_config)
confman.dump_to_file(core_conf["generated_config_path"])
return plugin_classes
# Relative pathnames here are all relative to "root_dir"
def define_core_config(confspec):
"""
Defines the config definition by populating the ConfigSpecification passed in
``confspec`` - the same pattern plugins use
"""
confspec.add_specs([
("name", StringSpec(helptext="Identifying name for this device")),
("hostname", StringSpec(default="", optional=True, helptext="If set, changes the system"
" hostname")),
("plugin_dir", StringSpec(default="~/shepherd-plugins", optional=True,
helptext="Optional directory for Shepherd to look for"
" plugins in.")),
("root_dir", StringSpec(default="~/shepherd", optional=True,
helptext="Operating directory for shepherd to place"
" working files.")),
("custom_config_path", StringSpec(optional=True, helptext="Path to custom config"
" layer TOML file.")),
("generated_config_path", StringSpec(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."))
])
confspec.add_spec("control_server", StringSpec())
confspec.add_spec("control_api_key", StringSpec())
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_and_plugins(confman, config_source):
"""
Load a config layer, find the necessary plugin classes, then validate it.
If this succeeds, the returned dict of plugin classes will directly match
the bundle names in the config manager.
"""
# 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 bundle names to get plugins we need to load
plugin_names = confman.get_bundle_names()
plugin_names.remove("shepherd")
# Load plugins to get their config specifications
plugin_classes = plugin.find_plugins(plugin_names, plugin_dir)
for plugin_name, plugin_class in plugin_classes.items():
new_conf_spec = ConfigSpecification()
plugin_class.define_config(new_conf_spec)
confman.add_confspec(plugin_name, new_conf_spec)
# Validate all plugin configs
confman.validate_bundles()
return plugin_classes