Metadata for Share Snapshots
Introduce MetadataCapableResource and MetadataCapableManager to abstract away metadata operations for a resource and collections of resources in the manilaclient SDK. Extend these into OSC capabilities where appropriate. Bumps max microversion to 2.73. In this change: 1) Shares 2) Snapshots Depends-On: I91151792d033a4297557cd5f330053d78895eb78 Implements: bp/metadata-for-share-resources Change-Id: I82791614b62b540eb108e99ae8ee5ce62b36b42c
This commit is contained in:
parent
43943fdf67
commit
bbfd7d5468
|
@ -27,7 +27,7 @@ from manilaclient import utils
|
|||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
MAX_VERSION = '2.72'
|
||||
MAX_VERSION = '2.73'
|
||||
MIN_VERSION = '2.0'
|
||||
DEPRECATED_VERSION = '1.0'
|
||||
_VERSIONED_METHOD_MAP = {}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import contextlib
|
||||
import copy
|
||||
import hashlib
|
||||
|
@ -354,3 +355,150 @@ class Resource(object):
|
|||
|
||||
def to_dict(self):
|
||||
return copy.deepcopy(self._info)
|
||||
|
||||
|
||||
class MetadataCapableResource(Resource, metaclass=abc.ABCMeta):
|
||||
|
||||
superresource = None
|
||||
|
||||
def _get_subresource_and_resource(self, superresource):
|
||||
resource = self
|
||||
subresource = None
|
||||
superresource = superresource or self.superresource
|
||||
if superresource is not None:
|
||||
resource = superresource
|
||||
subresource = self
|
||||
return resource, subresource
|
||||
|
||||
def get_metadata(self, superresource=None):
|
||||
"""Get metadata of a resource
|
||||
|
||||
:param superresource: either a parent resource object or text with
|
||||
its ID. Required for sub-resources such as share export
|
||||
locations which do not include a reference to the parent object
|
||||
by default
|
||||
"""
|
||||
|
||||
resource, subresource = self._get_subresource_and_resource(
|
||||
superresource)
|
||||
|
||||
return self.manager.get_metadata(resource, subresource=subresource)
|
||||
|
||||
def set_metadata(self, metadata, superresource=None):
|
||||
"""Set or update metadata for the resource.
|
||||
|
||||
:param metadata: A dictionary of key:value pairs to be set as
|
||||
resource metadata
|
||||
:param superresource: either a parent resource object or text with
|
||||
its ID. Required for sub-resources such as share share export
|
||||
locations which do not include a reference to the parent object
|
||||
by default
|
||||
"""
|
||||
resource, subresource = self._get_subresource_and_resource(
|
||||
superresource)
|
||||
|
||||
return self.manager.set_metadata(resource, metadata,
|
||||
subresource=subresource)
|
||||
|
||||
def delete_metadata(self, keys, superresource=None):
|
||||
"""Delete specified keys from the given resource.
|
||||
|
||||
:param keys: An iterable with keys of metadata items to be deleted
|
||||
:param superresource: either a parent resource object or text with
|
||||
its ID. Required for sub-resources such as share share export
|
||||
locations which do not include a reference to the parent object
|
||||
by default
|
||||
"""
|
||||
resource, subresource = self._get_subresource_and_resource(
|
||||
superresource)
|
||||
|
||||
return self.manager.delete_metadata(resource, keys,
|
||||
subresource=subresource)
|
||||
|
||||
def update_all_metadata(self, metadata, superresource=None):
|
||||
"""Update all metadata for this resource.
|
||||
|
||||
:param metadata: A dictionary of key:value pairs of resource metadata
|
||||
to be updated
|
||||
:param superresource: either a parent resource object or text with
|
||||
its ID. Required for sub-resources such as share share export
|
||||
locations which do not include a reference to the parent object
|
||||
by default
|
||||
"""
|
||||
resource, subresource = self._get_subresource_and_resource(
|
||||
superresource)
|
||||
|
||||
return self.manager.update_all_metadata(resource,
|
||||
metadata,
|
||||
subresource=subresource)
|
||||
|
||||
|
||||
class MetadataCapableManager(ManagerWithFind, metaclass=abc.ABCMeta):
|
||||
"""Provides extended behavior to objects to handle key=value metadata."""
|
||||
|
||||
resource_path = None
|
||||
subresource_path = None
|
||||
|
||||
def get_metadata(self, resource, subresource=None):
|
||||
"""Get metadata of a resource.
|
||||
|
||||
:param resource: either resource object or text with its ID.
|
||||
:param subresource: either a child resource object or text with its ID
|
||||
"""
|
||||
resource = getid(resource)
|
||||
if subresource:
|
||||
subresource = getid(subresource)
|
||||
resource = f"{resource}{self.subresource_path}/{subresource}"
|
||||
|
||||
return self._get(f"{self.resource_path}/{resource}/metadata",
|
||||
"metadata")
|
||||
|
||||
def set_metadata(self, resource, metadata, subresource=None):
|
||||
"""Set or update metadata for resource.
|
||||
|
||||
:param resource: either resource object or text with its ID.
|
||||
:param metadata: A dictionary of key:value pairs to be set as
|
||||
resource metadata
|
||||
:param subresource: either a child resource object or text with its ID
|
||||
"""
|
||||
body = {'metadata': metadata}
|
||||
resource = getid(resource)
|
||||
if subresource:
|
||||
subresource = getid(subresource)
|
||||
resource = f"{resource}{self.subresource_path}/{subresource}"
|
||||
|
||||
return self._create(f"{self.resource_path}/{resource}/metadata",
|
||||
body,
|
||||
"metadata")
|
||||
|
||||
def delete_metadata(self, resource, keys, subresource=None):
|
||||
"""Delete specified keys from resource metadata.
|
||||
|
||||
:param resource: either resource object or text with its ID.
|
||||
:param keys: An iterable with keys of metadata items to be deleted
|
||||
:param subresource: either a child resource object or text with its ID
|
||||
"""
|
||||
resource = getid(resource)
|
||||
if subresource:
|
||||
subresource = getid(subresource)
|
||||
resource = f"{resource}{self.subresource_path}/{subresource}"
|
||||
|
||||
for key in keys:
|
||||
self._delete(f"{self.resource_path}/{resource}/metadata/{key}")
|
||||
|
||||
def update_all_metadata(self, resource, metadata, subresource=None):
|
||||
"""Update all metadata of a resource.
|
||||
|
||||
:param resource: either resource object or text with its ID.
|
||||
:param metadata: A dictionary of key:value pairs of resource metadata
|
||||
to be updated
|
||||
:param subresource: either a child resource object or text with its ID
|
||||
"""
|
||||
body = {'metadata': metadata}
|
||||
resource = getid(resource)
|
||||
if subresource:
|
||||
subresource = getid(subresource)
|
||||
resource = f"{resource}{self.subresource_path}/{subresource}"
|
||||
|
||||
return self._update(f"{self.resource_path}/{resource}/metadata",
|
||||
body)
|
||||
|
|
|
@ -765,8 +765,7 @@ class SetShare(command.Command):
|
|||
|
||||
if parsed_args.property:
|
||||
try:
|
||||
share_client.shares.set_metadata(
|
||||
share_obj.id, parsed_args.property)
|
||||
share_obj.set_metadata(parsed_args.property)
|
||||
except Exception as e:
|
||||
LOG.error(_("Failed to set share properties "
|
||||
"'%(properties)s': %(exception)s"),
|
||||
|
@ -860,8 +859,7 @@ class UnsetShare(command.Command):
|
|||
if parsed_args.property:
|
||||
for key in parsed_args.property:
|
||||
try:
|
||||
share_client.shares.delete_metadata(
|
||||
share_obj.id, [key])
|
||||
share_obj.delete_metadata([key])
|
||||
except Exception as e:
|
||||
LOG.error(_("Failed to unset share property "
|
||||
"'%(key)s': %(e)s"),
|
||||
|
@ -1218,9 +1216,9 @@ class ShowShareProperties(command.ShowOne):
|
|||
|
||||
def take_action(self, parsed_args):
|
||||
share_client = self.app.client_manager.share
|
||||
share = apiutils.find_resource(
|
||||
share_obj = apiutils.find_resource(
|
||||
share_client.shares, parsed_args.share)
|
||||
share_properties = share_client.shares.get_metadata(share)
|
||||
share_properties = share_client.shares.get_metadata(share_obj)
|
||||
|
||||
return self.dict2columns(share_properties._info)
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
import logging
|
||||
|
||||
from osc_lib.cli import format_columns
|
||||
from osc_lib.cli import parseractions
|
||||
from osc_lib.command import command
|
||||
from osc_lib import exceptions
|
||||
|
@ -20,6 +21,7 @@ from osc_lib import utils
|
|||
from manilaclient import api_versions
|
||||
from manilaclient.common._i18n import _
|
||||
from manilaclient.common import cliutils
|
||||
from manilaclient.osc import utils as oscutils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -61,6 +63,15 @@ class CreateShareSnapshot(command.ShowOne):
|
|||
default=False,
|
||||
help=_('Wait for share snapshot creation')
|
||||
)
|
||||
parser.add_argument(
|
||||
"--property",
|
||||
metavar="<key=value>",
|
||||
default={},
|
||||
action=parseractions.KeyValueAction,
|
||||
help=_("Set a property to this snapshot "
|
||||
"(repeat option to set multiple properties)."
|
||||
"Available only for microversion >= 2.73"),
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
|
@ -69,11 +80,19 @@ class CreateShareSnapshot(command.ShowOne):
|
|||
share = utils.find_resource(share_client.shares,
|
||||
parsed_args.share)
|
||||
|
||||
if share_client.api_version >= api_versions.APIVersion("2.73"):
|
||||
property = parsed_args.property or {}
|
||||
elif parsed_args.property:
|
||||
raise exceptions.CommandError(
|
||||
"Setting metadtaa is only available with manila API version "
|
||||
">= 2.73")
|
||||
|
||||
share_snapshot = share_client.share_snapshots.create(
|
||||
share=share,
|
||||
force=parsed_args.force,
|
||||
name=parsed_args.name or None,
|
||||
description=parsed_args.description or None
|
||||
description=parsed_args.description or None,
|
||||
metadata=property
|
||||
)
|
||||
if parsed_args.wait:
|
||||
if not utils.wait_for_status(
|
||||
|
@ -188,6 +207,14 @@ class ShowShareSnapshot(command.ShowOne):
|
|||
|
||||
data = share_snapshot._info
|
||||
data['export_locations'] = locations
|
||||
# Special mapping for columns to make the output easier to read:
|
||||
# 'metadata' --> 'properties'
|
||||
data.update(
|
||||
{
|
||||
'properties':
|
||||
format_columns.DictColumn(data.pop('metadata', {})),
|
||||
},
|
||||
)
|
||||
data.pop('links', None)
|
||||
|
||||
return self.dict2columns(data)
|
||||
|
@ -228,6 +255,14 @@ class SetShareSnapshot(command.Command):
|
|||
"deleting, manage_starting, manage_error, "
|
||||
"unmanage_starting, unmanage_error, error_deleting.")
|
||||
)
|
||||
parser.add_argument(
|
||||
"--property",
|
||||
metavar="<key=value>",
|
||||
default={},
|
||||
action=parseractions.KeyValueAction,
|
||||
help=_("Set a property to this snapshot "
|
||||
"(repeat option to set multiple properties)"),
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
|
@ -270,7 +305,15 @@ class SetShareSnapshot(command.Command):
|
|||
"Failed to update snapshot status to "
|
||||
"'%(status)s': %(e)s"),
|
||||
{'status': parsed_args.status, 'e': e})
|
||||
|
||||
if parsed_args.property:
|
||||
try:
|
||||
share_snapshot.set_metadata(parsed_args.property)
|
||||
except Exception as e:
|
||||
LOG.error(_("Failed to set share snapshot properties "
|
||||
"'%(properties)s': %(exception)s"),
|
||||
{'properties': parsed_args.property,
|
||||
'exception': e})
|
||||
result += 1
|
||||
if result > 0:
|
||||
raise exceptions.CommandError(_("One or more of the "
|
||||
"set operations failed"))
|
||||
|
@ -297,6 +340,13 @@ class UnsetShareSnapshot(command.Command):
|
|||
action='store_true',
|
||||
help=_("Unset snapshot description."),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--property',
|
||||
metavar='<key>',
|
||||
action='append',
|
||||
help=_('Remove a property from snapshot '
|
||||
'(repeat option to remove multiple properties)'),
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
|
@ -321,6 +371,15 @@ class UnsetShareSnapshot(command.Command):
|
|||
raise exceptions.CommandError(_(
|
||||
"Failed to unset snapshot display name "
|
||||
"or display description : %s" % e))
|
||||
if parsed_args.property:
|
||||
for key in parsed_args.property:
|
||||
try:
|
||||
share_snapshot.delete_metadata([key])
|
||||
except Exception as e:
|
||||
raise exceptions.CommandError(_(
|
||||
"Failed to unset snapshot property "
|
||||
"'%(key)s': %(e)s"),
|
||||
{'key': key, 'e': e})
|
||||
|
||||
|
||||
class ListShareSnapshot(command.Lister):
|
||||
|
@ -408,6 +467,14 @@ class ListShareSnapshot(command.Lister):
|
|||
default=False,
|
||||
help=_("List share snapshots with details")
|
||||
)
|
||||
parser.add_argument(
|
||||
'--property',
|
||||
metavar='<key=value>',
|
||||
action=parseractions.KeyValueAction,
|
||||
help=_('Filter snapshots having a given metadata key=value '
|
||||
'property. (repeat option to filter by multiple '
|
||||
'properties)'),
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
|
@ -427,6 +494,8 @@ class ListShareSnapshot(command.Lister):
|
|||
'status': parsed_args.status,
|
||||
'share_id': share_id,
|
||||
'usage': parsed_args.usage,
|
||||
'metadata': oscutils.extract_key_value_options(
|
||||
parsed_args.property),
|
||||
}
|
||||
|
||||
if share_client.api_version >= api_versions.APIVersion("2.36"):
|
||||
|
|
|
@ -320,7 +320,7 @@ class BaseTestCase(base.ClientTestBase):
|
|||
'description': description,
|
||||
'public': public,
|
||||
'snapshot': snapshot,
|
||||
'metadata': metadata,
|
||||
'metadata': metadata or {},
|
||||
'microversion': microversion,
|
||||
'wait': use_wait_option,
|
||||
}
|
||||
|
|
|
@ -1112,7 +1112,8 @@ class TestShareSet(TestShare):
|
|||
super(TestShareSet, self).setUp()
|
||||
|
||||
self._share = manila_fakes.FakeShare.create_one_share(
|
||||
methods={"reset_state": None, "reset_task_state": None}
|
||||
methods={"reset_state": None, "reset_task_state": None,
|
||||
"set_metadata": None}
|
||||
)
|
||||
self.shares_mock.get.return_value = self._share
|
||||
|
||||
|
@ -1132,8 +1133,7 @@ class TestShareSet(TestShare):
|
|||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.shares_mock.set_metadata.assert_called_with(
|
||||
self._share.id,
|
||||
self._share.set_metadata.assert_called_with(
|
||||
{'Zorilla': 'manila'})
|
||||
|
||||
def test_share_set_name(self):
|
||||
|
@ -1224,13 +1224,12 @@ class TestShareSet(TestShare):
|
|||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.shares_mock.set_metadata.assert_called_with(
|
||||
self._share.id,
|
||||
self._share.set_metadata.assert_called_with(
|
||||
{'key': ''})
|
||||
|
||||
# '--property' takes key=value arguments
|
||||
# missing a value would raise a BadRequest
|
||||
self.shares_mock.set_metadata.side_effect = exceptions.BadRequest()
|
||||
self._share.set_metadata.side_effect = exceptions.BadRequest
|
||||
self.assertRaises(
|
||||
osc_exceptions.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
|
@ -1290,7 +1289,9 @@ class TestShareUnset(TestShare):
|
|||
def setUp(self):
|
||||
super(TestShareUnset, self).setUp()
|
||||
|
||||
self._share = manila_fakes.FakeShare.create_one_share()
|
||||
self._share = manila_fakes.FakeShare.create_one_share(
|
||||
methods={"delete_metadata": None}
|
||||
)
|
||||
self.shares_mock.get.return_value = self._share
|
||||
|
||||
# Get the command objects to test
|
||||
|
@ -1309,8 +1310,7 @@ class TestShareUnset(TestShare):
|
|||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.shares_mock.delete_metadata.assert_called_with(
|
||||
self._share.id,
|
||||
self._share.delete_metadata.assert_called_with(
|
||||
parsed_args.property)
|
||||
|
||||
def test_share_unset_name(self):
|
||||
|
@ -1376,12 +1376,11 @@ class TestShareUnset(TestShare):
|
|||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.shares_mock.delete_metadata.assert_called_with(
|
||||
self._share.id,
|
||||
self._share.delete_metadata.assert_called_with(
|
||||
parsed_args.property)
|
||||
|
||||
# 404 Not Found would be raised, if property 'Manila' doesn't exist
|
||||
self.shares_mock.delete_metadata.side_effect = exceptions.NotFound()
|
||||
self._share.delete_metadata.side_effect = exceptions.NotFound
|
||||
self.assertRaises(
|
||||
osc_exceptions.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
|
@ -1869,7 +1868,8 @@ class TestShowShareProperties(TestShare):
|
|||
attrs={
|
||||
'metadata': osc_fakes.FakeResource(
|
||||
info=self.properties)
|
||||
}
|
||||
},
|
||||
methods={'get_metadata': None}
|
||||
)
|
||||
self.shares_mock.get.return_value = self._share
|
||||
self.shares_mock.get_metadata.return_value = self._share.metadata
|
||||
|
|
|
@ -59,6 +59,9 @@ class TestShareSnapshot(manila_fakes.TestShare):
|
|||
self.app.client_manager.share.share_snapshot_export_locations)
|
||||
self.export_locations_mock.reset_mock()
|
||||
|
||||
self.app.client_manager.share.api_version = api_versions.APIVersion(
|
||||
api_versions.MAX_VERSION)
|
||||
|
||||
|
||||
class TestShareSnapshotCreate(TestShareSnapshot):
|
||||
|
||||
|
@ -105,7 +108,8 @@ class TestShareSnapshotCreate(TestShareSnapshot):
|
|||
share=self.share,
|
||||
force=False,
|
||||
name=None,
|
||||
description=None
|
||||
description=None,
|
||||
metadata={}
|
||||
)
|
||||
|
||||
self.assertCountEqual(self.columns, columns)
|
||||
|
@ -129,7 +133,8 @@ class TestShareSnapshotCreate(TestShareSnapshot):
|
|||
share=self.share,
|
||||
force=True,
|
||||
name=None,
|
||||
description=None
|
||||
description=None,
|
||||
metadata={}
|
||||
)
|
||||
|
||||
self.assertCountEqual(columns, columns)
|
||||
|
@ -155,7 +160,38 @@ class TestShareSnapshotCreate(TestShareSnapshot):
|
|||
share=self.share,
|
||||
force=False,
|
||||
name=self.share_snapshot.name,
|
||||
description=self.share_snapshot.description
|
||||
description=self.share_snapshot.description,
|
||||
metadata={}
|
||||
)
|
||||
|
||||
self.assertCountEqual(self.columns, columns)
|
||||
self.assertCountEqual(self.data, data)
|
||||
|
||||
def test_share_snapshot_create_metadata(self):
|
||||
arglist = [
|
||||
self.share.id,
|
||||
'--name', self.share_snapshot.name,
|
||||
'--description', self.share_snapshot.description,
|
||||
'--property', 'Manila=zorilla',
|
||||
'--property', 'Zorilla=manila'
|
||||
]
|
||||
verifylist = [
|
||||
('share', self.share.id),
|
||||
('name', self.share_snapshot.name),
|
||||
('description', self.share_snapshot.description),
|
||||
('property', {'Manila': 'zorilla', 'Zorilla': 'manila'}),
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.snapshots_mock.create.assert_called_with(
|
||||
share=self.share,
|
||||
force=False,
|
||||
name=self.share_snapshot.name,
|
||||
description=self.share_snapshot.description,
|
||||
metadata={'Manila': 'zorilla', 'Zorilla': 'manila'},
|
||||
)
|
||||
|
||||
self.assertCountEqual(self.columns, columns)
|
||||
|
@ -179,7 +215,8 @@ class TestShareSnapshotCreate(TestShareSnapshot):
|
|||
share=self.share,
|
||||
force=False,
|
||||
name=None,
|
||||
description=None
|
||||
description=None,
|
||||
metadata={}
|
||||
)
|
||||
|
||||
self.snapshots_mock.get.assert_called_with(
|
||||
|
@ -207,7 +244,8 @@ class TestShareSnapshotCreate(TestShareSnapshot):
|
|||
share=self.share,
|
||||
force=False,
|
||||
name=None,
|
||||
description=None
|
||||
description=None,
|
||||
metadata={}
|
||||
)
|
||||
|
||||
mock_logger.error.assert_called_with(
|
||||
|
@ -396,7 +434,9 @@ class TestShareSnapshotSet(TestShareSnapshot):
|
|||
super(TestShareSnapshotSet, self).setUp()
|
||||
|
||||
self.share_snapshot = (
|
||||
manila_fakes.FakeShareSnapshot.create_one_snapshot())
|
||||
manila_fakes.FakeShareSnapshot.create_one_snapshot(
|
||||
methods={"set_metadata": None}
|
||||
))
|
||||
|
||||
self.snapshots_mock.get.return_value = self.share_snapshot
|
||||
|
||||
|
@ -458,6 +498,22 @@ class TestShareSnapshotSet(TestShareSnapshot):
|
|||
parsed_args.status)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_set_snapshot_property(self):
|
||||
arglist = [
|
||||
self.share_snapshot.id,
|
||||
'--property', 'Zorilla=manila',
|
||||
]
|
||||
verifylist = [
|
||||
('snapshot', self.share_snapshot.id),
|
||||
('property', {'Zorilla': 'manila'}),
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.share_snapshot.set_metadata.assert_called_with(
|
||||
{'Zorilla': 'manila'})
|
||||
|
||||
def test_set_snapshot_update_exception(self):
|
||||
snapshot_name = 'snapshot-name-' + uuid.uuid4().hex
|
||||
arglist = [
|
||||
|
@ -495,6 +551,29 @@ class TestShareSnapshotSet(TestShareSnapshot):
|
|||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
|
||||
def test_set_snapshot_property_exception(self):
|
||||
arglist = [
|
||||
'--property', 'key=',
|
||||
self.share_snapshot.id,
|
||||
]
|
||||
verifylist = [
|
||||
('property', {'key': ''}),
|
||||
('snapshot', self.share_snapshot.id)
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.share_snapshot.set_metadata.assert_called_with(
|
||||
{'key': ''})
|
||||
|
||||
# '--property' takes key=value arguments
|
||||
# missing a value would raise a BadRequest
|
||||
self.share_snapshot.set_metadata.side_effect = exceptions.BadRequest
|
||||
self.assertRaises(
|
||||
exceptions.CommandError, self.cmd.take_action,
|
||||
parsed_args)
|
||||
|
||||
|
||||
class TestShareSnapshotUnset(TestShareSnapshot):
|
||||
|
||||
|
@ -502,7 +581,9 @@ class TestShareSnapshotUnset(TestShareSnapshot):
|
|||
super(TestShareSnapshotUnset, self).setUp()
|
||||
|
||||
self.share_snapshot = (
|
||||
manila_fakes.FakeShareSnapshot.create_one_snapshot())
|
||||
manila_fakes.FakeShareSnapshot.create_one_snapshot(
|
||||
methods={"delete_metadata": None}
|
||||
))
|
||||
|
||||
self.snapshots_mock.get.return_value = self.share_snapshot
|
||||
|
||||
|
@ -544,6 +625,22 @@ class TestShareSnapshotUnset(TestShareSnapshot):
|
|||
display_description=None)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_unset_snapshot_property(self):
|
||||
arglist = [
|
||||
'--property', 'Manila',
|
||||
self.share_snapshot.id,
|
||||
]
|
||||
verifylist = [
|
||||
('property', ['Manila']),
|
||||
('snapshot', self.share_snapshot.id)
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.share_snapshot.delete_metadata.assert_called_with(
|
||||
parsed_args.property)
|
||||
|
||||
def test_unset_snapshot_name_exception(self):
|
||||
arglist = [
|
||||
self.share_snapshot.id,
|
||||
|
@ -562,6 +659,27 @@ class TestShareSnapshotUnset(TestShareSnapshot):
|
|||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
|
||||
def test_unset_snapshot_property_exception(self):
|
||||
arglist = [
|
||||
'--property', 'Manila',
|
||||
self.share_snapshot.id,
|
||||
]
|
||||
verifylist = [
|
||||
('property', ['Manila']),
|
||||
('snapshot', self.share_snapshot.id)
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.share_snapshot.delete_metadata.assert_called_with(
|
||||
parsed_args.property)
|
||||
|
||||
# 404 Not Found would be raised, if property 'Manila' doesn't exist
|
||||
self.share_snapshot.delete_metadata.side_effect = exceptions.NotFound
|
||||
self.assertRaises(
|
||||
exceptions.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
|
||||
class TestShareSnapshotList(TestShareSnapshot):
|
||||
|
||||
|
@ -600,9 +718,10 @@ class TestShareSnapshotList(TestShareSnapshot):
|
|||
'status': None,
|
||||
'share_id': None,
|
||||
'usage': None,
|
||||
'metadata': {},
|
||||
'name~': None,
|
||||
'description~': None,
|
||||
'description': None
|
||||
'description': None,
|
||||
})
|
||||
|
||||
self.assertEqual(COLUMNS, columns)
|
||||
|
@ -635,9 +754,10 @@ class TestShareSnapshotList(TestShareSnapshot):
|
|||
'status': None,
|
||||
'share_id': None,
|
||||
'usage': None,
|
||||
'metadata': {},
|
||||
'name~': None,
|
||||
'description~': None,
|
||||
'description': None
|
||||
'description': None,
|
||||
})
|
||||
|
||||
self.assertEqual(all_tenants_list, columns)
|
||||
|
@ -668,6 +788,7 @@ class TestShareSnapshotList(TestShareSnapshot):
|
|||
'status': None,
|
||||
'share_id': None,
|
||||
'usage': None,
|
||||
'metadata': {},
|
||||
'name~': None,
|
||||
'description~': None,
|
||||
'description': None
|
||||
|
@ -724,6 +845,7 @@ class TestShareSnapshotList(TestShareSnapshot):
|
|||
'status': None,
|
||||
'share_id': self.share.id,
|
||||
'usage': None,
|
||||
'metadata': {},
|
||||
'name~': None,
|
||||
'description~': None,
|
||||
'description': None
|
||||
|
|
|
@ -1160,6 +1160,24 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
|
|||
}]}
|
||||
return (200, {}, access_list)
|
||||
|
||||
def delete_snapshots_1234_metadata_test_key(self, **kw):
|
||||
return (204, {}, None)
|
||||
|
||||
def delete_snapshots_1234_metadata_key1(self, **kw):
|
||||
return (204, {}, None)
|
||||
|
||||
def delete_snapshots_1234_metadata_key2(self, **kw):
|
||||
return (204, {}, None)
|
||||
|
||||
def post_snapshots_1234_metadata(self, **kw):
|
||||
return (204, {}, {'metadata': {'test_key': 'test_value'}})
|
||||
|
||||
def put_snapshots_1234_metadata(self, **kw):
|
||||
return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}})
|
||||
|
||||
def get_snapshots_1234_metadata(self, **kw):
|
||||
return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}})
|
||||
|
||||
def post_snapshot_instances_1234_action(self, body, **kw):
|
||||
_body = None
|
||||
resp = 202
|
||||
|
|
|
@ -215,3 +215,30 @@ class ShareSnapshotsTest(utils.TestCase):
|
|||
def test_access_list(self):
|
||||
cs.share_snapshots.access_list(1234)
|
||||
cs.assert_called('GET', '/snapshots/1234/access-list')
|
||||
|
||||
def test_get_metadata(self):
|
||||
cs.share_snapshots.get_metadata(1234)
|
||||
cs.assert_called('GET', '/snapshots/1234/metadata')
|
||||
|
||||
def test_set_metadata(self):
|
||||
cs.share_snapshots.set_metadata(1234, {'k1': 'v2'})
|
||||
cs.assert_called('POST', '/snapshots/1234/metadata',
|
||||
{'metadata': {'k1': 'v2'}})
|
||||
|
||||
@ddt.data(
|
||||
type('SnapshotUUID', (object, ), {'uuid': '1234'}),
|
||||
type('SnapshotID', (object, ), {'id': '1234'}),
|
||||
'1234')
|
||||
def test_delete_metadata(self, snapshot):
|
||||
keys = ['key1']
|
||||
cs.share_snapshots.delete_metadata(snapshot, keys)
|
||||
cs.assert_called('DELETE', '/snapshots/1234/metadata/key1')
|
||||
|
||||
@ddt.data(
|
||||
type('SnapshotUUID', (object, ), {'uuid': '1234'}),
|
||||
type('SnapshotID', (object, ), {'id': '1234'}),
|
||||
'1234')
|
||||
def test_metadata_update_all(self, snapshot):
|
||||
cs.share_snapshots.update_all_metadata(snapshot, {'k1': 'v1'})
|
||||
cs.assert_called('PUT', '/snapshots/1234/metadata',
|
||||
{'metadata': {'k1': 'v1'}})
|
||||
|
|
|
@ -19,7 +19,8 @@ from manilaclient import base
|
|||
from manilaclient.common import constants
|
||||
|
||||
|
||||
class ShareSnapshot(base.Resource):
|
||||
class ShareSnapshot(base.MetadataCapableResource):
|
||||
|
||||
"""Represent a snapshot of a share."""
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -57,11 +58,13 @@ class ShareSnapshot(base.Resource):
|
|||
return self.manager.access_list(self)
|
||||
|
||||
|
||||
class ShareSnapshotManager(base.ManagerWithFind):
|
||||
class ShareSnapshotManager(base.MetadataCapableManager):
|
||||
"""Manage :class:`ShareSnapshot` resources."""
|
||||
resource_class = ShareSnapshot
|
||||
resource_path = '/snapshots'
|
||||
|
||||
def create(self, share, force=False, name=None, description=None):
|
||||
def _do_create(self, share, force=False, name=None, description=None,
|
||||
metadata=None):
|
||||
"""Create a snapshot of the given share.
|
||||
|
||||
:param share_id: The ID of the share to snapshot.
|
||||
|
@ -69,14 +72,27 @@ class ShareSnapshotManager(base.ManagerWithFind):
|
|||
share is busy. Default is False.
|
||||
:param name: Name of the snapshot
|
||||
:param description: Description of the snapshot
|
||||
:param metadata: dict - optional metadata to set on share creation
|
||||
:rtype: :class:`ShareSnapshot`
|
||||
"""
|
||||
|
||||
metadata = metadata if metadata is not None else dict()
|
||||
body = {'snapshot': {'share_id': base.getid(share),
|
||||
'force': force,
|
||||
'name': name,
|
||||
'description': description}}
|
||||
'description': description,
|
||||
'metadata': metadata}}
|
||||
return self._create('/snapshots', body, 'snapshot')
|
||||
|
||||
@api_versions.wraps("2.0", "2.72")
|
||||
def create(self, share, force=False, name=None, description=None):
|
||||
return self._do_create(share, force, name, description)
|
||||
|
||||
@api_versions.wraps("2.73")
|
||||
def create(self, share, force=False, name=None, description=None,# noqa F811
|
||||
metadata=None):
|
||||
return self._do_create(share, force, name, description, metadata)
|
||||
|
||||
@api_versions.wraps("2.12")
|
||||
def manage(self, share, provider_location,
|
||||
driver_options=None,
|
||||
|
|
|
@ -27,7 +27,8 @@ from manilaclient import exceptions
|
|||
from manilaclient.v2 import share_instances
|
||||
|
||||
|
||||
class Share(base.Resource):
|
||||
class Share(base.MetadataCapableResource):
|
||||
|
||||
"""A share is an extra block level storage to the OpenStack instances."""
|
||||
def __repr__(self):
|
||||
return "<Share: %s>" % self.id
|
||||
|
@ -87,10 +88,6 @@ class Share(base.Resource):
|
|||
"""Get access list from a share."""
|
||||
return self.manager.access_list(self)
|
||||
|
||||
def update_all_metadata(self, metadata):
|
||||
"""Update all metadata of this share."""
|
||||
return self.manager.update_all_metadata(self, metadata)
|
||||
|
||||
def reset_state(self, state):
|
||||
"""Update the share with the provided state."""
|
||||
self.manager.reset_state(self, state)
|
||||
|
@ -120,9 +117,10 @@ class Share(base.Resource):
|
|||
self.manager.restore(self)
|
||||
|
||||
|
||||
class ShareManager(base.ManagerWithFind):
|
||||
class ShareManager(base.MetadataCapableManager):
|
||||
"""Manage :class:`Share` resources."""
|
||||
resource_class = Share
|
||||
resource_path = '/shares'
|
||||
|
||||
def create(self, share_proto, size, snapshot_id=None, name=None,
|
||||
description=None, metadata=None, share_network=None,
|
||||
|
@ -659,45 +657,6 @@ class ShareManager(base.ManagerWithFind):
|
|||
def access_list(self, share): # noqa
|
||||
return self._do_access_list(share, "access_list")
|
||||
|
||||
def get_metadata(self, share):
|
||||
"""Get metadata of a share.
|
||||
|
||||
:param share: either share object or text with its ID.
|
||||
"""
|
||||
return self._get("/shares/%s/metadata" % base.getid(share),
|
||||
"metadata")
|
||||
|
||||
def set_metadata(self, share, metadata):
|
||||
"""Set or update metadata for share.
|
||||
|
||||
:param share: either share object or text with its ID.
|
||||
:param metadata: A list of keys to be set.
|
||||
"""
|
||||
body = {'metadata': metadata}
|
||||
return self._create("/shares/%s/metadata" % base.getid(share),
|
||||
body, "metadata")
|
||||
|
||||
def delete_metadata(self, share, keys):
|
||||
"""Delete specified keys from shares metadata.
|
||||
|
||||
:param share: either share object or text with its ID.
|
||||
:param keys: A list of keys to be removed.
|
||||
"""
|
||||
share_id = base.getid(share)
|
||||
for key in keys:
|
||||
self._delete("/shares/%(share_id)s/metadata/%(key)s" % {
|
||||
'share_id': share_id, 'key': key})
|
||||
|
||||
def update_all_metadata(self, share, metadata):
|
||||
"""Update all metadata of a share.
|
||||
|
||||
:param share: either share object or text with its ID.
|
||||
:param metadata: A list of keys to be updated.
|
||||
"""
|
||||
body = {'metadata': metadata}
|
||||
return self._update("/shares/%s/metadata" % base.getid(share),
|
||||
body)
|
||||
|
||||
def _action(self, action, share, info=None, **kwargs):
|
||||
"""Perform a share 'action'.
|
||||
|
||||
|
|
|
@ -1408,16 +1408,16 @@ def do_share_server_migration_get_progress(cs, args):
|
|||
metavar='<key=value>',
|
||||
nargs='+',
|
||||
default=[],
|
||||
help='Metadata to set or unset (key is only necessary on unset).')
|
||||
help='Metadata to set or unset (only key is necessary to unset).')
|
||||
def do_metadata(cs, args):
|
||||
"""Set or delete metadata on a share."""
|
||||
share = _find_share(cs, args.share)
|
||||
metadata = _extract_metadata(args)
|
||||
|
||||
if args.action == 'set':
|
||||
cs.shares.set_metadata(share, metadata)
|
||||
share.set_metadata(metadata)
|
||||
elif args.action == 'unset':
|
||||
cs.shares.delete_metadata(share, sorted(list(metadata), reverse=True))
|
||||
share.delete_metadata(sorted(list(metadata), reverse=True))
|
||||
|
||||
|
||||
@cliutils.arg(
|
||||
|
@ -1427,7 +1427,7 @@ def do_metadata(cs, args):
|
|||
def do_metadata_show(cs, args):
|
||||
"""Show metadata of given share."""
|
||||
share = _find_share(cs, args.share)
|
||||
metadata = cs.shares.get_metadata(share)._info
|
||||
metadata = share.get_metadata()._info
|
||||
cliutils.print_dict(metadata, 'Property')
|
||||
|
||||
|
||||
|
@ -2826,6 +2826,14 @@ def do_share_instance_export_location_show(cs, args):
|
|||
default=None,
|
||||
help='Filter results matching a share snapshot description pattern. '
|
||||
'Available only for microversion >= 2.36.')
|
||||
@cliutils.arg(
|
||||
'--metadata',
|
||||
metavar='<key=value>',
|
||||
type=str,
|
||||
default=None,
|
||||
nargs='*',
|
||||
help='Filters results by a metadata key and value. OPTIONAL: '
|
||||
'Default=None, Available only for microversion >= 2.73. ')
|
||||
def do_snapshot_list(cs, args):
|
||||
"""List all the snapshots."""
|
||||
all_projects = int(
|
||||
|
@ -2852,6 +2860,7 @@ def do_snapshot_list(cs, args):
|
|||
'status': args.status,
|
||||
'share_id': share.id,
|
||||
'usage': args.usage,
|
||||
'metadata': _extract_metadata(args),
|
||||
}
|
||||
if cs.api_version.matches(api_versions.APIVersion("2.36"),
|
||||
api_versions.APIVersion()):
|
||||
|
@ -5218,7 +5227,7 @@ def do_type_delete(cs, args):
|
|||
metavar='<key=value>',
|
||||
nargs='*',
|
||||
default=None,
|
||||
help='Extra_specs to set or unset (key is only necessary on unset).')
|
||||
help='Extra_specs to set or unset (only key is necessary to unset).')
|
||||
def do_type_key(cs, args):
|
||||
"""Set or unset extra_spec for a share type (Admin only)."""
|
||||
stype = _find_share_type(cs, args.stype)
|
||||
|
@ -5455,7 +5464,7 @@ def do_share_group_type_delete(cs, args):
|
|||
metavar='<key=value>',
|
||||
nargs='*',
|
||||
default=None,
|
||||
help='Group specs to set or unset (key is only necessary on unset).')
|
||||
help='Group specs to set or unset (only key is necessary to unset).')
|
||||
@cliutils.service_type('sharev2')
|
||||
def do_share_group_type_key(cs, args):
|
||||
"""Set or unset group_spec for a share group type (Admin only)."""
|
||||
|
@ -6438,11 +6447,11 @@ def do_share_replica_export_location_list(cs, args):
|
|||
@cliutils.arg(
|
||||
'replica',
|
||||
metavar='<replica>',
|
||||
help='Name or ID of the share instance.')
|
||||
help='Name or ID of the share replica.')
|
||||
@cliutils.arg(
|
||||
'export_location',
|
||||
metavar='<export_location>',
|
||||
help='ID of the share instance export location.')
|
||||
help='ID of the share replica export location.')
|
||||
def do_share_replica_export_location_show(cs, args):
|
||||
"""Show details of a share replica's export location."""
|
||||
replica = _find_share_replica(cs, args.replica)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
features:
|
||||
- |
|
||||
Adds support to set snapshot property on snapshot create, filter list on
|
||||
snapshot property, and snapshot property set and unset. Only in OSC.
|
Loading…
Reference in New Issue