Add support for inspection rules

Related-Change: https://review.opendev.org/c/openstack/ironic/+/939217

Change-Id: I41141fd0aff9a2c8961caf0ef2caadc4a4a45e19
This commit is contained in:
cid
2025-03-27 09:33:21 +01:00
committed by Afonne-CID
parent 82fd250eac
commit ad9896c12a
10 changed files with 530 additions and 0 deletions

View File

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

View File

@@ -15,3 +15,4 @@ Baremetal Resources
v1/deploy_templates
v1/conductor
v1/runbooks
v1/inspection_rules

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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