Skip to content

Commit adfb433

Browse files
authored
Intercept host logs Range header for Systemd v256+ compatibility (#5827)
Since Systemd v256 the Range header must not end with a trailing colon. We relied on this undocumented feature when following logs, and the frontend or CLI may still use it in requests. To fix the requests failing with new Systemd version, intercept the header and fill in the num_entries to maximum possible value, which avoids the journal-gatewayd returning the response prematurely and also works on older Systemd versions. The journal-gatewayd would still return response if follow flag is used along with num_entries, but this behavior is unchanged and would be better fixed in the backend. Link: systemd/systemd#37172
1 parent 198af54 commit adfb433

File tree

5 files changed

+43
-6
lines changed

5 files changed

+43
-6
lines changed

supervisor/api/host.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
LogFormat,
3838
LogFormatter,
3939
)
40+
from ..host.logs import SYSTEMD_JOURNAL_GATEWAYD_LINES_MAX
4041
from ..utils.systemd_journal import journal_logs_reader
4142
from .const import (
4243
ATTR_AGENT_VERSION,
@@ -238,13 +239,11 @@ async def advanced_logs_handler(
238239
# return 2 lines at minimum.
239240
lines = max(2, lines)
240241
# entries=cursor[[:num_skip]:num_entries]
241-
range_header = f"entries=:-{lines - 1}:{'' if follow else lines}"
242+
range_header = f"entries=:-{lines - 1}:{SYSTEMD_JOURNAL_GATEWAYD_LINES_MAX if follow else lines}"
242243
elif RANGE in request.headers:
243244
range_header = request.headers[RANGE]
244245
else:
245-
range_header = (
246-
f"entries=:-{DEFAULT_LINES - 1}:{'' if follow else DEFAULT_LINES}"
247-
)
246+
range_header = f"entries=:-{DEFAULT_LINES - 1}:{SYSTEMD_JOURNAL_GATEWAYD_LINES_MAX if follow else DEFAULT_LINES}"
248247

249248
async with self.sys_host.logs.journald_logs(
250249
params=params, range_header=range_header, accept=LogFormat.JOURNAL

supervisor/host/logs.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import logging
99
import os
1010
from pathlib import Path
11+
import re
1112
from typing import Self
1213

1314
from aiohttp import ClientError, ClientSession, ClientTimeout
@@ -34,6 +35,8 @@
3435
)
3536
# pylint: enable=no-member
3637

38+
SYSTEMD_JOURNAL_GATEWAYD_LINES_MAX = (1 << 64) - 1
39+
3740
SYSTEMD_JOURNAL_GATEWAYD_SOCKET: Path = Path("/run/systemd-journal-gatewayd.sock")
3841

3942
# From systemd catalog for message IDs (`journalctl --dump-catalog``)
@@ -42,6 +45,10 @@
4245
# Defined-By: systemd
4346
BOOT_IDS_QUERY = {"MESSAGE_ID": "b07a249cd024414a82dd00cd181378ff"}
4447

48+
RE_ENTRIES_HEADER = re.compile(
49+
r"^entries=(?P<cursor>[^:]*):(?P<num_skip>-?\d+):(?P<num_lines>\d*)$"
50+
)
51+
4552

4653
class LogsControl(CoreSysAttributes):
4754
"""Handle systemd-journal logs."""
@@ -186,6 +193,16 @@ async def journald_logs(
186193
async with ClientSession(base_url=base_url, connector=connector) as session:
187194
headers = {ACCEPT: accept}
188195
if range_header:
196+
if range_header.endswith(":"):
197+
# Make sure that num_entries is always set - before Systemd v256 it was
198+
# possible to omit it, which made sense when the "follow" option was used,
199+
# but this syntax is now invalid and triggers HTTP 400.
200+
# See: https://github.com/systemd/systemd/issues/37172
201+
if not (matches := re.match(RE_ENTRIES_HEADER, range_header)):
202+
raise HostNotSupportedError(
203+
f"Invalid range header: {range_header}"
204+
)
205+
range_header = f"entries={matches.group('cursor')}:{matches.group('num_skip')}:{SYSTEMD_JOURNAL_GATEWAYD_LINES_MAX}"
189206
headers[RANGE] = range_header
190207
async with session.get(
191208
f"{path}",

tests/api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from supervisor.host.const import LogFormat
88

99
DEFAULT_LOG_RANGE = "entries=:-99:100"
10-
DEFAULT_LOG_RANGE_FOLLOW = "entries=:-99:"
10+
DEFAULT_LOG_RANGE_FOLLOW = "entries=:-99:18446744073709551615"
1111

1212

1313
async def common_test_api_advanced_logs(

tests/api/test_host.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from tests.dbus_service_mocks.systemd import Systemd as SystemdService
1717

1818
DEFAULT_RANGE = "entries=:-99:100"
19-
DEFAULT_RANGE_FOLLOW = "entries=:-99:"
19+
DEFAULT_RANGE_FOLLOW = "entries=:-99:18446744073709551615"
2020
# pylint: disable=protected-access
2121

2222

tests/host/test_logs.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,24 @@ async def test_connection_refused_handled(
171171
with pytest.raises(HostServiceError):
172172
async with coresys.host.logs.journald_logs():
173173
pass
174+
175+
176+
@pytest.mark.parametrize(
177+
"range_header,range_reparse",
178+
[
179+
("entries=:-99:", "entries=:-99:18446744073709551615"),
180+
("entries=:-99:100", "entries=:-99:100"),
181+
("entries=cursor:0:100", "entries=cursor:0:100"),
182+
],
183+
)
184+
async def test_range_header_reparse(
185+
journald_gateway: MagicMock, coresys: CoreSys, range_header: str, range_reparse: str
186+
):
187+
"""Test that range header with trailing colon contains num_entries."""
188+
async with coresys.host.logs.journald_logs(range_header=range_header):
189+
journald_gateway.get.assert_called_with(
190+
"/entries",
191+
headers={"Accept": "text/plain", "Range": range_reparse},
192+
params={},
193+
timeout=None,
194+
)

0 commit comments

Comments
 (0)