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:
parent
27f1474d7b
commit
0957b33e9b
@ -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
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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):
|
||||
|
@ -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')
|
||||
|
@ -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"]})
|
||||
|
84
manila/api/v2/share_access_metadata.py
Normal file
84
manila/api/v2/share_access_metadata.py
Normal 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())
|
81
manila/api/v2/share_accesses.py
Normal file
81
manila/api/v2/share_accesses.py
Normal 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())
|
@ -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."""
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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.")
|
||||
|
||||
|
||||
|
@ -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(),
|
||||
)
|
||||
|
50
manila/policies/share_access.py
Normal file
50
manila/policies/share_access.py
Normal 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
|
49
manila/policies/share_access_metadata.py
Normal file
49
manila/policies/share_access_metadata.py
Normal 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
|
@ -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)
|
||||
|
||||
|
132
manila/tests/api/v2/test_share_access_metadata.py
Normal file
132
manila/tests/api/v2/test_share_access_metadata.py
Normal 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')
|
134
manila/tests/api/v2/test_share_accesses.py
Normal file
134
manila/tests/api/v2/test_share_accesses.py
Normal 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'])
|
@ -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):
|
||||
|
@ -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']
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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'}
|
||||
|
@ -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):
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user