mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-08 09:46:29 +00:00
Add fallback for boot IDs query (#5391)
This commit is contained in:
parent
ac5ce4cc9e
commit
55e58d39d9
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@ -101,18 +102,33 @@ class LogsControl(CoreSysAttributes):
|
|||||||
timeout=ClientTimeout(total=20),
|
timeout=ClientTimeout(total=20),
|
||||||
) as resp:
|
) as resp:
|
||||||
text = await resp.text()
|
text = await resp.text()
|
||||||
self._boot_ids = [
|
|
||||||
json.loads(entry)[PARAM_BOOT_ID]
|
|
||||||
for entry in text.split("\n")
|
|
||||||
if entry
|
|
||||||
]
|
|
||||||
return self._boot_ids
|
|
||||||
except (ClientError, TimeoutError) as err:
|
except (ClientError, TimeoutError) as err:
|
||||||
raise HostLogError(
|
raise HostLogError(
|
||||||
"Could not get a list of boot IDs from systemd-journal-gatewayd",
|
"Could not get a list of boot IDs from systemd-journal-gatewayd",
|
||||||
_LOGGER.error,
|
_LOGGER.error,
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
# If a system has not been rebooted in a long time query can come back with zero results
|
||||||
|
# Fallback is to get latest log line and its boot ID so we always have at least one.
|
||||||
|
if not text:
|
||||||
|
try:
|
||||||
|
async with self.journald_logs(
|
||||||
|
range_header="entries=:-1:1",
|
||||||
|
accept=LogFormat.JSON,
|
||||||
|
timeout=ClientTimeout(total=20),
|
||||||
|
) as resp:
|
||||||
|
text = await resp.text()
|
||||||
|
except (ClientError, TimeoutError) as err:
|
||||||
|
raise HostLogError(
|
||||||
|
"Could not get a list of boot IDs from systemd-journal-gatewayd",
|
||||||
|
_LOGGER.error,
|
||||||
|
) from err
|
||||||
|
|
||||||
|
self._boot_ids = [
|
||||||
|
json.loads(entry)[PARAM_BOOT_ID] for entry in text.split("\n") if entry
|
||||||
|
]
|
||||||
|
return self._boot_ids
|
||||||
|
|
||||||
async def get_identifiers(self) -> list[str]:
|
async def get_identifiers(self) -> list[str]:
|
||||||
"""Get syslog identifiers."""
|
"""Get syslog identifiers."""
|
||||||
try:
|
try:
|
||||||
@ -135,7 +151,7 @@ class LogsControl(CoreSysAttributes):
|
|||||||
range_header: str | None = None,
|
range_header: str | None = None,
|
||||||
accept: LogFormat = LogFormat.TEXT,
|
accept: LogFormat = LogFormat.TEXT,
|
||||||
timeout: ClientTimeout | None = None,
|
timeout: ClientTimeout | None = None,
|
||||||
) -> ClientResponse:
|
) -> AsyncGenerator[ClientResponse]:
|
||||||
"""Get logs from systemd-journal-gatewayd.
|
"""Get logs from systemd-journal-gatewayd.
|
||||||
|
|
||||||
See https://www.freedesktop.org/software/systemd/man/systemd-journal-gatewayd.service.html for params and more info.
|
See https://www.freedesktop.org/software/systemd/man/systemd-journal-gatewayd.service.html for params and more info.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Test Host API."""
|
"""Test Host API."""
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
from unittest.mock import ANY, MagicMock, patch
|
from unittest.mock import ANY, MagicMock, patch
|
||||||
|
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
@ -20,7 +21,7 @@ DEFAULT_RANGE_FOLLOW = "entries=:-99:"
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="coresys_disk_info")
|
@pytest.fixture(name="coresys_disk_info")
|
||||||
async def fixture_coresys_disk_info(coresys: CoreSys) -> CoreSys:
|
async def fixture_coresys_disk_info(coresys: CoreSys) -> AsyncGenerator[CoreSys]:
|
||||||
"""Mock basic disk information for host APIs."""
|
"""Mock basic disk information for host APIs."""
|
||||||
coresys.hardware.disk.get_disk_life_time = lambda _: 0
|
coresys.hardware.disk.get_disk_life_time = lambda _: 0
|
||||||
coresys.hardware.disk.get_disk_free_space = lambda _: 5000
|
coresys.hardware.disk.get_disk_free_space = lambda _: 5000
|
||||||
|
@ -422,27 +422,25 @@ async def tmp_supervisor_data(coresys: CoreSys, tmp_path: Path) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def journald_gateway() -> MagicMock:
|
async def journald_gateway() -> AsyncGenerator[MagicMock]:
|
||||||
"""Mock logs control."""
|
"""Mock logs control."""
|
||||||
with (
|
with (
|
||||||
patch("supervisor.host.logs.Path.is_socket", return_value=True),
|
patch("supervisor.host.logs.Path.is_socket", return_value=True),
|
||||||
patch("supervisor.host.logs.ClientSession.get") as get,
|
patch("supervisor.host.logs.ClientSession.get") as get,
|
||||||
):
|
):
|
||||||
reader = asyncio.StreamReader(loop=asyncio.get_running_loop())
|
reader = asyncio.StreamReader(loop=asyncio.get_running_loop())
|
||||||
|
client_response = MagicMock(content=reader, get=get)
|
||||||
|
|
||||||
async def response_text():
|
async def response_text():
|
||||||
return (await reader.read()).decode("utf-8")
|
return (await client_response.content.read()).decode("utf-8")
|
||||||
|
|
||||||
client_response = MagicMock(
|
client_response.text = response_text
|
||||||
content=reader,
|
|
||||||
text=response_text,
|
|
||||||
)
|
|
||||||
|
|
||||||
get.return_value.__aenter__.return_value = client_response
|
get.return_value.__aenter__.return_value = client_response
|
||||||
get.return_value.__aenter__.return_value.__aenter__.return_value = (
|
get.return_value.__aenter__.return_value.__aenter__.return_value = (
|
||||||
client_response
|
client_response
|
||||||
)
|
)
|
||||||
yield reader
|
yield client_response
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Test host logs control."""
|
"""Test host logs control."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
from aiohttp.client_exceptions import UnixClientConnectorError
|
from aiohttp.client_exceptions import UnixClientConnectorError
|
||||||
@ -36,8 +37,10 @@ async def test_logs(coresys: CoreSys, journald_gateway: MagicMock):
|
|||||||
"""Test getting logs and errors."""
|
"""Test getting logs and errors."""
|
||||||
assert coresys.host.logs.available is True
|
assert coresys.host.logs.available is True
|
||||||
|
|
||||||
journald_gateway.feed_data(load_fixture("logs_export_host.txt").encode("utf-8"))
|
journald_gateway.content.feed_data(
|
||||||
journald_gateway.feed_eof()
|
load_fixture("logs_export_host.txt").encode("utf-8")
|
||||||
|
)
|
||||||
|
journald_gateway.content.feed_eof()
|
||||||
|
|
||||||
async with coresys.host.logs.journald_logs() as resp:
|
async with coresys.host.logs.journald_logs() as resp:
|
||||||
cursor, line = await anext(
|
cursor, line = await anext(
|
||||||
@ -62,10 +65,10 @@ async def test_logs(coresys: CoreSys, journald_gateway: MagicMock):
|
|||||||
|
|
||||||
async def test_logs_coloured(coresys: CoreSys, journald_gateway: MagicMock):
|
async def test_logs_coloured(coresys: CoreSys, journald_gateway: MagicMock):
|
||||||
"""Test ANSI control sequences being preserved in binary messages."""
|
"""Test ANSI control sequences being preserved in binary messages."""
|
||||||
journald_gateway.feed_data(
|
journald_gateway.content.feed_data(
|
||||||
load_fixture("logs_export_supervisor.txt").encode("utf-8")
|
load_fixture("logs_export_supervisor.txt").encode("utf-8")
|
||||||
)
|
)
|
||||||
journald_gateway.feed_eof()
|
journald_gateway.content.feed_eof()
|
||||||
|
|
||||||
async with coresys.host.logs.journald_logs() as resp:
|
async with coresys.host.logs.journald_logs() as resp:
|
||||||
cursor, line = await anext(journal_logs_reader(resp))
|
cursor, line = await anext(journal_logs_reader(resp))
|
||||||
@ -81,13 +84,15 @@ async def test_logs_coloured(coresys: CoreSys, journald_gateway: MagicMock):
|
|||||||
|
|
||||||
async def test_boot_ids(coresys: CoreSys, journald_gateway: MagicMock):
|
async def test_boot_ids(coresys: CoreSys, journald_gateway: MagicMock):
|
||||||
"""Test getting boot ids."""
|
"""Test getting boot ids."""
|
||||||
journald_gateway.feed_data(load_fixture("logs_boot_ids.txt").encode("utf-8"))
|
journald_gateway.content.feed_data(
|
||||||
journald_gateway.feed_eof()
|
load_fixture("logs_boot_ids.txt").encode("utf-8")
|
||||||
|
)
|
||||||
|
journald_gateway.content.feed_eof()
|
||||||
|
|
||||||
assert await coresys.host.logs.get_boot_ids() == TEST_BOOT_IDS
|
assert await coresys.host.logs.get_boot_ids() == TEST_BOOT_IDS
|
||||||
|
|
||||||
# Boot ID query should not be run again, mock a failure for it to ensure
|
# Boot ID query should not be run again, mock a failure for it to ensure
|
||||||
journald_gateway.side_effect = TimeoutError()
|
journald_gateway.get.side_effect = TimeoutError()
|
||||||
assert await coresys.host.logs.get_boot_ids() == TEST_BOOT_IDS
|
assert await coresys.host.logs.get_boot_ids() == TEST_BOOT_IDS
|
||||||
|
|
||||||
assert await coresys.host.logs.get_boot_id(0) == "b1c386a144fd44db8f855d7e907256f8"
|
assert await coresys.host.logs.get_boot_id(0) == "b1c386a144fd44db8f855d7e907256f8"
|
||||||
@ -104,10 +109,37 @@ async def test_boot_ids(coresys: CoreSys, journald_gateway: MagicMock):
|
|||||||
await coresys.host.logs.get_boot_id(3)
|
await coresys.host.logs.get_boot_id(3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_boot_ids_fallback(coresys: CoreSys, journald_gateway: MagicMock):
|
||||||
|
"""Test getting boot ids using fallback."""
|
||||||
|
# Initial response has no log lines
|
||||||
|
journald_gateway.content.feed_data(b"")
|
||||||
|
journald_gateway.content.feed_eof()
|
||||||
|
|
||||||
|
# Fallback contains exactly one with a boot ID
|
||||||
|
boot_id_data = load_fixture("logs_boot_ids.txt")
|
||||||
|
reader = asyncio.StreamReader(loop=asyncio.get_running_loop())
|
||||||
|
reader.feed_data(boot_id_data.split("\n")[0].encode("utf-8"))
|
||||||
|
reader.feed_eof()
|
||||||
|
|
||||||
|
readers = [journald_gateway.content, reader]
|
||||||
|
|
||||||
|
def get_side_effect(*args, **kwargs):
|
||||||
|
journald_gateway.content = readers.pop(0)
|
||||||
|
return journald_gateway.get.return_value
|
||||||
|
|
||||||
|
journald_gateway.get.side_effect = get_side_effect
|
||||||
|
|
||||||
|
assert await coresys.host.logs.get_boot_ids() == [
|
||||||
|
"b2aca10d5ca54fb1b6fb35c85a0efca9"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def test_identifiers(coresys: CoreSys, journald_gateway: MagicMock):
|
async def test_identifiers(coresys: CoreSys, journald_gateway: MagicMock):
|
||||||
"""Test getting identifiers."""
|
"""Test getting identifiers."""
|
||||||
journald_gateway.feed_data(load_fixture("logs_identifiers.txt").encode("utf-8"))
|
journald_gateway.content.feed_data(
|
||||||
journald_gateway.feed_eof()
|
load_fixture("logs_identifiers.txt").encode("utf-8")
|
||||||
|
)
|
||||||
|
journald_gateway.content.feed_eof()
|
||||||
|
|
||||||
# Mock is large so just look for a few different types of identifiers
|
# Mock is large so just look for a few different types of identifiers
|
||||||
identifiers = await coresys.host.logs.get_identifiers()
|
identifiers = await coresys.host.logs.get_identifiers()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user