Merge "Adding basic microversion tests to CI"
diff --git a/ironic_tempest_plugin/common/waiters.py b/ironic_tempest_plugin/common/waiters.py
index cd13fe7..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():
@@ -181,7 +185,16 @@
         if raise_if_insufficent_access and '** Redacted' in field_value:
             msg = ('Unable to see contents of redacted field '
                    'indicating insufficient access to execute this test.')
-            raise lib_exc.InsufficientAPIAccess(msg)
+            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 1dce427..47bcb55 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -224,9 +224,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 "
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
index ad9e4c9..7e93fe0 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
@@ -139,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):
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 a0bcfc3..e3a9a58 100644
--- a/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
+++ b/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
@@ -178,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)
 
diff --git a/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py b/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py
index b347431..5bb7df5 100644
--- a/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py
+++ b/ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py
@@ -47,11 +47,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, create_router=True):
         network = self.create_network(
@@ -118,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(
@@ -132,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
@@ -140,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(
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/zuul.d/project.yaml b/zuul.d/project.yaml
index a1cfde0..8ba9c3a 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -6,20 +6,15 @@
     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-tempest-functional-python3
+        - ironic-tempest-functional-python3-2025.1
         - 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-standalone-anaconda
+        - ironic-standalone-anaconda-2025.1
         - ironic-standalone-anaconda-2024.2
-        - ironic-standalone-anaconda-2024.1
         - ironic-standalone-redfish
+        - ironic-standalone-redfish-2025.1
         - ironic-standalone-redfish-2024.2
-        - ironic-standalone-redfish-2024.1
         # NOTE(dtantsur): inspector is deprecated and rarely sees any changes,
         # no point in running many jobs
         - ironic-inspector-tempest
@@ -27,24 +22,19 @@
         # unstable, so keep them non-voting.
         - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode:
             voting: false
-        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.2:
+        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2025.1:
             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-2025.1
         - 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-standalone-anaconda
+        - ironic-standalone-anaconda-2025.1
         - ironic-standalone-anaconda-2024.2
-        - ironic-standalone-anaconda-2024.1
         - ironic-standalone-redfish
+        - ironic-standalone-redfish-2025.1
         - ironic-standalone-redfish-2024.2
-        - ironic-standalone-redfish-2024.1
         - ironic-inspector-tempest
diff --git a/zuul.d/stable-jobs.yaml b/zuul.d/stable-jobs.yaml
index c75f782..40fd43d 100644
--- a/zuul.d/stable-jobs.yaml
+++ b/zuul.d/stable-jobs.yaml
@@ -14,21 +14,31 @@
     override-checkout: stable/2023.2
 
 - job:
+    name: ironic-standalone-redfish-2025.1
+    parent: ironic-standalone-redfish
+    override-checkout: stable/2025.1
+
+- job:
     name: ironic-standalone-redfish-2024.2
-    parent: ironic-standalone
+    parent: ironic-standalone-redfish
     override-checkout: stable/2024.2
 
 - job:
     name: ironic-standalone-redfish-2024.1
-    parent: ironic-standalone
+    parent: ironic-standalone-redfish
     override-checkout: stable/2024.1
 
 - job:
     name: ironic-standalone-redfish-2023.2
-    parent: ironic-standalone
+    parent: ironic-standalone-redfish
     override-checkout: stable/2023.2
 
 - job:
+    name: ironic-standalone-anaconda-2025.1
+    parent: ironic-standalone-anaconda
+    override-checkout: stable/2025.1
+
+- job:
     name: ironic-standalone-anaconda-2024.2
     parent: ironic-standalone-anaconda
     override-checkout: stable/2024.2
@@ -44,6 +54,11 @@
     override-checkout: stable/2023.2
 
 - job:
+    name: ironic-tempest-functional-python3-2025.1
+    parent: ironic-tempest-functional-python3
+    override-checkout: stable/2025.1
+
+- job:
     name: ironic-tempest-functional-python3-2024.2
     parent: ironic-tempest-functional-python3
     override-checkout: stable/2024.2
@@ -74,6 +89,11 @@
     override-checkout: stable/2023.2
 
 - job:
+    name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2025.1
+    parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
+    override-checkout: stable/2025.1
+
+- job:
     name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-2024.2
     parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
     override-checkout: stable/2024.2