diff --git a/manila/api/openstack/api_version_request.py b/manila/api/openstack/api_version_request.py index 716889ab16..471bf71ee4 100644 --- a/manila/api/openstack/api_version_request.py +++ b/manila/api/openstack/api_version_request.py @@ -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 diff --git a/manila/api/openstack/rest_api_version_history.rst b/manila/api/openstack/rest_api_version_history.rst index eb045c77f0..bcdb0bb7f4 100644 --- a/manila/api/openstack/rest_api_version_history.rst +++ b/manila/api/openstack/rest_api_version_history.rst @@ -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. diff --git a/manila/api/v2/metadata.py b/manila/api/v2/metadata.py index e3ab4253cf..9f1520c003 100644 --- a/manila/api/v2/metadata.py +++ b/manila/api/v2/metadata.py @@ -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) diff --git a/manila/api/v2/router.py b/manila/api/v2/router.py index 5c277055f2..33e7db481f 100644 --- a/manila/api/v2/router.py +++ b/manila/api/v2/router.py @@ -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", diff --git a/manila/api/v2/share_export_locations.py b/manila/api/v2/share_export_locations.py index 1da599749d..4e5fe33438 100644 --- a/manila/api/v2/share_export_locations.py +++ b/manila/api/v2/share_export_locations.py @@ -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()) diff --git a/manila/api/v2/shares.py b/manila/api/v2/shares.py index 1234db5af2..f79bf0d599 100644 --- a/manila/api/v2/shares.py +++ b/manila/api/v2/shares.py @@ -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) diff --git a/manila/api/views/export_locations.py b/manila/api/views/export_locations.py index ea71d031c5..8f2e787b9a 100644 --- a/manila/api/views/export_locations.py +++ b/manila/api/views/export_locations.py @@ -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 diff --git a/manila/common/config.py b/manila/common/config.py index 879a6f9979..1a086e34bf 100644 --- a/manila/common/config.py +++ b/manila/common/config.py @@ -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) diff --git a/manila/common/constants.py b/manila/common/constants.py index f888a471e8..9d285ae41b 100644 --- a/manila/common/constants.py +++ b/manila/common/constants.py @@ -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, + ] diff --git a/manila/db/api.py b/manila/db/api.py index df59f708d9..1356f3e345 100644 --- a/manila/db/api.py +++ b/manila/db/api.py @@ -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) + #################### diff --git a/manila/db/sqlalchemy/api.py b/manila/db/sqlalchemy/api.py index 0b80963b44..e1da68c3e5 100644 --- a/manila/db/sqlalchemy/api.py +++ b/manila/db/sqlalchemy/api.py @@ -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 + ################################### diff --git a/manila/policies/share_export_location.py b/manila/policies/share_export_location.py index 498fa3097b..cc4f4fd730 100644 --- a/manila/policies/share_export_location.py +++ b/manila/policies/share_export_location.py @@ -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 + ), ] diff --git a/manila/tests/api/v2/test_share_export_locations.py b/manila/tests/api/v2/test_share_export_locations.py index e2a3df0682..67111abcd6 100644 --- a/manila/tests/api/v2/test_share_export_locations.py +++ b/manila/tests/api/v2/test_share_export_locations.py @@ -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') diff --git a/manila/tests/db/sqlalchemy/test_api.py b/manila/tests/db/sqlalchemy/test_api.py index 409748a36c..9977f78d4f 100644 --- a/manila/tests/db/sqlalchemy/test_api.py +++ b/manila/tests/db/sqlalchemy/test_api.py @@ -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"), diff --git a/releasenotes/notes/add_export_location_metadata-d3c279b73f4c4728.yaml b/releasenotes/notes/add_export_location_metadata-d3c279b73f4c4728.yaml new file mode 100644 index 0000000000..0921799fa8 --- /dev/null +++ b/releasenotes/notes/add_export_location_metadata-d3c279b73f4c4728.yaml @@ -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.