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:
+20
-3
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user