From ca0ee56ccc7312c3010f1725d6f7fdb6d97bf37a Mon Sep 17 00:00:00 2001 From: Afonne-CID Date: Sun, 30 Nov 2025 18:50:58 +0100 Subject: [PATCH] `is-empty` inspection rule to handle missing field Pass the ``is-empty`` rule check when checking fields that don't exist in the inventory. Closes-Bug: #2132346 Change-Id: I177740dd3a8558ed357af22c581e5cbf1c3e862a Signed-off-by: Afonne-CID --- ironic/common/inspection_rules/base.py | 26 ++++++++---- .../tests/unit/common/test_inspection_rule.py | 42 +++++++++++++++++++ ...n-rule-missing-field-3c489a9b71c47538.yaml | 9 ++++ 3 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/is-empty-inspection-rule-missing-field-3c489a9b71c47538.yaml diff --git a/ironic/common/inspection_rules/base.py b/ironic/common/inspection_rules/base.py index 29d4d07dc4..3c3c6ff198 100644 --- a/ironic/common/inspection_rules/base.py +++ b/ironic/common/inspection_rules/base.py @@ -106,7 +106,7 @@ class Base(object): _("args must be either a list or dictionary")) def interpolate_variables(value, node, inventory, plugin_data, - loop_context=None): + loop_context=None, op=None): loop_context = loop_context or {} format_context = { 'node': node, @@ -115,12 +115,20 @@ class Base(object): **loop_context } - def safe_format(val, context): + def safe_format(val, context, op=None): if isinstance(val, str): try: return val.format(**context) except (AttributeError, KeyError, ValueError, IndexError, TypeError) as e: + if isinstance(e, KeyError): + # Treat missing fields as empty for 'is-empty'. + if op == 'is-empty': + LOG.debug( + "Interpolation failed (missing field) " + "for 'is-empty': %(value)s, returning None", + {'value': val}) + return None LOG.warning( "Interpolation failed: %(value)s: %(error_class)s, " "%(error)s", {'value': val, @@ -148,21 +156,21 @@ class Base(object): # 'path': 'driver_info/ipmi_address', # 'value': '{inventory[bmc_address]}' # } - value = safe_format(value, format_context) + value = safe_format(value, format_context, op) if isinstance(value, str): - return safe_format(value, format_context) + return safe_format(value, format_context, op) elif isinstance(value, dict): return { - safe_format(k, format_context): Base.interpolate_variables( - v, node, inventory, plugin_data, loop_context) + safe_format(k, format_context, op): Base.interpolate_variables( + v, node, inventory, plugin_data, loop_context, op) for k, v in value.items() } elif isinstance(value, list): return [ - safe_format(v, format_context) if isinstance(v, str) + safe_format(v, format_context, op) if isinstance(v, str) else Base.interpolate_variables( - v, node, inventory, plugin_data, loop_context) + v, node, inventory, plugin_data, loop_context, op) for v in value ] return value @@ -192,7 +200,7 @@ class Base(object): formatted_args = getattr(self, 'FORMATTED_ARGS', []) return { k: (Base.interpolate_variables( - v, node, inventory, plugin_data, loop_context) + v, node, inventory, plugin_data, loop_context, op) if (k in formatted_args or loop_context) else v) for k, v in dict_args.items() } diff --git a/ironic/tests/unit/common/test_inspection_rule.py b/ironic/tests/unit/common/test_inspection_rule.py index 1dd6d6393a..81299073c8 100644 --- a/ironic/tests/unit/common/test_inspection_rule.py +++ b/ironic/tests/unit/common/test_inspection_rule.py @@ -526,6 +526,37 @@ class TestOperators(TestInspectionRules): result = op(task, **test_cases[1]) self.assertFalse(result) + def test_is_empty_with_missing_field(self): + """Test is-empty condition with missing field in inventory.""" + with task_manager.acquire(self.context, self.node.uuid) as task: + condition = { + 'op': 'is-empty', + 'args': {'value': '{inventory[i.do.not.exist]}'} + } + + op = inspection_rules.operators.EmptyOperator() + result = op.check_condition(task, condition, self.inventory, + self.plugin_data) + self.assertTrue(result) + + condition2 = { + 'op': 'is-empty', + 'args': {'value': '{inventory[bmc_address]}'} + } + result2 = op.check_condition(task, condition2, self.inventory, + self.plugin_data) + self.assertFalse(result2) + + test_inventory = self.inventory.copy() + test_inventory['empty_field'] = '' + condition3 = { + 'op': 'is-empty', + 'args': {'value': '{inventory[empty_field]}'} + } + result3 = op.check_condition(task, condition3, test_inventory, + self.plugin_data) + self.assertTrue(result3) + class TestActions(TestInspectionRules): """Test inspection rule actions""" @@ -1027,6 +1058,17 @@ class TestInterpolation(TestInspectionRules): value, task.node, self.inventory, self.plugin_data) self.assertEqual(value, result) + value = "{inventory[i.do.not.exist]}" + result = base.Base.interpolate_variables( + value, task.node, self.inventory, self.plugin_data, + op='is-empty') + self.assertIsNone(result) + + value = "{inventory[missing][key]}" + result = base.Base.interpolate_variables( + value, task.node, self.inventory, self.plugin_data, op='eq') + self.assertEqual(value, result) + class TestValidation(TestInspectionRules): def test_unsupported_operator_rejected(self): diff --git a/releasenotes/notes/is-empty-inspection-rule-missing-field-3c489a9b71c47538.yaml b/releasenotes/notes/is-empty-inspection-rule-missing-field-3c489a9b71c47538.yaml new file mode 100644 index 0000000000..fa5bb713e7 --- /dev/null +++ b/releasenotes/notes/is-empty-inspection-rule-missing-field-3c489a9b71c47538.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixes the ``is-empty`` inspection rule to properly handle missing fields + in the inventory. Previously, when checking for empty values using fields + that don't exist in the inventory, the rule would fail with a KeyError + during variable interpolation. Now, missing fields are treated as empty + for the ``is-empty`` operation, allowing the rule to pass when checking + for non-existent inventory fields.