Change handling kernel udev events (#2484)

* Change handling kernel udev events / add USB events

* finish USB

* map dev ro

* fix bind
This commit is contained in:
Pascal Vizeli 2021-01-30 14:19:53 +01:00 committed by GitHub
parent 10b14132b9
commit b8a976b344
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 130 additions and 218 deletions

View File

@ -27,8 +27,8 @@ from ..const import (
SECURITY_PROFILE, SECURITY_PROFILE,
) )
from ..coresys import CoreSys from ..coresys import CoreSys
from ..exceptions import CoreDNSError, DockerError from ..exceptions import CoreDNSError, DockerError, HardwareNotFound
from ..hardware.const import PolicyGroup, UdevSubsystem from ..hardware.const import PolicyGroup
from ..utils import process_lock from ..utils import process_lock
from .interface import DockerInterface from .interface import DockerInterface
@ -124,64 +124,21 @@ class DockerAddon(DockerInterface):
ENV_TOKEN_HASSIO: self.addon.supervisor_token, 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 @property
def cgroups_rules(self) -> Optional[List[str]]: def cgroups_rules(self) -> Optional[List[str]]:
"""Return a list of needed cgroups permission.""" """Return a list of needed cgroups permission."""
rules = set() 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: for device in self.addon.devices:
rules.add(self.sys_hardware.policy.get_cgroups_rule(device)) rules.add(self.sys_hardware.policy.get_cgroups_rule(device))
@ -197,6 +154,10 @@ class DockerAddon(DockerInterface):
if self.addon.with_uart: if self.addon.with_uart:
rules.update(self.sys_hardware.policy.get_cgroups_rules(PolicyGroup.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 # Return None if no rules is present
if rules: if rules:
return list(rules) return list(rules)
@ -267,7 +228,10 @@ class DockerAddon(DockerInterface):
"""Generate volumes for mappings.""" """Generate volumes for mappings."""
addon_mapping = self.addon.map_volumes 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 # setup config mappings
if MAP_CONFIG in addon_mapping: if MAP_CONFIG in addon_mapping:
@ -354,10 +318,6 @@ class DockerAddon(DockerInterface):
if self.addon.with_udev: if self.addon.with_udev:
volumes.update({"/run/udev": {"bind": "/run/udev", "mode": "ro"}}) 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 # Kernel Modules support
if self.addon.with_kernel_modules: if self.addon.with_kernel_modules:
volumes.update({"/lib/modules": {"bind": "/lib/modules", "mode": "ro"}}) 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 return volumes
def _run(self) -> None: def _run(self) -> None:
@ -430,7 +383,6 @@ class DockerAddon(DockerInterface):
pid_mode=self.pid_mode, pid_mode=self.pid_mode,
ports=self.ports, ports=self.ports,
extra_hosts=self.network_mapping, extra_hosts=self.network_mapping,
devices=self.devices,
device_cgroup_rules=self.cgroups_rules, device_cgroup_rules=self.cgroups_rules,
cap_add=self.addon.privileged, cap_add=self.addon.privileged,
security_opt=self.security_opt, security_opt=self.security_opt,

View File

@ -1,6 +1,5 @@
"""Audio docker object.""" """Audio docker object."""
import logging import logging
from pathlib import Path
from typing import Dict from typing import Dict
from ..const import ENV_TIME, MACHINE_ID from ..const import ENV_TIME, MACHINE_ID
@ -30,6 +29,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
def volumes(self) -> Dict[str, Dict[str, str]]: def volumes(self) -> Dict[str, Dict[str, str]]:
"""Return Volumes for the mount.""" """Return Volumes for the mount."""
volumes = { volumes = {
"/dev": {"bind": "/dev", "mode": "ro"},
str(self.sys_config.path_extern_audio): {"bind": "/data", "mode": "rw"}, str(self.sys_config.path_extern_audio): {"bind": "/data", "mode": "rw"},
"/run/dbus": {"bind": "/run/dbus", "mode": "ro"}, "/run/dbus": {"bind": "/run/dbus", "mode": "ro"},
"/run/udev": {"bind": "/run/udev", "mode": "ro"}, "/run/udev": {"bind": "/run/udev", "mode": "ro"},
@ -39,12 +39,6 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
if MACHINE_ID.exists(): if MACHINE_ID.exists():
volumes.update({str(MACHINE_ID): {"bind": str(MACHINE_ID), "mode": "ro"}}) 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 return volumes
def _run(self) -> None: def _run(self) -> None:

View File

@ -8,7 +8,7 @@ import requests
from ..const import ENV_TIME, ENV_TOKEN, ENV_TOKEN_HASSIO, LABEL_MACHINE, MACHINE_ID from ..const import ENV_TIME, ENV_TOKEN, ENV_TOKEN_HASSIO, LABEL_MACHINE, MACHINE_ID
from ..exceptions import DockerError from ..exceptions import DockerError
from ..hardware.const import PolicyGroup, UdevSubsystem from ..hardware.const import PolicyGroup
from .interface import CommandReturn, DockerInterface from .interface import CommandReturn, DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__) _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.UART)
+ self.sys_hardware.policy.get_cgroups_rules(PolicyGroup.VIDEO) + 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.GPIO)
+ self.sys_hardware.policy.get_cgroups_rules(PolicyGroup.USB)
) )
@property @property
def volumes(self) -> Dict[str, Dict[str, str]]: def volumes(self) -> Dict[str, Dict[str, str]]:
"""Return Volumes for the mount.""" """Return Volumes for the mount."""
volumes = { volumes = {
"/dev": {"bind": "/dev", "mode": "ro"},
"/run/dbus": {"bind": "/run/dbus", "mode": "ro"}, "/run/dbus": {"bind": "/run/dbus", "mode": "ro"},
"/run/udev": {"bind": "/run/udev", "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 return volumes
def _run(self) -> None: def _run(self) -> None:

View File

@ -35,3 +35,10 @@ class PolicyGroup(str, Enum):
USB = "usb" USB = "usb"
VIDEO = "video" VIDEO = "video"
AUDIO = "audio" AUDIO = "audio"
class UdevAction(str, Enum):
"""Udev device action."""
ADD = "add"
REMOVE = "remove"

View File

@ -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,
)

View File

@ -9,7 +9,6 @@ from supervisor.hardware.const import UdevSubsystem
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HardwareNotFound from ..exceptions import HardwareNotFound
from .container import HwContainer
from .data import Device from .data import Device
from .helper import HwHelper from .helper import HwHelper
from .monitor import HwMonitor from .monitor import HwMonitor
@ -30,7 +29,6 @@ class HardwareManager(CoreSysAttributes):
self._montior: HwMonitor = HwMonitor(coresys) self._montior: HwMonitor = HwMonitor(coresys)
self._helper: HwHelper = HwHelper(coresys) self._helper: HwHelper = HwHelper(coresys)
self._policy: HwPolicy = HwPolicy(coresys) self._policy: HwPolicy = HwPolicy(coresys)
self._container: HwContainer = HwContainer(coresys)
@property @property
def monitor(self) -> HwMonitor: def monitor(self) -> HwMonitor:
@ -47,11 +45,6 @@ class HardwareManager(CoreSysAttributes):
"""Return Hardware policy instance.""" """Return Hardware policy instance."""
return self._policy return self._policy
@property
def container(self) -> HwContainer:
"""Return Hardware container instance."""
return self._container
@property @property
def devices(self) -> List[Device]: def devices(self) -> List[Device]:
"""Return List of devices.""" """Return List of devices."""

View File

@ -1,4 +1,5 @@
"""Supervisor Hardware monitor based on udev.""" """Supervisor Hardware monitor based on udev."""
from contextlib import suppress
import logging import logging
from pathlib import Path from pathlib import Path
from pprint import pformat from pprint import pformat
@ -8,8 +9,9 @@ import pyudev
from ..const import CoreState from ..const import CoreState
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HardwareNotFound
from ..resolution.const import UnhealthyReason from ..resolution.const import UnhealthyReason
from .const import UdevSubsystem from .const import PolicyGroup, UdevAction, UdevSubsystem
from .data import Device from .data import Device
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -45,82 +47,107 @@ class HwMonitor(CoreSysAttributes):
self.observer.stop() self.observer.stop()
_LOGGER.info("Stopped Supervisor hardware monitor") _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. """Incomming events from udev.
This is inside a observe thread and need pass into our eventloop. This is inside a observe thread and need pass into our eventloop.
""" """
_LOGGER.debug("Hardware monitor: %s - %s", action, pformat(device)) _LOGGER.debug("Hardware monitor: %s - %s", action, pformat(kernel))
self.sys_loop.call_soon_threadsafe(self._async_udev_events, action, device) 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.""" """Incomming events from udev into loop."""
# Update device List # Update device List
if not udev_device.device_node or self.sys_hardware.helper.hide_virtual_device( if not kernel.device_node or self.sys_hardware.helper.hide_virtual_device(
udev_device kernel
): ):
return return
device = Device( forward_action = None
udev_device.sys_name, device = None
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},
)
# 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) self.sys_hardware.update_device(device)
if action == "remove": forward_action = UdevAction.ADD
self.sys_hardware.delete_device(device)
# Process device # Process Action
if self.sys_core.state in (CoreState.RUNNING, CoreState.FREEZE): if (
device
and forward_action
and self.sys_core.state in (CoreState.RUNNING, CoreState.FREEZE)
):
# New Sound device # New Sound device
if device.subsystem == UdevSubsystem.AUDIO and action == "add": if device.subsystem == UdevSubsystem.AUDIO:
self._action_sound_add(device) self._action_sound(device, forward_action)
# New serial device # serial device
if device.subsystem == UdevSubsystem.SERIAL and action == "add": elif device.subsystem == UdevSubsystem.SERIAL:
self._action_tty_add(device) self._action_tty(device, forward_action)
# New input device # input device
if device.subsystem == UdevSubsystem.INPUT and action == "add": elif device.subsystem == UdevSubsystem.INPUT:
self._action_input_add(device) 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.""" """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()) 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.""" """Process tty actions."""
_LOGGER.info( if not device.by_id or not self.sys_hardware.policy.is_match_cgroup(
"Detecting changed serial hardware %s - %s", device.path, device.by_id PolicyGroup.UART, device
) ):
if not device.by_id:
return return
_LOGGER.info(
# Start process TTY "Detecting %s serial hardware %s - %s", action, device.path, device.by_id
self.sys_loop.call_later(
2,
self.sys_create_task,
self.sys_hardware.container.process_serial_device(device),
) )
def _action_input_add(self, device: Device): def _action_input(self, device: Device, action: UdevAction):
"""Process input actions.""" """Process input actions."""
_LOGGER.info(
"Detecting changed serial hardware %s - %s", device.path, device.by_id
)
if not device.by_id: if not device.by_id:
return return
_LOGGER.info(
# Start process input "Detecting %s serial hardware %s - %s", action, device.path, device.by_id
self.sys_loop.call_later(
2,
self.sys_create_task,
self.sys_hardware.container.process_serial_device(device),
) )
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)

View File

@ -14,6 +14,7 @@ _GROUP_CGROUPS: Dict[PolicyGroup, List[int]] = {
PolicyGroup.GPIO: [254, 245], PolicyGroup.GPIO: [254, 245],
PolicyGroup.VIDEO: [239, 29, 81, 251, 242, 226], PolicyGroup.VIDEO: [239, 29, 81, 251, 242, 226],
PolicyGroup.AUDIO: [116], PolicyGroup.AUDIO: [116],
PolicyGroup.USB: [189],
} }
@ -24,6 +25,10 @@ class HwPolicy(CoreSysAttributes):
"""Init hardware policy object.""" """Init hardware policy object."""
self.coresys = coresys 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]: def get_cgroups_rules(self, group: PolicyGroup) -> List[str]:
"""Generate cgroups rules for a policy group.""" """Generate cgroups rules for a policy group."""
return [f"c {dev}:* rwm" for dev in _GROUP_CGROUPS.get(group, [])] return [f"c {dev}:* rwm" for dev in _GROUP_CGROUPS.get(group, [])]

View File

@ -31,3 +31,18 @@ def test_policy_group(coresys):
"c 242:* rwm", "c 242:* rwm",
"c 226:* 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)