Merge "Test BM with VM on the same network"
diff --git a/ironic_tempest_plugin/common/waiters.py b/ironic_tempest_plugin/common/waiters.py
index b706ac0..08e0941 100644
--- a/ironic_tempest_plugin/common/waiters.py
+++ b/ironic_tempest_plugin/common/waiters.py
@@ -110,3 +110,37 @@
                '%(instance_uuid)s within the required time (%(timeout)s s).'
                % {'instance_uuid': instance_uuid, 'timeout': timeout})
         raise lib_exc.TimeoutException(msg)
+
+
+def wait_for_allocation(client, allocation_ident, timeout=15, interval=1,
+                        expect_error=False):
+    """Wait for the allocation to become active.
+
+    :param client: an instance of tempest plugin BaremetalClient.
+    :param allocation_ident: UUID or name of the allocation.
+    :param timeout: the timeout after which the allocation is considered as
+        failed. Defaults to 15 seconds.
+    :param interval: an interval between show_allocation calls.
+        Defaults to 1 second.
+    :param expect_error: if True, return successfully even in case of an error.
+    """
+    result = [None]  # a mutable object to modify in the closure
+
+    def check():
+        result[0] = client.show_allocation(allocation_ident)
+        allocation = result[0][1]
+
+        if allocation['state'] == 'error' and not expect_error:
+            raise lib_exc.TempestException(
+                "Allocation %(ident)s failed: %(error)s" %
+                {'ident': allocation_ident,
+                 'error': allocation.get('last_error')})
+        else:
+            return allocation['state'] != 'allocating'
+
+    if not test_utils.call_until_true(check, timeout, interval):
+        msg = ('Timed out waiting for the allocation %s to become active' %
+               allocation_ident)
+        raise lib_exc.TimeoutException(msg)
+
+    return result[0]
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index 7cabf69..a3b5019 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -195,4 +195,7 @@
                default='fake',
                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',
+               help="The driver to set on the newly discovered nodes. "
+                    "Only has effect with auto_discovery_feature is True."),
 ]
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 f60361a..08e15cd 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -109,6 +109,16 @@
         return self._list_request('conductors', **kwargs)
 
     @base.handle_errors
+    def list_allocations(self, **kwargs):
+        """List all registered allocations."""
+        return self._list_request('allocations', **kwargs)
+
+    @base.handle_errors
+    def list_deploy_templates(self, **kwargs):
+        """List all deploy templates."""
+        return self._list_request('deploy_templates', **kwargs)
+
+    @base.handle_errors
     def show_node(self, uuid, api_version=None):
         """Gets a specific node.
 
@@ -212,6 +222,31 @@
         """
         return self._show_request('conductors', hostname)
 
+    def show_allocation(self, allocation_ident):
+        """Gets a specific allocation.
+
+        :param allocation_ident: UUID or name of allocation.
+        :return: Serialized allocation as a dictionary.
+        """
+        return self._show_request('allocations', allocation_ident)
+
+    def show_node_allocation(self, node_ident):
+        """Gets an allocation for the node.
+
+        :param node_ident: Node UUID or name.
+        :return: Serialized allocation as a dictionary.
+        """
+        uri = '/nodes/%s/allocation' % node_ident
+        return self._show_request('nodes', uuid=None, uri=uri)
+
+    def show_deploy_template(self, deploy_template_ident):
+        """Gets a specific deploy template.
+
+        :param deploy_template_ident: Name or UUID of deploy template.
+        :return: Serialized deploy template as a dictionary.
+        """
+        return self._show_request('deploy_templates', deploy_template_ident)
+
     @base.handle_errors
     def create_node(self, chassis_id=None, **kwargs):
         """Create a baremetal node with the specified parameters.
@@ -226,8 +261,9 @@
 
         """
         node = {}
-        if kwargs.get('resource_class'):
-            node['resource_class'] = kwargs['resource_class']
+        for field in ('resource_class', 'name', 'description'):
+            if kwargs.get(field):
+                node[field] = kwargs[field]
 
         node.update(
             {'chassis_uuid': chassis_id,
@@ -364,6 +400,21 @@
         return self._create_request('volume/targets', volume_target)
 
     @base.handle_errors
+    def create_deploy_template(self, name, **kwargs):
+        """Create a deploy template with the specified parameters.
+
+        :param name: The name of the deploy template.
+        :param kwargs:
+            steps: deploy steps of the template.
+            uuid: UUID of the deploy template. Optional.
+            extra: meta-data of the deploy template. Optional.
+        :return: A tuple with the server response and the created deploy
+            template.
+        """
+        kwargs['name'] = name
+        return self._create_request('deploy_templates', kwargs)
+
+    @base.handle_errors
     def delete_node(self, uuid):
         """Deletes a node having the specified UUID.
 
@@ -422,6 +473,15 @@
         return self._delete_request('volume/targets', volume_target_ident)
 
     @base.handle_errors
+    def delete_deploy_template(self, deploy_template_ident):
+        """Deletes a deploy template having the specified name or UUID.
+
+        :param deploy_template_ident: Name or UUID of the deploy template.
+        :return: A tuple with the server response and the response body.
+        """
+        return self._delete_request('deploy_templates', deploy_template_ident)
+
+    @base.handle_errors
     def update_node(self, uuid, patch=None, **kwargs):
         """Update the specified node.
 
@@ -452,7 +512,8 @@
                            'protected_reason',
                            # TODO(dtantsur): maintenance is set differently
                            # in newer API versions.
-                           'maintenance')
+                           'maintenance',
+                           'description')
         if not patch:
             patch = self._make_patch(node_attributes, **kwargs)
 
@@ -510,6 +571,20 @@
         return self._patch_request('volume/targets', uuid, patch)
 
     @base.handle_errors
+    def update_deploy_template(self, deploy_template_ident, patch):
+        """Update the specified deploy template.
+
+        :param deploy_template_ident: Name or UUID of the deploy template.
+        :param patch: List of dicts representing json patches. Each dict
+            has keys 'path', 'op' and 'value'; to update a field.
+        :return: A tuple with the server response and the updated deploy
+            template.
+        """
+
+        return self._patch_request('deploy_templates', deploy_template_ident,
+                                   patch)
+
+    @base.handle_errors
     def set_node_power_state(self, node_uuid, state):
         """Set power state of the specified node.
 
@@ -761,3 +836,25 @@
                                           (node_uuid, trait), {})
         self.expected_success(http_client.NO_CONTENT, resp.status)
         return resp, body
+
+    @base.handle_errors
+    def create_allocation(self, resource_class, **kwargs):
+        """Create a baremetal allocation with the specified parameters.
+
+        :param resource_class: Resource class to request.
+        :param kwargs: Other fields to pass.
+        :return: A tuple with the server response and the created allocation.
+
+        """
+        kwargs['resource_class'] = resource_class
+        return self._create_request('allocations', kwargs)
+
+    @base.handle_errors
+    def delete_allocation(self, allocation_ident):
+        """Deletes an allocation.
+
+        :param allocation_ident: UUID or name of the allocation.
+        :return: A tuple with the server response and the response body.
+
+        """
+        return self._delete_request('allocations', allocation_ident)
diff --git a/ironic_tempest_plugin/services/introspection_client.py b/ironic_tempest_plugin/services/introspection_client.py
index 3b1a75b..f82aed6 100644
--- a/ironic_tempest_plugin/services/introspection_client.py
+++ b/ironic_tempest_plugin/services/introspection_client.py
@@ -17,13 +17,14 @@
 
 
 CONF = config.CONF
-ADMIN_CREDS = common_creds.get_configured_admin_credentials()
 
 
 class Manager(clients.Manager):
     def __init__(self,
-                 credentials=ADMIN_CREDS,
+                 credentials=None,
                  api_microversions=None):
+        if not credentials:
+            credentials = common_creds.get_configured_admin_credentials()
         super(Manager, self).__init__(credentials)
         self.introspection_client = BaremetalIntrospectionClient(
             self.auth_provider,
diff --git a/ironic_tempest_plugin/tests/api/admin/base.py b/ironic_tempest_plugin/tests/api/admin/base.py
index 6f8586f..746eea6 100644
--- a/ironic_tempest_plugin/tests/api/admin/base.py
+++ b/ironic_tempest_plugin/tests/api/admin/base.py
@@ -19,6 +19,7 @@
 
 from ironic_tempest_plugin import clients
 from ironic_tempest_plugin.common import waiters
+from ironic_tempest_plugin.services.baremetal import base
 from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
 
 CONF = config.CONF
@@ -33,8 +34,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', 'volume_connector', 'volume_target',
-                  'node', 'chassis']
+RESOURCE_TYPES = ['port', 'portgroup', 'node', 'volume_connector',
+                  'volume_target', 'chassis', 'deploy_template']
 
 
 def creates(resource):
@@ -110,6 +111,9 @@
     @classmethod
     def resource_cleanup(cls):
         """Ensure that all created objects get destroyed."""
+        # Use the requested microversion for cleanup to ensure we can delete
+        # resources.
+        base.set_baremetal_api_microversion(cls.request_microversion)
         try:
             for node in cls.deployed_nodes:
                 try:
@@ -118,12 +122,19 @@
                 except lib_exc.BadRequest:
                     pass
 
+            for node in cls.created_objects['node']:
+                try:
+                    cls.client.update_node(node, instance_uuid=None)
+                except lib_exc.TempestException:
+                    pass
+
             for resource in RESOURCE_TYPES:
                 uuids = cls.created_objects[resource]
                 delete_method = getattr(cls.client, 'delete_%s' % resource)
                 for u in uuids:
                     delete_method(u, ignore_errors=lib_exc.NotFound)
         finally:
+            base.reset_baremetal_api_microversion()
             super(BaseBaremetalTest, cls).resource_cleanup()
 
     def _assertExpected(self, expected, actual):
@@ -171,7 +182,7 @@
     @classmethod
     @creates('node')
     def create_node(cls, chassis_id, cpu_arch='x86_64', cpus=8, local_gb=10,
-                    memory_mb=4096, resource_class=None):
+                    memory_mb=4096, **kwargs):
         """Wrapper utility for creating test baremetal nodes.
 
         :param chassis_id: The unique identifier of the chassis.
@@ -179,7 +190,7 @@
         :param cpus: Number of CPUs. Default: 8.
         :param local_gb: Disk size. Default: 10.
         :param memory_mb: Available RAM. Default: 4096.
-        :param resource_class: Node resource class.
+        :param kwargs: Other optional node fields.
         :return: A tuple with the server response and the created node.
 
         """
@@ -187,7 +198,7 @@
                                             cpus=cpus, local_gb=local_gb,
                                             memory_mb=memory_mb,
                                             driver=cls.driver,
-                                            resource_class=resource_class)
+                                            **kwargs)
 
         return resp, body
 
@@ -315,6 +326,19 @@
         return resp, body
 
     @classmethod
+    @creates('deploy_template')
+    def create_deploy_template(cls, name, **kwargs):
+        """Wrapper utility for creating test deploy template.
+
+        :param name: The name of the deploy template.
+        :return: A tuple with the server response and the created deploy
+            template.
+        """
+        resp, body = cls.client.create_deploy_template(name=name, **kwargs)
+
+        return resp, body
+
+    @classmethod
     def delete_chassis(cls, chassis_id):
         """Deletes a chassis having the specified UUID.
 
@@ -405,6 +429,21 @@
 
         return resp
 
+    @classmethod
+    def delete_deploy_template(cls, deploy_template_ident):
+        """Deletes a deploy template having the specified name or UUID.
+
+        :param deploy_template_ident: Name or UUID of the deploy template.
+        :return: Server response.
+        """
+        resp, body = cls.client.delete_deploy_template(deploy_template_ident)
+
+        if deploy_template_ident in cls.created_objects['deploy_template']:
+            cls.created_objects['deploy_template'].remove(
+                deploy_template_ident)
+
+        return resp
+
     def validate_self_link(self, resource, uuid, link):
         """Check whether the given self link formatted correctly."""
         expected_link = "{base}/{pref}/{res}/{uuid}".format(
diff --git a/ironic_tempest_plugin/tests/api/admin/test_allocations.py b/ironic_tempest_plugin/tests/api/admin/test_allocations.py
new file mode 100644
index 0000000..a6e8e9c
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_allocations.py
@@ -0,0 +1,210 @@
+#    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 oslo_utils import uuidutils
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from ironic_tempest_plugin.common import waiters
+from ironic_tempest_plugin.tests.api.admin import base
+
+CONF = config.CONF
+
+
+class TestAllocations(base.BaseBaremetalTest):
+    """Tests for baremetal allocations."""
+
+    min_microversion = '1.52'
+
+    def provide_node(self, node_id, cleaning_timeout=None):
+        super(TestAllocations, self).provide_node(node_id, cleaning_timeout)
+        # Force non-empty power state, otherwise allocation API won't pick it
+        self.client.set_node_power_state(node_id, 'power off')
+
+    def setUp(self):
+        super(TestAllocations, self).setUp()
+
+        # Generate a resource class to prevent parallel tests from clashing
+        # with each other.
+        self.resource_class = uuidutils.generate_uuid()
+
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis['uuid'],
+                                        resource_class=self.resource_class)
+        self.provide_node(self.node['uuid'])
+
+    @decorators.idempotent_id('9203ea28-3c61-4108-8498-22247b654ff6')
+    def test_create_show_allocation(self):
+        self.assertIsNone(self.node['allocation_uuid'])
+        _, body = self.client.create_allocation(self.resource_class)
+        uuid = body['uuid']
+
+        self.assertTrue(uuid)
+        self.assertEqual('allocating', body['state'])
+        self.assertEqual(self.resource_class, body['resource_class'])
+        self.assertIsNone(body['last_error'])
+        self.assertIsNone(body['node_uuid'])
+
+        _, body = waiters.wait_for_allocation(self.client, uuid)
+        self.assertEqual('active', body['state'])
+        self.assertEqual(self.resource_class, body['resource_class'])
+        self.assertIsNone(body['last_error'])
+        self.assertEqual(self.node['uuid'], body['node_uuid'])
+
+        _, body2 = self.client.show_node_allocation(body['node_uuid'])
+        self.assertEqual(body, body2)
+
+        _, node = self.client.show_node(self.node['uuid'])
+        self.assertEqual(uuid, node['allocation_uuid'])
+
+    @decorators.idempotent_id('eb074d06-e5f4-4fb4-b992-c9929db488ae')
+    def test_create_allocation_with_traits(self):
+        _, node2 = self.create_node(self.chassis['uuid'],
+                                    resource_class=self.resource_class)
+        self.client.set_node_traits(node2['uuid'], ['CUSTOM_MEOW'])
+        self.provide_node(node2['uuid'])
+
+        _, body = self.client.create_allocation(self.resource_class,
+                                                traits=['CUSTOM_MEOW'])
+        uuid = body['uuid']
+
+        self.assertTrue(uuid)
+        self.assertEqual('allocating', body['state'])
+        self.assertEqual(['CUSTOM_MEOW'], body['traits'])
+        self.assertIsNone(body['last_error'])
+
+        _, body = waiters.wait_for_allocation(self.client, uuid)
+        self.assertEqual('active', body['state'])
+        self.assertEqual(['CUSTOM_MEOW'], body['traits'])
+        self.assertIsNone(body['last_error'])
+        self.assertEqual(node2['uuid'], body['node_uuid'])
+
+    @decorators.idempotent_id('12d19297-f35a-408a-8b1e-3cd244e30abe')
+    def test_create_allocation_candidate_node(self):
+        node_name = 'allocation-test-1'
+        _, node2 = self.create_node(self.chassis['uuid'],
+                                    resource_class=self.resource_class,
+                                    name=node_name)
+        self.provide_node(node2['uuid'])
+
+        _, body = self.client.create_allocation(self.resource_class,
+                                                candidate_nodes=[node_name])
+        uuid = body['uuid']
+
+        self.assertTrue(uuid)
+        self.assertEqual('allocating', body['state'])
+        self.assertEqual([node2['uuid']], body['candidate_nodes'])
+        self.assertIsNone(body['last_error'])
+
+        _, body = waiters.wait_for_allocation(self.client, uuid)
+        self.assertEqual('active', body['state'])
+        self.assertEqual([node2['uuid']], body['candidate_nodes'])
+        self.assertIsNone(body['last_error'])
+        self.assertEqual(node2['uuid'], body['node_uuid'])
+
+    @decorators.idempotent_id('84eb3c21-4e16-4f33-9551-dce0f8689462')
+    def test_delete_allocation(self):
+        _, body = self.client.create_allocation(self.resource_class)
+        self.client.delete_allocation(body['uuid'])
+        self.assertRaises(lib_exc.NotFound, self.client.show_allocation,
+                          body['uuid'])
+
+    @decorators.idempotent_id('5e30452d-ee92-4342-82c1-5eea5e55c937')
+    def test_delete_allocation_by_name(self):
+        _, body = self.client.create_allocation(self.resource_class,
+                                                name='banana')
+        self.client.delete_allocation('banana')
+        self.assertRaises(lib_exc.NotFound, self.client.show_allocation,
+                          'banana')
+
+    @decorators.idempotent_id('fbbc13bc-86da-438b-af01-d1bc1bab57d6')
+    def test_show_by_name(self):
+        _, body = self.client.create_allocation(self.resource_class,
+                                                name='banana')
+        _, loaded_body = self.client.show_allocation('banana')
+        # The allocation will likely have been processed by this time, so do
+        # not compare the whole body.
+        for field in ('name', 'uuid', 'resource_class'):
+            self.assertEqual(body[field], loaded_body[field])
+
+    @decorators.idempotent_id('4ca123c4-160d-4d8d-a3f7-15feda812263')
+    def test_list_allocations(self):
+        _, body = self.client.create_allocation(self.resource_class)
+
+        _, listing = self.client.list_allocations()
+        self.assertIn(body['uuid'],
+                      [i['uuid'] for i in listing['allocations']])
+
+        _, listing = self.client.list_allocations(
+            resource_class=self.resource_class)
+        self.assertEqual([body['uuid']],
+                         [i['uuid'] for i in listing['allocations']])
+
+    @decorators.idempotent_id('092b7148-9ff0-4107-be57-2cfcd21eb5d7')
+    def test_list_allocations_by_state(self):
+        _, body = self.client.create_allocation(self.resource_class)
+        _, body2 = self.client.create_allocation(self.resource_class + 'foo2')
+
+        waiters.wait_for_allocation(self.client, body['uuid'])
+        waiters.wait_for_allocation(self.client, body2['uuid'],
+                                    expect_error=True)
+
+        _, listing = self.client.list_allocations(state='active')
+        uuids = [i['uuid'] for i in listing['allocations']]
+        self.assertIn(body['uuid'], uuids)
+        self.assertNotIn(body2['uuid'], uuids)
+
+        _, listing = self.client.list_allocations(state='error')
+        uuids = [i['uuid'] for i in listing['allocations']]
+        self.assertNotIn(body['uuid'], uuids)
+        self.assertIn(body2['uuid'], uuids)
+
+        _, listing = self.client.list_allocations(state='allocating')
+        uuids = [i['uuid'] for i in listing['allocations']]
+        self.assertNotIn(body['uuid'], uuids)
+        self.assertNotIn(body2['uuid'], uuids)
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('bf7e1375-019a-466a-a294-9c1052827ada')
+    def test_create_allocation_resource_class_mismatch(self):
+        _, body = self.client.create_allocation(self.resource_class + 'foo')
+
+        _, body = waiters.wait_for_allocation(self.client, body['uuid'],
+                                              expect_error=True)
+        self.assertEqual('error', body['state'])
+        self.assertTrue(body['last_error'])
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('b4eeddee-ca34-44f9-908b-490b78b18486')
+    def test_create_allocation_traits_mismatch(self):
+        _, body = self.client.create_allocation(
+            self.resource_class, traits=['CUSTOM_DOES_NOT_EXIST'])
+
+        _, body = waiters.wait_for_allocation(self.client, body['uuid'],
+                                              expect_error=True)
+        self.assertEqual('error', body['state'])
+        self.assertTrue(body['last_error'])
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('2378727f-77c3-4289-9562-bd2f3b147a60')
+    def test_create_allocation_node_mismatch(self):
+        _, node2 = self.create_node(self.chassis['uuid'],
+                                    resource_class=self.resource_class + 'alt')
+        # Mismatch between the resource class and the candidate node
+        _, body = self.client.create_allocation(
+            self.resource_class, candidate_nodes=[node2['uuid']])
+
+        _, body = waiters.wait_for_allocation(self.client, body['uuid'],
+                                              expect_error=True)
+        self.assertEqual('error', body['state'])
+        self.assertTrue(body['last_error'])
diff --git a/ironic_tempest_plugin/tests/api/admin/test_deploy_templates.py b/ironic_tempest_plugin/tests/api/admin/test_deploy_templates.py
new file mode 100644
index 0000000..0092a06
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_deploy_templates.py
@@ -0,0 +1,334 @@
+#    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 copy
+
+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.admin import base
+
+
+EXAMPLE_STEPS = [{
+    'interface': 'bios',
+    'step': 'apply_configuration',
+    'args': {},
+    'priority': 10
+}]
+
+
+def _get_random_trait():
+    return data_utils.rand_name('CUSTOM', '').replace('-', '_')
+
+
+class TestDeployTemplates(base.BaseBaremetalTest):
+    """Tests for deploy templates."""
+
+    min_microversion = '1.55'
+
+    def setUp(self):
+        super(TestDeployTemplates, self).setUp()
+        self.name = _get_random_trait()
+        self.steps = copy.deepcopy(EXAMPLE_STEPS)
+        _, self.template = self.create_deploy_template(self.name,
+                                                       steps=self.steps)
+
+    @decorators.idempotent_id('8f39c794-4e6f-42fc-bfcb-e1f6eafff2e9')
+    def test_create_deploy_template_specifying_uuid(self):
+        name = _get_random_trait()
+        uuid = data_utils.rand_uuid()
+
+        _, template = self.create_deploy_template(name=name, steps=self.steps,
+                                                  uuid=uuid)
+
+        _, body = self.client.show_deploy_template(uuid)
+        self._assertExpected(template, body)
+
+    @decorators.idempotent_id('7ac8fdd5-928b-4fbc-8341-3ebaad9def5e')
+    def test_delete_deploy_template(self):
+        self.delete_deploy_template(self.template['uuid'])
+
+        self.assertRaises(lib_exc.NotFound, self.client.show_deploy_template,
+                          self.template['uuid'])
+
+    @decorators.idempotent_id('f424dd67-46c3-4169-b8f0-7e0c18b70437')
+    def test_show_deploy_template(self):
+        _, template = self.client.show_deploy_template(self.template['uuid'])
+        self._assertExpected(self.template, template)
+        self.assertEqual(self.name, template['name'])
+        self.assertEqual(self.steps, template['steps'])
+        self.assertIn('uuid', template)
+        self.assertEqual({}, template['extra'])
+
+    @decorators.idempotent_id('2fd98e9a-10ce-405a-a32c-0d6079766183')
+    def test_show_deploy_template_with_links(self):
+        _, template = self.client.show_deploy_template(self.template['uuid'])
+        self.assertIn('links', template)
+        self.assertEqual(2, len(template['links']))
+        self.assertIn(template['uuid'], template['links'][0]['href'])
+
+    @decorators.idempotent_id('cec2a01d-07af-4062-a8b0-9a1703f65bcf')
+    def test_list_deploy_templates(self):
+        _, body = self.client.list_deploy_templates()
+        self.assertIn(self.template['uuid'],
+                      [i['uuid'] for i in body['deploy_templates']])
+        # Verify self links.
+        for template in body['deploy_templates']:
+            self.validate_self_link('deploy_templates', template['uuid'],
+                                    template['links'][0]['href'])
+
+    @decorators.idempotent_id('89aea2bf-c094-445f-b869-9fd56d1dfe5a')
+    def test_list_with_limit(self):
+        for i in range(2):
+            name = _get_random_trait()
+            self.create_deploy_template(name, steps=self.steps)
+
+        _, body = self.client.list_deploy_templates(limit=3)
+
+        next_marker = body['deploy_templates'][-1]['uuid']
+        self.assertIn(next_marker, body['next'])
+
+    @decorators.idempotent_id('c09c917e-c4d2-4148-9b6a-459bd126ed7c')
+    def test_list_deploy_templates_detail(self):
+        uuids = [
+            self.create_deploy_template(_get_random_trait(), steps=self.steps)
+            [1]['uuid'] for i in range(0, 5)]
+
+        _, body = self.client.list_deploy_templates(detail=True)
+
+        templates_dict = dict((template['uuid'], template)
+                              for template in body['deploy_templates']
+                              if template['uuid'] in uuids)
+
+        for uuid in uuids:
+            self.assertIn(uuid, templates_dict)
+            template = templates_dict[uuid]
+            self.assertIn('name', template)
+            self.assertEqual(self.steps, template['steps'])
+            self.assertIn('uuid', template)
+            self.assertEqual({}, template['extra'])
+            # Verify self link.
+            self.validate_self_link('deploy_templates', template['uuid'],
+                                    template['links'][0]['href'])
+
+    @decorators.idempotent_id('a6cf1ade-e19a-41e2-b151-13ecf0d8f08c')
+    def test_update_deploy_template_replace(self):
+        new_name = _get_random_trait()
+        new_steps = [{
+            'interface': 'raid',
+            'step': 'create_configuration',
+            'args': {},
+            'priority': 10,
+        }]
+
+        patch = [{'path': '/name', 'op': 'replace', 'value': new_name},
+                 {'path': '/steps', 'op': 'replace', 'value': new_steps}]
+
+        self.client.update_deploy_template(self.template['uuid'], patch)
+
+        _, body = self.client.show_deploy_template(self.template['uuid'])
+        self.assertEqual(new_name, body['name'])
+        self.assertEqual(new_steps, body['steps'])
+
+    @decorators.idempotent_id('bb168c63-452b-4065-9a81-77853ca9540a')
+    def test_update_deploy_template_add(self):
+        new_steps = [
+            {
+                'interface': 'bios',
+                'step': 'cache_bios_settings',
+                'args': {},
+                'priority': 20
+            },
+            {
+                'interface': 'bios',
+                'step': 'factory_reset',
+                'args': {},
+                'priority': 30
+            },
+        ]
+
+        patch = [{'path': '/steps/1', 'op': 'add', 'value': new_steps[0]},
+                 {'path': '/steps/2', 'op': 'add', 'value': new_steps[1]}]
+
+        self.client.update_deploy_template(self.template['uuid'], patch)
+
+        _, body = self.client.show_deploy_template(self.template['uuid'])
+        self.assertEqual(self.steps + new_steps, body['steps'])
+
+    @decorators.idempotent_id('2aa204a2-1d50-48fd-8b76-d2ed15586d50')
+    def test_update_deploy_template_mixed_ops(self):
+        new_name = _get_random_trait()
+        new_steps = [
+            {
+                'interface': 'bios',
+                'step': 'apply_configuration',
+                'args': {},
+                'priority': 20
+            },
+            {
+                'interface': 'bios',
+                'step': 'apply_configuration',
+                'args': {},
+                'priority': 30
+            },
+        ]
+
+        patch = [{'path': '/name', 'op': 'replace', 'value': new_name},
+                 {'path': '/steps/0', 'op': 'replace', 'value': new_steps[0]},
+                 {'path': '/steps/0', 'op': 'remove'},
+                 {'path': '/steps/0', 'op': 'add', 'value': new_steps[1]}]
+
+        self.client.update_deploy_template(self.template['uuid'], patch)
+
+        _, body = self.client.show_deploy_template(self.template['uuid'])
+        self.assertEqual(new_name, body['name'])
+        self.assertEqual([new_steps[1]], body['steps'])
+
+
+class TestDeployTemplatesOldAPI(base.BaseBaremetalTest):
+    """Negative tests for deploy templates using an old API version."""
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('e9481a0d-23e0-4757-bc11-c3c9ab9d3839')
+    def test_create_deploy_template_old_api(self):
+        # With deploy templates support, ironic returns 404. Without, 405.
+        self.assertRaises((lib_exc.NotFound, lib_exc.UnexpectedResponseCode),
+                          self.create_deploy_template,
+                          name=_get_random_trait(), steps=EXAMPLE_STEPS)
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('0d3af2aa-ba53-4c8a-92d4-91f9b4179fe7')
+    def test_update_deploy_template_old_api(self):
+        patch = [{'path': '/name', 'op': 'replace',
+                  'value': _get_random_trait()}]
+
+        # With deploy templates support, ironic returns 404. Without, 405.
+        self.assertRaises((lib_exc.NotFound, lib_exc.UnexpectedResponseCode),
+                          self.client.update_deploy_template,
+                          _get_random_trait(), patch)
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('1646b1e5-ab81-45a8-9ea0-30444a4dcaa2')
+    def test_delete_deploy_template_old_api(self):
+        # With deploy templates support, ironic returns 404. Without, 405.
+        self.assertRaises((lib_exc.NotFound, lib_exc.UnexpectedResponseCode),
+                          self.client.delete_deploy_template,
+                          _get_random_trait())
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('819480ac-f36a-4402-b1d5-504d7cf55b1f')
+    def test_list_deploy_templates_old_api(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.client.list_deploy_templates)
+
+
+class TestDeployTemplatesNegative(base.BaseBaremetalTest):
+    """Negative tests for deploy templates."""
+
+    min_microversion = '1.55'
+
+    def setUp(self):
+        super(TestDeployTemplatesNegative, self).setUp()
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture(
+                self.min_microversion)
+        )
+        self.steps = EXAMPLE_STEPS
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('a4085c08-e718-4c2f-a796-0e115b659243')
+    def test_create_deploy_template_invalid_name(self):
+        name = 'invalid-name'
+        self.assertRaises(lib_exc.BadRequest,
+                          self.create_deploy_template, name=name,
+                          steps=self.steps)
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('6390acc4-9490-4b23-8b4c-41888a78c9b7')
+    def test_create_deploy_template_duplicated_deploy_template_name(self):
+        name = _get_random_trait()
+        self.create_deploy_template(name=name, steps=self.steps)
+        self.assertRaises(lib_exc.Conflict, self.create_deploy_template,
+                          name=name, steps=self.steps)
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('ed3f0cec-13e8-4175-9fdb-d129e7b7fe10')
+    def test_create_deploy_template_no_mandatory_field_name(self):
+        self.assertRaises(lib_exc.BadRequest, self.create_deploy_template,
+                          name=None, steps=self.steps)
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('af5dd0df-d903-463f-9535-9e4e9d6fd576')
+    def test_create_deploy_template_no_mandatory_field_steps(self):
+        self.assertRaises(lib_exc.BadRequest, self.create_deploy_template,
+                          name=_get_random_trait())
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('cbd33bc5-7602-40b7-943e-3e92217567a3')
+    def test_create_deploy_template_malformed_steps(self):
+        steps = {'key': 'value'}
+        self.assertRaises(lib_exc.BadRequest, self.create_deploy_template,
+                          name=_get_random_trait(), steps=steps)
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('2a562fca-f377-4a6e-b332-37ee82d3a983')
+    def test_create_deploy_template_malformed_deploy_template_uuid(self):
+        uuid = 'malformed:uuid'
+        self.assertRaises(lib_exc.BadRequest, self.create_deploy_template,
+                          name=_get_random_trait(), steps=self.steps,
+                          uuid=uuid)
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('2c006994-88ca-43b7-b605-897d479229d9')
+    def test_show_deploy_template_nonexistent(self):
+        self.assertRaises(lib_exc.NotFound, self.client.show_deploy_template,
+                          data_utils.rand_uuid())
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('5a815f37-f015-4d68-9b22-099504f74805')
+    def test_update_deploy_template_remove_mandatory_field_steps(self):
+        name = _get_random_trait()
+        _, template = self.create_deploy_template(name=name, steps=self.steps)
+
+        # Removing one item from the collection
+        self.assertRaises(lib_exc.BadRequest,
+                          self.client.update_deploy_template,
+                          template['uuid'],
+                          [{'path': '/steps/0', 'op': 'remove'}])
+
+        # Removing the collection
+        self.assertRaises(lib_exc.BadRequest,
+                          self.client.update_deploy_template,
+                          template['uuid'],
+                          [{'path': '/steps', 'op': 'remove'}])
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('ee852ebb-a601-4593-9d59-063fcbc8f964')
+    def test_update_deploy_template_remove_mandatory_field_name(self):
+        name = _get_random_trait()
+        _, template = self.create_deploy_template(name=name, steps=self.steps)
+        self.assertRaises(lib_exc.BadRequest,
+                          self.client.update_deploy_template,
+                          template['uuid'],
+                          [{'path': '/name', 'op': 'remove'}])
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('e59bf38d-272f-4490-b21e-9db217f11378')
+    def test_update_deploy_template_replace_empty_name(self):
+        name = _get_random_trait()
+        _, template = self.create_deploy_template(name=name, steps=self.steps)
+        self.assertRaises(lib_exc.BadRequest,
+                          self.client.update_deploy_template,
+                          template['uuid'],
+                          [{'path': '/name', 'op': 'replace', 'value': ''}])
diff --git a/ironic_tempest_plugin/tests/api/admin/test_nodes.py b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
index 31a9d61..2709905 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_nodes.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_nodes.py
@@ -175,6 +175,11 @@
         _, loaded_node = self.client.show_node(self.node['uuid'])
         self.assertNotIn('conductor', loaded_node)
 
+    @decorators.idempotent_id('5e7f4c54-8216-42d3-83cc-7bd776ffd16f')
+    def test_description_hidden(self):
+        _, loaded_node = self.client.show_node(self.node['uuid'])
+        self.assertNotIn('description', loaded_node)
+
 
 class TestNodesResourceClass(base.BaseBaremetalTest):
 
@@ -975,3 +980,35 @@
         _, nodes = self.client.list_nodes(conductor=hostname)
         self.assertIn(self.node['uuid'],
                       [n['uuid'] for n in nodes['nodes']])
+
+
+class TestNodeDescription(base.BaseBaremetalTest):
+    """Tests for the description field."""
+
+    min_microversion = '1.51'
+
+    def setUp(self):
+        super(TestNodeDescription, self).setUp()
+
+        _, self.chassis = self.create_chassis()
+        _, self.node = self.create_node(self.chassis['uuid'])
+
+    @decorators.idempotent_id('66d0da49-e5ac-4f49-b065-9d2207d8a3af')
+    def test_description_exposed(self):
+        _, loaded_node = self.client.show_node(self.node['uuid'])
+        self.assertIn('description', loaded_node)
+
+    @decorators.idempotent_id('85b4a4b5-37e5-4b60-8dc7-f5a26dfa78a3')
+    def test_node_description_set_unset(self):
+        self.client.update_node(self.node['uuid'], description='meow')
+        _, self.node = self.client.show_node(self.node['uuid'])
+        self.assertEqual('meow', self.node['description'])
+
+        self.client.update_node(self.node['uuid'], description=None)
+        _, self.node = self.client.show_node(self.node['uuid'])
+        self.assertIsNone(self.node['description'])
+
+    @decorators.idempotent_id('3d649bb3-a58b-4b9e-8dfa-41ab634b1153')
+    def test_create_node_with_description(self):
+        _, body = self.create_node(self.chassis['uuid'], description='meow')
+        self.assertEqual('meow', body['description'])
diff --git a/ironic_tempest_plugin/tests/api/admin/test_ports.py b/ironic_tempest_plugin/tests/api/admin/test_ports.py
index a4aea4f..89d86f1 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_ports.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_ports.py
@@ -105,6 +105,9 @@
 
     @decorators.idempotent_id('324a910e-2f80-4258-9087-062b5ae06240')
     def test_list_with_limit(self):
+        for i in range(2):
+            self.create_port(self.node['uuid'], data_utils.rand_mac_address())
+
         _, body = self.client.list_ports(limit=3)
 
         next_marker = body['ports'][-1]['uuid']
diff --git a/ironic_tempest_plugin/tests/api/admin/test_volume_connector.py b/ironic_tempest_plugin/tests/api/admin/test_volume_connector.py
index 0b936a2..0be12bf 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_volume_connector.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_volume_connector.py
@@ -107,6 +107,12 @@
     @decorators.idempotent_id('1d0459ad-01c0-46db-b930-7301bc2a3c98')
     def test_list_with_limit(self):
         """List volume connectors with limit."""
+        for i in range(2):
+            _, self.volume_connector = self.create_volume_connector(
+                self.node['uuid'], type='iqn',
+                connector_id=data_utils.rand_name('connector_id'),
+                extra=self.extra)
+
         _, body = self.client.list_volume_connectors(limit=3)
 
         next_marker = body['connectors'][-1]['uuid']
diff --git a/ironic_tempest_plugin/tests/api/admin/test_volume_target.py b/ironic_tempest_plugin/tests/api/admin/test_volume_target.py
index 731467c..fa6990f 100644
--- a/ironic_tempest_plugin/tests/api/admin/test_volume_target.py
+++ b/ironic_tempest_plugin/tests/api/admin/test_volume_target.py
@@ -104,6 +104,14 @@
     @decorators.idempotent_id('9da25447-0370-4b33-9c1f-d4503f5950ae')
     def test_list_with_limit(self):
         """List volume targets with limit."""
+        for i in range(2):
+            _, self.volume_target = self.create_volume_target(
+                self.node['uuid'],
+                volume_type=data_utils.rand_name('volume_type'),
+                volume_id=data_utils.rand_name('volume_id'),
+                boot_index=11 + i,
+                extra=self.extra)
+
         _, body = self.client.list_volume_targets(limit=3)
 
         next_marker = body['targets'][-1]['uuid']
diff --git a/ironic_tempest_plugin/tests/scenario/introspection_manager.py b/ironic_tempest_plugin/tests/scenario/introspection_manager.py
index 21b9911..c45f285 100644
--- a/ironic_tempest_plugin/tests/scenario/introspection_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/introspection_manager.py
@@ -10,10 +10,11 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import json
 import os
 import time
 
+from oslo_serialization import jsonutils as json
+
 import six
 import tempest
 from tempest import config
@@ -112,7 +113,7 @@
         self.introspection_client.purge_rules()
 
     def rule_import(self, rule_path):
-        with open(rule_path, 'r') as fp:
+        with open(rule_path, 'rb') as fp:
             rules = json.load(fp)
         self.introspection_client.create_rules(rules)
 
diff --git a/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py b/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
index 2ec91f1..c44844f 100644
--- a/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
+++ b/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
@@ -37,6 +37,13 @@
 
         discovered_node = self._get_discovery_node()
         self.node_info = self._get_node_info(discovered_node)
+        self.default_driver = (
+            CONF.baremetal_introspection.auto_discovery_default_driver
+        )
+        self.expected_driver = (
+            CONF.baremetal_introspection.auto_discovery_target_driver
+            or CONF.baremetal_introspection.auto_discovery_default_driver
+        )
 
         rule = {
             "description": "Auto-discovery rule",
@@ -51,14 +58,26 @@
                  "path": "/name",
                  "value": self.node_info['name']},
             ],
-            # This flag must be automatically set by the auto-discovery process
             "conditions": [
+                # This flag must be automatically set by the auto-discovery
+                # process.
                 {"op": "eq",
                  "field": "data://auto_discovered",
-                 "value": True}
+                 "value": True},
+                # Making sure the initial driver matches the expected.
+                {"op": "eq",
+                 "field": "node://driver",
+                 "value": self.default_driver},
             ]
         }
 
+        if self.expected_driver != self.default_driver:
+            rule['actions'].append({
+                'action': 'set-attribute',
+                'path': '/driver',
+                'value': self.expected_driver
+            })
+
         self.rule_import_from_dict(rule)
         self.addCleanup(self.rule_purge)
 
@@ -141,7 +160,5 @@
             self.verify_node_introspection_data(inspected_node)
         self.assertEqual(ProvisionStates.ENROLL,
                          inspected_node['provision_state'])
-        self.assertEqual(
-            CONF.baremetal_introspection.auto_discovery_default_driver,
-            inspected_node['driver'])
+        self.assertEqual(self.expected_driver, inspected_node['driver'])
         self.assertEqual('yes', inspected_node['extra']['discovered'])
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index e32f918..2c6dce8 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -6,44 +6,49 @@
     check:
       jobs:
         # NOTE(dtantsur): keep N-3 and older non-voting for these jobs.
-        - ironic-dsvm-standalone
+        - ironic-standalone
         - ironic-dsvm-standalone-rocky
-        - ironic-dsvm-standalone-queens
+        # NOTE(iurygregory): we want to make voting again
+        - ironic-dsvm-standalone-queens:
+            voting: false
         - ironic-dsvm-standalone-pike:
             voting: false
-        - ironic-tempest-dsvm-functional-python3
+        - ironic-tempest-functional-python3
         - ironic-tempest-dsvm-functional-python3-rocky
-        - ironic-tempest-dsvm-ironic-inspector
+        - ironic-inspector-tempest
         - ironic-tempest-dsvm-ironic-inspector-rocky
         - ironic-tempest-dsvm-ironic-inspector-queens
         - ironic-tempest-dsvm-ironic-inspector-pike:
             voting: false
         # NOTE(dtantsur): these jobs cover rarely changed tests and are quite
         # unstable, so keep them non-voting on stable branches.
-        - ironic-tempest-dsvm-ipa-wholedisk-direct-tinyipa-multinode
+        # NOTE(iurygregory):  debug rocky and queens
+        # since before was working, pike was broken before
+        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
         - ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode-rocky:
             voting: false
         - ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode-queens:
             voting: false
         - ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode-pike:
             voting: false
-        - ironic-inspector-tempest-dsvm-discovery
+        - ironic-inspector-tempest-discovery
         - ironic-inspector-tempest-dsvm-discovery-rocky:
             voting: false
         - ironic-inspector-tempest-dsvm-discovery-queens:
             voting: false
+        # NOTE(iurygregory): we may want to debug why its failling since before was green
         - ironic-inspector-tempest-dsvm-discovery-pike:
             voting: false
     gate:
       queue: ironic
       jobs:
-        - ironic-dsvm-standalone
+        # NOTE(iurygregory): re add ironic-dsvm-standalone-queens when is green again
+        - ironic-standalone
         - ironic-dsvm-standalone-rocky
-        - ironic-dsvm-standalone-queens
-        - ironic-tempest-dsvm-functional-python3
+        - ironic-tempest-functional-python3
         - ironic-tempest-dsvm-functional-python3-rocky
-        - ironic-tempest-dsvm-ironic-inspector
+        - ironic-inspector-tempest
         - ironic-tempest-dsvm-ironic-inspector-rocky
         - ironic-tempest-dsvm-ironic-inspector-queens
-        - ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode
-        - ironic-inspector-tempest-dsvm-discovery
+        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
+        - ironic-inspector-tempest-discovery
diff --git a/zuul.d/stable-jobs.yaml b/zuul.d/stable-jobs.yaml
index 4413ba7..e4cf069 100644
--- a/zuul.d/stable-jobs.yaml
+++ b/zuul.d/stable-jobs.yaml
@@ -1,65 +1,136 @@
----
 - job:
     name: ironic-dsvm-standalone-rocky
-    parent: ironic-dsvm-standalone
-    override-checkout: stable/rocky
+    parent: ironic-standalone
+    override-branch: stable/rocky
+    nodeset: openstack-single-node-xenial
 
 - job:
     name: ironic-dsvm-standalone-queens
-    parent: ironic-dsvm-standalone
-    override-checkout: stable/queens
+    parent: ironic-standalone
+    override-branch: stable/queens
+    nodeset: openstack-single-node-xenial
+    vars:
+      devstack_localrc:
+        FIXED_NETWORK_SIZE: 4096
+        EBTABLES_RACE_FIX: True
+        IRONIC_USE_MOD_WSGI: True
+
 
 - job:
     name: ironic-dsvm-standalone-pike
-    parent: ironic-dsvm-standalone
-    override-checkout: stable/pike
+    parent: ironic-standalone
+    override-branch: stable/pike
+    nodeset: openstack-single-node-xenial
 
 - job:
     name: ironic-tempest-dsvm-functional-python3-rocky
-    parent: ironic-tempest-dsvm-functional-python3
-    override-checkout: stable/rocky
+    parent: ironic-tempest-functional-python3
+    override-branch: stable/rocky
+    nodeset: openstack-single-node-xenial
+    vars:
+      devstack_localrc:
+        IRONIC_RPC_TRANSPORT: ""
+      devstack_services:
+        rabbit: True
 
 - job:
     name: ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode-rocky
-    parent: ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode
-    override-checkout: stable/rocky
+    parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
+    override-branch: stable/rocky
+    nodeset: openstack-two-node-xenial
+    vars:
+      devstack_localrc:
+        IRONIC_DEFAULT_BOOT_OPTION: netboot
+        FIXED_NETWORK_SIZE: 4096
+        IRONIC_DEFAULT_RESCUE_INTERFACE: agent
+        EBTABLES_RACE_FIX: True
+        PUBLIC_BRIDGE: br_ironic_vxlan
+        OVS_BRIDGE_MAPPINGS: 'mynetwork:brbm,public:br_ironic_vxlan'
+    group-vars:
+      subnode:
+        devstack_localrc:
+          OVS_BRIDGE_MAPPINGS: 'mynetwork:sub1brbm,public:br_ironic_vxlan'
 
 - job:
     name: ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode-queens
-    parent: ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode
-    override-checkout: stable/queens
+    parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
+    override-branch: stable/queens
+    nodeset: openstack-two-node-xenial
+    vars:
+      devstack_localrc:
+        IRONIC_DEFAULT_BOOT_OPTION: netboot
+        FIXED_NETWORK_SIZE: 4096
+        IRONIC_DEFAULT_RESCUE_INTERFACE: agent
+        EBTABLES_RACE_FIX: True
+        PUBLIC_BRIDGE: br_ironic_vxlan
+        OVS_BRIDGE_MAPPINGS: 'mynetwork:brbm,public:br_ironic_vxlan'
+    group-vars:
+      subnode:
+        devstack_localrc:
+          OVS_BRIDGE_MAPPINGS: 'mynetwork:sub1brbm,public:br_ironic_vxlan'
 
 - job:
     name: ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode-pike
-    parent: ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode
-    override-checkout: stable/pike
+    parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
+    override-branch: stable/pike
+    nodeset: openstack-two-node-xenial
+    vars:
+      devstack_localrc:
+        IRONIC_DEFAULT_BOOT_OPTION: netboot
+        FIXED_NETWORK_SIZE: 4096
+        IRONIC_DEFAULT_RESCUE_INTERFACE: agent
+        EBTABLES_RACE_FIX: True
+        PUBLIC_BRIDGE: br_ironic_vxlan
+        OVS_BRIDGE_MAPPINGS: 'mynetwork:brbm,public:br_ironic_vxlan'
+    group-vars:
+      subnode:
+        devstack_localrc:
+          OVS_BRIDGE_MAPPINGS: 'mynetwork:sub1brbm,public:br_ironic_vxlan'
 
 - job:
     name: ironic-tempest-dsvm-ironic-inspector-rocky
-    parent: ironic-tempest-dsvm-ironic-inspector
-    override-checkout: stable/rocky
+    parent: ironic-inspector-tempest
+    override-branch: stable/rocky
+    nodeset: openstack-single-node-xenial
+    vars:
+      devstack_localrc:
+        FIXED_NETWORK_SIZE: 4096
+        EBTABLES_RACE_FIX: True
 
 - job:
     name: ironic-tempest-dsvm-ironic-inspector-queens
-    parent: ironic-tempest-dsvm-ironic-inspector
-    override-checkout: stable/queens
+    parent: ironic-inspector-tempest
+    override-branch: stable/queens
+    nodeset: openstack-single-node-xenial
+    vars:
+      devstack_localrc:
+        FIXED_NETWORK_SIZE: 4096
+        EBTABLES_RACE_FIX: True
 
 - job:
     name: ironic-tempest-dsvm-ironic-inspector-pike
-    parent: ironic-tempest-dsvm-ironic-inspector
-    override-checkout: stable/pike
+    parent: ironic-inspector-tempest
+    override-branch: stable/pike
+    nodeset: openstack-single-node-xenial
+    vars:
+      devstack_localrc:
+        FIXED_NETWORK_SIZE: 4096
+        EBTABLES_RACE_FIX: True
 
 - job:
     name: ironic-inspector-tempest-dsvm-discovery-rocky
-    parent: ironic-inspector-tempest-dsvm-discovery
-    override-checkout: stable/rocky
+    parent: ironic-inspector-tempest-discovery
+    override-branch: stable/rocky
+    nodeset: openstack-single-node-xenial
 
 - job:
     name: ironic-inspector-tempest-dsvm-discovery-queens
-    parent: ironic-inspector-tempest-dsvm-discovery
-    override-checkout: stable/queens
+    parent: ironic-inspector-tempest-discovery
+    override-branch: stable/queens
+    nodeset: openstack-single-node-xenial
 
 - job:
     name: ironic-inspector-tempest-dsvm-discovery-pike
-    parent: ironic-inspector-tempest-dsvm-discovery
-    override-checkout: stable/pike
+    parent: ironic-inspector-tempest-discovery
+    override-branch: stable/pike
+    nodeset: openstack-single-node-xenial