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