From ad9896c12ad85451673c1cb8cafc558e39909e7e Mon Sep 17 00:00:00 2001 From: cid Date: Thu, 27 Mar 2025 09:33:21 +0100 Subject: [PATCH] Add support for inspection rules Related-Change: https://review.opendev.org/c/openstack/ironic/+/939217 Change-Id: I41141fd0aff9a2c8961caf0ef2caadc4a4a45e19 --- doc/source/user/proxies/baremetal.rst | 8 + doc/source/user/resources/baremetal/index.rst | 1 + .../baremetal/v1/inspection_rules.rst | 13 ++ openstack/baremetal/v1/_proxy.py | 104 ++++++++++ openstack/baremetal/v1/inspection_rules.py | 57 ++++++ openstack/tests/functional/baremetal/base.py | 12 ++ .../test_baremetal_inspection_rules.py | 192 ++++++++++++++++++ .../baremetal/v1/test_inspection_rules.py | 90 ++++++++ .../tests/unit/baremetal/v1/test_proxy.py | 47 +++++ .../inspection-rules-86b1c59def73f757.yaml | 6 + 10 files changed, 530 insertions(+) create mode 100644 doc/source/user/resources/baremetal/v1/inspection_rules.rst create mode 100644 openstack/baremetal/v1/inspection_rules.py create mode 100644 openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py create mode 100644 openstack/tests/unit/baremetal/v1/test_inspection_rules.py create mode 100644 releasenotes/notes/inspection-rules-86b1c59def73f757.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 4d054dc05..1fc657ad7 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -110,6 +110,14 @@ Runbook Operations create_runbook, update_runbook, patch_runbook, delete_runbook +Inspection Rule Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + :noindex: + :members: inspection_rules, get_inspection_rule, + create_inspection_rule, update_inspection_rule, + patch_inspection_rule, delete_inspection_rule + Utilities --------- diff --git a/doc/source/user/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst index fa198a0ab..4e0a5f196 100644 --- a/doc/source/user/resources/baremetal/index.rst +++ b/doc/source/user/resources/baremetal/index.rst @@ -15,3 +15,4 @@ Baremetal Resources v1/deploy_templates v1/conductor v1/runbooks + v1/inspection_rules diff --git a/doc/source/user/resources/baremetal/v1/inspection_rules.rst b/doc/source/user/resources/baremetal/v1/inspection_rules.rst new file mode 100644 index 000000000..dcfdc6320 --- /dev/null +++ b/doc/source/user/resources/baremetal/v1/inspection_rules.rst @@ -0,0 +1,13 @@ +openstack.baremetal.v1.inspection_rules +======================================= + +.. automodule:: openstack.baremetal.v1.inspection_rules + +The InspectionRule Class +------------------------- + +The ``InspectionRule`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.baremetal.v1.inspection_rules.InspectionRule + :members: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index ecccf17cc..98b589102 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -20,6 +20,7 @@ from openstack.baremetal.v1 import chassis as _chassis from openstack.baremetal.v1 import conductor as _conductor from openstack.baremetal.v1 import deploy_templates as _deploytemplates from openstack.baremetal.v1 import driver as _driver +from openstack.baremetal.v1 import inspection_rules as _inspectionrules from openstack.baremetal.v1 import node as _node from openstack.baremetal.v1 import port as _port from openstack.baremetal.v1 import port_group as _portgroup @@ -47,6 +48,7 @@ class Proxy(proxy.Proxy): "runbook": _runbooks.Runbook, "volume_connector": _volumeconnector.VolumeConnector, "volume_target": _volumetarget.VolumeTarget, + "inspection_rules": _inspectionrules.InspectionRule, } def _get_with_fields(self, resource_type, value, fields=None): @@ -2026,3 +2028,105 @@ class Proxy(proxy.Proxy): to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait, callback) + + # ========== Inspection Rules ========== + + def inspection_rules(self, details=False, **query): + """Retrieve a generator of inspection rules. + + :param dict query: Optional query parameters to be sent to + restrict the inspection rules to be returned. + + :returns: A generator of InspectionRule instances. + """ + if details: + query['details'] = True + return _inspectionrules.InspectionRule.list(self, **query) + + def create_inspection_rule(self, **attrs): + """Create a new inspection rule from attributes. + + :param dict attrs: Keyword arguments that will be used to create a + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule`. + + :returns: The results of inspection rule creation. + :rtype: + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule`. + """ + return self._create(_inspectionrules.InspectionRule, **attrs) + + def get_inspection_rule(self, inspection_rule, fields=None): + """Get a specific inspection rule. + + :param inspection_rule: The ID of an inspection rule + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + instance. + + :param fields: Limit the resource fields to fetch. + + :returns: One + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + :raises: :class:`~openstack.exceptions.NotFoundException` when no + inspection rule matching the ID could be found. + """ + return self._get_with_fields( + _inspectionrules.InspectionRule, inspection_rule, fields=fields + ) + + def update_inspection_rule(self, inspection_rule, **attrs): + """Update an inspection rule. + + :param inspection_rule: Either the ID of an inspection rule + or an instance of + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule`. + :param dict attrs: The attributes to update on the + inspection rule represented by the ``inspection_rule`` parameter. + + :returns: The updated inspection rule. + :rtype: + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + """ + return self._update( + _inspectionrules.InspectionRule, inspection_rule, **attrs + ) + + def delete_inspection_rule(self, inspection_rule, ignore_missing=True): + """Delete an inspection rule. + + :param inspection_rule: The value can be either the ID of a + inspection_rule or a + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + instance. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.NotFoundException` will be raised + when the inspection rule could not be found. + When set to ``True``, no exception will be raised when + attempting to delete a non-existent inspection rule. + + :returns: The instance of the inspection rule which was deleted. + :rtype: + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule`. + """ + return self._delete( + _inspectionrules.InspectionRule, + inspection_rule, + ignore_missing=ignore_missing, + ) + + def patch_inspection_rule(self, inspection_rule, patch): + """Apply a JSON patch to the inspection rule. + + :param inspection_rule: The value can be the ID of a + inspection_rule or a + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + instance. + + :param patch: JSON patch to apply. + + :returns: The updated inspection rule. + :rtype: + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + """ + return self._get_resource( + _inspectionrules.InspectionRule, inspection_rule + ).patch(self, patch) diff --git a/openstack/baremetal/v1/inspection_rules.py b/openstack/baremetal/v1/inspection_rules.py new file mode 100644 index 000000000..10c509e3c --- /dev/null +++ b/openstack/baremetal/v1/inspection_rules.py @@ -0,0 +1,57 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack.baremetal.v1 import _common +from openstack import resource + + +class InspectionRule(_common.Resource): + resources_key = 'inspection_rules' + base_path = '/inspection_rules' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_patch = True + commit_method = 'PATCH' + commit_jsonpatch = True + + _query_mapping = resource.QueryParameters( + 'detail', + fields={'type': _common.fields_type}, + ) + + # Inspection rules is available since 1.96 + _max_microversion = '1.96' + #: The actions to be executed when the rule conditions are met. + actions = resource.Body('actions', type=list) + #: A brief explanation about the inspection rule. + description = resource.Body('description') + #: The conditions under which the rule should be triggered. + conditions = resource.Body('conditions', type=list) + #: Timestamp at which the resource was created. + created_at = resource.Body('created_at') + #: A list of relative links. Includes the self and bookmark links. + links = resource.Body('links', type=list) + #: Specifies the phase when the rule should run, defaults to 'main'. + phase = resource.Body('phase') + #: Specifies the rule's precedence level during execution. + priority = resource.Body('priority') + #: Indicates whether the rule contains sensitive information. + sensitive = resource.Body('sensitive', type=bool) + #: Timestamp at which the resource was last updated. + updated_at = resource.Body('updated_at') + #: The UUID of the resource. + id = resource.Body('uuid', alternate_id=True) diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 11a0cbadd..4fb49446d 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -132,3 +132,15 @@ class BaseBaremetalTest(base.BaseFunctionalTest): ) ) return runbook + + def create_inspection_rule(self, **kwargs): + """Create a new inspection_rule from attributes.""" + + inspection_rule = self.conn.baremetal.create_inspection_rule(**kwargs) + + self.addCleanup( + lambda: self.conn.baremetal.delete_inspection_rule( + inspection_rule.id, ignore_missing=True + ) + ) + return inspection_rule diff --git a/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py b/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py new file mode 100644 index 000000000..6424048ca --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py @@ -0,0 +1,192 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack import exceptions +from openstack.tests.functional.baremetal import base + + +class TestBareMetalInspectionRule(base.BaseBaremetalTest): + min_microversion = '1.96' + + def setUp(self): + super().setUp() + + def test_baremetal_inspection_rule_create_get_delete(self): + actions = [{"op": "set-attribute", "args": ["/driver", "idrac"]}] + conditions = [ + {"op": "eq", "args": ["node:memory_mb", 4096], "multiple": "all"} + ] + inspection_rule = self.create_inspection_rule( + actions=actions, + conditions=conditions, + description="Test inspection rule", + phase="main", + priority=100, + sensitive=False, + ) + loaded = self.conn.baremetal.get_inspection_rule(inspection_rule.id) + self.assertEqual(loaded.id, inspection_rule.id) + self.conn.baremetal.delete_inspection_rule( + inspection_rule, ignore_missing=False + ) + self.assertRaises( + exceptions.NotFoundException, + self.conn.baremetal.get_inspection_rule, + inspection_rule.id, + ) + + def test_baremetal_inspection_rule_list(self): + actions = [{"op": "set-attribute", "args": ["/driver", "idrac"]}] + conditions = [ + { + "op": "is-true", + "args": ["{node.auto_discovered}"], + "multiple": "any", + } + ] + + inspection_rule1 = self.create_inspection_rule( + actions=actions, + conditions=conditions, + description="Test inspection rule 1", + ) + inspection_rule2 = self.create_inspection_rule( + actions=actions, + conditions=conditions, + description="Test inspection rule 2", + ) + inspection_rules = self.conn.baremetal.inspection_rules() + ids = [rule.id for rule in inspection_rules] + self.assertIn(inspection_rule1.id, ids) + self.assertIn(inspection_rule2.id, ids) + + inspection_rules_with_details = self.conn.baremetal.inspection_rules( + details=True + ) + for rule in inspection_rules_with_details: + self.assertIsNotNone(rule.id) + self.assertIsNotNone(rule.description) + + inspection_rule_with_fields = self.conn.baremetal.inspection_rules( + fields=['uuid'] + ) + for rule in inspection_rule_with_fields: + self.assertIsNotNone(rule.id) + self.assertIsNone(rule.description) + + def test_baremetal_inspection_rule_list_update_delete(self): + actions = [{"op": "set-attribute", "args": ["/driver", "idrac"]}] + conditions = [ + { + "op": "eq", + "args": ["node:cpu_arch", "x86_64"], + "multiple": "all", + } + ] + inspection_rule = self.create_inspection_rule( + actions=actions, + conditions=conditions, + description="Test inspection rule", + ) + self.assertFalse(inspection_rule.extra) + inspection_rule.description = 'Updated inspection rule' + + inspection_rule = self.conn.baremetal.update_inspection_rule( + inspection_rule + ) + self.assertEqual( + 'Updated inspection rule', inspection_rule.description + ) + + inspection_rule = self.conn.baremetal.get_inspection_rule( + inspection_rule.id + ) + + self.conn.baremetal.delete_inspection_rule( + inspection_rule.id, ignore_missing=False + ) + + def test_baremetal_inspection_rule_update(self): + actions = [{"op": "set-attribute", "args": ["/driver", "idrac"]}] + conditions = [ + {"op": "ge", "args": ["node:memory_mb", 4096], "multiple": "all"} + ] + inspection_rule = self.create_inspection_rule( + actions=actions, conditions=conditions, phase="main", priority=100 + ) + inspection_rule.priority = 150 + + inspection_rule = self.conn.baremetal.update_inspection_rule( + inspection_rule + ) + self.assertEqual(150, inspection_rule.priority) + + inspection_rule = self.conn.baremetal.get_inspection_rule( + inspection_rule.id + ) + self.assertEqual(150, inspection_rule.priority) + + def test_inspection_rule_patch(self): + description = "BIOS configuration rule" + actions = [ + { + "op": "set-attribute", + "args": ["/properties/capabilities", "boot_mode:uefi"], + } + ] + conditions = [ + { + "op": "is-true", + "args": ["{node.auto_discovered}"], + "multiple": "any", + } + ] + inspection_rule = self.create_inspection_rule( + actions=actions, + conditions=conditions, + description=description, + sensitive=False, + ) + + updated_actions = [ + { + "op": "set-attribute", + "args": ["/driver", "fake"], + } + ] + + inspection_rule = self.conn.baremetal.patch_inspection_rule( + inspection_rule, + dict(path='/actions', op='add', value=updated_actions), + ) + self.assertEqual(updated_actions, inspection_rule.actions) + self.assertEqual(description, inspection_rule.description) + + inspection_rule = self.conn.baremetal.get_inspection_rule( + inspection_rule.id + ) + self.assertEqual(updated_actions, inspection_rule.actions) + + def test_inspection_rule_negative_non_existing(self): + uuid = "bbb45f41-d4bc-4307-8d1d-32f95ce1e920" + self.assertRaises( + exceptions.NotFoundException, + self.conn.baremetal.get_inspection_rule, + uuid, + ) + self.assertRaises( + exceptions.NotFoundException, + self.conn.baremetal.delete_inspection_rule, + uuid, + ignore_missing=False, + ) + self.assertIsNone(self.conn.baremetal.delete_inspection_rule(uuid)) diff --git a/openstack/tests/unit/baremetal/v1/test_inspection_rules.py b/openstack/tests/unit/baremetal/v1/test_inspection_rules.py new file mode 100644 index 000000000..446dba659 --- /dev/null +++ b/openstack/tests/unit/baremetal/v1/test_inspection_rules.py @@ -0,0 +1,90 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack.baremetal.v1 import inspection_rules +from openstack.tests.unit import base + + +FAKE = { + "created_at": "2025-03-18T22:28:48.643434+11:11", + "description": "BMC credentials", + "phase": "main", + "priority": 100, + "sensitive": False, + "actions": [ + { + "op": "set-attribute", + "args": { + "path": "/properties/cpus", + "value": "{inventory[cpu][count]}", + }, + }, + { + "op": "set-attribute", + "args": { + "path": "/properties/memory_mb", + "value": "{inventory[memory][physical_mb]}", + }, + }, + { + "op": "set-attribute", + "args": { + "path": "/properties/cpu_arch", + "value": "{inventory[cpu][architecture]}", + }, + }, + ], + "conditions": [ + {"op": "is-true", "args": {"value": "{inventory[cpu][count]}"}} + ], + "links": [ + { + "href": "http://10.60.253.180:6385/v1/inspection_rules" + "/783bf33a-a8e3-1e23-a645-1e95a1f95186", + "rel": "self", + }, + { + "href": "http://10.60.253.180:6385/inspection_rules" + "/783bf33a-a8e3-1e23-a645-1e95a1f95186", + "rel": "bookmark", + }, + ], + "updated_at": None, + "uuid": "783bf33a-a8e3-1e23-a645-1e95a1f95186", +} + + +class InspectionRules(base.TestCase): + def test_basic(self): + sot = inspection_rules.InspectionRule() + self.assertIsNone(sot.resource_key) + self.assertEqual('inspection_rules', sot.resources_key) + self.assertEqual('/inspection_rules', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + + def test_instantiate(self): + sot = inspection_rules.InspectionRule(**FAKE) + self.assertEqual(FAKE['actions'], sot.actions) + self.assertEqual(FAKE['description'], sot.description) + self.assertEqual(FAKE['conditions'], sot.conditions) + self.assertEqual(FAKE['created_at'], sot.created_at) + self.assertEqual(FAKE['links'], sot.links) + self.assertEqual(FAKE['phase'], sot.phase) + self.assertEqual(FAKE['priority'], sot.priority) + self.assertEqual(FAKE['sensitive'], sot.sensitive) + self.assertEqual(FAKE['updated_at'], sot.updated_at) + self.assertEqual(FAKE['uuid'], sot.id) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index bd330d1af..0afad7ba7 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -17,6 +17,7 @@ from openstack.baremetal.v1 import allocation from openstack.baremetal.v1 import chassis from openstack.baremetal.v1 import deploy_templates from openstack.baremetal.v1 import driver +from openstack.baremetal.v1 import inspection_rules from openstack.baremetal.v1 import node from openstack.baremetal.v1 import port from openstack.baremetal.v1 import port_group @@ -510,3 +511,49 @@ class TestWaitForNodesProvisionState(base.TestCase): self.assertEqual(['1'], [x.id for x in result.success]) self.assertEqual(['3'], [x.id for x in result.timeout]) self.assertEqual(['2'], [x.id for x in result.failure]) + + +class TestInspectionRules(TestBaremetalProxy): + @mock.patch.object(inspection_rules.InspectionRule, 'list') + def test_inspection_rules_detailed(self, mock_list): + result = self.proxy.inspection_rules(details=True, query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, details=True, query=1) + + @mock.patch.object(inspection_rules.InspectionRule, 'list') + def test_inspection_rules_not_detailed(self, mock_list): + result = self.proxy.inspection_rules(query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, query=1) + + def test_create_inspection_rule(self): + self.verify_create( + self.proxy.create_inspection_rule, inspection_rules.InspectionRule + ) + + def test_get_inspection_rule(self): + self.verify_get( + self.proxy.get_inspection_rule, + inspection_rules.InspectionRule, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}, + ) + + def test_update_inspection_rule(self): + self.verify_update( + self.proxy.update_inspection_rule, inspection_rules.InspectionRule + ) + + def test_delete_inspection_rule(self): + self.verify_delete( + self.proxy.delete_inspection_rule, + inspection_rules.InspectionRule, + False, + ) + + def test_delete_inspection_rule_ignore(self): + self.verify_delete( + self.proxy.delete_inspection_rule, + inspection_rules.InspectionRule, + True, + ) diff --git a/releasenotes/notes/inspection-rules-86b1c59def73f757.yaml b/releasenotes/notes/inspection-rules-86b1c59def73f757.yaml new file mode 100644 index 000000000..1712e5863 --- /dev/null +++ b/releasenotes/notes/inspection-rules-86b1c59def73f757.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds support for inspection rules; an API feature to create a resource + containing conditions that evaluate against inspection data and actions + that run on a node when conditions are met during inspection.