Break cli out from core

master
Tom Wilson 6 years ago
parent 199acb7e3b
commit c264531c68

@ -0,0 +1,255 @@
import logging
import os
import sys
import glob
from types import SimpleNamespace
from datetime import datetime
from pprint import pprint
import pkg_resources
import chromalog
import click
from . import core
chromalog.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
log = logging.getLogger("shepherd.agent")
def echo_heading(title, on_nl=True):
if on_nl:
click.echo("")
click.echo(click.style(".: ", fg='blue', bold=True) +
click.style(title, fg='white', bold=True) +
click.style(" :.", fg='blue', bold=True))
def echo_section(title, input_text=None, on_nl=True):
if on_nl:
click.echo("")
click.secho(":: ", bold=True, fg='blue', nl=False)
click.secho(title, bold=True, nl=False)
if input_text:
click.secho(F" {input_text}", fg='green', bold=True, nl=False)
click.echo("")
@click.group(invoke_without_command=True)
@click.option('-c', '--config', 'default_config_path', type=click.Path(),
help="Shepherd config TOML file to be used as default config layer."
" Overrides default './shepherd*.toml' search")
@click.option('-l', '--local', 'local_operation', is_flag=True,
help="Only use the local config layers (default and custom), and disable all"
" Shepherd Control remote features")
@click.option('-d', '--default-config-only', 'only_default_layer', is_flag=True,
help="Ignore the custom config layer (still uses the Control config above that)")
@click.option('-n', '--new', 'new_run', is_flag=True,
help="Clear existing device identity and cached Shepherd Control config layer."
" Also triggered by the presence of a shepherd.new file in the"
" same directory as the custom config layer file.")
@click.pass_context
def cli(ctx, default_config_path, local_operation, only_default_layer, new_run):
"""
Core service. If default config file is not provided with '-c' option, the first filename
in the current working directory beginning with "shepherd" and
ending with ".toml" will be used.
"""
ctx.ensure_object(SimpleNamespace)
version_text = pkg_resources.get_distribution("shepherd")
log.info(F"Shepherd Agent [{version_text}]")
# Drop down to subcommand if it doesn't need default config file processing
if ctx.invoked_subcommand == "template":
return
# Get a default config path to use
if not default_config_path:
default_config_path = sorted(glob.glob("./shepherd*.toml"))[:1]
if default_config_path:
default_config_path = default_config_path[0]
log.info(F"No default config file provided, found {default_config_path}")
with open(default_config_path, 'r+') as f:
content = f.read()
if "Compiled Shepherd config" in content:
log.warning("Default config file looks like it is full compiled config"
" file generated by Shepherd and picked up due to accidental"
" name match")
else:
log.error("No default config file provided, and no 'shepherd*.toml' could be"
" found in the current directory")
sys.exit(1)
# Establish what config layers we're going to try and use
layers_disabled = []
if local_operation or (ctx.invoked_subcommand == "test"):
layers_disabled.append("control")
log.info("Running in local only mode")
if only_default_layer:
layers_disabled.append("custom")
agent = core.Agent(control_enabled)
agent.load(default_config_path, use_custom_config, new_device_mode)
# Drop down to subcommands that needed a config compiled
if ctx.invoked_subcommand == "test":
ctx.obj.agent = agent
return
agent.start()
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
@cli.command()
@click.argument('plugin_name', required=False)
@click.argument('interface_function', required=False)
@click.pass_context
def test(ctx, plugin_name, interface_function):
echo_heading("Shepherd Test")
if not plugin_name:
echo_section("Plugins loaded:")
if len(ctx.obj.plugin_configs) == 0:
click.echo("---none---")
for plugin_name, config in ctx.obj.plugin_configs.items():
click.secho(F" {plugin_name}", fg='green')
echo_section("Core config:")
pprint(ctx.obj.core_config)
echo_section("Plugin configs:")
if len(ctx.obj.plugin_configs) == 0:
click.echo("---none---")
for name, config in ctx.obj.plugin_configs.items():
click.secho(F" {plugin_name}", fg='green')
pprint(config)
click.echo("")
log.info("Initialising plugins...")
plugin.init_plugins(ctx.obj.plugin_configs, ctx.obj.core_config)
log.info("Plugin initialisation done")
return
echo_section("Target plugin:", input_text=plugin_name, on_nl=False)
# TODO find plugin dependancies
if plugin_name not in ctx.obj.plugin_configs:
log.error(F"Supplied plugin name '{plugin_name}' is not loaded"
" (not present in config)")
sys.exit(1)
echo_section(F"Config [{plugin_name}]:")
pprint(ctx.obj.plugin_configs[plugin_name])
interface = plugin.load_plugin(plugin_name)
if not interface_function:
echo_section(F"Interface functions [{plugin_name}]:")
for name, func in interface._functions.items():
click.echo(F" {name}")
return
echo_section("Target interface function:", input_text=interface_function)
if interface_function not in interface._functions:
log.error(F"Supplied interface function name '{interface_function}' is not present in"
F" plugin {plugin_name}")
sys.exit(1)
log.info("Initialising plugins...")
plugin.init_plugins({plugin_name: ctx.obj.plugin_configs[plugin_name]}, ctx.obj.core_config)
log.info("Plugin initialisation done")
interface._functions[interface_function]()
@cli.command()
@click.argument('plugin_name', required=False)
@click.option('-a', '--include-all', is_flag=True,
help="Include all optional fields in the template")
@click.option('-c', '--config', 'config_path', type=click.Path(),
help="Path to append or create config tempalate")
@click.option('-d', '--plugin-dir', type=click.Path(),
help="Directory to search for plugin modules, in addition to built in Shepherd"
" plugins and the global import path. Defaults to current directory.")
@click.pass_context
def template(ctx, plugin_name, include_all, config_path, plugin_dir):
"""
Generate a template config TOML file for PLUGIN_NAME, or for the Shepherd core if
PLUGIN_NAME is not provided.
If config path is provided ("-c"), append to that file (if it exists) or write to
a new file (if it doesn't yet exist).
"""
if not plugin_dir:
plugin_dir = Path.cwd()
confspec = ConfigSpecification()
if (not plugin_name) or (plugin_name == "shepherd"):
plugin_name = "shepherd"
confspec = core_confspec()
else:
try:
plugin_interface = plugin.load_plugin(plugin_name, plugin_dir)
except plugin.PluginLoadError as e:
log.error(e.args[0])
sys.exit(1)
confspec = plugin_interface.confspec
template_dict = confspec.get_template(include_all)
template_toml = toml.dumps({plugin_name: template_dict})
log.info(F"Config template for [{plugin_name}]: \n\n"+template_toml)
if not config_path:
# reuse parent "-c" for convenience
config_path = ctx.parent.params["default_config_path"]
if not config_path:
return
if Path(config_path).is_file():
try:
existing_config = toml.load(config_path)
except Exception:
click.confirm(
F"File {config_path} already exists and is not a valid TOML file. Overwrite?",
default=True, abort=True)
click.echo(F"Writing [{plugin_name}] template to {config_path}")
with open(config_path, 'w+') as f:
f.write(template_toml)
else:
if plugin_name in existing_config:
click.confirm(F"Overwrite [{plugin_name}] section in {config_path}?",
default=True, abort=True)
click.echo(F"Overwriting [{plugin_name}] section in {config_path}")
else:
click.confirm(F"Add [{plugin_name}] section to {config_path}?",
default=True, abort=True)
click.echo(F"Adding [{plugin_name}] section to {config_path}")
existing_config[plugin_name] = template_dict
with open(config_path, 'w+') as f:
f.write(toml.dumps(existing_config))
else:
click.echo(F"Writing [{plugin_name}] template to {config_path}")
with open(config_path, 'w+') as f:
f.write(template_toml)

@ -3,303 +3,125 @@ Core shepherd module, tying together main service functionality. Provides main C
"""
import os
import sys
from pathlib import Path
import glob
from copy import deepcopy
from datetime import datetime
from types import SimpleNamespace
from pprint import pprint
import logging
import chromalog
import pkg_resources
import click
import os
from configspec import *
from . import scheduler
from . import plugin
from . import control
chromalog.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
log = logging.getLogger("shepherd.agent")
def echo_heading(title, on_nl=True):
if on_nl:
click.echo("")
click.echo(click.style(".: ", fg='blue', bold=True) +
click.style(title, fg='white', bold=True) +
click.style(" :.", fg='blue', bold=True))
def echo_section(title, input_text=None, on_nl=True):
if on_nl:
click.echo("")
click.secho(":: ", bold=True, fg='blue', nl=False)
click.secho(title, bold=True, nl=False)
if input_text:
click.secho(F" {input_text}", fg='green', bold=True, nl=False)
click.echo("")
@click.group(invoke_without_command=True)
@click.option('-c', '--config', 'default_config_path', type=click.Path(),
help="Shepherd config TOML file to be used as default config layer."
" Overrides default './shepherd*.toml' search")
@click.option('-l', '--local', 'local_operation', is_flag=True,
help="Only use the local config layers (default and custom), and disable all"
" Shepherd Control remote features")
@click.option('-d', '--default-config-only', 'only_default_layer', is_flag=True,
help="Ignore the custom config layer (still uses the Control config above that)")
@click.option('-n', '--new', 'new_run', is_flag=True,
help="Clear existing device identity and cached Shepherd Control config layer."
" Also triggered by the presence of a shepherd.new file in the"
" same directory as the custom config layer file.")
@click.pass_context
def cli(ctx, default_config_path, local_operation, only_default_layer, new_run):
class Agent():
"""
Core service. If default config file is not provided with '-c' option, the first filename
in the current working directory beginning with "shepherd" and
ending with ".toml" will be used.
Holds the main state required to run Shepherd Agent
"""
ctx.ensure_object(SimpleNamespace)
version_text = pkg_resources.get_distribution("shepherd")
log.info(F"Shepherd Agent [{version_text}]")
if ctx.invoked_subcommand == "template":
return
if not default_config_path:
default_config_path = sorted(glob.glob("./shepherd*.toml"))[:1]
if default_config_path:
default_config_path = default_config_path[0]
log.info(F"No default config file provided, found {default_config_path}")
with open(default_config_path, 'r+') as f:
content = f.read()
if "Compiled Shepherd config" in content:
log.warning("Default config file looks like it is full compiled config"
" file generated by Shepherd and picked up due to accidental"
" name match")
def __init__(self, control_enabled=True):
# The config defined by the device (everything before the Control layer)
self.local_config = None
# The config actually being used
self.applied_config = None
# Split the applied_config up into core and plugins
self.core_config = None
self.plugin_configs = None
self.interface_functions = None
self.control_enabled = control_enabled
def load(self, default_config_path, use_custom_config=True, new_device_mode=False):
"""
Load in the Shepherd Agent config and associated plugins.
Args:
default_config_path: The path to the default config file
use_custom_config: Set False to disable the local custom config layer
control_enabled: Set False to disable Shepherd Control remote management
(including any cached Control config layer)
new_device_mode: Set True to clear out any cached state and trigger new generation
of ID, as if it were being run on a fresh system.
"""
# Compile the config layers
confman = ConfigManager()
compile_local_config(confman, default_config_path, use_custom_config)
self.local_config = deepcopy(confman.get_config_bundles())
# Check for new device mode
core_conf = confman.get_config_bundle('shepherd')
if new_device_mode or check_new_device_file(core_conf["custom_config_path"]):
log.info("'new device' mode enabled, clearing old state...")
control.generate_device_identity(core_conf["root_dir"])
control.clear_cached_config(core_conf["root_dir"])
if self.control_enabled:
compile_remote_config(confman)
else:
log.error("No default config file provided, and no 'shepherd*.toml' could be"
" found in the current directory")
sys.exit(1)
layers_disabled = []
if local_operation or (ctx.invoked_subcommand == "test"):
layers_disabled.append("control")
log.info("Running in local only mode")
if only_default_layer:
layers_disabled.append("custom")
confman = ConfigManager()
compile_config(confman, default_config_path, layers_disabled, new_run)
plugin_configs = confman.get_config_bundles()
del plugin_configs["shepherd"]
core_config = confman.get_config_bundle("shepherd")
if ctx.invoked_subcommand == "test":
ctx.obj.plugin_configs = plugin_configs
ctx.obj.core_config = core_config
return
applied_config = confman.get_config_bundles()
# Not part of normal ConfigManager, we just saved this here to pass it to Control
local_config = confman.saved_local_config
core_update_state = control.CoreUpdateState(local_config, applied_config)
plugin_update_states = {name: iface._update_state
for name, iface in plugin.plugin_interfaces.items()}
if core_config["control"] is not None:
control.init_control(core_config["control"], core_config["root_dir"],
core_update_state, plugin_update_states)
else:
log.warning("Shepherd control config section not present. Will not attempt to connect to"
" Shepherd Control server.")
scheduler.init_scheduler(core_config)
plugin.init_plugins(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
@cli.command()
@click.argument('plugin_name', required=False)
@click.argument('interface_function', required=False)
@click.pass_context
def test(ctx, plugin_name, interface_function):
echo_heading("Shepherd Test")
if not plugin_name:
echo_section("Plugins loaded:")
if len(ctx.obj.plugin_configs) == 0:
click.echo("---none---")
for plugin_name, config in ctx.obj.plugin_configs.items():
click.secho(F" {plugin_name}", fg='green')
echo_section("Core config:")
pprint(ctx.obj.core_config)
echo_section("Plugin configs:")
if len(ctx.obj.plugin_configs) == 0:
click.echo("---none---")
for name, config in ctx.obj.plugin_configs.items():
click.secho(F" {plugin_name}", fg='green')
pprint(config)
click.echo("")
log.info("Shepherd Control config layer disabled")
log.info("Initialising plugins...")
plugin.init_plugins(ctx.obj.plugin_configs, ctx.obj.core_config)
log.info("Plugin initialisation done")
self.applied_config = confman.get_config_bundles()
self.plugin_configs = confman.get_config_bundles()
self.core_config = self.plugin_configs.pop('shepherd')
return
echo_section("Target plugin:", input_text=plugin_name, on_nl=False)
# TODO find plugin dependancies
if plugin_name not in ctx.obj.plugin_configs:
log.error(F"Supplied plugin name '{plugin_name}' is not loaded"
" (not present in config)")
sys.exit(1)
echo_section(F"Config [{plugin_name}]:")
pprint(ctx.obj.plugin_configs[plugin_name])
interface = plugin.load_plugin(plugin_name)
log.debug("Compiled config: %s", confman.root_config)
if core_conf["compiled_config_path"]:
message = F"Compiled Shepherd config at {datetime.now()}"
confman.dump_to_file(core_conf["compiled_config_path"], message=message)
log.info(F"Saved compiled config to {core_conf['compiled_config_path']}")
if not interface_function:
echo_section(F"Interface functions [{plugin_name}]:")
def start(self):
for name, func in interface._functions.items():
click.echo(F" {name}")
return
plugin_interfaces, self.interface_functions = plugin.init_plugins(
self.plugin_configs, self.core_config, {})
echo_section("Target interface function:", input_text=interface_function)
if interface_function not in interface._functions:
log.error(F"Supplied interface function name '{interface_function}' is not present in"
F" plugin {plugin_name}")
sys.exit(1)
log.info("Initialising plugins...")
plugin.init_plugins({plugin_name: ctx.obj.plugin_configs[plugin_name]}, ctx.obj.core_config)
log.info("Plugin initialisation done")
interface._functions[interface_function]()
@cli.command()
@click.argument('plugin_name', required=False)
@click.option('-a', '--include-all', is_flag=True,
help="Include all optional fields in the template")
@click.option('-c', '--config', 'config_path', type=click.Path(),
help="Path to append or create config tempalate")
@click.option('-d', '--plugin-dir', type=click.Path(),
help="Directory to search for plugin modules, in addition to built in Shepherd"
" plugins and the global import path. Defaults to current directory.")
@click.pass_context
def template(ctx, plugin_name, include_all, config_path, plugin_dir):
"""
Generate a template config TOML file for PLUGIN_NAME, or for the Shepherd core if
PLUGIN_NAME is not provided.
cmd_runner = control.CommandRunner(self.interface_functions)
core_update_state = control.CoreUpdateState(cmd_runner.cmd_reader,
cmd_runner.cmd_result_writer)
core_update_state.set_static_state(self.local_config, self.applied_config, core_confspec())
If config path is provided ("-c"), append to that file (if it exists) or write to
a new file (if it doesn't yet exist).
"""
plugin_update_states = {name: iface._update_state
for name, iface in plugin_interfaces.items()}
if not plugin_dir:
plugin_dir = Path.cwd()
confspec = ConfigSpecification()
if (not plugin_name) or (plugin_name == "shepherd"):
plugin_name = "shepherd"
confspec = core_confspec()
else:
try:
plugin_interface = plugin.load_plugin(plugin_name, plugin_dir)
except plugin.PluginLoadError as e:
log.error(e.args[0])
sys.exit(1)
confspec = plugin_interface.confspec
if self.control_enabled:
if self.core_config["control"] is not None:
control.start_control(self.core_config["control"], self.core_config["root_dir"],
core_update_state, plugin_update_states)
else:
log.warning("Shepherd control config section not present. Will not attempt to"
" connect to Shepherd Control server.")
template_dict = confspec.get_template(include_all)
template_toml = toml.dumps({plugin_name: template_dict})
# tasks.init_tasks(self.core_config) # seperate tasks.start?
log.info(F"Config template for [{plugin_name}]: \n\n"+template_toml)
# plugin.start() # Run the plugin `.run` hooks in seperate threads
if not config_path:
# reuse parent "-c" for convenience
config_path = ctx.parent.params["default_config_path"]
# scheduler.restore_jobs()
if not config_path:
return
if Path(config_path).is_file():
try:
existing_config = toml.load(config_path)
except Exception:
click.confirm(
F"File {config_path} already exists and is not a valid TOML file. Overwrite?",
default=True, abort=True)
click.echo(F"Writing [{plugin_name}] template to {config_path}")
with open(config_path, 'w+') as f:
f.write(template_toml)
else:
if plugin_name in existing_config:
click.confirm(F"Overwrite [{plugin_name}] section in {config_path}?",
default=True, abort=True)
click.echo(F"Overwriting [{plugin_name}] section in {config_path}")
else:
click.confirm(F"Add [{plugin_name}] section to {config_path}?",
default=True, abort=True)
click.echo(F"Adding [{plugin_name}] section to {config_path}")
existing_config[plugin_name] = template_dict
with open(config_path, 'w+') as f:
f.write(toml.dumps(existing_config))
else:
click.echo(F"Writing [{plugin_name}] template to {config_path}")
with open(config_path, 'w+') as f:
f.write(template_toml)
def check_new_run_file(custom_config_path):
def check_new_device_file(custom_config_path):
if not custom_config_path:
return False
trigger_path = Path(Path(custom_config_path).parent, 'shepherd.new')
if trigger_path.exists():
trigger_path.unlink()
log.info("'shepherd.new' file detected, removing file and clearing old state...")
log.info("'shepherd.new' file detected, removing file and"
" triggering 'new device' mode")
return True
return False
def compile_config(confman, default_config_path, layers_disabled, new_run):
def compile_local_config(confman, default_config_path, use_custom_config):
"""
Run through the process of assembling the various config layers, falling back to working
ones where necessary. As part of this, loads in the required plugins.
Load the default config and optionally try to overlay the custom config layer.
As part of this, load the required plugins into cache (required to validate their config).
"""
confman.add_confspec("shepherd", core_confspec())
@ -316,7 +138,7 @@ def compile_config(confman, default_config_path, layers_disabled, new_run):
F" {chr(10).join(e.args)}")
else:
log.error(F"Failed to load default config from {default_config_path}", exc_info=True)
sys.exit(1)
raise
# Resolve and freeze local install paths that shouldn't be changed from default config
core_conf = confman.get_config_bundle("shepherd")
@ -334,96 +156,90 @@ def compile_config(confman, default_config_path, layers_disabled, new_run):
log.warning("Custom plugin path is empty, won't load custom plugins")
# ====Custom Local Config Layer====
# If this fails, maintain default config but continue on to Control layer
if "custom" in layers_disabled:
# If this fails, fallback to default config
if not use_custom_config:
log.info("Custom config layer disabled")
elif not custom_config_path:
return
if not custom_config_path:
log.warning("Custom config path is empty, skipping custom config layer")
else:
try:
load_config_layer_and_plugins(confman, custom_config_path)
log.info(F"Loaded custom config layer from {custom_config_path}")
except Exception as e:
if isinstance(e, InvalidConfigError):
log.error(F"Failed to load custom config layer from {custom_config_path}."
F" {e.args[0]}")
else:
log.error(F"Failed to load custom config layer from {custom_config_path}.",
exc_info=True)
log.warning("Falling back to default config.")
confman.fallback()
return
try:
load_config_layer_and_plugins(confman, custom_config_path)
log.info(F"Loaded custom config layer from {custom_config_path}")
except Exception as e:
if isinstance(e, InvalidConfigError):
log.error(F"Failed to load custom config layer from {custom_config_path}."
F" {e.args[0]}")
else:
log.error(F"Failed to load custom config layer from {custom_config_path}.",
exc_info=True)
log.warning("Falling back to default config.")
confman.fallback()
def compile_remote_config(confman):
"""
Attempt to load and apply the Shepherd Control config layer (cached from prior communication,
Control hasn't actually started up yet). Falls back to previous config if it fails.
As part of this, load the required plugins into cache (required to validate their config).
"""
# ====Control Remote Config Layer====
# Freeze Shepherd Control related config.
core_conf = confman.get_config_bundle("shepherd")
resolve_core_conf_paths(core_conf, default_config_path.parent)
confman.freeze_value("shepherd", "control_server")
confman.freeze_value("shepherd", "control_api_key")
# Save current good local config
confman.save_fallback()
# Tuck it away so we can pass the local config to Control
confman.saved_local_config = deepcopy(confman.get_config_bundles())
if check_new_run_file(custom_config_path) or new_run:
if new_run:
log.info("'new run' selected, clearing old state...")
control.generate_device_identity(core_conf["root_dir"])
control.clear_cached_config(core_conf["root_dir"])
# ====Control Remote Config Layer====
# If this fails, maintain current local config.
if "control" not in layers_disabled:
core_conf = confman.get_config_bundle("shepherd")
try:
control_config = control.get_cached_config(core_conf["root_dir"])
try:
control_config = control.get_cached_config(core_conf["root_dir"])
try:
load_config_layer_and_plugins(confman, control_config)
log.info(F"Loaded cached Shepherd Control config layer")
except Exception as e:
if isinstance(e, InvalidConfigError):
log.error(F"Failed to load cached Shepherd Control config layer."
F" {e.args[0]}")
else:
log.error(F"Failed to load cached Shepherd Control config layer.",
exc_info=True)
log.warning("Falling back to local config.")
confman.fallback()
except Exception:
log.warning("No cached Shepherd Control config layer available.")
else:
log.info("Shepherd Control config layer disabled")
log.debug("Compiled config: %s", confman.root_config)
if core_conf["compiled_config_path"]:
message = F"Compiled Shepherd config at {datetime.now()}"
confman.dump_to_file(core_conf["compiled_config_path"], message=message)
log.info(F"Saved compiled config to {core_conf['compiled_config_path']}")
load_config_layer_and_plugins(confman, control_config)
log.info(F"Loaded cached Shepherd Control config layer")
except Exception as e:
if isinstance(e, InvalidConfigError):
log.error(F"Failed to load cached Shepherd Control config layer."
F" {e.args[0]}")
else:
log.error(F"Failed to load cached Shepherd Control config layer.",
exc_info=True)
log.warning("Falling back to local config.")
confman.fallback()
except Exception:
log.warning("No cached Shepherd Control config layer available.")
# Relative pathnames here are all relative to "root_dir"
# Relative pathnames here are all relative to "root_dir". `root_dir` itself is relative to
# the directory the default config is loaded from
def core_confspec():
"""
Returns the core config specification
"""
confspec = ConfigSpecification()
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=False,
helptext="Operating directory for shepherd to place"
" working files.")),
("custom_config_path", StringSpec(optional=True, helptext="Path to custom config"
" layer TOML file.")),
("compiled_config_path", StringSpec(default="compiled-config.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", control.control_confspec())
confspec.add_specs({
"name": StringSpec(helptext="Identifying name for this device"),
})
confspec.add_specs(optional=True, spec_dict={
"root_dir":
(StringSpec(helptext="Operating directory for shepherd to place working files."),
"./shepherd"),
"custom_config_path":
StringSpec(helptext="Path to custom config layer TOML file."),
"compiled_config_path":
(StringSpec(helptext="Path to custom file Shepherd will generate to show compiled"
" config that was used and any errors in validation."),
"compiled-config.toml"),
"plugin_dir":
(StringSpec(helptext="Optional directory for Shepherd to look for plugins in."),
"./shepherd-plugins")
})
confspec.add_spec("control", control.control_confspec(), optional=True)
return confspec

Loading…
Cancel
Save