Remove discovery config validation from supervisor (#4937)

* Remove discovery config validation from supervisor

* Remove invalid test

* Change validation to require a dictionary for compatibility
This commit is contained in:
Mike Degatano 2024-03-05 10:25:15 -05:00 committed by GitHub
parent 202ebf6d4e
commit 74a5899626
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 67 additions and 641 deletions

View File

@ -99,7 +99,6 @@ from ..const import (
AddonStartup,
AddonState,
)
from ..discovery.validate import valid_discovery_service
from ..docker.const import Capabilities
from ..validate import (
docker_image,
@ -190,20 +189,6 @@ def _warn_addon_config(config: dict[str, Any]):
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

View File

@ -15,7 +15,6 @@ from ..const import (
AddonState,
)
from ..coresys import CoreSysAttributes
from ..discovery.validate import valid_discovery_service
from ..exceptions import APIError, APIForbidden
from .utils import api_process, api_validate, require_home_assistant
@ -24,7 +23,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
SCHEMA_DISCOVERY = vol.Schema(
{
vol.Required(ATTR_SERVICE): str,
vol.Optional(ATTR_CONFIG): vol.Maybe(dict),
vol.Required(ATTR_CONFIG): dict,
}
)
@ -71,15 +70,6 @@ class APIDiscovery(CoreSysAttributes):
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?
if body[ATTR_SERVICE] not in addon.discovery:
_LOGGER.error(

View File

@ -7,14 +7,12 @@ from typing import TYPE_CHECKING, Any
from uuid import UUID, uuid4
import attr
import voluptuous as vol
from voluptuous.humanize import humanize_error
from ..const import ATTR_CONFIG, ATTR_DISCOVERY, FILE_HASSIO_DISCOVERY
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import DiscoveryError, HomeAssistantAPIError
from ..exceptions import HomeAssistantAPIError
from ..utils.common import FileConfiguration
from .validate import SCHEMA_DISCOVERY_CONFIG, valid_discovery_config
from .validate import SCHEMA_DISCOVERY_CONFIG
if TYPE_CHECKING:
from ..addons.addon import Addon
@ -75,12 +73,6 @@ class Discovery(CoreSysAttributes, FileConfiguration):
def send(self, addon: Addon, service: str, config: dict[str, Any]) -> Message:
"""Send a discovery message to Home Assistant."""
try:
config = valid_discovery_config(service, config)
except vol.Invalid as err:
_LOGGER.error("Invalid discovery %s config", humanize_error(config, err))
raise DiscoveryError() from err
# Create message
message = Message(addon.slug, service, config)

View File

@ -1 +0,0 @@
"""Discovery service modules."""

View File

@ -1,9 +0,0 @@
"""Discovery service for AdGuard."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_HOST, ATTR_PORT
SCHEMA = vol.Schema(
{vol.Required(ATTR_HOST): str, vol.Required(ATTR_PORT): network_port}
)

View File

@ -1,9 +0,0 @@
"""Discovery service for Almond."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_HOST, ATTR_PORT
SCHEMA = vol.Schema(
{vol.Required(ATTR_HOST): str, vol.Required(ATTR_PORT): network_port}
)

View File

@ -1,14 +0,0 @@
"""Discovery service for MQTT."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_API_KEY, ATTR_HOST, ATTR_PORT, ATTR_SERIAL
SCHEMA = vol.Schema(
{
vol.Required(ATTR_HOST): str,
vol.Required(ATTR_PORT): network_port,
vol.Required(ATTR_SERIAL): str,
vol.Required(ATTR_API_KEY): str,
}
)

View File

@ -1,9 +0,0 @@
"""Discovery service for the ESPHome Dashboard."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_HOST, ATTR_PORT
SCHEMA = vol.Schema(
{vol.Required(ATTR_HOST): str, vol.Required(ATTR_PORT): network_port}
)

View File

@ -1,16 +0,0 @@
"""Discovery service for HomeMatic."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_HOST, ATTR_PORT
SCHEMA = vol.Schema(
{
str: vol.Schema(
{
vol.Required(ATTR_HOST): str,
vol.Required(ATTR_PORT): network_port,
}
)
}
)

View File

@ -1,13 +0,0 @@
"""Discovery service for Matter Server."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_HOST, ATTR_PORT
# pylint: disable=no-value-for-parameter
SCHEMA = vol.Schema(
{
vol.Required(ATTR_HOST): str,
vol.Required(ATTR_PORT): network_port,
}
)

View File

@ -1,6 +0,0 @@
"""Discovery service for motionEye."""
import voluptuous as vol
from ..const import ATTR_URL
SCHEMA = vol.Schema({vol.Required(ATTR_URL): str})

View File

@ -1,26 +0,0 @@
"""Discovery service for MQTT."""
import voluptuous as vol
from ...validate import network_port
from ..const import (
ATTR_HOST,
ATTR_PASSWORD,
ATTR_PORT,
ATTR_PROTOCOL,
ATTR_SSL,
ATTR_USERNAME,
)
# pylint: disable=no-value-for-parameter
SCHEMA = vol.Schema(
{
vol.Required(ATTR_HOST): str,
vol.Required(ATTR_PORT): network_port,
vol.Optional(ATTR_USERNAME): str,
vol.Optional(ATTR_PASSWORD): str,
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_PROTOCOL, default="3.1.1"): vol.All(
str, vol.In(["3.1", "3.1.1"])
),
}
)

View File

@ -1,13 +0,0 @@
"""Discovery service for OpenThread Border Router."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_HOST, ATTR_PORT
# pylint: disable=no-value-for-parameter
SCHEMA = vol.Schema(
{
vol.Required(ATTR_HOST): str,
vol.Required(ATTR_PORT): network_port,
}
)

View File

@ -1,15 +0,0 @@
"""Discovery service for OpenZwave MQTT."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_HOST, ATTR_PASSWORD, ATTR_PORT, ATTR_USERNAME
# pylint: disable=no-value-for-parameter
SCHEMA = vol.Schema(
{
vol.Required(ATTR_HOST): str,
vol.Required(ATTR_PORT): network_port,
vol.Required(ATTR_USERNAME): str,
vol.Required(ATTR_PASSWORD): str,
}
)

View File

@ -1,9 +0,0 @@
"""Discovery service for RTSPtoWebRTC."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_HOST, ATTR_PORT
SCHEMA = vol.Schema(
{vol.Required(ATTR_HOST): str, vol.Required(ATTR_PORT): network_port}
)

View File

@ -1,9 +0,0 @@
"""Discovery service for UniFi."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_HOST, ATTR_PORT
SCHEMA = vol.Schema(
{vol.Required(ATTR_HOST): str, vol.Required(ATTR_PORT): network_port}
)

View File

@ -1,14 +0,0 @@
"""Discovery service for VLC Telnet."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_HOST, ATTR_PASSWORD, ATTR_PORT
# pylint: disable=no-value-for-parameter
SCHEMA = vol.Schema(
{
vol.Required(ATTR_HOST): str,
vol.Required(ATTR_PORT): network_port,
vol.Required(ATTR_PASSWORD): str,
}
)

View File

@ -1,25 +0,0 @@
"""Discovery service for the Wyoming Protocol integration."""
from typing import Any, cast
from urllib.parse import urlparse
import voluptuous as vol
from ..const import ATTR_URI
def validate_uri(value: Any) -> str:
"""Validate an Wyoming URI.
Currently accepts TCP URIs, can extended
to accept UNIX sockets in the future.
"""
uri_value = str(value)
if urlparse(uri_value).scheme == "tcp":
# pylint: disable-next=no-value-for-parameter
return cast(str, vol.Schema(vol.Url())(uri_value))
raise vol.Invalid("invalid Wyoming Protocol URI")
SCHEMA = vol.Schema({vol.Required(ATTR_URI): validate_uri})

View File

@ -1,13 +0,0 @@
"""Discovery service for Zwave JS."""
import voluptuous as vol
from ...validate import network_port
from ..const import ATTR_HOST, ATTR_PORT
# pylint: disable=no-value-for-parameter
SCHEMA = vol.Schema(
{
vol.Required(ATTR_HOST): str,
vol.Required(ATTR_PORT): network_port,
}
)

View File

@ -1,6 +1,4 @@
"""Validate services schema."""
from importlib import import_module
from pathlib import Path
import voluptuous as vol
@ -8,25 +6,6 @@ from ..const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_SERVICE, ATTR_
from ..utils.validate import schema_or
from ..validate import uuid_match
def valid_discovery_service(service):
"""Validate service name."""
service_file = Path(__file__).parent.joinpath(f"services/{service}.py")
if not service_file.exists():
raise vol.Invalid(f"Service {service} not found") from None
return service
def valid_discovery_config(service, config):
"""Validate service name."""
try:
service_mod = import_module(f".services.{service}", "supervisor.discovery")
except ImportError:
raise vol.Invalid(f"Service {service} not found") from None
return service_mod.SCHEMA(config)
SCHEMA_DISCOVERY = vol.Schema(
[
vol.Schema(

View File

@ -1,7 +1,5 @@
"""Validate Add-on configs."""
import logging
from unittest.mock import Mock
import pytest
import voluptuous as vol
@ -288,14 +286,3 @@ def test_valid_slug():
config["slug"] = "complemento telefónico"
with pytest.raises(vol.Invalid):
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

View File

@ -1,8 +1,7 @@
"""Test discovery API."""
import logging
from unittest.mock import ANY, MagicMock, patch
from uuid import uuid4
from unittest.mock import ANY, AsyncMock, MagicMock, patch
from aiohttp.test_utils import TestClient
import pytest
@ -10,9 +9,10 @@ import pytest
from supervisor.addons.addon import Addon
from supervisor.const import AddonState
from supervisor.coresys import CoreSys
from supervisor.discovery import Discovery, Message
from supervisor.discovery import Message
from tests.common import load_json_fixture
from tests.const import TEST_ADDON_SLUG
@pytest.mark.parametrize("api_client", ["local_ssh"], indirect=True)
@ -23,7 +23,9 @@ async def test_api_discovery_forbidden(
caplog.clear()
with caplog.at_level(logging.ERROR):
resp = await api_client.post("/discovery", json={"service": "mqtt"})
resp = await api_client.post(
"/discovery", json={"service": "mqtt", "config": {}}
)
assert resp.status == 403
result = await resp.json()
@ -35,28 +37,6 @@ async def test_api_discovery_forbidden(
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_api_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
@pytest.mark.parametrize(
"skip_state", [AddonState.ERROR, AddonState.STOPPED, AddonState.STARTUP]
)
@ -97,3 +77,61 @@ async def test_api_list_discovery(
assert resp.status == 200
result = await resp.json()
assert result["data"]["discovery"] == []
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
async def test_api_send_del_discovery(
api_client: TestClient, coresys: CoreSys, install_addon_ssh: Addon
):
"""Test adding and removing discovery."""
install_addon_ssh.data["discovery"] = ["test"]
coresys.homeassistant.api.ensure_access_token = AsyncMock()
coresys.websession.post = MagicMock()
resp = await api_client.post("/discovery", json={"service": "test", "config": {}})
assert resp.status == 200
result = await resp.json()
uuid = result["data"]["uuid"]
coresys.websession.post.assert_called_once()
assert (
coresys.websession.post.call_args.args[0]
== f"http://172.30.32.1:8123/api/hassio_push/discovery/{uuid}"
)
assert coresys.websession.post.call_args.kwargs["json"] == {
"addon": TEST_ADDON_SLUG,
"service": "test",
"uuid": uuid,
}
message = coresys.discovery.get(uuid)
assert message.addon == TEST_ADDON_SLUG
assert message.service == "test"
assert message.config == {}
coresys.websession.delete = MagicMock()
resp = await api_client.delete(f"/discovery/{uuid}")
assert resp.status == 200
coresys.websession.delete.assert_called_once()
assert (
coresys.websession.delete.call_args.args[0]
== f"http://172.30.32.1:8123/api/hassio_push/discovery/{uuid}"
)
assert coresys.websession.delete.call_args.kwargs["json"] == {
"addon": TEST_ADDON_SLUG,
"service": "test",
"uuid": uuid,
}
assert coresys.discovery.get(uuid) is None
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
async def test_api_invalid_discovery(api_client: TestClient, install_addon_ssh: Addon):
"""Test invalid discovery messages."""
install_addon_ssh.data["discovery"] = ["test"]
resp = await api_client.post("/discovery", json={"service": "test"})
assert resp.status == 400
resp = await api_client.post("/discovery", json={"service": "test", "config": None})
assert resp.status == 400

View File

@ -1 +0,0 @@
"""Tests for discovery."""

View File

@ -1,19 +0,0 @@
"""Test adguard discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good adguard config."""
valid_discovery_config("adguard", {"host": "test", "port": 3812})
def test_bad_config():
"""Test bad adguard config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("adguard", {"host": "test"})

View File

@ -1,19 +0,0 @@
"""Test almond discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good almond config."""
valid_discovery_config("almond", {"host": "test", "port": 3812})
def test_bad_config():
"""Test bad almond config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("almond", {"host": "test"})

View File

@ -1,22 +0,0 @@
"""Test DeConz discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good deconz config."""
valid_discovery_config(
"deconz",
{"host": "test", "port": 3812, "api_key": "MY_api_KEY99", "serial": "xyz"},
)
def test_bad_config():
"""Test bad deconz config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("deconz", {"host": "test", "port": 8080})

View File

@ -1,25 +0,0 @@
"""Test ESPHome Dashboard discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good ESPHome config."""
valid_discovery_config("esphome", {"host": "test", "port": 6052})
def test_bad_config():
"""Test bad ESPHome config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("esphome", {"host": "test"})
with pytest.raises(vol.Invalid):
valid_discovery_config("esphome", {"port": 6052})
with pytest.raises(vol.Invalid):
valid_discovery_config("esphome", {"port": -1})

View File

@ -1,22 +0,0 @@
"""Test HomeMatic discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good homematic config."""
valid_discovery_config(
"homematic",
{"ip": {"host": "test", "port": 3812}, "rf": {"host": "test", "port": 3712}},
)
def test_bad_config():
"""Test bad homematic config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("homematic", {"test": {"bla": "test", "port": 8080}})

View File

@ -1,22 +0,0 @@
"""Test Matter Server discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good Matter Server config."""
valid_discovery_config(
"matter",
{"host": "test", "port": 3812},
)
def test_bad_config():
"""Test bad Matter Server config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("matter", {"host": "test"})

View File

@ -1,17 +0,0 @@
"""Test motionEye discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config() -> None:
"""Test good motionEye config."""
valid_discovery_config("motioneye", {"url": "http://example.com:1234"})
def test_bad_config() -> None:
"""Test bad motionEye config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("motioneye", {})

View File

@ -1,21 +0,0 @@
"""Test MQTT discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good mqtt config."""
valid_discovery_config(
"mqtt", {"host": "test", "port": 3812, "username": "bla", "ssl": True}
)
def test_bad_config():
"""Test bad mqtt config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("mqtt", {"host": "test", "username": "bla", "ssl": True})

View File

@ -1,22 +0,0 @@
"""Test OTBR discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good OTBR config."""
valid_discovery_config(
"otbr",
{"host": "test", "port": 3812},
)
def test_bad_config():
"""Test bad OTBR config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("otbr", {"host": "test"})

View File

@ -1,22 +0,0 @@
"""Test OpenZwave MQTT discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good zwave mqtt config."""
valid_discovery_config(
"ozw",
{"host": "test", "port": 3812, "username": "bla", "password": "test"},
)
def test_bad_config():
"""Test bad zwave mqtt config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("ozw", {"host": "test", "username": "bla", "ssl": True})

View File

@ -1,21 +0,0 @@
"""Test rtsp_to_webrtc discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
SERVICE = "rtsp_to_webrtc"
def test_good_config():
"""Test good config."""
valid_discovery_config(SERVICE, {"host": "test", "port": 3812})
def test_bad_config():
"""Test bad config."""
with pytest.raises(vol.Invalid):
valid_discovery_config(SERVICE, {"host": "test"})

View File

@ -1,19 +0,0 @@
"""Test unifi discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good unifi config."""
valid_discovery_config("unifi", {"host": "test", "port": 3812})
def test_bad_config():
"""Test bad unifi config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("unifi", {"host": "test"})

View File

@ -1,21 +0,0 @@
"""Test validate of discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery import validate
def test_valid_services():
"""Validate that service is valid."""
for service in ("mqtt", "deconz"):
validate.valid_discovery_service(service)
def test_invalid_services():
"""Test that validate is invalid for a service."""
for service in ("fadsfasd", "203432"):
with pytest.raises(vol.Invalid):
validate.valid_discovery_service(service)

View File

@ -1,22 +0,0 @@
"""Test VLC Telnet discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good vlc telnet config."""
valid_discovery_config(
"vlc_telnet",
{"host": "test", "port": 3812, "password": "darksideofthemoon"},
)
def test_bad_config():
"""Test bad vlc telnet config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("vlc_telnet", {"host": "test", "port": 8283})

View File

@ -1,27 +0,0 @@
"""Test wyoming discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good wyoming config."""
valid_discovery_config("wyoming", {"uri": "tcp://core-wyoming"})
valid_discovery_config("wyoming", {"uri": "tcp://core-wyoming:1234"})
def test_bad_config():
"""Test bad wyoming config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("wyoming", {"host": "test"})
with pytest.raises(vol.Invalid):
valid_discovery_config("wyoming", {"uri": "https://also.an.uri.com"})
with pytest.raises(vol.Invalid):
valid_discovery_config("wyoming", {"uri": "unix://not/supported/yet.socket"})

View File

@ -1,22 +0,0 @@
"""Test Zwave JS discovery."""
import pytest
import voluptuous as vol
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good zwave js config."""
valid_discovery_config(
"zwave_js",
{"host": "test", "port": 3812},
)
def test_bad_config():
"""Test bad zwave js config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("zwave_js", {"host": "test"})