Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support dynamic device access cgroup #3421

Merged
merged 5 commits into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions supervisor/bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
_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."""

Expand All @@ -34,10 +42,9 @@ def fire_event(self, event: BusEvent, reference: Any) -> None:
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)
59 changes: 57 additions & 2 deletions supervisor/docker/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import requests

from ..addons.build import AddonBuild
from ..bus import EventListener
from ..const import (
DOCKER_CPU_RUNTIME_ALLOCATION,
ENV_TIME,
Expand All @@ -28,10 +29,19 @@
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
Expand All @@ -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:
Expand Down Expand Up @@ -495,6 +507,12 @@ def _run(self) -> None:
_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:
Expand Down Expand Up @@ -636,15 +654,52 @@ def _stop(self, remove_container=True) -> None:

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)
)
pvizeli marked this conversation as resolved.
Show resolved Hide resolved
except DBusError as err:
raise DockerError(
f"Can't cgroup permission on the host for {self.name}",
_LOGGER.error,
) from err
26 changes: 26 additions & 0 deletions tests/test_bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,29 @@ async def callback(data) -> 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"