From 2f6d37129bb6197a091f0d4a5f1ff96ebac42396 Mon Sep 17 00:00:00 2001 From: Yuiko Takada Date: Thu, 17 Dec 2015 15:56:42 +0900 Subject: [PATCH] Add support for API microversions in Tempest tests This adds support for testing Ironic API microversions, specified as an additional 'X-OpenStack-Ironic-API-Version' header. This change also adds tests for Ironic API /v1/nodes/(node_ident)/states/* endpoint for microversions that were changing state machine. Co-Authored-By: Vladyslav Drok Change-Id: Ibf0c73aa6795aaa52e945fd6baa821de20a599e7 --- ironic_tempest_plugin/clients.py | 9 +- ironic_tempest_plugin/config.py | 16 ++ .../services/baremetal/base.py | 22 +++ .../baremetal/v1/json/baremetal_client.py | 23 +++ .../api/admin/api_microversion_fixture.py | 29 ++++ ironic_tempest_plugin/tests/api/admin/base.py | 33 +++- .../tests/api/admin/test_nodestates.py | 146 +++++++++++++++++- 7 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 ironic_tempest_plugin/tests/api/admin/api_microversion_fixture.py diff --git a/ironic_tempest_plugin/clients.py b/ironic_tempest_plugin/clients.py index 70ce1340a6..2cb7c73977 100644 --- a/ironic_tempest_plugin/clients.py +++ b/ironic_tempest_plugin/clients.py @@ -28,8 +28,13 @@ ADMIN_CREDS = common_creds.get_configured_credentials('identity_admin') class Manager(clients.Manager): def __init__(self, credentials=ADMIN_CREDS, - service=None, - api_microversions=None): + service=None): + """Initialization of Manager class. + + Setup service client and make it available for test cases. + :param credentials: type Credentials or TestResources + :param service: service name + """ super(Manager, self).__init__(credentials, service) self.baremetal_client = BaremetalClient( self.auth_provider, diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py index 1f9ba51a5a..36d59dfffb 100644 --- a/ironic_tempest_plugin/config.py +++ b/ironic_tempest_plugin/config.py @@ -66,4 +66,20 @@ BaremetalGroup = [ # help="Timeout for unprovisioning an Ironic node. " # "Takes longer since Kilo as Ironic performs an extra " # "step in Node cleaning.") + cfg.StrOpt('min_microversion', + default=None, + help="Lower version of the test target microversion range. " + "The format is 'X.Y', where 'X' and 'Y' are int values. " + "Tempest selects tests based on the range between " + "min_microversion and max_microversion. " + "If both values are None, Tempest avoids tests which " + "require a microversion."), + cfg.StrOpt('max_microversion', + default='latest', + help="Upper version of the test target microversion range. " + "The format is 'X.Y', where 'X' and 'Y' are int values. " + "Tempest selects tests based on the range between " + "min_microversion and max_microversion. " + "If both values are None, Tempest avoids tests which " + "require a microversion."), ] diff --git a/ironic_tempest_plugin/services/baremetal/base.py b/ironic_tempest_plugin/services/baremetal/base.py index edb9ecbc77..b7a9c3291d 100644 --- a/ironic_tempest_plugin/services/baremetal/base.py +++ b/ironic_tempest_plugin/services/baremetal/base.py @@ -15,8 +15,11 @@ import functools from oslo_serialization import jsonutils as json import six from six.moves.urllib import parse as urllib +from tempest.lib.common import api_version_utils from tempest.lib.common import rest_client +BAREMETAL_MICROVERSION = None + def handle_errors(f): """A decorator that allows to ignore certain types of errors.""" @@ -41,8 +44,27 @@ def handle_errors(f): class BaremetalClient(rest_client.RestClient): """Base Tempest REST client for Ironic API.""" + api_microversion_header_name = 'X-OpenStack-Ironic-API-Version' uri_prefix = '' + def get_headers(self): + headers = super(BaremetalClient, self).get_headers() + if BAREMETAL_MICROVERSION: + headers[self.api_microversion_header_name] = BAREMETAL_MICROVERSION + return headers + + def request(self, method, url, extra_headers=False, headers=None, + body=None): + resp, resp_body = super(BaremetalClient, self).request( + method, url, extra_headers, headers, body) + if (BAREMETAL_MICROVERSION and + BAREMETAL_MICROVERSION != api_version_utils.LATEST_MICROVERSION): + api_version_utils.assert_version_header_matches_request( + self.api_microversion_header_name, + BAREMETAL_MICROVERSION, + resp) + return resp, resp_body + def serialize(self, object_dict): """Serialize an Ironic object.""" 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 cea449a3ee..1863fc2509 100644 --- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py +++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py @@ -264,6 +264,29 @@ class BaremetalClient(base.BaremetalClient): return self._put_request('nodes/%s/states/power' % node_uuid, target) + @base.handle_errors + def set_node_provision_state(self, node_uuid, state, configdrive=None): + """Set provision state of the specified node. + + :param node_uuid: The unique identifier of the node. + :state: desired state to set + (active/rebuild/deleted/inspect/manage/provide). + :config_drive: A gzipped, base64-encoded configuration drive string. + """ + data = {'target': state, 'configdrive': configdrive} + return self._put_request('nodes/%s/states/provision' % node_uuid, + data) + + @base.handle_errors + def set_node_raid_config(self, node_uuid, target_raid_config): + """Set raid config of the specified node. + + :param node_uuid: The unique identifier of the node. + :target_raid_config: desired RAID configuration of the node. + """ + return self._put_request('nodes/%s/states/raid' % node_uuid, + target_raid_config) + @base.handle_errors def validate_driver_interface(self, node_uuid): """Get all driver interfaces of a specific node. diff --git a/ironic_tempest_plugin/tests/api/admin/api_microversion_fixture.py b/ironic_tempest_plugin/tests/api/admin/api_microversion_fixture.py new file mode 100644 index 0000000000..9dd643c41b --- /dev/null +++ b/ironic_tempest_plugin/tests/api/admin/api_microversion_fixture.py @@ -0,0 +1,29 @@ +# 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 fixtures + +from ironic_tempest_plugin.services.baremetal import base + + +class APIMicroversionFixture(fixtures.Fixture): + + def __init__(self, baremetal_microversion): + self.baremetal_microversion = baremetal_microversion + + def _setUp(self): + super(APIMicroversionFixture, self)._setUp() + base.BAREMETAL_MICROVERSION = self.baremetal_microversion + self.addCleanup(self._reset_compute_microversion) + + def _reset_compute_microversion(self): + base.BAREMETAL_MICROVERSION = None diff --git a/ironic_tempest_plugin/tests/api/admin/base.py b/ironic_tempest_plugin/tests/api/admin/base.py index 61270c7395..124fc3315e 100644 --- a/ironic_tempest_plugin/tests/api/admin/base.py +++ b/ironic_tempest_plugin/tests/api/admin/base.py @@ -13,11 +13,13 @@ import functools from tempest import config +from tempest.lib.common import api_version_utils from tempest.lib.common.utils import data_utils from tempest.lib import exceptions as lib_exc from tempest import test from ironic_tempest_plugin import clients +from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture CONF = config.CONF @@ -50,7 +52,8 @@ def creates(resource): return decorator -class BaseBaremetalTest(test.BaseTestCase): +class BaseBaremetalTest(api_version_utils.BaseMicroversionTest, + test.BaseTestCase): """Base class for Baremetal API tests.""" credentials = ['admin'] @@ -64,6 +67,23 @@ class BaseBaremetalTest(test.BaseTestCase): (cls.__name__, CONF.baremetal.driver)) raise cls.skipException(skip_msg) + cfg_min_version = CONF.baremetal.min_microversion + cfg_max_version = CONF.baremetal.max_microversion + api_version_utils.check_skip_with_microversion(cls.min_microversion, + cls.max_microversion, + cfg_min_version, + cfg_max_version) + + @classmethod + def setup_credentials(cls): + cls.request_microversion = ( + api_version_utils.select_request_microversion( + cls.min_microversion, + CONF.baremetal.min_microversion)) + cls.services_microversion = { + CONF.baremetal.catalog_type: cls.request_microversion} + super(BaseBaremetalTest, cls).setup_credentials() + @classmethod def setup_clients(cls): super(BaseBaremetalTest, cls).setup_clients() @@ -72,9 +92,13 @@ class BaseBaremetalTest(test.BaseTestCase): @classmethod def resource_setup(cls): super(BaseBaremetalTest, cls).resource_setup() - + cls.request_microversion = ( + api_version_utils.select_request_microversion( + cls.min_microversion, + CONF.baremetal.min_microversion)) cls.driver = CONF.baremetal.driver cls.power_timeout = CONF.baremetal.power_timeout + cls.unprovision_timeout = CONF.baremetal.unprovision_timeout cls.created_objects = {} for resource in RESOURCE_TYPES: cls.created_objects[resource] = set() @@ -92,6 +116,11 @@ class BaseBaremetalTest(test.BaseTestCase): finally: super(BaseBaremetalTest, cls).resource_cleanup() + def setUp(self): + super(BaseBaremetalTest, self).setUp() + self.useFixture(api_microversion_fixture.APIMicroversionFixture( + self.request_microversion)) + @classmethod @creates('chassis') def create_chassis(cls, description=None, expect_errors=False): diff --git a/ironic_tempest_plugin/tests/api/admin/test_nodestates.py b/ironic_tempest_plugin/tests/api/admin/test_nodestates.py index f418fcc72a..58ca016e73 100644 --- a/ironic_tempest_plugin/tests/api/admin/test_nodestates.py +++ b/ironic_tempest_plugin/tests/api/admin/test_nodestates.py @@ -16,17 +16,17 @@ from oslo_utils import timeutils from tempest.lib import exceptions from tempest import test +from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture from ironic_tempest_plugin.tests.api.admin import base -class TestNodeStates(base.BaseBaremetalTest): - """Tests for baremetal NodeStates.""" +class TestNodeStatesMixin(object): + """Mixin for for baremetal node states tests.""" @classmethod def resource_setup(cls): - super(TestNodeStates, cls).resource_setup() + super(TestNodeStatesMixin, cls).resource_setup() _, cls.chassis = cls.create_chassis() - _, cls.node = cls.create_node(cls.chassis['uuid']) def _validate_power_state(self, node_uuid, power_state): # Validate that power state is set within timeout @@ -42,11 +42,26 @@ class TestNodeStates(base.BaseBaremetalTest): 'the required time: %s sec.' % self.power_timeout) raise exceptions.TimeoutException(message) + def _validate_provision_state(self, node_uuid, target_state): + # Validate that provision state is set within timeout + start = timeutils.utcnow() + while timeutils.delta_seconds( + start, timeutils.utcnow()) < self.unprovision_timeout: + _, node = self.client.show_node(node_uuid) + if node['provision_state'] == target_state: + return + message = ('Failed to set provision state %(state)s within ' + 'the required time: %(timeout)s sec.', + {'state': target_state, + 'timeout': self.unprovision_timeout}) + raise exceptions.TimeoutException(message) + @test.idempotent_id('cd8afa5e-3f57-4e43-8185-beb83d3c9015') def test_list_nodestates(self): - _, nodestates = self.client.list_nodestates(self.node['uuid']) + _, node = self.create_node(self.chassis['uuid']) + _, nodestates = self.client.list_nodestates(node['uuid']) for key in nodestates: - self.assertEqual(nodestates[key], self.node[key]) + self.assertEqual(nodestates[key], node[key]) @test.idempotent_id('fc5b9320-0c98-4e5a-8848-877fe5a0322c') def test_set_node_power_state(self): @@ -57,3 +72,122 @@ class TestNodeStates(base.BaseBaremetalTest): self.client.set_node_power_state(node['uuid'], state) # Check power state after state is set self._validate_power_state(node['uuid'], state) + + +class TestNodeStatesV1_1(TestNodeStatesMixin, base.BaseBaremetalTest): + + @test.idempotent_id('ccb8fca9-2ba0-480c-a037-34c3bd09dc74') + def test_set_node_provision_state(self): + _, node = self.create_node(self.chassis['uuid']) + # Nodes appear in NONE state by default until v1.1 + self.assertEqual(None, node['provision_state']) + provision_states_list = ['active', 'deleted'] + target_states_list = ['active', None] + 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) + + +class TestNodeStatesV1_2(TestNodeStatesMixin, base.BaseBaremetalTest): + + def setUp(self): + super(TestNodeStatesV1_2, self).setUp() + self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.2')) + + @test.idempotent_id('9c414984-f3b6-4b3d-81da-93b60d4662fb') + def test_set_node_provision_state(self): + _, node = self.create_node(self.chassis['uuid']) + # Nodes appear in AVAILABLE state by default from v1.2 to v1.10 + self.assertEqual('available', 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._validate_provision_state(node['uuid'], target_state) + + +class TestNodeStatesV1_4(TestNodeStatesMixin, base.BaseBaremetalTest): + + def setUp(self): + super(TestNodeStatesV1_4, self).setUp() + self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.4')) + + @test.idempotent_id('3d606003-05ce-4b5a-964d-bdee382fafe9') + def test_set_node_provision_state(self): + _, node = self.create_node(self.chassis['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 + provision_states_list = [ + 'manage', 'provide', 'active', 'deleted'] + target_states_list = [ + '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) + + +class TestNodeStatesV1_6(TestNodeStatesMixin, base.BaseBaremetalTest): + + def setUp(self): + super(TestNodeStatesV1_6, self).setUp() + self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.6')) + + @test.idempotent_id('6c9ce4a3-713b-4c76-91af-18c48d01f1bb') + def test_set_node_provision_state(self): + _, node = self.create_node(self.chassis['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 + 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) + + +class TestNodeStatesV1_11(TestNodeStatesMixin, base.BaseBaremetalTest): + + def setUp(self): + super(TestNodeStatesV1_11, self).setUp() + self.useFixture( + api_microversion_fixture.APIMicroversionFixture('1.11') + ) + + @test.idempotent_id('31f53828-b83d-40c7-98e5-843e28a1b6b9') + def test_set_node_provision_state(self): + _, node = self.create_node(self.chassis['uuid']) + # Nodes appear in ENROLL state by default from v1.11 + self.assertEqual('enroll', 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) + + +class TestNodeStatesV1_12(TestNodeStatesMixin, base.BaseBaremetalTest): + + def setUp(self): + super(TestNodeStatesV1_12, self).setUp() + self.useFixture( + api_microversion_fixture.APIMicroversionFixture('1.12') + ) + + @test.idempotent_id('4427b1ca-8e79-4139-83d6-77dfac03e61e') + def test_set_node_raid_config(self): + _, node = self.create_node(self.chassis['uuid']) + target_raid_config = {'logical_disks': [{'size_gb': 100, + 'raid_level': '1'}]} + self.client.set_node_raid_config(node['uuid'], target_raid_config) + _, ret = self.client.show_node(node['uuid']) + self.assertEqual(target_raid_config, ret['target_raid_config'])