Allocation API: functional tests
This change adds tests on allocation API against fake nodes.
Change-Id: I9f750fe9088e4dda3d5d95cd8905101046ce71d1
Depends-On: https://review.openstack.org/636110
Story: #2004341
Task: #29411
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/services/baremetal/v1/json/baremetal_client.py b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
index f60361a..e6c02b2 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,11 @@
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 show_node(self, uuid, api_version=None):
"""Gets a specific node.
@@ -212,6 +217,23 @@
"""
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)
+
@base.handle_errors
def create_node(self, chassis_id=None, **kwargs):
"""Create a baremetal node with the specified parameters.
@@ -226,8 +248,9 @@
"""
node = {}
- if kwargs.get('resource_class'):
- node['resource_class'] = kwargs['resource_class']
+ for field in ('resource_class', 'name'):
+ if kwargs.get(field):
+ node[field] = kwargs[field]
node.update(
{'chassis_uuid': chassis_id,
@@ -761,3 +784,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/tests/api/admin/base.py b/ironic_tempest_plugin/tests/api/admin/base.py
index 6f8586f..f34ac68 100644
--- a/ironic_tempest_plugin/tests/api/admin/base.py
+++ b/ironic_tempest_plugin/tests/api/admin/base.py
@@ -118,6 +118,12 @@
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)
@@ -171,7 +177,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 +185,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 +193,7 @@
cpus=cpus, local_gb=local_gb,
memory_mb=memory_mb,
driver=cls.driver,
- resource_class=resource_class)
+ **kwargs)
return resp, body
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..1891a3b
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_allocations.py
@@ -0,0 +1,208 @@
+# 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 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 = 'x-small-%d' % random.randrange(1024)
+
+ _, 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')
+ self._assertExpected(body, loaded_body)
+
+ @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'])