mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-05-19 22:28:52 +01:00
7fb621234e
* Use Unix socket for Supervisor to Core communication Reintroduce Unix socket support for Supervisor-to-Core communication (reverted in #6735) with the addition of a feature flag gate. The feature is now controlled by the `core_unix_socket` feature flag and disabled by default. When enabled and Core version supports it, Supervisor communicates with Core via a Unix socket at /run/os/core.sock instead of TCP. This eliminates the need for access token authentication on the socket path, as Core authenticates the peer by the socket connection itself. Key changes: - Add FeatureFlag.CORE_UNIX_SOCKET to gate the feature - HomeAssistantAPI: transport-aware session/url/websocket management - WSClient: separate connect() (Unix, no auth) and connect_with_auth() (TCP) class methods with proper error handling - APIProxy delegates websocket setup to api.connect_websocket() - Container state tracking for Unix session lifecycle - CI builder mounts /run/supervisor for integration tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Sort feature flags alphabetically * Drop per-call max_msg_size from WSClient Hardcode the WebSocket message size cap to 64 MB in WSClient and remove the parameter from WSClient.connect, connect_with_auth, _ws_connect, and HomeAssistantAPI.connect_websocket. This was only ever overridden by APIProxy, so threading it through four layers was unnecessary. max_msg_size is a cap, not a pre-allocation; aiohttp only grows buffers to the size of actual incoming messages. Supervisor's own control channel never approaches 64 MB, so unifying the limit has no runtime cost. Addresses review feedback on #6742. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
259 lines
9.4 KiB
Python
259 lines
9.4 KiB
Python
"""Test Home Assistant container."""
|
|
|
|
from ipaddress import IPv4Address
|
|
from unittest.mock import ANY, patch
|
|
|
|
from aiodocker.containers import DockerContainer
|
|
from awesomeversion import AwesomeVersion
|
|
import pytest
|
|
|
|
from supervisor.const import FeatureFlag
|
|
from supervisor.coresys import CoreSys
|
|
from supervisor.docker.const import (
|
|
MOUNT_CORE_RUN,
|
|
DockerMount,
|
|
MountBindOptions,
|
|
MountType,
|
|
PropagationMode,
|
|
)
|
|
from supervisor.docker.homeassistant import DockerHomeAssistant
|
|
from supervisor.docker.manager import DockerAPI
|
|
from supervisor.homeassistant.const import LANDINGPAGE
|
|
|
|
from . import DEV_MOUNT
|
|
|
|
|
|
@pytest.mark.usefixtures("tmp_supervisor_data", "path_extern")
|
|
async def test_homeassistant_start(coresys: CoreSys, container: DockerContainer):
|
|
"""Test starting homeassistant."""
|
|
coresys.homeassistant.version = AwesomeVersion("2023.8.1")
|
|
|
|
with (
|
|
patch.object(DockerAPI, "run", return_value=container.show.return_value) as run,
|
|
patch.object(
|
|
DockerHomeAssistant, "is_running", side_effect=[False, False, True]
|
|
),
|
|
patch("supervisor.homeassistant.core.asyncio.sleep"),
|
|
):
|
|
await coresys.homeassistant.core.start()
|
|
|
|
run.assert_called_once()
|
|
assert run.call_args.kwargs["name"] == "homeassistant"
|
|
assert run.call_args.kwargs["hostname"] == "homeassistant"
|
|
assert run.call_args.kwargs["privileged"] is True
|
|
assert run.call_args.kwargs["oom_score_adj"] == -300
|
|
assert run.call_args.kwargs["device_cgroup_rules"]
|
|
assert run.call_args.kwargs["extra_hosts"] == {
|
|
"supervisor": IPv4Address("172.30.32.2"),
|
|
"observer": IPv4Address("172.30.32.6"),
|
|
}
|
|
assert run.call_args.kwargs["environment"] == {
|
|
"SUPERVISOR": "172.30.32.2",
|
|
"HASSIO": "172.30.32.2",
|
|
"TZ": ANY,
|
|
"SUPERVISOR_TOKEN": ANY,
|
|
"HASSIO_TOKEN": ANY,
|
|
# no "HA_DUPLICATE_LOG_FILE"
|
|
}
|
|
assert run.call_args.kwargs["mounts"] == [
|
|
DEV_MOUNT,
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source="/run/dbus",
|
|
target="/run/dbus",
|
|
read_only=True,
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source="/run/udev",
|
|
target="/run/udev",
|
|
read_only=True,
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source=coresys.config.path_extern_homeassistant.as_posix(),
|
|
target="/config",
|
|
read_only=False,
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source=coresys.config.path_extern_ssl.as_posix(),
|
|
target="/ssl",
|
|
read_only=True,
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source=coresys.config.path_extern_share.as_posix(),
|
|
target="/share",
|
|
read_only=False,
|
|
bind_options=MountBindOptions(propagation=PropagationMode.RSLAVE),
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source=coresys.config.path_extern_media.as_posix(),
|
|
target="/media",
|
|
read_only=False,
|
|
bind_options=MountBindOptions(propagation=PropagationMode.RSLAVE),
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source=coresys.homeassistant.path_extern_pulse.as_posix(),
|
|
target="/etc/pulse/client.conf",
|
|
read_only=True,
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source=coresys.plugins.audio.path_extern_pulse.as_posix(),
|
|
target="/run/audio",
|
|
read_only=True,
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source=coresys.plugins.audio.path_extern_asound.as_posix(),
|
|
target="/etc/asound.conf",
|
|
read_only=True,
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source="/etc/machine-id",
|
|
target="/etc/machine-id",
|
|
read_only=True,
|
|
),
|
|
]
|
|
assert "volumes" not in run.call_args.kwargs
|
|
|
|
|
|
@pytest.mark.usefixtures("tmp_supervisor_data", "path_extern")
|
|
async def test_homeassistant_start_with_duplicate_log_file(
|
|
coresys: CoreSys, container: DockerContainer
|
|
):
|
|
"""Test starting homeassistant with duplicate_log_file enabled."""
|
|
coresys.homeassistant.version = AwesomeVersion("2025.12.0")
|
|
coresys.homeassistant.duplicate_log_file = True
|
|
|
|
with (
|
|
patch.object(DockerAPI, "run", return_value=container.show.return_value) as run,
|
|
patch.object(
|
|
DockerHomeAssistant, "is_running", side_effect=[False, False, True]
|
|
),
|
|
patch("supervisor.homeassistant.core.asyncio.sleep"),
|
|
):
|
|
await coresys.homeassistant.core.start()
|
|
|
|
run.assert_called_once()
|
|
env = run.call_args.kwargs["environment"]
|
|
assert "HA_DUPLICATE_LOG_FILE" in env
|
|
assert env["HA_DUPLICATE_LOG_FILE"] == "1"
|
|
|
|
|
|
@pytest.mark.usefixtures("tmp_supervisor_data", "path_extern")
|
|
async def test_homeassistant_start_with_unix_socket(
|
|
coresys: CoreSys, container: DockerContainer
|
|
):
|
|
"""Test starting homeassistant with unix socket env var for supported version."""
|
|
coresys.homeassistant.version = AwesomeVersion("2026.4.0")
|
|
coresys.config.set_feature_flag(FeatureFlag.UNIX_SOCKET_CORE_API, True)
|
|
|
|
with (
|
|
patch.object(DockerAPI, "run", return_value=container.show.return_value) as run,
|
|
patch.object(
|
|
DockerHomeAssistant, "is_running", side_effect=[False, False, True]
|
|
),
|
|
patch("supervisor.homeassistant.core.asyncio.sleep"),
|
|
):
|
|
await coresys.homeassistant.core.start()
|
|
|
|
run.assert_called_once()
|
|
env = run.call_args.kwargs["environment"]
|
|
assert "SUPERVISOR_CORE_API_SOCKET" in env
|
|
assert env["SUPERVISOR_CORE_API_SOCKET"] == "/run/supervisor/core.sock"
|
|
assert MOUNT_CORE_RUN in run.call_args.kwargs["mounts"]
|
|
|
|
|
|
@pytest.mark.usefixtures("tmp_supervisor_data", "path_extern")
|
|
async def test_landingpage_start(coresys: CoreSys, container: DockerContainer):
|
|
"""Test starting landingpage."""
|
|
coresys.homeassistant.version = LANDINGPAGE
|
|
|
|
with (
|
|
patch.object(DockerAPI, "run", return_value=container.show.return_value) as run,
|
|
patch.object(DockerHomeAssistant, "is_running", return_value=False),
|
|
):
|
|
await coresys.homeassistant.core.start()
|
|
|
|
run.assert_called_once()
|
|
assert run.call_args.kwargs["name"] == "homeassistant"
|
|
assert run.call_args.kwargs["hostname"] == "homeassistant"
|
|
assert run.call_args.kwargs["privileged"] is False
|
|
assert run.call_args.kwargs["oom_score_adj"] == -300
|
|
assert not run.call_args.kwargs["device_cgroup_rules"]
|
|
assert run.call_args.kwargs["extra_hosts"] == {
|
|
"supervisor": IPv4Address("172.30.32.2"),
|
|
"observer": IPv4Address("172.30.32.6"),
|
|
}
|
|
assert run.call_args.kwargs["environment"] == {
|
|
"SUPERVISOR": "172.30.32.2",
|
|
"HASSIO": "172.30.32.2",
|
|
"TZ": ANY,
|
|
"SUPERVISOR_TOKEN": ANY,
|
|
"HASSIO_TOKEN": ANY,
|
|
# no "HA_DUPLICATE_LOG_FILE"
|
|
}
|
|
assert run.call_args.kwargs["mounts"] == [
|
|
DEV_MOUNT,
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source="/run/dbus",
|
|
target="/run/dbus",
|
|
read_only=True,
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source="/run/udev",
|
|
target="/run/udev",
|
|
read_only=True,
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source=coresys.config.path_extern_homeassistant.as_posix(),
|
|
target="/config",
|
|
read_only=False,
|
|
),
|
|
DockerMount(
|
|
type=MountType.BIND,
|
|
source="/etc/machine-id",
|
|
target="/etc/machine-id",
|
|
read_only=True,
|
|
),
|
|
]
|
|
assert "volumes" not in run.call_args.kwargs
|
|
|
|
|
|
async def test_timeout(coresys: CoreSys, container: DockerContainer):
|
|
"""Test timeout for set from S6_SERVICES_GRACETIME."""
|
|
assert coresys.homeassistant.core.instance.timeout == 260
|
|
|
|
# Env missing, remain at default
|
|
await coresys.homeassistant.core.instance.attach(AwesomeVersion("2024.3.0"))
|
|
assert coresys.homeassistant.core.instance.timeout == 260
|
|
|
|
# Set a mock value for env in attrs, see that it changes
|
|
container.show.return_value["Config"] = {
|
|
"Env": [
|
|
"SUPERVISOR=172.30.32.2",
|
|
"HASSIO=172.30.32.2",
|
|
"TZ=America/New_York",
|
|
"SUPERVISOR_TOKEN=abc123",
|
|
"HASSIO_TOKEN=abc123",
|
|
"PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
"LANG=C.UTF-8",
|
|
"S6_BEHAVIOUR_IF_STAGE2_FAILS=2",
|
|
"S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0",
|
|
"S6_CMD_WAIT_FOR_SERVICES=1",
|
|
"S6_SERVICES_READYTIME=50",
|
|
"S6_SERVICES_GRACETIME=300000",
|
|
]
|
|
}
|
|
await coresys.homeassistant.core.instance.attach(AwesomeVersion("2024.3.0"))
|
|
assert coresys.homeassistant.core.instance.timeout == 320
|