mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-07-03 20:05:36 +01:00
fe4f3b5761
On newer HAOS (home-assistant/operating-system#4736), hassos-supervisor.service gets a long stop timeout (420 s docker stop + 450 s TimeoutStopSec) so Supervisor can handle the SIGTERM during host teardown and gracefully stop Core, apps and plugins via its existing shutdown handler. On older releases the timeout is too short, so reboot()/shutdown() keep stopping Core in-process before requesting the reboot/power off. For host shutdowns not initiated by Supervisor (ACPI, power button, systemctl reboot), the SIGTERM handler now checks the systemd manager state. When it is "stopping", Supervisor runs Core.shutdown() to stop managed services gracefully; on a plain Supervisor restart the state is "running", so only Supervisor stops as before. The manager state is exposed by every systemd version, so this works regardless of the OS release. Closes home-assistant/operating-system#4642
125 lines
4.4 KiB
Python
125 lines
4.4 KiB
Python
"""Test host control."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
from dbus_fast import DBusError, ErrorType
|
|
import pytest
|
|
|
|
from supervisor.coresys import CoreSys
|
|
from supervisor.exceptions import HostInvalidHostnameError
|
|
|
|
from tests.dbus_service_mocks.base import DBusServiceMock
|
|
from tests.dbus_service_mocks.hostname import Hostname as HostnameService
|
|
from tests.dbus_service_mocks.logind import Logind as LogindService
|
|
from tests.dbus_service_mocks.timedate import TimeDate as TimeDateService
|
|
|
|
|
|
async def test_set_hostname(
|
|
coresys: CoreSys,
|
|
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
|
):
|
|
"""Test set hostname."""
|
|
hostname_service: HostnameService = all_dbus_services["hostname"]
|
|
hostname_service.SetStaticHostname.calls.clear()
|
|
|
|
assert coresys.dbus.hostname.hostname == "homeassistant-n2"
|
|
|
|
await coresys.host.control.set_hostname("test")
|
|
assert hostname_service.SetStaticHostname.calls == [("test", False)]
|
|
await hostname_service.ping()
|
|
assert coresys.dbus.hostname.hostname == "test"
|
|
|
|
|
|
async def test_set_hostname_rejected_by_host(
|
|
coresys: CoreSys,
|
|
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
|
):
|
|
"""A hostname rejected by hostnamed surfaces as HostInvalidHostnameError."""
|
|
hostname_service: HostnameService = all_dbus_services["hostname"]
|
|
hostname_service.response_set_static_hostname = DBusError(
|
|
ErrorType.INVALID_ARGS, "Invalid static hostname 'bad name'"
|
|
)
|
|
|
|
with pytest.raises(HostInvalidHostnameError) as exc_info:
|
|
await coresys.host.control.set_hostname("bad name")
|
|
|
|
assert exc_info.value.error_key == "host_invalid_hostname"
|
|
assert exc_info.value.extra_fields == {"hostname": "bad name"}
|
|
assert str(exc_info.value) == "Invalid hostname 'bad name'"
|
|
|
|
|
|
@pytest.mark.parametrize("os_available", ["16.2"], indirect=True)
|
|
async def test_set_timezone(
|
|
coresys: CoreSys,
|
|
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
|
os_available: str,
|
|
):
|
|
"""Test set timezone."""
|
|
timedate_service: TimeDateService = all_dbus_services["timedate"]
|
|
timedate_service.SetTimezone.calls.clear()
|
|
|
|
assert coresys.dbus.timedate.timezone == "Etc/UTC"
|
|
|
|
await coresys.host.control.set_timezone("Europe/Prague")
|
|
assert timedate_service.SetTimezone.calls == [("Europe/Prague", False)]
|
|
|
|
|
|
@pytest.mark.parametrize("os_available", ["16.1"], indirect=True)
|
|
async def test_set_timezone_unsupported(
|
|
coresys: CoreSys,
|
|
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
|
os_available: str,
|
|
):
|
|
"""Test DBus call is not made when OS doesn't support it."""
|
|
timedate_service: TimeDateService = all_dbus_services["timedate"]
|
|
timedate_service.SetTimezone.calls.clear()
|
|
|
|
await coresys.host.control.set_timezone("Europe/Prague")
|
|
assert timedate_service.SetTimezone.calls == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("os_available", "in_process_shutdown"),
|
|
[("18.0", False), ("17.3", True)],
|
|
indirect=["os_available"],
|
|
ids=["new-os", "old-os"],
|
|
)
|
|
async def test_reboot_graceful_shutdown_gating(
|
|
coresys: CoreSys,
|
|
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
|
os_available: str,
|
|
in_process_shutdown: bool,
|
|
):
|
|
"""reboot() stops Core in-process only on OS too old for coordinated shutdown."""
|
|
logind_service: LogindService = all_dbus_services["logind"]
|
|
logind_service.Reboot.calls.clear()
|
|
|
|
with patch.object(coresys.core, "shutdown") as core_shutdown:
|
|
await coresys.host.control.reboot()
|
|
|
|
assert core_shutdown.called is in_process_shutdown
|
|
assert len(logind_service.Reboot.calls) == 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("os_available", "in_process_shutdown"),
|
|
[("18.0", False), ("17.3", True)],
|
|
indirect=["os_available"],
|
|
ids=["new-os", "old-os"],
|
|
)
|
|
async def test_shutdown_graceful_shutdown_gating(
|
|
coresys: CoreSys,
|
|
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
|
os_available: str,
|
|
in_process_shutdown: bool,
|
|
):
|
|
"""shutdown() stops Core in-process only on OS too old for coordinated shutdown."""
|
|
logind_service: LogindService = all_dbus_services["logind"]
|
|
logind_service.PowerOff.calls.clear()
|
|
|
|
with patch.object(coresys.core, "shutdown") as core_shutdown:
|
|
await coresys.host.control.shutdown()
|
|
|
|
assert core_shutdown.called is in_process_shutdown
|
|
assert len(logind_service.PowerOff.calls) == 1
|