Merge "CI: Increment stable jobs for 2024.1/drop Zed"
diff --git a/ironic_tempest_plugin/services/baremetal/base.py b/ironic_tempest_plugin/services/baremetal/base.py
index 2acad18..f23310f 100644
--- a/ironic_tempest_plugin/services/baremetal/base.py
+++ b/ironic_tempest_plugin/services/baremetal/base.py
@@ -63,9 +63,23 @@
     def get_headers(self):
         headers = super(BaremetalClient, self).get_headers()
         if BAREMETAL_MICROVERSION:
+            # NOTE(TheJulia): This is not great, because it can blind a test
+            # to the actual version supported.
             headers[self.api_microversion_header_name] = BAREMETAL_MICROVERSION
         return headers
 
+    def get_raw_headers(self):
+        """A proper get headers without guessing the microversion."""
+        return super(BaremetalClient, self).get_headers()
+
+    def get_min_max_api_microversions(self):
+        """Returns a tuple of minimum and remote microversions."""
+        _, resp_body = self._show_request(None, uri='/')
+        version = resp_body.get('default_version', {})
+        api_min = version.get('min_version')
+        api_max = version.get('version')
+        return (api_min, api_max)
+
     def request(self, *args, **kwargs):
         resp, resp_body = super(BaremetalClient, self).request(*args, **kwargs)
         latest_microversion = api_version_utils.LATEST_MICROVERSION
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 628a075..5715609 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -261,7 +261,12 @@
 
         """
         node = {}
-        for field in ('resource_class', 'name', 'description', 'shard'):
+        # Explicitly allow definition of network interface and deploy
+        # interface to allow tests to specify the required values
+        # as they hold a great deal of logic which is executed upon and
+        # they can ultimately impact test behavior.
+        for field in ('resource_class', 'name', 'description', 'shard',
+                      'network_interface', 'deploy_interface'):
             if kwargs.get(field):
                 node[field] = kwargs[field]
 
@@ -271,7 +276,7 @@
                             'cpus': kwargs.get('cpus', 8),
                             'local_gb': kwargs.get('local_gb', 1024),
                             'memory_mb': kwargs.get('memory_mb', 4096)},
-             'driver': kwargs.get('driver', 'fake')}
+             'driver': kwargs.get('driver', 'fake-hardware')}
         )
 
         return self._create_request('nodes', node)
diff --git a/ironic_tempest_plugin/tests/api/admin/test_nodes.py b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
index c3408ec..79e8da4 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_nodes.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
@@ -312,7 +312,10 @@
         super(TestNodesVif, self).setUp()
 
         _, self.chassis = self.create_chassis()
-        _, self.node = self.create_node(self.chassis['uuid'])
+        # The tests will mostly fail in this class if exposed to the
+        # noop network interface, which is what the default is.
+        _, self.node = self.create_node(self.chassis['uuid'],
+                                        network_interface='flat')
         if CONF.network.shared_physical_network:
             self.net = self.os_admin.networks_client.list_networks(
                 name=CONF.compute.fixed_network_name)['networks'][0]
@@ -432,7 +435,9 @@
                                         data_utils.rand_mac_address())
         self.client.vif_attach(self.node['uuid'], self.nport_id)
         _, body = self.client.vif_list(self.node['uuid'])
+
         self.assertEqual({'vifs': [{'id': self.nport_id}]}, body)
+
         self.assertRaises(lib_exc.Conflict, self.client.vif_attach,
                           self.node['uuid'], self.nport_id)
         self.client.vif_detach(self.node['uuid'], self.nport_id)
@@ -495,6 +500,7 @@
         _, port = self.client.show_port(self.port['uuid'])
         self.assertEqual(self.nport_id,
                          port['internal_info']['tenant_vif_port_id'])
+        self.client.vif_detach(self.node['uuid'], self.nport_id)
 
     @decorators.attr(type='negative')
     @decorators.idempotent_id('85b610cd-5ba8-49a7-8ce2-5e364056fd29')
@@ -545,6 +551,7 @@
                          port['internal_info']['tenant_vif_port_id'])
         _, portgroup = self.client.show_portgroup(self.portgroup['uuid'])
         self.assertNotIn('tenant_vif_port_id', portgroup['internal_info'])
+        self.client.vif_detach(self.node['uuid'], self.nport_id)
 
     @decorators.attr(type='negative')
     @decorators.idempotent_id('3affca81-9f3f-4dab-ad3d-77c892d8d0d7')
@@ -556,20 +563,6 @@
                           self.nport_id)
 
     @decorators.attr(type='negative')
-    @decorators.idempotent_id('9290e1f9-7e75-4e12-aea7-3649348e7f36')
-    def test_vif_attach_no_args(self):
-        """Negative test for VIF attachment with lack of arguments."""
-        self.assertRaises(lib_exc.BadRequest,
-                          self.client.vif_attach,
-                          self.node['uuid'], '')
-        self.assertRaises(lib_exc.BadRequest,
-                          self.client.vif_attach,
-                          '', '')
-        self.assertRaises(lib_exc.BadRequest,
-                          self.client.vif_attach,
-                          '', self.nport_id)
-
-    @decorators.attr(type='negative')
     @decorators.idempotent_id('da036225-47b0-43b7-9586-0d6390bd3cd9')
     def test_vif_detach_not_existing(self):
         """Negative test for VIF detachment of not existing VIF."""
@@ -1119,9 +1112,7 @@
     def setUp(self):
         super(TestNodesProtectedOldApi, self).setUp()
         _, self.chassis = self.create_chassis()
-        _, self.node = self.create_node(self.chassis['uuid'],
-                                        deploy_interface='fake',
-                                        network_interface='noop')
+        _, self.node = self.create_node(self.chassis['uuid'])
         self.deploy_node(self.node['uuid'])
         _, self.node = self.client.show_node(self.node['uuid'])
 
diff --git a/ironic_tempest_plugin/tests/api/admin/test_nodestates.py b/ironic_tempest_plugin/tests/api/admin/test_nodestates.py
index d03a876..010e45b 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_nodestates.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_nodestates.py
@@ -78,9 +78,7 @@
 
     @decorators.idempotent_id('ccb8fca9-2ba0-480c-a037-34c3bd09dc74')
     def test_set_node_provision_state(self):
-        _, node = self.create_node(self.chassis['uuid'],
-                                   deploy_interface='fake',
-                                   network_interface='noop')
+        _, node = self.create_node(self.chassis['uuid'])
         # Nodes appear in NONE state by default until v1.1
         self.assertIsNone(node['provision_state'])
         provision_states_list = ['active', 'deleted']
@@ -96,19 +94,33 @@
     def setUp(self):
         super(TestNodeStatesV1_2, self).setUp()
         self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.2'))
+        # Make node with 1.2, so the start state written to the DB as expected.
+        _, self.node = self.create_node(self.chassis['uuid'])
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture('1.31'))
+        # Now with a 1.31 microversion, swap the deploy and 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': '/deploy_interface',
+                                  'op': 'replace',
+                                  'value': 'fake'},
+                                 {'path': '/network_interface',
+                                  'op': 'replace',
+                                  'value': 'noop'}])
+        self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.2'))
 
     @decorators.idempotent_id('9c414984-f3b6-4b3d-81da-93b60d4662fb')
     def test_set_node_provision_state(self):
-        _, node = self.create_node(self.chassis['uuid'],
-                                   deploy_interface='fake',
-                                   network_interface='noop')
+        _, node = self.client.show_node(self.node['uuid'])
         # Nodes appear in AVAILABLE state by default from v1.2 to v1.10
-        self.assertEqual('available', node['provision_state'])
+        self.assertEqual('available', self.node['provision_state'])
         provision_states_list = ['active', 'deleted']
         target_states_list = ['active', 'available']
         for (provision_state, target_state) in zip(provision_states_list,
                                                    target_states_list):
-            self.client.set_node_provision_state(node['uuid'], provision_state)
+            self.client.set_node_provision_state(node['uuid'],
+                                                 provision_state)
             self._validate_provision_state(node['uuid'], target_state)
 
 
@@ -117,12 +129,25 @@
     def setUp(self):
         super(TestNodeStatesV1_4, self).setUp()
         self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.4'))
+        _, self.node = self.create_node(self.chassis['uuid'])
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture('1.31'))
+        # Now with a 1.31 microversion, swap the deploy and 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': '/deploy_interface',
+                                  'op': 'replace',
+                                  'value': 'fake'},
+                                 {'path': '/network_interface',
+                                  'op': 'replace',
+                                  'value': 'noop'}])
+        self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.4'))
 
     @decorators.idempotent_id('3d606003-05ce-4b5a-964d-bdee382fafe9')
     def test_set_node_provision_state(self):
-        _, node = self.create_node(self.chassis['uuid'],
-                                   deploy_interface='fake',
-                                   network_interface='noop')
+        # Make node with 1.2, so the start state written to the DB as expected.
+        _, node = self.client.show_node(self.node['uuid'])
         # Nodes appear in AVAILABLE state by default from v1.2 to v1.10
         self.assertEqual('available', node['provision_state'])
         # MANAGEABLE state and PROVIDE transition have been added in v1.4
@@ -132,7 +157,8 @@
             'manageable', 'available', 'active', 'available']
         for (provision_state, target_state) in zip(provision_states_list,
                                                    target_states_list):
-            self.client.set_node_provision_state(node['uuid'], provision_state)
+            self.client.set_node_provision_state(node['uuid'],
+                                                 provision_state)
             self._validate_provision_state(node['uuid'], target_state)
 
 
@@ -140,13 +166,27 @@
 
     def setUp(self):
         super(TestNodeStatesV1_6, self).setUp()
+        # Creates a node with 1.31, and is later reset for the rest of the test
+        # due to Ironic's evolution of drivers.
+        self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.6'))
+        _, self.node = self.create_node(self.chassis['uuid'])
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture('1.31'))
+        # Now with a 1.31 microversion, swap the deploy and 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': '/deploy_interface',
+                                  'op': 'replace',
+                                  'value': 'fake'},
+                                 {'path': '/network_interface',
+                                  'op': 'replace',
+                                  'value': 'noop'}])
         self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.6'))
 
     @decorators.idempotent_id('6c9ce4a3-713b-4c76-91af-18c48d01f1bb')
     def test_set_node_provision_state(self):
-        _, node = self.create_node(self.chassis['uuid'],
-                                   deploy_interface='fake',
-                                   network_interface='noop')
+        _, node = self.client.show_node(self.node['uuid'])
         # Nodes appear in AVAILABLE state by default from v1.2 to v1.10
         self.assertEqual('available', node['provision_state'])
         # INSPECT* states have been added in v1.6
@@ -156,33 +196,41 @@
             'manageable', 'manageable', 'available', 'active', 'available']
         for (provision_state, target_state) in zip(provision_states_list,
                                                    target_states_list):
-            self.client.set_node_provision_state(node['uuid'], provision_state)
-            self._validate_provision_state(node['uuid'], target_state)
+            self.client.set_node_provision_state(node['uuid'],
+                                                 provision_state)
+            self._validate_provision_state(node['uuid'],
+                                           target_state)
 
 
 class TestNodeStatesV1_11(TestNodeStatesMixin, base.BaseBaremetalTest):
 
     def setUp(self):
         super(TestNodeStatesV1_11, self).setUp()
+        # Creates a node with 1.31, and is later reset for the rest of the test
+        # due to Ironic's evolution of drivers.
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture('1.31'))
+        _, self.node = self.create_node(self.chassis['uuid'],
+                                        deploy_interface='fake',
+                                        network_interface='noop')
         self.useFixture(
             api_microversion_fixture.APIMicroversionFixture('1.11')
         )
 
     @decorators.idempotent_id('31f53828-b83d-40c7-98e5-843e28a1b6b9')
     def test_set_node_provision_state(self):
-        _, node = self.create_node(self.chassis['uuid'],
-                                   deploy_interface='fake',
-                                   network_interface='noop')
         # Nodes appear in ENROLL state by default from v1.11
-        self.assertEqual('enroll', node['provision_state'])
+        self.assertEqual('enroll', self.node['provision_state'])
         provision_states_list = [
             'manage', 'inspect', 'provide', 'active', 'deleted']
         target_states_list = [
             'manageable', 'manageable', 'available', 'active', 'available']
         for (provision_state, target_state) in zip(provision_states_list,
                                                    target_states_list):
-            self.client.set_node_provision_state(node['uuid'], provision_state)
-            self._validate_provision_state(node['uuid'], target_state)
+            self.client.set_node_provision_state(self.node['uuid'],
+                                                 provision_state)
+            self._validate_provision_state(self.node['uuid'],
+                                           target_state)
 
 
 class TestNodeStatesV1_12(TestNodeStatesMixin, base.BaseBaremetalTest):
diff --git a/ironic_tempest_plugin/tests/api/admin/test_ports.py b/ironic_tempest_plugin/tests/api/admin/test_ports.py
index 9988279..dfb371c 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_ports.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_ports.py
@@ -25,8 +25,7 @@
         super(TestPorts, self).setUp()
 
         _, self.chassis = self.create_chassis()
-        _, self.node = self.create_node(self.chassis['uuid'],
-                                        network_interface='noop')
+        _, self.node = self.create_node(self.chassis['uuid'])
         _, self.port = self.create_port(self.node['uuid'],
                                         data_utils.rand_mac_address())
 
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 8923aa3..bd338f9 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
@@ -25,8 +25,7 @@
         super(TestPortsNegative, self).setUp()
 
         _, self.chassis = self.create_chassis()
-        _, self.node = self.create_node(self.chassis['uuid'],
-                                        network_interface='noop')
+        _, self.node = self.create_node(self.chassis['uuid'])
 
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('0a6ee1f7-d0d9-4069-8778-37f3aa07303a')
diff --git a/ironic_tempest_plugin/tests/api/base.py b/ironic_tempest_plugin/tests/api/base.py
index 6ebb162..59f2f94 100644
--- a/ironic_tempest_plugin/tests/api/base.py
+++ b/ironic_tempest_plugin/tests/api/base.py
@@ -76,6 +76,8 @@
 
         cfg_min_version = CONF.baremetal.min_microversion
         cfg_max_version = CONF.baremetal.max_microversion
+
+        # Check versions and skip based upon *configuration*
         api_version_utils.check_skip_with_microversion(cls.min_microversion,
                                                        cls.max_microversion,
                                                        cfg_min_version,
@@ -100,6 +102,15 @@
         else:
             cls.client = cls.os_admin.baremetal.BaremetalClient()
 
+        # Skip the test if the class version doesn't match.
+        if cls.min_microversion or cls.max_microversion:
+            api_min, api_max = cls.client.get_min_max_api_microversions()
+            api_version_utils.check_skip_with_microversion(
+                cls.min_microversion,
+                cls.max_microversion,
+                api_min,
+                api_max)
+
     @classmethod
     def resource_setup(cls):
         super(BaseBaremetalTest, cls).resource_setup()
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
index 27b3c9d..9ceecb7 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
@@ -82,6 +82,7 @@
         super(BaremetalScenarioTest, cls).skip_checks()
         if not CONF.service_available.ironic:
             raise cls.skipException('Ironic is not enabled.')
+        # This is the configuration based skip test
         cfg_min_version = CONF.baremetal.min_microversion
         cfg_max_version = CONF.baremetal.max_microversion
         api_version_utils.check_skip_with_microversion(cls.min_microversion,
@@ -96,6 +97,17 @@
             client = cls.os_system_admin.baremetal.BaremetalClient()
         else:
             client = cls.os_admin.baremetal.BaremetalClient()
+
+        # This is the automatic based upon remote version skip check,
+        # as it requires an API client to obtain the remote version.
+        if cls.min_microversion or cls.max_microversion:
+            api_min, api_max = client.get_min_max_api_microversions()
+            api_version_utils.check_skip_with_microversion(
+                cls.min_microversion,
+                cls.max_microversion,
+                api_min,
+                api_max)
+
         cls.baremetal_client = client
 
     @classmethod
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
index a66d7d6..48a6bd5 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
@@ -192,7 +192,12 @@
         """
         vifs = cls.get_node_vifs(node_id)
         for vif in vifs:
-            cls.baremetal_client.vif_detach(node_id, vif)
+            try:
+                cls.baremetal_client.vif_detach(node_id, vif)
+            except lib_exc.BadRequest:
+                # When the vif was already removed, such as the
+                # node was already unprovisioned.
+                pass
             if force_delete:
                 try:
                     cls.ports_client.delete_port(vif)
@@ -791,8 +796,12 @@
                     "args": {"raid_config": config},
                 }
             ]
-            self.baremetal_client.create_deploy_template(
+            _, template = self.baremetal_client.create_deploy_template(
                 'CUSTOM_RAID', steps=steps)
+            # Set a cleanup to ensure the deploy template is removed.
+            self.addCleanup(
+                self.baremetal_client.delete_deploy_template,
+                template['uuid'])
             self.baremetal_client.add_node_trait(self.node['uuid'],
                                                  'CUSTOM_RAID')
 
@@ -873,8 +882,7 @@
         self.assertTrue(self.ping_ip_address(self.node_ip,
                                              should_succeed=True))
 
-    @classmethod
-    def boot_node_ramdisk(cls, ramdisk_ref, iso=False):
+    def boot_node_ramdisk(self, ramdisk_ref, iso=False):
         """Boot ironic using a ramdisk node.
 
         The following actions are executed:
@@ -890,11 +898,11 @@
                     us actually an ISO image.
         """
         if ramdisk_ref is None:
-            ramdisk_ref = cls.image_ref
+            ramdisk_ref = self.image_ref
 
-        network, subnet, router = cls.create_networks()
-        n_port = cls.create_neutron_port(network_id=network['id'])
-        cls.vif_attach(node_id=cls.node['uuid'], vif_id=n_port['id'])
+        network, subnet, router = self.create_networks()
+        n_port = self.create_neutron_port(network_id=network['id'])
+        self.vif_attach(node_id=self.node['uuid'], vif_id=n_port['id'])
         if iso:
             patch_path = '/instance_info/boot_iso'
         else:
@@ -905,22 +913,25 @@
         patch = [{'path': patch_path,
                   'op': 'add',
                   'value': ramdisk_ref}]
-        cls.update_node(cls.node['uuid'], patch=patch)
-        cls.set_node_provision_state(cls.node['uuid'], 'active')
+        self.update_node(self.node['uuid'], patch=patch)
+        self.set_node_provision_state(self.node['uuid'], 'active')
+        self.addCleanup(
+            self.set_node_provision_state,
+            self.node['uuid'], 'deleted')
         if CONF.validation.connect_method == 'floating':
-            cls.node_ip = cls.add_floatingip_to_node(cls.node['uuid'])
+            self.node_ip = self.add_floatingip_to_node(self.node['uuid'])
         elif CONF.validation.connect_method == 'fixed':
-            cls.node_ip = cls.get_server_ip(cls.node['uuid'])
+            self.node_ip = self.get_server_ip(self.node['uuid'])
         else:
             m = ('Configuration option "[validation]/connect_method" '
                  'must be set.')
             raise lib_exc.InvalidConfiguration(m)
-        cls.wait_power_state(cls.node['uuid'],
-                             bm.BaremetalPowerStates.POWER_ON)
-        cls.wait_provisioning_state(cls.node['uuid'],
-                                    bm.BaremetalProvisionStates.ACTIVE,
-                                    timeout=CONF.baremetal.active_timeout,
-                                    interval=30)
+        self.wait_power_state(self.node['uuid'],
+                              bm.BaremetalPowerStates.POWER_ON)
+        self.wait_provisioning_state(self.node['uuid'],
+                                     bm.BaremetalProvisionStates.ACTIVE,
+                                     timeout=CONF.baremetal.active_timeout,
+                                     interval=30)
 
     def boot_and_verify_ramdisk_node(self, ramdisk_ref=None, iso=False,
                                      should_succeed=True):