From be338ad0396c4e5e69510f4be5eaa4a67f2120bb Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Fri, 14 Jan 2022 11:10:32 -0500 Subject: [PATCH] machine: add systemd dbus provider implmentation Signed-off-by: Eric Callahan --- moonraker/components/machine.py | 195 ++++++++++++++++++++++++++++- scripts/moonraker-requirements.txt | 1 + 2 files changed, 194 insertions(+), 2 deletions(-) diff --git a/moonraker/components/machine.py b/moonraker/components/machine.py index 7d43ded..bb8e5aa 100644 --- a/moonraker/components/machine.py +++ b/moonraker/components/machine.py @@ -18,9 +18,11 @@ import distro from typing import ( TYPE_CHECKING, Any, + Callable, Dict, List, - Optional + Optional, + Tuple ) if TYPE_CHECKING: @@ -28,6 +30,9 @@ if TYPE_CHECKING: from websockets import WebRequest from .shell_command import ShellCommandFactory as SCMDComp from .proc_stats import ProcStats + from .dbus_manager import DbusManager + from dbus_next.aio import ProxyInterface + from dbus_next import Variant ALLOWED_SERVICES = [ "moonraker", "klipper", "webcamd", "MoonCord", @@ -61,7 +66,16 @@ class Machine: 'virtualization': self._check_inside_container() } self._update_log_rollover(log=True) - self.sys_provider = SystemdCliProvider(config) + providers: Dict[str, type] = { + "systemd_cli": SystemdCliProvider, + "systemd_dbus": SystemdDbusProvider + } + ptype = config.get('provider', 'systemd_dbus') + pclass = providers.get(ptype) + if pclass is None: + raise config.error(f"Invalid Provider: {ptype}") + self.sys_provider: BaseProvider = pclass(config) + logging.info(f"Using System Provider: {ptype}") self.server.register_endpoint( "/machine/reboot", ['POST'], self._handle_machine_request) @@ -487,5 +501,182 @@ class SystemdCliProvider(BaseProvider): except Exception: logging.exception("Error processing service state update") +class SystemdDbusProvider(BaseProvider): + def __init__(self, config: ConfigHelper) -> None: + super().__init__(config) + self.dbus_mgr: DbusManager = self.server.lookup_component( + "dbus_manager") + self.login_mgr: Optional[ProxyInterface] = None + self.props: List[Tuple[ProxyInterface, Callable]] = [] + + async def initialize(self) -> None: + if not self.dbus_mgr.is_connected(): + self.server.add_warning( + "[machine]: DBus Connection Not available, systemd " + " service tracking and actions are disabled") + return + # Get the systemd manager interface + self.systemd_mgr = await self.dbus_mgr.get_interface( + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager" + ) + # Check for systemd PolicyKit Permissions + await self.dbus_mgr.check_permission( + "org.freedesktop.systemd1.manage-units", + "System Service Management (start, stop, restart) " + "will be disabled") + await self.dbus_mgr.check_permission( + "org.freedesktop.login1.power-off", + "The shutdown API will be disabled" + ) + await self.dbus_mgr.check_permission( + "org.freedesktop.login1.power-off-multiple-sessions", + "The shutdown API will be disabled if multiple user " + "sessions are open." + ) + try: + # Get the login manaager interface + self.login_mgr = await self.dbus_mgr.get_interface( + "org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager" + ) + except self.dbus_mgr.DbusError as e: + logging.info( + "Unable to acquire the systemd-logind D-Bus interface, " + f"falling back to CLI Reboot and Shutdown APIs. {e}") + self.login_mgr = None + else: + # Check for logind permissions + await self.dbus_mgr.check_permission( + "org.freedesktop.login1.reboot", + "The reboot API will be disabled" + ) + await self.dbus_mgr.check_permission( + "org.freedesktop.login1.reboot-multiple-sessions", + "The reboot API will be disabled if multiple user " + "sessions are open." + ) + await self._detect_active_services() + + async def reboot(self) -> None: + if self.login_mgr is None: + await super().reboot() + await self.login_mgr.call_reboot(False) # type: ignore + + async def shutdown(self) -> None: + if self.login_mgr is None: + await super().shutdown() + await self.login_mgr.call_power_off(False) # type: ignore + + async def do_service_action(self, + action: str, + service_name: str + ) -> None: + if not self.dbus_mgr.is_connected(): + raise self.server.error("DBus Not Connected, ", 503) + mgr = self.systemd_mgr + if not service_name.endswith(".service"): + service_name += ".service" + if action == "start": + await mgr.call_start_unit(service_name, "replace") # type: ignore + elif action == "stop": + await mgr.call_stop_unit(service_name, "replace") # type: ignore + elif action == "restart": + await mgr.call_restart_unit( # type: ignore + service_name, "replace") + else: + raise self.server.error(f"Invalid service action: {action}") + + async def check_virt_status(self) -> Dict[str, Any]: + if not self.dbus_mgr.is_connected(): + return { + 'virt_type': "unknown", + 'virt_identifier': "unknown" + } + mgr = self.systemd_mgr + virt_id = virt_type = "none" + virt: str = await mgr.get_virtualization() # type: ignore + virt = virt.strip() + if virt: + virt_id = virt + container_types = [ + "openvz", "lxc", "lxc-libvirt", "systemd-nspawn", + "docker", "podman", "rkt", "wsl", "proot", "pouch"] + if virt_id in container_types: + virt_type = "container" + else: + virt_type = "vm" + logging.info( + f"Virtualized Environment Detected, Type: {virt_type} " + f"id: {virt_id}") + else: + logging.info("No Virtualization Detected") + return { + 'virt_type': virt_type, + 'virt_identifier': virt_id + } + + async def _detect_active_services(self) -> None: + # Get loaded service + mgr = self.systemd_mgr + patterns = [f"{svc}*.service" for svc in ALLOWED_SERVICES] + units = await mgr.call_list_units_by_patterns( # type: ignore + ["loaded"], patterns) + for unit in units: + name: str = unit[0].split('.')[0] + state: str = unit[3] + substate: str = unit[4] + dbus_path: str = unit[6] + if name in self.available_services: + continue + self.available_services[name] = { + 'active_state': state, + 'sub_state': substate + } + # setup state monitoring + props = await self.dbus_mgr.get_interface( + "org.freedesktop.systemd1", dbus_path, + "org.freedesktop.DBus.Properties" + ) + prop_callback = self._create_properties_callback(name) + self.props.append((props, prop_callback)) + props.on_properties_changed( # type: ignore + prop_callback) + + def _create_properties_callback(self, name) -> Callable: + def prop_wrapper(dbus_obj: str, + changed_props: Dict[str, Variant], + invalid_props: Dict[str, Variant] + ) -> None: + if dbus_obj != 'org.freedesktop.systemd1.Unit': + return + self._on_service_update(name, changed_props) + return prop_wrapper + + def _on_service_update(self, + service_name: str, + changed_props: Dict[str, Variant] + ) -> None: + if service_name not in self.available_services: + return + svc = self.available_services[service_name] + notify = False + if "ActiveState" in changed_props: + state: str = changed_props['ActiveState'].value + if state != svc['active_state']: + notify = True + svc['active_state'] = state + if "SubState" in changed_props: + state = changed_props['SubState'].value + if state != svc['sub_state']: + notify = True + svc['sub_state'] = state + if notify: + self.server.send_event("machine:service_state_changed", + {service_name: dict(svc)}) + + def load_component(config: ConfigHelper) -> Machine: return Machine(config) diff --git a/scripts/moonraker-requirements.txt b/scripts/moonraker-requirements.txt index b41efba..8a42de3 100644 --- a/scripts/moonraker-requirements.txt +++ b/scripts/moonraker-requirements.txt @@ -12,3 +12,4 @@ pycurl==7.44.1 zeroconf==0.37.0 preprocess-cancellation==0.1.6 jinja2==3.0.3 +dbus-next==0.2.3