Merge "Add tests for Cinder user messages v3 API"
diff --git a/tempest/api/volume/api_microversion_fixture.py b/tempest/api/volume/api_microversion_fixture.py
new file mode 100644
index 0000000..6817eaa
--- /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 cd21424..9010c89 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -45,6 +45,10 @@
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 0000000..e69de29
--- /dev/null
+++ b/tempest/api/volume/v3/__init__.py
diff --git a/tempest/api/volume/v3/admin/__init__.py b/tempest/api/volume/v3/admin/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/api/volume/v3/admin/__init__.py
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 0000000..19c37be
--- /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 0000000..c31c83c
--- /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 ccbec4e..93ab74b 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -183,6 +183,7 @@
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 @@
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 1f88871..a9cf537 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -682,6 +682,24 @@
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 @@
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 0000000..ad6f760
--- /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 0000000..e69de29
--- /dev/null
+++ b/tempest/services/volume/v3/__init__.py
diff --git a/tempest/services/volume/v3/json/__init__.py b/tempest/services/volume/v3/json/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/volume/v3/json/__init__.py
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 0000000..6be6d59
--- /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'