Metadata for Share Export Location

This patch adds support for Share Export Locations in the
Metadata Controller.

Co-Authored-By: Ashley Rodriguez <ashrod98@gmail.com>
Partially-implements: bp metadata-for-share-resources
Change-Id: Icf096a5cbc650f02eca68d714c876eb854499b9b
This commit is contained in:
Clifford 2023-07-03 22:51:02 +00:00 committed by ashrod98
parent d643c2601d
commit 2016062027
15 changed files with 471 additions and 29 deletions

View File

@ -204,13 +204,14 @@ REST_API_VERSION_HISTORY = """
* 2.84 - Added mount_point_name to shares. * 2.84 - Added mount_point_name to shares.
* 2.85 - Added backup_type field to share backups. * 2.85 - Added backup_type field to share backups.
* 2.86 - Add ensure share API. * 2.86 - Add ensure share API.
* 2.87 - Added Share export location metadata API
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
# The default api version request is defined to be the # The default api version request is defined to be the
# minimum version of the API supported. # minimum version of the API supported.
_MIN_API_VERSION = "2.0" _MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.86" _MAX_API_VERSION = "2.87"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -462,3 +462,8 @@ user documentation.
2.86 2.86
---- ----
Added ensure shares API. Added ensure shares API.
2.87
----
Added Metadata API methods (GET, PUT, POST, DELETE)
to Share Export Locations.

View File

@ -28,36 +28,42 @@ class MetadataController(object):
"share": "share_get", "share": "share_get",
"share_snapshot": "share_snapshot_get", "share_snapshot": "share_snapshot_get",
"share_network_subnet": "share_network_subnet_get", "share_network_subnet": "share_network_subnet_get",
"share_export_location": "export_location_get_by_uuid",
} }
resource_metadata_get = { resource_metadata_get = {
"share": "share_metadata_get", "share": "share_metadata_get",
"share_snapshot": "share_snapshot_metadata_get", "share_snapshot": "share_snapshot_metadata_get",
"share_network_subnet": "share_network_subnet_metadata_get", "share_network_subnet": "share_network_subnet_metadata_get",
"share_export_location": "export_location_metadata_get",
} }
resource_metadata_get_item = { resource_metadata_get_item = {
"share": "share_metadata_get_item", "share": "share_metadata_get_item",
"share_snapshot": "share_snapshot_metadata_get_item", "share_snapshot": "share_snapshot_metadata_get_item",
"share_network_subnet": "share_network_subnet_metadata_get_item", "share_network_subnet": "share_network_subnet_metadata_get_item",
"share_export_location": "export_location_metadata_get_item",
} }
resource_metadata_update = { resource_metadata_update = {
"share": "share_metadata_update", "share": "share_metadata_update",
"share_snapshot": "share_snapshot_metadata_update", "share_snapshot": "share_snapshot_metadata_update",
"share_network_subnet": "share_network_subnet_metadata_update", "share_network_subnet": "share_network_subnet_metadata_update",
"share_export_location": "export_location_metadata_update",
} }
resource_metadata_update_item = { resource_metadata_update_item = {
"share": "share_metadata_update_item", "share": "share_metadata_update_item",
"share_snapshot": "share_snapshot_metadata_update_item", "share_snapshot": "share_snapshot_metadata_update_item",
"share_network_subnet": "share_network_subnet_metadata_update_item", "share_network_subnet": "share_network_subnet_metadata_update_item",
"share_export_location": "export_location_metadata_update_item",
} }
resource_metadata_delete = { resource_metadata_delete = {
"share": "share_metadata_delete", "share": "share_metadata_delete",
"share_snapshot": "share_snapshot_metadata_delete", "share_snapshot": "share_snapshot_metadata_delete",
"share_network_subnet": "share_network_subnet_metadata_delete", "share_network_subnet": "share_network_subnet_metadata_delete",
"share_export_location": "export_location_metadata_delete",
} }
resource_policy_get = { resource_policy_get = {
@ -72,10 +78,13 @@ class MetadataController(object):
def _get_resource(self, context, resource_id, def _get_resource(self, context, resource_id,
for_modification=False, parent_id=None): for_modification=False, parent_id=None):
if self.resource_name in ['share', 'share_network_subnet']: if self.resource_name in ['share', 'share_network_subnet',
# we would allow retrieving some "public" resources 'share_export_location']:
# across project namespaces except share snapshots, # some resources don't have a "project_id" field (like
# project_only=True is hard coded # 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 = {} kwargs = {}
else: else:
kwargs = {'project_only': True} kwargs = {'project_only': True}
@ -86,7 +95,9 @@ class MetadataController(object):
kwargs["parent_id"] = parent_id kwargs["parent_id"] = parent_id
res = get_res_method(context, resource_id, **kwargs) res = get_res_method(context, resource_id, **kwargs)
if self.resource_name not in ["share_export_location"]:
get_policy = self.resource_policy_get[self.resource_name] get_policy = self.resource_policy_get[self.resource_name]
# skip policy check for export locations
if res.get('is_public') is False: if res.get('is_public') is False:
authorized = policy.check_policy(context, authorized = policy.check_policy(context,
self.resource_name, self.resource_name,
@ -120,6 +131,7 @@ class MetadataController(object):
@wsgi.response(200) @wsgi.response(200)
def _index_metadata(self, req, resource_id, parent_id=None): def _index_metadata(self, req, resource_id, parent_id=None):
"""Lists existing metadata."""
context = req.environ['manila.context'] context = req.environ['manila.context']
metadata = self._get_metadata(context, resource_id, metadata = self._get_metadata(context, resource_id,
parent_id=parent_id) parent_id=parent_id)

View File

@ -259,6 +259,45 @@ class APIRouter(manila.api.openstack.APIRouter):
controller=self.resources["share_export_locations"], controller=self.resources["share_export_locations"],
action="show", action="show",
conditions={"method": ["GET"]}) 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() self.resources["snapshots"] = share_snapshots.create_resource()
mapper.resource("snapshot", "snapshots", mapper.resource("snapshot", "snapshots",

View File

@ -13,23 +13,33 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from oslo_config import cfg
from oslo_log import log
from webob import exc from webob import exc
from manila.api.openstack import wsgi 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.api.views import export_locations as export_locations_views
from manila.db import api as db_api from manila.db import api as db_api
from manila import exception from manila import exception
from manila.i18n import _ from manila.i18n import _
from manila import policy 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.""" """The Share Export Locations API controller."""
def __init__(self): def __init__(self):
self._view_builder_class = export_locations_views.ViewBuilder self._view_builder_class = export_locations_views.ViewBuilder
self.resource_name = 'share_export_location' self.resource_name = 'share_export_location'
super(ShareExportLocationController, self).__init__() super(ShareExportLocationController, self).__init__()
self._conf_admin_only_metadata_keys = getattr(
CONF, 'admin_only_el_metadata', []
)
def _verify_share(self, context, share_id): def _verify_share(self, context, share_id):
try: try:
@ -94,6 +104,96 @@ class ShareExportLocationController(wsgi.Controller):
return self._show(req, share_id, export_location_uuid, return self._show(req, share_id, export_location_uuid,
ignore_secondary_replicas=True) 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(): def create_resource():
return wsgi.Resource(ShareExportLocationController()) return wsgi.Resource(ShareExportLocationController())

View File

@ -627,9 +627,9 @@ class ShareController(wsgi.Controller,
def _validate_metadata_for_update(self, req, share_id, metadata, def _validate_metadata_for_update(self, req, share_id, metadata,
delete=True): 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'] context = req.environ['manila.context']
if set(metadata).intersection(admin_metadata_ignore_keys): if set(metadata).intersection(persistent_keys):
try: try:
policy.check_policy( policy.check_policy(
context, 'share', 'update_admin_only_metadata') context, 'share', 'update_admin_only_metadata')
@ -637,17 +637,17 @@ class ShareController(wsgi.Controller,
msg = _("Cannot set or update admin only metadata.") msg = _("Cannot set or update admin only metadata.")
LOG.exception(msg) LOG.exception(msg)
raise exc.HTTPForbidden(explanation=msg) raise exc.HTTPForbidden(explanation=msg)
admin_metadata_ignore_keys = [] persistent_keys = []
current_share_metadata = db.share_metadata_get(context, share_id) current_share_metadata = db.share_metadata_get(context, share_id)
if delete: if delete:
_metadata = metadata _metadata = metadata
for key in admin_metadata_ignore_keys: for key in persistent_keys:
if key in current_share_metadata: if key in current_share_metadata:
_metadata[key] = current_share_metadata[key] _metadata[key] = current_share_metadata[key]
else: else:
metadata_copy = metadata.copy() metadata_copy = metadata.copy()
for key in admin_metadata_ignore_keys: for key in persistent_keys:
metadata_copy.pop(key, None) metadata_copy.pop(key, None)
_metadata = current_share_metadata.copy() _metadata = current_share_metadata.copy()
_metadata.update(metadata_copy) _metadata.update(metadata_copy)

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
from oslo_utils import strutils from oslo_utils import strutils
from manila.api import common from manila.api import common
@ -25,6 +27,7 @@ class ViewBuilder(common.ViewBuilder):
_detail_version_modifiers = [ _detail_version_modifiers = [
'add_preferred_path_attribute', 'add_preferred_path_attribute',
'add_metadata_attribute',
] ]
def _get_export_location_view(self, request, export_location, def _get_export_location_view(self, request, export_location,
@ -87,3 +90,11 @@ class ViewBuilder(common.ViewBuilder):
export_location): export_location):
view_dict['preferred'] = strutils.bool_from_string( view_dict['preferred'] = strutils.bool_from_string(
export_location['el_metadata'].get('preferred')) 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

View File

@ -152,6 +152,10 @@ global_opts = [
help='Whether Manila should update the status of all shares ' help='Whether Manila should update the status of all shares '
'within a backend during ongoing ensure_shares ' 'within a backend during ongoing ensure_shares '
'run.'), '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) CONF.register_opts(global_opts)

View File

@ -362,8 +362,13 @@ class ExtraSpecs(object):
class AdminOnlyMetadata(object): class AdminOnlyMetadata(object):
AFFINITY_KEY = "__affinity_same_host" AFFINITY_KEY = "__affinity_same_host"
ANTI_AFFINITY_KEY = "__affinity_different_host" ANTI_AFFINITY_KEY = "__affinity_different_host"
PREFERRED_KEY = "preferred"
SCHEDULER_FILTERS = [ SCHEDULER_FILTERS = [
AFFINITY_KEY, AFFINITY_KEY,
ANTI_AFFINITY_KEY, ANTI_AFFINITY_KEY,
] ]
EXPORT_LOCATION_KEYS = [
PREFERRED_KEY,
]

View File

@ -1004,6 +1004,13 @@ def export_location_metadata_get(context, export_location_uuid):
return IMPL.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): def export_location_metadata_delete(context, export_location_uuid, keys):
"""Delete metadata of an export location.""" """Delete metadata of an export location."""
return IMPL.export_location_metadata_delete( 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( return IMPL.export_location_metadata_update(
context, export_location_uuid, metadata, delete) 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)
#################### ####################

View File

@ -4698,6 +4698,33 @@ def _export_location_metadata_update(
return metadata 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
################################### ###################################

View File

@ -34,6 +34,30 @@ deprecated_export_location_show = policy.DeprecatedRule(
deprecated_reason=DEPRECATED_REASON, deprecated_reason=DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.WALLABY 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 = [ share_export_location_policies = [
@ -64,6 +88,79 @@ share_export_location_policies = [
], ],
deprecated_rule=deprecated_export_location_show 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
),
] ]

View File

@ -249,3 +249,98 @@ class ShareExportLocationsAPITest(test.TestCase):
self.share['id'], self.share['id'],
index_result['export_locations'][0]['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')

View File

@ -2481,7 +2481,6 @@ class ShareInstanceExportLocationsMetadataDatabaseAPITestCase(test.TestCase):
self.assertEqual({}, result) self.assertEqual({}, result)
def test_export_location_metadata_update_get(self): def test_export_location_metadata_update_get(self):
# Write metadata for target export location # Write metadata for target export location
export_location_uuid = self._get_export_location_uuid_by_path( export_location_uuid = self._get_export_location_uuid_by_path(
self.initial_locations[0]) self.initial_locations[0])
@ -2514,6 +2513,29 @@ class ShareInstanceExportLocationsMetadataDatabaseAPITestCase(test.TestCase):
self.assertEqual(updated_metadata, result) 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( @ddt.data(
("k", "v"), ("k", "v"),
("k" * 256, "v"), ("k" * 256, "v"),

View File

@ -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.