
Cinder supports filter volumes with metadata, but in some case, users don't know what metadata the volumes contain or what metadata is valid to filter volumes. This patch updated volumes/summary API to return all valid distinct metadata to users. Then users could use these metadatas to filter volumes easily. This function is useful for dashboard, such as Horizon, as well. APIImpact Implements: blueprint metadata-for-volume-summary Change-Id: I33c77d9db88f70d8d3b8ea86c86c01220dcc537c
491 lines
22 KiB
Python
491 lines
22 KiB
Python
#
|
|
# 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 datetime
|
|
import ddt
|
|
import iso8601
|
|
|
|
import mock
|
|
import webob
|
|
|
|
from cinder.api import extensions
|
|
from cinder.api.openstack import api_version_request as api_version
|
|
from cinder.api.v3 import volumes
|
|
from cinder import context
|
|
from cinder import db
|
|
from cinder import exception
|
|
from cinder.group import api as group_api
|
|
from cinder import test
|
|
from cinder.tests.unit.api import fakes
|
|
from cinder.tests.unit.api.v2 import fakes as v2_fakes
|
|
from cinder.tests.unit.api.v2 import test_volumes as v2_test_volumes
|
|
from cinder.tests.unit import fake_constants as fake
|
|
from cinder.tests.unit import utils as test_utils
|
|
from cinder import utils
|
|
from cinder.volume import api as volume_api
|
|
from cinder.volume import api as vol_get
|
|
|
|
version_header_name = 'OpenStack-API-Version'
|
|
|
|
DEFAULT_AZ = "zone1:host1"
|
|
|
|
|
|
@ddt.ddt
|
|
class VolumeApiTest(test.TestCase):
|
|
def setUp(self):
|
|
super(VolumeApiTest, self).setUp()
|
|
self.ext_mgr = extensions.ExtensionManager()
|
|
self.ext_mgr.extensions = {}
|
|
self.controller = volumes.VolumeController(self.ext_mgr)
|
|
|
|
self.flags(host='fake')
|
|
self.ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True)
|
|
|
|
def test_check_volume_filters_called(self):
|
|
with mock.patch.object(vol_get.API,
|
|
'check_volume_filters') as volume_get:
|
|
req = fakes.HTTPRequest.blank('/v3/volumes?bootable=True')
|
|
req.method = 'GET'
|
|
req.content_type = 'application/json'
|
|
req.headers = {version_header_name: 'volume 3.0'}
|
|
req.environ['cinder.context'].is_admin = True
|
|
|
|
self.override_config('query_volume_filters', 'bootable')
|
|
self.controller.index(req)
|
|
filters = req.params.copy()
|
|
|
|
volume_get.assert_called_with(filters, False)
|
|
|
|
def test_check_volume_filters_strict_called(self):
|
|
|
|
with mock.patch.object(vol_get.API,
|
|
'check_volume_filters') as volume_get:
|
|
req = fakes.HTTPRequest.blank('/v3/volumes?bootable=True')
|
|
req.method = 'GET'
|
|
req.content_type = 'application/json'
|
|
req.headers = {version_header_name: 'volume 3.2'}
|
|
req.environ['cinder.context'].is_admin = True
|
|
req.api_version_request = api_version.APIVersionRequest('3.29')
|
|
|
|
self.override_config('query_volume_filters', 'bootable')
|
|
self.controller.index(req)
|
|
filters = req.params.copy()
|
|
|
|
volume_get.assert_called_with(filters, True)
|
|
|
|
def _create_volume_with_glance_metadata(self):
|
|
vol1 = db.volume_create(self.ctxt, {'display_name': 'test1',
|
|
'project_id':
|
|
self.ctxt.project_id})
|
|
db.volume_glance_metadata_create(self.ctxt, vol1.id, 'image_name',
|
|
'imageTestOne')
|
|
vol2 = db.volume_create(self.ctxt, {'display_name': 'test2',
|
|
'project_id':
|
|
self.ctxt.project_id})
|
|
db.volume_glance_metadata_create(self.ctxt, vol2.id, 'image_name',
|
|
'imageTestTwo')
|
|
db.volume_glance_metadata_create(self.ctxt, vol2.id, 'disk_format',
|
|
'qcow2')
|
|
return [vol1, vol2]
|
|
|
|
def _create_volume_with_group(self):
|
|
vol1 = db.volume_create(self.ctxt, {'display_name': 'test1',
|
|
'project_id':
|
|
self.ctxt.project_id,
|
|
'group_id':
|
|
fake.GROUP_ID})
|
|
vol2 = db.volume_create(self.ctxt, {'display_name': 'test2',
|
|
'project_id':
|
|
self.ctxt.project_id,
|
|
'group_id':
|
|
fake.GROUP2_ID})
|
|
return [vol1, vol2]
|
|
|
|
def test_volume_index_filter_by_glance_metadata(self):
|
|
vols = self._create_volume_with_glance_metadata()
|
|
req = fakes.HTTPRequest.blank("/v3/volumes?glance_metadata="
|
|
"{'image_name': 'imageTestOne'}")
|
|
req.headers["OpenStack-API-Version"] = "volume 3.4"
|
|
req.api_version_request = api_version.APIVersionRequest('3.4')
|
|
req.environ['cinder.context'] = self.ctxt
|
|
res_dict = self.controller.index(req)
|
|
volumes = res_dict['volumes']
|
|
self.assertEqual(1, len(volumes))
|
|
self.assertEqual(vols[0].id, volumes[0]['id'])
|
|
|
|
def test_volume_index_filter_by_glance_metadata_in_unsupport_version(self):
|
|
self._create_volume_with_glance_metadata()
|
|
req = fakes.HTTPRequest.blank("/v3/volumes?glance_metadata="
|
|
"{'image_name': 'imageTestOne'}")
|
|
req.headers["OpenStack-API-Version"] = "volume 3.0"
|
|
req.api_version_request = api_version.APIVersionRequest('3.0')
|
|
req.environ['cinder.context'] = self.ctxt
|
|
res_dict = self.controller.index(req)
|
|
volumes = res_dict['volumes']
|
|
self.assertEqual(2, len(volumes))
|
|
|
|
def test_volume_index_filter_by_group_id(self):
|
|
vols = self._create_volume_with_group()
|
|
req = fakes.HTTPRequest.blank(("/v3/volumes?group_id=%s") %
|
|
fake.GROUP_ID)
|
|
req.headers["OpenStack-API-Version"] = "volume 3.10"
|
|
req.api_version_request = api_version.APIVersionRequest('3.10')
|
|
req.environ['cinder.context'] = self.ctxt
|
|
res_dict = self.controller.index(req)
|
|
volumes = res_dict['volumes']
|
|
self.assertEqual(1, len(volumes))
|
|
self.assertEqual(vols[0].id, volumes[0]['id'])
|
|
|
|
def test_volume_index_filter_by_group_id_in_unsupport_version(self):
|
|
self._create_volume_with_group()
|
|
req = fakes.HTTPRequest.blank(("/v3/volumes?group_id=%s") %
|
|
fake.GROUP_ID)
|
|
req.headers["OpenStack-API-Version"] = "volume 3.9"
|
|
req.api_version_request = api_version.APIVersionRequest('3.9')
|
|
req.environ['cinder.context'] = self.ctxt
|
|
res_dict = self.controller.index(req)
|
|
volumes = res_dict['volumes']
|
|
self.assertEqual(2, len(volumes))
|
|
|
|
def _fake_volumes_summary_request(self, version='3.12', all_tenant=False,
|
|
is_admin=False):
|
|
req_url = '/v3/volumes/summary'
|
|
if all_tenant:
|
|
req_url += '?all_tenants=True'
|
|
req = fakes.HTTPRequest.blank(req_url, use_admin_context=is_admin)
|
|
req.headers = {'OpenStack-API-Version': 'volume ' + version}
|
|
req.api_version_request = api_version.APIVersionRequest(version)
|
|
return req
|
|
|
|
def test_volumes_summary_in_unsupport_version(self):
|
|
"""Function call to test summary volumes API in unsupported version"""
|
|
req = self._fake_volumes_summary_request(version='3.7')
|
|
self.assertRaises(exception.VersionNotFoundForAPIMethod,
|
|
self.controller.summary, req)
|
|
|
|
def test_volumes_summary_in_supported_version(self):
|
|
"""Function call to test the summary volumes API for version v3."""
|
|
req = self._fake_volumes_summary_request()
|
|
res_dict = self.controller.summary(req)
|
|
expected = {'volume-summary': {'total_size': 0.0, 'total_count': 0}}
|
|
self.assertEqual(expected, res_dict)
|
|
|
|
vol = v2_test_volumes.VolumeApiTest._vol_in_request_body(
|
|
availability_zone="nova")
|
|
body = {"volume": vol}
|
|
req = fakes.HTTPRequest.blank('/v3/volumes')
|
|
res_dict = self.controller.create(req, body)
|
|
|
|
req = self._fake_volumes_summary_request()
|
|
res_dict = self.controller.summary(req)
|
|
expected = {'volume-summary': {'total_size': 1.0, 'total_count': 1}}
|
|
self.assertEqual(expected, res_dict)
|
|
|
|
@ddt.data(
|
|
('3.35', {'volume-summary': {'total_size': 0.0,
|
|
'total_count': 0}}),
|
|
('3.36', {'volume-summary': {'total_size': 0.0,
|
|
'total_count': 0,
|
|
'metadata': {}}}))
|
|
@ddt.unpack
|
|
def test_volume_summary_empty(self, summary_api_version, expect_result):
|
|
req = self._fake_volumes_summary_request(version=summary_api_version)
|
|
res_dict = self.controller.summary(req)
|
|
self.assertEqual(expect_result, res_dict)
|
|
|
|
@ddt.data(
|
|
('3.35', {'volume-summary': {'total_size': 2,
|
|
'total_count': 2}}),
|
|
('3.36', {'volume-summary': {'total_size': 2,
|
|
'total_count': 2,
|
|
'metadata': {
|
|
'name': ['test_name1', 'test_name2'],
|
|
'age': ['test_age']}}}))
|
|
@ddt.unpack
|
|
def test_volume_summary_return_metadata(self, summary_api_version,
|
|
expect_result):
|
|
test_utils.create_volume(self.ctxt, metadata={'name': 'test_name1',
|
|
'age': 'test_age'})
|
|
test_utils.create_volume(self.ctxt, metadata={'name': 'test_name2',
|
|
'age': 'test_age'})
|
|
ctxt2 = context.RequestContext(fake.USER_ID, fake.PROJECT2_ID, True)
|
|
test_utils.create_volume(ctxt2, metadata={'name': 'test_name3'})
|
|
|
|
req = self._fake_volumes_summary_request(version=summary_api_version)
|
|
res_dict = self.controller.summary(req)
|
|
self.assertEqual(expect_result, res_dict)
|
|
|
|
@ddt.data(
|
|
('3.35', {'volume-summary': {'total_size': 2,
|
|
'total_count': 2}}),
|
|
('3.36', {'volume-summary': {'total_size': 2,
|
|
'total_count': 2,
|
|
'metadata': {
|
|
'name': ['test_name1', 'test_name2'],
|
|
'age': ['test_age']}}}))
|
|
@ddt.unpack
|
|
def test_volume_summary_return_metadata_all_tenant(
|
|
self, summary_api_version, expect_result):
|
|
test_utils.create_volume(self.ctxt, metadata={'name': 'test_name1',
|
|
'age': 'test_age'})
|
|
ctxt2 = context.RequestContext(fake.USER_ID, fake.PROJECT2_ID, True)
|
|
test_utils.create_volume(ctxt2, metadata={'name': 'test_name2',
|
|
'age': 'test_age'})
|
|
|
|
req = self._fake_volumes_summary_request(version=summary_api_version,
|
|
all_tenant=True,
|
|
is_admin=True)
|
|
res_dict = self.controller.summary(req)
|
|
self.assertEqual(expect_result, res_dict)
|
|
|
|
def _vol_in_request_body(self,
|
|
size=v2_fakes.DEFAULT_VOL_SIZE,
|
|
name=v2_fakes.DEFAULT_VOL_NAME,
|
|
description=v2_fakes.DEFAULT_VOL_DESCRIPTION,
|
|
availability_zone=DEFAULT_AZ,
|
|
snapshot_id=None,
|
|
source_volid=None,
|
|
source_replica=None,
|
|
consistencygroup_id=None,
|
|
volume_type=None,
|
|
image_ref=None,
|
|
image_id=None,
|
|
group_id=None):
|
|
vol = {"size": size,
|
|
"name": name,
|
|
"description": description,
|
|
"availability_zone": availability_zone,
|
|
"snapshot_id": snapshot_id,
|
|
"source_volid": source_volid,
|
|
"source_replica": source_replica,
|
|
"consistencygroup_id": consistencygroup_id,
|
|
"volume_type": volume_type,
|
|
"group_id": group_id,
|
|
}
|
|
|
|
if image_id is not None:
|
|
vol['image_id'] = image_id
|
|
elif image_ref is not None:
|
|
vol['imageRef'] = image_ref
|
|
|
|
return vol
|
|
|
|
def _expected_vol_from_controller(
|
|
self,
|
|
size=v2_fakes.DEFAULT_VOL_SIZE,
|
|
availability_zone=DEFAULT_AZ,
|
|
description=v2_fakes.DEFAULT_VOL_DESCRIPTION,
|
|
name=v2_fakes.DEFAULT_VOL_NAME,
|
|
consistencygroup_id=None,
|
|
source_volid=None,
|
|
snapshot_id=None,
|
|
metadata=None,
|
|
attachments=None,
|
|
volume_type=v2_fakes.DEFAULT_VOL_TYPE,
|
|
status=v2_fakes.DEFAULT_VOL_STATUS,
|
|
with_migration_status=False,
|
|
group_id=None,
|
|
req_version=None):
|
|
metadata = metadata or {}
|
|
attachments = attachments or []
|
|
volume = {'volume':
|
|
{'attachments': attachments,
|
|
'availability_zone': availability_zone,
|
|
'bootable': 'false',
|
|
'consistencygroup_id': consistencygroup_id,
|
|
'group_id': group_id,
|
|
'created_at': datetime.datetime(
|
|
1900, 1, 1, 1, 1, 1, tzinfo=iso8601.iso8601.Utc()),
|
|
'updated_at': datetime.datetime(
|
|
1900, 1, 1, 1, 1, 1, tzinfo=iso8601.iso8601.Utc()),
|
|
'description': description,
|
|
'id': v2_fakes.DEFAULT_VOL_ID,
|
|
'links':
|
|
[{'href': 'http://localhost/v3/%s/volumes/%s' % (
|
|
fake.PROJECT_ID, fake.VOLUME_ID),
|
|
'rel': 'self'},
|
|
{'href': 'http://localhost/%s/volumes/%s' % (
|
|
fake.PROJECT_ID, fake.VOLUME_ID),
|
|
'rel': 'bookmark'}],
|
|
'metadata': metadata,
|
|
'name': name,
|
|
'replication_status': 'disabled',
|
|
'multiattach': False,
|
|
'size': size,
|
|
'snapshot_id': snapshot_id,
|
|
'source_volid': source_volid,
|
|
'status': status,
|
|
'user_id': fake.USER_ID,
|
|
'volume_type': volume_type,
|
|
'encrypted': False}}
|
|
|
|
if with_migration_status:
|
|
volume['volume']['migration_status'] = None
|
|
|
|
# Remove group_id if max version is less than 3.13.
|
|
if req_version and req_version.matches(None, "3.12"):
|
|
volume['volume'].pop('group_id')
|
|
|
|
return volume
|
|
|
|
def _expected_volume_api_create_kwargs(self, snapshot=None,
|
|
availability_zone=DEFAULT_AZ,
|
|
source_volume=None,
|
|
test_group=None,
|
|
req_version=None):
|
|
volume = {
|
|
'metadata': None,
|
|
'snapshot': snapshot,
|
|
'source_volume': source_volume,
|
|
'source_replica': None,
|
|
'consistencygroup': None,
|
|
'availability_zone': availability_zone,
|
|
'scheduler_hints': None,
|
|
'multiattach': False,
|
|
'group': test_group,
|
|
}
|
|
|
|
# Remove group_id if max version is less than 3.13.
|
|
if req_version and req_version.matches(None, "3.12"):
|
|
volume.pop('group')
|
|
|
|
return volume
|
|
|
|
@ddt.data('3.13', '3.12')
|
|
@mock.patch(
|
|
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
|
|
def test_volume_create(self, max_ver, mock_validate):
|
|
self.mock_object(volume_api.API, 'get', v2_fakes.fake_volume_get)
|
|
self.mock_object(volume_api.API, "create",
|
|
v2_fakes.fake_volume_api_create)
|
|
self.mock_object(db.sqlalchemy.api, '_volume_type_get_full',
|
|
v2_fakes.fake_volume_type_get)
|
|
|
|
vol = self._vol_in_request_body()
|
|
body = {"volume": vol}
|
|
req = fakes.HTTPRequest.blank('/v3/volumes')
|
|
req.api_version_request = api_version.APIVersionRequest(max_ver)
|
|
res_dict = self.controller.create(req, body)
|
|
ex = self._expected_vol_from_controller(
|
|
req_version=req.api_version_request)
|
|
self.assertEqual(ex, res_dict)
|
|
self.assertTrue(mock_validate.called)
|
|
|
|
@ddt.data('3.14', '3.13')
|
|
@mock.patch.object(group_api.API, 'get')
|
|
@mock.patch.object(db.sqlalchemy.api, '_volume_type_get_full',
|
|
autospec=True)
|
|
@mock.patch.object(volume_api.API, 'get_snapshot', autospec=True)
|
|
@mock.patch.object(volume_api.API, 'create', autospec=True)
|
|
def test_volume_creation_from_snapshot(self, max_ver, create, get_snapshot,
|
|
volume_type_get, group_get):
|
|
create.side_effect = v2_fakes.fake_volume_api_create
|
|
get_snapshot.side_effect = v2_fakes.fake_snapshot_get
|
|
volume_type_get.side_effect = v2_fakes.fake_volume_type_get
|
|
fake_group = {
|
|
'id': fake.GROUP_ID,
|
|
'group_type_id': fake.GROUP_TYPE_ID,
|
|
'name': 'fake_group'
|
|
}
|
|
group_get.return_value = fake_group
|
|
|
|
snapshot_id = fake.SNAPSHOT_ID
|
|
vol = self._vol_in_request_body(snapshot_id=snapshot_id,
|
|
group_id=fake.GROUP_ID)
|
|
body = {"volume": vol}
|
|
req = fakes.HTTPRequest.blank('/v3/volumes')
|
|
req.api_version_request = api_version.APIVersionRequest(max_ver)
|
|
res_dict = self.controller.create(req, body)
|
|
ex = self._expected_vol_from_controller(
|
|
snapshot_id=snapshot_id,
|
|
req_version=req.api_version_request)
|
|
self.assertEqual(ex, res_dict)
|
|
|
|
context = req.environ['cinder.context']
|
|
get_snapshot.assert_called_once_with(self.controller.volume_api,
|
|
context, snapshot_id)
|
|
|
|
kwargs = self._expected_volume_api_create_kwargs(
|
|
v2_fakes.fake_snapshot(snapshot_id),
|
|
test_group=fake_group,
|
|
req_version=req.api_version_request)
|
|
create.assert_called_once_with(self.controller.volume_api, context,
|
|
vol['size'], v2_fakes.DEFAULT_VOL_NAME,
|
|
v2_fakes.DEFAULT_VOL_DESCRIPTION,
|
|
**kwargs)
|
|
|
|
@ddt.data({'s': 'ea895e29-8485-4930-bbb8-c5616a309c0e'},
|
|
['ea895e29-8485-4930-bbb8-c5616a309c0e'],
|
|
42)
|
|
def test_volume_creation_fails_with_invalid_snapshot_type(self, value):
|
|
snapshot_id = value
|
|
vol = self._vol_in_request_body(snapshot_id=snapshot_id)
|
|
body = {"volume": vol}
|
|
req = fakes.HTTPRequest.blank('/v3/volumes')
|
|
# Raise 400 when snapshot has not uuid type.
|
|
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
|
req, body)
|
|
|
|
@ddt.data({'source_volid': 1},
|
|
{'source_volid': []},
|
|
{'source_replica': 1},
|
|
{'source_replica': []},
|
|
{'consistencygroup_id': 1},
|
|
{'consistencygroup_id': []})
|
|
def test_volume_creation_fails_with_invalid_uuids(self, updated_uuids):
|
|
vol = self._vol_in_request_body()
|
|
vol.update(updated_uuids)
|
|
body = {"volume": vol}
|
|
req = fakes.HTTPRequest.blank('/v2/volumes')
|
|
# Raise 400 for resource requested with invalid uuids.
|
|
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
|
req, body)
|
|
|
|
@ddt.data('3.30', '3.31', '3.34')
|
|
@mock.patch.object(volume_api.API, 'check_volume_filters', mock.Mock())
|
|
@mock.patch.object(utils, 'add_visible_admin_metadata', mock.Mock())
|
|
@mock.patch('cinder.api.common.reject_invalid_filters')
|
|
def test_list_volume_with_general_filter(self, version, mock_update):
|
|
req = fakes.HTTPRequest.blank('/v3/volumes', version=version)
|
|
self.controller.index(req)
|
|
if version != '3.30':
|
|
support_like = True if version == '3.34' else False
|
|
mock_update.assert_called_once_with(req.environ['cinder.context'],
|
|
mock.ANY, 'volume',
|
|
support_like)
|
|
|
|
@ddt.data({'admin': True, 'version': '3.21'},
|
|
{'admin': False, 'version': '3.21'},
|
|
{'admin': True, 'version': '3.20'},
|
|
{'admin': False, 'version': '3.20'})
|
|
@ddt.unpack
|
|
def test_volume_show_provider_id(self, admin, version):
|
|
self.mock_object(volume_api.API, 'get', v2_fakes.fake_volume_api_get)
|
|
self.mock_object(db.sqlalchemy.api, '_volume_type_get_full',
|
|
v2_fakes.fake_volume_type_get)
|
|
|
|
req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID,
|
|
version=version)
|
|
if admin:
|
|
admin_ctx = context.RequestContext(fake.USER_ID, fake.PROJECT_ID,
|
|
True)
|
|
req.environ['cinder.context'] = admin_ctx
|
|
res_dict = self.controller.show(req, fake.VOLUME_ID)
|
|
req_version = req.api_version_request
|
|
# provider_id is in view if min version is greater than or equal to
|
|
# 3.21 for admin.
|
|
if req_version.matches("3.21", None) and admin:
|
|
self.assertIn('provider_id', res_dict['volume'])
|
|
else:
|
|
self.assertNotIn('provider_id', res_dict['volume'])
|