From fcb0b68e7b9a42636faa5b7698f95d8e4d6a003b Mon Sep 17 00:00:00 2001 From: Andrew Kerr Date: Fri, 1 Apr 2016 16:01:34 -0400 Subject: [PATCH] Add tests for Cinder user messages v3 API This commit adds basic tests for the list/show/delete v3 APIs for Cinder's new user messages. Depends-On: Id8a4a700c1159be24b15056f401a2ea77804d0a0 Depends-On: I97caa6bfababf7d1cc714296ae66f77d22bf24ab Depends-On: I80c6a0c46c667291c6f7fe2a036717504c110314 Change-Id: I3d9b3fe288333721bf3b2c6c988949f2f253bfcc --- .../api/volume/api_microversion_fixture.py | 30 ++++++ tempest/api/volume/base.py | 4 + tempest/api/volume/v3/__init__.py | 0 tempest/api/volume/v3/admin/__init__.py | 0 .../api/volume/v3/admin/test_user_messages.py | 98 +++++++++++++++++++ tempest/api/volume/v3/base.py | 64 ++++++++++++ tempest/clients.py | 3 + tempest/config.py | 21 ++++ .../services/volume/base/base_v3_client.py | 46 +++++++++ tempest/services/volume/v3/__init__.py | 0 tempest/services/volume/v3/json/__init__.py | 0 .../volume/v3/json/messages_client.py | 59 +++++++++++ 12 files changed, 325 insertions(+) create mode 100644 tempest/api/volume/api_microversion_fixture.py create mode 100644 tempest/api/volume/v3/__init__.py create mode 100644 tempest/api/volume/v3/admin/__init__.py create mode 100644 tempest/api/volume/v3/admin/test_user_messages.py create mode 100644 tempest/api/volume/v3/base.py create mode 100644 tempest/services/volume/base/base_v3_client.py create mode 100644 tempest/services/volume/v3/__init__.py create mode 100644 tempest/services/volume/v3/json/__init__.py create mode 100644 tempest/services/volume/v3/json/messages_client.py diff --git a/tempest/api/volume/api_microversion_fixture.py b/tempest/api/volume/api_microversion_fixture.py new file mode 100644 index 0000000000..6817eaade6 --- /dev/null +++ b/tempest/api/volume/api_microversion_fixture.py @@ -0,0 +1,30 @@ +# +# 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 tempest.services.volume.base import base_v3_client + + +class APIMicroversionFixture(fixtures.Fixture): + + def __init__(self, volume_microversion): + self.volume_microversion = volume_microversion + + def _setUp(self): + super(APIMicroversionFixture, self)._setUp() + base_v3_client.VOLUME_MICROVERSION = self.volume_microversion + self.addCleanup(self._reset_volume_microversion) + + def _reset_volume_microversion(self): + base_v3_client.VOLUME_MICROVERSION = None diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py index cd21424de8..9010c8904a 100644 --- a/tempest/api/volume/base.py +++ b/tempest/api/volume/base.py @@ -45,6 +45,10 @@ class BaseVolumeTest(tempest.test.BaseTestCase): if not CONF.volume_feature_enabled.api_v2: msg = "Volume API v2 is disabled" raise cls.skipException(msg) + elif cls._api_version == 3: + if not CONF.volume_feature_enabled.api_v3: + msg = "Volume API v3 is disabled" + raise cls.skipException(msg) else: msg = ("Invalid Cinder API version (%s)" % cls._api_version) raise exceptions.InvalidConfiguration(message=msg) diff --git a/tempest/api/volume/v3/__init__.py b/tempest/api/volume/v3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/api/volume/v3/admin/__init__.py b/tempest/api/volume/v3/admin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/api/volume/v3/admin/test_user_messages.py b/tempest/api/volume/v3/admin/test_user_messages.py new file mode 100644 index 0000000000..19c37be96e --- /dev/null +++ b/tempest/api/volume/v3/admin/test_user_messages.py @@ -0,0 +1,98 @@ +# Copyright 2016 Andrew Kerr +# All Rights Reserved. +# +# 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 tempest.api.volume.v3 import base +from tempest.common.utils import data_utils +from tempest.common import waiters +from tempest import exceptions +from tempest import test + +MESSAGE_KEYS = [ + 'created_at', + 'event_id', + 'guaranteed_until', + 'id', + 'message_level', + 'request_id', + 'resource_type', + 'resource_uuid', + 'user_message', + 'links'] + + +class UserMessagesTest(base.VolumesV3AdminTest): + min_microversion = '3.3' + max_microversion = 'latest' + + def _delete_volume(self, volume_id): + self.volumes_client.delete_volume(volume_id) + self.volumes_client.wait_for_resource_deletion(volume_id) + + def _create_user_message(self): + """Trigger a 'no valid host' situation to generate a message.""" + bad_protocol = data_utils.rand_name('storage_protocol') + bad_vendor = data_utils.rand_name('vendor_name') + extra_specs = {'storage_protocol': bad_protocol, + 'vendor_name': bad_vendor} + vol_type_name = data_utils.rand_name('volume-type') + bogus_type = self.admin_volume_types_client.create_volume_type( + name=vol_type_name, + extra_specs=extra_specs)['volume_type'] + self.addCleanup(self.admin_volume_types_client.delete_volume_type, + bogus_type['id']) + params = {'volume_type': bogus_type['id']} + volume = self.volumes_client.create_volume(**params)['volume'] + self.addCleanup(self._delete_volume, volume['id']) + try: + waiters.wait_for_volume_status(self.volumes_client, volume['id'], + 'error') + except exceptions.VolumeBuildErrorException: + # Error state is expected and desired + pass + messages = self.messages_client.list_messages()['messages'] + message_id = None + for message in messages: + if message['resource_uuid'] == volume['id']: + message_id = message['id'] + break + self.assertIsNotNone(message_id, 'No user message generated for ' + 'volume %s' % volume['id']) + return message_id + + @test.idempotent_id('50f29e6e-f363-42e1-8ad1-f67ae7fd4d5a') + def test_list_messages(self): + self._create_user_message() + messages = self.messages_client.list_messages()['messages'] + self.assertIsInstance(messages, list) + for message in messages: + for key in MESSAGE_KEYS: + self.assertIn(key, message.keys(), + 'Missing expected key %s' % key) + + @test.idempotent_id('55a4a61e-c7b2-4ba0-a05d-b914bdef3070') + def test_show_message(self): + message_id = self._create_user_message() + self.addCleanup(self.messages_client.delete_message, message_id) + + message = self.messages_client.show_message(message_id)['message'] + + for key in MESSAGE_KEYS: + self.assertIn(key, message.keys(), 'Missing expected key %s' % key) + + @test.idempotent_id('c6eb6901-cdcc-490f-b735-4fe251842aed') + def test_delete_message(self): + message_id = self._create_user_message() + self.messages_client.delete_message(message_id) + self.messages_client.wait_for_resource_deletion(message_id) diff --git a/tempest/api/volume/v3/base.py b/tempest/api/volume/v3/base.py new file mode 100644 index 0000000000..c31c83cc63 --- /dev/null +++ b/tempest/api/volume/v3/base.py @@ -0,0 +1,64 @@ +# Copyright 2016 Andrew Kerr +# All Rights Reserved. +# +# 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 tempest.api.volume import api_microversion_fixture +from tempest.api.volume import base +from tempest import config +from tempest.lib.common import api_version_utils + +CONF = config.CONF + + +class VolumesV3Test(api_version_utils.BaseMicroversionTest, + base.BaseVolumeTest): + """Base test case class for all v3 Cinder API tests.""" + + _api_version = 3 + + @classmethod + def skip_checks(cls): + super(VolumesV3Test, cls).skip_checks() + api_version_utils.check_skip_with_microversion( + cls.min_microversion, cls.max_microversion, + CONF.volume.min_microversion, CONF.volume.max_microversion) + + @classmethod + def resource_setup(cls): + super(VolumesV3Test, cls).resource_setup() + cls.request_microversion = ( + api_version_utils.select_request_microversion( + cls.min_microversion, + CONF.volume.min_microversion)) + + @classmethod + def setup_clients(cls): + super(VolumesV3Test, cls).setup_clients() + cls.messages_client = cls.os.volume_messages_client + + def setUp(self): + super(VolumesV3Test, self).setUp() + self.useFixture(api_microversion_fixture.APIMicroversionFixture( + self.request_microversion)) + + +class VolumesV3AdminTest(VolumesV3Test): + """Base test case class for all v3 Volume Admin API tests.""" + + credentials = ['primary', 'admin'] + + @classmethod + def setup_clients(cls): + super(VolumesV3AdminTest, cls).setup_clients() + cls.admin_messages_client = cls.os_adm.volume_messages_client + cls.admin_volume_types_client = cls.os_adm.volume_types_v2_client diff --git a/tempest/clients.py b/tempest/clients.py index ccbec4e36d..93ab74ba45 100644 --- a/tempest/clients.py +++ b/tempest/clients.py @@ -183,6 +183,7 @@ from tempest.services.volume.v2.json.snapshots_client import \ SnapshotsClient as SnapshotsV2Client from tempest.services.volume.v2.json.volumes_client import \ VolumesClient as VolumesV2Client +from tempest.services.volume.v3.json.messages_client import MessagesClient CONF = config.CONF LOG = logging.getLogger(__name__) @@ -508,6 +509,8 @@ class Manager(manager.Manager): self.volumes_v2_client = VolumesV2Client( self.auth_provider, default_volume_size=CONF.volume.volume_size, **params) + self.volume_messages_client = MessagesClient(self.auth_provider, + **params) self.volume_types_client = VolumeTypesClient(self.auth_provider, **params) self.volume_types_v2_client = VolumeTypesV2Client( diff --git a/tempest/config.py b/tempest/config.py index 1f88871a9d..a9cf5379bc 100644 --- a/tempest/config.py +++ b/tempest/config.py @@ -682,6 +682,24 @@ VolumeGroup = [ cfg.IntOpt('volume_size', default=1, help='Default size in GB for volumes created by volumes tests'), + 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 not specified, Tempest avoids tests " + "which require a microversion. Valid values are string " + "with format 'X.Y' or string 'latest'",), + cfg.StrOpt('max_microversion', + default=None, + 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 not specified, Tempest avoids tests " + "which require a microversion. Valid values are string " + "with format 'X.Y' or string 'latest'",), ] volume_feature_group = cfg.OptGroup(name='volume-feature-enabled', @@ -711,6 +729,9 @@ VolumeFeaturesGroup = [ cfg.BoolOpt('api_v2', default=True, help="Is the v2 volume API enabled"), + cfg.BoolOpt('api_v3', + default=False, + help="Is the v3 volume API enabled"), cfg.BoolOpt('bootable', default=True, help='Update bootable status of a volume ' diff --git a/tempest/services/volume/base/base_v3_client.py b/tempest/services/volume/base/base_v3_client.py new file mode 100644 index 0000000000..ad6f76077b --- /dev/null +++ b/tempest/services/volume/base/base_v3_client.py @@ -0,0 +1,46 @@ +# Copyright 2016 Andrew Kerr +# All Rights Reserved. +# +# 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 tempest.lib.common import api_version_utils +from tempest.lib.common import rest_client + +VOLUME_MICROVERSION = None + + +class BaseV3Client(rest_client.RestClient): + """Base class to handle Cinder v3 client microversion support.""" + api_version = 'v3' + api_microversion_header_name = 'Openstack-Api-Version' + + def get_headers(self, accept_type=None, send_type=None): + headers = super(BaseV3Client, self).get_headers( + accept_type=accept_type, send_type=send_type) + if VOLUME_MICROVERSION: + headers[self.api_microversion_header_name] = ('volume %s' % + VOLUME_MICROVERSION) + return headers + + def request(self, method, url, extra_headers=False, headers=None, + body=None, chunked=False): + + resp, resp_body = super(BaseV3Client, self).request( + method, url, extra_headers, headers, body, chunked) + if (VOLUME_MICROVERSION and + VOLUME_MICROVERSION != api_version_utils.LATEST_MICROVERSION): + api_version_utils.assert_version_header_matches_request( + self.api_microversion_header_name, + 'volume %s' % VOLUME_MICROVERSION, + resp) + return resp, resp_body diff --git a/tempest/services/volume/v3/__init__.py b/tempest/services/volume/v3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/services/volume/v3/json/__init__.py b/tempest/services/volume/v3/json/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/services/volume/v3/json/messages_client.py b/tempest/services/volume/v3/json/messages_client.py new file mode 100644 index 0000000000..6be6d595bb --- /dev/null +++ b/tempest/services/volume/v3/json/messages_client.py @@ -0,0 +1,59 @@ +# Copyright 2016 Andrew Kerr +# All Rights Reserved. +# +# 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_serialization import jsonutils as json + +from tempest.lib.common import rest_client +from tempest.lib import exceptions as lib_exc +from tempest.services.volume.base import base_v3_client + + +class MessagesClient(base_v3_client.BaseV3Client): + """Client class to send user messages API requests.""" + + def show_message(self, message_id): + """Show details for a single message.""" + url = 'messages/%s' % str(message_id) + resp, body = self.get(url) + body = json.loads(body) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) + + def list_messages(self): + """List all messages.""" + url = 'messages' + resp, body = self.get(url) + body = json.loads(body) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) + + def delete_message(self, message_id): + """Delete a single message.""" + url = 'messages/%s' % str(message_id) + resp, body = self.delete(url) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp, body) + + def is_resource_deleted(self, id): + try: + self.show_message(id) + except lib_exc.NotFound: + return True + return False + + @property + def resource_type(self): + """Returns the primary type of resource this client works with.""" + return 'message'