From 15544ae58973623b055d89451689c3a0d30f1da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 22 Feb 2021 13:43:03 +0100 Subject: [PATCH] Show add-on with bad config in log (#2601) --- supervisor/addons/addon.py | 6 ++-- supervisor/addons/model.py | 4 ++- supervisor/addons/options.py | 55 ++++++++++++++++++++++++++---------- tests/addons/test_options.py | 45 +++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 18 deletions(-) diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 35f83ed8f..ff47fde05 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -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 diff --git a/supervisor/addons/model.py b/supervisor/addons/model.py index a65c4a088..a19757a55 100644 --- a/supervisor/addons/model.py +++ b/supervisor/addons/model.py @@ -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]]]: diff --git a/supervisor/addons/options.py b/supervisor/addons/options.py index 1b7caf3b7..c7bbf31cd 100644 --- a/supervisor/addons/options.py +++ b/supervisor/addons/options.py @@ -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): diff --git a/tests/addons/test_options.py b/tests/addons/test_options.py index 1cd52171c..2b081b71f 100644 --- a/tests/addons/test_options.py +++ b/tests/addons/test_options.py @@ -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 + )