deb-cinder/cinder/tests/unit/objects/test_volume.py
xing-yang 8c74c74695 Add generic volume groups
This is the second patch that implements the generic-volume-group
bluerpint. It adds the groups table and introduces create/delete/
update/list/show APIs for groups.

It depends on the first patch which adds group types and group specs:
    https://review.openstack.org/#/c/320165/

Client side patch is here:
    https://review.openstack.org/#/c/322627/

Current microversion is 3.13. The following CLI's are supported:
cinder --os-volume-api-version 3.13 group-create --name my_group
    <group type uuid> <volume type uuid>
cinder --os-volume-api-version 3.13 group-list
cinder --os-volume-api-version 3.13 create --group-id <group uuid>
    --volume-type <volume type uuid> <size>
cinder --os-volume-api-version 3.13 group-update <group uuid>
    --name new_name  description new_description
    --add-volumes <uuid of volume to add>
    --remove-volumes <uuid of volume to remove>
cinder --os-volume-api-version 3.13 group-show <group uuid>
cinder --os-volume-api-version 3.13 group-delete
    --delete-volumes <group uuid>

APIImpact
DocImpact
Change-Id: I35157439071786872bc9976741c4ef75698f7cb7
Partial-Implements: blueprint generic-volume-group
2016-07-16 19:34:39 -04:00

481 lines
23 KiB
Python

# Copyright 2015 SimpliVity Corp.
#
# 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 ddt
import mock
from oslo_utils import timeutils
import pytz
import six
from cinder import context
from cinder import exception
from cinder import objects
from cinder.tests.unit.consistencygroup import fake_consistencygroup
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.tests.unit import objects as test_objects
@ddt.ddt
class TestVolume(test_objects.BaseObjectsTestCase):
@staticmethod
def _compare(test, db, obj):
db = {k: v for k, v in db.items()
if not k.endswith('metadata') or k.startswith('volume')}
test_objects.BaseObjectsTestCase._compare(test, db, obj)
@mock.patch('cinder.db.sqlalchemy.api.volume_get')
def test_get_by_id(self, volume_get):
db_volume = fake_volume.fake_db_volume()
volume_get.return_value = db_volume
volume = objects.Volume.get_by_id(self.context, fake.VOLUME_ID)
volume_get.assert_called_once_with(self.context, fake.VOLUME_ID)
self._compare(self, db_volume, volume)
@mock.patch('cinder.db.sqlalchemy.api.model_query')
def test_get_by_id_no_existing_id(self, model_query):
pf = (model_query().options().options().options().options().options().
options())
pf.filter_by().first.return_value = None
self.assertRaises(exception.VolumeNotFound,
objects.Volume.get_by_id, self.context, 123)
@mock.patch('cinder.db.volume_create')
def test_create(self, volume_create):
db_volume = fake_volume.fake_db_volume()
volume_create.return_value = db_volume
volume = objects.Volume(context=self.context)
volume.create()
self.assertEqual(db_volume['id'], volume.id)
@mock.patch('cinder.db.volume_update')
def test_save(self, volume_update):
db_volume = fake_volume.fake_db_volume()
volume = objects.Volume._from_db_object(self.context,
objects.Volume(), db_volume)
volume.display_name = 'foobar'
volume.save()
volume_update.assert_called_once_with(self.context, volume.id,
{'display_name': 'foobar'})
@mock.patch('cinder.db.volume_metadata_update',
return_value={'key1': 'value1'})
@mock.patch('cinder.db.volume_update')
def test_save_with_metadata(self, volume_update, metadata_update):
db_volume = fake_volume.fake_db_volume()
volume = objects.Volume._from_db_object(self.context,
objects.Volume(), db_volume)
volume.display_name = 'foobar'
volume.metadata = {'key1': 'value1'}
self.assertEqual({'display_name': 'foobar',
'metadata': {'key1': 'value1'}},
volume.obj_get_changes())
volume.save()
volume_update.assert_called_once_with(self.context, volume.id,
{'display_name': 'foobar'})
metadata_update.assert_called_once_with(self.context, volume.id,
{'key1': 'value1'}, True)
@mock.patch('cinder.db.volume_admin_metadata_update',
return_value={'key1': 'value1'})
@mock.patch('cinder.db.volume_update')
def test_save_with_admin_metadata(self, volume_update,
admin_metadata_update):
# Test with no admin context
db_volume = fake_volume.fake_db_volume()
volume = objects.Volume._from_db_object(self.context,
objects.Volume(), db_volume)
volume.admin_metadata = {'key1': 'value1'}
volume.save()
self.assertFalse(admin_metadata_update.called)
# Test with admin context
admin_context = context.RequestContext(self.user_id, self.project_id,
is_admin=True)
volume = objects.Volume._from_db_object(admin_context,
objects.Volume(), db_volume)
volume.admin_metadata = {'key1': 'value1'}
volume.save()
admin_metadata_update.assert_called_once_with(
admin_context, volume.id, {'key1': 'value1'}, True)
def test_save_with_glance_metadata(self):
db_volume = fake_volume.fake_db_volume()
volume = objects.Volume._from_db_object(self.context,
objects.Volume(), db_volume)
volume.display_name = 'foobar'
volume.glance_metadata = {'key1': 'value1'}
self.assertRaises(exception.ObjectActionError, volume.save)
def test_save_with_consistencygroup(self):
db_volume = fake_volume.fake_db_volume()
volume = objects.Volume._from_db_object(self.context,
objects.Volume(), db_volume)
volume.display_name = 'foobar'
volume.consistencygroup = objects.ConsistencyGroup()
self.assertRaises(exception.ObjectActionError, volume.save)
def test_save_with_snapshots(self):
db_volume = fake_volume.fake_db_volume()
volume = objects.Volume._from_db_object(self.context,
objects.Volume(), db_volume)
volume.display_name = 'foobar'
volume.snapshots = objects.SnapshotList()
self.assertRaises(exception.ObjectActionError, volume.save)
@mock.patch('oslo_utils.timeutils.utcnow', return_value=timeutils.utcnow())
@mock.patch('cinder.db.sqlalchemy.api.volume_destroy')
def test_destroy(self, volume_destroy, utcnow_mock):
volume_destroy.return_value = {
'status': 'deleted',
'deleted': True,
'deleted_at': utcnow_mock.return_value}
db_volume = fake_volume.fake_db_volume()
volume = objects.Volume._from_db_object(self.context,
objects.Volume(), db_volume)
volume.destroy()
self.assertTrue(volume_destroy.called)
admin_context = volume_destroy.call_args[0][0]
self.assertTrue(admin_context.is_admin)
self.assertTrue(volume.deleted)
self.assertEqual('deleted', volume.status)
self.assertEqual(utcnow_mock.return_value.replace(tzinfo=pytz.UTC),
volume.deleted_at)
self.assertIsNone(volume.migration_status)
def test_obj_fields(self):
volume = objects.Volume(context=self.context, id=fake.VOLUME_ID,
name_id=fake.VOLUME_NAME_ID)
self.assertEqual(['name', 'name_id', 'volume_metadata',
'volume_admin_metadata', 'volume_glance_metadata'],
volume.obj_extra_fields)
self.assertEqual('volume-%s' % fake.VOLUME_NAME_ID, volume.name)
self.assertEqual(fake.VOLUME_NAME_ID, volume.name_id)
def test_obj_field_previous_status(self):
volume = objects.Volume(context=self.context,
previous_status='backing-up')
self.assertEqual('backing-up', volume.previous_status)
@mock.patch('cinder.db.volume_metadata_delete')
def test_delete_metadata_key(self, metadata_delete):
volume = objects.Volume(self.context, id=fake.VOLUME_ID)
volume.metadata = {'key1': 'value1', 'key2': 'value2'}
self.assertEqual({}, volume._orig_metadata)
volume.delete_metadata_key('key2')
self.assertEqual({'key1': 'value1'}, volume.metadata)
metadata_delete.assert_called_once_with(self.context, fake.VOLUME_ID,
'key2')
@mock.patch('cinder.db.volume_metadata_get')
@mock.patch('cinder.db.volume_glance_metadata_get')
@mock.patch('cinder.db.volume_admin_metadata_get')
@mock.patch('cinder.objects.volume_type.VolumeType.get_by_id')
@mock.patch('cinder.objects.volume_attachment.VolumeAttachmentList.'
'get_all_by_volume_id')
@mock.patch('cinder.objects.consistencygroup.ConsistencyGroup.get_by_id')
@mock.patch('cinder.objects.snapshot.SnapshotList.get_all_for_volume')
def test_obj_load_attr(self, mock_sl_get_all_for_volume, mock_cg_get_by_id,
mock_va_get_all_by_vol, mock_vt_get_by_id,
mock_admin_metadata_get, mock_glance_metadata_get,
mock_metadata_get):
volume = objects.Volume._from_db_object(
self.context, objects.Volume(), fake_volume.fake_db_volume())
# Test metadata lazy-loaded field
metadata = {'foo': 'bar'}
mock_metadata_get.return_value = metadata
self.assertEqual(metadata, volume.metadata)
mock_metadata_get.assert_called_once_with(self.context, volume.id)
# Test glance_metadata lazy-loaded field
glance_metadata = [{'key': 'foo', 'value': 'bar'}]
mock_glance_metadata_get.return_value = glance_metadata
self.assertEqual({'foo': 'bar'}, volume.glance_metadata)
mock_glance_metadata_get.assert_called_once_with(
self.context, volume.id)
# Test volume_type lazy-loaded field
# Case1. volume.volume_type_id = None
self.assertIsNone(volume.volume_type)
# Case2. volume2.volume_type_id = 1
fake2 = fake_volume.fake_db_volume()
fake2.update({'volume_type_id': fake.VOLUME_ID})
volume2 = objects.Volume._from_db_object(
self.context, objects.Volume(), fake2)
volume_type = objects.VolumeType(context=self.context,
id=fake.VOLUME_TYPE_ID)
mock_vt_get_by_id.return_value = volume_type
self.assertEqual(volume_type, volume2.volume_type)
mock_vt_get_by_id.assert_called_once_with(self.context,
volume2.volume_type_id)
# Test consistencygroup lazy-loaded field
consistencygroup = objects.ConsistencyGroup(
context=self.context, id=fake.CONSISTENCY_GROUP_ID)
mock_cg_get_by_id.return_value = consistencygroup
self.assertEqual(consistencygroup, volume.consistencygroup)
mock_cg_get_by_id.assert_called_once_with(self.context,
volume.consistencygroup_id)
# Test snapshots lazy-loaded field
snapshots = objects.SnapshotList(context=self.context,
id=fake.SNAPSHOT_ID)
mock_sl_get_all_for_volume.return_value = snapshots
self.assertEqual(snapshots, volume.snapshots)
mock_sl_get_all_for_volume.assert_called_once_with(self.context,
volume.id)
# Test volume_attachment lazy-loaded field
va_objs = [objects.VolumeAttachment(context=self.context, id=i)
for i in [fake.OBJECT_ID, fake.OBJECT2_ID, fake.OBJECT3_ID]]
va_list = objects.VolumeAttachmentList(context=self.context,
objects=va_objs)
mock_va_get_all_by_vol.return_value = va_list
self.assertEqual(va_list, volume.volume_attachment)
mock_va_get_all_by_vol.assert_called_once_with(self.context, volume.id)
# Test admin_metadata lazy-loaded field - user context
adm_metadata = {'bar': 'foo'}
mock_admin_metadata_get.return_value = adm_metadata
self.assertEqual({}, volume.admin_metadata)
self.assertFalse(mock_admin_metadata_get.called)
# Test admin_metadata lazy-loaded field - admin context
adm_context = self.context.elevated()
volume = objects.Volume._from_db_object(adm_context, objects.Volume(),
fake_volume.fake_db_volume())
adm_metadata = {'bar': 'foo'}
mock_admin_metadata_get.return_value = adm_metadata
self.assertEqual(adm_metadata, volume.admin_metadata)
mock_admin_metadata_get.assert_called_once_with(adm_context, volume.id)
def test_from_db_object_with_all_expected_attributes(self):
expected_attrs = ['metadata', 'admin_metadata', 'glance_metadata',
'volume_type', 'volume_attachment',
'consistencygroup']
db_metadata = [{'key': 'foo', 'value': 'bar'}]
db_admin_metadata = [{'key': 'admin_foo', 'value': 'admin_bar'}]
db_glance_metadata = [{'key': 'glance_foo', 'value': 'glance_bar'}]
db_volume_type = fake_volume.fake_db_volume_type()
db_volume_attachments = fake_volume.fake_db_volume_attachment()
db_consistencygroup = fake_consistencygroup.fake_db_consistencygroup()
db_snapshots = fake_snapshot.fake_db_snapshot()
db_volume = fake_volume.fake_db_volume(
volume_metadata=db_metadata,
volume_admin_metadata=db_admin_metadata,
volume_glance_metadata=db_glance_metadata,
volume_type=db_volume_type,
volume_attachment=[db_volume_attachments],
consistencygroup=db_consistencygroup,
snapshots=[db_snapshots],
)
volume = objects.Volume._from_db_object(self.context, objects.Volume(),
db_volume, expected_attrs)
self.assertEqual({'foo': 'bar'}, volume.metadata)
self.assertEqual({'admin_foo': 'admin_bar'}, volume.admin_metadata)
self.assertEqual({'glance_foo': 'glance_bar'}, volume.glance_metadata)
self._compare(self, db_volume_type, volume.volume_type)
self._compare(self, db_volume_attachments, volume.volume_attachment)
self._compare(self, db_consistencygroup, volume.consistencygroup)
self._compare(self, db_snapshots, volume.snapshots)
@mock.patch('cinder.db.volume_glance_metadata_get', return_value={})
@mock.patch('cinder.db.sqlalchemy.api.volume_get')
def test_refresh(self, volume_get, volume_metadata_get):
db_volume1 = fake_volume.fake_db_volume()
db_volume2 = db_volume1.copy()
db_volume2['display_name'] = 'foobar'
# On the second volume_get, return the volume with an updated
# display_name
volume_get.side_effect = [db_volume1, db_volume2]
volume = objects.Volume.get_by_id(self.context, fake.VOLUME_ID)
self._compare(self, db_volume1, volume)
# display_name was updated, so a volume refresh should have a new value
# for that field
volume.refresh()
self._compare(self, db_volume2, volume)
if six.PY3:
call_bool = mock.call.__bool__()
else:
call_bool = mock.call.__nonzero__()
volume_get.assert_has_calls([mock.call(self.context, fake.VOLUME_ID),
call_bool,
mock.call(self.context, fake.VOLUME_ID)])
def test_metadata_aliases(self):
volume = objects.Volume(context=self.context)
# metadata<->volume_metadata
volume.metadata = {'abc': 'def'}
self.assertEqual([{'key': 'abc', 'value': 'def'}],
volume.volume_metadata)
md = [{'key': 'def', 'value': 'abc'}]
volume.volume_metadata = md
self.assertEqual({'def': 'abc'}, volume.metadata)
# admin_metadata<->volume_admin_metadata
volume.admin_metadata = {'foo': 'bar'}
self.assertEqual([{'key': 'foo', 'value': 'bar'}],
volume.volume_admin_metadata)
volume.volume_admin_metadata = [{'key': 'xyz', 'value': '42'}]
self.assertEqual({'xyz': '42'}, volume.admin_metadata)
# glance_metadata<->volume_glance_metadata
volume.glance_metadata = {'jkl': 'mno'}
self.assertEqual([{'key': 'jkl', 'value': 'mno'}],
volume.volume_glance_metadata)
volume.volume_glance_metadata = [{'key': 'prs', 'value': 'tuw'}]
self.assertEqual({'prs': 'tuw'}, volume.glance_metadata)
@mock.patch('cinder.db.volume_metadata_update', return_value={})
@mock.patch('cinder.db.volume_update')
@ddt.data({'src_vol_type_id': fake.VOLUME_TYPE_ID,
'dest_vol_type_id': fake.VOLUME_TYPE2_ID},
{'src_vol_type_id': None,
'dest_vol_type_id': fake.VOLUME_TYPE2_ID})
@ddt.unpack
def test_finish_volume_migration(self, volume_update, metadata_update,
src_vol_type_id, dest_vol_type_id):
src_volume_db = fake_volume.fake_db_volume(
**{'id': fake.VOLUME_ID, 'volume_type_id': src_vol_type_id})
if src_vol_type_id:
src_volume_db['volume_type'] = fake_volume.fake_db_volume_type(
id=src_vol_type_id)
dest_volume_db = fake_volume.fake_db_volume(
**{'id': fake.VOLUME2_ID, 'volume_type_id': dest_vol_type_id})
if dest_vol_type_id:
dest_volume_db['volume_type'] = fake_volume.fake_db_volume_type(
id=dest_vol_type_id)
expected_attrs = objects.Volume._get_expected_attrs(self.context)
src_volume = objects.Volume._from_db_object(
self.context, objects.Volume(), src_volume_db,
expected_attrs=expected_attrs)
dest_volume = objects.Volume._from_db_object(
self.context, objects.Volume(), dest_volume_db,
expected_attrs=expected_attrs)
updated_dest_volume = src_volume.finish_volume_migration(
dest_volume)
self.assertEqual('deleting', updated_dest_volume.migration_status)
self.assertEqual('migration src for ' + src_volume.id,
updated_dest_volume.display_description)
self.assertEqual(src_volume.id, updated_dest_volume._name_id)
self.assertTrue(volume_update.called)
volume_update.assert_has_calls([
mock.call(self.context, src_volume.id, mock.ANY),
mock.call(self.context, dest_volume.id, mock.ANY)])
ctxt, vol_id, updates = volume_update.call_args[0]
self.assertNotIn('volume_type', updates)
# Ensure that the destination volume type has not been overwritten
self.assertEqual(dest_vol_type_id,
getattr(updated_dest_volume, 'volume_type_id'))
# Ignore these attributes, since they were updated by
# finish_volume_migration
ignore_keys = ('id', 'provider_location', '_name_id',
'migration_status', 'display_description', 'status',
'volume_glance_metadata', 'volume_type')
dest_vol_dict = {k: updated_dest_volume[k] for k in
updated_dest_volume.keys() if k not in ignore_keys}
src_vol_dict = {k: src_volume[k] for k in src_volume.keys()
if k not in ignore_keys}
self.assertEqual(src_vol_dict, dest_vol_dict)
def test_volume_with_metadata_serialize_deserialize_no_changes(self):
updates = {'volume_glance_metadata': [{'key': 'foo', 'value': 'bar'}],
'expected_attrs': ['glance_metadata']}
volume = fake_volume.fake_volume_obj(self.context, **updates)
serializer = objects.base.CinderObjectSerializer()
serialized_volume = serializer.serialize_entity(self.context, volume)
volume = serializer.deserialize_entity(self.context, serialized_volume)
self.assertDictEqual({}, volume.obj_get_changes())
class TestVolumeList(test_objects.BaseObjectsTestCase):
@mock.patch('cinder.db.volume_get_all')
def test_get_all(self, volume_get_all):
db_volume = fake_volume.fake_db_volume()
volume_get_all.return_value = [db_volume]
volumes = objects.VolumeList.get_all(self.context,
mock.sentinel.marker,
mock.sentinel.limit,
mock.sentinel.sort_key,
mock.sentinel.sort_dir)
self.assertEqual(1, len(volumes))
TestVolume._compare(self, db_volume, volumes[0])
@mock.patch('cinder.db.volume_get_all_by_host')
def test_get_by_host(self, get_all_by_host):
db_volume = fake_volume.fake_db_volume()
get_all_by_host.return_value = [db_volume]
volumes = objects.VolumeList.get_all_by_host(
self.context, 'fake-host')
self.assertEqual(1, len(volumes))
TestVolume._compare(self, db_volume, volumes[0])
@mock.patch('cinder.db.volume_get_all_by_group')
def test_get_by_group(self, get_all_by_group):
db_volume = fake_volume.fake_db_volume()
get_all_by_group.return_value = [db_volume]
volumes = objects.VolumeList.get_all_by_group(
self.context, 'fake-host')
self.assertEqual(1, len(volumes))
TestVolume._compare(self, db_volume, volumes[0])
@mock.patch('cinder.db.volume_get_all_by_project')
def test_get_by_project(self, get_all_by_project):
db_volume = fake_volume.fake_db_volume()
get_all_by_project.return_value = [db_volume]
volumes = objects.VolumeList.get_all_by_project(
self.context, mock.sentinel.project_id, mock.sentinel.marker,
mock.sentinel.limit, mock.sentinel.sorted_keys,
mock.sentinel.sorted_dirs, mock.sentinel.filters)
self.assertEqual(1, len(volumes))
TestVolume._compare(self, db_volume, volumes[0])
@mock.patch('cinder.db.volume_include_in_cluster')
def test_include_in_cluster(self, include_mock):
filters = {'host': mock.sentinel.host,
'cluster_name': mock.sentinel.cluster_name}
cluster = 'new_cluster'
objects.VolumeList.include_in_cluster(self.context, cluster, **filters)
include_mock.assert_called_once_with(self.context, cluster, True,
**filters)
@mock.patch('cinder.db.volume_include_in_cluster')
def test_include_in_cluster_specify_partial(self, include_mock):
filters = {'host': mock.sentinel.host,
'cluster_name': mock.sentinel.cluster_name}
cluster = 'new_cluster'
objects.VolumeList.include_in_cluster(self.context, cluster,
mock.sentinel.partial_rename,
**filters)
include_mock.assert_called_once_with(self.context, cluster,
mock.sentinel.partial_rename,
**filters)