1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2025-12-24 20:35:55 +00:00

Use journal-gatewayd's new /boots endpoint to list boots (#5914)

* Use journal-gatewayd's new /boots endpoint to list boots

Current method we use for getting boots has several known downsides, for
example it can miss some incomplete boots and the performance might be
worse than what we could get by using Systemd directly. Systemd was
missing a method to get list boots through the journal-gatewayd but that
should be addressed by the new /boots endpoint added in [1] which
returns application/json-seq response containing all boots as reported
in `journalctl --list-boots`.

Implement Supervisor methods to parse this format and use the endpoint
at first, falling back to the old method if it fails.

[1] https://github.com/systemd/systemd/pull/37574

* Log info instead of warning when /boots is not present

Co-authored-by: Stefan Agner <stefan@agner.ch>

* Split records only by RS instead of LF in journal_boots_reader

* Strip only RS, json.loads is fine with whitespace

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
This commit is contained in:
Jan Čermák
2025-05-29 11:41:23 +02:00
committed by GitHub
parent 705e76abe3
commit 4d1a5e2dc2
7 changed files with 201 additions and 14 deletions

View File

@@ -8,6 +8,7 @@ import pytest
from supervisor.exceptions import MalformedBinaryEntryError
from supervisor.host.const import LogFormatter
from supervisor.utils.systemd_journal import (
journal_boots_reader,
journal_logs_reader,
journal_plain_formatter,
journal_verbose_formatter,
@@ -205,3 +206,72 @@ async def test_parsing_colored_supervisor_logs():
line
== "\x1b[32m24-03-04 23:56:56 INFO (MainThread) [__main__] Closing Supervisor\x1b[0m"
)
async def test_parsing_boots():
"""Test parsing of boots."""
journal_logs, stream = _journal_logs_mock()
stream.feed_data(
b'\x1e{"index":0,"boot_id":"e9ba6e5d9bc745c591686a502e3ed817","first_entry":1748251653514247,"last_entry":1748258595644563}\x0a'
b'\x1e{"index":-1,"boot_id":"2087f5f269724a48852c92a2e663fb94","first_entry":1748012078520355,"last_entry":1748023322834353}\x0a'
b'\x1e{"index":-2,"boot_id":"865a4aa6128e4747917047c09f400d0d","first_entry":1748011941404183,"last_entry":1748012025742472}'
)
stream.feed_eof()
boots = []
async for index, boot_id in journal_boots_reader(journal_logs):
boots.append((index, boot_id))
assert boots == [
(0, "e9ba6e5d9bc745c591686a502e3ed817"),
(-1, "2087f5f269724a48852c92a2e663fb94"),
(-2, "865a4aa6128e4747917047c09f400d0d"),
]
async def test_parsing_boots_no_lf():
"""Test parsing of boots without LF separator (only RS)."""
journal_logs, stream = _journal_logs_mock()
stream.feed_data(
b'\x1e{"index":0,"boot_id":"e9ba6e5d9bc745c591686a502e3ed817","first_entry":1748251653514247,"last_entry":1748258595644563}'
b'\x1e{"index":-1,"boot_id":"2087f5f269724a48852c92a2e663fb94","first_entry":1748012078520355,"last_entry":1748023322834353}'
b'\x1e{"index":-2,"boot_id":"865a4aa6128e4747917047c09f400d0d","first_entry":1748011941404183,"last_entry":1748012025742472}'
)
stream.feed_eof()
boots = []
async for index, boot_id in journal_boots_reader(journal_logs):
boots.append((index, boot_id))
assert boots == [
(0, "e9ba6e5d9bc745c591686a502e3ed817"),
(-1, "2087f5f269724a48852c92a2e663fb94"),
(-2, "865a4aa6128e4747917047c09f400d0d"),
]
async def test_parsing_boots_single():
"""Test parsing of single boot with trailing LF."""
journal_logs, stream = _journal_logs_mock()
stream.feed_data(
b'\x1e{"index":0,"boot_id":"e9ba6e5d9bc745c591686a502e3ed817","first_entry":1748251653514247,"last_entry":1748258595644563}\x0a'
)
stream.feed_eof()
boots = []
async for index, boot_id in journal_boots_reader(journal_logs):
boots.append((index, boot_id))
assert boots == [(0, "e9ba6e5d9bc745c591686a502e3ed817")]
async def test_parsing_boots_none():
"""Test parsing of empty boot response."""
journal_logs, stream = _journal_logs_mock()
stream.feed_eof()
boots = []
async for index, boot_id in journal_boots_reader(journal_logs):
boots.append((index, boot_id))
assert boots == []