|
|
|
|
@ -3,6 +3,7 @@
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import glob
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
@ -11,12 +12,13 @@ import pkg_resources
|
|
|
|
|
|
|
|
|
|
import chromalog
|
|
|
|
|
import click
|
|
|
|
|
import toml
|
|
|
|
|
|
|
|
|
|
from . import core
|
|
|
|
|
from . import core, plugin
|
|
|
|
|
|
|
|
|
|
chromalog.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
|
|
|
|
# chromalog.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger("shepherd.agent")
|
|
|
|
|
log = logging.getLogger("shepherd.cli")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def echo_heading(title, on_nl=True):
|
|
|
|
|
@ -46,22 +48,25 @@ def echo_section(title, input_text=None, on_nl=True):
|
|
|
|
|
" 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,
|
|
|
|
|
@click.option('-n', '--new-device-mode', 'new_device_mode', 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):
|
|
|
|
|
def cli(ctx, default_config_path, local_operation, only_default_layer, new_device_mode):
|
|
|
|
|
"""
|
|
|
|
|
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}]")
|
|
|
|
|
|
|
|
|
|
agent = core.Agent()
|
|
|
|
|
ctx.ensure_object(SimpleNamespace)
|
|
|
|
|
ctx.obj.agent = agent
|
|
|
|
|
|
|
|
|
|
# Drop down to subcommand if it doesn't need default config file processing
|
|
|
|
|
if ctx.invoked_subcommand == "template":
|
|
|
|
|
return
|
|
|
|
|
@ -84,34 +89,25 @@ def cli(ctx, default_config_path, local_operation, only_default_layer, new_run):
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
# Establish what config layers we're going to try and use
|
|
|
|
|
layers_disabled = []
|
|
|
|
|
control_enabled = True
|
|
|
|
|
use_custom_config = True
|
|
|
|
|
|
|
|
|
|
if local_operation or (ctx.invoked_subcommand == "test"):
|
|
|
|
|
layers_disabled.append("control")
|
|
|
|
|
control_enabled = False
|
|
|
|
|
log.info("Running in local only mode")
|
|
|
|
|
|
|
|
|
|
if only_default_layer:
|
|
|
|
|
layers_disabled.append("custom")
|
|
|
|
|
use_custom_config = False
|
|
|
|
|
|
|
|
|
|
agent = core.Agent(control_enabled)
|
|
|
|
|
agent.load(default_config_path, use_custom_config, new_device_mode)
|
|
|
|
|
agent.load(default_config_path, use_custom_config, control_enabled, 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
|
|
|
|
|
agent.start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
|
@ -119,30 +115,37 @@ def cli(ctx, default_config_path, local_operation, only_default_layer, new_run):
|
|
|
|
|
@click.argument('interface_function', required=False)
|
|
|
|
|
@click.pass_context
|
|
|
|
|
def test(ctx, plugin_name, interface_function):
|
|
|
|
|
echo_heading("Shepherd Test")
|
|
|
|
|
agent = ctx.obj.agent
|
|
|
|
|
plugin_configs = agent.applied_config.copy()
|
|
|
|
|
del plugin_configs['shepherd']
|
|
|
|
|
|
|
|
|
|
echo_heading("Shepherd - Test")
|
|
|
|
|
|
|
|
|
|
if not plugin_name:
|
|
|
|
|
log.info("Test initialisation of all plugins in config...")
|
|
|
|
|
|
|
|
|
|
echo_section("Plugins loaded:")
|
|
|
|
|
if len(ctx.obj.plugin_configs) == 0:
|
|
|
|
|
if len(plugin_configs) == 0:
|
|
|
|
|
click.echo("---none---")
|
|
|
|
|
for plugin_name, config in ctx.obj.plugin_configs.items():
|
|
|
|
|
for plugin_name, config in plugin_configs.items():
|
|
|
|
|
click.secho(F" {plugin_name}", fg='green')
|
|
|
|
|
|
|
|
|
|
echo_section("Core config:")
|
|
|
|
|
pprint(ctx.obj.core_config)
|
|
|
|
|
print(toml.dumps(agent.core_config))
|
|
|
|
|
# pprint(agent.core_config)
|
|
|
|
|
|
|
|
|
|
echo_section("Plugin configs:")
|
|
|
|
|
if len(ctx.obj.plugin_configs) == 0:
|
|
|
|
|
if len(plugin_configs) == 0:
|
|
|
|
|
click.echo("---none---")
|
|
|
|
|
for name, config in ctx.obj.plugin_configs.items():
|
|
|
|
|
for name, config in plugin_configs.items():
|
|
|
|
|
click.secho(F" {plugin_name}", fg='green')
|
|
|
|
|
pprint(config)
|
|
|
|
|
print(toml.dumps(config))
|
|
|
|
|
# pprint(config)
|
|
|
|
|
|
|
|
|
|
click.echo("")
|
|
|
|
|
|
|
|
|
|
log.info("Initialising plugins...")
|
|
|
|
|
plugin.init_plugins(ctx.obj.plugin_configs, ctx.obj.core_config)
|
|
|
|
|
plugin.init_plugins(agent.applied_config)
|
|
|
|
|
log.info("Plugin initialisation done")
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
@ -150,34 +153,61 @@ def test(ctx, plugin_name, interface_function):
|
|
|
|
|
echo_section("Target plugin:", input_text=plugin_name, on_nl=False)
|
|
|
|
|
# TODO find plugin dependancies
|
|
|
|
|
|
|
|
|
|
if plugin_name not in ctx.obj.plugin_configs:
|
|
|
|
|
if plugin_name not in 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])
|
|
|
|
|
print(toml.dumps(plugin_configs[plugin_name]))
|
|
|
|
|
# pprint(plugin_configs[plugin_name])
|
|
|
|
|
|
|
|
|
|
interface = plugin.load_plugin(plugin_name)
|
|
|
|
|
|
|
|
|
|
if not interface_function:
|
|
|
|
|
echo_section(F"Interface functions [{plugin_name}]:")
|
|
|
|
|
echo_section(F"Interface functions [{plugin_name}]:", on_nl=False)
|
|
|
|
|
|
|
|
|
|
for name, func in interface._functions.items():
|
|
|
|
|
click.echo(F" {name}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
echo_section("Target interface function:", input_text=interface_function)
|
|
|
|
|
echo_section("Target interface function:", input_text=interface_function, on_nl=False)
|
|
|
|
|
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)
|
|
|
|
|
plugin.init_plugins({plugin_name: plugin_configs[plugin_name]})
|
|
|
|
|
log.info("Plugin initialisation done")
|
|
|
|
|
|
|
|
|
|
interface._functions[interface_function]()
|
|
|
|
|
# TODO look for a spec on the interface function, and parse cmdline values if it's there
|
|
|
|
|
|
|
|
|
|
print(interface._functions[interface_function]())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BlankEncoder(toml.TomlEncoder):
|
|
|
|
|
"""
|
|
|
|
|
A TOML encoder that emit empty keys (values of None). This isn't valid TOML,
|
|
|
|
|
but is useful for generating templates.
|
|
|
|
|
"""
|
|
|
|
|
class BlankValue:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def __init__(self, _dict=dict, preserve=False):
|
|
|
|
|
super().__init__(_dict, preserve)
|
|
|
|
|
self.dump_funcs[self.BlankValue] = lambda v: ''
|
|
|
|
|
|
|
|
|
|
def dump_sections(self, o, sup):
|
|
|
|
|
for section in o:
|
|
|
|
|
if o[section] is None:
|
|
|
|
|
o[section] = self.BlankValue()
|
|
|
|
|
return super().dump_sections(o, sup)
|
|
|
|
|
|
|
|
|
|
def dump_value(self, v):
|
|
|
|
|
if v is None:
|
|
|
|
|
v = self.BlankValue()
|
|
|
|
|
return super().dump_value(v)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
|
@ -199,13 +229,17 @@ def template(ctx, plugin_name, include_all, config_path, plugin_dir):
|
|
|
|
|
a new file (if it doesn't yet exist).
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
agent = ctx.obj.agent
|
|
|
|
|
|
|
|
|
|
echo_heading("Shepherd - Template")
|
|
|
|
|
|
|
|
|
|
if not plugin_dir:
|
|
|
|
|
plugin_dir = Path.cwd()
|
|
|
|
|
|
|
|
|
|
confspec = ConfigSpecification()
|
|
|
|
|
confspec = None
|
|
|
|
|
if (not plugin_name) or (plugin_name == "shepherd"):
|
|
|
|
|
plugin_name = "shepherd"
|
|
|
|
|
confspec = core_confspec()
|
|
|
|
|
confspec = agent.core_interface.confspec
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
plugin_interface = plugin.load_plugin(plugin_name, plugin_dir)
|
|
|
|
|
@ -215,9 +249,16 @@ def template(ctx, plugin_name, include_all, config_path, plugin_dir):
|
|
|
|
|
confspec = plugin_interface.confspec
|
|
|
|
|
|
|
|
|
|
template_dict = confspec.get_template(include_all)
|
|
|
|
|
template_toml = toml.dumps({plugin_name: template_dict})
|
|
|
|
|
template_toml = toml.dumps({plugin_name: template_dict}, encoder=BlankEncoder())
|
|
|
|
|
|
|
|
|
|
log.info(F"Config template for [{plugin_name}]: \n\n"+template_toml)
|
|
|
|
|
if include_all:
|
|
|
|
|
log.info("Including all optional fields")
|
|
|
|
|
else:
|
|
|
|
|
log.info("Including required fields only")
|
|
|
|
|
|
|
|
|
|
echo_section("Config template for", input_text=F"[{plugin_name}]")
|
|
|
|
|
click.echo("")
|
|
|
|
|
click.echo(template_toml)
|
|
|
|
|
|
|
|
|
|
if not config_path:
|
|
|
|
|
# reuse parent "-c" for convenience
|
|
|
|
|
|