Set bind propagation mode for media (#4308)
* Set bind propagation mode for media * Add some test cases
This commit is contained in:
parent
bb497c0c9f
commit
a7c1693911
|
@ -532,14 +532,14 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||
return ATTR_IMAGE not in self.data
|
||||
|
||||
@property
|
||||
def map_volumes(self) -> dict[str, str]:
|
||||
"""Return a dict of {volume: policy} from add-on."""
|
||||
def map_volumes(self) -> dict[str, bool]:
|
||||
"""Return a dict of {volume: read-only} from add-on."""
|
||||
volumes = {}
|
||||
for volume in self.data[ATTR_MAP]:
|
||||
result = RE_VOLUME.match(volume)
|
||||
if not result:
|
||||
continue
|
||||
volumes[result.group(1)] = result.group(2) or "ro"
|
||||
volumes[result.group(1)] = result.group(2) != "rw"
|
||||
|
||||
return volumes
|
||||
|
||||
|
|
|
@ -181,9 +181,9 @@ class CoreConfig(FileConfiguration):
|
|||
return PurePath(os.environ[ENV_SUPERVISOR_SHARE])
|
||||
|
||||
@property
|
||||
def path_extern_homeassistant(self) -> str:
|
||||
def path_extern_homeassistant(self) -> PurePath:
|
||||
"""Return config path external for Docker."""
|
||||
return str(PurePath(self.path_extern_supervisor, HOMEASSISTANT_CONFIG))
|
||||
return PurePath(self.path_extern_supervisor, HOMEASSISTANT_CONFIG)
|
||||
|
||||
@property
|
||||
def path_homeassistant(self) -> Path:
|
||||
|
@ -191,9 +191,9 @@ class CoreConfig(FileConfiguration):
|
|||
return self.path_supervisor / HOMEASSISTANT_CONFIG
|
||||
|
||||
@property
|
||||
def path_extern_ssl(self) -> str:
|
||||
def path_extern_ssl(self) -> PurePath:
|
||||
"""Return SSL path external for Docker."""
|
||||
return str(PurePath(self.path_extern_supervisor, HASSIO_SSL))
|
||||
return PurePath(self.path_extern_supervisor, HASSIO_SSL)
|
||||
|
||||
@property
|
||||
def path_ssl(self) -> Path:
|
||||
|
@ -291,9 +291,9 @@ class CoreConfig(FileConfiguration):
|
|||
return PurePath(self.path_extern_supervisor, SHARE_DATA)
|
||||
|
||||
@property
|
||||
def path_extern_dns(self) -> str:
|
||||
def path_extern_dns(self) -> PurePath:
|
||||
"""Return dns path external for Docker."""
|
||||
return str(PurePath(self.path_extern_supervisor, DNS_DATA))
|
||||
return PurePath(self.path_extern_supervisor, DNS_DATA)
|
||||
|
||||
@property
|
||||
def path_dns(self) -> Path:
|
||||
|
|
|
@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from awesomeversion import AwesomeVersion
|
||||
import docker
|
||||
from docker.types import Mount
|
||||
import requests
|
||||
|
||||
from ..addons.build import AddonBuild
|
||||
|
@ -46,12 +47,16 @@ from ..resolution.const import ContextType, IssueType, SuggestionType
|
|||
from ..utils import process_lock
|
||||
from ..utils.sentry import capture_exception
|
||||
from .const import (
|
||||
DBUS_PATH,
|
||||
DBUS_VOLUME,
|
||||
ENV_TIME,
|
||||
ENV_TOKEN,
|
||||
ENV_TOKEN_OLD,
|
||||
MOUNT_DBUS,
|
||||
MOUNT_DEV,
|
||||
MOUNT_DOCKER,
|
||||
MOUNT_UDEV,
|
||||
Capabilities,
|
||||
MountType,
|
||||
PropagationMode,
|
||||
)
|
||||
from .interface import DockerInterface
|
||||
|
||||
|
@ -320,74 +325,80 @@ class DockerAddon(DockerInterface):
|
|||
return None
|
||||
|
||||
@property
|
||||
def volumes(self) -> dict[str, dict[str, str]]:
|
||||
"""Generate volumes for mappings."""
|
||||
def mounts(self) -> list[Mount]:
|
||||
"""Return mounts for container."""
|
||||
addon_mapping = self.addon.map_volumes
|
||||
|
||||
volumes = {
|
||||
"/dev": {"bind": "/dev", "mode": "ro"},
|
||||
str(self.addon.path_extern_data): {"bind": "/data", "mode": "rw"},
|
||||
}
|
||||
mounts = [
|
||||
MOUNT_DEV,
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.addon.path_extern_data.as_posix(),
|
||||
target="/data",
|
||||
read_only=False,
|
||||
),
|
||||
]
|
||||
|
||||
# setup config mappings
|
||||
if MAP_CONFIG in addon_mapping:
|
||||
volumes.update(
|
||||
{
|
||||
str(self.sys_config.path_extern_homeassistant): {
|
||||
"bind": "/config",
|
||||
"mode": addon_mapping[MAP_CONFIG],
|
||||
}
|
||||
}
|
||||
mounts.append(
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_homeassistant.as_posix(),
|
||||
target="/config",
|
||||
read_only=addon_mapping[MAP_CONFIG],
|
||||
)
|
||||
)
|
||||
|
||||
if MAP_SSL in addon_mapping:
|
||||
volumes.update(
|
||||
{
|
||||
str(self.sys_config.path_extern_ssl): {
|
||||
"bind": "/ssl",
|
||||
"mode": addon_mapping[MAP_SSL],
|
||||
}
|
||||
}
|
||||
mounts.append(
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_ssl.as_posix(),
|
||||
target="/ssl",
|
||||
read_only=addon_mapping[MAP_SSL],
|
||||
)
|
||||
)
|
||||
|
||||
if MAP_ADDONS in addon_mapping:
|
||||
volumes.update(
|
||||
{
|
||||
str(self.sys_config.path_extern_addons_local): {
|
||||
"bind": "/addons",
|
||||
"mode": addon_mapping[MAP_ADDONS],
|
||||
}
|
||||
}
|
||||
mounts.append(
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_addons_local.as_posix(),
|
||||
target="/addons",
|
||||
read_only=addon_mapping[MAP_ADDONS],
|
||||
)
|
||||
)
|
||||
|
||||
if MAP_BACKUP in addon_mapping:
|
||||
volumes.update(
|
||||
{
|
||||
str(self.sys_config.path_extern_backup): {
|
||||
"bind": "/backup",
|
||||
"mode": addon_mapping[MAP_BACKUP],
|
||||
}
|
||||
}
|
||||
mounts.append(
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_backup.as_posix(),
|
||||
target="/backup",
|
||||
read_only=addon_mapping[MAP_BACKUP],
|
||||
)
|
||||
)
|
||||
|
||||
if MAP_SHARE in addon_mapping:
|
||||
volumes.update(
|
||||
{
|
||||
str(self.sys_config.path_extern_share): {
|
||||
"bind": "/share",
|
||||
"mode": addon_mapping[MAP_SHARE],
|
||||
}
|
||||
}
|
||||
mounts.append(
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_share.as_posix(),
|
||||
target="/share",
|
||||
read_only=addon_mapping[MAP_SHARE],
|
||||
)
|
||||
)
|
||||
|
||||
if MAP_MEDIA in addon_mapping:
|
||||
volumes.update(
|
||||
{
|
||||
str(self.sys_config.path_extern_media): {
|
||||
"bind": "/media",
|
||||
"mode": addon_mapping[MAP_MEDIA],
|
||||
}
|
||||
}
|
||||
mounts.append(
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_media.as_posix(),
|
||||
target="/media",
|
||||
read_only=addon_mapping[MAP_MEDIA],
|
||||
propagation=PropagationMode.SLAVE.value,
|
||||
)
|
||||
)
|
||||
|
||||
# Init other hardware mappings
|
||||
|
@ -397,72 +408,90 @@ class DockerAddon(DockerInterface):
|
|||
for gpio_path in ("/sys/class/gpio", "/sys/devices/platform/soc"):
|
||||
if not Path(gpio_path).exists():
|
||||
continue
|
||||
volumes.update({gpio_path: {"bind": gpio_path, "mode": "rw"}})
|
||||
mounts.append(
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=gpio_path,
|
||||
target=gpio_path,
|
||||
read_only=False,
|
||||
)
|
||||
)
|
||||
|
||||
# DeviceTree support
|
||||
if self.addon.with_devicetree:
|
||||
volumes.update(
|
||||
{
|
||||
"/sys/firmware/devicetree/base": {
|
||||
"bind": "/device-tree",
|
||||
"mode": "ro",
|
||||
}
|
||||
}
|
||||
mounts.append(
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source="/sys/firmware/devicetree/base",
|
||||
target="/device-tree",
|
||||
read_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Host udev support
|
||||
if self.addon.with_udev:
|
||||
volumes.update({"/run/udev": {"bind": "/run/udev", "mode": "ro"}})
|
||||
mounts.append(MOUNT_UDEV)
|
||||
|
||||
# Kernel Modules support
|
||||
if self.addon.with_kernel_modules:
|
||||
volumes.update({"/lib/modules": {"bind": "/lib/modules", "mode": "ro"}})
|
||||
mounts.append(
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source="/lib/modules",
|
||||
target="/lib/modules",
|
||||
read_only=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Docker API support
|
||||
if not self.addon.protected and self.addon.access_docker_api:
|
||||
volumes.update(
|
||||
{"/run/docker.sock": {"bind": "/run/docker.sock", "mode": "ro"}}
|
||||
)
|
||||
mounts.append(MOUNT_DOCKER)
|
||||
|
||||
# Host D-Bus system
|
||||
if self.addon.host_dbus:
|
||||
volumes.update({DBUS_PATH: DBUS_VOLUME})
|
||||
mounts.append(MOUNT_DBUS)
|
||||
|
||||
# Configuration Audio
|
||||
if self.addon.with_audio:
|
||||
volumes.update(
|
||||
{
|
||||
str(self.addon.path_extern_pulse): {
|
||||
"bind": "/etc/pulse/client.conf",
|
||||
"mode": "ro",
|
||||
},
|
||||
str(self.sys_plugins.audio.path_extern_pulse): {
|
||||
"bind": "/run/audio",
|
||||
"mode": "ro",
|
||||
},
|
||||
str(self.sys_plugins.audio.path_extern_asound): {
|
||||
"bind": "/etc/asound.conf",
|
||||
"mode": "ro",
|
||||
},
|
||||
}
|
||||
)
|
||||
mounts += [
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_homeassistant.path_extern_pulse.as_posix(),
|
||||
target="/etc/pulse/client.conf",
|
||||
read_only=True,
|
||||
),
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_plugins.audio.path_extern_pulse.as_posix(),
|
||||
target="/run/audio",
|
||||
read_only=True,
|
||||
),
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_plugins.audio.path_extern_asound.as_posix(),
|
||||
target="/etc/asound.conf",
|
||||
read_only=True,
|
||||
),
|
||||
]
|
||||
|
||||
# System Journal access
|
||||
if self.addon.with_journald:
|
||||
volumes.update(
|
||||
{
|
||||
str(SYSTEMD_JOURNAL_PERSISTENT): {
|
||||
"bind": str(SYSTEMD_JOURNAL_PERSISTENT),
|
||||
"mode": "ro",
|
||||
},
|
||||
str(SYSTEMD_JOURNAL_VOLATILE): {
|
||||
"bind": str(SYSTEMD_JOURNAL_VOLATILE),
|
||||
"mode": "ro",
|
||||
},
|
||||
}
|
||||
)
|
||||
mounts += [
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=SYSTEMD_JOURNAL_PERSISTENT.as_posix(),
|
||||
target=SYSTEMD_JOURNAL_PERSISTENT.as_posix(),
|
||||
read_only=True,
|
||||
),
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=SYSTEMD_JOURNAL_VOLATILE.as_posix(),
|
||||
target=SYSTEMD_JOURNAL_VOLATILE.as_posix(),
|
||||
read_only=True,
|
||||
),
|
||||
]
|
||||
|
||||
return volumes
|
||||
return mounts
|
||||
|
||||
def _run(self) -> None:
|
||||
"""Run Docker image.
|
||||
|
@ -503,7 +532,7 @@ class DockerAddon(DockerInterface):
|
|||
cpu_rt_runtime=self.cpu_rt_runtime,
|
||||
security_opt=self.security_opt,
|
||||
environment=self.environment,
|
||||
volumes=self.volumes,
|
||||
mounts=self.mounts,
|
||||
tmpfs=self.tmpfs,
|
||||
oom_score_adj=200,
|
||||
)
|
||||
|
|
|
@ -2,11 +2,20 @@
|
|||
import logging
|
||||
|
||||
import docker
|
||||
from docker.types import Mount
|
||||
|
||||
from ..const import DOCKER_CPU_RUNTIME_ALLOCATION, MACHINE_ID
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..hardware.const import PolicyGroup
|
||||
from .const import ENV_TIME, Capabilities
|
||||
from .const import (
|
||||
ENV_TIME,
|
||||
MOUNT_DBUS,
|
||||
MOUNT_DEV,
|
||||
MOUNT_MACHINE_ID,
|
||||
MOUNT_UDEV,
|
||||
Capabilities,
|
||||
MountType,
|
||||
)
|
||||
from .interface import DockerInterface
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
@ -28,20 +37,25 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
|
|||
return AUDIO_DOCKER_NAME
|
||||
|
||||
@property
|
||||
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"},
|
||||
}
|
||||
def mounts(self) -> list[Mount]:
|
||||
"""Return mounts for container."""
|
||||
mounts = [
|
||||
MOUNT_DEV,
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_audio.as_posix(),
|
||||
target="/data",
|
||||
read_only=False,
|
||||
),
|
||||
MOUNT_DBUS,
|
||||
MOUNT_UDEV,
|
||||
]
|
||||
|
||||
# Machine ID
|
||||
if MACHINE_ID.exists():
|
||||
volumes.update({str(MACHINE_ID): {"bind": str(MACHINE_ID), "mode": "ro"}})
|
||||
mounts.append(MOUNT_MACHINE_ID)
|
||||
|
||||
return volumes
|
||||
return mounts
|
||||
|
||||
@property
|
||||
def cgroups_rules(self) -> list[str]:
|
||||
|
@ -96,7 +110,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
|
|||
environment={
|
||||
ENV_TIME: self.sys_timezone,
|
||||
},
|
||||
volumes=self.volumes,
|
||||
mounts=self.mounts,
|
||||
)
|
||||
|
||||
self._meta = docker_container.attrs
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
"""Docker constants."""
|
||||
from enum import Enum
|
||||
|
||||
from docker.types import Mount
|
||||
|
||||
from ..const import MACHINE_ID
|
||||
|
||||
|
||||
class Capabilities(str, Enum):
|
||||
"""Linux Capabilities."""
|
||||
|
@ -40,11 +44,50 @@ class RestartPolicy(str, Enum):
|
|||
ALWAYS = "always"
|
||||
|
||||
|
||||
DBUS_PATH = "/run/dbus"
|
||||
DBUS_VOLUME = {"bind": DBUS_PATH, "mode": "ro"}
|
||||
class MountType(str, Enum):
|
||||
"""Mount type."""
|
||||
|
||||
BIND = "bind"
|
||||
VOLUME = "volume"
|
||||
TMPFS = "tmpfs"
|
||||
NPIPE = "npipe"
|
||||
|
||||
|
||||
class PropagationMode(str, Enum):
|
||||
"""Propagataion mode, only for bind type mounts."""
|
||||
|
||||
PRIVATE = "private"
|
||||
SHARED = "shared"
|
||||
SLAVE = "slave"
|
||||
RPRIVATE = "rprivate"
|
||||
RSHARED = "rshared"
|
||||
RSLAVE = "rslave"
|
||||
|
||||
|
||||
ENV_TIME = "TZ"
|
||||
ENV_TOKEN = "SUPERVISOR_TOKEN"
|
||||
ENV_TOKEN_OLD = "HASSIO_TOKEN"
|
||||
|
||||
LABEL_MANAGED = "supervisor_managed"
|
||||
|
||||
MOUNT_DBUS = Mount(
|
||||
type=MountType.BIND.value, source="/run/dbus", target="/run/dbus", read_only=True
|
||||
)
|
||||
MOUNT_DEV = Mount(
|
||||
type=MountType.BIND.value, source="/dev", target="/dev", read_only=True
|
||||
)
|
||||
MOUNT_DOCKER = Mount(
|
||||
type=MountType.BIND.value,
|
||||
source="/run/docker.sock",
|
||||
target="/run/docker.sock",
|
||||
read_only=True,
|
||||
)
|
||||
MOUNT_MACHINE_ID = Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=MACHINE_ID.as_posix(),
|
||||
target=MACHINE_ID.as_posix(),
|
||||
read_only=True,
|
||||
)
|
||||
MOUNT_UDEV = Mount(
|
||||
type=MountType.BIND.value, source="/run/udev", target="/run/udev", read_only=True
|
||||
)
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
"""DNS docker object."""
|
||||
import logging
|
||||
|
||||
from docker.types import Mount
|
||||
|
||||
from ..coresys import CoreSysAttributes
|
||||
from .const import DBUS_PATH, DBUS_VOLUME, ENV_TIME
|
||||
from .const import ENV_TIME, MOUNT_DBUS, MountType
|
||||
from .interface import DockerInterface
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
@ -46,10 +48,15 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
|
|||
detach=True,
|
||||
security_opt=self.security_opt,
|
||||
environment={ENV_TIME: self.sys_timezone},
|
||||
volumes={
|
||||
str(self.sys_config.path_extern_dns): {"bind": "/config", "mode": "rw"},
|
||||
DBUS_PATH: DBUS_VOLUME,
|
||||
},
|
||||
mounts=[
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_dns.as_posix(),
|
||||
target="/config",
|
||||
read_only=False,
|
||||
),
|
||||
MOUNT_DBUS,
|
||||
],
|
||||
oom_score_adj=-300,
|
||||
)
|
||||
|
||||
|
|
|
@ -5,13 +5,24 @@ import logging
|
|||
|
||||
from awesomeversion import AwesomeVersion, AwesomeVersionCompareException
|
||||
import docker
|
||||
from docker.types import Mount
|
||||
import requests
|
||||
|
||||
from ..const import LABEL_MACHINE, MACHINE_ID
|
||||
from ..exceptions import DockerError
|
||||
from ..hardware.const import PolicyGroup
|
||||
from ..homeassistant.const import LANDINGPAGE
|
||||
from .const import ENV_TIME, ENV_TOKEN, ENV_TOKEN_OLD
|
||||
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__)
|
||||
|
@ -62,56 +73,64 @@ class DockerHomeAssistant(DockerInterface):
|
|||
)
|
||||
|
||||
@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"},
|
||||
}
|
||||
|
||||
# Add folders
|
||||
volumes.update(
|
||||
{
|
||||
str(self.sys_config.path_extern_homeassistant): {
|
||||
"bind": "/config",
|
||||
"mode": "rw",
|
||||
},
|
||||
str(self.sys_config.path_extern_ssl): {"bind": "/ssl", "mode": "ro"},
|
||||
str(self.sys_config.path_extern_share): {
|
||||
"bind": "/share",
|
||||
"mode": "rw",
|
||||
},
|
||||
str(self.sys_config.path_extern_media): {
|
||||
"bind": "/media",
|
||||
"mode": "rw",
|
||||
},
|
||||
}
|
||||
)
|
||||
def mounts(self) -> list[Mount]:
|
||||
"""Return mounts for container."""
|
||||
mounts = [
|
||||
MOUNT_DEV,
|
||||
MOUNT_DBUS,
|
||||
MOUNT_UDEV,
|
||||
# Add folders
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_homeassistant.as_posix(),
|
||||
target="/config",
|
||||
read_only=False,
|
||||
),
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_ssl.as_posix(),
|
||||
target="/ssl",
|
||||
read_only=True,
|
||||
),
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_share.as_posix(),
|
||||
target="/share",
|
||||
read_only=False,
|
||||
),
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_media.as_posix(),
|
||||
target="/media",
|
||||
read_only=False,
|
||||
propagation=PropagationMode.SLAVE.value,
|
||||
),
|
||||
# Configuration audio
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_homeassistant.path_extern_pulse.as_posix(),
|
||||
target="/etc/pulse/client.conf",
|
||||
read_only=True,
|
||||
),
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_plugins.audio.path_extern_pulse.as_posix(),
|
||||
target="/run/audio",
|
||||
read_only=True,
|
||||
),
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_plugins.audio.path_extern_asound.as_posix(),
|
||||
target="/etc/asound.conf",
|
||||
read_only=True,
|
||||
),
|
||||
]
|
||||
|
||||
# Machine ID
|
||||
if MACHINE_ID.exists():
|
||||
volumes.update({str(MACHINE_ID): {"bind": str(MACHINE_ID), "mode": "ro"}})
|
||||
mounts.append(MOUNT_MACHINE_ID)
|
||||
|
||||
# Configuration Audio
|
||||
volumes.update(
|
||||
{
|
||||
str(self.sys_homeassistant.path_extern_pulse): {
|
||||
"bind": "/etc/pulse/client.conf",
|
||||
"mode": "ro",
|
||||
},
|
||||
str(self.sys_plugins.audio.path_extern_pulse): {
|
||||
"bind": "/run/audio",
|
||||
"mode": "ro",
|
||||
},
|
||||
str(self.sys_plugins.audio.path_extern_asound): {
|
||||
"bind": "/etc/asound.conf",
|
||||
"mode": "ro",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return volumes
|
||||
return mounts
|
||||
|
||||
def _run(self) -> None:
|
||||
"""Run Docker image.
|
||||
|
@ -135,7 +154,7 @@ class DockerHomeAssistant(DockerInterface):
|
|||
init=False,
|
||||
security_opt=self.security_opt,
|
||||
network_mode="host",
|
||||
volumes=self.volumes,
|
||||
mounts=self.mounts,
|
||||
device_cgroup_rules=self.cgroups_rules,
|
||||
extra_hosts={
|
||||
"supervisor": self.sys_docker.network.supervisor,
|
||||
|
@ -172,17 +191,26 @@ class DockerHomeAssistant(DockerInterface):
|
|||
detach=True,
|
||||
stdout=True,
|
||||
stderr=True,
|
||||
volumes={
|
||||
str(self.sys_config.path_extern_homeassistant): {
|
||||
"bind": "/config",
|
||||
"mode": "rw",
|
||||
},
|
||||
str(self.sys_config.path_extern_ssl): {"bind": "/ssl", "mode": "ro"},
|
||||
str(self.sys_config.path_extern_share): {
|
||||
"bind": "/share",
|
||||
"mode": "ro",
|
||||
},
|
||||
},
|
||||
mounts=[
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_homeassistant.as_posix(),
|
||||
target="/config",
|
||||
read_only=False,
|
||||
),
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_ssl.as_posix(),
|
||||
target="/ssl",
|
||||
read_only=True,
|
||||
),
|
||||
Mount(
|
||||
type=MountType.BIND.value,
|
||||
source=self.sys_config.path_extern_share.as_posix(),
|
||||
target="/share",
|
||||
read_only=False,
|
||||
),
|
||||
],
|
||||
environment={ENV_TIME: self.sys_timezone},
|
||||
)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import logging
|
|||
|
||||
from ..const import DOCKER_NETWORK_MASK
|
||||
from ..coresys import CoreSysAttributes
|
||||
from .const import ENV_TIME, ENV_TOKEN, RestartPolicy
|
||||
from .const import ENV_TIME, ENV_TOKEN, MOUNT_DOCKER, RestartPolicy
|
||||
from .interface import DockerInterface
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
@ -53,7 +53,7 @@ class DockerObserver(DockerInterface, CoreSysAttributes):
|
|||
ENV_TOKEN: self.sys_plugins.observer.supervisor_token,
|
||||
ENV_NETWORK_MASK: DOCKER_NETWORK_MASK,
|
||||
},
|
||||
volumes={"/run/docker.sock": {"bind": "/run/docker.sock", "mode": "ro"}},
|
||||
mounts=[MOUNT_DOCKER],
|
||||
ports={"80/tcp": 4357},
|
||||
oom_score_adj=-300,
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import asyncio
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePath
|
||||
import shutil
|
||||
import tarfile
|
||||
from tempfile import TemporaryDirectory
|
||||
|
@ -219,14 +219,14 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes):
|
|||
self._data[ATTR_REFRESH_TOKEN] = value
|
||||
|
||||
@property
|
||||
def path_pulse(self):
|
||||
def path_pulse(self) -> Path:
|
||||
"""Return path to asound config."""
|
||||
return Path(self.sys_config.path_tmp, "homeassistant_pulse")
|
||||
|
||||
@property
|
||||
def path_extern_pulse(self):
|
||||
def path_extern_pulse(self) -> PurePath:
|
||||
"""Return path to asound config for Docker."""
|
||||
return Path(self.sys_config.path_extern_tmp, "homeassistant_pulse")
|
||||
return PurePath(self.sys_config.path_extern_tmp, "homeassistant_pulse")
|
||||
|
||||
@property
|
||||
def audio_output(self) -> str | None:
|
||||
|
|
|
@ -374,6 +374,8 @@ async def tmp_supervisor_data(coresys: CoreSys, tmp_path: Path) -> Path:
|
|||
coresys.config.path_backup.mkdir()
|
||||
coresys.config.path_tmp.mkdir()
|
||||
coresys.config.path_homeassistant.mkdir()
|
||||
coresys.config.path_audio.mkdir()
|
||||
coresys.config.path_dns.mkdir()
|
||||
yield tmp_path
|
||||
|
||||
|
||||
|
|
|
@ -3,13 +3,13 @@ from ipaddress import IPv4Address
|
|||
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||
|
||||
from docker.errors import NotFound
|
||||
from docker.types import Mount
|
||||
import pytest
|
||||
|
||||
from supervisor.addons import validate as vd
|
||||
from supervisor.addons.addon import Addon
|
||||
from supervisor.addons.model import Data
|
||||
from supervisor.addons.options import AddonOptions
|
||||
from supervisor.const import SYSTEMD_JOURNAL_PERSISTENT, SYSTEMD_JOURNAL_VOLATILE
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.docker.addon import DockerAddon
|
||||
from supervisor.exceptions import CoreDNSError, DockerNotFound
|
||||
|
@ -66,18 +66,23 @@ def test_base_volumes_included(
|
|||
docker_addon = get_docker_addon(
|
||||
coresys, addonsdata_system, "basic-addon-config.json"
|
||||
)
|
||||
volumes = docker_addon.volumes
|
||||
|
||||
# Dev added as ro
|
||||
assert "/dev" in volumes
|
||||
assert volumes["/dev"]["bind"] == "/dev"
|
||||
assert volumes["/dev"]["mode"] == "ro"
|
||||
assert (
|
||||
Mount(type="bind", source="/dev", target="/dev", read_only=True)
|
||||
in docker_addon.mounts
|
||||
)
|
||||
|
||||
# Data added as rw
|
||||
data_path = str(docker_addon.addon.path_extern_data)
|
||||
assert data_path in volumes
|
||||
assert volumes[data_path]["bind"] == "/data"
|
||||
assert volumes[data_path]["mode"] == "rw"
|
||||
assert (
|
||||
Mount(
|
||||
type="bind",
|
||||
source=docker_addon.addon.path_extern_data.as_posix(),
|
||||
target="/data",
|
||||
read_only=False,
|
||||
)
|
||||
in docker_addon.mounts
|
||||
)
|
||||
|
||||
|
||||
def test_addon_map_folder_defaults(
|
||||
|
@ -87,22 +92,42 @@ def test_addon_map_folder_defaults(
|
|||
docker_addon = get_docker_addon(
|
||||
coresys, addonsdata_system, "basic-addon-config.json"
|
||||
)
|
||||
volumes = docker_addon.volumes
|
||||
|
||||
# Config added and is marked rw
|
||||
config_path = str(docker_addon.sys_config.path_extern_homeassistant)
|
||||
assert config_path in volumes
|
||||
assert volumes[config_path]["bind"] == "/config"
|
||||
assert volumes[config_path]["mode"] == "rw"
|
||||
assert (
|
||||
Mount(
|
||||
type="bind",
|
||||
source=coresys.config.path_extern_homeassistant.as_posix(),
|
||||
target="/config",
|
||||
read_only=False,
|
||||
)
|
||||
in docker_addon.mounts
|
||||
)
|
||||
|
||||
# SSL added and defaults to ro
|
||||
ssl_path = str(docker_addon.sys_config.path_extern_ssl)
|
||||
assert ssl_path in volumes
|
||||
assert volumes[ssl_path]["bind"] == "/ssl"
|
||||
assert volumes[ssl_path]["mode"] == "ro"
|
||||
assert (
|
||||
Mount(
|
||||
type="bind",
|
||||
source=coresys.config.path_extern_ssl.as_posix(),
|
||||
target="/ssl",
|
||||
read_only=True,
|
||||
)
|
||||
in docker_addon.mounts
|
||||
)
|
||||
|
||||
# Media added and propagation set
|
||||
assert (
|
||||
Mount(
|
||||
type="bind",
|
||||
source=coresys.config.path_extern_media.as_posix(),
|
||||
target="/media",
|
||||
read_only=True,
|
||||
propagation="slave",
|
||||
)
|
||||
in docker_addon.mounts
|
||||
)
|
||||
|
||||
# Share not mapped
|
||||
assert str(docker_addon.sys_config.path_extern_share) not in volumes
|
||||
assert "/share" not in [mount["Target"] for mount in docker_addon.mounts]
|
||||
|
||||
|
||||
def test_journald_addon(
|
||||
|
@ -112,18 +137,25 @@ def test_journald_addon(
|
|||
docker_addon = get_docker_addon(
|
||||
coresys, addonsdata_system, "journald-addon-config.json"
|
||||
)
|
||||
volumes = docker_addon.volumes
|
||||
|
||||
assert str(SYSTEMD_JOURNAL_PERSISTENT) in volumes
|
||||
assert volumes.get(str(SYSTEMD_JOURNAL_PERSISTENT)).get("bind") == str(
|
||||
SYSTEMD_JOURNAL_PERSISTENT
|
||||
assert (
|
||||
Mount(
|
||||
type="bind",
|
||||
source="/var/log/journal",
|
||||
target="/var/log/journal",
|
||||
read_only=True,
|
||||
)
|
||||
in docker_addon.mounts
|
||||
)
|
||||
assert volumes.get(str(SYSTEMD_JOURNAL_PERSISTENT)).get("mode") == "ro"
|
||||
assert str(SYSTEMD_JOURNAL_VOLATILE) in volumes
|
||||
assert volumes.get(str(SYSTEMD_JOURNAL_VOLATILE)).get("bind") == str(
|
||||
SYSTEMD_JOURNAL_VOLATILE
|
||||
assert (
|
||||
Mount(
|
||||
type="bind",
|
||||
source="/run/log/journal",
|
||||
target="/run/log/journal",
|
||||
read_only=True,
|
||||
)
|
||||
in docker_addon.mounts
|
||||
)
|
||||
assert volumes.get(str(SYSTEMD_JOURNAL_VOLATILE)).get("mode") == "ro"
|
||||
|
||||
|
||||
def test_not_journald_addon(
|
||||
|
@ -133,9 +165,8 @@ def test_not_journald_addon(
|
|||
docker_addon = get_docker_addon(
|
||||
coresys, addonsdata_system, "basic-addon-config.json"
|
||||
)
|
||||
volumes = docker_addon.volumes
|
||||
|
||||
assert str(SYSTEMD_JOURNAL_PERSISTENT) not in volumes
|
||||
assert "/var/log/journal" not in [mount["Target"] for mount in docker_addon.mounts]
|
||||
|
||||
|
||||
async def test_addon_run_docker_error(
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
"""Test audio plugin container."""
|
||||
|
||||
from ipaddress import IPv4Address
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from docker.types import Mount
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.docker.manager import DockerAPI
|
||||
|
||||
|
||||
async def test_start(coresys: CoreSys, tmp_supervisor_data: Path, path_extern):
|
||||
"""Test starting audio plugin."""
|
||||
config_file = tmp_supervisor_data / "audio" / "pulse_audio.json"
|
||||
assert not config_file.exists()
|
||||
|
||||
with patch.object(DockerAPI, "run") as run:
|
||||
await coresys.plugins.audio.start()
|
||||
|
||||
run.assert_called_once()
|
||||
assert run.call_args.kwargs["ipv4"] == IPv4Address("172.30.32.4")
|
||||
assert run.call_args.kwargs["name"] == "hassio_audio"
|
||||
assert run.call_args.kwargs["hostname"] == "hassio-audio"
|
||||
assert run.call_args.kwargs["cap_add"] == ["SYS_NICE", "SYS_RESOURCE"]
|
||||
assert run.call_args.kwargs["ulimits"] == [
|
||||
{"Name": "rtprio", "Soft": 10, "Hard": 10}
|
||||
]
|
||||
assert run.call_args.kwargs["mounts"] == [
|
||||
Mount(type="bind", source="/dev", target="/dev", read_only=True),
|
||||
Mount(
|
||||
type="bind",
|
||||
source=coresys.config.path_extern_audio.as_posix(),
|
||||
target="/data",
|
||||
read_only=False,
|
||||
),
|
||||
Mount(type="bind", source="/run/dbus", target="/run/dbus", read_only=True),
|
||||
Mount(type="bind", source="/run/udev", target="/run/udev", read_only=True),
|
||||
Mount(
|
||||
type="bind",
|
||||
source="/etc/machine-id",
|
||||
target="/etc/machine-id",
|
||||
read_only=True,
|
||||
),
|
||||
]
|
||||
assert "volumes" not in run.call_args.kwargs
|
||||
|
||||
assert config_file.exists()
|
|
@ -0,0 +1,38 @@
|
|||
"""Test DNS plugin container."""
|
||||
|
||||
from ipaddress import IPv4Address
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from docker.types import Mount
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.docker.manager import DockerAPI
|
||||
|
||||
|
||||
async def test_start(coresys: CoreSys, tmp_supervisor_data: Path, path_extern):
|
||||
"""Test starting dns plugin."""
|
||||
config_file = tmp_supervisor_data / "dns" / "coredns.json"
|
||||
assert not config_file.exists()
|
||||
|
||||
with patch.object(DockerAPI, "run") as run:
|
||||
await coresys.plugins.dns.start()
|
||||
|
||||
run.assert_called_once()
|
||||
assert run.call_args.kwargs["ipv4"] == IPv4Address("172.30.32.3")
|
||||
assert run.call_args.kwargs["name"] == "hassio_dns"
|
||||
assert run.call_args.kwargs["hostname"] == "hassio-dns"
|
||||
assert run.call_args.kwargs["dns"] is False
|
||||
assert run.call_args.kwargs["oom_score_adj"] == -300
|
||||
assert run.call_args.kwargs["mounts"] == [
|
||||
Mount(
|
||||
type="bind",
|
||||
source=coresys.config.path_extern_dns.as_posix(),
|
||||
target="/config",
|
||||
read_only=False,
|
||||
),
|
||||
Mount(type="bind", source="/run/dbus", target="/run/dbus", read_only=True),
|
||||
]
|
||||
assert "volumes" not in run.call_args.kwargs
|
||||
|
||||
assert config_file.exists()
|
|
@ -0,0 +1,38 @@
|
|||
"""Test Observer plugin container."""
|
||||
|
||||
from ipaddress import IPv4Address, ip_network
|
||||
from unittest.mock import patch
|
||||
|
||||
from docker.types import Mount
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.docker.manager import DockerAPI
|
||||
|
||||
|
||||
async def test_start(coresys: CoreSys):
|
||||
"""Test starting observer plugin."""
|
||||
with patch.object(DockerAPI, "run") as run:
|
||||
await coresys.plugins.observer.start()
|
||||
|
||||
run.assert_called_once()
|
||||
assert run.call_args.kwargs["ipv4"] == IPv4Address("172.30.32.6")
|
||||
assert run.call_args.kwargs["name"] == "hassio_observer"
|
||||
assert run.call_args.kwargs["hostname"] == "hassio-observer"
|
||||
assert run.call_args.kwargs["restart_policy"] == {"Name": "always"}
|
||||
assert run.call_args.kwargs["extra_hosts"] == {
|
||||
"supervisor": IPv4Address("172.30.32.2")
|
||||
}
|
||||
assert run.call_args.kwargs["oom_score_adj"] == -300
|
||||
assert run.call_args.kwargs["environment"]["NETWORK_MASK"] == ip_network(
|
||||
"172.30.32.0/23"
|
||||
)
|
||||
assert run.call_args.kwargs["ports"] == {"80/tcp": 4357}
|
||||
assert run.call_args.kwargs["mounts"] == [
|
||||
Mount(
|
||||
type="bind",
|
||||
source="/run/docker.sock",
|
||||
target="/run/docker.sock",
|
||||
read_only=True,
|
||||
),
|
||||
]
|
||||
assert "volumes" not in run.call_args.kwargs
|
|
@ -7,8 +7,8 @@
|
|||
"url": "https://www.home-assistant.io/",
|
||||
"startup": "application",
|
||||
"boot": "auto",
|
||||
"map": ["config:rw", "ssl"],
|
||||
"map": ["config:rw", "ssl", "media"],
|
||||
"options": {},
|
||||
"schema": {},
|
||||
"image": "test/{arch}-my-custom-addon"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue