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

View File

@ -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:

View File

@ -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:

View File

@ -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"

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 ..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."""

View File

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

View File

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

View File

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