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__)
@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):
"""Handle Bus event system."""
@ -34,10 +42,9 @@ class Bus(CoreSysAttributes):
for listener in self._listeners.get(event, []):
self.sys_create_task(listener.callback(reference))
@attr.s(slots=True, frozen=True)
class EventListener:
"""Event listener."""
event_type: BusEvent = attr.ib()
callback: Callable[[Any], Awaitable[None]] = attr.ib()
def remove_listener(self, listener: EventListener) -> None:
"""Unregister an listener."""
try:
self._listeners[listener.event_type].remove(listener)
except (ValueError, KeyError):
_LOGGER.warning("Listener %s not registered", listener)

View File

@ -13,6 +13,7 @@ import docker
import requests
from ..addons.build import AddonBuild
from ..bus import EventListener
from ..const import (
DOCKER_CPU_RUNTIME_ALLOCATION,
ENV_TIME,
@ -28,10 +29,19 @@ from ..const import (
SECURITY_PROFILE,
SYSTEMD_JOURNAL_PERSISTENT,
SYSTEMD_JOURNAL_VOLATILE,
BusEvent,
)
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.data import Device
from ..jobs.decorator import Job, JobCondition
from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils import process_lock
from .const import Capabilities
@ -52,7 +62,9 @@ class DockerAddon(DockerInterface):
def __init__(self, coresys: CoreSys, addon: Addon):
"""Initialize Docker Home Assistant wrapper."""
super().__init__(coresys)
self.addon = addon
self.addon: Addon = addon
self._hw_listener: EventListener | None = None
@property
def image(self) -> str | None:
@ -495,6 +507,12 @@ class DockerAddon(DockerInterface):
_LOGGER.warning("Can't update DNS for %s", self.name)
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(
self, version: AwesomeVersion, image: str | None = None, latest: bool = False
) -> None:
@ -636,15 +654,55 @@ class DockerAddon(DockerInterface):
Need run inside executor.
"""
# DNS
if self.ip_address != NO_ADDDRESS:
try:
self.sys_plugins.dns.delete_host(self.addon.hostname)
except CoreDNSError as err:
_LOGGER.warning("Can't update DNS for %s", self.name)
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)
def _validate_trust(
self, image_id: str, image: str, version: AwesomeVersion
) -> None:
"""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)
await asyncio.sleep(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"