diff --git a/supervisor/docker/addon.py b/supervisor/docker/addon.py index ea5357ee4..ee9e63e47 100644 --- a/supervisor/docker/addon.py +++ b/supervisor/docker/addon.py @@ -27,8 +27,8 @@ from ..const import ( SECURITY_PROFILE, ) from ..coresys import CoreSys -from ..exceptions import CoreDNSError, DockerError -from ..hardware.const import PolicyGroup, UdevSubsystem +from ..exceptions import CoreDNSError, DockerError, HardwareNotFound +from ..hardware.const import PolicyGroup from ..utils import process_lock from .interface import DockerInterface @@ -124,64 +124,21 @@ class DockerAddon(DockerInterface): ENV_TOKEN_HASSIO: self.addon.supervisor_token, } - @property - def devices(self) -> Optional[List[str]]: - """Return needed devices.""" - devices = set() - - def _create_dev(device_path: Path) -> str: - """Add device to list.""" - devices.add(f"{device_path.as_posix()}:{device_path.as_posix()}:rwm") - - # Static devices - for device_path in self.addon.static_devices: - if not self.sys_hardware.exists_device_node(device_path): - _LOGGER.debug("Ignore static device path %s", device_path) - continue - _create_dev(device_path) - - # Dynamic devices - for device in self.addon.devices: - _create_dev(device.path) - - # Auto mapping UART devices / LINKS - if self.addon.with_uart: - for device in self.sys_hardware.filter_devices( - subsystem=UdevSubsystem.SERIAL - ): - _create_dev(device.path) - - # Auto mapping GPIO - if self.addon.with_gpio: - for subsystem in ( - UdevSubsystem.GPIO, - UdevSubsystem.GPIOMEM, - ): - for device in self.sys_hardware.filter_devices(subsystem=subsystem): - _create_dev(device.path) - - # Auto mapping Video - if self.addon.with_video: - for subsystem in ( - UdevSubsystem.VIDEO, - UdevSubsystem.CEC, - UdevSubsystem.VCHIQ, - UdevSubsystem.MEDIA, - ): - for device in self.sys_hardware.filter_devices(subsystem=subsystem): - _create_dev(device.path) - - # Return None if no devices is present - if devices: - return list(devices) - return None - @property def cgroups_rules(self) -> Optional[List[str]]: """Return a list of needed cgroups permission.""" rules = set() - # Attach correct cgroups + # Attach correct cgroups for static devices + for device_path in self.addon.static_devices: + try: + device = self.sys_hardware.get_by_path(device_path) + except HardwareNotFound: + _LOGGER.debug("Ignore static device path %s", device_path) + else: + rules.add(self.sys_hardware.policy.get_cgroups_rule(device)) + + # Attach correct cgroups for devices for device in self.addon.devices: rules.add(self.sys_hardware.policy.get_cgroups_rule(device)) @@ -197,6 +154,10 @@ class DockerAddon(DockerInterface): if self.addon.with_uart: rules.update(self.sys_hardware.policy.get_cgroups_rules(PolicyGroup.UART)) + # USB + if self.addon.with_usb: + rules.update(self.sys_hardware.policy.get_cgroups_rules(PolicyGroup.USB)) + # Return None if no rules is present if rules: return list(rules) @@ -267,7 +228,10 @@ class DockerAddon(DockerInterface): """Generate volumes for mappings.""" addon_mapping = self.addon.map_volumes - volumes = {str(self.addon.path_extern_data): {"bind": "/data", "mode": "rw"}} + volumes = { + "/dev": {"bind": "/dev", "mode": "ro"}, + str(self.addon.path_extern_data): {"bind": "/data", "mode": "rw"}, + } # setup config mappings if MAP_CONFIG in addon_mapping: @@ -354,10 +318,6 @@ class DockerAddon(DockerInterface): if self.addon.with_udev: volumes.update({"/run/udev": {"bind": "/run/udev", "mode": "ro"}}) - # USB support - if self.addon.with_usb and self.sys_hardware.helper.support_usb: - volumes.update({"/dev/bus/usb": {"bind": "/dev/bus/usb", "mode": "rw"}}) - # Kernel Modules support if self.addon.with_kernel_modules: volumes.update({"/lib/modules": {"bind": "/lib/modules", "mode": "ro"}}) @@ -391,13 +351,6 @@ class DockerAddon(DockerInterface): } ) - # With UART - if self.addon.with_uart: - mount = self.sys_hardware.container.get_udev_id_mount(UdevSubsystem.SERIAL) - volumes.update( - {str(mount.as_posix()): {"bind": str(mount.as_posix()), "mode": "ro"}} - ) - return volumes def _run(self) -> None: @@ -430,7 +383,6 @@ class DockerAddon(DockerInterface): pid_mode=self.pid_mode, ports=self.ports, extra_hosts=self.network_mapping, - devices=self.devices, device_cgroup_rules=self.cgroups_rules, cap_add=self.addon.privileged, security_opt=self.security_opt, diff --git a/supervisor/docker/audio.py b/supervisor/docker/audio.py index d4f0ae0b2..1b148f623 100644 --- a/supervisor/docker/audio.py +++ b/supervisor/docker/audio.py @@ -1,6 +1,5 @@ """Audio docker object.""" import logging -from pathlib import Path from typing import Dict from ..const import ENV_TIME, MACHINE_ID @@ -30,6 +29,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes): def volumes(self) -> Dict[str, Dict[str, str]]: """Return Volumes for the mount.""" volumes = { + "/dev": {"bind": "/dev", "mode": "ro"}, str(self.sys_config.path_extern_audio): {"bind": "/data", "mode": "rw"}, "/run/dbus": {"bind": "/run/dbus", "mode": "ro"}, "/run/udev": {"bind": "/run/udev", "mode": "ro"}, @@ -39,12 +39,6 @@ class DockerAudio(DockerInterface, CoreSysAttributes): if MACHINE_ID.exists(): volumes.update({str(MACHINE_ID): {"bind": str(MACHINE_ID), "mode": "ro"}}) - # SND support - if Path("/dev/snd").exists(): - volumes.update({"/dev/snd": {"bind": "/dev/snd", "mode": "rw"}}) - else: - _LOGGER.warning("Kernel have no audio support") - return volumes def _run(self) -> None: diff --git a/supervisor/docker/homeassistant.py b/supervisor/docker/homeassistant.py index 618d73ada..a367cd950 100644 --- a/supervisor/docker/homeassistant.py +++ b/supervisor/docker/homeassistant.py @@ -8,7 +8,7 @@ import requests from ..const import ENV_TIME, ENV_TOKEN, ENV_TOKEN_HASSIO, LABEL_MACHINE, MACHINE_ID from ..exceptions import DockerError -from ..hardware.const import PolicyGroup, UdevSubsystem +from ..hardware.const import PolicyGroup from .interface import CommandReturn, DockerInterface _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -55,12 +55,14 @@ class DockerHomeAssistant(DockerInterface): self.sys_hardware.policy.get_cgroups_rules(PolicyGroup.UART) + self.sys_hardware.policy.get_cgroups_rules(PolicyGroup.VIDEO) + self.sys_hardware.policy.get_cgroups_rules(PolicyGroup.GPIO) + + self.sys_hardware.policy.get_cgroups_rules(PolicyGroup.USB) ) @property def volumes(self) -> Dict[str, Dict[str, str]]: """Return Volumes for the mount.""" volumes = { + "/dev": {"bind": "/dev", "mode": "ro"}, "/run/dbus": {"bind": "/run/dbus", "mode": "ro"}, "/run/udev": {"bind": "/run/udev", "mode": "ro"}, } @@ -106,16 +108,6 @@ class DockerHomeAssistant(DockerInterface): } ) - # udev helper - for mount in ( - self.sys_hardware.container.get_udev_id_mount(UdevSubsystem.SERIAL), - self.sys_hardware.container.get_udev_id_mount(UdevSubsystem.DISK), - self.sys_hardware.container.get_udev_id_mount(UdevSubsystem.INPUT), - ): - volumes.update( - {str(mount.as_posix()): {"bind": str(mount.as_posix()), "mode": "ro"}} - ) - return volumes def _run(self) -> None: diff --git a/supervisor/hardware/const.py b/supervisor/hardware/const.py index 1a1709f38..d43f25e0e 100644 --- a/supervisor/hardware/const.py +++ b/supervisor/hardware/const.py @@ -35,3 +35,10 @@ class PolicyGroup(str, Enum): USB = "usb" VIDEO = "video" AUDIO = "audio" + + +class UdevAction(str, Enum): + """Udev device action.""" + + ADD = "add" + REMOVE = "remove" diff --git a/supervisor/hardware/container.py b/supervisor/hardware/container.py deleted file mode 100644 index 163203a78..000000000 --- a/supervisor/hardware/container.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Handle udev for container.""" -from contextlib import suppress -import logging -from pathlib import Path -from typing import Optional - -from supervisor.const import AddonState - -from ..coresys import CoreSys, CoreSysAttributes -from ..docker.interface import DockerInterface -from ..exceptions import DockerError, DockerNotFound, HardwareError -from .const import UdevSubsystem -from .data import Device - -_UDEV_BY_ID = { - UdevSubsystem.SERIAL: Path("/dev/serial/by-id"), - UdevSubsystem.DISK: Path("/dev/disk"), - UdevSubsystem.INPUT: Path("/dev/input/by-id"), -} - -_LOGGER: logging.Logger = logging.getLogger(__name__) - - -class HwContainer(CoreSysAttributes): - """Representation of an interface to udev / container.""" - - def __init__(self, coresys: CoreSys): - """Init hardware object.""" - self.coresys = coresys - - def get_udev_id_mount(self, subystem: UdevSubsystem) -> Optional[Path]: - """Return mount path for udev device by-id path.""" - return _UDEV_BY_ID.get(subystem) - - async def process_serial_device(self, device: Device) -> None: - """Process a new Serial device.""" - # Add to all needed add-ons - for addon in self.sys_addons.installed: - if addon.state != AddonState.STARTED or addon.with_uart: - continue - with suppress(HardwareError): - await self._create_device(addon.instance, device) - - # Process on Home Assistant Core - with suppress(HardwareError): - await self._create_device(self.sys_homeassistant.core.instance, device) - - async def process_input_device(self, device: Device) -> None: - """Process a new Serial device.""" - # Process on Home Assistant Core - with suppress(HardwareError): - await self._create_device(self.sys_homeassistant.core.instance, device) - - async def _create_device(self, instance: DockerInterface, device: Device) -> None: - """Add device into container.""" - try: - answer = await instance.run_inside( - f'sh -c "mknod -m 660 {device.path.as_posix()} c {device.cgroups_major} {device.cgroups_minor}"' - ) - except DockerNotFound: - return - except DockerError as err: - _LOGGER.warning("Can't add new device %s to %s", device.path, instance.name) - raise HardwareError() from err - - if answer.exit_code == 0: - return - - _LOGGER.warning( - "Container response with '%s' during process %s", - answer.output.encode(), - device.path, - ) diff --git a/supervisor/hardware/module.py b/supervisor/hardware/module.py index cd4723dc9..f9846a0c6 100644 --- a/supervisor/hardware/module.py +++ b/supervisor/hardware/module.py @@ -9,7 +9,6 @@ from supervisor.hardware.const import UdevSubsystem from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import HardwareNotFound -from .container import HwContainer from .data import Device from .helper import HwHelper from .monitor import HwMonitor @@ -30,7 +29,6 @@ class HardwareManager(CoreSysAttributes): self._montior: HwMonitor = HwMonitor(coresys) self._helper: HwHelper = HwHelper(coresys) self._policy: HwPolicy = HwPolicy(coresys) - self._container: HwContainer = HwContainer(coresys) @property def monitor(self) -> HwMonitor: @@ -47,11 +45,6 @@ class HardwareManager(CoreSysAttributes): """Return Hardware policy instance.""" return self._policy - @property - def container(self) -> HwContainer: - """Return Hardware container instance.""" - return self._container - @property def devices(self) -> List[Device]: """Return List of devices.""" diff --git a/supervisor/hardware/monitor.py b/supervisor/hardware/monitor.py index c6219241f..7cf559cec 100644 --- a/supervisor/hardware/monitor.py +++ b/supervisor/hardware/monitor.py @@ -1,4 +1,5 @@ """Supervisor Hardware monitor based on udev.""" +from contextlib import suppress import logging from pathlib import Path from pprint import pformat @@ -8,8 +9,9 @@ import pyudev from ..const import CoreState from ..coresys import CoreSys, CoreSysAttributes +from ..exceptions import HardwareNotFound from ..resolution.const import UnhealthyReason -from .const import UdevSubsystem +from .const import PolicyGroup, UdevAction, UdevSubsystem from .data import Device _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -45,82 +47,107 @@ class HwMonitor(CoreSysAttributes): self.observer.stop() _LOGGER.info("Stopped Supervisor hardware monitor") - def _udev_events(self, action: str, device: pyudev.Device): + def _udev_events(self, action: str, kernel: pyudev.Device): """Incomming events from udev. This is inside a observe thread and need pass into our eventloop. """ - _LOGGER.debug("Hardware monitor: %s - %s", action, pformat(device)) - self.sys_loop.call_soon_threadsafe(self._async_udev_events, action, device) + _LOGGER.debug("Hardware monitor: %s - %s", action, pformat(kernel)) + udev = None + with suppress(pyudev.DeviceNotFoundAtPathError): + udev = pyudev.Devices.from_sys_path(self.context, kernel.sys_path) - def _async_udev_events(self, action: str, udev_device: pyudev.Device): + self.sys_loop.call_soon_threadsafe( + self._async_udev_events, action, kernel, udev + ) + + def _async_udev_events( + self, action: str, kernel: pyudev.Device, udev: Optional[pyudev.Device] + ): """Incomming events from udev into loop.""" # Update device List - if not udev_device.device_node or self.sys_hardware.helper.hide_virtual_device( - udev_device + if not kernel.device_node or self.sys_hardware.helper.hide_virtual_device( + kernel ): return - device = Device( - udev_device.sys_name, - Path(udev_device.device_node), - Path(udev_device.sys_path), - udev_device.subsystem, - [Path(node) for node in udev_device.device_links], - {attr: udev_device.properties[attr] for attr in udev_device.properties}, - ) + forward_action = None + device = None - # Update internal Database - if action == "add": + ## + # Remove + if action in ("remove", "unbind") and udev is None: + try: + device = self.sys_hardware.get_by_path(Path(kernel.sys_path)) + except HardwareNotFound: + return + else: + self.sys_hardware.delete_device(device) + forward_action = UdevAction.REMOVE + + ## + # Add + if action in ("add", "bind") and udev is not None: + device = Device( + udev.sys_name, + Path(udev.device_node), + Path(udev.sys_path), + udev.subsystem, + [Path(node) for node in udev.device_links], + {attr: udev.properties[attr] for attr in udev.properties}, + ) self.sys_hardware.update_device(device) - if action == "remove": - self.sys_hardware.delete_device(device) + forward_action = UdevAction.ADD - # Process device - if self.sys_core.state in (CoreState.RUNNING, CoreState.FREEZE): + # Process Action + if ( + device + and forward_action + and self.sys_core.state in (CoreState.RUNNING, CoreState.FREEZE) + ): # New Sound device - if device.subsystem == UdevSubsystem.AUDIO and action == "add": - self._action_sound_add(device) + if device.subsystem == UdevSubsystem.AUDIO: + self._action_sound(device, forward_action) - # New serial device - if device.subsystem == UdevSubsystem.SERIAL and action == "add": - self._action_tty_add(device) + # serial device + elif device.subsystem == UdevSubsystem.SERIAL: + self._action_tty(device, forward_action) - # New input device - if device.subsystem == UdevSubsystem.INPUT and action == "add": - self._action_input_add(device) + # input device + elif device.subsystem == UdevSubsystem.INPUT: + self._action_input(device, forward_action) - def _action_sound_add(self, device: Device): + # USB device + elif device.subsystem == UdevSubsystem.USB: + self._action_usb(device, forward_action) + + def _action_sound(self, device: Device, action: UdevAction): """Process sound actions.""" - _LOGGER.info("Detecting changed audio hardware - %s", device.path) + if not self.sys_hardware.policy.is_match_cgroup(PolicyGroup.AUDIO, device): + return + _LOGGER.info("Detecting %s audio hardware - %s", action, device.path) self.sys_loop.call_later(2, self.sys_create_task, self.sys_host.sound.update()) - def _action_tty_add(self, device: Device): + def _action_tty(self, device: Device, action: UdevAction): """Process tty actions.""" - _LOGGER.info( - "Detecting changed serial hardware %s - %s", device.path, device.by_id - ) - if not device.by_id: + if not device.by_id or not self.sys_hardware.policy.is_match_cgroup( + PolicyGroup.UART, device + ): return - - # Start process TTY - self.sys_loop.call_later( - 2, - self.sys_create_task, - self.sys_hardware.container.process_serial_device(device), + _LOGGER.info( + "Detecting %s serial hardware %s - %s", action, device.path, device.by_id ) - def _action_input_add(self, device: Device): + def _action_input(self, device: Device, action: UdevAction): """Process input actions.""" - _LOGGER.info( - "Detecting changed serial hardware %s - %s", device.path, device.by_id - ) if not device.by_id: return - - # Start process input - self.sys_loop.call_later( - 2, - self.sys_create_task, - self.sys_hardware.container.process_serial_device(device), + _LOGGER.info( + "Detecting %s serial hardware %s - %s", action, device.path, device.by_id ) + + def _action_usb(self, device: Device, action: UdevAction): + """Process usb actions.""" + if not self.sys_hardware.policy.is_match_cgroup(PolicyGroup.USB, device): + return + _LOGGER.info("Detecting %s usb hardware %s", action, device.path) diff --git a/supervisor/hardware/policy.py b/supervisor/hardware/policy.py index b4bbf57dd..a5298b6bf 100644 --- a/supervisor/hardware/policy.py +++ b/supervisor/hardware/policy.py @@ -14,6 +14,7 @@ _GROUP_CGROUPS: Dict[PolicyGroup, List[int]] = { PolicyGroup.GPIO: [254, 245], PolicyGroup.VIDEO: [239, 29, 81, 251, 242, 226], PolicyGroup.AUDIO: [116], + PolicyGroup.USB: [189], } @@ -24,6 +25,10 @@ class HwPolicy(CoreSysAttributes): """Init hardware policy object.""" self.coresys = coresys + def is_match_cgroup(self, group: PolicyGroup, device: Device) -> bool: + """Return true if device is in cgroup Policy.""" + return device.cgroups_major in _GROUP_CGROUPS.get(group, []) + def get_cgroups_rules(self, group: PolicyGroup) -> List[str]: """Generate cgroups rules for a policy group.""" return [f"c {dev}:* rwm" for dev in _GROUP_CGROUPS.get(group, [])] diff --git a/tests/hardware/test_policy.py b/tests/hardware/test_policy.py index 928e167b8..b0e464ee7 100644 --- a/tests/hardware/test_policy.py +++ b/tests/hardware/test_policy.py @@ -31,3 +31,18 @@ def test_policy_group(coresys): "c 242:* rwm", "c 226:* rwm", ] + + +def test_device_in_policy(coresys): + """Test device cgroup policy.""" + device = Device( + "ttyACM0", + Path("/dev/ttyACM0"), + Path("/sys/bus/usb/001"), + "tty", + [], + {"MAJOR": "204", "MINOR": "10"}, + ) + + assert coresys.hardware.policy.is_match_cgroup(PolicyGroup.UART, device) + assert not coresys.hardware.policy.is_match_cgroup(PolicyGroup.GPIO, device)