ha-supervisor/supervisor/docker/homeassistant.py

256 lines
8.5 KiB
Python

"""Init file for Supervisor Docker object."""
from collections.abc import Awaitable
from ipaddress import IPv4Address
import logging
from awesomeversion import AwesomeVersion, AwesomeVersionCompareException
from docker.types import Mount
from ..const import LABEL_MACHINE, MACHINE_ID
from ..exceptions import DockerJobError
from ..hardware.const import PolicyGroup
from ..homeassistant.const import LANDINGPAGE
from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job
from .const import (
ENV_TIME,
ENV_TOKEN,
ENV_TOKEN_OLD,
MOUNT_DBUS,
MOUNT_DEV,
MOUNT_MACHINE_ID,
MOUNT_UDEV,
MountType,
PropagationMode,
)
from .interface import CommandReturn, DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__)
_VERIFY_TRUST: AwesomeVersion = AwesomeVersion("2021.5.0")
_HASS_DOCKER_NAME: str = "homeassistant"
class DockerHomeAssistant(DockerInterface):
"""Docker Supervisor wrapper for Home Assistant."""
@property
def machine(self) -> str | None:
"""Return machine of Home Assistant Docker image."""
if self._meta and LABEL_MACHINE in self._meta["Config"]["Labels"]:
return self._meta["Config"]["Labels"][LABEL_MACHINE]
return None
@property
def image(self) -> str:
"""Return name of Docker image."""
return self.sys_homeassistant.image
@property
def name(self) -> str:
"""Return name of Docker container."""
return _HASS_DOCKER_NAME
@property
def timeout(self) -> int:
"""Return timeout for Docker actions."""
# Synchronized homeassistant's S6_SERVICES_GRACETIME
# to avoid killing Home Assistant Core
return 220 + 20
@property
def ip_address(self) -> IPv4Address:
"""Return IP address of this container."""
return self.sys_docker.network.gateway
@property
def cgroups_rules(self) -> list[str]:
"""Return a list of needed cgroups permission."""
return (
[]
if self.sys_homeassistant.version == LANDINGPAGE
else (
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 mounts(self) -> list[Mount]:
"""Return mounts for container."""
mounts = [
MOUNT_DEV,
MOUNT_DBUS,
MOUNT_UDEV,
# HA config folder
Mount(
type=MountType.BIND,
source=self.sys_config.path_extern_homeassistant.as_posix(),
target="/config",
read_only=False,
),
]
# Landingpage does not need all this access
if self.sys_homeassistant.version != LANDINGPAGE:
mounts.extend(
[
# All other folders
Mount(
type=MountType.BIND,
source=self.sys_config.path_extern_ssl.as_posix(),
target="/ssl",
read_only=True,
),
Mount(
type=MountType.BIND,
source=self.sys_config.path_extern_share.as_posix(),
target="/share",
read_only=False,
propagation=PropagationMode.RSLAVE.value,
),
Mount(
type=MountType.BIND,
source=self.sys_config.path_extern_media.as_posix(),
target="/media",
read_only=False,
propagation=PropagationMode.RSLAVE.value,
),
# Configuration audio
Mount(
type=MountType.BIND,
source=self.sys_homeassistant.path_extern_pulse.as_posix(),
target="/etc/pulse/client.conf",
read_only=True,
),
Mount(
type=MountType.BIND,
source=self.sys_plugins.audio.path_extern_pulse.as_posix(),
target="/run/audio",
read_only=True,
),
Mount(
type=MountType.BIND,
source=self.sys_plugins.audio.path_extern_asound.as_posix(),
target="/etc/asound.conf",
read_only=True,
),
]
)
# Machine ID
if MACHINE_ID.exists():
mounts.append(MOUNT_MACHINE_ID)
return mounts
@Job(
name="docker_home_assistant_run",
limit=JobExecutionLimit.GROUP_ONCE,
on_condition=DockerJobError,
)
async def run(self) -> None:
"""Run Docker image."""
if await self.is_running():
return
# Cleanup
await self.stop()
# Create & Run container
docker_container = await self.sys_run_in_executor(
self.sys_docker.run,
self.image,
tag=(self.sys_homeassistant.version),
name=self.name,
hostname=self.name,
detach=True,
privileged=self.sys_homeassistant.version != LANDINGPAGE,
init=False,
security_opt=self.security_opt,
network_mode="host",
mounts=self.mounts,
device_cgroup_rules=self.cgroups_rules,
extra_hosts={
"supervisor": self.sys_docker.network.supervisor,
"observer": self.sys_docker.network.observer,
},
environment={
"SUPERVISOR": self.sys_docker.network.supervisor,
"HASSIO": self.sys_docker.network.supervisor,
ENV_TIME: self.sys_timezone,
ENV_TOKEN: self.sys_homeassistant.supervisor_token,
ENV_TOKEN_OLD: self.sys_homeassistant.supervisor_token,
},
tmpfs={"/tmp": ""},
oom_score_adj=-300,
)
self._meta = docker_container.attrs
_LOGGER.info(
"Starting Home Assistant %s with version %s", self.image, self.version
)
@Job(
name="docker_home_assistant_execute_command",
limit=JobExecutionLimit.GROUP_ONCE,
on_condition=DockerJobError,
)
async def execute_command(self, command: str) -> CommandReturn:
"""Create a temporary container and run command."""
return await self.sys_run_in_executor(
self.sys_docker.run_command,
self.image,
version=self.sys_homeassistant.version,
command=command,
privileged=True,
init=True,
entrypoint=[],
detach=True,
stdout=True,
stderr=True,
mounts=[
Mount(
type=MountType.BIND,
source=self.sys_config.path_extern_homeassistant.as_posix(),
target="/config",
read_only=False,
),
Mount(
type=MountType.BIND,
source=self.sys_config.path_extern_ssl.as_posix(),
target="/ssl",
read_only=True,
),
Mount(
type=MountType.BIND,
source=self.sys_config.path_extern_share.as_posix(),
target="/share",
read_only=False,
),
],
environment={ENV_TIME: self.sys_timezone},
)
def is_initialize(self) -> Awaitable[bool]:
"""Return True if Docker container exists."""
return self.sys_run_in_executor(
self.sys_docker.container_is_initialized,
self.name,
self.image,
self.sys_homeassistant.version,
)
async def _validate_trust(
self, image_id: str, image: str, version: AwesomeVersion
) -> None:
"""Validate trust of content."""
try:
if version != LANDINGPAGE and version < _VERIFY_TRUST:
return
except AwesomeVersionCompareException:
return
await super()._validate_trust(image_id, image, version)