Show add-on with bad config in log (#2601)

This commit is contained in:
Joakim Sørensen 2021-02-22 13:43:03 +01:00 committed by GitHub
parent e421284471
commit 15544ae589
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 92 additions and 18 deletions

View File

@ -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

View File

@ -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]]]:

View File

@ -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):

View File

@ -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
)