diff --git a/docs/configuration.md b/docs/configuration.md index ae259fd..2cda4c9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -120,8 +120,9 @@ with system services such as systemd. provider: systemd_dbus # The provider implementation used to collect system service information # and run service actions (ie: start, restart, stop). This can be "none", -# "systemd_dbus", or "systemd_cli". If the provider is set to "none" service -# action APIs will be disabled. The default is systemd_dbus. +# "supervisord", "systemd_dbus", or "systemd_cli". If the provider is set +# to "none" service action APIs will be disabled. +# The default is systemd_dbus. sudo_password: # The password for the linux user. When set Moonraker can run linux commands # that require elevated permissions. This option accepts Jinja2 Templates, @@ -140,6 +141,10 @@ force_validation: # By default Moonraker will not attempt to revalidate if a previous attempt # at validation successfully completed. Setting this value to True will force # Moonraker to perform validation. The default is False. +supervisord_config_path: +# Path to the superrvisord config file. In case for multi supervisord instance +# running on single machine, the default '/var/run/supervisord.sock' is occupied +# by other services. ``` !!! Note diff --git a/moonraker/components/machine.py b/moonraker/components/machine.py index d4655b2..57dbb31 100644 --- a/moonraker/components/machine.py +++ b/moonraker/components/machine.py @@ -108,7 +108,8 @@ class Machine: providers: Dict[str, type] = { "none": BaseProvider, "systemd_cli": SystemdCliProvider, - "systemd_dbus": SystemdDbusProvider + "systemd_dbus": SystemdDbusProvider, + "supervisord": SuperisordProvider } self.provider_type = config.get('provider', 'systemd_dbus') pclass = providers.get(self.provider_type) @@ -747,7 +748,7 @@ class BaseProvider: action: str, service_name: str ) -> None: - raise self.server.error("Serice Actions Not Available", 503) + raise self.server.error("Service Actions Not Available", 503) async def check_virt_status(self) -> Dict[str, Any]: return { @@ -1171,6 +1172,113 @@ class SystemdDbusProvider(BaseProvider): processed[key] = val return processed +# for docker klipper-moonraker image multi-service managing +# since in container, all command is launched by normal user, +# sudo_cmd is not needed. +class SuperisordProvider(BaseProvider): + def __init__(self, config: ConfigHelper) -> None: + super().__init__(config) + self.spv_conf: str = config.get("supervisord_config_path", "") + + async def initialize(self) -> None: + for svc in ("klipper", "moonraker"): + self.available_services[svc] = { + 'active_state': "none", + 'sub_state': "unknown" + } + self.svc_cmd = self.shell_cmd.build_shell_command( + f"supervisorctl {self.spv_conf}" + f"status {' '.join(list(self.available_services.keys()))}" + ) + await self._update_service_status(0, notify=True) + pstats: ProcStats = self.server.lookup_component('proc_stats') + pstats.register_stat_callback(self._update_service_status) + + async def shutdown(self) -> None: + raise self.server.error( + "[machine]: Supervisord manager can not process SHUTDOWN." + "Please try KILL container or stop Supervisord via ssh terminal." + ) + return + + async def reboot(self) -> None: + raise self.server.error( + "[machine]: Supervisord manager can not process REBOOT." + "Please try KILL container or stop Supervisord via ssh terminal." + ) + return + + async def do_service_action( + self, action: str, service_name: str + ) -> None: + # slow reaction for supervisord, timeout set to 6.0 + await self._exec_command( + f"supervisorctl {self.spv_conf}" + f"{action} {service_name}", + timeout=6. + ) + + async def check_virt_status(self) -> Dict[str, Any]: + virt_id = virt_type = "none" + if ( + os.path.exists("/.dockerenv") or + os.path.exists("/.dockerinit") + ): + virt_id = "docker" + virt_type = "docker" + return { + 'virt_type': virt_type, + 'virt_identifier': virt_id + } + + async def _exec_command( + self, command: str, tries: int = 1, timeout=2. + ) -> str: + return await self.shell_cmd.exec_cmd( + command, proc_input=None, log_complete=False, retries=tries, + timeout=timeout + ) + + async def _update_service_status(self, + sequence: int, + notify: bool = True + ) -> None: + if sequence % 2: + # Update every other sequence + return + svcs = list(self.available_services.keys()) + try: + # slow reaction for supervisord, timeout set to 6.0 + resp = await self.svc_cmd.run_with_response( + log_complete=False, timeout=6. + ) + resp_l = resp.strip().split("\n") # drop lengend + for svc, state in zip(svcs, resp_l): + sub_state = state.split()[1].lower() + new_state: Dict[str, str] = { + 'active_state': "active", + 'sub_state': sub_state + } + if self.available_services[svc] != new_state: + self.available_services[svc] = new_state + if notify: + self.server.send_event( + "machine:service_state_changed", + {svc: new_state}) + except Exception: + logging.exception("Error processing service state update") + + # service files is defined since docker build. + # not needed to implete SV. + async def extract_service_info( + self, + service: str, + pid: int, + properties: List[str], + raw: bool = False + ) -> Dict[str, Any]: + return {} + # Install validation INSTALL_VERSION = 1