|  |  | @ -30,12 +30,20 @@ class Agent(): | 
			
		
	
		
		
			
				
					
					|  |  |  |         self.applied_config = None |  |  |  |         self.applied_config = None | 
			
		
	
		
		
			
				
					
					|  |  |  |         # Split the applied_config up into core and plugins |  |  |  |         # Split the applied_config up into core and plugins | 
			
		
	
		
		
			
				
					
					|  |  |  |         self.core_config = None |  |  |  |         self.core_config = None | 
			
		
	
		
		
			
				
					
					|  |  |  |         self.plugin_configs = None |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         self.interface_functions = None |  |  |  |         self.interface_functions = None | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         self.control_enabled = control_enabled |  |  |  |         self.control_enabled = control_enabled | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         self.core_interface = plugin.PluginInterface() | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         self.plugin_interfaces = None | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     def root_dir(self): | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         return self.core_config["root_dir"] | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     def device_name(self): | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         return self.core_config["name"] | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     def load(self, default_config_path, use_custom_config=True,  new_device_mode=False): |  |  |  |     def load(self, default_config_path, use_custom_config=True,  new_device_mode=False): | 
			
		
	
		
		
			
				
					
					|  |  |  |         """ |  |  |  |         """ | 
			
		
	
		
		
			
				
					
					|  |  |  |         Load in the Shepherd Agent config and associated plugins. |  |  |  |         Load in the Shepherd Agent config and associated plugins. | 
			
		
	
	
		
		
			
				
					|  |  | @ -48,18 +56,45 @@ class Agent(): | 
			
		
	
		
		
			
				
					
					|  |  |  |                 of ID, as if it were being run on a fresh system. |  |  |  |                 of ID, as if it were being run on a fresh system. | 
			
		
	
		
		
			
				
					
					|  |  |  |         """ |  |  |  |         """ | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Setup core interface | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         self.core_interface.register_confspec(core_confspec()) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         self.core_interface.register_function(self.root_dir) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         self.core_interface.register_function(self.device_name) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Allows plugins to add delay for system time to stabilise | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         self.core_interface.register_hook("wait_for_stable_time") | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Allow other modules to add to the core interface (confspec, hooks, interface functions) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Having modules modify a confspec after it's registered here is a bit of a hack. | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         tasks.register(self.core_interface) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         control.register(self.core_interface) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |          | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Because the plugin module caches interfaces, this will then get used when loading | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # config layers and validating them | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         plugin.load_plugin_interface("shepherd", self.core_interface) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         # Compile the config layers |  |  |  |         # Compile the config layers | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         confman = ConfigManager() |  |  |  |         confman = ConfigManager() | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Pre-seed confman with core confspec to bootstrap 'plugin_dir'. This is required even | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # though 'load_config_layer_and_plugins()' will get the core confspec from the cached | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # interface, as it needs 'plugin_dir' to list the plugins to load first. | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         confman.add_confspec("shepherd", self.core_interface._confspec) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         compile_local_config(confman, default_config_path, use_custom_config) |  |  |  |         compile_local_config(confman, default_config_path, use_custom_config) | 
			
		
	
		
		
			
				
					
					|  |  |  |         self.local_config = deepcopy(confman.get_config_bundles()) |  |  |  |         self.local_config = deepcopy(confman.get_config_bundles()) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         # Check for new device mode |  |  |  |         local_core_conf = confman.get_config_bundle('shepherd') | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         core_conf = confman.get_config_bundle('shepherd') |  |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         if new_device_mode or check_new_device_file(core_conf["custom_config_path"]): |  |  |  |         # Check for new device mode | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         if new_device_mode or check_new_device_file(local_core_conf["custom_config_path"]): | 
			
		
	
		
		
			
				
					
					|  |  |  |             log.info("'new device' mode enabled, clearing old state...") |  |  |  |             log.info("'new device' mode enabled, clearing old state...") | 
			
		
	
		
		
			
				
					
					|  |  |  |             control.generate_device_identity(core_conf["root_dir"]) |  |  |  |             control.generate_device_identity(local_core_conf["root_dir"]) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             control.clear_cached_config(core_conf["root_dir"]) |  |  |  |             control.clear_cached_config(local_core_conf["root_dir"]) | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         if local_core_conf["control"] is None: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             self.control_enabled = False | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             log.warning("Shepherd control config section not present. Will not attempt to" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                         " connect to Shepherd Control server.") | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         if self.control_enabled: |  |  |  |         if self.control_enabled: | 
			
		
	
		
		
			
				
					
					|  |  |  |             compile_remote_config(confman) |  |  |  |             compile_remote_config(confman) | 
			
		
	
	
		
		
			
				
					|  |  | @ -67,21 +102,23 @@ class Agent(): | 
			
		
	
		
		
			
				
					
					|  |  |  |             log.info("Shepherd Control config layer disabled") |  |  |  |             log.info("Shepherd Control config layer disabled") | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         self.applied_config = confman.get_config_bundles() |  |  |  |         self.applied_config = confman.get_config_bundles() | 
			
		
	
		
		
			
				
					
					|  |  |  |         self.plugin_configs = confman.get_config_bundles() |  |  |  |         self.core_config = confman.get_config_bundle('shepherd') | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         self.core_config = self.plugin_configs.pop('shepherd') |  |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         log.debug("Compiled config: %s", confman.root_config) |  |  |  |         log.debug("Compiled config: %s", confman.root_config) | 
			
		
	
		
		
			
				
					
					|  |  |  |         if core_conf["compiled_config_path"]: |  |  |  |         if self.core_config["compiled_config_path"]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |             message = F"Compiled Shepherd config at {datetime.now()}" |  |  |  |             message = F"Compiled Shepherd config at {datetime.now()}" | 
			
		
	
		
		
			
				
					
					|  |  |  |             confman.dump_to_file(core_conf["compiled_config_path"], message=message) |  |  |  |             confman.dump_to_file(self.core_config["compiled_config_path"], message=message) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             log.info(F"Saved compiled config to {core_conf['compiled_config_path']}") |  |  |  |             log.info(F"Saved compiled config to {self.core_config['compiled_config_path']}") | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     def restart(self): | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         pass | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     def start(self): |  |  |  |     def start(self): | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         # After this point, plugins may already have their own threads running if they create |  |  |  |         # After this point, plugins may already have their own threads running if they create | 
			
		
	
		
		
			
				
					
					|  |  |  |         # them during init |  |  |  |         # them during init | 
			
		
	
		
		
			
				
					
					|  |  |  |         plugin_interfaces, self.interface_functions = plugin.init_plugins( |  |  |  |         self.plugin_interfaces = plugin.init_plugins(self.applied_config) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             self.plugin_configs, self.core_config, {}) |  |  |  |         self.interface_functions = self.core_interface.plugins | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         cmd_runner = control.CommandRunner(self.interface_functions) |  |  |  |         cmd_runner = control.CommandRunner(self.interface_functions) | 
			
		
	
		
		
			
				
					
					|  |  |  |         core_update_state = control.CoreUpdateState(cmd_runner.cmd_reader, |  |  |  |         core_update_state = control.CoreUpdateState(cmd_runner.cmd_reader, | 
			
		
	
	
		
		
			
				
					|  |  | @ -89,22 +126,11 @@ class Agent(): | 
			
		
	
		
		
			
				
					
					|  |  |  |         core_update_state.set_static_state(self.local_config, self.applied_config, core_confspec()) |  |  |  |         core_update_state.set_static_state(self.local_config, self.applied_config, core_confspec()) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         plugin_update_states = {name: iface._update_state |  |  |  |         plugin_update_states = {name: iface._update_state | 
			
		
	
		
		
			
				
					
					|  |  |  |                                 for name, iface in plugin_interfaces.items()} |  |  |  |                                 for name, iface in self.plugin_interfaces.items()} | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         if self.control_enabled: |  |  |  |         if self.control_enabled: | 
			
		
	
		
		
			
				
					
					|  |  |  |             if self.core_config["control"] is not None: |  |  |  |             control.start_control(self.core_config["control"], self.root_dir(), | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |                 control.start_control(self.core_config["control"], self.core_config["root_dir"], |  |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |                                       core_update_state, plugin_update_states) |  |  |  |                                       core_update_state, plugin_update_states) | 
			
		
	
		
		
			
				
					
					|  |  |  |             else: |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |                 log.warning("Shepherd control config section not present. Will not attempt to" |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |                             " connect to Shepherd Control server.") |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         # Shift Control check to when it actually tries to find config, and just set  |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         # control_enabled to false.  |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         # Does all the other cmd_runner and update state stuff need to happen still if |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         # control is disabled? |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         # Should Core really need to know anything about the command runner, or should it |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         # just hand the interface functions in directly to Control? |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         # Need somewhere to eventually pass in the hooks Tasks will need for the lowpower stuff,  |  |  |  |         # Need somewhere to eventually pass in the hooks Tasks will need for the lowpower stuff,  | 
			
		
	
		
		
			
				
					
					|  |  |  |         # probably just another init_plugins arg. |  |  |  |         # probably just another init_plugins arg. | 
			
		
	
	
		
		
			
				
					|  |  | @ -139,8 +165,6 @@ def compile_local_config(confman, default_config_path, use_custom_config): | 
			
		
	
		
		
			
				
					
					|  |  |  |     As part of this, load the required plugins into cache (required to validate their config). |  |  |  |     As part of this, load the required plugins into cache (required to validate their config). | 
			
		
	
		
		
			
				
					
					|  |  |  |     """ |  |  |  |     """ | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     confman.add_confspec("shepherd", core_confspec()) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     # ====Default Local Config Layer==== |  |  |  |     # ====Default Local Config Layer==== | 
			
		
	
		
		
			
				
					
					|  |  |  |     # This must validate to continue. |  |  |  |     # This must validate to continue. | 
			
		
	
		
		
			
				
					
					|  |  |  |     default_config_path = Path(default_config_path).expanduser().resolve() |  |  |  |     default_config_path = Path(default_config_path).expanduser().resolve() | 
			
		
	
	
		
		
			
				
					|  |  | @ -202,8 +226,8 @@ def compile_remote_config(confman): | 
			
		
	
		
		
			
				
					
					|  |  |  |     # ====Control Remote Config Layer==== |  |  |  |     # ====Control Remote Config Layer==== | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     # Freeze Shepherd Control related config. |  |  |  |     # Freeze Shepherd Control related config. | 
			
		
	
		
		
			
				
					
					|  |  |  |     confman.freeze_value("shepherd", "control_server") |  |  |  |     confman.freeze_value("shepherd", "control", "server") | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     confman.freeze_value("shepherd", "control_api_key") |  |  |  |     confman.freeze_value("shepherd", "control", "intro_key") | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     # Save current good local config |  |  |  |     # Save current good local config | 
			
		
	
		
		
			
				
					
					|  |  |  |     confman.save_fallback() |  |  |  |     confman.save_fallback() | 
			
		
	
	
		
		
			
				
					|  |  | @ -255,7 +279,6 @@ def core_confspec(): | 
			
		
	
		
		
			
				
					
					|  |  |  |                 "./shepherd-plugins") |  |  |  |                 "./shepherd-plugins") | 
			
		
	
		
		
			
				
					
					|  |  |  |     }) |  |  |  |     }) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     confspec.add_spec("control", control.control_confspec(), optional=True) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     return confspec |  |  |  |     return confspec | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -300,7 +323,6 @@ def load_config_layer_and_plugins(confman: ConfigManager, config_source): | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     # List other bundle names to get plugins we need to load |  |  |  |     # List other bundle names to get plugins we need to load | 
			
		
	
		
		
			
				
					
					|  |  |  |     plugin_names = confman.get_bundle_names() |  |  |  |     plugin_names = confman.get_bundle_names() | 
			
		
	
		
		
			
				
					
					|  |  |  |     plugin_names.remove("shepherd") |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     # Load plugins to get their config specifications |  |  |  |     # Load plugins to get their config specifications | 
			
		
	
		
		
			
				
					
					|  |  |  |     plugin_interfaces = {name: plugin.load_plugin(name, plugin_dir) for name in plugin_names} |  |  |  |     plugin_interfaces = {name: plugin.load_plugin(name, plugin_dir) for name in plugin_names} | 
			
		
	
	
		
		
			
				
					|  |  | 
 |