|
|
|
@ -1,5 +1,6 @@
|
|
|
|
import threading
|
|
|
|
import threading
|
|
|
|
import secrets
|
|
|
|
import secrets
|
|
|
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from pathlib import Path
|
|
|
|
from pathlib import Path
|
|
|
|
from urllib.parse import urlparse, urlunparse, urljoin
|
|
|
|
from urllib.parse import urlparse, urlunparse, urljoin
|
|
|
|
from hashlib import blake2b
|
|
|
|
from hashlib import blake2b
|
|
|
|
@ -10,6 +11,15 @@ import requests
|
|
|
|
from configspec import *
|
|
|
|
from configspec import *
|
|
|
|
import statesman
|
|
|
|
import statesman
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Namespace of types intended for server-side use.
|
|
|
|
|
|
|
|
def get_export():
|
|
|
|
|
|
|
|
from . import plugin
|
|
|
|
|
|
|
|
export = SimpleNamespace()
|
|
|
|
|
|
|
|
export.InterfaceCall = plugin.InterfaceCall
|
|
|
|
|
|
|
|
return export
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger("shepherd.agent.control")
|
|
|
|
log = logging.getLogger("shepherd.agent.control")
|
|
|
|
|
|
|
|
|
|
|
|
_control_update_required = threading.Condition()
|
|
|
|
_control_update_required = threading.Condition()
|
|
|
|
@ -24,7 +34,7 @@ def control_confspec():
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Returns the control config specification
|
|
|
|
Returns the control config specification
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
confspec = ConfigSpecification(optional=True)
|
|
|
|
confspec = ConfigSpecification()
|
|
|
|
confspec.add_spec("server", StringSpec())
|
|
|
|
confspec.add_spec("server", StringSpec())
|
|
|
|
confspec.add_spec("intro_key", StringSpec())
|
|
|
|
confspec.add_spec("intro_key", StringSpec())
|
|
|
|
|
|
|
|
|
|
|
|
@ -32,31 +42,73 @@ def control_confspec():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CoreUpdateState():
|
|
|
|
class CoreUpdateState():
|
|
|
|
def __init__(self, local_config, applied_config):
|
|
|
|
def __init__(self, cmd_reader, cmd_result_writer):
|
|
|
|
self.topic_bundle = statesman.TopicBundle()
|
|
|
|
"""
|
|
|
|
|
|
|
|
Control update handler for the `/update` core endpoint.
|
|
|
|
self.topic_bundle.add_writer('status', statesman.StateWriter())
|
|
|
|
"""
|
|
|
|
self.topic_bundle.add_writer('config-spec', statesman.StateWriter())
|
|
|
|
self.topic_bundle = statesman.TopicBundle({
|
|
|
|
self.topic_bundle.add_writer('device-config', statesman.StateWriter())
|
|
|
|
'status': statesman.StateWriter(),
|
|
|
|
self.topic_bundle.add_writer('applied-config', statesman.StateWriter())
|
|
|
|
'config-spec': statesman.StateWriter(),
|
|
|
|
|
|
|
|
'device-config': statesman.StateWriter(),
|
|
|
|
|
|
|
|
'applied-config': statesman.StateWriter(),
|
|
|
|
|
|
|
|
'control-commands': cmd_reader,
|
|
|
|
|
|
|
|
'command-results': cmd_result_writer})
|
|
|
|
|
|
|
|
|
|
|
|
self.topic_bundle.set_update_required_callback(_update_required_callback)
|
|
|
|
self.topic_bundle.set_update_required_callback(_update_required_callback)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_static_state(self, local_config, applied_config, confspec):
|
|
|
|
# These should all effectively be static
|
|
|
|
# These should all effectively be static
|
|
|
|
self.topic_bundle['device-config'].set_state(local_config)
|
|
|
|
self.topic_bundle['device-config'].set_state(local_config)
|
|
|
|
self.topic_bundle['applied-config'].set_state(applied_config)
|
|
|
|
self.topic_bundle['applied-config'].set_state(applied_config)
|
|
|
|
|
|
|
|
self.topic_bundle['config-spec'].set_state(confspec)
|
|
|
|
|
|
|
|
|
|
|
|
def set_status(self, status_dict):
|
|
|
|
def set_status(self, status_dict):
|
|
|
|
self.topic_bundle['status'].set_state(status_dict)
|
|
|
|
self.topic_bundle['status'].set_state(status_dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CommandRunner():
|
|
|
|
|
|
|
|
def __init__(self, interface_functions):
|
|
|
|
|
|
|
|
self.cmd_reader = statesman.SequenceReader(
|
|
|
|
|
|
|
|
new_message_callback=self.on_new_command_message)
|
|
|
|
|
|
|
|
self.cmd_result_writer = statesman.SequenceWriter()
|
|
|
|
|
|
|
|
self._functions = interface_functions
|
|
|
|
|
|
|
|
self.current_commands = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_new_command_message(self, message):
|
|
|
|
|
|
|
|
# This should be a single list, where the first value is the command ID and the second
|
|
|
|
|
|
|
|
# value is a plugin.FunctionCall
|
|
|
|
|
|
|
|
commandID = message[0]
|
|
|
|
|
|
|
|
command_call = message[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
command_thread = threading.Thread(target=self._process_command,
|
|
|
|
|
|
|
|
args=(commandID, command_call))
|
|
|
|
|
|
|
|
command_thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _process_command(self, commandID, command_call):
|
|
|
|
|
|
|
|
if commandID in self.current_commands:
|
|
|
|
|
|
|
|
raise ValueError(F"Already running a command with ID {commandID}")
|
|
|
|
|
|
|
|
self.current_commands[commandID] = threading.current_thread()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
command_call.resolve(self._functions)
|
|
|
|
|
|
|
|
result = command_call.call()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.cmd_result_writer.add_message([commandID, result])
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
|
|
|
self.current_commands.pop(commandID)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PluginUpdateState():
|
|
|
|
class PluginUpdateState():
|
|
|
|
def __init__(self):
|
|
|
|
def __init__(self):
|
|
|
|
self.topic_bundle = statesman.TopicBundle()
|
|
|
|
self.topic_bundle = statesman.TopicBundle()
|
|
|
|
|
|
|
|
|
|
|
|
# config-spec should be static, but isn't known yet when this is created
|
|
|
|
# config-spec should be static, but isn't known yet when this is created
|
|
|
|
self.topic_bundle.add_writer('status', statesman.StateWriter())
|
|
|
|
self.topic_bundle.add('status', statesman.StateWriter())
|
|
|
|
self.topic_bundle.add_writer('config-spec', statesman.StateWriter())
|
|
|
|
self.topic_bundle.add('config-spec', statesman.StateWriter())
|
|
|
|
|
|
|
|
self.topic_bundle.add('command-spec', statesman.StateWriter())
|
|
|
|
|
|
|
|
# Why is config split out into plugins? Just like the device config and applied config,
|
|
|
|
|
|
|
|
# it's only loaded once at the start. Is this purely because it's easy to get at from the
|
|
|
|
|
|
|
|
# PluginInterface where this object is created?
|
|
|
|
|
|
|
|
|
|
|
|
self.topic_bundle.set_update_required_callback(_update_required_callback)
|
|
|
|
self.topic_bundle.set_update_required_callback(_update_required_callback)
|
|
|
|
|
|
|
|
|
|
|
|
@ -66,6 +118,9 @@ class PluginUpdateState():
|
|
|
|
def set_confspec(self, config_spec):
|
|
|
|
def set_confspec(self, config_spec):
|
|
|
|
self.topic_bundle['config-spec'].set_state(config_spec)
|
|
|
|
self.topic_bundle['config-spec'].set_state(config_spec)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_commandspec(self, command_spec):
|
|
|
|
|
|
|
|
self.topic_bundle['command-spec'].set_state(command_spec)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clean_https_url(dirty_url):
|
|
|
|
def clean_https_url(dirty_url):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
@ -143,7 +198,7 @@ def stop():
|
|
|
|
log.info("Control thread stop requested.")
|
|
|
|
log.info("Control thread stop requested.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_control(config, root_dir, core_update_state, plugin_update_states):
|
|
|
|
def start_control(config, root_dir, core_update_state, plugin_update_states):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Start the Control update thread and initialise the Shepherd Control systems.
|
|
|
|
Start the Control update thread and initialise the Shepherd Control systems.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
@ -217,6 +272,7 @@ def _control_update_loop(config, root_dir, core_update_state, plugin_update_stat
|
|
|
|
# Breaking here is a clean way of killing any delay and allowing a final update before
|
|
|
|
# Breaking here is a clean way of killing any delay and allowing a final update before
|
|
|
|
# the thread ends.
|
|
|
|
# the thread ends.
|
|
|
|
log.warning("Control thread stopping...")
|
|
|
|
log.warning("Control thread stopping...")
|
|
|
|
|
|
|
|
_stop_event.clear()
|
|
|
|
break
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
delay = update_rate_limiter.new_event(time.monotonic())
|
|
|
|
delay = update_rate_limiter.new_event(time.monotonic())
|
|
|
|
|