List manageable volumes and snapshots

Cinder currently has the ability to take over the management of
existing volumes and snapshots ("manage existing") and to relinquish
management of volumes and snapshots ("unmanage"). The API to manage an
existing volume takes a reference, which is a driver-specific string
that is used to identify the volume on the storage backend.  This
patch adds the client code for APIs for listing volumes and snapshots
available for management to make this flow more user-friendly.

Change-Id: Icd81a77294d9190ac6dbaa7e7d35e4dedf45e49f
Implements: blueprint list-manage-existing
This commit is contained in:
Avishay Traeger 2016-08-04 18:52:44 +03:00 committed by xing-yang
parent f7928c4058
commit d24ba31afa
13 changed files with 425 additions and 3 deletions

View File

@ -34,7 +34,7 @@ from cinderclient import utils
# Valid sort directions and client sort keys
SORT_DIR_VALUES = ('asc', 'desc')
SORT_KEY_VALUES = ('id', 'status', 'size', 'availability_zone', 'name',
'bootable', 'created_at')
'bootable', 'created_at', 'reference')
# Mapping of client keys to actual sort keys
SORT_KEY_MAPPINGS = {'name': 'display_name'}
# Additional sort keys for resources
@ -126,7 +126,7 @@ class Manager(common_base.HookableMixin):
def _build_list_url(self, resource_type, detailed=True, search_opts=None,
marker=None, limit=None, sort_key=None, sort_dir=None,
sort=None):
sort=None, offset=None):
if search_opts is None:
search_opts = {}
@ -156,6 +156,9 @@ class Manager(common_base.HookableMixin):
query_params['sort_dir'] = self._format_sort_dir_param(
sort_dir)
if offset:
query_params['offset'] = offset
# Transform the dict to a sequence of two-element tuples in fixed
# order, then the encoded string will be consistent in Python 2&3.
query_string = ""

View File

@ -1156,11 +1156,58 @@ class FakeHTTPClient(base_client.HTTPClient):
def put_snapshots_1234_metadata(self, **kw):
return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}})
def get_os_volume_manage(self, **kw):
vol_id = "volume-ffffffff-0000-ffff-0000-ffffffffffff"
vols = [{"size": 4, "safe_to_manage": False, "actual_size": 4.0,
"reference": {"source-name": vol_id}},
{"size": 5, "safe_to_manage": True, "actual_size": 4.3,
"reference": {"source-name": "myvol"}}]
return (200, {}, {"manageable-volumes": vols})
def get_os_volume_manage_detail(self, **kw):
vol_id = "volume-ffffffff-0000-ffff-0000-ffffffffffff"
vols = [{"size": 4, "reason_not_safe": "volume in use",
"safe_to_manage": False, "extra_info": "qos_setting:high",
"reference": {"source-name": vol_id},
"actual_size": 4.0},
{"size": 5, "reason_not_safe": None, "safe_to_manage": True,
"extra_info": "qos_setting:low", "actual_size": 4.3,
"reference": {"source-name": "myvol"}}]
return (200, {}, {"manageable-volumes": vols})
def post_os_volume_manage(self, **kw):
volume = _stub_volume(id='1234')
volume.update(kw['body']['volume'])
return (202, {}, {'volume': volume})
def get_os_snapshot_manage(self, **kw):
snap_id = "snapshot-ffffffff-0000-ffff-0000-ffffffffffff"
snaps = [{"actual_size": 4.0, "size": 4,
"safe_to_manage": False, "source_id_type": "source-name",
"source_cinder_id": "00000000-ffff-0000-ffff-00000000",
"reference": {"source-name": snap_id},
"source_identifier": "volume-00000000-ffff-0000-ffff-000000"},
{"actual_size": 4.3, "reference": {"source-name": "mysnap"},
"source_id_type": "source-name", "source_identifier": "myvol",
"safe_to_manage": True, "source_cinder_id": None, "size": 5}]
return (200, {}, {"manageable-snapshots": snaps})
def get_os_snapshot_manage_detail(self, **kw):
snap_id = "snapshot-ffffffff-0000-ffff-0000-ffffffffffff"
snaps = [{"actual_size": 4.0, "size": 4,
"safe_to_manage": False, "source_id_type": "source-name",
"source_cinder_id": "00000000-ffff-0000-ffff-00000000",
"reference": {"source-name": snap_id},
"source_identifier": "volume-00000000-ffff-0000-ffff-000000",
"extra_info": "qos_setting:high",
"reason_not_safe": "snapshot in use"},
{"actual_size": 4.3, "reference": {"source-name": "mysnap"},
"safe_to_manage": True, "source_cinder_id": None,
"source_id_type": "source-name", "identifier": "mysnap",
"source_identifier": "myvol", "size": 5,
"extra_info": "qos_setting:low", "reason_not_safe": None}]
return (200, {}, {"manageable-snapshots": snaps})
def post_os_snapshot_manage(self, **kw):
snapshot = _stub_snapshot(id='1234', volume_id='volume_id1')
snapshot.update(kw['body']['snapshot'])

View File

@ -1115,6 +1115,18 @@ class ShellTest(utils.TestCase):
'bootable': False}}
self.assert_called_anytime('POST', '/os-volume-manage', body=expected)
def test_volume_manageable_list(self):
self.run_command('manageable-list fakehost')
self.assert_called('GET', '/os-volume-manage/detail?host=fakehost')
def test_volume_manageable_list_details(self):
self.run_command('manageable-list fakehost --detailed True')
self.assert_called('GET', '/os-volume-manage/detail?host=fakehost')
def test_volume_manageable_list_no_details(self):
self.run_command('manageable-list fakehost --detailed False')
self.assert_called('GET', '/os-volume-manage?host=fakehost')
def test_volume_unmanage(self):
self.run_command('unmanage 1234')
self.assert_called('POST', '/volumes/1234/action',
@ -1327,6 +1339,18 @@ class ShellTest(utils.TestCase):
self.assert_called_anytime('POST', '/os-snapshot-manage',
body=expected)
def test_snapshot_manageable_list(self):
self.run_command('snapshot-manageable-list fakehost')
self.assert_called('GET', '/os-snapshot-manage/detail?host=fakehost')
def test_snapshot_manageable_list_details(self):
self.run_command('snapshot-manageable-list fakehost --detailed True')
self.assert_called('GET', '/os-snapshot-manage/detail?host=fakehost')
def test_snapshot_manageable_list_no_details(self):
self.run_command('snapshot-manageable-list fakehost --detailed False')
self.assert_called('GET', '/os-snapshot-manage?host=fakehost')
def test_snapshot_unmanage(self):
self.run_command('snapshot-unmanage 1234')
self.assert_called('POST', '/snapshots/1234/action',

View File

@ -274,6 +274,14 @@ class VolumesTest(utils.TestCase):
cs.assert_called('POST', '/os-volume-manage', {'volume': expected})
self._assert_request_id(vol)
def test_volume_list_manageable(self):
cs.volumes.list_manageable('host1', detailed=False)
cs.assert_called('GET', '/os-volume-manage?host=host1')
def test_volume_list_manageable_detailed(self):
cs.volumes.list_manageable('host1', detailed=True)
cs.assert_called('GET', '/os-volume-manage/detail?host=host1')
def test_volume_unmanage(self):
v = cs.volumes.get('1234')
self._assert_request_id(v)
@ -288,6 +296,14 @@ class VolumesTest(utils.TestCase):
cs.assert_called('POST', '/os-snapshot-manage', {'snapshot': expected})
self._assert_request_id(vol)
def test_snapshot_list_manageable(self):
cs.volume_snapshots.list_manageable('host1', detailed=False)
cs.assert_called('GET', '/os-snapshot-manage?host=host1')
def test_snapshot_list_manageable_detailed(self):
cs.volume_snapshots.list_manageable('host1', detailed=True)
cs.assert_called('GET', '/os-snapshot-manage/detail?host=host1')
def test_replication_promote(self):
v = cs.volumes.get('1234')
self._assert_request_id(v)

View File

@ -364,3 +364,53 @@ class FakeHTTPClient(fake_v2.FakeHTTPClient):
def delete_group_snapshots_1234(self, **kw):
return (202, {}, {})
#
# Manageable volumes/snapshots
#
def get_manageable_volumes(self, **kw):
vol_id = "volume-ffffffff-0000-ffff-0000-ffffffffffff"
vols = [{"size": 4, "safe_to_manage": False, "actual_size": 4.0,
"reference": {"source-name": vol_id}},
{"size": 5, "safe_to_manage": True, "actual_size": 4.3,
"reference": {"source-name": "myvol"}}]
return (200, {}, {"manageable-volumes": vols})
def get_manageable_volumes_detail(self, **kw):
vol_id = "volume-ffffffff-0000-ffff-0000-ffffffffffff"
vols = [{"size": 4, "reason_not_safe": "volume in use",
"safe_to_manage": False, "extra_info": "qos_setting:high",
"reference": {"source-name": vol_id},
"actual_size": 4.0},
{"size": 5, "reason_not_safe": None, "safe_to_manage": True,
"extra_info": "qos_setting:low", "actual_size": 4.3,
"reference": {"source-name": "myvol"}}]
return (200, {}, {"manageable-volumes": vols})
def get_manageable_snapshots(self, **kw):
snap_id = "snapshot-ffffffff-0000-ffff-0000-ffffffffffff"
snaps = [{"actual_size": 4.0, "size": 4,
"safe_to_manage": False, "source_id_type": "source-name",
"source_cinder_id": "00000000-ffff-0000-ffff-00000000",
"reference": {"source-name": snap_id},
"source_identifier": "volume-00000000-ffff-0000-ffff-000000"},
{"actual_size": 4.3, "reference": {"source-name": "mysnap"},
"source_id_type": "source-name", "source_identifier": "myvol",
"safe_to_manage": True, "source_cinder_id": None, "size": 5}]
return (200, {}, {"manageable-snapshots": snaps})
def get_manageable_snapshots_detail(self, **kw):
snap_id = "snapshot-ffffffff-0000-ffff-0000-ffffffffffff"
snaps = [{"actual_size": 4.0, "size": 4,
"safe_to_manage": False, "source_id_type": "source-name",
"source_cinder_id": "00000000-ffff-0000-ffff-00000000",
"reference": {"source-name": snap_id},
"source_identifier": "volume-00000000-ffff-0000-ffff-000000",
"extra_info": "qos_setting:high",
"reason_not_safe": "snapshot in use"},
{"actual_size": 4.3, "reference": {"source-name": "mysnap"},
"safe_to_manage": True, "source_cinder_id": None,
"source_id_type": "source-name", "identifier": "mysnap",
"source_identifier": "myvol", "size": 5,
"extra_info": "qos_setting:low", "reason_not_safe": None}]
return (200, {}, {"manageable-snapshots": snaps})

View File

@ -309,3 +309,33 @@ class ShellTest(utils.TestCase):
cmd += src
self.run_command(cmd)
self.assert_called_anytime('POST', '/groups/action', body=expected)
def test_volume_manageable_list(self):
self.run_command('--os-volume-api-version 3.8 '
'manageable-list fakehost')
self.assert_called('GET', '/manageable_volumes/detail?host=fakehost')
def test_volume_manageable_list_details(self):
self.run_command('--os-volume-api-version 3.8 '
'manageable-list fakehost --detailed True')
self.assert_called('GET', '/manageable_volumes/detail?host=fakehost')
def test_volume_manageable_list_no_details(self):
self.run_command('--os-volume-api-version 3.8 '
'manageable-list fakehost --detailed False')
self.assert_called('GET', '/manageable_volumes?host=fakehost')
def test_snapshot_manageable_list(self):
self.run_command('--os-volume-api-version 3.8 '
'snapshot-manageable-list fakehost')
self.assert_called('GET', '/manageable_snapshots/detail?host=fakehost')
def test_snapshot_manageable_list_details(self):
self.run_command('--os-volume-api-version 3.8 '
'snapshot-manageable-list fakehost --detailed True')
self.assert_called('GET', '/manageable_snapshots/detail?host=fakehost')
def test_snapshot_manageable_list_no_details(self):
self.run_command('--os-volume-api-version 3.8 '
'snapshot-manageable-list fakehost --detailed False')
self.assert_called('GET', '/manageable_snapshots?host=fakehost')

View File

@ -65,3 +65,23 @@ class VolumesTest(utils.TestCase):
'group_id': '1234'}}
cs.assert_called('POST', '/volumes', body=expected)
self._assert_request_id(vol)
def test_volume_list_manageable(self):
cs = fakes.FakeClient(api_versions.APIVersion('3.8'))
cs.volumes.list_manageable('host1', detailed=False)
cs.assert_called('GET', '/manageable_volumes?host=host1')
def test_volume_list_manageable_detailed(self):
cs = fakes.FakeClient(api_versions.APIVersion('3.8'))
cs.volumes.list_manageable('host1', detailed=True)
cs.assert_called('GET', '/manageable_volumes/detail?host=host1')
def test_snapshot_list_manageable(self):
cs = fakes.FakeClient(api_versions.APIVersion('3.8'))
cs.volume_snapshots.list_manageable('host1', detailed=False)
cs.assert_called('GET', '/manageable_snapshots?host=host1')
def test_snapshot_list_manageable_detailed(self):
cs = fakes.FakeClient(api_versions.APIVersion('3.8'))
cs.volume_snapshots.list_manageable('host1', detailed=True)
cs.assert_called('GET', '/manageable_snapshots/detail?host=host1')

View File

@ -108,3 +108,89 @@ def do_upload_to_image(cs, args):
args.image_name,
args.container_format,
args.disk_format))
@utils.arg('host',
metavar='<host>',
help='Cinder host on which to list manageable volumes; '
'takes the form: host@backend-name#pool')
@utils.arg('--detailed',
metavar='<detailed>',
default=True,
help='Returned detailed information (default true).')
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning volumes that appear later in the volume '
'list than that represented by this volume id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--offset',
metavar='<offset>',
default=None,
help='Number of volumes to skip after marker. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.service_type('volumev2')
def do_manageable_list(cs, args):
"""Lists all manageable volumes."""
detailed = strutils.bool_from_string(args.detailed)
volumes = cs.volumes.list_manageable(host=args.host, detailed=detailed,
marker=args.marker, limit=args.limit,
offset=args.offset, sort=args.sort)
columns = ['reference', 'size', 'safe_to_manage']
if detailed:
columns.extend(['reason_not_safe', 'cinder_id', 'extra_info'])
utils.print_list(volumes, columns, sortby_index=None)
@utils.arg('host',
metavar='<host>',
help='Cinder host on which to list manageable snapshots; '
'takes the form: host@backend-name#pool')
@utils.arg('--detailed',
metavar='<detailed>',
default=True,
help='Returned detailed information (default true).')
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning volumes that appear later in the volume '
'list than that represented by this volume id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--offset',
metavar='<offset>',
default=None,
help='Number of volumes to skip after marker. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.service_type('volumev2')
def do_snapshot_manageable_list(cs, args):
"""Lists all manageable snapshots."""
detailed = strutils.bool_from_string(args.detailed)
snapshots = cs.volume_snapshots.list_manageable(host=args.host,
detailed=detailed,
marker=args.marker,
limit=args.limit,
offset=args.offset,
sort=args.sort)
columns = ['reference', 'size', 'safe_to_manage', 'source_reference']
if detailed:
columns.extend(['reason_not_safe', 'cinder_id', 'extra_info'])
utils.print_list(snapshots, columns, sortby_index=None)

View File

@ -15,5 +15,25 @@
"""Volume snapshot interface (v2 extension)."""
from cinderclient.v3.volume_snapshots import * # flake8: noqa
from cinderclient import api_versions
from cinderclient.v3 import volume_snapshots
class Snapshot(volume_snapshots.Snapshot):
def list_manageable(self, host, detailed=True, marker=None, limit=None,
offset=None, sort=None):
return self.manager.list_manageable(host, detailed=detailed,
marker=marker, limit=limit,
offset=offset, sort=sort)
class SnapshotManager(volume_snapshots.SnapshotManager):
resource_class = Snapshot
@api_versions.wraps("2.0")
def list_manageable(self, host, detailed=True, marker=None, limit=None,
offset=None, sort=None):
url = self._build_list_url("os-snapshot-manage", detailed=detailed,
search_opts={'host': host}, marker=marker,
limit=limit, offset=offset, sort=sort)
return self._list(url, "manageable-snapshots")

View File

@ -43,3 +43,11 @@ class VolumeManager(volumes.VolumeManager):
'image_name': image_name,
'container_format': container_format,
'disk_format': disk_format})
@api_versions.wraps("2.0")
def list_manageable(self, host, detailed=True, marker=None, limit=None,
offset=None, sort=None):
url = self._build_list_url("os-volume-manage", detailed=detailed,
search_opts={'host': host}, marker=marker,
limit=limit, offset=offset, sort=sort)
return self._list(url, "manageable-volumes")

View File

@ -2577,6 +2577,49 @@ def do_manage(cs, args):
utils.print_dict(info)
@utils.service_type('volumev3')
@api_versions.wraps('3.8')
@utils.arg('host',
metavar='<host>',
help='Cinder host on which to list manageable volumes; '
'takes the form: host@backend-name#pool')
@utils.arg('--detailed',
metavar='<detailed>',
default=True,
help='Returned detailed information (default true).')
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning volumes that appear later in the volume '
'list than that represented by this volume id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--offset',
metavar='<offset>',
default=None,
help='Number of volumes to skip after marker. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
def do_manageable_list(cs, args):
"""Lists all manageable volumes."""
detailed = strutils.bool_from_string(args.detailed)
volumes = cs.volumes.list_manageable(host=args.host, detailed=detailed,
marker=args.marker, limit=args.limit,
offset=args.offset, sort=args.sort)
columns = ['reference', 'size', 'safe_to_manage']
if detailed:
columns.extend(['reason_not_safe', 'cinder_id', 'extra_info'])
utils.print_list(volumes, columns, sortby_index=None)
@utils.arg('volume', metavar='<volume>',
help='Name or ID of the volume to unmanage.')
@utils.service_type('volumev3')
@ -3240,6 +3283,52 @@ def do_snapshot_manage(cs, args):
utils.print_dict(info)
@utils.service_type('volumev3')
@api_versions.wraps('3.8')
@utils.arg('host',
metavar='<host>',
help='Cinder host on which to list manageable snapshots; '
'takes the form: host@backend-name#pool')
@utils.arg('--detailed',
metavar='<detailed>',
default=True,
help='Returned detailed information (default true).')
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='Begin returning volumes that appear later in the volume '
'list than that represented by this volume id. '
'Default=None.')
@utils.arg('--limit',
metavar='<limit>',
default=None,
help='Maximum number of volumes to return. Default=None.')
@utils.arg('--offset',
metavar='<offset>',
default=None,
help='Number of volumes to skip after marker. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
def do_snapshot_manageable_list(cs, args):
"""Lists all manageable snapshots."""
detailed = strutils.bool_from_string(args.detailed)
snapshots = cs.volume_snapshots.list_manageable(host=args.host,
detailed=detailed,
marker=args.marker,
limit=args.limit,
offset=args.offset,
sort=args.sort)
columns = ['reference', 'size', 'safe_to_manage', 'source_reference']
if detailed:
columns.extend(['reason_not_safe', 'cinder_id', 'extra_info'])
utils.print_list(snapshots, columns, sortby_index=None)
@utils.arg('snapshot', metavar='<snapshot>',
help='Name or ID of the snapshot to unmanage.')
@utils.service_type('volumev3')

View File

@ -15,6 +15,7 @@
"""Volume snapshot interface (v3 extension)."""
from cinderclient import api_versions
from cinderclient import base
from cinderclient.openstack.common.apiclient import base as common_base
@ -63,6 +64,12 @@ class Snapshot(base.Resource):
self.manager.manage(volume_id=volume_id, ref=ref, name=name,
description=description, metadata=metadata)
def list_manageable(self, host, detailed=True, marker=None, limit=None,
offset=None, sort=None):
return self.manager.list_manageable(host, detailed=detailed,
marker=marker, limit=limit,
offset=offset, sort=sort)
def unmanage(self, snapshot):
"""Unmanage a snapshot."""
self.manager.unmanage(snapshot)
@ -204,6 +211,14 @@ class SnapshotManager(base.ManagerWithFind):
}
return self._create('/os-snapshot-manage', body, 'snapshot')
@api_versions.wraps("3.8")
def list_manageable(self, host, detailed=True, marker=None, limit=None,
offset=None, sort=None):
url = self._build_list_url("manageable_snapshots", detailed=detailed,
search_opts={'host': host}, marker=marker,
limit=limit, offset=offset, sort=sort)
return self._list(url, "manageable-snapshots")
def unmanage(self, snapshot):
"""Unmanage a snapshot."""
return self._action('os-unmanage', snapshot, None)

View File

@ -193,6 +193,12 @@ class Volume(base.Resource):
availability_zone=availability_zone,
metadata=metadata, bootable=bootable)
def list_manageable(self, host, detailed=True, marker=None, limit=None,
offset=None, sort=None):
return self.manager.list_manageable(host, detailed=detailed,
marker=marker, limit=limit,
offset=offset, sort=sort)
def unmanage(self, volume):
"""Unmanage a volume."""
return self.manager.unmanage(volume)
@ -624,6 +630,14 @@ class VolumeManager(base.ManagerWithFind):
}}
return self._create('/os-volume-manage', body, 'volume')
@api_versions.wraps("3.8")
def list_manageable(self, host, detailed=True, marker=None, limit=None,
offset=None, sort=None):
url = self._build_list_url("manageable_volumes", detailed=detailed,
search_opts={'host': host}, marker=marker,
limit=limit, offset=offset, sort=sort)
return self._list(url, "manageable-volumes")
def unmanage(self, volume):
"""Unmanage a volume."""
return self._action('os-unmanage', volume, None)