mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-18 14:46:30 +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()
|
return set()
|
||||||
|
|
||||||
# Validate devices
|
# Validate devices
|
||||||
options_validator = AddonOptions(self.coresys, raw_schema)
|
options_validator = AddonOptions(self.coresys, raw_schema, self.name, self.slug)
|
||||||
with suppress(vol.Invalid):
|
with suppress(vol.Invalid):
|
||||||
options_validator(self.options)
|
options_validator(self.options)
|
||||||
|
|
||||||
@ -551,7 +551,9 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
# create voluptuous
|
# create voluptuous
|
||||||
new_schema = vol.Schema(
|
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
|
# validate
|
||||||
|
@ -531,7 +531,9 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
|
|
||||||
if isinstance(raw_schema, bool):
|
if isinstance(raw_schema, bool):
|
||||||
raw_schema = {}
|
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
|
@property
|
||||||
def schema_ui(self) -> Optional[List[Dict[str, Any]]]:
|
def schema_ui(self) -> Optional[List[Dict[str, Any]]]:
|
||||||
|
@ -58,14 +58,14 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
"""Validate Add-ons Options."""
|
"""Validate Add-ons Options."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, coresys: CoreSys, raw_schema: Dict[str, Any], name: str, slug: str
|
||||||
coresys: CoreSys,
|
|
||||||
raw_schema: Dict[str, Any],
|
|
||||||
):
|
):
|
||||||
"""Validate schema."""
|
"""Validate schema."""
|
||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.raw_schema: Dict[str, Any] = raw_schema
|
self.raw_schema: Dict[str, Any] = raw_schema
|
||||||
self.devices: Set[Device] = set()
|
self.devices: Set[Device] = set()
|
||||||
|
self._name = name
|
||||||
|
self._slug = slug
|
||||||
|
|
||||||
def __call__(self, struct):
|
def __call__(self, struct):
|
||||||
"""Create schema validator for add-ons options."""
|
"""Create schema validator for add-ons options."""
|
||||||
@ -75,7 +75,12 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
for key, value in struct.items():
|
for key, value in struct.items():
|
||||||
# Ignore unknown options / remove from list
|
# Ignore unknown options / remove from list
|
||||||
if key not in self.raw_schema:
|
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
|
continue
|
||||||
|
|
||||||
typ = self.raw_schema[key]
|
typ = self.raw_schema[key]
|
||||||
@ -90,7 +95,9 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
# normal value
|
# normal value
|
||||||
options[key] = self._single_validate(typ, value, key)
|
options[key] = self._single_validate(typ, value, key)
|
||||||
except (IndexError, KeyError):
|
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")
|
self._check_missing_options(self.raw_schema, options, "root")
|
||||||
return options
|
return options
|
||||||
@ -100,20 +107,26 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
"""Validate a single element."""
|
"""Validate a single element."""
|
||||||
# if required argument
|
# if required argument
|
||||||
if value is None:
|
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
|
# Lookup secret
|
||||||
if str(value).startswith("!secret "):
|
if str(value).startswith("!secret "):
|
||||||
secret: str = value.partition(" ")[2]
|
secret: str = value.partition(" ")[2]
|
||||||
value = self.sys_homeassistant.secrets.get(secret)
|
value = self.sys_homeassistant.secrets.get(secret)
|
||||||
if value is None:
|
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
|
# parse extend data from type
|
||||||
match = RE_SCHEMA_ELEMENT.match(typ)
|
match = RE_SCHEMA_ELEMENT.match(typ)
|
||||||
|
|
||||||
if not match:
|
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
|
# prepare range
|
||||||
range_args = {}
|
range_args = {}
|
||||||
@ -144,7 +157,9 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
try:
|
try:
|
||||||
device = self.sys_hardware.get_by_path(Path(value))
|
device = self.sys_hardware.get_by_path(Path(value))
|
||||||
except HardwareNotFound:
|
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
|
# Have filter
|
||||||
if match.group("filter"):
|
if match.group("filter"):
|
||||||
@ -152,14 +167,16 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
device_filter = _create_device_filter(str_filter)
|
device_filter = _create_device_filter(str_filter)
|
||||||
if device not in self.sys_hardware.filter_devices(**device_filter):
|
if device not in self.sys_hardware.filter_devices(**device_filter):
|
||||||
raise vol.Invalid(
|
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
|
# Device valid
|
||||||
self.devices.add(device)
|
self.devices.add(device)
|
||||||
return str(device.path)
|
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):
|
def _nested_validate_list(self, typ: Any, data_list: List[Any], key: str):
|
||||||
"""Validate nested items."""
|
"""Validate nested items."""
|
||||||
@ -167,7 +184,9 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
|
|
||||||
# Make sure it is a list
|
# Make sure it is a list
|
||||||
if not isinstance(data_list, 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
|
# Process list
|
||||||
for element in data_list:
|
for element in data_list:
|
||||||
@ -188,13 +207,17 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
|
|
||||||
# Make sure it is a dict
|
# Make sure it is a dict
|
||||||
if not isinstance(data_dict, 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
|
# Process dict
|
||||||
for c_key, c_value in data_dict.items():
|
for c_key, c_value in data_dict.items():
|
||||||
# Ignore unknown options / remove from list
|
# Ignore unknown options / remove from list
|
||||||
if c_key not in typ:
|
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
|
continue
|
||||||
|
|
||||||
# Nested?
|
# Nested?
|
||||||
@ -216,7 +239,9 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
for miss_opt in missing:
|
for miss_opt in missing:
|
||||||
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
|
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
|
||||||
continue
|
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):
|
class UiOptions(CoreSysAttributes):
|
||||||
|
@ -7,29 +7,40 @@ import voluptuous as vol
|
|||||||
from supervisor.addons.options import AddonOptions, UiOptions
|
from supervisor.addons.options import AddonOptions, UiOptions
|
||||||
from supervisor.hardware.data import Device
|
from supervisor.hardware.data import Device
|
||||||
|
|
||||||
|
MOCK_ADDON_NAME = "Mock Add-on"
|
||||||
|
MOCK_ADDON_SLUG = "mock_addon"
|
||||||
|
|
||||||
|
|
||||||
def test_simple_schema(coresys):
|
def test_simple_schema(coresys):
|
||||||
"""Test with simple schema."""
|
"""Test with simple schema."""
|
||||||
assert AddonOptions(
|
assert AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "fires": True, "alias": "test"})
|
)({"name": "Pascal", "password": "1234", "fires": True, "alias": "test"})
|
||||||
|
|
||||||
assert AddonOptions(
|
assert AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "fires": True})
|
)({"name": "Pascal", "password": "1234", "fires": True})
|
||||||
|
|
||||||
with pytest.raises(vol.error.Invalid):
|
with pytest.raises(vol.error.Invalid):
|
||||||
AddonOptions(
|
AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "fires": "hah"})
|
)({"name": "Pascal", "password": "1234", "fires": "hah"})
|
||||||
|
|
||||||
with pytest.raises(vol.error.Invalid):
|
with pytest.raises(vol.error.Invalid):
|
||||||
AddonOptions(
|
AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
{"name": "str", "password": "password", "fires": "bool", "alias": "str?"},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "fires": True})
|
)({"name": "Pascal", "fires": True})
|
||||||
|
|
||||||
|
|
||||||
@ -38,18 +49,24 @@ def test_complex_schema_list(coresys):
|
|||||||
assert AddonOptions(
|
assert AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "extend": ["str"]},
|
{"name": "str", "password": "password", "extend": ["str"]},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "extend": ["test", "blu"]})
|
)({"name": "Pascal", "password": "1234", "extend": ["test", "blu"]})
|
||||||
|
|
||||||
with pytest.raises(vol.error.Invalid):
|
with pytest.raises(vol.error.Invalid):
|
||||||
AddonOptions(
|
AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "extend": ["str"]},
|
{"name": "str", "password": "password", "extend": ["str"]},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "extend": ["test", 1]})
|
)({"name": "Pascal", "password": "1234", "extend": ["test", 1]})
|
||||||
|
|
||||||
with pytest.raises(vol.error.Invalid):
|
with pytest.raises(vol.error.Invalid):
|
||||||
AddonOptions(
|
AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "extend": ["str"]},
|
{"name": "str", "password": "password", "extend": ["str"]},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "extend": "test"})
|
)({"name": "Pascal", "password": "1234", "extend": "test"})
|
||||||
|
|
||||||
|
|
||||||
@ -58,18 +75,24 @@ def test_complex_schema_dict(coresys):
|
|||||||
assert AddonOptions(
|
assert AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "extend": {"test": "int"}},
|
{"name": "str", "password": "password", "extend": {"test": "int"}},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "extend": {"test": 1}})
|
)({"name": "Pascal", "password": "1234", "extend": {"test": 1}})
|
||||||
|
|
||||||
with pytest.raises(vol.error.Invalid):
|
with pytest.raises(vol.error.Invalid):
|
||||||
AddonOptions(
|
AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "extend": {"test": "int"}},
|
{"name": "str", "password": "password", "extend": {"test": "int"}},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "extend": {"wrong": 1}})
|
)({"name": "Pascal", "password": "1234", "extend": {"wrong": 1}})
|
||||||
|
|
||||||
with pytest.raises(vol.error.Invalid):
|
with pytest.raises(vol.error.Invalid):
|
||||||
AddonOptions(
|
AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "extend": ["str"]},
|
{"name": "str", "password": "password", "extend": ["str"]},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "extend": "test"})
|
)({"name": "Pascal", "password": "1234", "extend": "test"})
|
||||||
|
|
||||||
|
|
||||||
@ -107,29 +130,39 @@ def test_simple_device_schema(coresys):
|
|||||||
assert AddonOptions(
|
assert AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "input": "device"},
|
{"name": "str", "password": "password", "input": "device"},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "input": "/dev/ttyUSB0"})
|
)({"name": "Pascal", "password": "1234", "input": "/dev/ttyUSB0"})
|
||||||
|
|
||||||
data = AddonOptions(
|
data = AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "input": "device"},
|
{"name": "str", "password": "password", "input": "device"},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "input": "/dev/serial/by-id/xyx"})
|
)({"name": "Pascal", "password": "1234", "input": "/dev/serial/by-id/xyx"})
|
||||||
assert data["input"] == "/dev/ttyUSB0"
|
assert data["input"] == "/dev/ttyUSB0"
|
||||||
|
|
||||||
assert AddonOptions(
|
assert AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "input": "device(subsystem=tty)"},
|
{"name": "str", "password": "password", "input": "device(subsystem=tty)"},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "input": "/dev/ttyACM0"})
|
)({"name": "Pascal", "password": "1234", "input": "/dev/ttyACM0"})
|
||||||
|
|
||||||
with pytest.raises(vol.error.Invalid):
|
with pytest.raises(vol.error.Invalid):
|
||||||
assert AddonOptions(
|
assert AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "input": "device"},
|
{"name": "str", "password": "password", "input": "device"},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "input": "/dev/not_exists"})
|
)({"name": "Pascal", "password": "1234", "input": "/dev/not_exists"})
|
||||||
|
|
||||||
with pytest.raises(vol.error.Invalid):
|
with pytest.raises(vol.error.Invalid):
|
||||||
assert AddonOptions(
|
assert AddonOptions(
|
||||||
coresys,
|
coresys,
|
||||||
{"name": "str", "password": "password", "input": "device(subsystem=tty)"},
|
{"name": "str", "password": "password", "input": "device(subsystem=tty)"},
|
||||||
|
MOCK_ADDON_NAME,
|
||||||
|
MOCK_ADDON_SLUG,
|
||||||
)({"name": "Pascal", "password": "1234", "input": "/dev/video1"})
|
)({"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"]
|
["/dev/serial/by-id/xyx", "/dev/ttyACM0", "/dev/ttyS0", "/dev/video1"]
|
||||||
)
|
)
|
||||||
assert data[-1]["type"] == "select"
|
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