mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-16 05:36:29 +00:00
Show add-on with bad config in log (#2601)
This commit is contained in:
parent
e421284471
commit
15544ae589
@ -404,7 +404,7 @@ class Addon(AddonModel):
|
||||
return set()
|
||||
|
||||
# Validate devices
|
||||
options_validator = AddonOptions(self.coresys, raw_schema)
|
||||
options_validator = AddonOptions(self.coresys, raw_schema, self.name, self.slug)
|
||||
with suppress(vol.Invalid):
|
||||
options_validator(self.options)
|
||||
|
||||
@ -551,7 +551,9 @@ class Addon(AddonModel):
|
||||
|
||||
# create voluptuous
|
||||
new_schema = vol.Schema(
|
||||
vol.All(dict, AddonOptions(self.coresys, new_raw_schema))
|
||||
vol.All(
|
||||
dict, AddonOptions(self.coresys, new_raw_schema, self.name, self.slug)
|
||||
)
|
||||
)
|
||||
|
||||
# validate
|
||||
|
@ -531,7 +531,9 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
|
||||
if isinstance(raw_schema, bool):
|
||||
raw_schema = {}
|
||||
return vol.Schema(vol.All(dict, AddonOptions(self.coresys, raw_schema)))
|
||||
return vol.Schema(
|
||||
vol.All(dict, AddonOptions(self.coresys, raw_schema, self.name, self.slug))
|
||||
)
|
||||
|
||||
@property
|
||||
def schema_ui(self) -> Optional[List[Dict[str, Any]]]:
|
||||
|
@ -58,14 +58,14 @@ class AddonOptions(CoreSysAttributes):
|
||||
"""Validate Add-ons Options."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coresys: CoreSys,
|
||||
raw_schema: Dict[str, Any],
|
||||
self, coresys: CoreSys, raw_schema: Dict[str, Any], name: str, slug: str
|
||||
):
|
||||
"""Validate schema."""
|
||||
self.coresys: CoreSys = coresys
|
||||
self.raw_schema: Dict[str, Any] = raw_schema
|
||||
self.devices: Set[Device] = set()
|
||||
self._name = name
|
||||
self._slug = slug
|
||||
|
||||
def __call__(self, struct):
|
||||
"""Create schema validator for add-ons options."""
|
||||
@ -75,7 +75,12 @@ class AddonOptions(CoreSysAttributes):
|
||||
for key, value in struct.items():
|
||||
# Ignore unknown options / remove from list
|
||||
if key not in self.raw_schema:
|
||||
_LOGGER.warning("Unknown options %s", key)
|
||||
_LOGGER.warning(
|
||||
"Option '%s' does not exist in the schema for %s (%s)",
|
||||
key,
|
||||
self._name,
|
||||
self._slug,
|
||||
)
|
||||
continue
|
||||
|
||||
typ = self.raw_schema[key]
|
||||
@ -90,7 +95,9 @@ class AddonOptions(CoreSysAttributes):
|
||||
# normal value
|
||||
options[key] = self._single_validate(typ, value, key)
|
||||
except (IndexError, KeyError):
|
||||
raise vol.Invalid(f"Type error for {key}") from None
|
||||
raise vol.Invalid(
|
||||
f"Type error for option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
self._check_missing_options(self.raw_schema, options, "root")
|
||||
return options
|
||||
@ -100,20 +107,26 @@ class AddonOptions(CoreSysAttributes):
|
||||
"""Validate a single element."""
|
||||
# if required argument
|
||||
if value is None:
|
||||
raise vol.Invalid(f"Missing required option '{key}'") from None
|
||||
raise vol.Invalid(
|
||||
f"Missing required option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Lookup secret
|
||||
if str(value).startswith("!secret "):
|
||||
secret: str = value.partition(" ")[2]
|
||||
value = self.sys_homeassistant.secrets.get(secret)
|
||||
if value is None:
|
||||
raise vol.Invalid(f"Unknown secret {secret}") from None
|
||||
raise vol.Invalid(
|
||||
f"Unknown secret '{secret}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# parse extend data from type
|
||||
match = RE_SCHEMA_ELEMENT.match(typ)
|
||||
|
||||
if not match:
|
||||
raise vol.Invalid(f"Unknown type {typ}") from None
|
||||
raise vol.Invalid(
|
||||
f"Unknown type '{typ}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# prepare range
|
||||
range_args = {}
|
||||
@ -144,7 +157,9 @@ class AddonOptions(CoreSysAttributes):
|
||||
try:
|
||||
device = self.sys_hardware.get_by_path(Path(value))
|
||||
except HardwareNotFound:
|
||||
raise vol.Invalid(f"Device {value} does not exists!") from None
|
||||
raise vol.Invalid(
|
||||
f"Device '{value}' does not exists! in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Have filter
|
||||
if match.group("filter"):
|
||||
@ -152,14 +167,16 @@ class AddonOptions(CoreSysAttributes):
|
||||
device_filter = _create_device_filter(str_filter)
|
||||
if device not in self.sys_hardware.filter_devices(**device_filter):
|
||||
raise vol.Invalid(
|
||||
f"Device {value} don't match the filter {str_filter}!"
|
||||
f"Device '{value}' don't match the filter {str_filter}! in {self._name} ({self._slug})"
|
||||
)
|
||||
|
||||
# Device valid
|
||||
self.devices.add(device)
|
||||
return str(device.path)
|
||||
|
||||
raise vol.Invalid(f"Fatal error for {key} type {typ}") from None
|
||||
raise vol.Invalid(
|
||||
f"Fatal error for option '{key}' with type '{typ}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
def _nested_validate_list(self, typ: Any, data_list: List[Any], key: str):
|
||||
"""Validate nested items."""
|
||||
@ -167,7 +184,9 @@ class AddonOptions(CoreSysAttributes):
|
||||
|
||||
# Make sure it is a list
|
||||
if not isinstance(data_list, list):
|
||||
raise vol.Invalid(f"Invalid list for {key}") from None
|
||||
raise vol.Invalid(
|
||||
f"Invalid list for option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Process list
|
||||
for element in data_list:
|
||||
@ -188,13 +207,17 @@ class AddonOptions(CoreSysAttributes):
|
||||
|
||||
# Make sure it is a dict
|
||||
if not isinstance(data_dict, dict):
|
||||
raise vol.Invalid(f"Invalid dict for {key}") from None
|
||||
raise vol.Invalid(
|
||||
f"Invalid dict for option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Process dict
|
||||
for c_key, c_value in data_dict.items():
|
||||
# Ignore unknown options / remove from list
|
||||
if c_key not in typ:
|
||||
_LOGGER.warning("Unknown options %s", c_key)
|
||||
_LOGGER.warning(
|
||||
"Unknown option '%s' for %s (%s)", c_key, self._name, self._slug
|
||||
)
|
||||
continue
|
||||
|
||||
# Nested?
|
||||
@ -216,7 +239,9 @@ class AddonOptions(CoreSysAttributes):
|
||||
for miss_opt in missing:
|
||||
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
|
||||
continue
|
||||
raise vol.Invalid(f"Missing option {miss_opt} in {root}") from None
|
||||
raise vol.Invalid(
|
||||
f"Missing option '{miss_opt}' in {root} in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
|
||||
class UiOptions(CoreSysAttributes):
|
||||
|
@ -7,29 +7,40 @@ import voluptuous as vol
|
||||
from supervisor.addons.options import AddonOptions, UiOptions
|
||||
from supervisor.hardware.data import Device
|
||||
|
||||
MOCK_ADDON_NAME = "Mock Add-on"
|
||||
MOCK_ADDON_SLUG = "mock_addon"
|
||||
|
||||
|
||||
def test_simple_schema(coresys):
|
||||
"""Test with simple schema."""
|
||||
assert AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "fires": True, "alias": "test"})
|
||||
|
||||
assert AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "fires": True})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "fires": "hah"})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "fires": True})
|
||||
|
||||
|
||||
@ -38,18 +49,24 @@ def test_complex_schema_list(coresys):
|
||||
assert AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "extend": ["str"]},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "extend": ["test", "blu"]})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "extend": ["str"]},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "extend": ["test", 1]})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "extend": ["str"]},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "extend": "test"})
|
||||
|
||||
|
||||
@ -58,18 +75,24 @@ def test_complex_schema_dict(coresys):
|
||||
assert AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "extend": {"test": "int"}},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "extend": {"test": 1}})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "extend": {"test": "int"}},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "extend": {"wrong": 1}})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "extend": ["str"]},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "extend": "test"})
|
||||
|
||||
|
||||
@ -107,29 +130,39 @@ def test_simple_device_schema(coresys):
|
||||
assert AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "input": "device"},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "input": "/dev/ttyUSB0"})
|
||||
|
||||
data = AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "input": "device"},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "input": "/dev/serial/by-id/xyx"})
|
||||
assert data["input"] == "/dev/ttyUSB0"
|
||||
|
||||
assert AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "input": "device(subsystem=tty)"},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "input": "/dev/ttyACM0"})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
assert AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "input": "device"},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "input": "/dev/not_exists"})
|
||||
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
assert AddonOptions(
|
||||
coresys,
|
||||
{"name": "str", "password": "password", "input": "device(subsystem=tty)"},
|
||||
MOCK_ADDON_NAME,
|
||||
MOCK_ADDON_SLUG,
|
||||
)({"name": "Pascal", "password": "1234", "input": "/dev/video1"})
|
||||
|
||||
|
||||
@ -299,3 +332,15 @@ def test_ui_simple_device_schema_no_filter(coresys):
|
||||
["/dev/serial/by-id/xyx", "/dev/ttyACM0", "/dev/ttyS0", "/dev/video1"]
|
||||
)
|
||||
assert data[-1]["type"] == "select"
|
||||
|
||||
|
||||
def test_log_entry(coresys, caplog):
|
||||
"""Test log entry when no option match in schema."""
|
||||
options = AddonOptions(coresys, {}, MOCK_ADDON_NAME, MOCK_ADDON_SLUG)(
|
||||
{"test": "str"}
|
||||
)
|
||||
assert options == {}
|
||||
assert (
|
||||
"Option 'test' does not exist in the schema for Mock Add-on (mock_addon)"
|
||||
in caplog.text
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user