--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 OpenStack Foundation.
+# 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.
+
+import webob
+
+from cinder.api import common
+from cinder.api.openstack import wsgi
+from cinder import exception
+from cinder import volume
+
+
+class Controller(object):
+ """The volume metadata API controller for the OpenStack API."""
+
+ def __init__(self):
+ self.volume_api = volume.API()
+ super(Controller, self).__init__()
+
+ def _get_metadata(self, context, volume_id):
+ try:
+ volume = self.volume_api.get(context, volume_id)
+ meta = self.volume_api.get_volume_metadata(context, volume)
+ except exception.VolumeNotFound:
+ msg = _('volume does not exist')
+ raise webob.exc.HTTPNotFound(explanation=msg)
+ return meta
+
+ @wsgi.serializers(xml=common.MetadataTemplate)
+ def index(self, req, volume_id):
+ """Returns the list of metadata for a given volume."""
+ context = req.environ['cinder.context']
+ return {'metadata': self._get_metadata(context, volume_id)}
+
+ @wsgi.serializers(xml=common.MetadataTemplate)
+ @wsgi.deserializers(xml=common.MetadataDeserializer)
+ def create(self, req, volume_id, body):
+ try:
+ metadata = body['metadata']
+ except (KeyError, TypeError):
+ msg = _("Malformed request body")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ context = req.environ['cinder.context']
+
+ new_metadata = self._update_volume_metadata(context,
+ volume_id,
+ metadata,
+ delete=False)
+
+ return {'metadata': new_metadata}
+
+ @wsgi.serializers(xml=common.MetaItemTemplate)
+ @wsgi.deserializers(xml=common.MetaItemDeserializer)
+ def update(self, req, volume_id, id, body):
+ try:
+ meta_item = body['meta']
+ except (TypeError, KeyError):
+ expl = _('Malformed request body')
+ raise webob.exc.HTTPBadRequest(explanation=expl)
+
+ if id not in meta_item:
+ expl = _('Request body and URI mismatch')
+ raise webob.exc.HTTPBadRequest(explanation=expl)
+
+ if len(meta_item) > 1:
+ expl = _('Request body contains too many items')
+ raise webob.exc.HTTPBadRequest(explanation=expl)
+
+ context = req.environ['cinder.context']
+ self._update_volume_metadata(context,
+ volume_id,
+ meta_item,
+ delete=False)
+
+ return {'meta': meta_item}
+
+ @wsgi.serializers(xml=common.MetadataTemplate)
+ @wsgi.deserializers(xml=common.MetadataDeserializer)
+ def update_all(self, req, volume_id, body):
+ try:
+ metadata = body['metadata']
+ except (TypeError, KeyError):
+ expl = _('Malformed request body')
+ raise webob.exc.HTTPBadRequest(explanation=expl)
+
+ context = req.environ['cinder.context']
+ new_metadata = self._update_volume_metadata(context,
+ volume_id,
+ metadata,
+ delete=True)
+
+ return {'metadata': new_metadata}
+
+ def _update_volume_metadata(self, context,
+ volume_id, metadata,
+ delete=False):
+ try:
+ volume = self.volume_api.get(context, volume_id)
+ return self.volume_api.update_volume_metadata(context,
+ volume,
+ metadata,
+ delete)
+ except exception.VolumeNotFound:
+ msg = _('volume does not exist')
+ raise webob.exc.HTTPNotFound(explanation=msg)
+
+ except (ValueError, AttributeError):
+ msg = _("Malformed request body")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ except exception.InvalidVolumeMetadata as error:
+ raise webob.exc.HTTPBadRequest(explanation=error.msg)
+
+ except exception.InvalidVolumeMetadataSize as error:
+ raise webob.exc.HTTPRequestEntityTooLarge(explanation=error.msg)
+
+ @wsgi.serializers(xml=common.MetaItemTemplate)
+ def show(self, req, volume_id, id):
+ """Return a single metadata item."""
+ context = req.environ['cinder.context']
+ data = self._get_metadata(context, volume_id)
+
+ try:
+ return {'meta': {id: data[id]}}
+ except KeyError:
+ msg = _("Metadata item was not found")
+ raise webob.exc.HTTPNotFound(explanation=msg)
+
+ def delete(self, req, volume_id, id):
+ """Deletes an existing metadata."""
+ context = req.environ['cinder.context']
+
+ metadata = self._get_metadata(context, volume_id)
+
+ if id not in metadata:
+ msg = _("Metadata item was not found")
+ raise webob.exc.HTTPNotFound(explanation=msg)
+
+ try:
+ volume = self.volume_api.get(context, volume_id)
+ self.volume_api.delete_volume_metadata(context, volume, id)
+ except exception.VolumeNotFound:
+ msg = _('volume does not exist')
+ raise webob.exc.HTTPNotFound(explanation=msg)
+ return webob.Response(status_int=200)
+
+
+def create_resource():
+ return wsgi.Resource(Controller())
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 OpenStack Foundation.
+# 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.
+
+import uuid
+
+from oslo.config import cfg
+import webob
+
+from cinder.api import extensions
+from cinder.api.v2 import volume_metadata
+from cinder.api.v2 import volumes
+from cinder import db
+from cinder import exception
+from cinder.openstack.common import jsonutils
+from cinder import test
+from cinder.tests.api import fakes
+from cinder.tests.api.v2 import stubs
+from cinder.volume import api as volume_api
+
+
+CONF = cfg.CONF
+
+
+def return_create_volume_metadata_max(context, volume_id, metadata, delete):
+ return stub_max_volume_metadata()
+
+
+def return_create_volume_metadata(context, volume_id, metadata, delete):
+ return stub_volume_metadata()
+
+
+def return_volume_metadata(context, volume_id):
+ if not isinstance(volume_id, str) or not len(volume_id) == 36:
+ msg = 'id %s must be a uuid in return volume metadata' % volume_id
+ raise Exception(msg)
+ return stub_volume_metadata()
+
+
+def return_empty_volume_metadata(context, volume_id):
+ return {}
+
+
+def delete_volume_metadata(context, volume_id, key):
+ pass
+
+
+def stub_volume_metadata():
+ metadata = {
+ "key1": "value1",
+ "key2": "value2",
+ "key3": "value3",
+ }
+ return metadata
+
+
+def stub_max_volume_metadata():
+ metadata = {"metadata": {}}
+ for num in range(CONF.quota_metadata_items):
+ metadata['metadata']['key%i' % num] = "blah"
+ return metadata
+
+
+def return_volume(context, volume_id):
+ return {'id': '0cc3346e-9fef-4445-abe6-5d2b2690ec64',
+ 'name': 'fake',
+ 'metadata': {}}
+
+
+def return_volume_nonexistent(context, volume_id):
+ raise exception.VolumeNotFound('bogus test message')
+
+
+def fake_update_volume_metadata(self, context, volume, diff):
+ pass
+
+
+class volumeMetaDataTest(test.TestCase):
+
+ def setUp(self):
+ super(volumeMetaDataTest, self).setUp()
+ self.volume_api = volume_api.API()
+ fakes.stub_out_key_pair_funcs(self.stubs)
+ self.stubs.Set(db, 'volume_get', return_volume)
+ self.stubs.Set(db, 'volume_metadata_get',
+ return_volume_metadata)
+ self.stubs.Set(db, 'service_get_all_by_topic',
+ stubs.stub_service_get_all_by_topic)
+
+ self.stubs.Set(self.volume_api, 'update_volume_metadata',
+ fake_update_volume_metadata)
+
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.volume_controller = volumes.VolumeController(self.ext_mgr)
+ self.controller = volume_metadata.Controller()
+ self.req_id = str(uuid.uuid4())
+ self.url = '/v2/fake/volumes/%s/metadata' % self.req_id
+
+ vol = {"size": 100,
+ "display_name": "Volume Test Name",
+ "display_description": "Volume Test Desc",
+ "availability_zone": "zone1:host1",
+ "metadata": {}}
+ body = {"volume": vol}
+ req = fakes.HTTPRequest.blank('/v2/volumes')
+ self.volume_controller.create(req, body)
+
+ def test_index(self):
+ req = fakes.HTTPRequest.blank(self.url)
+ res_dict = self.controller.index(req, self.req_id)
+
+ expected = {
+ 'metadata': {
+ 'key1': 'value1',
+ 'key2': 'value2',
+ 'key3': 'value3',
+ },
+ }
+ self.assertEqual(expected, res_dict)
+
+ def test_index_nonexistent_volume(self):
+ self.stubs.Set(db, 'volume_metadata_get',
+ return_volume_nonexistent)
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.index, req, self.url)
+
+ def test_index_no_data(self):
+ self.stubs.Set(db, 'volume_metadata_get',
+ return_empty_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url)
+ res_dict = self.controller.index(req, self.req_id)
+ expected = {'metadata': {}}
+ self.assertEqual(expected, res_dict)
+
+ def test_show(self):
+ req = fakes.HTTPRequest.blank(self.url + '/key2')
+ res_dict = self.controller.show(req, self.req_id, 'key2')
+ expected = {'meta': {'key2': 'value2'}}
+ self.assertEqual(expected, res_dict)
+
+ def test_show_nonexistent_volume(self):
+ self.stubs.Set(db, 'volume_metadata_get',
+ return_volume_nonexistent)
+ req = fakes.HTTPRequest.blank(self.url + '/key2')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.show, req, self.req_id, 'key2')
+
+ def test_show_meta_not_found(self):
+ self.stubs.Set(db, 'volume_metadata_get',
+ return_empty_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key6')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.show, req, self.req_id, 'key6')
+
+ def test_delete(self):
+ self.stubs.Set(db, 'volume_metadata_get',
+ return_volume_metadata)
+ self.stubs.Set(db, 'volume_metadata_delete',
+ delete_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key2')
+ req.method = 'DELETE'
+ res = self.controller.delete(req, self.req_id, 'key2')
+
+ self.assertEqual(200, res.status_int)
+
+ def test_delete_nonexistent_volume(self):
+ self.stubs.Set(db, 'volume_get',
+ return_volume_nonexistent)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'DELETE'
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.delete, req, self.req_id, 'key1')
+
+ def test_delete_meta_not_found(self):
+ self.stubs.Set(db, 'volume_metadata_get',
+ return_empty_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key6')
+ req.method = 'DELETE'
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.delete, req, self.req_id, 'key6')
+
+ def test_create(self):
+ self.stubs.Set(db, 'volume_metadata_get',
+ return_empty_volume_metadata)
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+
+ req = fakes.HTTPRequest.blank('/v2/volume_metadata')
+ req.method = 'POST'
+ req.content_type = "application/json"
+ body = {"metadata": {"key9": "value9"}}
+ req.body = jsonutils.dumps(body)
+ res_dict = self.controller.create(req, self.req_id, body)
+ self.assertEqual(body, res_dict)
+
+ def test_create_empty_body(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url)
+ req.method = 'POST'
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, self.req_id, None)
+
+ def test_create_item_empty_key(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'PUT'
+ body = {"meta": {"": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, self.req_id, body)
+
+ def test_create_item_key_too_long(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'PUT'
+ body = {"meta": {("a" * 260): "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create,
+ req, self.req_id, body)
+
+ def test_create_nonexistent_volume(self):
+ self.stubs.Set(db, 'volume_get',
+ return_volume_nonexistent)
+ self.stubs.Set(db, 'volume_metadata_get',
+ return_volume_metadata)
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+
+ req = fakes.HTTPRequest.blank('/v2/volume_metadata')
+ req.method = 'POST'
+ req.content_type = "application/json"
+ body = {"metadata": {"key9": "value9"}}
+ req.body = jsonutils.dumps(body)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.create, req, self.req_id, body)
+
+ def test_update_all(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url)
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {
+ 'metadata': {
+ 'key10': 'value10',
+ 'key99': 'value99',
+ },
+ }
+ req.body = jsonutils.dumps(expected)
+ res_dict = self.controller.update_all(req, self.req_id, expected)
+
+ self.assertEqual(expected, res_dict)
+
+ def test_update_all_empty_container(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url)
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'metadata': {}}
+ req.body = jsonutils.dumps(expected)
+ res_dict = self.controller.update_all(req, self.req_id, expected)
+
+ self.assertEqual(expected, res_dict)
+
+ def test_update_all_malformed_container(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url)
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'meta': {}}
+ req.body = jsonutils.dumps(expected)
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update_all, req, self.req_id,
+ expected)
+
+ def test_update_all_malformed_data(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url)
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'metadata': ['asdf']}
+ req.body = jsonutils.dumps(expected)
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update_all, req, self.req_id,
+ expected)
+
+ def test_update_all_nonexistent_volume(self):
+ self.stubs.Set(db, 'volume_get', return_volume_nonexistent)
+ req = fakes.HTTPRequest.blank(self.url)
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ body = {'metadata': {'key10': 'value10'}}
+ req.body = jsonutils.dumps(body)
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update_all, req, '100', body)
+
+ def test_update_item(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res_dict = self.controller.update(req, self.req_id, 'key1', body)
+ expected = {'meta': {'key1': 'value1'}}
+ self.assertEqual(expected, res_dict)
+
+ def test_update_item_nonexistent_volume(self):
+ self.stubs.Set(db, 'volume_get',
+ return_volume_nonexistent)
+ req = fakes.HTTPRequest.blank('/v2/fake/volumes/asdf/metadata/key1')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update, req, self.req_id, 'key1',
+ body)
+
+ def test_update_item_empty_body(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'PUT'
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, self.req_id, 'key1',
+ None)
+
+ def test_update_item_empty_key(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'PUT'
+ body = {"meta": {"": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, self.req_id, '', body)
+
+ def test_update_item_key_too_long(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'PUT'
+ body = {"meta": {("a" * 260): "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
+ self.controller.update,
+ req, self.req_id, ("a" * 260), body)
+
+ def test_update_item_value_too_long(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'PUT'
+ body = {"meta": {"key1": ("a" * 260)}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
+ self.controller.update,
+ req, self.req_id, "key1", body)
+
+ def test_update_item_too_many_keys(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "value1", "key2": "value2"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, self.req_id, 'key1',
+ body)
+
+ def test_update_item_body_uri_mismatch(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/bad')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, self.req_id, 'bad',
+ body)
+
+ def test_invalid_metadata_items_on_create(self):
+ self.stubs.Set(db, 'volume_metadata_update',
+ return_create_volume_metadata)
+ req = fakes.HTTPRequest.blank(self.url)
+ req.method = 'POST'
+ req.headers["content-type"] = "application/json"
+
+ #test for long key
+ data = {"metadata": {"a" * 260: "value1"}}
+ req.body = jsonutils.dumps(data)
+ self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
+ self.controller.create, req, self.req_id, data)
+
+ #test for long value
+ data = {"metadata": {"key": "v" * 260}}
+ req.body = jsonutils.dumps(data)
+ self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
+ self.controller.create, req, self.req_id, data)
+
+ #test for empty key.
+ data = {"metadata": {"": "value1"}}
+ req.body = jsonutils.dumps(data)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, self.req_id, data)