Merge "Metadata for Share Export Location"
This commit is contained in:
commit
84ab5df84a
@ -204,13 +204,14 @@ REST_API_VERSION_HISTORY = """
|
||||
* 2.84 - Added mount_point_name to shares.
|
||||
* 2.85 - Added backup_type field to share backups.
|
||||
* 2.86 - Add ensure share API.
|
||||
* 2.87 - Added Share export location metadata API
|
||||
"""
|
||||
|
||||
# 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.86"
|
||||
_MAX_API_VERSION = "2.87"
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
|
||||
|
@ -462,3 +462,8 @@ user documentation.
|
||||
2.86
|
||||
----
|
||||
Added ensure shares API.
|
||||
|
||||
2.87
|
||||
----
|
||||
Added Metadata API methods (GET, PUT, POST, DELETE)
|
||||
to Share Export Locations.
|
||||
|
@ -28,36 +28,42 @@ class MetadataController(object):
|
||||
"share": "share_get",
|
||||
"share_snapshot": "share_snapshot_get",
|
||||
"share_network_subnet": "share_network_subnet_get",
|
||||
"share_export_location": "export_location_get_by_uuid",
|
||||
}
|
||||
|
||||
resource_metadata_get = {
|
||||
"share": "share_metadata_get",
|
||||
"share_snapshot": "share_snapshot_metadata_get",
|
||||
"share_network_subnet": "share_network_subnet_metadata_get",
|
||||
"share_export_location": "export_location_metadata_get",
|
||||
}
|
||||
|
||||
resource_metadata_get_item = {
|
||||
"share": "share_metadata_get_item",
|
||||
"share_snapshot": "share_snapshot_metadata_get_item",
|
||||
"share_network_subnet": "share_network_subnet_metadata_get_item",
|
||||
"share_export_location": "export_location_metadata_get_item",
|
||||
}
|
||||
|
||||
resource_metadata_update = {
|
||||
"share": "share_metadata_update",
|
||||
"share_snapshot": "share_snapshot_metadata_update",
|
||||
"share_network_subnet": "share_network_subnet_metadata_update",
|
||||
"share_export_location": "export_location_metadata_update",
|
||||
}
|
||||
|
||||
resource_metadata_update_item = {
|
||||
"share": "share_metadata_update_item",
|
||||
"share_snapshot": "share_snapshot_metadata_update_item",
|
||||
"share_network_subnet": "share_network_subnet_metadata_update_item",
|
||||
"share_export_location": "export_location_metadata_update_item",
|
||||
}
|
||||
|
||||
resource_metadata_delete = {
|
||||
"share": "share_metadata_delete",
|
||||
"share_snapshot": "share_snapshot_metadata_delete",
|
||||
"share_network_subnet": "share_network_subnet_metadata_delete",
|
||||
"share_export_location": "export_location_metadata_delete",
|
||||
}
|
||||
|
||||
resource_policy_get = {
|
||||
@ -72,10 +78,13 @@ class MetadataController(object):
|
||||
|
||||
def _get_resource(self, context, resource_id,
|
||||
for_modification=False, parent_id=None):
|
||||
if self.resource_name in ['share', 'share_network_subnet']:
|
||||
# we would allow retrieving some "public" resources
|
||||
# across project namespaces except share snapshots,
|
||||
# project_only=True is hard coded
|
||||
if self.resource_name in ['share', 'share_network_subnet',
|
||||
'share_export_location']:
|
||||
# some resources don't have a "project_id" field (like
|
||||
# share_export_location or share_network_subnet),
|
||||
# and sometimes we want to retrieve "public" resources
|
||||
# (like shares), so avoid hard coding project_only=True in the
|
||||
# lookup where necessary
|
||||
kwargs = {}
|
||||
else:
|
||||
kwargs = {'project_only': True}
|
||||
@ -86,23 +95,25 @@ class MetadataController(object):
|
||||
kwargs["parent_id"] = parent_id
|
||||
res = get_res_method(context, resource_id, **kwargs)
|
||||
|
||||
get_policy = self.resource_policy_get[self.resource_name]
|
||||
if res.get('is_public') is False:
|
||||
authorized = policy.check_policy(context,
|
||||
self.resource_name,
|
||||
get_policy,
|
||||
res,
|
||||
do_raise=False)
|
||||
if not authorized:
|
||||
# Raising NotFound to prevent existence detection
|
||||
raise exception.NotFound()
|
||||
elif for_modification:
|
||||
# a public resource's metadata can be viewed, but not
|
||||
# modified by non owners
|
||||
policy.check_policy(context,
|
||||
self.resource_name,
|
||||
get_policy,
|
||||
res)
|
||||
if self.resource_name not in ["share_export_location"]:
|
||||
get_policy = self.resource_policy_get[self.resource_name]
|
||||
# skip policy check for export locations
|
||||
if res.get('is_public') is False:
|
||||
authorized = policy.check_policy(context,
|
||||
self.resource_name,
|
||||
get_policy,
|
||||
res,
|
||||
do_raise=False)
|
||||
if not authorized:
|
||||
# Raising NotFound to prevent existence detection
|
||||
raise exception.NotFound()
|
||||
elif for_modification:
|
||||
# a public resource's metadata can be viewed, but not
|
||||
# modified by non owners
|
||||
policy.check_policy(context,
|
||||
self.resource_name,
|
||||
get_policy,
|
||||
res)
|
||||
except exception.NotFound:
|
||||
msg = _('%s not found.' % self.resource_name.capitalize())
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
@ -120,6 +131,7 @@ class MetadataController(object):
|
||||
|
||||
@wsgi.response(200)
|
||||
def _index_metadata(self, req, resource_id, parent_id=None):
|
||||
"""Lists existing metadata."""
|
||||
context = req.environ['manila.context']
|
||||
metadata = self._get_metadata(context, resource_id,
|
||||
parent_id=parent_id)
|
||||
|
@ -259,6 +259,45 @@ class APIRouter(manila.api.openstack.APIRouter):
|
||||
controller=self.resources["share_export_locations"],
|
||||
action="show",
|
||||
conditions={"method": ["GET"]})
|
||||
mapper.connect("export_locations_metadata",
|
||||
"%s/shares/{share_id}/export_locations"
|
||||
"/{resource_id}/metadata" % path_prefix,
|
||||
controller=self.resources["share_export_locations"],
|
||||
action="create_metadata",
|
||||
conditions={"method": ["POST"]})
|
||||
mapper.connect("export_locations_metadata",
|
||||
"%s/shares/{share_id}/export_locations"
|
||||
"/{resource_id}/metadata" % path_prefix,
|
||||
controller=self.resources["share_export_locations"],
|
||||
action="update_all_metadata",
|
||||
conditions={"method": ["PUT"]})
|
||||
mapper.connect("export_locations_metadata",
|
||||
"%s/shares/{share_id}/export_locations/"
|
||||
"{resource_id}/metadata/{key}"
|
||||
% path_prefix,
|
||||
controller=self.resources["share_export_locations"],
|
||||
action="update_metadata_item",
|
||||
conditions={"method": ["POST"]})
|
||||
mapper.connect("export_locations_metadata",
|
||||
"%s/shares/{share_id}/export_locations/"
|
||||
"{resource_id}/metadata" % path_prefix,
|
||||
controller=self.resources["share_export_locations"],
|
||||
action="index_metadata",
|
||||
conditions={"method": ["GET"]})
|
||||
mapper.connect("export_locations_metadata",
|
||||
"%s/shares/{share_id}/export_locations/"
|
||||
"{resource_id}/metadata/{key}"
|
||||
% path_prefix,
|
||||
controller=self.resources["share_export_locations"],
|
||||
action="show_metadata",
|
||||
conditions={"method": ["GET"]})
|
||||
mapper.connect("export_locations_metadata",
|
||||
"%s/shares/{share_id}/export_locations/"
|
||||
"{resource_id}/metadata/{key}"
|
||||
% path_prefix,
|
||||
controller=self.resources["share_export_locations"],
|
||||
action="delete_metadata",
|
||||
conditions={"method": ["DELETE"]})
|
||||
|
||||
self.resources["snapshots"] = share_snapshots.create_resource()
|
||||
mapper.resource("snapshot", "snapshots",
|
||||
|
@ -13,23 +13,33 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from webob import exc
|
||||
|
||||
from manila.api.openstack import wsgi
|
||||
from manila.api.v2 import metadata
|
||||
from manila.api.views import export_locations as export_locations_views
|
||||
from manila.db import api as db_api
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
from manila import policy
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
class ShareExportLocationController(wsgi.Controller):
|
||||
|
||||
class ShareExportLocationController(wsgi.Controller,
|
||||
metadata.MetadataController):
|
||||
"""The Share Export Locations API controller."""
|
||||
|
||||
def __init__(self):
|
||||
self._view_builder_class = export_locations_views.ViewBuilder
|
||||
self.resource_name = 'share_export_location'
|
||||
super(ShareExportLocationController, self).__init__()
|
||||
self._conf_admin_only_metadata_keys = getattr(
|
||||
CONF, 'admin_only_el_metadata', []
|
||||
)
|
||||
|
||||
def _verify_share(self, context, share_id):
|
||||
try:
|
||||
@ -94,6 +104,96 @@ class ShareExportLocationController(wsgi.Controller):
|
||||
return self._show(req, share_id, export_location_uuid,
|
||||
ignore_secondary_replicas=True)
|
||||
|
||||
def _validate_metadata_for_update(self, req, share_export_location,
|
||||
metadata, delete=True):
|
||||
persistent_keys = set(self._conf_admin_only_metadata_keys)
|
||||
context = req.environ['manila.context']
|
||||
if set(metadata).intersection(persistent_keys):
|
||||
try:
|
||||
policy.check_policy(
|
||||
context, 'share_export_location',
|
||||
'update_admin_only_metadata')
|
||||
except exception.PolicyNotAuthorized:
|
||||
msg = _("Cannot set or update admin only metadata.")
|
||||
LOG.exception(msg)
|
||||
raise exc.HTTPForbidden(explanation=msg)
|
||||
persistent_keys = []
|
||||
|
||||
current_export_metadata = db_api.export_location_metadata_get(
|
||||
context, share_export_location)
|
||||
if delete:
|
||||
_metadata = metadata
|
||||
for key in persistent_keys:
|
||||
if key in current_export_metadata:
|
||||
_metadata[key] = current_export_metadata[key]
|
||||
else:
|
||||
metadata_copy = metadata.copy()
|
||||
for key in persistent_keys:
|
||||
metadata_copy.pop(key, None)
|
||||
_metadata = current_export_metadata.copy()
|
||||
_metadata.update(metadata_copy)
|
||||
|
||||
return _metadata
|
||||
|
||||
@wsgi.Controller.api_version("2.87")
|
||||
@wsgi.Controller.authorize("get_metadata")
|
||||
def index_metadata(self, req, share_id, resource_id):
|
||||
"""Returns the list of metadata for a given share export location."""
|
||||
context = req.environ['manila.context']
|
||||
self._verify_share(context, share_id)
|
||||
return self._index_metadata(req, resource_id)
|
||||
|
||||
@wsgi.Controller.api_version("2.87")
|
||||
@wsgi.Controller.authorize("update_metadata")
|
||||
def create_metadata(self, req, share_id, resource_id, body):
|
||||
"""Create metadata for a given share export location."""
|
||||
_metadata = self._validate_metadata_for_update(req, resource_id,
|
||||
body['metadata'],
|
||||
delete=False)
|
||||
body['metadata'] = _metadata
|
||||
context = req.environ['manila.context']
|
||||
self._verify_share(context, share_id)
|
||||
return self._create_metadata(req, resource_id, body)
|
||||
|
||||
@wsgi.Controller.api_version("2.87")
|
||||
@wsgi.Controller.authorize("update_metadata")
|
||||
def update_all_metadata(self, req, share_id, resource_id, body):
|
||||
"""Update entire metadata for a given share export location."""
|
||||
_metadata = self._validate_metadata_for_update(req, resource_id,
|
||||
body['metadata'])
|
||||
body['metadata'] = _metadata
|
||||
context = req.environ['manila.context']
|
||||
self._verify_share(context, share_id)
|
||||
return self._update_all_metadata(req, resource_id, body)
|
||||
|
||||
@wsgi.Controller.api_version("2.87")
|
||||
@wsgi.Controller.authorize("update_metadata")
|
||||
def update_metadata_item(self, req, share_id, resource_id, body, key):
|
||||
"""Update metadata item for a given share export location."""
|
||||
_metadata = self._validate_metadata_for_update(req, resource_id,
|
||||
body['metadata'],
|
||||
delete=False)
|
||||
body['metadata'] = _metadata
|
||||
context = req.environ['manila.context']
|
||||
self._verify_share(context, share_id)
|
||||
return self._update_metadata_item(req, resource_id, body, key)
|
||||
|
||||
@wsgi.Controller.api_version("2.87")
|
||||
@wsgi.Controller.authorize("get_metadata")
|
||||
def show_metadata(self, req, share_id, resource_id, key):
|
||||
"""Show metadata for a given share export location."""
|
||||
context = req.environ['manila.context']
|
||||
self._verify_share(context, share_id)
|
||||
return self._show_metadata(req, resource_id, key)
|
||||
|
||||
@wsgi.Controller.api_version("2.87")
|
||||
@wsgi.Controller.authorize("delete_metadata")
|
||||
def delete_metadata(self, req, share_id, resource_id, key):
|
||||
"""Delete metadata for a given share export location."""
|
||||
context = req.environ['manila.context']
|
||||
self._verify_share(context, share_id)
|
||||
return self._delete_metadata(req, resource_id, key)
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(ShareExportLocationController())
|
||||
|
@ -627,9 +627,9 @@ class ShareController(wsgi.Controller,
|
||||
|
||||
def _validate_metadata_for_update(self, req, share_id, metadata,
|
||||
delete=True):
|
||||
admin_metadata_ignore_keys = set(self._conf_admin_only_metadata_keys)
|
||||
persistent_keys = set(self._conf_admin_only_metadata_keys)
|
||||
context = req.environ['manila.context']
|
||||
if set(metadata).intersection(admin_metadata_ignore_keys):
|
||||
if set(metadata).intersection(persistent_keys):
|
||||
try:
|
||||
policy.check_policy(
|
||||
context, 'share', 'update_admin_only_metadata')
|
||||
@ -637,17 +637,17 @@ class ShareController(wsgi.Controller,
|
||||
msg = _("Cannot set or update admin only metadata.")
|
||||
LOG.exception(msg)
|
||||
raise exc.HTTPForbidden(explanation=msg)
|
||||
admin_metadata_ignore_keys = []
|
||||
persistent_keys = []
|
||||
|
||||
current_share_metadata = db.share_metadata_get(context, share_id)
|
||||
if delete:
|
||||
_metadata = metadata
|
||||
for key in admin_metadata_ignore_keys:
|
||||
for key in persistent_keys:
|
||||
if key in current_share_metadata:
|
||||
_metadata[key] = current_share_metadata[key]
|
||||
else:
|
||||
metadata_copy = metadata.copy()
|
||||
for key in admin_metadata_ignore_keys:
|
||||
for key in persistent_keys:
|
||||
metadata_copy.pop(key, None)
|
||||
_metadata = current_share_metadata.copy()
|
||||
_metadata.update(metadata_copy)
|
||||
|
@ -13,6 +13,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
|
||||
from oslo_utils import strutils
|
||||
|
||||
from manila.api import common
|
||||
@ -25,6 +27,7 @@ class ViewBuilder(common.ViewBuilder):
|
||||
|
||||
_detail_version_modifiers = [
|
||||
'add_preferred_path_attribute',
|
||||
'add_metadata_attribute',
|
||||
]
|
||||
|
||||
def _get_export_location_view(self, request, export_location,
|
||||
@ -87,3 +90,11 @@ class ViewBuilder(common.ViewBuilder):
|
||||
export_location):
|
||||
view_dict['preferred'] = strutils.bool_from_string(
|
||||
export_location['el_metadata'].get('preferred'))
|
||||
|
||||
@common.ViewBuilder.versioned_method('2.87')
|
||||
def add_metadata_attribute(self, context, view_dict,
|
||||
export_location):
|
||||
metadata = export_location.get('el_metadata')
|
||||
meta_copy = copy.copy(metadata)
|
||||
meta_copy.pop('preferred', None)
|
||||
view_dict['metadata'] = meta_copy
|
||||
|
@ -152,6 +152,10 @@ global_opts = [
|
||||
help='Whether Manila should update the status of all shares '
|
||||
'within a backend during ongoing ensure_shares '
|
||||
'run.'),
|
||||
cfg.ListOpt('admin_only_el_metadata',
|
||||
default=constants.AdminOnlyMetadata.EXPORT_LOCATION_KEYS,
|
||||
help='Metadata keys for export locations that should only be '
|
||||
'manipulated by administrators.'),
|
||||
]
|
||||
|
||||
CONF.register_opts(global_opts)
|
||||
|
@ -362,8 +362,13 @@ class ExtraSpecs(object):
|
||||
class AdminOnlyMetadata(object):
|
||||
AFFINITY_KEY = "__affinity_same_host"
|
||||
ANTI_AFFINITY_KEY = "__affinity_different_host"
|
||||
PREFERRED_KEY = "preferred"
|
||||
|
||||
SCHEDULER_FILTERS = [
|
||||
AFFINITY_KEY,
|
||||
ANTI_AFFINITY_KEY,
|
||||
]
|
||||
|
||||
EXPORT_LOCATION_KEYS = [
|
||||
PREFERRED_KEY,
|
||||
]
|
||||
|
@ -1004,6 +1004,13 @@ def export_location_metadata_get(context, export_location_uuid):
|
||||
return IMPL.export_location_metadata_get(context, export_location_uuid)
|
||||
|
||||
|
||||
def export_location_metadata_get_item(context, export_location_uuid, key):
|
||||
"""Get metadata item for a share export location."""
|
||||
return IMPL.export_location_metadata_get_item(context,
|
||||
export_location_uuid,
|
||||
key)
|
||||
|
||||
|
||||
def export_location_metadata_delete(context, export_location_uuid, keys):
|
||||
"""Delete metadata of an export location."""
|
||||
return IMPL.export_location_metadata_delete(
|
||||
@ -1016,6 +1023,14 @@ def export_location_metadata_update(context, export_location_uuid, metadata,
|
||||
return IMPL.export_location_metadata_update(
|
||||
context, export_location_uuid, metadata, delete)
|
||||
|
||||
|
||||
def export_location_metadata_update_item(context, export_location_uuid,
|
||||
metadata):
|
||||
"""Update metadata item if it exists, otherwise create it."""
|
||||
return IMPL.export_location_metadata_update_item(context,
|
||||
export_location_uuid,
|
||||
metadata)
|
||||
|
||||
####################
|
||||
|
||||
|
||||
|
@ -4698,6 +4698,33 @@ def _export_location_metadata_update(
|
||||
return metadata
|
||||
|
||||
|
||||
@require_context
|
||||
@context_manager.reader
|
||||
def export_location_metadata_get_item(context, export_location_uuid, key):
|
||||
|
||||
row = _export_location_metadata_get_item(
|
||||
context, export_location_uuid, key)
|
||||
result = {row['key']: row['value']}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@require_context
|
||||
@context_manager.writer
|
||||
def export_location_metadata_update_item(context, export_location_uuid,
|
||||
item):
|
||||
return _export_location_metadata_update(context, export_location_uuid,
|
||||
item, delete=False)
|
||||
|
||||
|
||||
def _export_location_metadata_get_item(context, export_location_uuid, key):
|
||||
result = _export_location_metadata_get_query(
|
||||
context, export_location_uuid,
|
||||
).filter_by(key=key).first()
|
||||
if not result:
|
||||
raise exception.MetadataItemNotFound()
|
||||
return result
|
||||
|
||||
###################################
|
||||
|
||||
|
||||
|
@ -34,6 +34,30 @@ deprecated_export_location_show = policy.DeprecatedRule(
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since=versionutils.deprecated.WALLABY
|
||||
)
|
||||
deprecated_update_export_location_metadata = policy.DeprecatedRule(
|
||||
name=BASE_POLICY_NAME % 'update_metadata',
|
||||
check_str=base.RULE_DEFAULT,
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since='2024.2/Dalmatian'
|
||||
)
|
||||
deprecated_delete_export_location_metadata = policy.DeprecatedRule(
|
||||
name=BASE_POLICY_NAME % 'delete_metadata',
|
||||
check_str=base.RULE_DEFAULT,
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since='2024.2/Dalmatian'
|
||||
)
|
||||
deprecated_get_export_location_metadata = policy.DeprecatedRule(
|
||||
name=BASE_POLICY_NAME % 'get_metadata',
|
||||
check_str=base.RULE_DEFAULT,
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since='2024.2/Dalmatian'
|
||||
)
|
||||
deprecated_update_admin_only_metadata = policy.DeprecatedRule(
|
||||
name=BASE_POLICY_NAME % 'update_admin_only_metadata',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since="2024.2/Dalmatian"
|
||||
)
|
||||
|
||||
|
||||
share_export_location_policies = [
|
||||
@ -64,6 +88,79 @@ share_export_location_policies = [
|
||||
],
|
||||
deprecated_rule=deprecated_export_location_show
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'update_metadata',
|
||||
check_str=base.ADMIN_OR_PROJECT_MEMBER,
|
||||
scope_types=['project'],
|
||||
description="Update share export location metadata.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'PUT',
|
||||
'path': ('/shares/{share_id}/export_locations/'
|
||||
'{export_location_id}/metadata'),
|
||||
},
|
||||
{
|
||||
'method': 'POST',
|
||||
'path': ('/shares/{share_id}/export_locations/'
|
||||
'{export_location_id}/metadata/{key}')
|
||||
},
|
||||
{
|
||||
'method': 'POST',
|
||||
'path': ('/shares/{share_id}/export_locations/'
|
||||
'{export_location_id}/metadata'),
|
||||
},
|
||||
],
|
||||
deprecated_rule=deprecated_update_export_location_metadata
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'delete_metadata',
|
||||
check_str=base.ADMIN_OR_PROJECT_MEMBER,
|
||||
scope_types=['project'],
|
||||
description="Delete share export location metadata",
|
||||
operations=[
|
||||
{
|
||||
'method': 'DELETE',
|
||||
'path': ('/shares/{share_id}/export_locations/'
|
||||
'{export_location_id}/metadata/{key}')
|
||||
},
|
||||
],
|
||||
deprecated_rule=deprecated_delete_export_location_metadata
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'get_metadata',
|
||||
check_str=base.ADMIN_OR_PROJECT_READER,
|
||||
scope_types=['project'],
|
||||
description='Get share export location metadata',
|
||||
operations=[
|
||||
{
|
||||
'method': "GET",
|
||||
'path': ('/shares/{share_id}/export_locations/'
|
||||
'{export_location_id}/metadata')
|
||||
},
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': ('/shares/{share_id}/export_locations/'
|
||||
'{export_location_id}/metadata/{key}')
|
||||
},
|
||||
],
|
||||
deprecated_rule=deprecated_get_export_location_metadata
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'update_admin_only_metadata',
|
||||
check_str=base.ADMIN,
|
||||
scope_types=['project'],
|
||||
description=(
|
||||
"Update metadata items that are considered \"admin only\" "
|
||||
"by the service."),
|
||||
operations=[
|
||||
{
|
||||
'method': 'PUT',
|
||||
'path': '/shares/{share_id}/export_locations/'
|
||||
'{export_location_id}/metadata',
|
||||
}
|
||||
],
|
||||
deprecated_rule=deprecated_update_admin_only_metadata
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
@ -249,3 +249,98 @@ class ShareExportLocationsAPITest(test.TestCase):
|
||||
self.share['id'],
|
||||
index_result['export_locations'][0]['id']
|
||||
)
|
||||
|
||||
def test_validate_metadata_for_update(self):
|
||||
index_result = self.controller.index(self.req, self.share['id'])
|
||||
el_id = index_result['export_locations'][0]['id']
|
||||
metadata = {"foo": "bar", "preferred": "False"}
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v2/shares/%s/export_locations/%s/metadata' % (
|
||||
self.share_instance_id, el_id),
|
||||
version="2.87", use_admin_context=True)
|
||||
result = self.controller._validate_metadata_for_update(
|
||||
req, el_id, metadata)
|
||||
|
||||
self.assertEqual(metadata, result)
|
||||
|
||||
def test_validate_metadata_for_update_invalid(self):
|
||||
index_result = self.controller.index(self.req, self.share['id'])
|
||||
el_id = index_result['export_locations'][0]['id']
|
||||
metadata = {"foo": "bar", "preferred": "False"}
|
||||
|
||||
self.mock_policy_check = self.mock_object(
|
||||
policy, 'check_policy', mock.Mock(
|
||||
side_effect=exception.PolicyNotAuthorized(
|
||||
action="update_admin_only_metadata")))
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v2/shares/%s/export_locations/%s/metadata' % (
|
||||
self.share_instance_id, el_id),
|
||||
version="2.87", use_admin_context=False)
|
||||
|
||||
self.assertRaises(exc.HTTPForbidden,
|
||||
self.controller._validate_metadata_for_update,
|
||||
req, el_id, metadata)
|
||||
self.mock_policy_check.assert_called_once_with(
|
||||
req.environ['manila.context'], 'share_export_location',
|
||||
'update_admin_only_metadata')
|
||||
|
||||
def test_create_metadata(self):
|
||||
index_result = self.controller.index(self.req, self.share['id'])
|
||||
el_id = index_result['export_locations'][0]['id']
|
||||
body = {'metadata': {'key1': 'val1', 'key2': 'val2'}}
|
||||
mock_validate = self.mock_object(
|
||||
self.controller, '_validate_metadata_for_update',
|
||||
mock.Mock(return_value=body['metadata']))
|
||||
mock_create = self.mock_object(
|
||||
self.controller, '_create_metadata',
|
||||
mock.Mock(return_value=body))
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v2/shares/%s/export_locations/%s/metadata' % (
|
||||
self.share_instance_id, el_id),
|
||||
version="2.87", use_admin_context=True)
|
||||
|
||||
res = self.controller.create_metadata(req, self.share['id'], el_id,
|
||||
body)
|
||||
self.assertEqual(body, res)
|
||||
mock_validate.assert_called_once_with(req, el_id, body['metadata'],
|
||||
delete=False)
|
||||
mock_create.assert_called_once_with(req, el_id, body)
|
||||
|
||||
def test_update_all_metadata(self):
|
||||
index_result = self.controller.index(self.req, self.share['id'])
|
||||
el_id = index_result['export_locations'][0]['id']
|
||||
body = {'metadata': {'key1': 'val1', 'key2': 'val2'}}
|
||||
mock_validate = self.mock_object(
|
||||
self.controller, '_validate_metadata_for_update',
|
||||
mock.Mock(return_value=body['metadata']))
|
||||
mock_update = self.mock_object(
|
||||
self.controller, '_update_all_metadata',
|
||||
mock.Mock(return_value=body))
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v2/shares/%s/export_locations/%s/metadata' % (
|
||||
self.share_instance_id, el_id),
|
||||
version="2.87", use_admin_context=True)
|
||||
|
||||
res = self.controller.update_all_metadata(req, self.share['id'], el_id,
|
||||
body)
|
||||
self.assertEqual(body, res)
|
||||
mock_validate.assert_called_once_with(req, el_id, body['metadata'])
|
||||
mock_update.assert_called_once_with(req, el_id, body)
|
||||
|
||||
def test_delete_metadata(self):
|
||||
index_result = self.controller.index(self.req, self.share['id'])
|
||||
el_id = index_result['export_locations'][0]['id']
|
||||
mock_delete = self.mock_object(
|
||||
self.controller, '_delete_metadata', mock.Mock())
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v2/shares/%s/export_locations/%s/metadata/fake_key' % (
|
||||
self.share_instance_id, el_id),
|
||||
version="2.87", use_admin_context=True)
|
||||
self.controller.delete_metadata(req, self.share['id'], el_id,
|
||||
'fake_key')
|
||||
mock_delete.assert_called_once_with(req, el_id, 'fake_key')
|
||||
|
@ -2481,7 +2481,6 @@ class ShareInstanceExportLocationsMetadataDatabaseAPITestCase(test.TestCase):
|
||||
self.assertEqual({}, result)
|
||||
|
||||
def test_export_location_metadata_update_get(self):
|
||||
|
||||
# Write metadata for target export location
|
||||
export_location_uuid = self._get_export_location_uuid_by_path(
|
||||
self.initial_locations[0])
|
||||
@ -2514,6 +2513,29 @@ class ShareInstanceExportLocationsMetadataDatabaseAPITestCase(test.TestCase):
|
||||
|
||||
self.assertEqual(updated_metadata, result)
|
||||
|
||||
def test_export_location_metadata_get_item(self):
|
||||
export_location_uuid = self._get_export_location_uuid_by_path(
|
||||
self.initial_locations[0])
|
||||
metadata = {'foo_key': 'foo_value', 'bar_key': 'bar_value'}
|
||||
db_api.export_location_metadata_update(
|
||||
self.ctxt, export_location_uuid, metadata, False)
|
||||
result = db_api.export_location_metadata_get_item(
|
||||
self.ctxt, export_location_uuid, 'foo_key')
|
||||
self.assertEqual(
|
||||
{'foo_key': 'foo_value'}, result)
|
||||
|
||||
def test_export_location_metadata_get_item_invalid(self):
|
||||
export_location_uuid = self._get_export_location_uuid_by_path(
|
||||
self.initial_locations[0])
|
||||
metadata = {'foo_key': 'foo_value', 'bar_key': 'bar_value'}
|
||||
db_api.export_location_metadata_update(
|
||||
self.ctxt, export_location_uuid, metadata, False)
|
||||
self.assertRaises(exception.MetadataItemNotFound,
|
||||
db_api.export_location_metadata_get_item,
|
||||
self.ctxt,
|
||||
export_location_uuid,
|
||||
'foo')
|
||||
|
||||
@ddt.data(
|
||||
("k", "v"),
|
||||
("k" * 256, "v"),
|
||||
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Added share export location metadata capabilities including
|
||||
create, update all, update single, show and delete metadata.
|
||||
Allows configuration of `admin_only_el_metadata`,
|
||||
such that keys in this list are able to be manipulated only by
|
||||
those with admin privileges. By default, this includes
|
||||
"preferred" key.
|
Loading…
Reference in New Issue
Block a user