mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-16 13:46:31 +00:00
Allow discovery messages for unknown services with a warning (#4449)
* Allow discovery messages for unknown services with a warning * Log at warning level and skip sentry report
This commit is contained in:
parent
24c5613a50
commit
be4a6a1564
@ -175,6 +175,20 @@ def _warn_addon_config(config: dict[str, Any]):
|
|||||||
name,
|
name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
invalid_services: list[str] = []
|
||||||
|
for service in config.get(ATTR_DISCOVERY, []):
|
||||||
|
try:
|
||||||
|
valid_discovery_service(service)
|
||||||
|
except vol.Invalid:
|
||||||
|
invalid_services.append(service)
|
||||||
|
|
||||||
|
if invalid_services:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Add-on lists the following unknown services for discovery: %s. Please report this to the maintainer of %s",
|
||||||
|
", ".join(invalid_services),
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
@ -313,7 +327,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
||||||
vol.Optional(ATTR_DISCOVERY): [valid_discovery_service],
|
vol.Optional(ATTR_DISCOVERY): [str],
|
||||||
vol.Optional(ATTR_BACKUP_EXCLUDE): [str],
|
vol.Optional(ATTR_BACKUP_EXCLUDE): [str],
|
||||||
vol.Optional(ATTR_BACKUP_PRE): str,
|
vol.Optional(ATTR_BACKUP_PRE): str,
|
||||||
vol.Optional(ATTR_BACKUP_POST): str,
|
vol.Optional(ATTR_BACKUP_POST): str,
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
"""Init file for Supervisor network RESTful API."""
|
"""Init file for Supervisor network RESTful API."""
|
||||||
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..addons.addon import Addon
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ADDON,
|
ATTR_ADDON,
|
||||||
ATTR_CONFIG,
|
ATTR_CONFIG,
|
||||||
@ -15,9 +18,11 @@ from ..discovery.validate import valid_discovery_service
|
|||||||
from ..exceptions import APIError, APIForbidden
|
from ..exceptions import APIError, APIForbidden
|
||||||
from .utils import api_process, api_validate, require_home_assistant
|
from .utils import api_process, api_validate, require_home_assistant
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCHEMA_DISCOVERY = vol.Schema(
|
SCHEMA_DISCOVERY = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_SERVICE): valid_discovery_service,
|
vol.Required(ATTR_SERVICE): str,
|
||||||
vol.Optional(ATTR_CONFIG): vol.Maybe(dict),
|
vol.Optional(ATTR_CONFIG): vol.Maybe(dict),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -62,11 +67,28 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
async def set_discovery(self, request):
|
async def set_discovery(self, request):
|
||||||
"""Write data into a discovery pipeline."""
|
"""Write data into a discovery pipeline."""
|
||||||
body = await api_validate(SCHEMA_DISCOVERY, request)
|
body = await api_validate(SCHEMA_DISCOVERY, request)
|
||||||
addon = request[REQUEST_FROM]
|
addon: Addon = request[REQUEST_FROM]
|
||||||
|
service = body[ATTR_SERVICE]
|
||||||
|
|
||||||
|
try:
|
||||||
|
valid_discovery_service(service)
|
||||||
|
except vol.Invalid:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Received discovery message for unknown service %s from addon %s. Please report this to the maintainer of the add-on",
|
||||||
|
service,
|
||||||
|
addon.name,
|
||||||
|
)
|
||||||
|
|
||||||
# Access?
|
# Access?
|
||||||
if body[ATTR_SERVICE] not in addon.discovery:
|
if body[ATTR_SERVICE] not in addon.discovery:
|
||||||
raise APIForbidden("Can't use discovery!")
|
_LOGGER.error(
|
||||||
|
"Add-on %s attempted to send discovery for service %s which is not listed in its config. Please report this to the maintainer of the add-on",
|
||||||
|
addon.name,
|
||||||
|
service,
|
||||||
|
)
|
||||||
|
raise APIForbidden(
|
||||||
|
"Add-ons must list services they provide via discovery in their config!"
|
||||||
|
)
|
||||||
|
|
||||||
# Process discovery message
|
# Process discovery message
|
||||||
message = self.sys_discovery.send(addon, **body)
|
message = self.sys_discovery.send(addon, **body)
|
||||||
|
@ -33,7 +33,7 @@ SCHEMA_DISCOVERY = vol.Schema(
|
|||||||
{
|
{
|
||||||
vol.Required(ATTR_UUID): uuid_match,
|
vol.Required(ATTR_UUID): uuid_match,
|
||||||
vol.Required(ATTR_ADDON): str,
|
vol.Required(ATTR_ADDON): str,
|
||||||
vol.Required(ATTR_SERVICE): valid_discovery_service,
|
vol.Required(ATTR_SERVICE): str,
|
||||||
vol.Required(ATTR_CONFIG): vol.Maybe(dict),
|
vol.Required(ATTR_CONFIG): vol.Maybe(dict),
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Utilities for sentry."""
|
"""Utilities for sentry."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
|
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
|
||||||
@ -16,6 +17,8 @@ from ..misc.filter import filter_data
|
|||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
only_once_events: set[str] = set()
|
||||||
|
|
||||||
|
|
||||||
def sentry_connected() -> bool:
|
def sentry_connected() -> bool:
|
||||||
"""Is sentry connected."""
|
"""Is sentry connected."""
|
||||||
@ -44,6 +47,14 @@ def init_sentry(coresys: CoreSys) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def capture_event(event: dict[str, Any], only_once: str | None = None):
|
||||||
|
"""Capture an event and send to sentry."""
|
||||||
|
if sentry_connected():
|
||||||
|
if only_once and only_once not in only_once_events:
|
||||||
|
only_once_events.add(only_once)
|
||||||
|
sentry_sdk.capture_event(event)
|
||||||
|
|
||||||
|
|
||||||
def capture_exception(err: Exception) -> None:
|
def capture_exception(err: Exception) -> None:
|
||||||
"""Capture an exception and send to sentry."""
|
"""Capture an exception and send to sentry."""
|
||||||
if sentry_connected():
|
if sentry_connected():
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
"""Validate Add-on configs."""
|
"""Validate Add-on configs."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -283,4 +286,13 @@ def test_valid_slug():
|
|||||||
with pytest.raises(vol.Invalid):
|
with pytest.raises(vol.Invalid):
|
||||||
assert vd.SCHEMA_ADDON_CONFIG(config)
|
assert vd.SCHEMA_ADDON_CONFIG(config)
|
||||||
|
|
||||||
#
|
|
||||||
|
def test_invalid_discovery(capture_event: Mock, caplog: pytest.LogCaptureFixture):
|
||||||
|
"""Test invalid discovery."""
|
||||||
|
config = load_json_fixture("basic-addon-config.json")
|
||||||
|
config["discovery"] = ["mqtt", "junk", "junk2"]
|
||||||
|
|
||||||
|
assert vd.SCHEMA_ADDON_CONFIG(config)
|
||||||
|
|
||||||
|
with caplog.at_level(logging.WARNING):
|
||||||
|
assert "unknown services for discovery: junk, junk2" in caplog.text
|
||||||
|
53
tests/api/test_discovery.py
Normal file
53
tests/api/test_discovery.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"""Test discovery API."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from aiohttp.test_utils import TestClient
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from supervisor.addons.addon import Addon
|
||||||
|
from supervisor.discovery import Discovery
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", ["local_ssh"], indirect=True)
|
||||||
|
async def test_discovery_forbidden(
|
||||||
|
api_client: TestClient, caplog: pytest.LogCaptureFixture, install_addon_ssh
|
||||||
|
):
|
||||||
|
"""Test addon sending discovery message for an unregistered service."""
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
with caplog.at_level(logging.ERROR):
|
||||||
|
resp = await api_client.post("/discovery", json={"service": "mqtt"})
|
||||||
|
|
||||||
|
assert resp.status == 400
|
||||||
|
result = await resp.json()
|
||||||
|
assert result["result"] == "error"
|
||||||
|
assert (
|
||||||
|
result["message"]
|
||||||
|
== "Add-ons must list services they provide via discovery in their config!"
|
||||||
|
)
|
||||||
|
assert "Please report this to the maintainer of the add-on" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_client", ["local_ssh"], indirect=True)
|
||||||
|
async def test_discovery_unknown_service(
|
||||||
|
api_client: TestClient, caplog: pytest.LogCaptureFixture, install_addon_ssh: Addon
|
||||||
|
):
|
||||||
|
"""Test addon sending discovery message for an unkown service."""
|
||||||
|
caplog.clear()
|
||||||
|
install_addon_ssh.data["discovery"] = ["junk"]
|
||||||
|
|
||||||
|
message = MagicMock()
|
||||||
|
message.uuid = uuid4().hex
|
||||||
|
|
||||||
|
with caplog.at_level(logging.WARNING), patch.object(
|
||||||
|
Discovery, "send", return_value=message
|
||||||
|
):
|
||||||
|
resp = await api_client.post("/discovery", json={"service": "junk"})
|
||||||
|
|
||||||
|
assert resp.status == 200
|
||||||
|
result = await resp.json()
|
||||||
|
assert result["data"]["uuid"] == message.uuid
|
||||||
|
assert "Please report this to the maintainer of the add-on" in caplog.text
|
@ -408,13 +408,21 @@ def sys_supervisor():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def api_client(aiohttp_client, coresys: CoreSys) -> TestClient:
|
async def api_client(
|
||||||
|
aiohttp_client, coresys: CoreSys, request: pytest.FixtureRequest
|
||||||
|
) -> TestClient:
|
||||||
"""Fixture for RestAPI client."""
|
"""Fixture for RestAPI client."""
|
||||||
|
|
||||||
|
request_from: str | None = getattr(request, "param", None)
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def _security_middleware(request: web.Request, handler: web.RequestHandler):
|
async def _security_middleware(request: web.Request, handler: web.RequestHandler):
|
||||||
"""Make request are from Core."""
|
"""Make request are from Core or specified add-on."""
|
||||||
request[REQUEST_FROM] = coresys.homeassistant
|
if request_from:
|
||||||
|
request[REQUEST_FROM] = coresys.addons.get(request_from, local_only=True)
|
||||||
|
else:
|
||||||
|
request[REQUEST_FROM] = coresys.homeassistant
|
||||||
|
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
api = RestAPI(coresys)
|
api = RestAPI(coresys)
|
||||||
@ -606,6 +614,15 @@ async def capture_exception() -> Mock:
|
|||||||
yield capture_exception
|
yield capture_exception
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def capture_event() -> Mock:
|
||||||
|
"""Mock capture event for testing."""
|
||||||
|
with patch("supervisor.utils.sentry.sentry_connected", return_value=True), patch(
|
||||||
|
"supervisor.utils.sentry.sentry_sdk.capture_event"
|
||||||
|
) as capture_event:
|
||||||
|
yield capture_event
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def os_available(request: pytest.FixtureRequest) -> None:
|
async def os_available(request: pytest.FixtureRequest) -> None:
|
||||||
"""Mock os as available."""
|
"""Mock os as available."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user