Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Systemd Journal API for all logs endpoints in API #4972

Merged
merged 5 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 101 additions & 12 deletions supervisor/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
from ..const import AddonState
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import APIAddonNotInstalled
from ..utils.sentry import capture_exception
from .addons import APIAddons
from .audio import APIAudio
from .auth import APIAuth
from .backups import APIBackups
from .cli import APICli
from .const import CONTENT_TYPE_TEXT
from .discovery import APIDiscovery
from .dns import APICoreDNS
from .docker import APIDocker
Expand All @@ -36,7 +38,7 @@
from .services import APIServices
from .store import APIStore
from .supervisor import APISupervisor
from .utils import api_process
from .utils import api_process, api_process_raw

_LOGGER: logging.Logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -71,8 +73,14 @@ def __init__(self, coresys: CoreSys):
self._runner: web.AppRunner = web.AppRunner(self.webapp, shutdown_timeout=5)
self._site: web.TCPSite | None = None

# share single host API handler for reuse in logging endpoints
self._api_host: APIHost | None = None

async def load(self) -> None:
"""Register REST API Calls."""
self._api_host = APIHost()
self._api_host.coresys = self.coresys

self._register_addons()
self._register_audio()
self._register_auth()
Expand Down Expand Up @@ -102,10 +110,41 @@ async def load(self) -> None:

await self.start()

def _register_advanced_logs(self, path: str, syslog_identifier: str):
"""Register logs endpoint for a given path, returning logs for single syslog identifier."""

self.webapp.add_routes(
[
web.get(
f"{path}/logs",
partial(self._api_host.advanced_logs, identifier=syslog_identifier),
),
web.get(
f"{path}/logs/follow",
partial(
self._api_host.advanced_logs,
identifier=syslog_identifier,
follow=True,
),
),
web.get(
f"{path}/logs/boots/{{bootid}}",
partial(self._api_host.advanced_logs, identifier=syslog_identifier),
),
web.get(
f"{path}/logs/boots/{{bootid}}/follow",
partial(
self._api_host.advanced_logs,
identifier=syslog_identifier,
follow=True,
),
),
]
)

def _register_host(self) -> None:
"""Register hostcontrol functions."""
api_host = APIHost()
api_host.coresys = self.coresys
api_host = self._api_host

self.webapp.add_routes(
[
Expand Down Expand Up @@ -261,11 +300,11 @@ def _register_multicast(self) -> None:
[
web.get("/multicast/info", api_multicast.info),
web.get("/multicast/stats", api_multicast.stats),
web.get("/multicast/logs", api_multicast.logs),
web.post("/multicast/update", api_multicast.update),
web.post("/multicast/restart", api_multicast.restart),
]
)
self._register_advanced_logs("/multicast", "hassio_multicast")

def _register_hardware(self) -> None:
"""Register hardware functions."""
Expand Down Expand Up @@ -352,7 +391,6 @@ def _register_supervisor(self) -> None:
web.get("/supervisor/ping", api_supervisor.ping),
web.get("/supervisor/info", api_supervisor.info),
web.get("/supervisor/stats", api_supervisor.stats),
web.get("/supervisor/logs", api_supervisor.logs),
web.post("/supervisor/update", api_supervisor.update),
web.post("/supervisor/reload", api_supervisor.reload),
web.post("/supervisor/restart", api_supervisor.restart),
Expand All @@ -361,6 +399,35 @@ def _register_supervisor(self) -> None:
]
)

async def get_supervisor_logs(*args, **kwargs):
try:
return await self._api_host.advanced_logs(
*args, identifier="hassio_supervisor", **kwargs
)
except Exception as err: # pylint: disable=broad-exception-caught
# Supervisor logs are critical, so catch everything, log the exception
# and try to return Docker container logs as the fallback
_LOGGER.exception(
"Failed to get supervisor logs using advanced_logs API"
)
capture_exception(err)
return await api_supervisor.logs(*args, **kwargs)

self.webapp.add_routes(
[
web.get("/supervisor/logs", get_supervisor_logs),
web.get(
"/supervisor/logs/follow",
partial(get_supervisor_logs, follow=True),
),
web.get("/supervisor/logs/boots/{bootid}", get_supervisor_logs),
web.get(
"/supervisor/logs/boots/{bootid}/follow",
partial(get_supervisor_logs, follow=True),
),
]
)

def _register_homeassistant(self) -> None:
"""Register Home Assistant functions."""
api_hass = APIHomeAssistant()
Expand All @@ -369,7 +436,6 @@ def _register_homeassistant(self) -> None:
self.webapp.add_routes(
[
web.get("/core/info", api_hass.info),
web.get("/core/logs", api_hass.logs),
web.get("/core/stats", api_hass.stats),
web.post("/core/options", api_hass.options),
web.post("/core/update", api_hass.update),
Expand All @@ -381,11 +447,12 @@ def _register_homeassistant(self) -> None:
]
)

self._register_advanced_logs("/core", "homeassistant")

# Reroute from legacy
self.webapp.add_routes(
[
web.get("/homeassistant/info", api_hass.info),
web.get("/homeassistant/logs", api_hass.logs),
web.get("/homeassistant/stats", api_hass.stats),
web.post("/homeassistant/options", api_hass.options),
web.post("/homeassistant/restart", api_hass.restart),
Expand All @@ -397,6 +464,8 @@ def _register_homeassistant(self) -> None:
]
)

self._register_advanced_logs("/homeassistant", "homeassistant")

def _register_proxy(self) -> None:
"""Register Home Assistant API Proxy."""
api_proxy = APIProxy()
Expand Down Expand Up @@ -443,13 +512,33 @@ def _register_addons(self) -> None:
),
web.get("/addons/{addon}/options/config", api_addons.options_config),
web.post("/addons/{addon}/rebuild", api_addons.rebuild),
web.get("/addons/{addon}/logs", api_addons.logs),
web.post("/addons/{addon}/stdin", api_addons.stdin),
web.post("/addons/{addon}/security", api_addons.security),
web.get("/addons/{addon}/stats", api_addons.stats),
]
)

@api_process_raw(CONTENT_TYPE_TEXT, error_type=CONTENT_TYPE_TEXT)
async def get_addon_logs(request, *args, **kwargs):
addon = api_addons.get_addon_for_request(request)
kwargs["identifier"] = f"addon_{addon.slug}"
return await self._api_host.advanced_logs(request, *args, **kwargs)

self.webapp.add_routes(
[
web.get("/addons/{addon}/logs", get_addon_logs),
web.get(
"/addons/{addon}/logs/follow",
partial(get_addon_logs, follow=True),
),
web.get("/addons/{addon}/logs/boots/{bootid}", get_addon_logs),
web.get(
"/addons/{addon}/logs/boots/{bootid}/follow",
partial(get_addon_logs, follow=True),
),
]
)

# Legacy routing to support requests for not installed addons
api_store = APIStore()
api_store.coresys = self.coresys
Expand Down Expand Up @@ -547,26 +636,24 @@ def _register_dns(self) -> None:
[
web.get("/dns/info", api_dns.info),
web.get("/dns/stats", api_dns.stats),
web.get("/dns/logs", api_dns.logs),
web.post("/dns/update", api_dns.update),
web.post("/dns/options", api_dns.options),
web.post("/dns/restart", api_dns.restart),
web.post("/dns/reset", api_dns.reset),
]
)

self._register_advanced_logs("/dns", "hassio_dns")

def _register_audio(self) -> None:
"""Register Audio functions."""
api_audio = APIAudio()
api_audio.coresys = self.coresys
api_host = APIHost()
api_host.coresys = self.coresys

self.webapp.add_routes(
[
web.get("/audio/info", api_audio.info),
web.get("/audio/stats", api_audio.stats),
web.get("/audio/logs", api_audio.logs),
web.post("/audio/update", api_audio.update),
web.post("/audio/restart", api_audio.restart),
web.post("/audio/reload", api_audio.reload),
Expand All @@ -579,6 +666,8 @@ def _register_audio(self) -> None:
]
)

self._register_advanced_logs("/audio", "hassio_audio")

def _register_mounts(self) -> None:
"""Register mounts endpoints."""
api_mounts = APIMounts()
Expand Down
38 changes: 16 additions & 22 deletions supervisor/api/addons.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@
PwnedSecret,
)
from ..validate import docker_ports
from .const import ATTR_REMOVE_CONFIG, ATTR_SIGNED, CONTENT_TYPE_BINARY
from .utils import api_process, api_process_raw, api_validate, json_loads
from .const import ATTR_REMOVE_CONFIG, ATTR_SIGNED
from .utils import api_process, api_validate, json_loads

_LOGGER: logging.Logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -137,8 +137,8 @@
class APIAddons(CoreSysAttributes):
"""Handle RESTful API for add-on functions."""

def _extract_addon(self, request: web.Request) -> Addon:
"""Return addon, throw an exception it it doesn't exist."""
def get_addon_for_request(self, request: web.Request) -> Addon:
"""Return addon, throw an exception if it doesn't exist."""
addon_slug: str = request.match_info.get("addon")

# Lookup itself
Expand Down Expand Up @@ -191,7 +191,7 @@ async def reload(self, request: web.Request) -> None:

async def info(self, request: web.Request) -> dict[str, Any]:
"""Return add-on information."""
addon: AnyAddon = self._extract_addon(request)
addon: AnyAddon = self.get_addon_for_request(request)

data = {
ATTR_NAME: addon.name,
Expand Down Expand Up @@ -272,7 +272,7 @@ async def info(self, request: web.Request) -> dict[str, Any]:
@api_process
async def options(self, request: web.Request) -> None:
"""Store user options for add-on."""
addon = self._extract_addon(request)
addon = self.get_addon_for_request(request)

# Update secrets for validation
await self.sys_homeassistant.secrets.reload()
Expand Down Expand Up @@ -307,7 +307,7 @@ async def options(self, request: web.Request) -> None:
@api_process
async def options_validate(self, request: web.Request) -> None:
"""Validate user options for add-on."""
addon = self._extract_addon(request)
addon = self.get_addon_for_request(request)
data = {ATTR_MESSAGE: "", ATTR_VALID: True, ATTR_PWNED: False}

options = await request.json(loads=json_loads) or addon.options
Expand Down Expand Up @@ -349,7 +349,7 @@ async def options_config(self, request: web.Request) -> None:
slug: str = request.match_info.get("addon")
if slug != "self":
raise APIForbidden("This can be only read by the Add-on itself!")
addon = self._extract_addon(request)
addon = self.get_addon_for_request(request)

# Lookup/reload secrets
await self.sys_homeassistant.secrets.reload()
Expand All @@ -361,7 +361,7 @@ async def options_config(self, request: web.Request) -> None:
@api_process
async def security(self, request: web.Request) -> None:
"""Store security options for add-on."""
addon = self._extract_addon(request)
addon = self.get_addon_for_request(request)
body: dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)

if ATTR_PROTECTED in body:
Expand All @@ -373,7 +373,7 @@ async def security(self, request: web.Request) -> None:
@api_process
async def stats(self, request: web.Request) -> dict[str, Any]:
"""Return resource information."""
addon = self._extract_addon(request)
addon = self.get_addon_for_request(request)

stats: DockerStats = await addon.stats()

Expand All @@ -391,7 +391,7 @@ async def stats(self, request: web.Request) -> dict[str, Any]:
@api_process
async def uninstall(self, request: web.Request) -> Awaitable[None]:
"""Uninstall add-on."""
addon = self._extract_addon(request)
addon = self.get_addon_for_request(request)
body: dict[str, Any] = await api_validate(SCHEMA_UNINSTALL, request)
return await asyncio.shield(
self.sys_addons.uninstall(
Expand All @@ -402,40 +402,34 @@ async def uninstall(self, request: web.Request) -> Awaitable[None]:
@api_process
async def start(self, request: web.Request) -> None:
"""Start add-on."""
addon = self._extract_addon(request)
addon = self.get_addon_for_request(request)
if start_task := await asyncio.shield(addon.start()):
await start_task

@api_process
def stop(self, request: web.Request) -> Awaitable[None]:
"""Stop add-on."""
addon = self._extract_addon(request)
addon = self.get_addon_for_request(request)
return asyncio.shield(addon.stop())

@api_process
async def restart(self, request: web.Request) -> None:
"""Restart add-on."""
addon: Addon = self._extract_addon(request)
addon: Addon = self.get_addon_for_request(request)
if start_task := await asyncio.shield(addon.restart()):
await start_task

@api_process
async def rebuild(self, request: web.Request) -> None:
"""Rebuild local build add-on."""
addon = self._extract_addon(request)
addon = self.get_addon_for_request(request)
if start_task := await asyncio.shield(self.sys_addons.rebuild(addon.slug)):
await start_task

@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request: web.Request) -> Awaitable[bytes]:
"""Return logs from add-on."""
addon = self._extract_addon(request)
return addon.logs()

@api_process
async def stdin(self, request: web.Request) -> None:
"""Write to stdin of add-on."""
addon = self._extract_addon(request)
addon = self.get_addon_for_request(request)
if not addon.with_stdin:
raise APIError(f"STDIN not supported the {addon.slug} add-on")

Expand Down
8 changes: 1 addition & 7 deletions supervisor/api/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@
from ..exceptions import APIError
from ..host.sound import StreamType
from ..validate import version_tag
from .const import CONTENT_TYPE_BINARY
from .utils import api_process, api_process_raw, api_validate
from .utils import api_process, api_validate

_LOGGER: logging.Logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -111,11 +110,6 @@ async def update(self, request: web.Request) -> None:
raise APIError(f"Version {version} is already in use")
await asyncio.shield(self.sys_plugins.audio.update(version))

@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request: web.Request) -> Awaitable[bytes]:
"""Return Audio Docker logs."""
return self.sys_plugins.audio.logs()

@api_process
def restart(self, request: web.Request) -> Awaitable[None]:
"""Restart Audio plugin."""
Expand Down
Loading
Loading