Merge "list detailed ports by node"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..62b24c9
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,66 @@
+---
+default_language_version:
+  # force all unspecified python hooks to run python3
+  python: python3
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.5.0
+    hooks:
+      - id: trailing-whitespace
+        # NOTE(JayF): We shouldn't modify release notes after their
+        #  associated release. Instead, ignore these minor lint issues.
+      - id: mixed-line-ending
+        args: ['--fix', 'lf']
+        exclude: |
+          (?x)(
+          .*.svg$|
+          )
+      - id: fix-byte-order-marker
+      - id: check-merge-conflict
+      - id: debug-statements
+      - id: check-json
+        files: .*\.json$
+      - id: check-yaml
+        files: .*\.(yaml|yml)$
+        exclude: releasenotes/.*$
+  - repo: https://github.com/Lucas-C/pre-commit-hooks
+    rev: v1.5.4
+    hooks:
+      - id: remove-tabs
+        exclude: '.*\.(svg)$'
+  - repo: https://opendev.org/openstack/hacking
+    rev: 6.1.0
+    hooks:
+      - id: hacking
+        additional_dependencies: []
+        exclude: '^(doc|releasenotes|tools)/.*$'
+  - repo: https://github.com/codespell-project/codespell
+    rev: v2.4.1
+    hooks:
+    - id: codespell
+      args: [--write-changes]
+  - repo: https://github.com/sphinx-contrib/sphinx-lint
+    rev: v1.0.0
+    hooks:
+      - id: sphinx-lint
+        args: [--enable=default-role]
+        files: ^doc/|releasenotes|api-ref
+  - repo: https://opendev.org/openstack/bashate
+    rev: 2.1.0
+    hooks:
+      - id: bashate
+        args: ["-iE006,E044", "-eE005,E042"]
+        name: bashate
+        description: This hook runs bashate for linting shell scripts
+        entry: bashate
+        language: python
+        types: [shell]
+  - repo: https://github.com/PyCQA/doc8
+    rev: v1.1.2
+    hooks:
+      - id: doc8
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.7.3
+    hooks:
+      - id: ruff
+        args: ['--fix', '--unsafe-fixes']
diff --git a/ironic_tempest_plugin/common/waiters.py b/ironic_tempest_plugin/common/waiters.py
index e538cd8..313b4c3 100644
--- a/ironic_tempest_plugin/common/waiters.py
+++ b/ironic_tempest_plugin/common/waiters.py
@@ -18,6 +18,7 @@
 from tempest.lib import exceptions as lib_exc
 
 from ironic_tempest_plugin.common import utils
+from ironic_tempest_plugin import exceptions as ironic_exc
 
 LOG = log.getLogger(__name__)
 
@@ -164,7 +165,8 @@
 
 def wait_node_value_in_field(client, node_id, field, value,
                              raise_if_insufficent_access=True,
-                             timeout=None, interval=None):
+                             timeout=None, interval=None,
+                             abort_on_error_state=False):
     """Waits for a node to have a field value appear.
 
     :param client: an instance of tempest plugin BaremetalClient.
@@ -173,6 +175,8 @@
     :param value: the value/key with-in the field to look for.
     :param timeout: the timeout after which the check is considered as failed.
     :param interval: an interval between show_node calls for status check.
+    :param abort_on_error_state: whether to abort waiting if the node reaches
+        an error state.
     """
 
     def is_field_updated():
@@ -180,8 +184,17 @@
         field_value = node[field]
         if raise_if_insufficent_access and '** Redacted' in field_value:
             msg = ('Unable to see contents of redacted field '
-                   'indicating insufficent access to execute this test.')
-            raise lib_exc.InsufficientAPIAccess(msg)
+                   'indicating insufficient access to execute this test.')
+            raise ironic_exc.InsufficientAPIAccess(msg)
+        elif (abort_on_error_state
+              and (node['provision_state'].endswith('failed')
+                   or node['provision_state'] == 'error')):
+            msg = ('Node %(node)s reached failure state %(state)s while '
+                   'waiting Error: %(error)s' %
+                   {'node': node_id, 'state': node['provision_state'],
+                    'error': node.get('last_error')})
+            LOG.debug(msg)
+            raise lib_exc.TempestException(msg)
         return value in field_value
 
     if not test_utils.call_until_true(is_field_updated, timeout,
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index 9e96863..38a67eb 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -20,6 +20,10 @@
 from tempest import config  # noqa
 
 
+_INSPECTOR_REASON = ('ironic-inspector was retired in favor of the built-in'
+                     'agent inspect interface.')
+
+
 # NOTE(TheJulia): The following options are loaded into a tempest
 # plugin configuration option via plugin.py.
 ironic_service_option = cfg.BoolOpt('ironic',
@@ -29,6 +33,8 @@
 
 inspector_service_option = cfg.BoolOpt("ironic_inspector",
                                        default=False,
+                                       deprecated_for_removal=True,
+                                       deprecated_reason=_INSPECTOR_REASON,
                                        help="Whether or not ironic-inspector "
                                        "is expected to be available")
 
@@ -40,6 +46,8 @@
 
 inspector_scope_enforcement = cfg.BoolOpt('ironic_inspector',
                                           default=True,
+                                          deprecated_for_removal=True,
+                                          deprecated_reason=_INSPECTOR_REASON,
                                           help='Whether or not '
                                                'ironic-inspector is expected '
                                                'to enforce auth scope.')
@@ -224,9 +232,6 @@
                min=0,
                help="Ironic adjusted disk size to use in the standalone tests "
                     "as instance_info/root_gb value."),
-    cfg.IntOpt('available_nodes', min=0, default=None,
-               help="The number of baremetal hosts available to use for "
-                    "the tests."),
     cfg.BoolOpt('partition_netboot',
                 default=True,
                 help="Treat partition images as netbooted as opposed to "
@@ -282,33 +287,51 @@
                      "support for embedded network metadata through glean "
                      "or cloud-init, and thus cannot be executed with "
                      "most default job configurations."),
+    cfg.BoolOpt('trunks_supported',
+                default=False,
+                help="Define if trunks are supported by networking driver "
+                     "with baremetal nodes."),
 ]
 
 BaremetalIntrospectionGroup = [
     cfg.StrOpt('catalog_type',
                default='baremetal-introspection',
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="Catalog type of the baremetal provisioning service"),
     cfg.StrOpt('endpoint_type',
                default='publicURL',
                choices=['public', 'admin', 'internal',
                         'publicURL', 'adminURL', 'internalURL'],
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="The endpoint type to use for the baremetal introspection"
                     " service"),
     cfg.IntOpt('introspection_sleep',
                default=30,
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="Introspection sleep before check status"),
     cfg.IntOpt('introspection_timeout',
                default=600,
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="Introspection time out"),
     cfg.IntOpt('introspection_start_timeout',
                default=90,
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="Timeout to start introspection"),
     cfg.IntOpt('hypervisor_update_sleep',
                default=60,
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="Time to wait until nova becomes aware of "
                     "bare metal instances"),
     cfg.IntOpt('hypervisor_update_timeout',
                default=300,
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="Time out for wait until nova becomes aware of "
                     "bare metal instances"),
     # NOTE(aarefiev): status_check_period default is 60s, but checking
@@ -316,14 +339,20 @@
     # 80s would be enough to make one more check.
     cfg.IntOpt('ironic_sync_timeout',
                default=80,
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="Time it might take for Ironic--Inspector "
                     "sync to happen"),
     cfg.IntOpt('discovery_timeout',
                default=300,
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="Time to wait until new node would enrolled in "
                     "ironic"),
     cfg.BoolOpt('auto_discovery_feature',
                 default=False,
+                deprecated_for_removal=True,
+                deprecated_reason=_INSPECTOR_REASON,
                 help="Is the auto-discovery feature enabled. Enroll hook "
                      "should be specified in node_not_found_hook - processing "
                      "section of inspector.conf"),
@@ -331,11 +360,17 @@
                # TODO(dtantsur): change to fake-hardware when Queens is no
                # longer supported.
                default='fake',
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="The driver expected to be set on newly discovered nodes. "
                     "Only has effect with auto_discovery_feature is True."),
     cfg.StrOpt('auto_discovery_target_driver',
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="The driver to set on the newly discovered nodes. "
                     "Only has effect with auto_discovery_feature is True."),
     cfg.StrOpt('data_store',
+               deprecated_for_removal=True,
+               deprecated_reason=_INSPECTOR_REASON,
                help="The storage backend for storing introspection data."),
 ]
diff --git a/ironic_tempest_plugin/exceptions.py b/ironic_tempest_plugin/exceptions.py
index 865ab08..a1a9873 100644
--- a/ironic_tempest_plugin/exceptions.py
+++ b/ironic_tempest_plugin/exceptions.py
@@ -30,5 +30,5 @@
 
 
 class InsufficientAPIAccess(exceptions.TempestException):
-    message = ("Insufficent Access to the API exists. Please use a user "
+    message = ("Insufficient Access to the API exists. Please use a user "
                "with an elevated level of access to execute this test.")
diff --git a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
index 599f448..d12ece4 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -331,7 +331,6 @@
         :param description: The description of the chassis.
             Default: test-chassis
         :return: A tuple with the server response and the created chassis.
-
         """
         chassis = {'description': kwargs.get('description', 'test-chassis')}
 
@@ -1029,3 +1028,69 @@
             uri=f'{self.uri_prefix}/nodes/{uuid}/inventory')
         self.expected_success(http_client.OK, resp.status)
         return body
+
+    @base.handle_errors
+    def get_shards(self, api_version='1.82'):
+        """Get all shards."""
+
+        extra_headers, headers = self._get_headers(api_version)
+        return self._list_request('shards', headers=headers,
+                                  extra_headers=extra_headers)
+
+    @base.handle_errors
+    def list_node_firmware(self, node_uuid):
+        """List firmware for a node.
+
+        :param node_uuid: The unique identifier of the node.
+        """
+        return self._list_request('/nodes/%s/firmware' % node_uuid)
+
+    @base.handle_errors
+    def list_portgroups_detail(self):
+        """List detailed portgroups."""
+        return self._list_request('portgroups/detail')
+
+    @base.handle_errors
+    def list_portgroups_by_node(self, node_ident):
+        """List portgroups filtered by node."""
+        return self._list_request(f'nodes/{node_ident}/portgroups')
+
+    @base.handle_errors
+    def list_portgroups_details_by_node(self, node_ident):
+        """List detailed portgroups filtered by node."""
+        return self._list_request(f'nodes/{node_ident}/portgroups/detail')
+
+    @base.handle_errors
+    def create_inspection_rule(self, payload):
+        """Create Inspection rule.
+
+        :param payload: Inspection rule JSON
+        """
+        return self._create_request('inspection_rules', payload)
+
+    @base.handle_errors
+    def show_inspection_rule(self, rule_uuid):
+        """Show Inspection rule."""
+        return self._show_request('inspection_rules', rule_uuid)
+
+    @base.handle_errors
+    def list_inspection_rule(self, **kwargs):
+        """List all Inspection rules."""
+        return self._list_request('inspection_rules', **kwargs)
+
+    @base.handle_errors
+    def update_inspection_rule(self, rule_uuid, patch):
+        """Update the specified Inspection rule.
+
+        :param rule_uuid: The unique identifier of the inspection rule.
+        :param patch: List of dicts representing json patches.
+        """
+        return self._patch_request('inspection_rules', rule_uuid, patch)
+
+    @base.handle_errors
+    def delete_inspection_rule(self, rule_uuid):
+        """Delete Inspection rule.
+
+        :param rule_uuid: uuid of the inspection rule.
+        """
+        return self._delete_request('inspection_rules', rule_uuid)
diff --git a/ironic_tempest_plugin/tests/api/admin/test_inspection_rules.py b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules.py
new file mode 100644
index 0000000..3d00ce3
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules.py
@@ -0,0 +1,172 @@
+# 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 tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
+from ironic_tempest_plugin.tests.api import base
+
+
+class TestInspectionRules(base.BaseBaremetalTest):
+    """API tests for Inspection Rules endpoints"""
+
+    # Inspection rules API was introduced in microversion 1.96
+    # We will be skipping this test for the older version.
+    min_microversion = '1.96'
+
+    def setUp(self):
+        super(TestInspectionRules, self).setUp()
+
+        _, self.node = self.create_node(None)
+
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture('1.96'))
+
+    def _create_inspection_rule_payload(self, **kwargs):
+        """Create a Inspection rule payload."""
+        payload = {
+            "description": "Inspection rule to log node UUID",
+            "conditions": [],
+            "actions": [
+                {
+                    "op": "log",
+                    "args": {
+                        "msg": "Node with UUID {node.uuid} is being inspected"
+                    }
+                }
+            ],
+            "phase": "main",
+            "priority": 0,
+            "sensitive": False
+        }
+
+        payload.update(kwargs)
+
+        return payload
+
+    @decorators.idempotent_id('7fb771cd-b011-409e-a255-3c71cf7251e8')
+    def test_create_rule_sensitive_true(self):
+        """Test creating rule with sensitive=True."""
+        rule_uuid = data_utils.rand_uuid()
+        payload = self._create_inspection_rule_payload(sensitive=True)
+
+        self.create_inspection_rule(rule_uuid, payload)
+
+        _, fetched_rule = self.client.show_inspection_rule(rule_uuid)
+
+        self.assertTrue(fetched_rule.get('sensitive'))
+        self.assertIsNone(fetched_rule.get('conditions'))
+        self.assertIsNone(fetched_rule.get('actions'))
+
+    @decorators.idempotent_id('e60b4513-7c3d-4b2c-b485-17443bf6485f')
+    def test_create_rule_complex_logging_conditions_actions(self):
+        """Test creating rule with loop conditions and actions"""
+        complex_log_conditions = [
+            {
+                "op": "eq",
+                "args": [
+                    "{inventory.system.product_name}",
+                    "{item}"
+                ],
+                "loop": [
+                    "product_name_1",
+                    "product_name_2",
+                    "product_name_3"
+                ],
+                "multiple": "any"
+            }
+        ]
+
+        complex_log_actions = [
+            {
+                "op": "set-attribute",
+                "args": [
+                    "{item[path]}",
+                    "{item[value]}"
+                ],
+                "loop": [
+                    {
+                        "path": "/driver_info/ipmi_username",
+                        "value": "admin"
+                    },
+                    {
+                        "path": "/driver_info/ipmi_password",
+                        "value": "password"
+                    },
+                    {
+                        "path": "/driver_info/ipmi_address",
+                        "value": "{inventory[bmc_address]}"
+                    }
+                ]
+            }
+        ]
+
+        payload = self._create_inspection_rule_payload(
+            conditions=complex_log_conditions,
+            actions=complex_log_actions,
+        )
+
+        _, created_rule = self.create_inspection_rule(None, payload)
+
+        self.assertEqual(complex_log_conditions,
+                         created_rule.get('conditions'))
+        self.assertEqual(complex_log_actions,
+                         created_rule.get('actions'))
+
+    @decorators.idempotent_id('a786a4ec-1e43-4fb9-8fc3-c53aa4e1f52f')
+    def test_patch_conditions_actions_priority(self):
+        """Test Updating rule'si priority, condition and actions"""
+        payload = self._create_inspection_rule_payload()
+
+        patch = [
+            {
+                "op": "replace",
+                "path": "/priority",
+                "value": 200
+            },
+            {
+                "op": "replace",
+                "path": "/conditions",
+                "value": [
+                    {
+                        "op": "eq",
+                        "args": ["{{ inventory.cpu.count }}", 8]
+                    }
+                ]
+            },
+            {
+                "op": "replace",
+                "path": "/actions",
+                "value": [
+                    {
+                        "op": "set-attribute",
+                        "args": ["{{ /properties/cpu_model }}", "cpu_xyz"]
+                    },
+                    {
+                        "op": "log",
+                        "args": ["CPU model updated via rule."]
+                    }
+                ]
+            }
+        ]
+
+        _, created_rule = self.create_inspection_rule(None, payload)
+        _, fetched_rule = self.client.update_inspection_rule(
+            created_rule.get('uuid'), patch)
+
+        self.assertEqual(fetched_rule.get('priority'),
+                         patch[0]['value'])
+        self.assertEqual(fetched_rule.get('conditions'),
+                         patch[1]['value'])
+        self.assertEqual(fetched_rule.get('actions'),
+                         patch[2]['value'])
diff --git a/ironic_tempest_plugin/tests/api/admin/test_inspection_rules_negatives.py b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules_negatives.py
new file mode 100644
index 0000000..a92728b
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules_negatives.py
@@ -0,0 +1,79 @@
+#    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 tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
+from ironic_tempest_plugin.tests.api import base
+
+
+class TestInspectionRulesNegative(base.BaseBaremetalTest):
+    """Negative Inspection Rules test"""
+
+    # Inspection rules API was introduced in microversion 1.96
+    # We will be skipping this test for the older version.
+    min_microversion = '1.96'
+
+    def setUp(self):
+        super(TestInspectionRulesNegative, self).setUp()
+
+        _, self.node = self.create_node(None)
+
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture('1.96'))
+
+    def _create_inspection_rule_payload(self, **kwargs):
+        """Create a Inspection rule payload."""
+        payload = {
+            "description": "Inspection rule to log node UUID",
+            "conditions": [],
+            "actions": [
+                {
+                    "op": "log",
+                    "args": {
+                        "msg": "Node with UUID {node.uuid} is being inspected"
+                    }
+                }
+            ],
+            "phase": "main",
+            "priority": 0,
+            "sensitive": False
+        }
+
+        payload.update(kwargs)
+
+        return payload
+
+    @decorators.idempotent_id('55403d94-53ce-41ab-989a-da3399314c9d')
+    @decorators.attr(type=['negative'])
+    def test_create_invalid_priority_fails(self):
+        """Test to create Inspection rule with invalid priorities"""
+        invalid_priorities = [-1, 10000, 5000.50]
+
+        for priority_val in invalid_priorities:
+            payload = self._create_inspection_rule_payload(
+                priority=priority_val)
+
+            self.assertRaises(lib_exc.BadRequest,
+                              self.create_inspection_rule,
+                              rule_uuid=None, payload=payload)
+
+    @decorators.idempotent_id('cf9615b3-904e-4456-b00a-622d39892b88')
+    @decorators.attr(type=['negative'])
+    def test_delete_by_wrong_uiid(self):
+        """Test to delete Inspection Rule with wrong uuid"""
+        rule_uuid = data_utils.rand_uuid()
+        self.assertRaises(lib_exc.NotFound,
+                          self.delete_inspection_rule,
+                          rule_uuid=rule_uuid)
diff --git a/ironic_tempest_plugin/tests/api/admin/test_microversion_enforcement.py b/ironic_tempest_plugin/tests/api/admin/test_microversion_enforcement.py
new file mode 100644
index 0000000..6dd9982
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_microversion_enforcement.py
@@ -0,0 +1,1499 @@
+# 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.
+
+import random
+
+from oslo_utils import timeutils
+from oslo_utils import uuidutils
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from ironic_tempest_plugin.services.baremetal.v1.json.baremetal_client import \
+    BaremetalClient
+from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
+from ironic_tempest_plugin.tests.api import base
+
+CONF = config.CONF
+
+
+class MicroversionTestMixin:
+    """Mixin class containing shared microversion test functionality."""
+
+    def _microversion_test(
+        self,
+        method_name,
+        min_version,
+        expected_error,
+        required_args,
+        ignore_positive=False,
+    ):
+        """Test methods with invalid API versions"""
+
+        major, minor = map(int, min_version.split("."))
+        invalid_versions = []
+        if minor >= 11:
+            invalid_versions.append(f"{major}.{minor - 10}")
+        if minor >= 2:
+            invalid_versions.append(f"{major}.{minor - 1}")
+        elif minor == 0 and major > 1:
+            invalid_versions.append(f"{major - 1}.99")
+        elif minor == 1:
+            invalid_versions.append(f"{major}.0")
+        else:
+            # We expect fails for v1.0 and below
+            raise ValueError(f"Invalid microversion {min_version}")
+
+        # Get method name from method object
+        method_name = method_name.__name__
+
+        # Test with invalid versions (should fail)
+        for microversion in invalid_versions:
+            msg = (
+                f"Testing {method_name} with version {microversion} "
+                f"and arguments {required_args} - should fail"
+            )
+            with self.subTest(
+                msg=msg, method=method_name, version=microversion
+            ):
+                self.useFixture(
+                    api_microversion_fixture.APIMicroversionFixture(
+                        microversion
+                    )
+                )
+                method = getattr(self.client, method_name)
+
+                try:
+                    method(**required_args)
+                    self.fail(
+                        f"Request for microversion {microversion} for "
+                        f"{method_name} unexpectedly succeeded. We expected "
+                        f"{expected_error.__name__}."
+                    )
+                except expected_error:
+                    pass  # Expected error, test passes
+                except Exception as e:
+                    self.fail(
+                        f"Request for microversion {microversion} for "
+                        f"{method_name} raised unexpected exception: {e}"
+                    )
+
+        if ignore_positive:
+            return True
+
+        # Test with valid version (should succeed)
+        # Use the minimum required version
+        msg = (
+            f"Testing {method_name} with version {min_version} "
+            f"and arguments {required_args} - should succeed"
+        )
+        with self.subTest(
+            msg=msg, method=method_name, version=min_version
+        ):
+            self.useFixture(
+                api_microversion_fixture.APIMicroversionFixture(
+                    min_version
+                )
+            )
+            method = getattr(self.client, method_name)
+
+            try:
+                # We don't check the actual response, just
+                # that it doesn't raise
+                # the expected error from the negative test
+                method(**required_args)
+                return True
+
+            except expected_error as e:
+                self.fail(
+                    f"Method {method_name} failed with valid "
+                    f"microversion {min_version}: {e}"
+                )
+            except Exception as e:
+                # Other exceptions might be expected due to invalid test data
+                # For example, a 404 might be expected if we're using fake IDs
+                self.assertNotIsInstance(
+                    e,
+                    expected_error,
+                    (
+                        f"Got unexpected {expected_error.__name__} with valid "
+                        f"microversion {min_version}: {e}"
+                    ),
+                )
+
+
+class BaseTestMicroversionEnforcement(base.BaseBaremetalTest):
+    """Base class for microversion enforcement tests."""
+
+    def setUp(self):
+        super().setUp()
+        self.resource_class = uuidutils.generate_uuid()
+
+
+class TestNodesMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for nodes-related API microversion enforcement."""
+
+    min_microversion = "1.1"
+
+    def setUp(self):
+        super(TestNodesMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+
+    # NOTE(adamcarthur) - We test the positive case in the other
+    # tests. Doing it here would require changing the test function
+    # to allow returns.
+    @decorators.idempotent_id("be29d566-43d5-41ce-8c8a-ce360af9995b")
+    def test_create_node(self):
+        self._microversion_test(
+            BaremetalClient.create_node,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"chassis_id": self.chassis["uuid"]},
+            ignore_positive=True
+        )
+
+    @decorators.idempotent_id("3ddd0ae9-979b-44ac-9cdc-2cabd2f61118")
+    def test_list_nodes(self):
+        self._microversion_test(
+            BaremetalClient.list_nodes, "1.1", lib_exc.NotAcceptable, {}
+        )
+
+    @decorators.idempotent_id("d47160dd-654e-427b-91b0-9a4298a7ff54")
+    def test_list_nodes_detail(self):
+        self._microversion_test(
+            BaremetalClient.list_nodes_detail, "1.1", lib_exc.NotAcceptable, {}
+        )
+
+    @decorators.idempotent_id("a612b489-d4c6-4f29-9dcd-ca7a71d93f77")
+    def test_show_node(self):
+        _, node = self.create_node(self.chassis["uuid"])
+        self._microversion_test(
+            BaremetalClient.show_node,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"uuid": node["uuid"]},
+        )
+        # delete the node
+        self.client.delete_node(node["uuid"])
+
+    @decorators.idempotent_id("02623737-84f5-42f1-a8f3-0f3a8c1319da")
+    def test_update_node(self):
+        _, node = self.create_node(self.chassis["uuid"])
+        instance_uuid = data_utils.rand_uuid()
+        self._microversion_test(
+            BaremetalClient.update_node,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"uuid": node["uuid"], "patch": instance_uuid},
+        )
+        # delete the node
+        self.client.delete_node(node["uuid"])
+
+    @decorators.idempotent_id("59f433f1-05b8-47a9-b14a-f4aba337698d")
+    def test_delete_node(self):
+        _, node = self.create_node(self.chassis["uuid"])
+        self._microversion_test(
+            BaremetalClient.delete_node,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"uuid": node["uuid"]},
+        )
+
+
+class TestNodeManagementMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node management-related API microversion enforcement."""
+
+    min_microversion = "1.1"
+
+    def setUp(self):
+        super(TestNodeManagementMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    def _validate_provision_state(self, node_uuid, target_state):
+        # Validate that provision state is set within timeout
+        start = timeutils.utcnow()
+        while timeutils.delta_seconds(
+                start, timeutils.utcnow()) < self.unprovision_timeout:
+            _, node = self.client.show_node(node_uuid)
+            if node['provision_state'] == target_state:
+                return
+        message = ('Failed to set provision state %(state)s within '
+                   'the required time: %(timeout)s sec.',
+                   {'state': target_state,
+                    'timeout': self.unprovision_timeout})
+        raise lib_exc.TimeoutException(message)
+
+    @decorators.idempotent_id("54a58e1e-8334-4152-84b4-2974cabb74e8")
+    def test_validate_driver_interface(self):
+        self._microversion_test(
+            BaremetalClient.validate_driver_interface,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"node_uuid": self.node["uuid"]},
+        )
+
+    @decorators.idempotent_id("9adfa595-8937-4a99-83e5-6012ddcbddc0")
+    def test_set_node_power_state(self):
+        self._microversion_test(
+            BaremetalClient.set_node_power_state,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"node_uuid": self.node["uuid"], "state": "power off"},
+        )
+
+    @decorators.idempotent_id("efd978ce-2ce5-4dc3-8f69-ff063644a550")
+    def test_set_node_provision_state(self):
+        contd = self._microversion_test(
+            BaremetalClient.set_node_provision_state,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"node_uuid": self.node["uuid"], "state": "manageable"},
+        )
+
+        if contd:
+            self._validate_provision_state(self.node["uuid"], "manageable")
+
+    @decorators.idempotent_id("893e4815-2c55-40f6-84e3-1816e28f6803")
+    def test_set_node_raid_config(self):
+        self._microversion_test(
+            BaremetalClient.set_node_raid_config,
+            "1.12",
+            lib_exc.NotAcceptable,
+            {"node_uuid": self.node["uuid"], "target_raid_config": {}},
+        )
+
+    @decorators.idempotent_id("f6b835da-dea3-46b2-8e16-6f910ad7cc86")
+    def test_get_console(self):
+        self._microversion_test(
+            BaremetalClient.get_console,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"node_uuid": self.node["uuid"]},
+        )
+
+    @decorators.idempotent_id("2dc96b6b-5713-467d-9707-389292ae5460")
+    def test_set_console_mode(self):
+        self._microversion_test(
+            BaremetalClient.set_console_mode,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"node_uuid": self.node["uuid"], "enabled": True},
+        )
+
+    @decorators.idempotent_id("4b4e23ae-3d18-4f35-a140-d10abcf22110")
+    def test_set_node_boot_device(self):
+        self._microversion_test(
+            BaremetalClient.set_node_boot_device,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"node_uuid": self.node["uuid"], "boot_device": "pxe"},
+        )
+
+    @decorators.idempotent_id("f13b398e-cde1-4862-a183-f20eb56d3234")
+    def test_get_node_boot_device(self):
+        self._microversion_test(
+            BaremetalClient.get_node_boot_device,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"node_uuid": self.node["uuid"]},
+        )
+
+    @decorators.idempotent_id("88985d9e-ccd1-4894-bc5d-e712ce29b892")
+    def test_get_node_supported_boot_devices(self):
+        self._microversion_test(
+            BaremetalClient.get_node_supported_boot_devices,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"node_uuid": self.node["uuid"]},
+        )
+
+
+class TestNodeVMediaMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node vmedia-related API microversion enforcement."""
+
+    min_microversion = "1.89"
+
+    def setUp(self):
+        super(TestNodeVMediaMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("305c0689-c43b-407c-8b78-932ff86e4b3d")
+    def test_attach_node_vmedia(self):
+        self._microversion_test(
+            BaremetalClient._put_request,
+            "1.89",
+            lib_exc.NotFound,
+            {
+                "resource": f"nodes/{self.node['uuid']}/vmedia",
+                "put_object": {
+                    "device_type": "CDROM", "image_url": "http://image"
+                },
+            },
+        )
+
+    @decorators.idempotent_id("1d4d15bb-a66c-47f9-bf41-cc5e09854bcc")
+    def test_detach_node_vmedia(self):
+        self._microversion_test(
+            BaremetalClient._delete_request,
+            "1.89",
+            lib_exc.NotFound,
+            {"resource": f"nodes/{self.node['uuid']}/vmedia", "uuid": ""},
+        )
+
+
+class TestNodeVendorPassthruMethodsMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node vendor passthru methods API microversion enforcement."""
+
+    min_microversion = "1.1"
+
+    def setUp(self):
+        super(TestNodeVendorPassthruMethodsMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("8599e266-debc-4548-af5a-cd2223a5e051")
+    def test_list_vendor_passthru_methods(self):
+        self._microversion_test(
+            BaremetalClient.list_vendor_passthru_methods,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"node_uuid": self.node["uuid"]},
+        )
+
+
+class TestNodeVendorPassthruCallMethodMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for vendor passthru call method API microversion"""
+
+    min_microversion = "1.1"
+
+    def setUp(self):
+        super(TestNodeVendorPassthruCallMethodMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("35b34e5d-78d4-49e6-81b3-48b536f9fb60")
+    def test_call_vendor_passthru_method(self):
+        self._microversion_test(
+            BaremetalClient._put_request,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {
+                "resource": f"nodes/{self.node['uuid']}/vendor_passthru",
+                "put_object": {"method": "fakemethod"},
+            },
+        )
+
+
+class TestNodeTraitsMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node traits-related API microversion enforcement."""
+
+    min_microversion = "1.37"
+
+    def setUp(self):
+        super(TestNodeTraitsMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+        self.traits = ["CUSTOM_TRAIT1", "HW_CPU_X86_VMX"]
+
+    @decorators.idempotent_id("3f4ba6c1-5b39-4cb1-a823-21dcea802d61")
+    def test_list_node_traits(self):
+        self._microversion_test(
+            BaremetalClient.list_node_traits,
+            "1.37",
+            lib_exc.NotAcceptable,
+            {"node_uuid": self.node["uuid"]},
+        )
+
+    @decorators.idempotent_id("10d77871-cd8a-4ecb-b6a4-f38d6ad719fe")
+    def test_set_node_traits(self):
+        self._microversion_test(
+            BaremetalClient.set_node_traits,
+            "1.37",
+            lib_exc.UnexpectedResponseCode,
+            {"node_uuid": self.node["uuid"], "traits": self.traits},
+        )
+
+    @decorators.idempotent_id("12ca1f62-9d4a-490c-8f8a-a10fc742630a")
+    def test_add_node_trait(self):
+        self._microversion_test(
+            BaremetalClient.add_node_trait,
+            "1.37",
+            lib_exc.UnexpectedResponseCode,
+            {"node_uuid": self.node["uuid"], "trait": "CUSTOM_TRAIT1"},
+        )
+
+    @decorators.idempotent_id("dd2f3b89-3fe3-4861-ba11-608b365ff484")
+    def test_remove_node_traits(self):
+        # add a trait to remove it
+        self.client.add_node_trait(self.node["uuid"], "CUSTOM_TRAIT1")
+        self._microversion_test(
+            BaremetalClient.remove_node_traits,
+            "1.37",
+            lib_exc.NotFound,
+            {"node_uuid": self.node["uuid"]},
+        )
+
+    @decorators.idempotent_id("be438903-26d0-4ee6-970e-2d6d66058842")
+    def test_remove_node_trait(self):
+        self.client.add_node_trait(self.node["uuid"], "CUSTOM_TRAIT1")
+        self._microversion_test(
+            BaremetalClient.remove_node_trait,
+            "1.37",
+            lib_exc.NotFound,
+            {"node_uuid": self.node["uuid"], "trait": "CUSTOM_TRAIT1"},
+        )
+
+
+class TestNodeVIFsMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node VIFs-related API microversion enforcement."""
+
+    min_microversion = "1.28"
+
+    def setUp(self):
+        super(TestNodeVIFsMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+        self.vif_id = uuidutils.generate_uuid()
+
+    @decorators.idempotent_id("9b26c364-0623-4bd4-ac53-a3eca8d58fea")
+    def test_list_node_vifs(self):
+        self._microversion_test(
+            BaremetalClient.vif_list,
+            "1.28",
+            lib_exc.NotFound,
+            {"node_uuid": self.node["uuid"]},
+        )
+
+    @decorators.idempotent_id("4e91a305-145d-408d-9aea-08d73ef8d082")
+    def test_attach_node_vif(self):
+        self._microversion_test(
+            BaremetalClient.vif_attach,
+            "1.28",
+            lib_exc.NotFound,
+            {"node_uuid": self.node["uuid"], "vif_id": self.vif_id},
+        )
+
+    @decorators.idempotent_id("ce93713b-5df8-44ce-815c-c18ae0203c63")
+    def test_detach_node_vif(self):
+        self._microversion_test(
+            BaremetalClient.vif_detach,
+            "1.28",
+            lib_exc.NotFound,
+            {"node_uuid": self.node["uuid"], "vif_id": self.vif_id},
+        )
+
+
+class TestNodeIndicatorsMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node indicators-related API microversion enforcement."""
+
+    min_microversion = "1.1"
+
+    def setUp(self):
+        super(TestNodeIndicatorsMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+        self.component = "system"
+        self.ind_ident = "led"
+        self.state = "ON"
+
+    @decorators.idempotent_id("79194e54-c98d-4e55-a368-2a2ba2199844")
+    def test_get_node_indicator_state(self):
+        self._microversion_test(
+            BaremetalClient.get_node_indicator_state,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {
+                "node_uuid": self.node["uuid"],
+                "component": self.component,
+                "ind_ident": self.ind_ident,
+            },
+        )
+
+
+class TestPortgroupMicroversions(BaseTestMicroversionEnforcement,
+                                 MicroversionTestMixin):
+    """Tests for Portgroup-related API microversion enforcement.
+
+    Portgroup APIs (e.g. listing, creating, showing, updating and deleting)
+    were introduced in microversion 1.23.
+    """
+
+    min_microversion = "1.23"
+
+    def setUp(self):
+        super(TestPortgroupMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis['uuid'])
+
+    @decorators.idempotent_id("a8b52cdf-8471-40d7-9bc0-6ecfefa33adf")
+    def test_list_portgroups(self):
+        self._microversion_test(
+            BaremetalClient.list_portgroups,
+            "1.23",
+            lib_exc.NotFound,
+            {}  # Empty dict for no required args
+        )
+
+    @decorators.idempotent_id("cb5b92f0-fdb6-4789-b9b5-ae147cbe288b")
+    def test_list_portgroups_detail(self):
+        self._microversion_test(
+            BaremetalClient.list_portgroups_detail,
+            "1.23",
+            lib_exc.NotFound,
+            {}  # Empty dict for no required args
+        )
+
+    @decorators.idempotent_id("aafb5f2d-22e4-4b07-98c6-7f8f5f4a6699")
+    def test_create_portgroup(self):
+        self._microversion_test(
+            BaremetalClient.create_portgroup,
+            "1.23",
+            lib_exc.NotFound,
+            {
+                "node_uuid": self.node['uuid'],
+                "address": "11:11:11:11:11:11",
+                "name": "test_portgroup"
+            }
+        )
+
+    @decorators.idempotent_id("ea27635c-132f-4cff-9f93-b11bd4baa5a2")
+    def test_show_portgroup(self):
+        _, portgroup = self.client.create_portgroup(self.node['uuid'])
+        self._microversion_test(
+            BaremetalClient.show_portgroup,
+            "1.23",
+            lib_exc.NotFound,
+            {"portgroup_ident": portgroup['uuid']}
+        )
+
+    @decorators.idempotent_id("48dfef1f-b95f-4767-86ee-d6257c52cefd")
+    def test_update_portgroup(self):
+        patch_doc = [
+            {"op": "replace", "path": "/address", "value": "22:22:22:22:22:22"}
+        ]
+        # Need to create a portgroup to delete it
+        _, portgroup = self.create_portgroup(self.node['uuid'])
+        self._microversion_test(
+            BaremetalClient.update_portgroup,
+            "1.23",
+            lib_exc.NotFound,
+            {
+                "uuid": portgroup['uuid'],
+                "patch": patch_doc
+            }
+        )
+
+    @decorators.idempotent_id("e0884cbe-bacc-4347-be14-72e475855eff")
+    def test_delete_portgroup(self):
+        # Need to create a portgroup to delete it
+        _, portgroup = self.create_portgroup(self.node['uuid'])
+
+        self._microversion_test(
+            BaremetalClient.delete_portgroup,
+            "1.23",
+            lib_exc.NotFound,
+            {"portgroup_ident": portgroup['uuid']}
+        )
+
+
+class TestPortgroupByNodeMicroversions(BaseTestMicroversionEnforcement,
+                                       MicroversionTestMixin):
+    """Tests for listing Portgroups by Node.
+
+    These endpoints (e.g. /v1/nodes/{node_ident}/portgroups and
+    /v1/nodes/{node_ident}/portgroups/detail) were introduced
+    in microversion 1.24.
+    """
+
+    min_microversion = "1.24"
+
+    def setUp(self):
+        super(TestPortgroupByNodeMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis['uuid'])
+
+    @decorators.idempotent_id("c967b02f-1acc-4714-98b0-07e49e9b3482")
+    def test_list_portgroups_by_node(self):
+        self._microversion_test(
+            BaremetalClient.list_portgroups_by_node,
+            "1.24",
+            lib_exc.NotFound,
+            {"node_ident": self.node['uuid']}
+        )
+
+    @decorators.idempotent_id("8c38415b-1aad-4519-81fb-0792ebf3731a")
+    def test_list_portgroups_details_by_node(self):
+        self._microversion_test(
+            BaremetalClient.list_portgroups_details_by_node,
+            "1.24",
+            lib_exc.NotFound,
+            {"node_ident": self.node['uuid']}
+        )
+
+
+class TestPortMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for port-related API microversion enforcement."""
+
+    min_microversion = "1.1"
+
+    def setUp(self):
+        super(TestPortMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+        random_mac = (
+            f"02:00:00:{random.randint(0, 255):02x}"
+            f":{random.randint(0, 255):02x}"
+            f":{random.randint(0, 255):02x}"
+        )
+
+        _, self.port = self.create_port(
+            self.node["uuid"], address=random_mac
+        )
+
+    @decorators.idempotent_id("99e628a4-0d10-44a1-aed5-0b82d0155103")
+    def test_list_ports(self):
+        self._microversion_test(
+            BaremetalClient.list_ports,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {},  # Empty dict for no required args
+        )
+
+    @decorators.idempotent_id("0c01ce9a-8945-4604-b315-3f8a1856c621")
+    def test_list_ports_detail(self):
+        self._microversion_test(
+            BaremetalClient.list_ports_detail,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {},  # Empty dict for no required args
+        )
+
+    @decorators.idempotent_id("15b3f4d5-d8ca-44af-b0fe-75f9a86896b1")
+    def test_create_port(self):
+        self._microversion_test(
+            BaremetalClient.create_port,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {
+                "node_id": self.node["uuid"],
+                "address": "11:11:11:11:11:11",
+            },
+        )
+
+    @decorators.idempotent_id("637f088f-1460-46c4-a3cf-3568c1997461")
+    def test_show_port(self):
+        self._microversion_test(
+            BaremetalClient.show_port,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"uuid": "fake_port"},
+        )
+
+    @decorators.idempotent_id("5cf7930d-3e0b-44f7-b9b8-a941dea2782e")
+    def test_update_port(self):
+        patch_doc = [
+            {"op": "replace", "path": "/address", "value": "22:22:22:22:22:22"}
+        ]
+        self._microversion_test(
+            BaremetalClient.update_port,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"uuid": self.port["uuid"], "patch": patch_doc},
+        )
+
+    @decorators.idempotent_id("b5d92c53-9d2e-485f-ac6c-1e0d020d5af3")
+    def test_delete_port(self):
+        self._microversion_test(
+            BaremetalClient.delete_port,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"uuid": self.port["uuid"]},
+        )
+
+
+class TestNodePortsMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for listing Ports by Node API microversion enforcement."""
+
+    min_microversion = "1.1"
+
+    def setUp(self):
+        super(TestNodePortsMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("7a0acf09-d527-4d77-92bd-3eac81687bc7")
+    def test_list_ports_by_node(self):
+        self._microversion_test(
+            BaremetalClient.list_node_ports,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"uuid": self.node["uuid"]},
+        )
+
+    @decorators.idempotent_id("df017231-a8d3-4b6b-8583-0e6fbf61e834")
+    def test_list_ports_details_by_node(self):
+        self._microversion_test(
+            BaremetalClient.list_node_ports,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"uuid": self.node["uuid"]},
+        )
+
+
+class TestPortgroupPortsMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for listing Ports by Portgroup API microversion enforcement."""
+
+    min_microversion = "1.24"
+
+    def setUp(self):
+        super(TestPortgroupPortsMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+        _, self.portgroup = self.create_portgroup(self.node["uuid"])
+
+    @decorators.idempotent_id("bb3a1fa6-52cf-4e19-98d0-746c2421ff9a")
+    def test_list_ports_by_portgroup(self):
+        self._microversion_test(
+            BaremetalClient.list_ports,
+            "1.24",
+            lib_exc.NotAcceptable,
+            {"portgroup": self.portgroup["uuid"]},
+        )
+
+    @decorators.idempotent_id("6740fe09-539d-415b-bc44-f38f458f2547")
+    def test_list_ports_details_by_portgroup(self):
+        self._microversion_test(
+            BaremetalClient.list_ports_detail,
+            "1.24",
+            lib_exc.NotAcceptable,
+            {"portgroup": self.portgroup["uuid"]},
+        )
+
+
+class TestVolumeConnectorMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for volume connector-related API microversion enforcement."""
+
+    min_microversion = "1.32"
+
+    def setUp(self):
+        super(TestVolumeConnectorMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("1bde0eb4-c63c-4d05-897d-b2247dd97863")
+    def test_list_volume_connectors(self):
+        self._microversion_test(
+            BaremetalClient.list_volume_connectors,
+            "1.32",
+            lib_exc.NotFound,
+            {},
+        )
+
+    @decorators.idempotent_id("7673b980-b882-45d7-bc89-8e255e2228cc")
+    def test_show_volume_connector(self):
+        self._microversion_test(
+            BaremetalClient.show_volume_connector,
+            "1.32",
+            lib_exc.NotFound,
+            {"volume_connector_ident": "fake_volume_connector"},
+        )
+
+    @decorators.idempotent_id("3e7d6a89-5302-4087-a10a-200b8d9dc026")
+    def test_create_volume_connector(self):
+        self._microversion_test(
+            BaremetalClient.create_volume_connector,
+            "1.32",
+            lib_exc.NotFound,
+            {
+                "node_uuid": self.node["uuid"],
+                "type": "iqn",
+                "connector_id": "fake_connector_id",
+            },
+        )
+
+    @decorators.idempotent_id("cca4b764-12cc-4d4d-a702-0b52ef1b0e94")
+    def test_update_volume_connector(self):
+        patch = [{"op": "replace", "path": "/connector_id", "value": "new_id"}]
+        self._microversion_test(
+            BaremetalClient.update_volume_connector,
+            "1.32",
+            lib_exc.NotFound,
+            {"uuid": "fake_volume_connector", "patch": patch},
+        )
+
+    @decorators.idempotent_id("e05e85d1-da6b-43b6-adc4-5e4bc8e4c72d")
+    def test_delete_volume_connector(self):
+        self._microversion_test(
+            BaremetalClient.delete_volume_connector,
+            "1.32",
+            lib_exc.NotFound,
+            {"volume_connector_ident": "fake_volume_connector"},
+        )
+
+
+class TestVolumeTargetMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for volume target-related API microversion enforcement."""
+
+    min_microversion = "1.32"
+
+    def setUp(self):
+        super(TestVolumeTargetMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("c7511a8a-4d6b-4439-bfb1-c659808c0967")
+    def test_list_volume_targets(self):
+        self._microversion_test(
+            BaremetalClient.list_volume_targets, "1.32", lib_exc.NotFound, {}
+        )
+
+    @decorators.idempotent_id("ddb8223e-2f63-4a74-aa2f-229a522ae3e8")
+    def test_show_volume_target(self):
+        self._microversion_test(
+            BaremetalClient.show_volume_target,
+            "1.32",
+            lib_exc.NotFound,
+            {"volume_target_ident": "fake_volume_target"},
+        )
+
+    @decorators.idempotent_id("3fb1e9c5-8154-4308-a765-393ab80762c8")
+    def test_create_volume_target(self):
+        self._microversion_test(
+            BaremetalClient.create_volume_target,
+            "1.32",
+            lib_exc.NotFound,
+            {
+                "node_uuid": self.node["uuid"],
+                "volume_type": "iscsi",
+                "volume_id": "fake_volume_id",
+                "boot_index": 0,
+            },
+        )
+
+    @decorators.idempotent_id("16d83cd1-0425-4e28-bfd4-33199e5a87c7")
+    def test_update_volume_target(self):
+        patch = [{"op": "replace", "path": "/volume_id", "value": "new_id"}]
+        self._microversion_test(
+            BaremetalClient.update_volume_target,
+            "1.32",
+            lib_exc.NotFound,
+            {"uuid": "fake_volume_target", "patch": patch},
+        )
+
+    @decorators.idempotent_id("697b67be-ccc5-4a7b-99ca-276e5e8c931a")
+    def test_delete_volume_target(self):
+        self._microversion_test(
+            BaremetalClient.delete_volume_target,
+            "1.32",
+            lib_exc.NotFound,
+            {"volume_target_ident": "fake_volume_target"},
+        )
+
+
+class TestNodeVolumeMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node volume-related API microversion enforcement."""
+
+    min_microversion = "1.32"
+
+    def setUp(self):
+        super(TestNodeVolumeMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("2ff700e6-4914-4eed-8342-308a6f2134a5")
+    def test_list_node_volume(self):
+        self._microversion_test(
+            BaremetalClient._list_request,
+            "1.32",
+            lib_exc.NotFound,
+            {"resource": f"nodes/{self.node['uuid']}/volume"},
+        )
+
+    @decorators.idempotent_id("3e56f64f-6bac-4126-ba7d-d89275318975")
+    def test_list_node_volume_connectors(self):
+        self._microversion_test(
+            BaremetalClient._list_request,
+            "1.32",
+            lib_exc.NotFound,
+            {"resource": f"nodes/{self.node['uuid']}/volume/connectors"},
+        )
+
+    @decorators.idempotent_id("060008fe-d478-49d2-bdf0-d327f1e360a5")
+    def test_list_node_volume_targets(self):
+        self._microversion_test(
+            BaremetalClient._list_request,
+            "1.32",
+            lib_exc.NotFound,
+            {"resource": f"nodes/{self.node['uuid']}/volume/targets"},
+        )
+
+
+class TestDriverMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for driver-related API microversion enforcement."""
+
+    min_microversion = "1.1"
+
+    @decorators.idempotent_id("c5f179c9-3b8a-49c6-bee0-734979d544bc")
+    def test_list_drivers(self):
+        self._microversion_test(
+            BaremetalClient.list_drivers, "1.1", lib_exc.NotAcceptable, {}
+        )
+
+    @decorators.idempotent_id("f79931be-dc7b-4e60-89ce-90a4e2793714")
+    def test_show_driver(self):
+        self._microversion_test(
+            BaremetalClient.show_driver,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"driver_name": "testdriver"},
+        )
+
+    @decorators.idempotent_id("daef2f42-e343-4640-a5d4-d8938e956ddb")
+    def test_show_driver_properties(self):
+        self._microversion_test(
+            BaremetalClient.get_driver_properties,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"driver_name": "testdriver"},
+        )
+
+    @decorators.idempotent_id("bade310a-0699-4470-8da3-6f8ae26e9d71")
+    def test_show_driver_logical_disk_properties(self):
+        self._microversion_test(
+            BaremetalClient.get_driver_logical_disk_properties,
+            "1.12",
+            lib_exc.NotAcceptable,
+            {"driver_name": "testdriver"},
+        )
+
+
+class TestDriverVendorPassthruMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for driver vendor passthru API microversion enforcement."""
+
+    min_microversion = "1.1"
+
+    @decorators.idempotent_id("27c0157d-758a-40c9-b0fd-bde05ed8a6fc")
+    def test_list_vendor_passthru_methods(self):
+        self._microversion_test(
+            BaremetalClient.list_vendor_passthru_methods,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"node_uuid": "fake_node"},
+        )
+
+
+class TestNodeBiosMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node bios-related API microversion enforcement."""
+
+    min_microversion = "1.40"
+
+    def setUp(self):
+        super(TestNodeBiosMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("126de288-71f9-4e4f-8591-6ddfc9ac3095")
+    def test_list_node_bios(self):
+        self._microversion_test(
+            BaremetalClient.list_node_bios_settings,
+            "1.40",
+            lib_exc.NotFound,
+            {"uuid": self.node["uuid"]},
+        )
+
+
+class TestNodeBiosDetailsMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node bios details API microversion enforcement."""
+
+    min_microversion = "1.40"
+
+    def setUp(self):
+        super(TestNodeBiosDetailsMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("68583cd5-63d0-4bda-a836-9bda6d5b2ab4")
+    def test_list_node_bios_details(self):
+        self._microversion_test(
+            BaremetalClient.list_node_bios_settings,
+            "1.40",
+            lib_exc.NotFound,
+            {"uuid": self.node["uuid"]},
+        )
+
+
+class TestNodeFirmwarenMicroversions(
+        BaseTestMicroversionEnforcement,
+        MicroversionTestMixin):
+
+    min_microversion = "1.86"
+
+    def setUp(self):
+        super(TestNodeFirmwarenMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis['uuid'])
+
+    @decorators.idempotent_id('f50e9098-1870-46b1-b05c-660d0f8c534d')
+    def test_list_node_firmware(self):
+        self._microversion_test(
+            BaremetalClient.list_node_firmware, "1.86",
+            lib_exc.NotFound, {"node_uuid": self.node['uuid']}
+        )
+
+
+class TestConductorMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+
+    min_microversion = "1.49"
+
+    def setUp(self):
+        super(TestConductorMicroversions, self).setUp()
+
+    @decorators.idempotent_id("412fde59-afe7-442d-83c9-538c87b95687")
+    def test_list_conductors(self):
+        self._microversion_test(
+            BaremetalClient.list_conductors, "1.49", lib_exc.NotFound, {}
+        )
+
+    @decorators.idempotent_id("ad496ff6-2d6e-4e63-9c64-60c5184a18a7")
+    def test_show_conductor(self):
+        _, conductors = self.client.list_conductors()
+        self.assertTrue(len(conductors['conductors']) > 0)
+        conductor = conductors['conductors'].pop()
+
+        _, conductor = self.client.show_conductor(conductor['hostname'])
+        self._microversion_test(
+            BaremetalClient.show_conductor,
+            "1.49",
+            lib_exc.NotFound,
+            {"hostname": conductor['hostname']},
+        )
+
+
+class TestAllocationMicroversions(
+        BaseTestMicroversionEnforcement,
+        MicroversionTestMixin):
+    """Tests for allocation-related API microversion enforcement."""
+
+    min_microversion = "1.52"
+
+    @decorators.idempotent_id('ef7bd40a-c1ef-4d40-a0b6-f388d23943c3')
+    def test_create_allocations(self):
+        self._microversion_test(
+            BaremetalClient.create_allocation, "1.52",
+            lib_exc.UnexpectedResponseCode, {
+                "resource_class": self.resource_class
+            }
+        )
+
+    @decorators.idempotent_id('511e0c4b-1320-4ac5-9c4a-fb0394d3ff67')
+    def test_list_allocations(self):
+        self._microversion_test(
+            BaremetalClient.list_allocations, "1.52",
+            lib_exc.NotFound, {}
+        )
+
+    @decorators.idempotent_id('a0d17f90-baa0-4518-95f7-a7eab73ff6d1')
+    def test_show_allocations(self):
+        _, allocation = self.create_allocation(self.resource_class)
+
+        self._microversion_test(
+            BaremetalClient.show_allocation, "1.52",
+            lib_exc.NotFound, {
+                "allocation_ident": allocation['uuid']
+            }
+        )
+
+    @decorators.idempotent_id('b05a9b1a-4a12-4b55-93c7-530c3f35c7d9')
+    def test_delete_allocations(self):
+        _, allocation = self.create_allocation(self.resource_class)
+
+        self._microversion_test(
+            BaremetalClient.delete_allocation, "1.52",
+            lib_exc.UnexpectedResponseCode, {
+                "allocation_ident": allocation['uuid']
+            }
+        )
+
+
+class TestNodeAllocationMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node allocation-related API microversion enforcement."""
+
+    min_microversion = "1.52"
+
+    def setUp(self):
+        super(TestNodeAllocationMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("9b170cac-18f7-4a17-be2e-314b548244ba")
+    def test_show_allocation_by_node(self):
+        _, body = self.create_allocation(self.resource_class)
+
+        self._microversion_test(
+            BaremetalClient.show_node_allocation,
+            "1.52",
+            lib_exc.NotFound,
+            {"node_ident": body["node_uuid"]},
+        )
+
+
+class TestDeployTemplateMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for deploy template-related API microversion enforcement."""
+
+    min_microversion = "1.55"
+
+    def setUp(self):
+        super(TestDeployTemplateMicroversions, self).setUp()
+        self.deploy_template_name = "CUSTOM_TEMPLATE_123"
+        self.steps = [
+            {
+                "interface": "bios",
+                "step": "apply_configuration",
+                "args": {
+                    "settings": [
+                        {"name": "LogicalProc", "value": "Enabled"}
+                    ]
+                },
+                "priority": 150,
+            }
+        ]
+
+    @decorators.idempotent_id("52806f1d-d64a-4b7f-a799-e623ecc09bda")
+    def test_create_deploy_template(self):
+        self._microversion_test(
+            BaremetalClient.create_deploy_template,
+            "1.55",
+            lib_exc.UnexpectedResponseCode,
+            {"name": self.deploy_template_name, "steps": self.steps},
+        )
+
+    @decorators.idempotent_id("1a3bb543-faa9-4da6-a1c4-20cb8177f53d")
+    def test_list_deploy_templates(self):
+        self._microversion_test(
+            BaremetalClient.list_deploy_templates, "1.55", lib_exc.NotFound, {}
+        )
+
+    @decorators.idempotent_id("a0933551-8569-4638-acd4-fad0a9daefd9")
+    def test_show_deploy_template(self):
+        _, deploy_template = self.client.create_deploy_template(
+            "CUSTOM_TEMPLATE_456", steps=self.steps
+        )
+        self._microversion_test(
+            BaremetalClient.show_deploy_template,
+            "1.55",
+            lib_exc.NotFound,
+            {"deploy_template_ident": deploy_template["uuid"]},
+        )
+
+    @decorators.idempotent_id("f47a12a7-71b7-4ff0-ab17-9f45c9ac68e7")
+    def test_update_deploy_template(self):
+        patch = [{"op": "replace", "path": "/name", "value": "new_name"}]
+        self._microversion_test(
+            BaremetalClient.update_deploy_template,
+            "1.55",
+            lib_exc.UnexpectedResponseCode,
+            {"deploy_template_ident": "fakedt", "patch": patch},
+        )
+
+    @decorators.idempotent_id("b987a887-22ca-4ec9-a074-0a9bd3eac7ac")
+    def test_delete_deploy_template(self):
+        self._microversion_test(
+            BaremetalClient.delete_deploy_template,
+            "1.55",
+            lib_exc.UnexpectedResponseCode,
+            {"deploy_template_ident": "fakedt"},
+        )
+
+
+class TestRunbookMicroversions(BaseTestMicroversionEnforcement,
+                               MicroversionTestMixin):
+    """Tests for runbook-related API microversion enforcement.
+
+    The runbook API was introduced in microversion 1.92. Operations such as
+    creating, updating, and deleting runbooks are expected to fail with an
+    error if used with an unsupported microversion.
+    """
+
+    min_microversion = "1.92"
+
+    @decorators.idempotent_id("e37be789-fcdf-435a-8a16-3d2f8f21e6e2")
+    def test_create_runbook(self):
+        # Define a minimal valid runbook payload.
+        steps = [
+            {
+                "interface": "bios",
+                "step": "apply_configuration",
+                "order": 1,
+                "args": {
+                    "settings": [
+                        {"name": "LogicalProc", "value": "Enabled"}
+                    ]
+                }
+            }
+        ]
+        self._microversion_test(
+            BaremetalClient.create_runbook,
+            "1.92",
+            lib_exc.UnexpectedResponseCode,
+            {"name": "CUSTOM_TEST", "steps": steps}
+        )
+
+    @decorators.idempotent_id("f1b5b500-6b9c-41f6-bb99-3a77e3bd6ac4")
+    def test_list_runbooks(self):
+        self._microversion_test(
+            BaremetalClient.list_runbooks,
+            "1.92",
+            lib_exc.NotFound,
+            {}
+        )
+
+    @decorators.idempotent_id("3d12345a-3d56-4fbb-afd2-7bd0ebc32e19")
+    def test_show_runbook(self):
+        steps = [
+            {
+                "interface": "bios",
+                "step": "apply_configuration",
+                "order": 1,
+                "args": {
+                    "settings": [
+                        {"name": "LogicalProc", "value": "Enabled"}
+                    ]
+                }
+            }
+        ]
+        _, runbook = self.client.create_runbook("CUSTOM_TEST_2", steps=steps)
+        self._microversion_test(
+            BaremetalClient.show_runbook,
+            "1.92",
+            lib_exc.NotFound,
+            {"runbook_ident": runbook['uuid']}
+        )
+
+    @decorators.idempotent_id("a1e4cd23-56be-4b39-b756-9a9b73f21492")
+    def test_update_runbook(self):
+        # Using an example JSON PATCH document to update the runbook.
+        patch_doc = [
+            {"op": "replace", "path": "/name", "value": "CUSTOM_TEST2"}
+        ]
+        self._microversion_test(
+            BaremetalClient.update_runbook,
+            "1.92",
+            lib_exc.UnexpectedResponseCode,
+            {"runbook_ident": "fake_runbook", "patch": patch_doc}
+        )
+
+    @decorators.idempotent_id("cb9cf9a6-2a3f-4aa7-9488-098f8aeeb5d9")
+    def test_delete_runbook(self):
+        self._microversion_test(
+            BaremetalClient.delete_runbook,
+            "1.92",
+            lib_exc.UnexpectedResponseCode,
+            {"runbook_ident": "fake_runbook"}
+        )
+
+
+class TestNodeHistoryMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node history-related API microversion enforcement."""
+
+    min_microversion = "1.78"
+
+    def setUp(self):
+        super(TestNodeHistoryMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("aea6cb45-6519-44d9-a33e-aab7cd72d769")
+    def test_list_node_history(self):
+        self._microversion_test(
+            BaremetalClient.list_node_history,
+            "1.78",
+            lib_exc.NotFound,
+            {"node_uuid": self.node["uuid"]},
+        )
+
+
+# NOTE(adamcarthur) - The positive test for this will need a configured
+# node to have history.
+class TestNodeInventoryMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for node inventory-related API microversion enforcement."""
+
+    min_microversion = "1.81"
+
+    def setUp(self):
+        super(TestNodeInventoryMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+
+    @decorators.idempotent_id("ca557dca-9b3e-44cb-a101-f31d58a0dedd")
+    def test_show_node_inventory(self):
+        _, node = self.client.show_node(self.node['uuid'])
+        self._microversion_test(
+            BaremetalClient.show_inventory,
+            "1.81",
+            lib_exc.NotFound,
+            {"uuid": node["uuid"]},
+            ignore_positive=True
+        )
+
+
+class TestShardMicroversions(
+        BaseTestMicroversionEnforcement,
+        MicroversionTestMixin):
+    """Tests for shard-related API microversion enforcement."""
+
+    min_microversion = "1.82"
+
+    @decorators.idempotent_id("e5403a31-e12b-4f97-a776-dcb819e5e9a0")
+    def test_shard(self):
+        self._microversion_test(
+            BaremetalClient.get_shards, "1.82", lib_exc.NotFound, {}
+        )
+
+    @decorators.idempotent_id("5df533c6-7a9c-4639-a47f-1377a2a87e6a")
+    def test_list_node_filter_shard(self):
+        self._microversion_test(
+            BaremetalClient.list_nodes, "1.82",
+            lib_exc.NotAcceptable, {"shard": "testshard"}
+        )
+
+
+class TestChassisMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for chassis-related API microversion enforcement."""
+
+    min_microversion = "1.1"
+
+    def setUp(self):
+        super(TestChassisMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+
+    @decorators.idempotent_id("59c20dbf-280e-4148-ac8e-be3b8d75d021")
+    def test_create_chassis(self):
+        self._microversion_test(
+            BaremetalClient.create_chassis, "1.1", lib_exc.NotAcceptable, {}
+        )
+
+    @decorators.idempotent_id("0e840e17-5659-4ad9-a218-84b14bc8b013")
+    def test_list_chassis(self):
+        self._microversion_test(
+            BaremetalClient.list_chassis, "1.1", lib_exc.NotAcceptable, {}
+        )
+
+    @decorators.idempotent_id("6c3a5728-0d96-41aa-a3ca-295bfbbe341e")
+    def test_show_chassis(self):
+        self._microversion_test(
+            BaremetalClient.show_chassis,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"uuid": self.chassis["uuid"]},
+        )
+
+    @decorators.idempotent_id("7ae8ca94-b7d9-4975-87c6-37d89779b8b5")
+    def test_update_chassis(self):
+        patch = [
+            {"op": "replace", "path": "/description", "value": "new_desc"}
+        ]
+        self._microversion_test(
+            BaremetalClient.update_chassis,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"uuid": self.chassis["uuid"], "patch": patch},
+        )
+
+    @decorators.idempotent_id("f6d70a3a-9212-46af-bb7b-c95a3043a722")
+    def test_delete_chassis(self):
+        self._microversion_test(
+            BaremetalClient.delete_chassis,
+            "1.1",
+            lib_exc.NotAcceptable,
+            {"uuid": self.chassis["uuid"]},
+        )
+
+
+class TestUtilityMicroversions(
+    BaseTestMicroversionEnforcement, MicroversionTestMixin
+):
+    """Tests for utility-related API microversion enforcement."""
+
+    min_microversion = "1.22"
+
+    def setUp(self):
+        super(TestUtilityMicroversions, self).setUp()
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis["uuid"])
+        self.callback_url = "http://example.com/callback"
+        self.agent_version = "1.0"
+        self.agent_token = "test_token"
+
+    @decorators.idempotent_id("dd2b17d9-c847-49bd-acfd-b3d63ae59f2f")
+    def test_ipa_heartbeat(self):
+        self._microversion_test(
+            BaremetalClient.ipa_heartbeat,
+            "1.22",
+            lib_exc.NotFound,
+            {
+                "node_uuid": self.node["uuid"],
+                "callback_url": self.callback_url,
+                "agent_version": self.agent_version,
+                "agent_token": self.agent_token,
+            },
+        )
diff --git a/ironic_tempest_plugin/tests/api/admin/test_nodes.py b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
index 25fa0a3..0d82577 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_nodes.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
@@ -384,7 +384,7 @@
     def test_create_node_resource_class_old_api(self):
         """Try to create a node with resource class using older api version."""
         resource_class = data_utils.arbitrary_string()
-        self.assertRaises(lib_exc.UnexpectedResponseCode, self.create_node,
+        self.assertRaises(lib_exc.NotAcceptable, self.create_node,
                           self.chassis['uuid'], resource_class=resource_class)
 
     @decorators.attr(type='negative')
@@ -392,7 +392,7 @@
     def test_update_node_resource_class_old_api(self):
         """Try to update a node with resource class using older api version."""
         resource_class = data_utils.arbitrary_string()
-        self.assertRaises(lib_exc.UnexpectedResponseCode,
+        self.assertRaises(lib_exc.NotAcceptable,
                           self.client.update_node,
                           self.node['uuid'], resource_class=resource_class)
 
@@ -402,10 +402,10 @@
         """Try to list nodes with resource class using older api version."""
         resource_class = data_utils.arbitrary_string()
         self.assertRaises(
-            lib_exc.UnexpectedResponseCode,
+            lib_exc.NotAcceptable,
             self.client.list_nodes, resource_class=resource_class)
         self.assertRaises(
-            lib_exc.UnexpectedResponseCode,
+            lib_exc.NotAcceptable,
             self.client.list_nodes_detail, resource_class=resource_class)
 
 
@@ -1055,10 +1055,9 @@
     @decorators.idempotent_id('5419af7b-4e27-4be4-88f6-e01c598a8102')
     def test_list_node_traits_old_api(self):
         """Try to list traits for a node using an older api version."""
-        exc = self.assertRaises(
-            lib_exc.UnexpectedResponseCode,
+        self.assertRaises(
+            lib_exc.NotAcceptable,
             self.client.list_node_traits, self.node['uuid'])
-        self.assertEqual(406, exc.resp.status)
 
     @decorators.attr(type='negative')
     @decorators.idempotent_id('a4353f3a-bedc-4579-9c7e-4bebcd95903d')
@@ -1108,10 +1107,9 @@
     @decorators.idempotent_id('eb75b3c8-ac9c-4399-90a2-c0030bfde7a6')
     def test_list_nodes_traits_field(self):
         """Try to list nodes' traits field using older api version."""
-        exc = self.assertRaises(
-            lib_exc.UnexpectedResponseCode,
+        self.assertRaises(
+            lib_exc.NotAcceptable,
             self.client.list_nodes, fields='traits')
-        self.assertEqual(406, exc.resp.status)
 
     @decorators.attr(type='negative')
     @decorators.idempotent_id('214ae7fc-149b-4657-b6bc-66353d49ade8')
diff --git a/ironic_tempest_plugin/tests/api/admin/test_portgroups.py b/ironic_tempest_plugin/tests/api/admin/test_portgroups.py
index 332c791..80724fe 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_portgroups.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_portgroups.py
@@ -21,7 +21,7 @@
 class TestPortGroups(base.BaseBaremetalTest):
     """Basic positive test cases for port groups."""
 
-    min_microversion = '1.23'
+    min_microversion = '1.26'  # portgroup mode and properties introduced
 
     def setUp(self):
         super(TestPortGroups, self).setUp()
diff --git a/ironic_tempest_plugin/tests/api/admin/test_ports.py b/ironic_tempest_plugin/tests/api/admin/test_ports.py
index dfb371c..9bf2771 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_ports.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_ports.py
@@ -25,9 +25,23 @@
         super(TestPorts, self).setUp()
 
         _, self.chassis = self.create_chassis()
+        # NOTE(TheJulia): noop is required for the network interface for
+        # these tests, as newer versions of ironic can invoke network
+        # logic, and if is not configured with full integration configuration,
+        # can then result in failed tests.
         _, self.node = self.create_node(self.chassis['uuid'])
         _, self.port = self.create_port(self.node['uuid'],
                                         data_utils.rand_mac_address())
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture('1.31'))
+        # Now with a 1.31 microversion, swap the network interfaces
+        # into place so the test doesn't break depending on
+        # the environment's default state.
+        self.client.update_node(self.node['uuid'],
+                                [{'path': '/network_interface',
+                                  'op': 'replace',
+                                  'value': 'noop'}])
+        self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.1'))
 
     @decorators.idempotent_id('83975898-2e50-42ed-b5f0-e510e36a0b56')
     def test_create_port(self):
diff --git a/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py b/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
index bd338f9..fb1913c 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
@@ -26,6 +26,16 @@
 
         _, self.chassis = self.create_chassis()
         _, self.node = self.create_node(self.chassis['uuid'])
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture('1.31'))
+        # Now with a 1.31 microversion, swap the network interfaces
+        # into place so the test doesn't break depending on
+        # the environment's default state.
+        self.client.update_node(self.node['uuid'],
+                                [{'path': '/network_interface',
+                                  'op': 'replace',
+                                  'value': 'noop'}])
+        self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.1'))
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('0a6ee1f7-d0d9-4069-8778-37f3aa07303a')
@@ -354,7 +364,7 @@
         node_id = self.node['uuid']
         address = data_utils.rand_mac_address()
 
-        self.assertRaises((lib_exc.BadRequest, lib_exc.UnexpectedResponseCode),
+        self.assertRaises((lib_exc.BadRequest, lib_exc.NotAcceptable),
                           self.create_port,
                           node_id=node_id, address=address,
                           physical_network='physnet1')
@@ -371,7 +381,7 @@
                   'op': 'replace',
                   'value': new_physnet}]
 
-        self.assertRaises((lib_exc.BadRequest, lib_exc.UnexpectedResponseCode),
+        self.assertRaises((lib_exc.BadRequest, lib_exc.NotAcceptable),
                           self.client.update_port,
                           port['uuid'], patch)
 
diff --git a/ironic_tempest_plugin/tests/api/admin/test_shards.py b/ironic_tempest_plugin/tests/api/admin/test_shards.py
index ffe6914..5a5e4ba 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_shards.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_shards.py
@@ -15,6 +15,7 @@
 
 from ironic_tempest_plugin.tests.api import base
 
+
 CONF = config.CONF
 
 
@@ -91,7 +92,7 @@
         _, fetched_nodes = self.client.list_nodes(shard=shard)
         fetched_node_ids = [node['uuid'] for node in fetched_nodes['nodes']]
 
-        self.assertItemsEqual(good_node_ids, fetched_node_ids)
+        self.assertCountEqual(good_node_ids, fetched_node_ids)
 
     @decorators.idempotent_id('6f1e241d-4386-4730-b9ff-28c6a3dcad31')
     def test_only_show_multiple_requested_shards(self):
@@ -107,7 +108,7 @@
             shard=','.join([shard, second_shard]))
         fetched_node_ids = [node['uuid'] for node in fetched_nodes['nodes']]
 
-        self.assertItemsEqual(good_node_ids, fetched_node_ids)
+        self.assertCountEqual(good_node_ids, fetched_node_ids)
         self.assertNotIn(self.bad_node_id, fetched_node_ids)
         self.assertNotIn(self.none_node_id, fetched_node_ids)
 
@@ -126,3 +127,25 @@
 
         self.assertIn(self.none_node_id, fetched_node_ids)
         self.assertNotIn(self.bad_node_id, fetched_node_ids)
+
+
+class TestGetAllShards(base.BaseBaremetalTest):
+    """Tests for baremetal shards."""
+
+    min_microversion = '1.82'
+
+    def setUp(self):
+        super(TestGetAllShards, self).setUp()
+        _, self.chassis = self.create_chassis()
+        self.shards = ["shard1", "shard2", "shard3"]
+        self.node_ids = []
+        for shard in self.shards:
+            _, node = self.create_node(self.chassis['uuid'], shard=shard)
+            self.node_ids.append(node['uuid'])
+
+    @decorators.idempotent_id('fc786196-63c7-4e0d-bd14-3e478d4d1e3e')
+    def test_get_all_shards(self):
+        _, fetched_shards = self.client.get_shards()
+        fetched_shards = [shard['name'] for shard in fetched_shards['shards']]
+
+        self.assertCountEqual(self.shards, fetched_shards)
diff --git a/ironic_tempest_plugin/tests/api/base.py b/ironic_tempest_plugin/tests/api/base.py
index 11ce859..7744aee 100644
--- a/ironic_tempest_plugin/tests/api/base.py
+++ b/ironic_tempest_plugin/tests/api/base.py
@@ -38,7 +38,8 @@
 # NOTE(jroll): resources must be deleted in a specific order, this list
 # defines the resource types to clean up, and the correct order.
 RESOURCE_TYPES = ['port', 'portgroup', 'node', 'volume_connector',
-                  'volume_target', 'chassis', 'deploy_template', 'runbook']
+                  'volume_target', 'chassis', 'deploy_template',
+                  'runbook', 'inspection_rule']
 
 
 def creates(resource):
@@ -520,11 +521,37 @@
         resp, body = cls.client.create_allocation(resource_class, **kwargs)
         return resp, body
 
+    @classmethod
+    @creates('inspection_rule')
+    def create_inspection_rule(cls, rule_uuid, payload):
+        """Wrapper utility for creating Inspection rule.
+
+        :param rule_uuid: UUID of the Inspection rule.
+        :param payload: Inspection rule other fields.
+        :return: Server response.
+        """
+        if rule_uuid is not None:
+            payload['uuid'] = rule_uuid
+
+        resp, body = cls.client.create_inspection_rule(payload)
+
+        return resp, body
+
+    @classmethod
+    def delete_inspection_rule(cls, rule_uuid):
+        """Delete a inspection rules having the specified UUID.
+
+        :param rule_uuid: UUID of the Inspection rule.
+        """
+        resp, body = cls.client.delete_inspection_rule(rule_uuid)
+
+        return resp
+
 
 class BaseBaremetalRBACTest(BaseBaremetalTest):
 
-    # Unless otherwise superceeded by a version, RBAC tests generally start at
-    # version 1.70 as that is when System scope and the delineation occured.
+    # Unless otherwise superseded by a version, RBAC tests generally start at
+    # version 1.70 as that is when System scope and the delineation occurred.
     min_microversion = '1.70'
 
     @classmethod
diff --git a/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py b/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py
index cd16fe2..2af624a 100644
--- a/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py
+++ b/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py
@@ -675,7 +675,7 @@
     All tests here must always expect *multiple* nodes visible, since
     this is a global reader role.
 
-    https://opendev.org/openstack/ironic/src/branch/master/ironic/common/policy.py#L60  # noqa
+    https://opendev.org/openstack/ironic/src/branch/master/ironic/common/policy.py#L60
     """
 
     credentials = ['system_admin', 'system_reader']
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
index e9090cb..7e93fe0 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
@@ -16,10 +16,12 @@
 
 import time
 
+from oslo_log import log as logging
 from tempest.common import waiters
 from tempest import config
 from tempest.lib.common import api_version_utils
 from tempest.lib.common.utils.linux import remote_client
+from tempest.lib.common.utils import test_utils
 from tempest.lib import exceptions as lib_exc
 
 from ironic_tempest_plugin.common import utils
@@ -27,6 +29,7 @@
 from ironic_tempest_plugin import manager
 
 CONF = config.CONF
+LOG = logging.getLogger(__name__)
 
 
 def retry_on_conflict(func):
@@ -136,14 +139,16 @@
                                                       instance_id)
 
     @classmethod
-    def wait_for_agent_heartbeat(cls, node_id, timeout=None):
+    def wait_for_agent_heartbeat(cls, node_id, timeout=None,
+                                 abort_on_error_state=True):
         ironic_waiters.wait_node_value_in_field(
             cls.baremetal_client,
             node_id=node_id,
             field='driver_internal_info',
             value='agent_last_heartbeat',
             timeout=timeout or CONF.baremetal.deploywait_timeout,
-            interval=10)
+            interval=10,
+            abort_on_error_state=abort_on_error_state)
 
     @classmethod
     def get_node(cls, node_id=None, instance_id=None, api_version=None):
@@ -307,3 +312,44 @@
                                        instance['id'], 'ACTIVE')
         # Verify server connection
         self.get_remote_client(server_ip, server=instance)
+
+    def wait_for_ssh(self, ip_address,
+                     username=None,
+                     private_key=None,
+                     server=None,
+                     timeout=60,
+                     delay=10):
+        def _wait_ssh():
+            try:
+                self.get_remote_client(ip_address, username, private_key,
+                                       server=server)
+            except Exception:
+                LOG.debug("Failed to get ssh client for %s", ip_address,
+                          exc_info=True)
+                return False
+            return True
+
+        res = test_utils.call_until_true(_wait_ssh, timeout, delay)
+        self.assertTrue(res, f"Failed to wait for ssh on {ip_address}")
+
+    def check_vm_connectivity(self,
+                              ip_address,
+                              username=None,
+                              private_key=None,
+                              should_connect=True,
+                              extra_msg="",
+                              server=None,
+                              mtu=None):
+        # NOTE(vsaienko): it may take some time to boot VM and initialize
+        # ssh by cloud init. Wait for SSH can pass authentication before
+        # checking connectivity.
+        if should_connect:
+            self.wait_for_ssh(ip_address=ip_address, username=username,
+                              private_key=private_key, server=server)
+        super().check_vm_connectivity(ip_address=ip_address,
+                                      username=username,
+                                      private_key=private_key,
+                                      should_connect=should_connect,
+                                      extra_msg=extra_msg,
+                                      server=server,
+                                      mtu=mtu)
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
index ddb550b..7fa8cb5 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
@@ -343,7 +343,7 @@
         :param image_ref: Reference to user image to boot node with.
         :param image_checksum: md5sum of image specified in image_ref.
                                Needed only when direct HTTP link is provided.
-        :param boot_option: The defaut boot option to utilize. If not
+        :param boot_option: The default boot option to utilize. If not
                             specified, the ironic deployment default shall
                             be utilized.
         :param config_drive_networking: If we should load configuration drive
@@ -583,7 +583,7 @@
     # If we don't require an explicit driver, then what drivers *can* we
     # operate with. In essence, this exists to prevent the test from failing
     # on 3rd party drivers, and vendor specific driers which do not support
-    # the sort of itnerfaces we may be trying to test by default.
+    # the sort of interfaces we may be trying to test by default.
     valid_driver_list = []
 
     # The bios interface to use by the HW type. The bios interface of the
@@ -719,7 +719,7 @@
         if (cls.use_available_driver
                 and not cls.driver
                 and cls.node['driver'] in cls.valid_driver_list):
-            # If we're attempting to re-use the existing driver, then
+            # If we're attempting to reuse the existing driver, then
             # lets save a value for update_node_driver to work with.
             cls.driver = cls.node['driver']
         cls.update_node_driver(cls.node['uuid'], cls.driver, **boot_kwargs)
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_advanced_ops.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_advanced_ops.py
index 0a7116a..b66309b 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_advanced_ops.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_advanced_ops.py
@@ -27,6 +27,7 @@
     driver = 'redfish'
     deploy_interface = 'direct'
     boot_interface = 'redfish-virtual-media'
+    # To force interface retoggling.
     image_ref = CONF.baremetal.whole_disk_image_ref
     image_checksum = CONF.baremetal.whole_disk_image_checksum
     wholedisk_image = True
@@ -95,7 +96,23 @@
 
         # Get the latest state for the node.
         self.node = self.get_node(self.node['uuid'])
+        # This test, as far as I'm remembering after the fact, was developed
+        # in an environment where neutron was the default network interface.
+        # so we must try to set it to properly ensure dhcp-less operation.
         prior_prov_net = self.node['driver_info'].get('provisioning_network')
+        try:
+            self.client.update_node(self.node['uuid'],
+                                    [{'path': '/network_interface',
+                                      'op': 'replace',
+                                      'value': 'neutron'}])
+            self.addCleanup(self.update_node,
+                            self.node['uuid'],
+                            [{'op': 'replace',
+                              'path': '/network_interface',
+                              'value': 'flat'}])
+        except Exception:
+            raise self.skipException(
+                "Ironic configuration incorrect to exercise this test.")
 
         ip_version = CONF.validation.ip_version_for_ssh
         tenant_cidr = '10.0.6.0/24'
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
index 5901d20..09da6d3 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
@@ -135,6 +135,7 @@
     image_ref = CONF.baremetal.whole_disk_image_ref
     wholedisk_image = True
 
+    @decorators.unstable_test(bug='2101021')
     @decorators.idempotent_id('cde532cc-81ba-4489-b374-b4a85cc203eb')
     @utils.services('image', 'network')
     def test_ip_access_to_server(self):
@@ -638,7 +639,7 @@
     use_available_driver = True
 
     # List of valid drivers which these tests *can* attempt to utilize.
-    # Generally these should be the most commom, stock, upstream drivers.
+    # Generally these should be the most common, stock, upstream drivers.
     valid_driver_list = ['ipmi', 'redfish']
 
     # Bypass secondary attribute presence check as these tests don't require
diff --git a/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py b/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
index 2a36c6a..e3a9a58 100644
--- a/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
+++ b/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
@@ -50,6 +50,7 @@
     TEST_RESCUE_MODE = False
     image_ref = None
     wholedisk_image = None
+    auto_lease = False
 
     @classmethod
     def skip_checks(cls):
@@ -177,6 +178,9 @@
             # CUSTOM_ prefix. Normalise it.
             node_resource_class = node['resource_class']
             node_resource_class = node_resource_class.upper()
+            node_resource_class = node_resource_class.translate(
+                str.maketrans(" -.:", "____", "!@#$%^&*()+=/\\?<>|\"'")
+            )
             node_resource_class = 'CUSTOM_' + node_resource_class
             self.assertEqual(resource_class, node_resource_class)
 
@@ -210,13 +214,25 @@
         output = client.exec_command(cmd).rstrip()
         self.assertEqual(success_string, output)
 
+    def validate_lessee(self):
+        iinfo = self.node.get('instance_info')
+        dii = self.node.get('driver_internal_info', {})
+        if 'automatic_lessee' in dii and iinfo:
+            # NOTE(JayF): This item not being in instance_info tells us we
+            # set the lessee.
+            self.assertEqual(iinfo['project_id'], self.node['lessee'])
+
     def baremetal_server_ops(self):
         self.add_keypair()
         self.instance, self.node = self.boot_instance(image_id=self.image_ref)
         self.validate_image()
         self.validate_ports()
         self.validate_scheduling()
+        self.validate_lessee()
         ip_address = self.get_server_ip(self.instance)
+        self.check_vm_connectivity(ip_address=ip_address,
+                                   private_key=self.keypair['private_key'],
+                                   server=self.instance)
         vm_client = self.get_remote_client(ip_address, server=self.instance)
 
         # We expect the ephemeral partition to be mounted on /mnt and to have
@@ -237,20 +253,6 @@
             self.rescue_instance(self.instance, self.node, ip_address)
             self.unrescue_instance(self.instance, self.node, ip_address)
 
-        # Reboot node
-        self.reboot_node(self.instance)
-
-        # Ensure we have some sort of connectivity
-        # Attempt to ping, if all else fails fall back to an ssh connection
-        # which worked previously.
-        pinging = self.ping_ip_address(ip_address)
-        if not pinging:
-            self.get_remote_client(ip_address, server=self.instance)
-        else:
-            # If we're here, this is successful. If ssh fails above,
-            # the job will ultimately fail.
-            self.assertTrue(pinging)
-
         self.terminate_instance(self.instance)
 
     @decorators.idempotent_id('549173a5-38ec-42bb-b0e2-c8b9f4a08943')
@@ -272,6 +274,7 @@
     def test_baremetal_server_ops_wholedisk_image(self):
         self.image_ref = CONF.baremetal.whole_disk_image_ref
         self.wholedisk_image = True
+        self.auto_lease = True
         self.baremetal_server_ops()
 
 
diff --git a/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py b/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py
index 086bf21..5bb7df5 100644
--- a/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py
+++ b/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py
@@ -13,14 +13,17 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+from oslo_log import log as logging
 from tempest.common import utils
 from tempest import config
 from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
 
 from ironic_tempest_plugin import manager
 from ironic_tempest_plugin.tests.scenario import baremetal_manager
 
+LOG = logging.getLogger(__name__)
 CONF = config.CONF
 
 
@@ -44,19 +47,17 @@
         if not CONF.baremetal.use_provision_network:
             msg = 'Ironic/Neutron tenant isolation is not configured.'
             raise cls.skipException(msg)
-        if (CONF.baremetal.available_nodes is not None
-                and CONF.baremetal.available_nodes < 2):
-            msg = ('Not enough baremetal nodes, %d configured, test requires '
-                   'a minimum of 2') % CONF.baremetal.available_nodes
-            raise cls.skipException(msg)
 
-    def create_tenant_network(self, clients, tenant_cidr):
+    def create_tenant_network(self, clients, tenant_cidr, create_router=True):
         network = self.create_network(
             networks_client=clients.networks_client,
             project_id=clients.credentials.project_id)
-        router = self.get_router(
-            client=clients.routers_client,
-            project_id=clients.credentials.tenant_id)
+
+        router = None
+        if create_router:
+            router = self.get_router(
+                client=clients.routers_client,
+                project_id=clients.credentials.tenant_id)
 
         result = clients.subnets_client.create_subnet(
             name=data_utils.rand_name('subnet'),
@@ -65,25 +66,39 @@
             ip_version=4,
             cidr=tenant_cidr)
         subnet = result['subnet']
-        clients.routers_client.add_router_interface(router['id'],
-                                                    subnet_id=subnet['id'])
+        if create_router:
+            clients.routers_client.add_router_interface(router['id'],
+                                                        subnet_id=subnet['id'])
         self.addCleanup(clients.subnets_client.delete_subnet, subnet['id'])
-        self.addCleanup(clients.routers_client.remove_router_interface,
-                        router['id'], subnet_id=subnet['id'])
+
+        if create_router:
+            self.addCleanup(clients.routers_client.remove_router_interface,
+                            router['id'], subnet_id=subnet['id'])
         return network, subnet, router
 
     def verify_l3_connectivity(self, source_ip, private_key,
-                               destination_ip, conn_expected=True):
+                               destination_ip, conn_expected=True, timeout=15):
         remote = self.get_remote_client(source_ip, private_key=private_key)
         remote.validate_authentication()
 
+        output = remote.exec_command('ip route')
+        LOG.debug("Routing table on %s is %s", source_ip, output)
+
         cmd = 'ping %s -c4 -w4 || exit 0' % destination_ip
         success_substring = " bytes from %s" % destination_ip
-        output = remote.exec_command(cmd)
-        if conn_expected:
-            self.assertIn(success_substring, output)
-        else:
-            self.assertNotIn(success_substring, output)
+
+        def ping_remote():
+            output = remote.exec_command(cmd)
+            LOG.debug("Got output %s while pinging %s", output, destination_ip)
+            if conn_expected:
+                return success_substring in output
+            else:
+                return success_substring not in output
+
+        # NOTE(vsaienko): we may lost couple of pings due to missing ARPs
+        # so do several retries to get stable output.
+        res = test_utils.call_until_true(ping_remote, timeout, 1)
+        self.assertTrue(res)
 
     def multitenancy_check(self, use_vm=False):
         tenant_cidr = '10.0.100.0/24'
@@ -98,6 +113,7 @@
             clients=self.os_primary,
             keypair=keypair,
             net_id=network['id'],
+            fixed_ip='10.0.100.101',
         )
         fixed_ip1 = instance1['addresses'][network['name']][0]['addr']
         floating_ip1 = self.create_floating_ip(
@@ -112,7 +128,8 @@
                 clients=self.os_alt,
                 key_name=alt_keypair['name'],
                 flavor=CONF.compute.flavor_ref_alt,
-                networks=[{'uuid': alt_network['id']}]
+                networks=[{'uuid': alt_network['id'],
+                           'fixed_ip': '10.0.100.102'}],
             )
         else:
             # Create BM
@@ -120,6 +137,7 @@
                 keypair=alt_keypair,
                 clients=self.os_alt,
                 net_id=alt_network['id'],
+                fixed_ip='10.0.100.102',
             )
         fixed_ip2 = alt_instance['addresses'][alt_network['name']][0]['addr']
         alt_floating_ip = self.create_floating_ip(
@@ -165,3 +183,137 @@
             self.skipTest('Compute service Nova is disabled,'
                           ' VM is required to run this test')
         self.multitenancy_check(use_vm=True)
+
+    @decorators.idempotent_id('6891929f-a254-43b1-bd97-6ea3ec74d6a9')
+    @utils.services('compute', 'image', 'network')
+    def test_baremetal_vm_multitenancy_trunk(self):
+        """Check Trunk scenario for two baremetal servers
+
+
+        fipA -- RouterA -- NetworkA (10.0.100.0/24)
+                              |
+                              |eth0
+                             eth0:instanceA
+                              |eth0.vlan_id
+                              |
+                           NetworkB(10.0.101.0/24)
+                              |
+                              |eth0
+                           instanceB
+
+
+        * Create instanceA within networkA and FIPA with trunk port plugged to
+          networkA as parent(native vlan/untagged) and networkB as
+          vlan subport
+        * Create instanceB within networkB
+        * Verify connectivity to instanceB from instanceA failed
+        * Assign ip address on subport inside instanceA. This step is needed
+          only unless nova configdrive support of trunks is not implemented.
+        * Verify connectivity to instanceB from instanceA success
+        * Remove subport from instanceA
+        * Verify connectivity to instanceB from instanceA failed
+        * Add subport to instanceA
+        * Verify connectivity to instanceB from instanceA Passed
+        """
+
+        if not CONF.baremetal_feature_enabled.trunks_supported:
+            msg = 'Trunks with baremetal are not supported.'
+            raise self.skipException(msg)
+
+        tenant_a_cidr = '10.0.100.0/24'
+        tenant_b_cidr = '10.0.101.0/24'
+
+        keypair = self.create_keypair()
+        networkA, subnetA, routerA = self.create_tenant_network(
+            self.os_primary, tenant_a_cidr)
+        networkB, subnetB, _ = self.create_tenant_network(
+            self.os_primary, tenant_b_cidr, create_router=False)
+        portB = self.create_port(network_id=networkB["id"])
+
+        parent_port = self.create_port(network_id=networkA["id"])
+        subport = self.create_port(network_id=networkB["id"])
+        subports = [{'port_id': subport['id'], 'segmentation_type': 'inherit'}]
+        trunk = self.os_primary.trunks_client.create_trunk(
+            name="test-trunk", port_id=parent_port['id'],
+            sub_ports=subports)['trunk']
+        self.addCleanup(self.os_primary.trunks_client.delete_trunk,
+                        trunk['id'])
+
+        # Create instanceB first as we will not check if its booted,
+        # as we don't have FIP, so it has more time than instanceA
+        # to boot.
+        instanceB, nodeB = self.boot_instance(
+            clients=self.os_primary,
+            keypair=keypair,
+            networks=[{'port': portB['id']}]
+        )
+
+        instanceA, nodeA = self.boot_instance(
+            clients=self.os_primary,
+            keypair=keypair,
+            networks=[{'port': parent_port['id']}]
+        )
+
+        floating_ipA = self.create_floating_ip(
+            instanceA,
+        )['floating_ip_address']
+
+        fixed_ipB = instanceB['addresses'][networkB['name']][0]['addr']
+
+        self.check_vm_connectivity(ip_address=floating_ipA,
+                                   private_key=keypair['private_key'],
+                                   server=instanceA)
+        ssh_client = self.get_remote_client(floating_ipA,
+                                            private_key=keypair['private_key'])
+
+        # TODO(vsaienko): add when cloudinit support is implemented
+        # add validation of network_data.json and drop next ip assignment
+
+        self.verify_l3_connectivity(
+            floating_ipA,
+            keypair['private_key'],
+            fixed_ipB,
+            conn_expected=False
+        )
+        vlan_id = trunk['sub_ports'][0]['segmentation_id']
+        subport_ip = subport['fixed_ips'][0]['ip_address']
+
+        interface_name = ssh_client.exec_command(
+            "sudo ip route | awk '/default/ {print $5}'").rstrip()
+        cmds = [
+            f"sudo ip link add link {interface_name} name "
+            f"{interface_name}.{vlan_id} type vlan id {vlan_id}",
+            f"sudo ip addr add {subport_ip}/24 dev {interface_name}.{vlan_id}",
+            f"sudo ip link set dev {interface_name}.{vlan_id} up"]
+
+        for cmd in cmds:
+            ssh_client.exec_command(cmd)
+
+        self.verify_l3_connectivity(
+            floating_ipA,
+            keypair['private_key'],
+            fixed_ipB,
+            conn_expected=True
+        )
+
+        self.os_primary.trunks_client.delete_subports_from_trunk(
+            trunk['id'], trunk['sub_ports'])
+        self.verify_l3_connectivity(
+            floating_ipA,
+            keypair['private_key'],
+            fixed_ipB,
+            conn_expected=False
+        )
+        self.os_primary.trunks_client.add_subports_to_trunk(
+            trunk['id'], trunk['sub_ports'])
+
+        # NOTE(vsaienko): it may take some time for network driver to
+        # setup vlans as this is async operation.
+        self.verify_l3_connectivity(
+            floating_ipA,
+            keypair['private_key'],
+            fixed_ipB,
+            conn_expected=True
+        )
+        self.terminate_instance(instance=instanceA)
+        self.terminate_instance(instance=instanceB)
diff --git a/ironic_tempest_plugin/tests/scenario/test_baremetal_single_tenant.py b/ironic_tempest_plugin/tests/scenario/test_baremetal_single_tenant.py
index 2138d3e..1be419b 100644
--- a/ironic_tempest_plugin/tests/scenario/test_baremetal_single_tenant.py
+++ b/ironic_tempest_plugin/tests/scenario/test_baremetal_single_tenant.py
@@ -44,11 +44,6 @@
         if not CONF.baremetal.use_provision_network:
             msg = 'Ironic/Neutron tenant isolation is not configured.'
             raise cls.skipException(msg)
-        if (CONF.baremetal.available_nodes is not None
-                and CONF.baremetal.available_nodes < 2):
-            msg = ('Not enough baremetal nodes, %d configured, test requires '
-                   'a minimum of 2') % CONF.baremetal.available_nodes
-            raise cls.skipException(msg)
 
     def create_tenant_network(self, clients, tenant_cidr):
         network = self.create_network(
diff --git a/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py b/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
index f30b5e0..44b8f98 100644
--- a/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
+++ b/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
@@ -61,9 +61,6 @@
             * Verifies all properties are inspected
             * Verifies introspection data
             * Sets node to available state
-            * Creates a keypair
-            * Boots an instance using the keypair
-            * Deletes the instance
 
         """
         # prepare introspection rule
@@ -103,11 +100,6 @@
                 timeout=CONF.baremetal.active_timeout,
                 interval=self.wait_provisioning_state_interval)
 
-        self.wait_for_nova_aware_of_bvms()
-        self.add_keypair()
-        ins, _node = self.boot_instance()
-        self.terminate_instance(ins)
-
     @decorators.idempotent_id('70ca3070-184b-4b7d-8892-e977d2bc2870')
     def test_introspection_abort(self):
         """This smoke test case follows this very basic set of operations:
diff --git a/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py b/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
index 6ebfcc6..9950453 100644
--- a/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
+++ b/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
@@ -136,13 +136,13 @@
            * Generate discovery rule;
            * Start introspection via ironic-inspector API;
            * Delete the node from ironic;
-           * Wating for node discovery;
+           * Waiting for node discovery;
            * Verify introspected node.
         """
         # NOTE(aarefiev): workaround for infra, 'tempest' user doesn't
         # have virsh privileges, so lets power on the node via ironic
         # and then delete it. Because of node is blacklisted in inspector
-        # we can't just power on it, therefor start introspection is used
+        # we can't just power on it, therefore start introspection is used
         # to whitelist discovered node first.
         self.baremetal_client.set_node_provision_state(
             self.node_info['uuid'], 'manage')
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..4fa5ffd
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,18 @@
+[build-system]
+requires = ["pbr>=6.0.0", "setuptools>=64.0.0"]
+build-backend = "pbr.build"
+
+[tool.doc8]
+ignore = ["D001"]
+
+[tool.ruff]
+line-length = 79
+target-version = "py37"
+
+[tool.ruff.lint]
+select = [
+    "E",        # pycodestyle (error)
+    "F",        # pyflakes
+    "G",        # flake8-logging-format
+    "LOG",      # flake8-logging
+]
diff --git a/releasenotes/notes/deprecate-inspector-0de255090cd2cb82.yaml b/releasenotes/notes/deprecate-inspector-0de255090cd2cb82.yaml
new file mode 100644
index 0000000..0607071
--- /dev/null
+++ b/releasenotes/notes/deprecate-inspector-0de255090cd2cb82.yaml
@@ -0,0 +1,10 @@
+---
+deprecations:
+  - |
+    Support for ironic-inspector has been deprecated, because ironic-inspector
+    was already retired. Due to this deprecation, the following options are
+    also deprecated.
+
+    - ``[service_available] ironic_inspector``
+    - ``[enforce_scope] ironic_inspector``
+    - All options in the ``[baremetal_introspection]`` section.
diff --git a/requirements.txt b/requirements.txt
index bfafa58..ff7b704 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-pbr>=2.0.0 # Apache-2.0
+pbr>=6.0.0 # Apache-2.0
 oslo.config>=5.2.0 # Apache-2.0
 oslo.log>=3.36.0 # Apache-2.0
 oslo.serialization>=2.18.0 # Apache-2.0
diff --git a/setup.py b/setup.py
index cd35c3c..b997e51 100644
--- a/setup.py
+++ b/setup.py
@@ -16,5 +16,5 @@
 import setuptools
 
 setuptools.setup(
-    setup_requires=['pbr>=2.0.0'],
+    setup_requires=['pbr>=6.0.0'],
     pbr=True)
diff --git a/test-requirements.txt b/test-requirements.txt
index 0ce003e..a080de9 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,2 +1 @@
 stestr>=1.0.0 # Apache-2.0
-coverage>=4.0 # Apache-2.0
diff --git a/tox.ini b/tox.ini
index 3a67abc..79892f5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-minversion = 3.18.0
+minversion = 4.4.0
 envlist = pep8
 ignore_basepython_conflict=true
 
@@ -14,25 +14,13 @@
 commands = stestr run --slowest {posargs}
 
 [testenv:pep8]
-deps =
-    hacking~=6.0.0 # Apache-2.0
-    flake8-import-order>=0.17.1 # LGPLv3
-    pycodestyle>=2.0.0,<3.0.0 # MIT
-commands = flake8 {posargs}
+deps = pre-commit
+allowlist_externals = pre-commit
+commands = pre-commit run --all-files --show-diff-on-failure {posargs}
 
 [testenv:venv]
 commands = {posargs}
 
-[testenv:cover]
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run --source ironic_tempest_plugin --parallel-mode
-commands =
-    stestr run {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-
 [testenv:docs]
 deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
        -r{toxinidir}/doc/requirements.txt
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 4d9da27..c89b63e 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -6,64 +6,30 @@
     check:
       jobs:
         # NOTE(dtantsur): keep N-3 and older non-voting for these jobs.
-        - ironic-standalone
-        - ironic-standalone-2024.2
-        - ironic-standalone-2024.1
-        - ironic-standalone-2023.2:
-            voting: false
         - ironic-tempest-functional-python3
-        - ironic-tempest-functional-python3-2024.2
-        - ironic-tempest-functional-python3-2024.1
-        - ironic-tempest-functional-python3-2023.2:
-            voting: false
-        - ironic-tempest-functional-rbac-scope-enforced-2024.2
-        - ironic-tempest-functional-rbac-scope-enforced-2024.1
-        - ironic-tempest-functional-rbac-scope-enforced-2023.2:
-            voting: false
+        - ironic-tempest-functional-python3-2025.2
+        - ironic-tempest-functional-python3-2025.1
+        # NOTE(TheJulia): Consensus in the 2026.1 PTG is that we don't need
+        # to run the anaconda job as heavily as we need to run the normal
+        # tempest jobs.
         - ironic-standalone-anaconda
-        - ironic-standalone-anaconda-2024.2
-        - ironic-standalone-anaconda-2024.1
-        - ironic-standalone-anaconda-2023.2:
-            voting: false
         - ironic-standalone-redfish
-        - ironic-standalone-redfish-2024.2
-        - ironic-standalone-redfish-2024.1
-        - ironic-standalone-redfish-2023.2:
-            voting: false
-        # NOTE(dtantsur): inspector is deprecated and rarely sees any changes,
-        # no point in running many jobs
-        - ironic-inspector-tempest
-        - ironic-inspector-tempest-2024.2:
-            voting: false
-        - ironic-inspector-tempest-2024.1:
-            voting: false
+        - ironic-standalone-redfish-2025.2
+        - ironic-standalone-redfish-2025.1
         # NOTE(dtantsur): these jobs cover rarely changed tests and are quite
         # unstable, so keep them non-voting.
-        # NOTE(TheJulia): Except this first one so we can validate fixes to
-        # the base tests as we make them.
-        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
-        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.2:
+        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode:
             voting: false
-        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.1:
-            voting: false
-        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2023.2:
+        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2025.2:
             voting: false
         - ironic-inspector-tempest-discovery:
             voting: false
     gate:
       jobs:
-        - ironic-standalone
-        - ironic-standalone-2024.2
-        - ironic-standalone-2024.1
         - ironic-tempest-functional-python3
-        - ironic-tempest-functional-python3-2024.2
-        - ironic-tempest-functional-python3-2024.1
-        - ironic-tempest-functional-rbac-scope-enforced-2024.2
-        - ironic-tempest-functional-rbac-scope-enforced-2024.1
+        - ironic-tempest-functional-python3-2025.2
+        - ironic-tempest-functional-python3-2025.1
         - ironic-standalone-anaconda
-        - ironic-standalone-anaconda-2024.2
-        - ironic-standalone-anaconda-2024.1
         - ironic-standalone-redfish
-        - ironic-standalone-redfish-2024.2
-        - ironic-standalone-redfish-2024.1
-        - ironic-inspector-tempest
+        - ironic-standalone-redfish-2025.2
+        - ironic-standalone-redfish-2025.1
diff --git a/zuul.d/stable-jobs.yaml b/zuul.d/stable-jobs.yaml
index c75f782..f67694b 100644
--- a/zuul.d/stable-jobs.yaml
+++ b/zuul.d/stable-jobs.yaml
@@ -1,99 +1,39 @@
 - job:
-    name: ironic-standalone-2024.2
-    parent: ironic-standalone
-    override-checkout: stable/2024.2
+    name: ironic-standalone-redfish-2025.2
+    parent: ironic-standalone-redfish
+    override-checkout: stable/2025.2
 
 - job:
-    name: ironic-standalone-2024.1
-    parent: ironic-standalone
-    override-checkout: stable/2024.1
+    name: ironic-standalone-redfish-2025.1
+    parent: ironic-standalone-redfish
+    override-checkout: stable/2025.1
 
 - job:
-    name: ironic-standalone-2023.2
-    parent: ironic-standalone
-    override-checkout: stable/2023.2
-
-- job:
-    name: ironic-standalone-redfish-2024.2
-    parent: ironic-standalone
-    override-checkout: stable/2024.2
-
-- job:
-    name: ironic-standalone-redfish-2024.1
-    parent: ironic-standalone
-    override-checkout: stable/2024.1
-
-- job:
-    name: ironic-standalone-redfish-2023.2
-    parent: ironic-standalone
-    override-checkout: stable/2023.2
-
-- job:
-    name: ironic-standalone-anaconda-2024.2
+    name: ironic-standalone-anaconda-2025.2
     parent: ironic-standalone-anaconda
-    override-checkout: stable/2024.2
+    override-checkout: stable/2025.2
 
 - job:
-    name: ironic-standalone-anaconda-2024.1
+    name: ironic-standalone-anaconda-2025.1
     parent: ironic-standalone-anaconda
-    override-checkout: stable/2024.1
+    override-checkout: stable/2025.1
 
 - job:
-    name: ironic-standalone-anaconda-2023.2
-    parent: ironic-standalone-anaconda
-    override-checkout: stable/2023.2
-
-- job:
-    name: ironic-tempest-functional-python3-2024.2
+    name: ironic-tempest-functional-python3-2025.2
     parent: ironic-tempest-functional-python3
-    override-checkout: stable/2024.2
+    override-checkout: stable/2025.2
 
 - job:
-    name: ironic-tempest-functional-python3-2024.1
+    name: ironic-tempest-functional-python3-2025.1
     parent: ironic-tempest-functional-python3
-    override-checkout: stable/2024.1
+    override-checkout: stable/2025.1
 
 - job:
-    name: ironic-tempest-functional-python3-2023.2
-    parent: ironic-tempest-functional-python3
-    override-checkout: stable/2023.2
-
-- job:
-    name: ironic-tempest-functional-rbac-scope-enforced-2024.2
-    parent: ironic-tempest-functional-rbac-scope-enforced
-    override-checkout: stable/2024.2
-
-- job:
-    name: ironic-tempest-functional-rbac-scope-enforced-2024.1
-    parent: ironic-tempest-functional-rbac-scope-enforced
-    override-checkout: stable/2024.1
-
-- job:
-    name: ironic-tempest-functional-rbac-scope-enforced-2023.2
-    parent: ironic-tempest-functional-rbac-scope-enforced
-    override-checkout: stable/2023.2
-
-- job:
-    name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.2
+    name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2025.2
     parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
-    override-checkout: stable/2024.2
+    override-checkout: stable/2025.2
 
 - job:
-    name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.1
+    name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2025.1
     parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
-    override-checkout: stable/2024.1
-
-- job:
-    name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2023.2
-    parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
-    override-checkout: stable/2023.2
-
-- job:
-    name: ironic-inspector-tempest-2024.2
-    parent: ironic-inspector-tempest
-    override-checkout: stable/2024.2
-
-- job:
-    name: ironic-inspector-tempest-2024.1
-    parent: ironic-inspector-tempest
-    override-checkout: stable/2024.1
+    override-checkout: stable/2025.1