# # 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 json import fixtures import mock from oslo_serialization import jsonutils from oslo_utils import strutils from six.moves import http_client import webob from cinder.api import api_utils from cinder.api import common from cinder.api import extensions from cinder.api import microversions as mv from cinder.api.v2.views.volumes import ViewBuilder from cinder.api.v3 import volumes from cinder.backup import api as backup_api from cinder import context from cinder import db from cinder import exception from cinder.group import api as group_api from cinder import objects from cinder.objects import fields from cinder.policies import volumes as policy 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.image import fake as fake_image from cinder.tests.unit import utils as test_utils from cinder.volume import api as volume_api from cinder.volume import api as vol_get 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 = {} fake_image.mock_image_service(self) self.controller = volumes.VolumeController(self.ext_mgr) self.flags(host='fake') self.ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) # This will be cleaned up by the NestedTempfile fixture in base class self.tmp_path = self.useFixture(fixtures.TempDir()).path def test_check_volume_filters_called(self): # Clear the filters collection to make sure the filters collection # cache can be reloaded using tmp filter file. common._FILTERS_COLLECTION = None 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 = mv.get_mv_header(mv.BASE_VERSION) req.environ['cinder.context'].is_admin = True tmp_filter_file = self.tmp_path + '/resource_filters_tests.json' self.override_config('resource_query_filters_file', tmp_filter_file) with open(tmp_filter_file, 'w') as f: f.write(json.dumps({"volume": ['bootable']})) self.controller.index(req) filters = req.params.copy() volume_get.assert_called_with(filters, False) # Reset the CONF.resource_query_filters_file and clear the filters # collection to avoid leaking other cases, and it will be re-loaded # from CONF.resource_query_filters_file in next call. self._reset_filter_file() def test_check_volume_filters_strict_called(self): # Clear the filters collection to make sure the filters collection # cache can be reloaded using tmp filter file. common._FILTERS_COLLECTION = None 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 = mv.get_mv_header(mv.VOLUME_LIST_BOOTABLE) req.environ['cinder.context'].is_admin = True req.api_version_request = mv.get_api_version( mv.VOLUME_LIST_BOOTABLE) tmp_filter_file = self.tmp_path + '/resource_filters_tests.json' self.override_config('resource_query_filters_file', tmp_filter_file) with open(tmp_filter_file, 'w') as f: f.write(json.dumps({"volume": ['bootable']})) self.controller.index(req) filters = req.params.copy() volume_get.assert_called_with(filters, True) # Reset the CONF.resource_query_filters_file and clear the filters # collection to avoid leaking other cases, and it will be re-loaded # from CONF.resource_query_filters_file in next call. self._reset_filter_file() 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 _create_multiple_volumes_with_different_project(self): # Create volumes in project 1 db.volume_create(self.ctxt, {'display_name': 'test1', 'project_id': fake.PROJECT_ID}) db.volume_create(self.ctxt, {'display_name': 'test2', 'project_id': fake.PROJECT_ID}) # Create volume in project 2 db.volume_create(self.ctxt, {'display_name': 'test3', 'project_id': fake.PROJECT2_ID}) 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 = mv.get_mv_header(mv.VOLUME_LIST_GLANCE_METADATA) req.api_version_request = mv.get_api_version( mv.VOLUME_LIST_GLANCE_METADATA) 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 = mv.get_mv_header(mv.BASE_VERSION) req.api_version_request = mv.get_api_version(mv.BASE_VERSION) 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 = mv.get_mv_header(mv.VOLUME_LIST_GROUP) req.api_version_request = mv.get_api_version(mv.VOLUME_LIST_GROUP) 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']) @ddt.data('volumes', 'volumes/detail') def test_list_volume_with_count_param_version_not_matched(self, action): self._create_multiple_volumes_with_different_project() is_detail = True if 'detail' in action else False req = fakes.HTTPRequest.blank("/v3/%s?with_count=True" % action) req.headers = mv.get_mv_header( mv.get_prior_version(mv.SUPPORT_COUNT_INFO)) req.api_version_request = mv.get_api_version( mv.get_prior_version(mv.SUPPORT_COUNT_INFO)) ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) req.environ['cinder.context'] = ctxt res_dict = self.controller._get_volumes(req, is_detail=is_detail) self.assertNotIn('count', res_dict) @ddt.data({'method': 'volumes', 'display_param': 'True'}, {'method': 'volumes', 'display_param': 'False'}, {'method': 'volumes', 'display_param': '1'}, {'method': 'volumes/detail', 'display_param': 'True'}, {'method': 'volumes/detail', 'display_param': 'False'}, {'method': 'volumes/detail', 'display_param': '1'} ) @ddt.unpack def test_list_volume_with_count_param(self, method, display_param): self._create_multiple_volumes_with_different_project() is_detail = True if 'detail' in method else False show_count = strutils.bool_from_string(display_param, strict=True) # Request with 'with_count' and 'limit' req = fakes.HTTPRequest.blank( "/v3/%s?with_count=%s&limit=1" % (method, display_param)) req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, False) req.environ['cinder.context'] = ctxt res_dict = self.controller._get_volumes(req, is_detail=is_detail) self.assertEqual(1, len(res_dict['volumes'])) if show_count: self.assertEqual(2, res_dict['count']) else: self.assertNotIn('count', res_dict) # Request with 'with_count' req = fakes.HTTPRequest.blank( "/v3/%s?with_count=%s" % (method, display_param)) req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, False) req.environ['cinder.context'] = ctxt res_dict = self.controller._get_volumes(req, is_detail=is_detail) self.assertEqual(2, len(res_dict['volumes'])) if show_count: self.assertEqual(2, res_dict['count']) else: self.assertNotIn('count', res_dict) # Request with admin context and 'all_tenants' req = fakes.HTTPRequest.blank( "/v3/%s?with_count=%s&all_tenants=1" % (method, display_param)) req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) req.environ['cinder.context'] = ctxt res_dict = self.controller._get_volumes(req, is_detail=is_detail) self.assertEqual(3, len(res_dict['volumes'])) if show_count: self.assertEqual(3, res_dict['count']) else: self.assertNotIn('count', res_dict) 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 = mv.get_mv_header(mv.BACKUP_UPDATE) req.api_version_request = mv.get_api_version(mv.BACKUP_UPDATE) 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=mv.VOLUME_SUMMARY, 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 = mv.get_mv_header(version) req.api_version_request = mv.get_api_version(version) return req @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) @mock.patch( 'cinder.api.openstack.wsgi.Controller.validate_name_and_description') def test_volume_create_with_snapshot_image(self, mock_validate, create, get_snapshot, volume_type_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 vol = self._vol_in_request_body( image_id="b0a599e0-41d7-3582-b260-769f443c862a") snapshot_id = fake.SNAPSHOT_ID ex = self._expected_vol_from_controller(snapshot_id=snapshot_id) body = {"volume": vol} req = fakes.HTTPRequest.blank('/v3/volumes') req.headers = mv.get_mv_header(mv.SUPPORT_NOVA_IMAGE) req.api_version_request = mv.get_api_version(mv.SUPPORT_NOVA_IMAGE) res_dict = self.controller.create(req, body=body) 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)) create.assert_called_once_with( self.controller.volume_api, context, vol['size'], v2_fakes.DEFAULT_VOL_NAME, v2_fakes.DEFAULT_VOL_DESCRIPTION, **kwargs) 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=mv.get_prior_version(mv.VOLUME_SUMMARY)) 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=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( (mv.get_prior_version(mv.VOLUME_SUMMARY_METADATA), {'volume-summary': {'total_size': 0.0, 'total_count': 0}}), (mv.VOLUME_SUMMARY_METADATA, {'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( (mv.get_prior_version(mv.VOLUME_SUMMARY_METADATA), {'volume-summary': {'total_size': 2, 'total_count': 2}}), (mv.VOLUME_SUMMARY_METADATA, {'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( (mv.get_prior_version(mv.VOLUME_SUMMARY_METADATA), {'volume-summary': {'total_size': 2, 'total_count': 2}}), (mv.VOLUME_SUMMARY_METADATA, {'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, consistencygroup_id=None, volume_type=None, image_ref=None, image_id=None, group_id=None, backup_id=None): vol = {"size": size, "name": name, "description": description, "availability_zone": availability_zone, "snapshot_id": snapshot_id, "source_volid": source_volid, "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 elif backup_id is not None: vol['backup_id'] = backup_id 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.UTC), 'updated_at': datetime.datetime( 1900, 1, 1, 1, 1, 1, tzinfo=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 GROUP_VOLUME. if req_version and req_version.matches( None, mv.get_prior_version(mv.GROUP_VOLUME)): 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, 'consistencygroup': None, 'availability_zone': availability_zone, 'scheduler_hints': None, 'multiattach': False, 'group': test_group, } # Remove group_id if max version is less than GROUP_VOLUME. if req_version and req_version.matches( None, mv.get_prior_version(mv.GROUP_VOLUME)): volume.pop('group') return volume @ddt.data((mv.GROUP_VOLUME, {'display_name': ' test name ', 'display_description': ' test desc ', 'size': 1}), (mv.get_prior_version(mv.GROUP_VOLUME), {'name': ' test name ', 'description': ' test desc ', 'size': 1}), ('3.0', {'name': 'test name', 'description': 'test desc', 'size': 1, 'user_id': 'teapot', 'project_id': 'kettle', 'status': 'confused'})) @ddt.unpack def test_volume_create(self, max_ver, volume_body): 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) req = fakes.HTTPRequest.blank('/v3/volumes') req.api_version_request = mv.get_api_version(max_ver) body = {'volume': volume_body} res_dict = self.controller.create(req, body=body) ex = self._expected_vol_from_controller( req_version=req.api_version_request, name='test name', description='test desc') self.assertEqual(ex['volume']['name'], res_dict['volume']['name']) self.assertEqual(ex['volume']['description'], res_dict['volume']['description']) def test_volume_create_extra_params(self): 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) req = fakes.HTTPRequest.blank('/v3/volumes') req.api_version_request = mv.get_api_version( mv.SUPPORT_VOLUME_SCHEMA_CHANGES) body = {'volume': { 'name': 'test name', 'description': 'test desc', 'size': 1, 'user_id': 'teapot', 'project_id': 'kettle', 'status': 'confused'}} self.assertRaises(exception.ValidationError, self.controller.create, req, body=body) @ddt.data(mv.get_prior_version(mv.VOLUME_DELETE_FORCE), mv.VOLUME_DELETE_FORCE) @mock.patch('cinder.context.RequestContext.authorize') def test_volume_delete_with_force(self, request_version, mock_authorize): mock_delete = self.mock_object(volume_api.API, "delete") self.mock_object(volume_api.API, 'get', return_value="fake_volume") req = fakes.HTTPRequest.blank('/v3/volumes/fake_id?force=True') req.api_version_request = mv.get_api_version(request_version) self.controller.delete(req, 'fake_id') context = req.environ['cinder.context'] if request_version == mv.VOLUME_DELETE_FORCE: mock_authorize.assert_called_with(policy.FORCE_DELETE_POLICY, target_obj="fake_volume") mock_delete.assert_called_with(context, "fake_volume", cascade=False, force=True) else: mock_authorize.assert_not_called() mock_delete.assert_called_with(context, "fake_volume", cascade=False, force=False) @ddt.data(mv.GROUP_SNAPSHOTS, mv.get_prior_version(mv.GROUP_SNAPSHOTS)) @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 = mv.get_api_version(max_ver) res_dict = self.controller.create(req, body=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(mv.VOLUME_CREATE_FROM_BACKUP, mv.get_prior_version(mv.VOLUME_CREATE_FROM_BACKUP)) @mock.patch.object(db.sqlalchemy.api, '_volume_type_get_full', autospec=True) @mock.patch.object(backup_api.API, 'get', autospec=True) @mock.patch.object(volume_api.API, 'create', autospec=True) def test_volume_creation_from_backup(self, max_ver, create, get_backup, volume_type_get): create.side_effect = v2_fakes.fake_volume_api_create get_backup.side_effect = v2_fakes.fake_backup_get volume_type_get.side_effect = v2_fakes.fake_volume_type_get backup_id = fake.BACKUP_ID req = fakes.HTTPRequest.blank('/v3/volumes') req.api_version_request = mv.get_api_version(max_ver) if max_ver == mv.VOLUME_CREATE_FROM_BACKUP: vol = self._vol_in_request_body(backup_id=backup_id) else: vol = self._vol_in_request_body() body = {"volume": vol} res_dict = self.controller.create(req, body=body) ex = self._expected_vol_from_controller( req_version=req.api_version_request) self.assertEqual(ex, res_dict) context = req.environ['cinder.context'] kwargs = self._expected_volume_api_create_kwargs( req_version=req.api_version_request) if max_ver >= mv.VOLUME_CREATE_FROM_BACKUP: get_backup.assert_called_once_with(self.controller.backup_api, context, backup_id) kwargs.update({'backup': v2_fakes.fake_backup_get(None, context, backup_id)}) create.assert_called_once_with(self.controller.volume_api, context, vol['size'], v2_fakes.DEFAULT_VOL_NAME, v2_fakes.DEFAULT_VOL_DESCRIPTION, **kwargs) def test_volume_creation_with_scheduler_hints(self): vol = self._vol_in_request_body(availability_zone=None) vol.pop('group_id') body = {"volume": vol, "OS-SCH-HNT:scheduler_hints": { 'different_host': [fake.UUID1, fake.UUID2]}} req = webob.Request.blank('/v3/%s/volumes' % fake.PROJECT_ID) req.method = 'POST' req.headers['Content-Type'] = 'application/json' req.body = jsonutils.dump_as_bytes(body) res = req.get_response(fakes.wsgi_app( fake_auth_context=self.ctxt)) res_dict = jsonutils.loads(res.body) self.assertEqual(http_client.ACCEPTED, res.status_int) self.assertIn('id', res_dict['volume']) @ddt.data('fake_host', '', 1234, ' ') def test_volume_creation_invalid_scheduler_hints(self, invalid_hints): vol = self._vol_in_request_body() vol.pop('group_id') body = {"volume": vol, "OS-SCH-HNT:scheduler_hints": { 'different_host': invalid_hints}} req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(exception.ValidationError, self.controller.create, req, body=body) @ddt.data({'size': 'a'}, {'size': ''}, {'size': 0}, {'size': 2 ** 31}) def test_volume_creation_fails_with_invalid_parameters( self, vol_body): body = {"volume": vol_body} req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(exception.ValidationError, self.controller.create, req, body=body) def test_volume_creation_fails_with_additional_properties(self): body = {"volume": {"size": 1, "user_id": fake.USER_ID, "project_id": fake.PROJECT_ID}} req = fakes.HTTPRequest.blank('/v3/volumes') req.api_version_request = mv.get_api_version( mv.SUPPORT_VOLUME_SCHEMA_CHANGES) self.assertRaises(exception.ValidationError, self.controller.create, req, body=body) def test_volume_update_without_vol_data(self): body = {"volume": {}} req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) req.api_version_request = mv.get_api_version( mv.SUPPORT_VOLUME_SCHEMA_CHANGES) self.assertRaises(exception.ValidationError, self.controller.update, req, fake.VOLUME_ID, body=body) @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(exception.ValidationError, self.controller.create, req, body=body) @ddt.data({'source_volid': 1}, {'source_volid': []}, {'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(exception.ValidationError, self.controller.create, req, body=body) @ddt.data(mv.get_prior_version(mv.RESOURCE_FILTER), mv.RESOURCE_FILTER, mv.LIKE_FILTER) @mock.patch.object(volume_api.API, 'check_volume_filters', mock.Mock()) @mock.patch.object(api_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 >= mv.RESOURCE_FILTER: support_like = True if version == mv.LIKE_FILTER else False mock_update.assert_called_once_with(req.environ['cinder.context'], mock.ANY, 'volume', support_like) @ddt.data({'admin': True, 'version': mv.VOLUME_DETAIL_PROVIDER_ID}, {'admin': False, 'version': mv.VOLUME_DETAIL_PROVIDER_ID}, {'admin': True, 'version': mv.get_prior_version(mv.VOLUME_DETAIL_PROVIDER_ID)}, {'admin': False, 'version': mv.get_prior_version(mv.VOLUME_DETAIL_PROVIDER_ID)}) @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 # VOLUME_DETAIL_PROVIDER_ID for admin. if req_version.matches(mv.VOLUME_DETAIL_PROVIDER_ID, None) and admin: self.assertIn('provider_id', res_dict['volume']) else: self.assertNotIn('provider_id', res_dict['volume']) def _fake_create_volume(self, size=1): vol = { 'display_name': 'fake_volume1', 'status': 'available', 'size': size } volume = objects.Volume(context=self.ctxt, **vol) volume.create() return volume def _fake_create_snapshot(self, volume_id, volume_size=1): snap = { 'display_name': 'fake_snapshot1', 'status': 'available', 'volume_id': volume_id, 'volume_size': volume_size } snapshot = objects.Snapshot(context=self.ctxt, **snap) snapshot.create() return snapshot @mock.patch.object(objects.Volume, 'get_latest_snapshot') @mock.patch.object(volume_api.API, 'get_volume') def test_volume_revert_with_snapshot_not_found(self, mock_volume, mock_latest): fake_volume = self._fake_create_volume() mock_volume.return_value = fake_volume mock_latest.side_effect = exception.VolumeSnapshotNotFound(volume_id= 'fake_id') req = fakes.HTTPRequest.blank('/v3/volumes/fake_id/revert') req.headers = mv.get_mv_header(mv.VOLUME_REVERT) req.api_version_request = mv.get_api_version( mv.VOLUME_REVERT) self.assertRaises(webob.exc.HTTPBadRequest, self.controller.revert, req, 'fake_id', {'revert': {'snapshot_id': 'fake_snapshot_id'}}) @mock.patch.object(objects.Volume, 'get_latest_snapshot') @mock.patch.object(volume_api.API, 'get_volume') def test_volume_revert_with_snapshot_not_match(self, mock_volume, mock_latest): fake_volume = self._fake_create_volume() mock_volume.return_value = fake_volume fake_snapshot = self._fake_create_snapshot(fake.UUID1) mock_latest.return_value = fake_snapshot req = fakes.HTTPRequest.blank('/v3/volumes/fake_id/revert') req.headers = mv.get_mv_header(mv.VOLUME_REVERT) req.api_version_request = mv.get_api_version( mv.VOLUME_REVERT) self.assertRaises(webob.exc.HTTPBadRequest, self.controller.revert, req, 'fake_id', {'revert': {'snapshot_id': 'fake_snapshot_id'}}) @mock.patch.object(objects.Volume, 'get_latest_snapshot') @mock.patch('cinder.objects.base.' 'CinderPersistentObject.update_single_status_where') @mock.patch.object(volume_api.API, 'get_volume') def test_volume_revert_update_status_failed(self, mock_volume, mock_update, mock_latest): fake_volume = self._fake_create_volume() fake_snapshot = self._fake_create_snapshot(fake_volume['id']) mock_volume.return_value = fake_volume mock_latest.return_value = fake_snapshot req = fakes.HTTPRequest.blank('/v3/volumes/%s/revert' % fake_volume['id']) req.headers = mv.get_mv_header(mv.VOLUME_REVERT) req.api_version_request = mv.get_api_version( mv.VOLUME_REVERT) # update volume's status failed mock_update.side_effect = [False, True] self.assertRaises(webob.exc.HTTPConflict, self.controller.revert, req, fake_volume['id'], {'revert': {'snapshot_id': fake_snapshot['id']}}) # update snapshot's status failed mock_update.side_effect = [True, False] self.assertRaises(webob.exc.HTTPConflict, self.controller.revert, req, fake_volume['id'], {'revert': {'snapshot_id': fake_snapshot['id']}}) @mock.patch.object(objects.Volume, 'get_latest_snapshot') @mock.patch.object(volume_api.API, 'get_volume') def test_volume_revert_with_not_equal_size(self, mock_volume, mock_latest): fake_volume = self._fake_create_volume(size=2) fake_snapshot = self._fake_create_snapshot(fake_volume['id'], volume_size=1) mock_volume.return_value = fake_volume mock_latest.return_value = fake_snapshot req = fakes.HTTPRequest.blank('/v3/volumes/%s/revert' % fake_volume['id']) req.headers = mv.get_mv_header(mv.VOLUME_REVERT) req.api_version_request = mv.get_api_version( mv.VOLUME_REVERT) self.assertRaises(webob.exc.HTTPBadRequest, self.controller.revert, req, fake_volume['id'], {'revert': {'snapshot_id': fake_snapshot['id']}}) def test_view_get_attachments(self): fake_volume = self._fake_create_volume() fake_volume['attach_status'] = fields.VolumeAttachStatus.ATTACHING att_time = datetime.datetime(2017, 8, 31, 21, 55, 7, tzinfo=iso8601.UTC) a1 = { 'id': fake.UUID1, 'volume_id': fake.UUID2, 'instance': None, 'attached_host': None, 'mountpoint': None, 'attach_time': None, 'attach_status': fields.VolumeAttachStatus.ATTACHING } a2 = { 'id': fake.UUID3, 'volume_id': fake.UUID4, 'instance_uuid': fake.UUID5, 'attached_host': 'host1', 'mountpoint': 'na', 'attach_time': att_time, 'attach_status': fields.VolumeAttachStatus.ATTACHED } attachment1 = objects.VolumeAttachment(self.ctxt, **a1) attachment2 = objects.VolumeAttachment(self.ctxt, **a2) atts = {'objects': [attachment1, attachment2]} attachments = objects.VolumeAttachmentList(self.ctxt, **atts) fake_volume['volume_attachment'] = attachments # get_attachments should only return attachments with the # attached status = ATTACHED attachments = ViewBuilder()._get_attachments(fake_volume) self.assertEqual(1, len(attachments)) self.assertEqual(fake.UUID3, attachments[0]['attachment_id']) self.assertEqual(fake.UUID4, attachments[0]['volume_id']) self.assertEqual(fake.UUID5, attachments[0]['server_id']) self.assertEqual('host1', attachments[0]['host_name']) self.assertEqual('na', attachments[0]['device']) self.assertEqual(att_time, attachments[0]['attached_at'])