Adds an ability to manage share metadata

Now on share creating, we can specify metadata and it
will be saved in database. Also support of setting,
deleting and updating metadata info was added.

Adds model for share metadata
Adds migration
Adds api extension for share metadata
Adds relative api functions

Partially implements bp manila-client-enhancements
Change-Id: Id421323dd9e947d536b577092f1875f178d9aba5
This commit is contained in:
Andrei V. Ostapenko 2013-11-28 14:42:44 +02:00
parent cce07d59e1
commit 2e1f4e191b
17 changed files with 1040 additions and 4 deletions

View File

@ -26,6 +26,7 @@ import manila.api.openstack
from manila.api.v1 import limits
from manila.api import versions
from manila.api.v1 import share_metadata
from manila.api.v1 import share_snapshots
from manila.api.v1 import shares
@ -62,6 +63,20 @@ class APIRouter(manila.api.openstack.APIRouter):
collection={'detail': 'GET'},
member={'action': 'POST'})
self.resources['share_metadata'] = share_metadata.create_resource()
share_metadata_controller = self.resources['share_metadata']
mapper.resource("share_metadata", "metadata",
controller=share_metadata_controller,
parent_resource=dict(member_name='share',
collection_name='shares'))
mapper.connect("metadata",
"/{project_id}/shares/{share_id}/metadata",
controller=share_metadata_controller,
action='update_all',
conditions={"method": ['PUT']})
self.resources['limits'] = limits.create_resource()
mapper.resource("limit", "limits",
controller=self.resources['limits'])

View File

@ -0,0 +1,161 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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 manila.api import common
from manila.api.openstack import wsgi
from manila import exception
from manila import share
from webob import exc
class ShareMetadataController(object):
"""The share metadata API controller for the OpenStack API."""
def __init__(self):
self.share_api = share.API()
super(ShareMetadataController, self).__init__()
def _get_metadata(self, context, share_id):
try:
share = self.share_api.get(context, share_id)
meta = self.share_api.get_share_metadata(context, share)
except exception.NotFound:
msg = _('share does not exist')
raise exc.HTTPNotFound(explanation=msg)
return meta
@wsgi.serializers(xml=common.MetadataTemplate)
def index(self, req, share_id):
"""Returns the list of metadata for a given share."""
context = req.environ['manila.context']
return {'metadata': self._get_metadata(context, share_id)}
@wsgi.serializers(xml=common.MetadataTemplate)
@wsgi.deserializers(xml=common.MetadataDeserializer)
def create(self, req, share_id, body):
try:
metadata = body['metadata']
except (KeyError, TypeError):
msg = _("Malformed request body")
raise exc.HTTPBadRequest(explanation=msg)
context = req.environ['manila.context']
new_metadata = self._update_share_metadata(context,
share_id,
metadata,
delete=False)
return {'metadata': new_metadata}
@wsgi.serializers(xml=common.MetaItemTemplate)
@wsgi.deserializers(xml=common.MetaItemDeserializer)
def update(self, req, share_id, id, body):
try:
meta_item = body['meta']
except (TypeError, KeyError):
expl = _('Malformed request body')
raise exc.HTTPBadRequest(explanation=expl)
if id not in meta_item:
expl = _('Request body and URI mismatch')
raise exc.HTTPBadRequest(explanation=expl)
if len(meta_item) > 1:
expl = _('Request body contains too many items')
raise exc.HTTPBadRequest(explanation=expl)
context = req.environ['manila.context']
self._update_share_metadata(context,
share_id,
meta_item,
delete=False)
return {'meta': meta_item}
@wsgi.serializers(xml=common.MetadataTemplate)
@wsgi.deserializers(xml=common.MetadataDeserializer)
def update_all(self, req, share_id, body):
try:
metadata = body['metadata']
except (TypeError, KeyError):
expl = _('Malformed request body')
raise exc.HTTPBadRequest(explanation=expl)
context = req.environ['manila.context']
new_metadata = self._update_share_metadata(context, share_id,
metadata, delete=True)
return {'metadata': new_metadata}
def _update_share_metadata(self, context,
share_id, metadata,
delete=False):
try:
share = self.share_api.get(context, share_id)
return self.share_api.update_share_metadata(context,
share,
metadata,
delete)
except exception.NotFound:
msg = _('share does not exist')
raise exc.HTTPNotFound(explanation=msg)
except (ValueError, AttributeError):
msg = _("Malformed request body")
raise exc.HTTPBadRequest(explanation=msg)
except exception.InvalidShareMetadata as error:
raise exc.HTTPBadRequest(explanation=error.msg)
except exception.InvalidShareMetadataSize as error:
raise exc.HTTPBadRequest(explanation=error.msg)
@wsgi.serializers(xml=common.MetaItemTemplate)
def show(self, req, share_id, id):
"""Return a single metadata item."""
context = req.environ['manila.context']
data = self._get_metadata(context, share_id)
try:
return {'meta': {id: data[id]}}
except KeyError:
msg = _("Metadata item was not found")
raise exc.HTTPNotFound(explanation=msg)
def delete(self, req, share_id, id):
"""Deletes an existing metadata."""
context = req.environ['manila.context']
metadata = self._get_metadata(context, share_id)
if id not in metadata:
msg = _("Metadata item was not found")
raise exc.HTTPNotFound(explanation=msg)
try:
share = self.share_api.get(context, share_id)
self.share_api.delete_share_metadata(context, share, id)
except exception.NotFound:
msg = _('share does not exist')
raise exc.HTTPNotFound(explanation=msg)
return webob.Response(status_int=200)
def create_resource():
return wsgi.Resource(ShareMetadataController())

View File

@ -205,6 +205,8 @@ class ShareController(wsgi.Controller):
kwargs = {}
kwargs['availability_zone'] = share.get('availability_zone')
kwargs['metadata'] = share.get('metadata', None)
snapshot_id = share.get('snapshot_id')
if snapshot_id:
kwargs['snapshot'] = self.share_api.get_snapshot(context,

View File

@ -44,6 +44,12 @@ class ViewBuilder(common.ViewBuilder):
def detail(self, request, share):
"""Detailed view of a single share."""
metadata = share.get('share_metadata')
if metadata:
metadata = dict((item['key'], item['value']) for item in metadata)
else:
metadata = {}
return {
'share': {
'id': share.get('id'),
@ -56,6 +62,7 @@ class ViewBuilder(common.ViewBuilder):
'snapshot_id': share.get('snapshot_id'),
'share_proto': share.get('share_proto'),
'export_location': share.get('export_location'),
'metadata': metadata,
'links': self._get_links(request, share['id'])
}
}

View File

@ -430,3 +430,18 @@ def share_snapshot_data_get_for_project(context, project_id, session=None):
####################
def share_metadata_get(context, share_id):
"""Get all metadata for a share."""
return IMPL.share_metadata_get(context, share_id)
def share_metadata_delete(context, share_id, key):
"""Delete the given metadata item."""
IMPL.share_metadata_delete(context, share_id, key)
def share_metadata_update(context, share, metadata, delete):
"""Update metadata if it exists, otherwise create it."""
IMPL.share_metadata_update(context, share, metadata, delete)

View File

@ -128,6 +128,20 @@ def require_context(f):
return wrapper
def require_share_exists(f):
"""Decorator to require the specified share to exist.
Requires the wrapped function to use context and share_id as
their first two arguments.
"""
def wrapper(context, share_id, *args, **kwargs):
db.share_get(context, share_id)
return f(context, share_id, *args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper
def model_query(context, *args, **kwargs):
"""Query helper that accounts for context's `read_deleted` field.
@ -1037,11 +1051,25 @@ def reservation_expire(context):
def _share_get_query(context, session=None):
if session is None:
session = get_session()
return model_query(context, models.Share, session=session)
return model_query(context, models.Share, session=session).\
options(joinedload('share_metadata'))
def _metadata_refs(metadata_dict, meta_class):
metadata_refs = []
if metadata_dict:
for k, v in metadata_dict.iteritems():
metadata_ref = meta_class()
metadata_ref['key'] = k
metadata_ref['value'] = v
metadata_refs.append(metadata_ref)
return metadata_refs
@require_context
def share_create(context, values):
values['share_metadata'] = _metadata_refs(values.get('metadata'),
models.ShareMetadata)
share_ref = models.Share()
if not values.get('id'):
values['id'] = str(uuid.uuid4())
@ -1112,6 +1140,12 @@ def share_delete(context, share_id):
'deleted_at': timeutils.utcnow(),
'updated_at': literal_column('updated_at'),
'status': 'deleted'})
session.query(models.ShareMetadata).\
filter_by(share_id=share_id).\
update({'deleted': True,
'deleted_at': timeutils.utcnow(),
'updated_at': literal_column('updated_at')})
share_ref.save(session)
@ -1284,3 +1318,99 @@ def share_snapshot_update(context, snapshot_id, values):
snapshot_ref.update(values)
snapshot_ref.save(session=session)
return snapshot_ref
#################################
@require_context
@require_share_exists
def share_metadata_get(context, share_id):
return _share_metadata_get(context, share_id)
@require_context
@require_share_exists
def share_metadata_delete(context, share_id, key):
_share_metadata_get_query(context, share_id).\
filter_by(key=key).\
update({'deleted': True,
'deleted_at': timeutils.utcnow(),
'updated_at': literal_column('updated_at')})
@require_context
@require_share_exists
def share_metadata_update(context, share_id, metadata, delete):
return _share_metadata_update(context, share_id, metadata, delete)
def _share_metadata_get_query(context, share_id, session=None):
return model_query(context, models.ShareMetadata, session=session,
read_deleted="no").\
filter_by(share_id=share_id)
@require_context
@require_share_exists
def _share_metadata_get(context, share_id, session=None):
rows = _share_metadata_get_query(context, share_id,
session=session).all()
result = {}
for row in rows:
result[row['key']] = row['value']
return result
@require_context
@require_share_exists
def _share_metadata_update(context, share_id, metadata, delete, session=None):
if not session:
session = get_session()
with session.begin():
# Set existing metadata to deleted if delete argument is True
if delete:
original_metadata = _share_metadata_get(context, share_id,
session=session)
for meta_key, meta_value in original_metadata.iteritems():
if meta_key not in metadata:
meta_ref = _share_metadata_get_item(context, share_id,
meta_key,
session=session)
meta_ref.update({'deleted': True})
meta_ref.save(session=session)
meta_ref = None
# Now update all existing items with new values, or create new meta
# objects
for meta_key, meta_value in metadata.items():
# update the value whether it exists or not
item = {"value": meta_value}
try:
meta_ref = _share_metadata_get_item(context, share_id,
meta_key,
session=session)
except exception.ShareMetadataNotFound:
meta_ref = models.ShareMetadata()
item.update({"key": meta_key, "share_id": share_id})
meta_ref.update(item)
meta_ref.save(session=session)
return metadata
def _share_metadata_get_item(context, share_id, key, session=None):
result = _share_metadata_get_query(context, share_id, session=session).\
filter_by(key=key).\
first()
if not result:
raise exception.ShareMetadataNotFound(metadata_key=key,
share_id=share_id)
return result

View File

@ -0,0 +1,61 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 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.
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer
from sqlalchemy import MetaData, String, Table
from manila.openstack.common.gettextutils import _
from manila.openstack.common import log as logging
LOG = logging.getLogger(__name__)
def upgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
shares = Table('shares', meta, autoload=True)
share_metadata = Table('share_metadata', meta,
Column('created_at', DateTime),
Column('updated_at', DateTime),
Column('deleted_at', DateTime),
Column('deleted', Boolean),
Column('id', Integer, primary_key=True, nullable=False),
Column('share_id', String(length=36), ForeignKey('shares.id'),
nullable=False),
Column('key', String(length=255), nullable=False),
Column('value', String(length=1024), nullable=False),
mysql_engine='InnoDB',
mysql_charset='utf8'
)
try:
share_metadata.create()
except Exception:
LOG.exception("Exception while creating table 'share_metadata'")
meta.drop_all(tables=[share_metadata])
raise
def downgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
share_metadata = Table('share_metadata', meta, autoload=True)
try:
share_metadata.drop()
except Exception:
LOG.error(_("share_metadata table not dropped"))
raise

View File

@ -21,7 +21,7 @@
SQLAlchemy models for Manila data.
"""
from sqlalchemy import Column, Integer, String, Text, schema
from sqlalchemy import Column, Index, Integer, String, Text, schema
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import ForeignKey, DateTime, Boolean, Enum
@ -257,6 +257,20 @@ class Share(BASE, ManilaBase):
export_location = Column(String(255))
class ShareMetadata(BASE, ManilaBase):
"""Represents a metadata key/value pair for a share."""
__tablename__ = 'share_metadata'
id = Column(Integer, primary_key=True)
key = Column(String(255), nullable=False)
value = Column(String(1024), nullable=False)
share_id = Column(String(36), ForeignKey('shares.id'), nullable=False)
share = relationship(Share, backref="share_metadata",
foreign_keys=share_id,
primaryjoin='and_('
'ShareMetadata.share_id == Share.id,'
'ShareMetadata.deleted == False)')
class ShareAccessMapping(BASE, ManilaBase):
"""Represents access to NFS."""
STATE_NEW = 'new'

View File

@ -131,6 +131,7 @@ class ManilaException(Exception):
# at least get the core message out if something happened
message = self.message
self.msg = message
super(ManilaException, self).__init__(message)
@ -478,3 +479,15 @@ class InvalidShareSnapshot(ManilaException):
class SwiftConnectionFailed(ManilaException):
message = _("Connection to swift failed") + ": %(reason)s"
class ShareMetadataNotFound(NotFound):
message = _("Metadata item is not found")
class InvalidShareMetadata(Invalid):
message = _("Invalid metadata")
class InvalidShareMetadataSize(Invalid):
message = _("Invalid metadata size")

View File

@ -77,9 +77,12 @@ class API(base.Base):
super(API, self).__init__(db_driver)
def create(self, context, share_proto, size, name, description,
snapshot=None, availability_zone=None):
snapshot=None, availability_zone=None, metadata=None):
"""Create new share."""
check_policy(context, 'create')
self._check_metadata_properties(context, metadata)
if snapshot is not None:
if snapshot['status'] != 'available':
msg = _('status must be available')
@ -151,6 +154,7 @@ class API(base.Base):
'project_id': context.project_id,
'snapshot_id': snapshot_id,
'availability_zone': availability_zone,
'metadata': metadata,
'status': "creating",
'scheduled_at': timeutils.utcnow(),
'display_name': name,
@ -423,3 +427,54 @@ class API(base.Base):
check_policy(context, 'access_get')
rule = self.db.share_access_get(context, access_id)
return rule
@wrap_check_policy
def get_share_metadata(self, context, share):
"""Get all metadata associated with a share."""
rv = self.db.share_metadata_get(context, share['id'])
return dict(rv.iteritems())
@wrap_check_policy
def delete_share_metadata(self, context, share, key):
"""Delete the given metadata item from a share."""
self.db.share_metadata_delete(context, share['id'], key)
def _check_metadata_properties(self, context, metadata=None):
if not metadata:
metadata = {}
for k, v in metadata.iteritems():
if len(k) == 0:
msg = _("Metadata property key is blank")
LOG.warn(msg)
raise exception.InvalidShareMetadata(message=msg)
if len(k) > 255:
msg = _("Metadata property key is greater than 255 characters")
LOG.warn(msg)
raise exception.InvalidShareMetadataSize(message=msg)
if len(v) > 1024:
msg = _("Metadata property value is "
"greater than 1024 characters")
LOG.warn(msg)
raise exception.InvalidShareMetadataSize(message=msg)
@wrap_check_policy
def update_share_metadata(self, context, share, metadata, delete=False):
"""Updates or creates share metadata.
If delete is True, metadata items that are not specified in the
`metadata` argument will be deleted.
"""
orig_meta = self.get_share_metadata(context, share)
if delete:
_metadata = metadata
else:
_metadata = orig_meta.copy()
_metadata.update(metadata)
self._check_metadata_properties(context, _metadata)
self.db.share_metadata_update(context, share['id'],
_metadata, delete)
return _metadata

View File

@ -284,3 +284,12 @@ class TestCase(unittest.TestCase):
self.assertFalse(a in b, *args, **kwargs)
else:
f(a, b, *args, **kwargs)
def assertIsInstance(self, a, b, *args, **kwargs):
"""Python < v2.7 compatibility."""
try:
f = super(TestCase, self).assertIsInstance
except AttributeError:
self.assertTrue(isinstance(a, b))
else:
f(a, b, *args, **kwargs)

View File

@ -0,0 +1,446 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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 manila.api import extensions
from manila.api.v1 import share_metadata
from manila.api.v1 import shares
import manila.db
from manila import exception
from manila.openstack.common import jsonutils
from manila import test
from manila.tests.api import fakes
CONF = cfg.CONF
def return_create_share_metadata_max(context, share_id, metadata, delete):
return stub_max_share_metadata()
def return_create_share_metadata(context, share_id, metadata, delete):
return stub_share_metadata()
def return_share_metadata(context, share_id):
if not isinstance(share_id, str) or not len(share_id) == 36:
msg = 'id %s must be a uuid in return share metadata' % share_id
raise Exception(msg)
return stub_share_metadata()
def return_empty_share_metadata(context, share_id):
return {}
def delete_share_metadata(context, share_id, key):
pass
def stub_share_metadata():
metadata = {
"key1": "value1",
"key2": "value2",
"key3": "value3",
}
return metadata
def stub_max_share_metadata():
metadata = {"metadata": {}}
for num in range(CONF.quota_metadata_items):
metadata['metadata']['key%i' % num] = "blah"
return metadata
def return_share(context, share_id):
return {'id': '0cc3346e-9fef-4445-abe6-5d2b2690ec64',
'name': 'fake',
'metadata': {}}
def return_share_nonexistent(context, share_id):
raise exception.NotFound('bogus test message')
def fake_update_share_metadata(self, context, share, diff):
pass
class ShareMetaDataTest(test.TestCase):
def setUp(self):
super(ShareMetaDataTest, self).setUp()
self.share_api = manila.share.api.API()
fakes.stub_out_key_pair_funcs(self.stubs)
self.stubs.Set(manila.db, 'share_get', return_share)
self.stubs.Set(manila.db, 'share_metadata_get',
return_share_metadata)
self.stubs.Set(self.share_api, 'update_share_metadata',
fake_update_share_metadata)
self.share_controller = shares.ShareController()
self.controller = share_metadata.ShareMetadataController()
self.req_id = str(uuid.uuid4())
self.url = '/shares/%s/metadata' % self.req_id
sh = {"size": 1,
"name": "Share Test Name",
"share_proto": "nfs",
"display_name": "Updated Desc",
"display_description": "Share Test Desc",
"metadata": {}}
body = {"share": sh}
req = fakes.HTTPRequest.blank('/shares')
self.share_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_share(self):
self.stubs.Set(manila.db, 'share_metadata_get',
return_share_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(manila.db, 'share_metadata_get',
return_empty_share_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_share(self):
self.stubs.Set(manila.db, 'share_metadata_get',
return_share_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(manila.db, 'share_metadata_get',
return_empty_share_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(manila.db, 'share_metadata_get',
return_share_metadata)
self.stubs.Set(manila.db, 'share_metadata_delete',
delete_share_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_share(self):
self.stubs.Set(manila.db, 'share_get',
return_share_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(manila.db, 'share_metadata_get',
return_empty_share_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(manila.db, 'share_metadata_get',
return_empty_share_metadata)
self.stubs.Set(manila.db, 'share_metadata_update',
return_create_share_metadata)
req = fakes.HTTPRequest.blank('/v1/share_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(manila.db, 'share_metadata_update',
return_create_share_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(manila.db, 'share_metadata_update',
return_create_share_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(manila.db, 'share_metadata_update',
return_create_share_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_share(self):
self.stubs.Set(manila.db, 'share_get',
return_share_nonexistent)
self.stubs.Set(manila.db, 'share_metadata_get',
return_share_metadata)
self.stubs.Set(manila.db, 'share_metadata_update',
return_create_share_metadata)
req = fakes.HTTPRequest.blank('/v1/share_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(manila.db, 'share_metadata_update',
return_create_share_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(manila.db, 'share_metadata_update',
return_create_share_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(manila.db, 'share_metadata_update',
return_create_share_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(manila.db, 'share_metadata_update',
return_create_share_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_share(self):
self.stubs.Set(manila.db, 'share_get', return_share_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(manila.db, 'share_metadata_update',
return_create_share_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_share(self):
self.stubs.Set(manila.db, 'share_get',
return_share_nonexistent)
req = fakes.HTTPRequest.blank('/v1.1/fake/shares/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(manila.db, 'share_metadata_update',
return_create_share_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(manila.db, 'share_metadata_update',
return_create_share_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(manila.db, 'share_metadata_update',
return_create_share_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.update,
req, self.req_id, ("a" * 260), body)
def test_update_item_value_too_long(self):
self.stubs.Set(manila.db, 'share_metadata_update',
return_create_share_metadata)
req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'PUT'
body = {"meta": {"key1": ("a" * 1025)}}
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_too_many_keys(self):
self.stubs.Set(manila.db, 'share_metadata_update',
return_create_share_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(manila.db, 'share_metadata_update',
return_create_share_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(manila.db, 'share_metadata_update',
return_create_share_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.HTTPBadRequest,
self.controller.create, req, self.req_id, data)
#test for long value
data = {"metadata": {"key": "v" * 1025}}
req.body = jsonutils.dumps(data)
self.assertRaises(webob.exc.HTTPBadRequest,
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)

View File

@ -141,6 +141,7 @@ class ShareApiTest(test.TestCase):
'id': '1',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'share_proto': 'fakeproto',
'metadata': {},
'size': 1,
'snapshot_id': '2',
'status': 'fakestatus',
@ -241,6 +242,7 @@ class ShareApiTest(test.TestCase):
'availability_zone': 'fakeaz',
'name': 'displayname',
'share_proto': 'fakeproto',
'metadata': {},
'id': '1',
'snapshot_id': '2',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),

View File

@ -11,5 +11,9 @@
"share:create_snapshot": [],
"share:delete_snapshot": [],
"share:get_snapshot": [],
"share:get_all_snapshots": []
"share:get_all_snapshots": [],
"share:get_share_metadata": [],
"share:delete_share_metadata": [],
"share:update_share_metadata": []
}

View File

@ -369,3 +369,43 @@ class TestMigrations(test.TestCase):
LOG.error("Failed to migrate to version %s on engine %s" %
(version, engine))
raise
def test_migration_006(self):
"""Test adding share_metadata table works correctly."""
for (key, engine) in self.engines.items():
migration_api.version_control(engine,
TestMigrations.REPOSITORY,
migration.INIT_VERSION)
migration_api.upgrade(engine, TestMigrations.REPOSITORY, 5)
metadata = sqlalchemy.schema.MetaData()
metadata.bind = engine
migration_api.upgrade(engine, TestMigrations.REPOSITORY, 6)
self.assertTrue(engine.dialect.has_table(engine.connect(),
"share_metadata"))
share_metadata = sqlalchemy.Table('share_metadata',
metadata,
autoload=True)
self.assertIsInstance(share_metadata.c.created_at.type,
sqlalchemy.types.DATETIME)
self.assertIsInstance(share_metadata.c.updated_at.type,
sqlalchemy.types.DATETIME)
self.assertIsInstance(share_metadata.c.deleted_at.type,
sqlalchemy.types.DATETIME)
self.assertIsInstance(share_metadata.c.deleted.type,
sqlalchemy.types.BOOLEAN)
self.assertIsInstance(share_metadata.c.id.type,
sqlalchemy.types.INTEGER)
self.assertIsInstance(share_metadata.c.share_id.type,
sqlalchemy.types.VARCHAR)
self.assertIsInstance(share_metadata.c.key.type,
sqlalchemy.types.VARCHAR)
self.assertIsInstance(share_metadata.c.value.type,
sqlalchemy.types.VARCHAR)
migration_api.downgrade(engine, TestMigrations.REPOSITORY, 5)
self.assertFalse(engine.dialect.has_table(engine.connect(),
"share_metadata"))

View File

@ -113,6 +113,7 @@ class ShareTestCase(test.TestCase):
share['snapshot_id'] = snapshot_id
share['user_id'] = 'fake'
share['project_id'] = 'fake'
share['metadata'] = {'fake_key': 'fake_value'}
share['availability_zone'] = CONF.storage_availability_zone
share['status'] = status
share['host'] = CONF.host
@ -367,3 +368,32 @@ class ShareTestCase(test.TestCase):
acs = db.share_access_get(self.context, access_id)
self.assertEquals(acs['state'], 'error')
def test_create_delete_share_with_metadata(self):
"""Test share can be created with metadata and deleted."""
test_meta = {'fake_key': 'fake_value'}
share = self._create_share()
share_id = share['id']
self.share.create_share(self.context, share_id)
result_meta = {
share.share_metadata[0].key: share.share_metadata[0].value}
self.assertEqual(result_meta, test_meta)
self.share.delete_share(self.context, share_id)
self.assertRaises(exception.NotFound,
db.share_get,
self.context,
share_id)
def test_create_share_with_invalid_metadata(self):
"""Test share create with too much metadata fails."""
share_api = manila.share.api.API()
test_meta = {'fake_key': 'fake_value' * 1025}
self.assertRaises(exception.InvalidShareMetadataSize,
share_api.create,
self.context,
'nfs',
1,
'name',
'description',
metadata=test_meta)

View File

@ -43,6 +43,7 @@ def fake_share(id, **kwargs):
'availability_zone': 'fakeaz',
'status': 'fakestatus',
'display_name': 'fakename',
'metadata': None,
'display_description': 'fakedesc',
'share_proto': 'nfs',
'export_location': 'fake_location',
@ -527,3 +528,34 @@ class ShareAPITestCase(test.TestCase):
'access_type': 'fakeacctype',
'access_to': 'fakeaccto',
'state': 'fakeerror'}])
def test_share_metadata_get(self):
metadata = {'a': 'b', 'c': 'd'}
db_driver.share_create(self.context, {'id': '1', 'metadata': metadata})
self.assertEqual(metadata,
db_driver.share_metadata_get(self.context, '1'))
def test_share_metadata_update(self):
metadata1 = {'a': '1', 'c': '2'}
metadata2 = {'a': '3', 'd': '5'}
should_be = {'a': '3', 'c': '2', 'd': '5'}
db_driver.share_create(self.context, {'id': '1',
'metadata': metadata1})
db_driver.share_metadata_update(self.context, '1', metadata2, False)
self.assertEqual(should_be,
db_driver.share_metadata_get(self.context, '1'))
def test_share_metadata_update_delete(self):
metadata1 = {'a': '1', 'c': '2'}
metadata2 = {'a': '3', 'd': '4'}
should_be = metadata2
db_driver.share_create(self.context, {'id': '1',
'metadata': metadata1})
db_driver.share_metadata_update(self.context, '1', metadata2, True)
self.assertEqual(should_be,
db_driver.share_metadata_get(self.context, '1'))