# Copyright 2012 OpenStack Foundation # # 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 mock import uuid from oslo_policy import policy as oslo_policy from oslo_serialization import jsonutils from oslo_utils import timeutils from six.moves import http_client import webob from cinder.api.contrib import volume_image_metadata from cinder import context from cinder import db from cinder import exception from cinder import objects from cinder.objects import fields from cinder.policies import base as base_policy from cinder.policies import volume_metadata as metadata_policy from cinder import policy from cinder import test from cinder.tests.unit.api import fakes from cinder.tests.unit import fake_constants as fake from cinder.tests.unit import fake_volume from cinder import volume def fake_db_volume_get(*args, **kwargs): return { 'id': kwargs.get('volume_id') or fake.VOLUME_ID, 'host': 'host001', 'status': 'available', 'size': 5, 'availability_zone': 'somewhere', 'created_at': timeutils.utcnow(), 'display_name': 'anothervolume', 'display_description': 'Just another volume!', 'volume_type_id': None, 'snapshot_id': None, 'project_id': fake.PROJECT_ID, 'migration_status': None, '_name_id': fake.VOLUME2_ID, 'attach_status': fields.VolumeAttachStatus.DETACHED, } def fake_volume_api_get(*args, **kwargs): ctx = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) db_volume = fake_db_volume_get(volume_id=kwargs.get('volume_id')) return fake_volume.fake_volume_obj(ctx, **db_volume) def fake_volume_get_all(*args, **kwargs): return objects.VolumeList(objects=[fake_volume_api_get(), fake_volume_api_get( volume_id=fake.VOLUME2_ID)]) def fake_volume_get_all_empty(*args, **kwargs): return objects.VolumeList(objects=[]) fake_image_metadata = { 'image_id': fake.IMAGE_ID, 'image_name': 'fake', 'kernel_id': 'somekernel', 'ramdisk_id': 'someramdisk', } def fake_get_volume_image_metadata(*args, **kwargs): return fake_image_metadata def fake_get_volumes_image_metadata(*args, **kwargs): return {'fake': fake_image_metadata} def return_empty_image_metadata(*args, **kwargs): return {} def volume_metadata_delete(context, volume_id, key, meta_type): pass def fake_create_volume_metadata(context, volume_id, metadata, delete, meta_type): return fake_get_volume_image_metadata() def return_volume_nonexistent(*args, **kwargs): raise exception.VolumeNotFound('bogus test message') class VolumeImageMetadataTest(test.TestCase): content_type = 'application/json' def setUp(self): super(VolumeImageMetadataTest, self).setUp() self.mock_object(volume.api.API, 'get', fake_volume_api_get) self.mock_object(volume.api.API, 'get_all', fake_volume_get_all) self.mock_object(volume.api.API, 'get_volume_image_metadata', fake_get_volume_image_metadata) self.mock_object(volume.api.API, 'get_volumes_image_metadata', fake_get_volumes_image_metadata) self.UUID = uuid.uuid4() self.controller = (volume_image_metadata. VolumeImageMetadataController()) self.user_ctxt = context.RequestContext( fake.USER_ID, fake.PROJECT_ID, auth_token=True) def _make_request(self, url): req = webob.Request.blank(url) req.accept = self.content_type res = req.get_response(fakes.wsgi_app( fake_auth_context=self.user_ctxt)) return res def _get_image_metadata(self, body): return jsonutils.loads(body)['volume']['volume_image_metadata'] def _get_image_metadata_list(self, body): return [ volume['volume_image_metadata'] for volume in jsonutils.loads(body)['volumes'] if volume.get('volume_image_metadata') ] def _create_volume_and_glance_metadata(self): ctxt = context.get_admin_context() # create a bootable volume db.volume_create(ctxt, {'id': fake.VOLUME_ID, 'status': 'available', 'host': 'test', 'provider_location': '', 'size': 1}) db.volume_glance_metadata_create(ctxt, fake.VOLUME_ID, 'image_id', fake.IMAGE_ID) db.volume_glance_metadata_create(ctxt, fake.VOLUME_ID, 'image_name', 'fake') db.volume_glance_metadata_create(ctxt, fake.VOLUME_ID, 'kernel_id', 'somekernel') db.volume_glance_metadata_create(ctxt, fake.VOLUME_ID, 'ramdisk_id', 'someramdisk') # create an unbootable volume db.volume_create(ctxt, {'id': fake.VOLUME2_ID, 'status': 'available', 'host': 'test', 'provider_location': '', 'size': 1}) def test_get_volume(self): self._create_volume_and_glance_metadata() res = self._make_request('/v2/%s/volumes/%s' % ( fake.PROJECT_ID, self.UUID)) self.assertEqual(http_client.OK, res.status_int) self.assertEqual(fake_image_metadata, self._get_image_metadata(res.body)) def test_list_detail_volumes(self): self._create_volume_and_glance_metadata() res = self._make_request('/v2/%s/volumes/detail' % fake.PROJECT_ID) self.assertEqual(http_client.OK, res.status_int) self.assertEqual(fake_image_metadata, self._get_image_metadata_list(res.body)[0]) def test_list_detail_empty_volumes(self): def fake_dont_call_this(*args, **kwargs): fake_dont_call_this.called = True fake_dont_call_this.called = False self.mock_object(volume.api.API, 'get_list_volumes_image_metadata', fake_dont_call_this) self.mock_object(volume.api.API, 'get_all', fake_volume_get_all_empty) res = self._make_request('/v2/%s/volumes/detail' % fake.PROJECT_ID) self.assertEqual(http_client.OK, res.status_int) self.assertFalse(fake_dont_call_this.called) def test_list_detail_volumes_with_limit(self): ctxt = context.get_admin_context() db.volume_create(ctxt, {'id': fake.VOLUME_ID, 'status': 'available', 'host': 'test', 'provider_location': '', 'size': 1}) db.volume_glance_metadata_create(ctxt, fake.VOLUME_ID, 'key1', 'value1') db.volume_glance_metadata_create(ctxt, fake.VOLUME_ID, 'key2', 'value2') res = self._make_request('/v2/%s/volumes/detail?limit=1' % fake.PROJECT_ID) self.assertEqual(http_client.OK, res.status_int) self.assertEqual({'key1': 'value1', 'key2': 'value2'}, self._get_image_metadata_list(res.body)[0]) @mock.patch('cinder.objects.Volume.get_by_id') def test_create_image_metadata(self, fake_get): self.mock_object(volume.api.API, 'get_volume_image_metadata', return_empty_image_metadata) self.mock_object(db, 'volume_metadata_update', fake_create_volume_metadata) body = {"os-set_image_metadata": {"metadata": fake_image_metadata}} req = webob.Request.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID)) req.method = "POST" req.body = jsonutils.dump_as_bytes(body) req.headers["content-type"] = "application/json" fake_get.return_value = {} res = req.get_response(fakes.wsgi_app( fake_auth_context=self.user_ctxt)) self.assertEqual(http_client.OK, res.status_int) self.assertEqual(fake_image_metadata, jsonutils.loads(res.body)["metadata"]) @mock.patch('cinder.objects.Volume.get_by_id') def test_create_image_metadata_policy_not_authorized(self, fake_get): rules = { metadata_policy.IMAGE_METADATA_POLICY: base_policy.RULE_ADMIN_API } policy.set_rules(oslo_policy.Rules.from_dict(rules)) self.addCleanup(policy.reset) fake_get.return_value = {} req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID), use_admin_context=False) req.method = 'POST' req.content_type = "application/json" body = {"os-set_image_metadata": { "metadata": {"image_name": "fake"}} } req.body = jsonutils.dump_as_bytes(body) self.assertRaises(exception.PolicyNotAuthorized, self.controller.create, req, fake.VOLUME_ID, body=body) @mock.patch('cinder.objects.Volume.get_by_id') def test_create_with_keys_case_insensitive(self, fake_get): # If the keys in uppercase_and_lowercase, should return the one # which server added self.mock_object(volume.api.API, 'get_volume_image_metadata', return_empty_image_metadata) self.mock_object(db, 'volume_metadata_update', fake_create_volume_metadata) fake_get.return_value = {} body = { "os-set_image_metadata": { "metadata": { "Image_Id": "someid", "image_name": "fake", "Kernel_id": "somekernel", "ramdisk_id": "someramdisk" }, }, } req = webob.Request.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID)) req.method = 'POST' req.body = jsonutils.dump_as_bytes(body) req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app( fake_auth_context=self.user_ctxt)) self.assertEqual(http_client.OK, res.status_int) self.assertEqual(fake_image_metadata, jsonutils.loads(res.body)["metadata"]) @mock.patch('cinder.objects.Volume.get_by_id') def test_create_empty_body(self, fake_get): req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID)) req.method = 'POST' req.headers["content-type"] = "application/json" fake_get.return_value = {} self.assertRaises(exception.ValidationError, self.controller.create, req, fake.VOLUME_ID, body=None) def test_create_nonexistent_volume(self): self.mock_object(volume.api.API, 'get', return_volume_nonexistent) req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID)) req.method = 'POST' req.content_type = "application/json" body = {"os-set_image_metadata": { "metadata": {"image_name": "fake"}} } req.body = jsonutils.dump_as_bytes(body) self.assertRaises(exception.VolumeNotFound, self.controller.create, req, fake.VOLUME_ID, body=body) @mock.patch('cinder.objects.Volume.get_by_id') def test_invalid_metadata_items_on_create(self, fake_get): self.mock_object(db, 'volume_metadata_update', fake_create_volume_metadata) req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID)) req.method = 'POST' req.headers["content-type"] = "application/json" data = {"os-set_image_metadata": { "metadata": {"a" * 260: "value1"}} } fake_get.return_value = {} # Test for long key req.body = jsonutils.dump_as_bytes(data) self.assertRaises(exception.ValidationError, self.controller.create, req, fake.VOLUME_ID, body=data) # Test for long value data = {"os-set_image_metadata": { "metadata": {"key": "v" * 260}} } req.body = jsonutils.dump_as_bytes(data) self.assertRaises(exception.ValidationError, self.controller.create, req, fake.VOLUME_ID, body=data) # Test for empty key. data = {"os-set_image_metadata": { "metadata": {"": "value1"}} } req.body = jsonutils.dump_as_bytes(data) self.assertRaises(exception.ValidationError, self.controller.create, req, fake.VOLUME_ID, body=data) @mock.patch('cinder.objects.Volume.get_by_id') def test_delete(self, fake_get): self.mock_object(db, 'volume_metadata_delete', volume_metadata_delete) body = {"os-unset_image_metadata": { "key": "ramdisk_id"} } req = webob.Request.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID)) req.method = 'POST' req.body = jsonutils.dump_as_bytes(body) req.headers["content-type"] = "application/json" fake_get.return_value = {} res = req.get_response(fakes.wsgi_app( fake_auth_context=self.user_ctxt)) self.assertEqual(http_client.OK, res.status_int) @mock.patch('cinder.objects.Volume.get_by_id') def test_delete_image_metadata_policy_not_authorized(self, fake_get): rules = { metadata_policy.IMAGE_METADATA_POLICY: base_policy.RULE_ADMIN_API } policy.set_rules(oslo_policy.Rules.from_dict(rules)) self.addCleanup(policy.reset) fake_get.return_value = {} req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID), use_admin_context=False) req.method = 'POST' req.content_type = "application/json" body = {"os-unset_image_metadata": { "metadata": {"image_name": "fake"}} } req.body = jsonutils.dump_as_bytes(body) self.assertRaises(exception.ValidationError, self.controller.delete, req, fake.VOLUME_ID, body=None) @mock.patch('cinder.objects.Volume.get_by_id') def test_delete_meta_not_found(self, fake_get): data = {"os-unset_image_metadata": { "key": "invalid_id"} } req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID)) req.method = 'POST' req.body = jsonutils.dump_as_bytes(data) req.headers["content-type"] = "application/json" fake_get.return_value = {} self.assertRaises(exception.GlanceMetadataNotFound, self.controller.delete, req, fake.VOLUME_ID, body=data) @mock.patch('cinder.objects.Volume.get_by_id') def test_delete_nonexistent_volume(self, fake_get): self.mock_object(db, 'volume_metadata_delete', return_volume_nonexistent) body = {"os-unset_image_metadata": { "key": "fake"} } req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID)) req.method = 'POST' req.body = jsonutils.dump_as_bytes(body) req.headers["content-type"] = "application/json" fake_get.return_value = {} self.assertRaises(exception.GlanceMetadataNotFound, self.controller.delete, req, fake.VOLUME_ID, body=body) def test_delete_empty_body(self): req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID)) req.method = 'POST' req.headers["content-type"] = "application/json" self.assertRaises(exception.ValidationError, self.controller.delete, req, fake.VOLUME_ID, body=None) def test_show_image_metadata(self): body = {"os-show_image_metadata": None} req = webob.Request.blank('/v2/%s/volumes/%s/action' % ( fake.PROJECT_ID, fake.VOLUME_ID)) req.method = 'POST' req.body = jsonutils.dump_as_bytes(body) req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app( fake_auth_context=self.user_ctxt)) self.assertEqual(http_client.OK, res.status_int) self.assertEqual(fake_image_metadata, jsonutils.loads(res.body)["metadata"])