Support metadata for access rule resource

Now only share have metadata property.
We should support it for access rule as well.

DocImpact

Needed-By: https://review.openstack.org/#/c/579534
Needed-By: https://review.openstack.org/#/c/571366

Change-Id: I2f2b3325a09e5af7f7c4e4fa3443259fb69f9771
Implements: bp metadata-for-access-rule
This commit is contained in:
zhongjun2 2018-05-28 11:06:07 +08:00
parent 27f1474d7b
commit 0957b33e9b
27 changed files with 991 additions and 64 deletions

View File

@ -116,13 +116,16 @@ REST_API_VERSION_HISTORY = """
* 2.42 - Added ``with_count`` in share list API to get total count info.
* 2.43 - Added filter search by extra spec for share type list.
* 2.44 - Added 'ou' field to 'security_service' object.
* 2.45 - Added access metadata for share access and also introduced
the GET /share-access-rules API. The prior API to retrieve
access rules will not work with API version >=2.45.
"""
# The minimum and maximum versions of the API supported
# The default api version request is defined to be the
# minimum version of the API supported.
_MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.44"
_MAX_API_VERSION = "2.45"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -242,3 +242,9 @@ user documentation.
2.44
----
Added 'ou' field to 'security_service' object.
2.45
----
Added access metadata for share access and also introduced the
GET /share-access-rules API. The prior API to retrieve access
rules will not work with API version >=2.45.

View File

@ -112,10 +112,10 @@ class ShareMetadataController(object):
msg = _("Malformed request body")
raise exc.HTTPBadRequest(explanation=msg)
except exception.InvalidShareMetadata as error:
except exception.InvalidMetadata as error:
raise exc.HTTPBadRequest(explanation=error.msg)
except exception.InvalidShareMetadataSize as error:
except exception.InvalidMetadataSize as error:
raise exc.HTTPBadRequest(explanation=error.msg)
def show(self, req, share_id, id):

View File

@ -392,10 +392,13 @@ class ShareMixin(object):
@wsgi.Controller.authorize('allow_access')
def _allow_access(self, req, id, body, enable_ceph=False,
allow_on_error_status=False, enable_ipv6=False):
allow_on_error_status=False, enable_ipv6=False,
enable_metadata=False):
"""Add share access rule."""
context = req.environ['manila.context']
access_data = body.get('allow_access', body.get('os-allow_access'))
if not enable_metadata:
access_data.pop('metadata', None)
share = self.share_api.get(context, id)
if (not allow_on_error_status and
@ -419,10 +422,16 @@ class ShareMixin(object):
try:
access = self.share_api.allow_access(
context, share, access_type, access_to,
access_data.get('access_level'))
access_data.get('access_level'), access_data.get('metadata'))
except exception.ShareAccessExists as e:
raise webob.exc.HTTPBadRequest(explanation=e.msg)
except exception.InvalidMetadata as error:
raise exc.HTTPBadRequest(explanation=error.msg)
except exception.InvalidMetadataSize as error:
raise exc.HTTPBadRequest(explanation=error.msg)
return self._access_view_builder.view(req, access)
@wsgi.Controller.authorize('deny_access')

View File

@ -35,6 +35,8 @@ from manila.api.v2 import messages
from manila.api.v2 import quota_class_sets
from manila.api.v2 import quota_sets
from manila.api.v2 import services
from manila.api.v2 import share_access_metadata
from manila.api.v2 import share_accesses
from manila.api.v2 import share_export_locations
from manila.api.v2 import share_group_snapshots
from manila.api.v2 import share_group_type_specs
@ -415,3 +417,26 @@ class APIRouter(manila.api.openstack.APIRouter):
self.resources['messages'] = messages.create_resource()
mapper.resource("message", "messages",
controller=self.resources['messages'])
self.resources["share-access-rules"] = share_accesses.create_resource()
mapper.resource(
"share-access-rule",
"share-access-rules",
controller=self.resources["share-access-rules"],
collection={"detail": "GET"})
self.resources["access-metadata"] = (
share_access_metadata.create_resource())
access_metadata_controller = self.resources["access-metadata"]
mapper.connect("share-access-rules",
"/{project_id}/share-access-rules/{access_id}/metadata",
controller=access_metadata_controller,
action="update",
conditions={"method": ["PUT"]})
mapper.connect("share-access-rules",
"/{project_id}/share-access-rules/"
"{access_id}/metadata/{key}",
controller=access_metadata_controller,
action="delete",
conditions={"method": ["DELETE"]})

View File

@ -0,0 +1,84 @@
# Copyright 2018 Huawei Corporation.
# 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.
"""The share access rule metadata api."""
import webob
from manila.api.openstack import wsgi
from manila.api.views import share_accesses as share_access_views
from manila import db
from manila import exception
from manila.i18n import _
from manila import share
class ShareAccessMetadataController(wsgi.Controller):
"""The Share access rule metadata API V2 controller."""
resource_name = 'share_access_metadata'
_view_builder_class = share_access_views.ViewBuilder
def __init__(self):
super(ShareAccessMetadataController, self).__init__()
self.share_api = share.API()
@wsgi.Controller.api_version('2.45')
@wsgi.Controller.authorize
def update(self, req, access_id, body=None):
context = req.environ['manila.context']
if not self.is_valid_body(body, 'metadata'):
raise webob.exc.HTTPBadRequest()
metadata = body['metadata']
md = self._update_share_access_metadata(context, access_id, metadata)
return self._view_builder.view_metadata(req, md)
@wsgi.Controller.api_version('2.45')
@wsgi.Controller.authorize
@wsgi.response(200)
def delete(self, req, access_id, key):
"""Deletes an existing access metadata."""
context = req.environ['manila.context']
self._assert_access_exists(context, access_id)
try:
db.share_access_metadata_delete(context, access_id, key)
except exception.ShareAccessMetadataNotFound as error:
raise webob.exc.HTTPNotFound(explanation=error.msg)
def _update_share_access_metadata(self, context, access_id, metadata):
self._assert_access_exists(context, access_id)
try:
return self.share_api.update_share_access_metadata(
context, access_id, metadata)
except (ValueError, AttributeError):
msg = _("Malformed request body")
raise webob.exc.HTTPBadRequest(explanation=msg)
except exception.InvalidMetadata as error:
raise webob.exc.HTTPBadRequest(explanation=error.msg)
except exception.InvalidMetadataSize as error:
raise webob.exc.HTTPBadRequest(explanation=error.msg)
def _assert_access_exists(self, context, access_id):
try:
self.share_api.access_get(context, access_id)
except exception.NotFound as ex:
raise webob.exc.HTTPNotFound(explanation=ex.msg)
def create_resource():
return wsgi.Resource(ShareAccessMetadataController())

View File

@ -0,0 +1,81 @@
# Copyright 2018 Huawei Corporation.
# 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.
"""The share accesses api."""
import ast
import webob
from manila.api.openstack import wsgi
from manila.api.views import share_accesses as share_access_views
from manila import exception
from manila.i18n import _
from manila import share
class ShareAccessesController(wsgi.Controller, wsgi.AdminActionsMixin):
"""The Share accesses API V2 controller for the OpenStack API."""
resource_name = 'share_access_rule'
_view_builder_class = share_access_views.ViewBuilder
def __init__(self):
super(ShareAccessesController, self).__init__()
self.share_api = share.API()
@wsgi.Controller.api_version('2.45')
@wsgi.Controller.authorize('get')
def show(self, req, id):
"""Return data about the given share access rule."""
context = req.environ['manila.context']
share_access = self._get_share_access(context, id)
return self._view_builder.view(req, share_access)
def _get_share_access(self, context, share_access_id):
try:
return self.share_api.access_get(context, share_access_id)
except exception.NotFound:
msg = _("Share access rule %s not found.") % share_access_id
raise webob.exc.HTTPNotFound(explanation=msg)
@wsgi.Controller.api_version('2.45')
@wsgi.Controller.authorize
def index(self, req):
"""Returns the list of access rules for a given share."""
context = req.environ['manila.context']
search_opts = {}
search_opts.update(req.GET)
if 'share_id' not in search_opts:
msg = _("The field 'share_id' has to be specified.")
raise webob.exc.HTTPBadRequest(explanation=msg)
share_id = search_opts.pop('share_id', None)
if 'metadata' in search_opts:
search_opts['metadata'] = ast.literal_eval(
search_opts['metadata'])
try:
share = self.share_api.get(context, share_id)
except exception.NotFound:
msg = _("Share %s not found.") % share_id
raise webob.exc.HTTPBadRequest(explanation=msg)
access_rules = self.share_api.access_get_all(
context, share, search_opts)
return self._view_builder.list_view(req, access_rules)
def create_resource():
return wsgi.Resource(ShareAccessesController())

View File

@ -346,7 +346,8 @@ class ShareController(shares.ShareMixin,
kwargs['allow_on_error_status'] = True
if req.api_version_request >= api_version.APIVersionRequest("2.38"):
kwargs['enable_ipv6'] = True
if req.api_version_request >= api_version.APIVersionRequest("2.45"):
kwargs['enable_metadata'] = True
return self._allow_access(*args, **kwargs)
@wsgi.Controller.api_version('2.0', '2.6')
@ -367,7 +368,7 @@ class ShareController(shares.ShareMixin,
"""List share access rules."""
return self._access_list(req, id, body)
@wsgi.Controller.api_version('2.7')
@wsgi.Controller.api_version('2.7', '2.44')
@wsgi.action('access_list')
def access_list(self, req, id, body):
"""List share access rules."""

View File

@ -26,6 +26,7 @@ class ViewBuilder(common.ViewBuilder):
"add_access_key",
"translate_transitional_statuses",
"add_created_at_and_updated_at",
"add_access_rule_metadata_field",
]
def list_view(self, request, accesses):
@ -60,6 +61,10 @@ class ViewBuilder(common.ViewBuilder):
request, access_dict, access)
return {'access': access_dict}
def view_metadata(self, request, metadata):
"""View of a share access rule metadata."""
return {'metadata': metadata}
@common.ViewBuilder.versioned_method("2.21")
def add_access_key(self, context, access_dict, access):
access_dict['access_key'] = access.get('access_key')
@ -69,6 +74,12 @@ class ViewBuilder(common.ViewBuilder):
access_dict['created_at'] = access.get('created_at')
access_dict['updated_at'] = access.get('updated_at')
@common.ViewBuilder.versioned_method("2.45")
def add_access_rule_metadata_field(self, context, access_dict, access):
metadata = access.get('share_access_rules_metadata') or {}
metadata = {item['key']: item['value'] for item in metadata}
access_dict['metadata'] = metadata
@common.ViewBuilder.versioned_method("1.0", "2.27")
def translate_transitional_statuses(self, context, access_dict, access):
"""In 2.28, the per access rule status was (re)introduced."""

View File

@ -429,9 +429,10 @@ def share_access_get(context, access_id):
return IMPL.share_access_get(context, access_id)
def share_access_get_all_for_share(context, share_id):
def share_access_get_all_for_share(context, share_id, filters=None):
"""Get all access rules for given share."""
return IMPL.share_access_get_all_for_share(context, share_id)
return IMPL.share_access_get_all_for_share(context, share_id,
filters=filters)
def share_access_get_all_for_instance(context, instance_id, filters=None,
@ -489,6 +490,17 @@ def share_instance_access_delete(context, mapping_id):
"""Deny access to share instance."""
return IMPL.share_instance_access_delete(context, mapping_id)
def share_access_metadata_update(context, access_id, metadata):
"""Update metadata of share access rule."""
return IMPL.share_access_metadata_update(context, access_id, metadata)
def share_access_metadata_delete(context, access_id, key):
"""Delete metadata of share access rule."""
return IMPL.share_access_metadata_delete(context, access_id, key)
####################

View File

@ -0,0 +1,64 @@
# Copyright 2018 Huawei Corporation.
# 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.
"""add metadata for access rule
Revision ID: 11ee96se625f3
Revises: 097fad24d2fc
Create Date: 2018-06-16 03:07:15.548947
"""
# revision identifiers, used by Alembic.
revision = '11ee96se625f3'
down_revision = '097fad24d2fc'
from alembic import op
from oslo_log import log
import sqlalchemy as sql
LOG = log.getLogger(__name__)
access_metadata_table_name = 'share_access_rules_metadata'
def upgrade():
try:
op.create_table(
access_metadata_table_name,
sql.Column('created_at', sql.DateTime(timezone=False)),
sql.Column('updated_at', sql.DateTime(timezone=False)),
sql.Column('deleted_at', sql.DateTime(timezone=False)),
sql.Column('deleted', sql.String(36), default='False'),
sql.Column('access_id', sql.String(36),
sql.ForeignKey('share_access_map.id'), nullable=False),
sql.Column('key', sql.String(255), nullable=False),
sql.Column('value', sql.String(1023), nullable=False),
sql.Column('id', sql.Integer, primary_key=True, nullable=False),
mysql_engine='InnoDB',
mysql_charset='utf8'
)
except Exception:
LOG.error("Table |%s| not created!",
access_metadata_table_name)
raise
def downgrade():
try:
op.drop_table(access_metadata_table_name)
except Exception:
LOG.error("%s table not dropped", access_metadata_table_name)
raise

View File

@ -1938,8 +1938,10 @@ def share_delete(context, share_id):
def _share_access_get_query(context, session, values, read_deleted='no'):
"""Get access record."""
query = model_query(context, models.ShareAccessMapping, session=session,
read_deleted=read_deleted)
query = (model_query(
context, models.ShareAccessMapping, session=session,
read_deleted=read_deleted).options(
joinedload('share_access_rules_metadata')))
return query.filter_by(**values)
@ -1957,11 +1959,66 @@ def _share_instance_access_query(context, session, access_id=None,
session=session).filter_by(**filters)
def _share_access_metadata_get_item(context, access_id, key, session=None):
result = (_share_access_metadata_get_query(
context, access_id, session=session).filter_by(key=key).first())
if not result:
raise exception.ShareAccessMetadataNotFound(
metadata_key=key, access_id=access_id)
return result
def _share_access_metadata_get_query(context, access_id, session=None):
return (model_query(
context, models.ShareAccessRulesMetadata, session=session,
read_deleted="no").
filter_by(access_id=access_id).
options(joinedload('access')))
@require_context
def share_access_metadata_update(context, access_id, metadata):
session = get_session()
with session.begin():
# 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_access_metadata_get_item(
context, access_id, meta_key, session=session)
except exception.ShareAccessMetadataNotFound:
meta_ref = models.ShareAccessRulesMetadata()
item.update({"key": meta_key, "access_id": access_id})
meta_ref.update(item)
meta_ref.save(session=session)
return metadata
@require_context
def share_access_metadata_delete(context, access_id, key):
session = get_session()
with session.begin():
metadata = _share_access_metadata_get_item(
context, access_id, key, session=session)
metadata.soft_delete(session)
@require_context
def share_access_create(context, values):
values = ensure_model_dict_has_id(values)
session = get_session()
with session.begin():
values['share_access_rules_metadata'] = (
_metadata_refs(values.get('metadata'),
models.ShareAccessRulesMetadata))
access_ref = models.ShareAccessMapping()
access_ref.update(values)
access_ref.save(session=session)
@ -2064,11 +2121,21 @@ def share_instance_access_get(context, access_id, instance_id,
@require_context
def share_access_get_all_for_share(context, share_id, session=None):
def share_access_get_all_for_share(context, share_id, filters=None,
session=None):
filters = filters or {}
session = session or get_session()
return _share_access_get_query(
query = (_share_access_get_query(
context, session, {'share_id': share_id}).filter(
models.ShareAccessMapping.instance_mappings.any()).all()
models.ShareAccessMapping.instance_mappings.any()))
if 'metadata' in filters:
for k, v in filters['metadata'].items():
query = query.filter(
or_(models.ShareAccessMapping.
share_access_rules_metadata.any(key=k, value=v)))
return query.all()
@require_context
@ -2193,11 +2260,11 @@ def share_instance_access_delete(context, mapping_id):
# NOTE(u_glide): Remove access rule if all mappings were removed.
if len(other_mappings) == 0:
(
session.query(models.ShareAccessMapping)
.filter_by(id=mapping['access_id'])
.soft_delete()
)
(session.query(models.ShareAccessRulesMetadata).filter_by(
access_id=mapping['access_id']).soft_delete())
(session.query(models.ShareAccessMapping).filter_by(
id=mapping['access_id']).soft_delete())
@require_context

View File

@ -568,6 +568,24 @@ class ShareAccessMapping(BASE, ManilaBase):
)
class ShareAccessRulesMetadata(BASE, ManilaBase):
"""Represents a metadata key/value pair for a share access rule."""
__tablename__ = 'share_access_rules_metadata'
id = Column(Integer, primary_key=True)
deleted = Column(String(36), default='False')
key = Column(String(255), nullable=False)
value = Column(String(1023), nullable=False)
access_id = Column(String(36), ForeignKey('share_access_map.id'),
nullable=False)
access = orm.relationship(
ShareAccessMapping, backref="share_access_rules_metadata",
foreign_keys=access_id,
lazy='immediate',
primaryjoin='and_('
'ShareAccessRulesMetadata.access_id == ShareAccessMapping.id,'
'ShareAccessRulesMetadata.deleted == "False")')
class ShareInstanceAccessMapping(BASE, ManilaBase):
"""Represents access to individual share instances."""

View File

@ -472,6 +472,10 @@ class ShareAccessExists(ManilaException):
message = _("Share access %(access_type)s:%(access)s exists.")
class ShareAccessMetadataNotFound(NotFound):
message = _("Share access rule metadata does not exist.")
class ShareSnapshotAccessExists(InvalidInput):
message = _("Share snapshot access %(access_type)s:%(access)s exists.")
@ -543,11 +547,11 @@ class ShareMetadataNotFound(NotFound):
message = _("Metadata item is not found.")
class InvalidShareMetadata(Invalid):
class InvalidMetadata(Invalid):
message = _("Invalid metadata.")
class InvalidShareMetadataSize(Invalid):
class InvalidMetadataSize(Invalid):
message = _("Invalid metadata size.")

View File

@ -24,6 +24,8 @@ from manila.policies import quota_set
from manila.policies import scheduler_stats
from manila.policies import security_service
from manila.policies import service
from manila.policies import share_access
from manila.policies import share_access_metadata
from manila.policies import share_export_location
from manila.policies import share_group
from manila.policies import share_group_snapshot
@ -70,4 +72,6 @@ def list_rules():
share_export_location.list_rules(),
share_instance.list_rules(),
message.list_rules(),
share_access.list_rules(),
share_access_metadata.list_rules(),
)

View File

@ -0,0 +1,50 @@
# Copyright 2018 Huawei Corporation.
# 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.
from oslo_policy import policy
from manila.policies import base
BASE_POLICY_NAME = 'share_access_rule:%s'
share_access_rule_policies = [
policy.DocumentedRuleDefault(
name=BASE_POLICY_NAME % 'get',
check_str=base.RULE_DEFAULT,
description="Get details of a share access rule.",
operations=[
{
'method': 'GET',
'path': '/share-access-rules/{share_access_id}'
}
]),
policy.DocumentedRuleDefault(
name=BASE_POLICY_NAME % 'index',
check_str=base.RULE_DEFAULT,
description="List access rules of a given share.",
operations=[
{
'method': 'GET',
'path': ('/share-access-rules?share_id={share_id}'
'&key1=value1&key2=value2')
}
]),
]
def list_rules():
return share_access_rule_policies

View File

@ -0,0 +1,49 @@
# Copyright 2018 Huawei Corporation.
# 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.
from oslo_policy import policy
from manila.policies import base
BASE_POLICY_NAME = 'share_access_metadata:%s'
share_access_rule_metadata_policies = [
policy.DocumentedRuleDefault(
name=BASE_POLICY_NAME % 'update',
check_str=base.RULE_DEFAULT,
description="Set metadata for a share access rule.",
operations=[
{
'method': 'PUT',
'path': '/share-access-rules/{share_access_id}/metadata'
}
]),
policy.DocumentedRuleDefault(
name=BASE_POLICY_NAME % 'delete',
check_str=base.RULE_DEFAULT,
description="Delete metadata for a share access rule.",
operations=[
{
'method': 'DELETE',
'path': '/share-access-rules/{share_access_id}/metadata/{key}'
}
]),
]
def list_rules():
return share_access_rule_metadata_policies

View File

@ -73,7 +73,7 @@ class API(base.Base):
"""Create new share."""
policy.check_policy(context, 'share', 'create')
self._check_metadata_properties(context, metadata)
self._check_metadata_properties(metadata)
if snapshot_id is not None:
snapshot = self.get_snapshot(context, snapshot_id)
@ -1598,7 +1598,7 @@ class API(base.Base):
INVALID_SHARE_INSTANCE_STATUSES_FOR_ACCESS_RULE_UPDATES)
def allow_access(self, ctx, share, access_type, access_to,
access_level=None):
access_level=None, metadata=None):
"""Allow access to share."""
# Access rule validation:
@ -1606,6 +1606,7 @@ class API(base.Base):
msg = _("Invalid share access level: %s.") % access_level
raise exception.InvalidShareAccess(reason=msg)
self._check_metadata_properties(metadata)
access_exists = self.db.share_access_check_for_existing_access(
ctx, share['id'], access_type, access_to)
@ -1626,6 +1627,7 @@ class API(base.Base):
'access_type': access_type,
'access_to': access_to,
'access_level': access_level,
'metadata': metadata,
}
access = self.db.share_access_create(ctx, values)
@ -1672,10 +1674,11 @@ class API(base.Base):
self.share_rpcapi.update_access(context, share_instance)
def access_get_all(self, context, share):
def access_get_all(self, context, share, filters=None):
"""Returns all access rules for share."""
policy.check_policy(context, 'share', 'access_get_all')
rules = self.db.share_access_get_all_for_share(context, share['id'])
rules = self.db.share_access_get_all_for_share(
context, share['id'], filters=filters)
return rules
def access_get(self, context, access_id):
@ -1705,7 +1708,7 @@ class API(base.Base):
}
raise exception.ShareBusyException(reason=msg)
def _check_metadata_properties(self, context, metadata=None):
def _check_metadata_properties(self, metadata=None):
if not metadata:
metadata = {}
@ -1713,21 +1716,27 @@ class API(base.Base):
if not k:
msg = _("Metadata property key is blank.")
LOG.warning(msg)
raise exception.InvalidShareMetadata(message=msg)
raise exception.InvalidMetadata(message=msg)
if len(k) > 255:
msg = _("Metadata property key is "
"greater than 255 characters.")
LOG.warning(msg)
raise exception.InvalidShareMetadataSize(message=msg)
raise exception.InvalidMetadataSize(message=msg)
if not v:
msg = _("Metadata property value is blank.")
LOG.warning(msg)
raise exception.InvalidShareMetadata(message=msg)
raise exception.InvalidMetadata(message=msg)
if len(v) > 1023:
msg = _("Metadata property value is "
"greater than 1023 characters.")
LOG.warning(msg)
raise exception.InvalidShareMetadataSize(message=msg)
raise exception.InvalidMetadataSize(message=msg)
def update_share_access_metadata(self, context, access_id, metadata):
"""Updates share access metadata."""
self._check_metadata_properties(metadata)
return self.db.share_access_metadata_update(
context, access_id, metadata)
@policy.wrap_check_policy('share')
def update_share_metadata(self, context, share, metadata, delete=False):
@ -1744,7 +1753,7 @@ class API(base.Base):
_metadata = orig_meta.copy()
_metadata.update(metadata)
self._check_metadata_properties(context, _metadata)
self._check_metadata_properties(_metadata)
self.db.share_metadata_update(context, share['id'],
_metadata, delete)

View File

@ -0,0 +1,132 @@
# Copyright (c) 2018 Huawei Inc.
# 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 ddt
import mock
import webob
from manila.api.v2 import share_access_metadata
from manila.api.v2 import share_accesses
from manila import context
from manila import exception
from manila import policy
from manila import test
from manila.tests.api import fakes
from manila.tests import db_utils
from oslo_utils import uuidutils
@ddt.ddt
class ShareAccessesMetadataAPITest(test.TestCase):
def _get_request(self, version="2.45", use_admin_context=True):
req = fakes.HTTPRequest.blank(
'/v2/share-access-rules',
version=version, use_admin_context=use_admin_context)
return req
def setUp(self):
super(ShareAccessesMetadataAPITest, self).setUp()
self.controller = (
share_access_metadata.ShareAccessMetadataController())
self.access_controller = (
share_accesses.ShareAccessesController())
self.resource_name = self.controller.resource_name
self.admin_context = context.RequestContext('admin', 'fake', True)
self.member_context = context.RequestContext('fake', 'fake')
self.mock_policy_check = self.mock_object(
policy, 'check_policy', mock.Mock(return_value=True))
self.share = db_utils.create_share()
self.access = db_utils.create_share_access(
id=uuidutils.generate_uuid(),
share_id=self.share['id'])
@ddt.data({'body': {'metadata': {'key1': 'v1'}}},
{'body': {'metadata': {'test_key1': 'test_v1'}}},
{'body': {'metadata': {'key1': 'v2'}}})
@ddt.unpack
def test_update_metadata(self, body):
url = self._get_request()
update = self.controller.update(url, self.access['id'], body=body)
self.assertEqual(body, update)
show_result = self.access_controller.show(url, self.access['id'])
self.assertEqual(1, len(show_result))
self.assertIn(self.access['id'], show_result['access']['id'])
self.assertEqual(body['metadata'], show_result['access']['metadata'])
def test_delete_metadata(self):
body = {'metadata': {'test_key3': 'test_v3'}}
url = self._get_request()
self.controller.update(url, self.access['id'], body=body)
self.controller.delete(url, self.access['id'], 'test_key3')
show_result = self.access_controller.show(url, self.access['id'])
self.assertEqual(1, len(show_result))
self.assertIn(self.access['id'], show_result['access']['id'])
self.assertNotIn('test_key3', show_result['access']['metadata'])
def test_update_access_metadata_with_access_id_not_found(self):
self.assertRaises(
webob.exc.HTTPNotFound,
self.controller.update,
self._get_request(), 'not_exist_access_id',
{'metadata': {'key1': 'v1'}})
def test_update_access_metadata_with_body_error(self):
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller.update,
self._get_request(), self.access['id'],
{'metadata_error': {'key1': 'v1'}})
@ddt.data({'metadata': {'key1': 'v1', 'key2': None}},
{'metadata': {None: 'v1', 'key2': 'v2'}},
{'metadata': {'k' * 256: 'v2'}},
{'metadata': {'key1': 'v' * 1024}})
@ddt.unpack
def test_update_metadata_with_invalid_metadata(self, metadata):
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller.update,
self._get_request(), self.access['id'],
{'metadata': metadata})
def test_delete_access_metadata_not_found(self):
body = {'metadata': {'test_key_exist': 'test_v_exsit'}}
update = self.controller.update(
self._get_request(), self.access['id'], body=body)
self.assertEqual(body, update)
self.assertRaises(
webob.exc.HTTPNotFound,
self.controller.delete,
self._get_request(), self.access['id'], 'key1')
@ddt.data('1.0', '2.0', '2.8', '2.44')
def test_update_metadata_unsupported_version(self, version):
self.assertRaises(
exception.VersionNotFoundForAPIMethod,
self.controller.update,
self._get_request(version=version), self.access['id'],
{'metadata': {'key1': 'v1'}})
@ddt.data('1.0', '2.0', '2.43')
def test_delete_metadata_with_unsupported_version(self, version):
self.assertRaises(
exception.VersionNotFoundForAPIMethod,
self.controller.delete,
self._get_request(version=version), self.access['id'], 'key1')

View File

@ -0,0 +1,134 @@
# Copyright (c) 2018 Huawei Inc.
# 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 ddt
import mock
from webob import exc
from manila.api.v2 import share_accesses
from manila import exception
from manila import policy
from manila import test
from manila.tests.api import fakes
from manila.tests import db_utils
from oslo_utils import uuidutils
@ddt.ddt
class ShareAccessesAPITest(test.TestCase):
def _get_index_request(self, share_id=None, filters='', version="2.45",
use_admin_context=True):
share_id = share_id or self.share['id']
req = fakes.HTTPRequest.blank(
'/v2/share-access-rules?share_id=%s' % share_id + filters,
version=version, use_admin_context=use_admin_context)
return req
def _get_show_request(self, access_id=None, version="2.45",
use_admin_context=True):
access_id = access_id or self.access['id']
req = fakes.HTTPRequest.blank(
'/v2/share-access-rules/%s' % access_id,
version=version, use_admin_context=use_admin_context)
return req
def setUp(self):
super(ShareAccessesAPITest, self).setUp()
self.controller = (
share_accesses.ShareAccessesController())
self.resource_name = self.controller.resource_name
self.mock_policy_check = self.mock_object(
policy, 'check_policy', mock.Mock(return_value=True))
self.share = db_utils.create_share()
self.access = db_utils.create_share_access(
id=uuidutils.generate_uuid(),
share_id=self.share['id'],
)
db_utils.create_share_access(
id=uuidutils.generate_uuid(),
share_id=self.share['id'],
metadata={'k1': 'v1'}
)
@ddt.data({'role': 'admin', 'version': '2.45',
'filters': '&metadata=%7B%27k1%27%3A+%27v1%27%7D'},
{'role': 'user', 'version': '2.45', 'filters': ''})
@ddt.unpack
def test_list_and_show(self, role, version, filters):
summary_keys = ['id', 'access_level', 'access_to',
'access_type', 'state', 'metadata']
self._test_list_and_show(role, filters, version, summary_keys)
def _test_list_and_show(self, role, filters, version, summary_keys):
req = self._get_index_request(
filters=filters, version=version,
use_admin_context=(role == 'admin'))
index_result = self.controller.index(req)
self.assertIn('access_list', index_result)
self.assertEqual(1, len(index_result))
access_count = 1 if filters else 2
self.assertEqual(access_count, len(index_result['access_list']))
for index_access in index_result['access_list']:
self.assertIn('id', index_access)
req = self._get_show_request(
index_access['id'], version=version,
use_admin_context=(role == 'admin'))
show_result = self.controller.show(req, index_access['id'])
self.assertIn('access', show_result)
self.assertEqual(1, len(show_result))
show_el = show_result['access']
# Ensure keys common to index & show results have matching values
for key in summary_keys:
self.assertEqual(index_access[key], show_el[key])
def test_list_accesses_share_not_found(self):
self.assertRaises(
exc.HTTPBadRequest,
self.controller.index,
self._get_index_request(share_id='inexistent_share_id'))
def test_list_accesses_share_req_share_id_not_exist(self):
req = fakes.HTTPRequest.blank('/v2/share-access-rules?',
version="2.45")
self.assertRaises(exc.HTTPBadRequest, self.controller.index, req)
def test_show_access_not_found(self):
self.assertRaises(
exc.HTTPNotFound,
self.controller.show,
self._get_show_request('inexistent_id'), 'inexistent_id')
@ddt.data('1.0', '2.0', '2.8', '2.44')
def test_list_with_unsupported_version(self, version):
self.assertRaises(
exception.VersionNotFoundForAPIMethod,
self.controller.index,
self._get_index_request(version=version))
@ddt.data('1.0', '2.0', '2.44')
def test_show_with_unsupported_version(self, version):
self.assertRaises(
exception.VersionNotFoundForAPIMethod,
self.controller.show,
self._get_show_request(version=version),
self.access['id'])

View File

@ -1949,6 +1949,12 @@ class ShareActionsTest(test.TestCase):
"version": "2.38"},
{"access": {'access_type': 'ip', 'access_to': '127.0.0.1'},
"version": "2.38"},
{"access": {'access_type': 'ip', 'access_to': '127.0.0.1',
'metadata': {'test_key': 'test_value'}},
"version": "2.45"},
{"access": {'access_type': 'ip', 'access_to': '127.0.0.1',
'metadata': {'k' * 255: 'v' * 1023}},
"version": "2.45"},
)
def test_allow_access(self, access, version):
self.mock_object(share_api.API,
@ -2008,6 +2014,12 @@ class ShareActionsTest(test.TestCase):
"version": "2.38"},
{"access": {'access_type': 'ip', 'access_to': 'ad80::abaa:0:c2:2/64'},
"version": "2.38"},
{"access": {'access_type': 'ip', 'access_to': '127.0.0.1',
'metadata': {'k' * 256: 'v' * 1024}},
"version": "2.45"},
{"access": {'access_type': 'ip', 'access_to': '127.0.0.1',
'metadata': {'key': None}},
"version": "2.45"},
)
def test_allow_access_error(self, access, version):
id = 'fake_share_id'
@ -2130,7 +2142,7 @@ class ShareActionsTest(test.TestCase):
self.assertEqual(expected_access, access['access'])
share_api.API.allow_access.assert_called_once_with(
req.environ['manila.context'], share, 'user',
'clemsontigers', 'rw')
'clemsontigers', 'rw', None)
@ddt.data(*itertools.product(
set(['2.28', api_version._MAX_API_VERSION]),
@ -2171,10 +2183,16 @@ class ShareActionsTest(test.TestCase):
'updated_at': updated_access['updated_at'],
})
if api_version.APIVersionRequest(version) >= (
api_version.APIVersionRequest("2.45")):
expected_access.update(
{
'metadata': {},
})
self.assertEqual(expected_access, access['access'])
share_api.API.allow_access.assert_called_once_with(
req.environ['manila.context'], share, 'user',
'clemsontigers', 'rw')
'clemsontigers', 'rw', None)
def test_deny_access(self):
def _stub_deny_access(*args, **kwargs):

View File

@ -39,6 +39,7 @@ class ViewBuilderTestCase(test.TestCase):
'access_key': 'fakeaccesskey',
'created_at': 'fakecreated_at',
'updated_at': 'fakeupdated_at',
'metadata': {},
}
self.fake_share = {
'access_rules_status': self.fake_access['state'],
@ -47,46 +48,29 @@ class ViewBuilderTestCase(test.TestCase):
def test_collection_name(self):
self.assertEqual('share_accesses', self.builder._collection_name)
@ddt.data("2.20", "2.21", "2.33")
@ddt.data("2.20", "2.21", "2.33", "2.45")
def test_view(self, version):
req = fakes.HTTPRequest.blank('/shares', version=version)
self.mock_object(api.API, 'get',
mock.Mock(return_value=self.fake_share))
result = self.builder.view(req, self.fake_access)
if (api_version.APIVersionRequest(version) <
api_version.APIVersionRequest("2.21")):
del self.fake_access['access_key']
if (api_version.APIVersionRequest(version) <
api_version.APIVersionRequest("2.33")):
del self.fake_access['created_at']
del self.fake_access['updated_at']
self._delete_unsupport_key(version, True)
self.assertEqual({'access': self.fake_access}, result)
@ddt.data("2.20", "2.21", "2.33")
@ddt.data("2.20", "2.21", "2.33", "2.45")
def test_summary_view(self, version):
req = fakes.HTTPRequest.blank('/shares', version=version)
self.mock_object(api.API, 'get',
mock.Mock(return_value=self.fake_share))
result = self.builder.summary_view(req, self.fake_access)
if (api_version.APIVersionRequest(version) <
api_version.APIVersionRequest("2.21")):
del self.fake_access['access_key']
if (api_version.APIVersionRequest(version) <
api_version.APIVersionRequest("2.33")):
del self.fake_access['created_at']
del self.fake_access['updated_at']
del self.fake_access['share_id']
self._delete_unsupport_key(version)
self.assertEqual({'access': self.fake_access}, result)
@ddt.data("2.20", "2.21", "2.33")
@ddt.data("2.20", "2.21", "2.33", "2.45")
def test_list_view(self, version):
req = fakes.HTTPRequest.blank('/shares', version=version)
self.mock_object(api.API, 'get',
@ -94,7 +78,11 @@ class ViewBuilderTestCase(test.TestCase):
accesses = [self.fake_access, ]
result = self.builder.list_view(req, accesses)
self._delete_unsupport_key(version)
self.assertEqual({'access_list': accesses}, result)
def _delete_unsupport_key(self, version, support_share_id=False):
if (api_version.APIVersionRequest(version) <
api_version.APIVersionRequest("2.21")):
del self.fake_access['access_key']
@ -103,6 +91,8 @@ class ViewBuilderTestCase(test.TestCase):
api_version.APIVersionRequest("2.33")):
del self.fake_access['created_at']
del self.fake_access['updated_at']
del self.fake_access['share_id']
self.assertEqual({'access_list': accesses}, result)
if (api_version.APIVersionRequest(version) <
api_version.APIVersionRequest("2.45")):
del self.fake_access['metadata']
if not support_share_id:
del self.fake_access['share_id']

View File

@ -2687,3 +2687,73 @@ class ShareInstancesShareIdIndexChecks(BaseMigrationChecks):
def check_downgrade(self, engine):
self.test_case.assertFalse(
self._get_share_instances_share_id_index(engine))
@map_to_migration('11ee96se625f3')
class AccessMetadataTableChecks(BaseMigrationChecks):
new_table_name = 'share_access_rules_metadata'
record_access_id = uuidutils.generate_uuid()
def setup_upgrade_data(self, engine):
share_data = {
'id': uuidutils.generate_uuid(),
'share_proto': "NFS",
'size': 1,
'snapshot_id': None,
'user_id': 'fake',
'project_id': 'fake'
}
share_table = utils.load_table('shares', engine)
engine.execute(share_table.insert(share_data))
share_instance_data = {
'id': uuidutils.generate_uuid(),
'deleted': 'False',
'host': 'fake',
'share_id': share_data['id'],
'status': 'available',
'access_rules_status': 'active',
'cast_rules_to_readonly': False,
}
share_instance_table = utils.load_table('share_instances', engine)
engine.execute(share_instance_table.insert(share_instance_data))
share_access_data = {
'id': self.record_access_id,
'share_id': share_data['id'],
'access_type': 'NFS',
'access_to': '10.0.0.1',
'deleted': 'False'
}
share_access_table = utils.load_table('share_access_map', engine)
engine.execute(share_access_table.insert(share_access_data))
share_instance_access_data = {
'id': uuidutils.generate_uuid(),
'share_instance_id': share_instance_data['id'],
'access_id': share_access_data['id'],
'deleted': 'False'
}
share_instance_access_table = utils.load_table(
'share_instance_access_map', engine)
engine.execute(share_instance_access_table.insert(
share_instance_access_data))
def check_upgrade(self, engine, data):
data = {
'id': 1,
'key': 't' * 255,
'value': 'v' * 1023,
'access_id': self.record_access_id,
'created_at': datetime.datetime(2017, 7, 10, 18, 5, 58),
'updated_at': None,
'deleted_at': None,
'deleted': 'False',
}
new_table = utils.load_table(self.new_table_name, engine)
engine.execute(new_table.insert(data))
def check_downgrade(self, engine):
self.test_case.assertRaises(sa_exc.NoSuchTableError, utils.load_table,
self.new_table_name, engine)

View File

@ -197,7 +197,8 @@ class ShareAccessDatabaseAPITestCase(test.TestCase):
def test_share_instance_access_delete(self):
share = db_utils.create_share()
access = db_utils.create_access(share_id=share['id'])
access = db_utils.create_access(share_id=share['id'],
metadata={'key1': 'v1'})
instance_access_mapping = db_api.share_instance_access_get(
self.ctxt, access['id'], share.instance['id'])
@ -211,6 +212,38 @@ class ShareAccessDatabaseAPITestCase(test.TestCase):
self.assertRaises(exception.NotFound, db_api.share_instance_access_get,
self.ctxt, access['id'], share['instance']['id'])
def test_one_share_with_two_share_instance_access_delete(self):
metadata = {'key2': 'v2', 'key3': 'v3'}
share = db_utils.create_share()
instance = db_utils.create_share_instance(share_id=share['id'])
access = db_utils.create_access(share_id=share['id'],
metadata=metadata)
instance_access_mapping1 = db_api.share_instance_access_get(
self.ctxt, access['id'], share.instance['id'])
instance_access_mapping2 = db_api.share_instance_access_get(
self.ctxt, access['id'], instance['id'])
self.assertEqual(instance_access_mapping1['access_id'],
instance_access_mapping2['access_id'])
db_api.share_instance_delete(self.ctxt, instance['id'])
get_accesses = db_api.share_access_get_all_for_share(self.ctxt,
share['id'])
self.assertEqual(1, len(get_accesses))
get_metadata = (
get_accesses[0].get('share_access_rules_metadata') or {})
get_metadata = {item['key']: item['value'] for item in get_metadata}
self.assertEqual(metadata, get_metadata)
self.assertEqual(access['id'], get_accesses[0]['id'])
db_api.share_instance_delete(self.ctxt, share['instance']['id'])
self.assertRaises(exception.NotFound,
db_api.share_instance_access_get,
self.ctxt, access['id'], share['instance']['id'])
get_accesses = db_api.share_access_get_all_for_share(self.ctxt,
share['id'])
self.assertEqual(0, len(get_accesses))
@ddt.data(True, False)
def test_share_instance_access_get_with_share_access_data(
self, with_share_access_data):
@ -263,6 +296,40 @@ class ShareAccessDatabaseAPITestCase(test.TestCase):
self.assertEqual(result, rule_exists)
def test_share_access_get_all_for_share_with_metadata(self):
share = db_utils.create_share()
rules = [db_utils.create_access(
share_id=share['id'], metadata={'key1': i})
for i in range(0, 3)]
rule_ids = [r['id'] for r in rules]
result = db_api.share_access_get_all_for_share(self.ctxt, share['id'])
self.assertEqual(3, len(result))
result_ids = [r['id'] for r in result]
self.assertEqual(rule_ids, result_ids)
result = db_api.share_access_get_all_for_share(
self.ctxt, share['id'], {'metadata': {'key1': '2'}})
self.assertEqual(1, len(result))
self.assertEqual(rules[2]['id'], result[0]['id'])
def test_share_access_metadata_update(self):
share = db_utils.create_share()
new_metadata = {'key1': 'test_update', 'key2': 'v2'}
rule = db_utils.create_access(share_id=share['id'],
metadata={'key1': 'v1'})
result_metadata = db_api.share_access_metadata_update(
self.ctxt, rule['id'], metadata=new_metadata)
result = db_api.share_access_get(self.ctxt, rule['id'])
self.assertEqual(new_metadata, result_metadata)
metadata = result.get('share_access_rules_metadata')
if metadata:
metadata = {item['key']: item['value'] for item in metadata}
else:
metadata = {}
self.assertEqual(new_metadata, metadata)
@ddt.ddt
class ShareDatabaseAPITestCase(test.TestCase):

View File

@ -2029,6 +2029,7 @@ class ShareAPITestCase(test.TestCase):
'access_type': 'fake_access_type',
'access_to': 'fake_access_to',
'access_level': level,
'metadata': None,
}
fake_access = copy.deepcopy(values)
fake_access.update({
@ -2168,7 +2169,7 @@ class ShareAPITestCase(test.TestCase):
share_api.policy.check_policy.assert_called_once_with(
self.context, 'share', 'access_get_all')
db_api.share_access_get_all_for_share.assert_called_once_with(
self.context, 'fakeid')
self.context, 'fakeid', filters=None)
def test_share_metadata_get(self):
metadata = {'a': 'b', 'c': 'd'}

View File

@ -216,13 +216,13 @@ class ManilaExceptionResponseCode400(test.TestCase):
self.assertIn(reason, e.msg)
def test_invalid_share_metadata(self):
# Verify response code for exception.InvalidShareMetadata
e = exception.InvalidShareMetadata()
# Verify response code for exception.InvalidMetadata
e = exception.InvalidMetadata()
self.assertEqual(400, e.code)
def test_invalid_share_metadata_size(self):
# Verify response code for exception.InvalidShareMetadataSize
e = exception.InvalidShareMetadataSize()
# Verify response code for exception.InvalidMetadataSize
e = exception.InvalidMetadataSize()
self.assertEqual(400, e.code)
def test_invalid_volume(self):

View File

@ -0,0 +1,18 @@
---
features:
- |
Metadata can be added to share access rules as key=value pairs, and also
introduced the GET /share-access-rules API with API version 2.45.
The prior API to retrieve access rules of a given share,
POST /shares/{share-id}/action {'access-list: null} has been
removed in API version 2.45.
upgrade:
- |
The API GET /share-access-rules?share_id={share-id} replaces
POST /shares/{share-id}/action with body {'access_list': null} in
API version 2.45. The new API supports access rule metadata and is expected
to support sorting, filtering and pagination features along with newer
fields to interact with access rules in future versions. The API request
header 'X-OpenStack-Manila-API-Version' can be set to 2.44 to
continue using the prior API to retrieve access rules, but no new features
will be added to that API.