1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2026-05-08 08:58:31 +01:00

Fix fallback time sync, create repair issue if time is out of sync (#6625)

* Fix fallback time sync, create repair issue if time is out of sync

The "poor man's NTP" using the whois service didn't work because it attempted
to sync the time when the NTP service was enabled, which is rejected by the
timedated service. To fix this, Supervisor now first disables the
systemd-timesyncd service and creates a repair issue before adjusting the time.
The timesyncd service stays disabled until submitting the fixup. Theoretically,
if the time moves backwards from an invalid time in the future,
systemd-timesyncd could otherwise restore the wrong time from a timestamp if we
did that after the time was set.

Also, the sync is now performed if the time is more that 1 hour off and in both
directions (previously it only intervened if it was more than 3 days in the
past).

Fixes #6015, refs #6549

* Update test_adjust_system_datetime_if_time_behind
This commit is contained in:
Jan Čermák
2026-03-13 16:01:38 +01:00
committed by GitHub
parent eedc623ec5
commit 093e98b164
5 changed files with 123 additions and 6 deletions
+20 -3
View File
@@ -16,6 +16,7 @@ from .const import (
CoreState,
)
from .coresys import CoreSys, CoreSysAttributes
from .dbus.const import StopUnitMode
from .exceptions import (
HassioError,
HomeAssistantCrashError,
@@ -423,11 +424,27 @@ class Core(CoreSysAttributes):
await self.sys_host.control.set_timezone(timezone)
# Calculate if system time is out of sync
delta = data.dt_utc - utcnow()
if delta <= timedelta(days=3) or self.sys_host.info.dt_synchronized:
delta = abs(data.dt_utc - utcnow())
if delta <= timedelta(hours=1) or self.sys_host.info.dt_synchronized:
return
_LOGGER.warning("System time/date shift over more than 3 days found!")
_LOGGER.warning("System time/date shift over more than 1 hour detected!")
if self.sys_host.info.use_ntp:
# Stop timesyncd if NTP is enabled, as set_time is blocked while it runs.
_LOGGER.info("Stopping systemd-timesyncd to allow manual time adjustment")
await self.sys_dbus.systemd.stop_unit(
"systemd-timesyncd.service", StopUnitMode.REPLACE
)
# Keep service disabled and create a repair issue
self.sys_resolution.create_issue(
IssueType.NTP_SYNC_FAILED,
ContextType.SYSTEM,
suggestions=[SuggestionType.ENABLE_NTP],
)
# We need to wait a bit for the service to stop.
await asyncio.sleep(1)
await self.sys_host.control.set_datetime(data.dt_utc)
await self.sys_supervisor.check_connectivity()
+2
View File
@@ -100,6 +100,7 @@ class IssueType(StrEnum):
MOUNT_FAILED = "mount_failed"
MULTIPLE_DATA_DISKS = "multiple_data_disks"
NO_CURRENT_BACKUP = "no_current_backup"
NTP_SYNC_FAILED = "ntp_sync_failed"
PWNED = "pwned"
REBOOT_REQUIRED = "reboot_required"
SECURITY = "security"
@@ -114,6 +115,7 @@ class SuggestionType(StrEnum):
CLEAR_FULL_BACKUP = "clear_full_backup"
CREATE_FULL_BACKUP = "create_full_backup"
DISABLE_BOOT = "disable_boot"
ENABLE_NTP = "enable_ntp"
EXECUTE_REBOOT = "execute_reboot"
EXECUTE_REBUILD = "execute_rebuild"
EXECUTE_RELOAD = "execute_reload"
@@ -0,0 +1,41 @@
"""Enable NTP fixup."""
import logging
from ...coresys import CoreSys
from ...dbus.const import StartUnitMode
from ..const import ContextType, IssueType, SuggestionType
from .base import FixupBase
_LOGGER: logging.Logger = logging.getLogger(__name__)
def setup(coresys: CoreSys) -> FixupBase:
"""Check setup function."""
return FixupSystemEnableNTP(coresys)
class FixupSystemEnableNTP(FixupBase):
"""Storage class for fixup."""
async def process_fixup(self, reference: str | None = None) -> None:
"""Initialize the fixup class."""
_LOGGER.info("Starting systemd-timesyncd service")
await self.sys_dbus.systemd.start_unit(
"systemd-timesyncd.service", StartUnitMode.REPLACE
)
@property
def suggestion(self) -> SuggestionType:
"""Return a SuggestionType enum."""
return SuggestionType.ENABLE_NTP
@property
def context(self) -> ContextType:
"""Return a ContextType enum."""
return ContextType.SYSTEM
@property
def issues(self) -> list[IssueType]:
"""Return a IssueType enum list."""
return [IssueType.NTP_SYNC_FAILED]
@@ -0,0 +1,32 @@
"""Test fixup system enable NTP."""
from supervisor.coresys import CoreSys
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
from supervisor.resolution.data import Issue, Suggestion
from supervisor.resolution.fixups.system_enable_ntp import FixupSystemEnableNTP
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.systemd import Systemd as SystemdService
async def test_fixup(
coresys: CoreSys,
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
):
"""Test fixup."""
systemd_service: SystemdService = all_dbus_services["systemd"]
systemd_service.StartUnit.calls.clear()
system_enable_ntp = FixupSystemEnableNTP(coresys)
assert system_enable_ntp.auto is False
coresys.resolution.add_suggestion(
Suggestion(SuggestionType.ENABLE_NTP, ContextType.SYSTEM)
)
coresys.resolution.add_issue(Issue(IssueType.NTP_SYNC_FAILED, ContextType.SYSTEM))
await system_enable_ntp()
assert systemd_service.StartUnit.calls == [("systemd-timesyncd.service", "replace")]
assert len(coresys.resolution.suggestions) == 0
assert len(coresys.resolution.issues) == 0
+28 -3
View File
@@ -12,9 +12,13 @@ from supervisor.coresys import CoreSys
from supervisor.exceptions import WhoamiSSLError
from supervisor.host.control import SystemControl
from supervisor.host.info import InfoCenter
from supervisor.resolution.const import IssueType, SuggestionType
from supervisor.supervisor import Supervisor
from supervisor.utils.whoami import WhoamiData
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.systemd import Systemd as SystemdService
@pytest.mark.parametrize("run_supervisor_state", ["test_file"], indirect=True)
async def test_write_state(run_supervisor_state: MagicMock, coresys: CoreSys):
@@ -70,11 +74,16 @@ async def test_adjust_system_datetime_without_ssl(
async def test_adjust_system_datetime_if_time_behind(
coresys: CoreSys, websession: MagicMock
coresys: CoreSys,
websession: MagicMock,
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
):
"""Test _adjust_system_datetime method when current time is ahead more than 3 days."""
"""Test _adjust_system_datetime method when current time is ahead more than 1 hour."""
systemd_service: SystemdService = all_dbus_services["systemd"]
systemd_service.StopUnit.calls.clear()
utc_ts = datetime.datetime.now().replace(tzinfo=datetime.UTC) + datetime.timedelta(
days=4
hours=1, minutes=1
)
with (
patch(
@@ -87,6 +96,7 @@ async def test_adjust_system_datetime_if_time_behind(
patch.object(
InfoCenter, "dt_synchronized", new=PropertyMock(return_value=False)
),
patch.object(InfoCenter, "use_ntp", new=PropertyMock(return_value=True)),
patch.object(Supervisor, "check_connectivity") as mock_check_connectivity,
):
await coresys.core._adjust_system_datetime()
@@ -95,6 +105,21 @@ async def test_adjust_system_datetime_if_time_behind(
mock_check_connectivity.assert_called_once()
mock_set_timezone.assert_called_once_with("Europe/Zurich")
# Verify timesyncd was stopped before setting time
assert systemd_service.StopUnit.calls == [
("systemd-timesyncd.service", "replace")
]
# Verify issue was created
assert any(
issue.type == IssueType.NTP_SYNC_FAILED
for issue in coresys.resolution.issues
)
assert any(
suggestion.type == SuggestionType.ENABLE_NTP
for suggestion in coresys.resolution.suggestions
)
async def test_adjust_system_datetime_sync_timezone_to_host(
coresys: CoreSys, websession: MagicMock