Support dynamic device access cgroup (#3421)

* Support dynamic device access cgroup

* Clean listener better

* Update supervisor/docker/addon.py

Co-authored-by: Stefan Agner <stefan@agner.ch>

* Update addon.py

* Fix black

Co-authored-by: Stefan Agner <stefan@agner.ch>
This commit is contained in:
Pascal Vizeli 2022-01-26 16:48:23 +01:00 committed by GitHub
parent 724eaddf19
commit caacb421c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 100 additions and 9 deletions

View File

@ -12,6 +12,14 @@ from .coresys import CoreSys, CoreSysAttributes
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@attr.s(slots=True, frozen=True)
class EventListener:
"""Event listener."""
event_type: BusEvent = attr.ib()
callback: Callable[[Any], Awaitable[None]] = attr.ib()
class Bus(CoreSysAttributes): class Bus(CoreSysAttributes):
"""Handle Bus event system.""" """Handle Bus event system."""
@ -34,10 +42,9 @@ class Bus(CoreSysAttributes):
for listener in self._listeners.get(event, []): for listener in self._listeners.get(event, []):
self.sys_create_task(listener.callback(reference)) self.sys_create_task(listener.callback(reference))
def remove_listener(self, listener: EventListener) -> None:
@attr.s(slots=True, frozen=True) """Unregister an listener."""
class EventListener: try:
"""Event listener.""" self._listeners[listener.event_type].remove(listener)
except (ValueError, KeyError):
event_type: BusEvent = attr.ib() _LOGGER.warning("Listener %s not registered", listener)
callback: Callable[[Any], Awaitable[None]] = attr.ib()

View File

@ -13,6 +13,7 @@ import docker
import requests import requests
from ..addons.build import AddonBuild from ..addons.build import AddonBuild
from ..bus import EventListener
from ..const import ( from ..const import (
DOCKER_CPU_RUNTIME_ALLOCATION, DOCKER_CPU_RUNTIME_ALLOCATION,
ENV_TIME, ENV_TIME,
@ -28,10 +29,19 @@ from ..const import (
SECURITY_PROFILE, SECURITY_PROFILE,
SYSTEMD_JOURNAL_PERSISTENT, SYSTEMD_JOURNAL_PERSISTENT,
SYSTEMD_JOURNAL_VOLATILE, SYSTEMD_JOURNAL_VOLATILE,
BusEvent,
) )
from ..coresys import CoreSys from ..coresys import CoreSys
from ..exceptions import CoreDNSError, DockerError, DockerNotFound, HardwareNotFound from ..exceptions import (
CoreDNSError,
DBusError,
DockerError,
DockerNotFound,
HardwareNotFound,
)
from ..hardware.const import PolicyGroup from ..hardware.const import PolicyGroup
from ..hardware.data import Device
from ..jobs.decorator import Job, JobCondition
from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils import process_lock from ..utils import process_lock
from .const import Capabilities from .const import Capabilities
@ -52,7 +62,9 @@ class DockerAddon(DockerInterface):
def __init__(self, coresys: CoreSys, addon: Addon): def __init__(self, coresys: CoreSys, addon: Addon):
"""Initialize Docker Home Assistant wrapper.""" """Initialize Docker Home Assistant wrapper."""
super().__init__(coresys) super().__init__(coresys)
self.addon = addon self.addon: Addon = addon
self._hw_listener: EventListener | None = None
@property @property
def image(self) -> str | None: def image(self) -> str | None:
@ -495,6 +507,12 @@ class DockerAddon(DockerInterface):
_LOGGER.warning("Can't update DNS for %s", self.name) _LOGGER.warning("Can't update DNS for %s", self.name)
self.sys_capture_exception(err) self.sys_capture_exception(err)
# Hardware Access
if self.addon.static_devices:
self._hw_listener = self.sys_bus.register_event(
BusEvent.HARDWARE_NEW_DEVICE, self._hardware_events
)
def _install( def _install(
self, version: AwesomeVersion, image: str | None = None, latest: bool = False self, version: AwesomeVersion, image: str | None = None, latest: bool = False
) -> None: ) -> None:
@ -636,15 +654,55 @@ class DockerAddon(DockerInterface):
Need run inside executor. Need run inside executor.
""" """
# DNS
if self.ip_address != NO_ADDDRESS: if self.ip_address != NO_ADDDRESS:
try: try:
self.sys_plugins.dns.delete_host(self.addon.hostname) self.sys_plugins.dns.delete_host(self.addon.hostname)
except CoreDNSError as err: except CoreDNSError as err:
_LOGGER.warning("Can't update DNS for %s", self.name) _LOGGER.warning("Can't update DNS for %s", self.name)
self.sys_capture_exception(err) self.sys_capture_exception(err)
# Hardware
if self._hw_listener:
self.sys_bus.remove_listener(self._hw_listener)
self._hw_listener = None
super()._stop(remove_container) super()._stop(remove_container)
def _validate_trust( def _validate_trust(
self, image_id: str, image: str, version: AwesomeVersion self, image_id: str, image: str, version: AwesomeVersion
) -> None: ) -> None:
"""Validate trust of content.""" """Validate trust of content."""
@Job(conditions=[JobCondition.OS_AGENT])
async def _hardware_events(self, device: Device) -> None:
"""Process Hardware events for adjust device access."""
if not any(
device_path in (device.path, device.sysfs)
for device_path in self.addon.static_devices
):
return
try:
docker_container = self.sys_docker.containers.get(self.name)
except docker.errors.NotFound:
self.sys_bus.remove_listener(self._hw_listener)
self._hw_listener = None
return
except (docker.errors.DockerException, requests.RequestException) as err:
raise DockerError(
f"Can't process Hardware Event on {self.name}: {err!s}", _LOGGER.error
) from err
try:
await self.sys_dbus.agent.cgroup.add_devices_allowed(
docker_container.id, self.sys_hardware.policy.get_cgroups_rule(device)
)
_LOGGER.info(
"Added cgroup permissions for device %s to %s", device.path, self.name
)
except DBusError as err:
raise DockerError(
f"Can't set cgroup permission on the host for {self.name}",
_LOGGER.error,
) from err

View File

@ -42,3 +42,29 @@ async def test_bus_event_not_called(coresys: CoreSys) -> None:
coresys.bus.fire_event(BusEvent.HARDWARE_REMOVE_DEVICE, None) coresys.bus.fire_event(BusEvent.HARDWARE_REMOVE_DEVICE, None)
await asyncio.sleep(0) await asyncio.sleep(0)
assert len(results) == 0 assert len(results) == 0
@pytest.mark.asyncio
async def test_bus_event_removed(coresys: CoreSys) -> None:
"""Test bus events over the backend and remove."""
results = []
async def callback(data) -> None:
"""Test callback."""
results.append(data)
listener = coresys.bus.register_event(BusEvent.HARDWARE_NEW_DEVICE, callback)
coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, None)
await asyncio.sleep(0)
assert results[-1] is None
coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, "test")
await asyncio.sleep(0)
assert results[-1] == "test"
coresys.bus.remove_listener(listener)
coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, None)
await asyncio.sleep(0)
assert results[-1] == "test"