Merge "Volume status management for volume migration"

This commit is contained in:
Jenkins
2015-09-02 09:27:45 +00:00
committed by Gerrit Code Review
5 changed files with 101 additions and 19 deletions

View File

@@ -276,7 +276,8 @@ class ShellTest(utils.TestCase):
with mock.patch('cinderclient.utils.print_list') as mock_print: with mock.patch('cinderclient.utils.print_list') as mock_print:
self.run_command(cmd) self.run_command(cmd)
mock_print.assert_called_once_with( mock_print.assert_called_once_with(
mock.ANY, mock.ANY, sortby_index=None) mock.ANY, mock.ANY, exclude_unavailable=True,
sortby_index=None)
def test_list_reorder_without_sort(self): def test_list_reorder_without_sort(self):
# sortby_index is 0 without sort information # sortby_index is 0 without sort information
@@ -284,7 +285,8 @@ class ShellTest(utils.TestCase):
with mock.patch('cinderclient.utils.print_list') as mock_print: with mock.patch('cinderclient.utils.print_list') as mock_print:
self.run_command(cmd) self.run_command(cmd)
mock_print.assert_called_once_with( mock_print.assert_called_once_with(
mock.ANY, mock.ANY, sortby_index=0) mock.ANY, mock.ANY, exclude_unavailable=True,
sortby_index=0)
def test_list_availability_zone(self): def test_list_availability_zone(self):
self.run_command('availability-zone-list') self.run_command('availability-zone-list')
@@ -710,17 +712,38 @@ class ShellTest(utils.TestCase):
self.assert_called_anytime('GET', '/types/1') self.assert_called_anytime('GET', '/types/1')
def test_migrate_volume(self): def test_migrate_volume(self):
self.run_command('migrate 1234 fakehost --force-host-copy=True') self.run_command('migrate 1234 fakehost --force-host-copy=True '
'--lock-volume=True')
expected = {'os-migrate_volume': {'force_host_copy': 'True', expected = {'os-migrate_volume': {'force_host_copy': 'True',
'lock_volume': 'True',
'host': 'fakehost'}} 'host': 'fakehost'}}
self.assert_called('POST', '/volumes/1234/action', body=expected) self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_migrate_volume_bool_force(self): def test_migrate_volume_bool_force(self):
self.run_command('migrate 1234 fakehost --force-host-copy') self.run_command('migrate 1234 fakehost --force-host-copy '
'--lock-volume')
expected = {'os-migrate_volume': {'force_host_copy': True, expected = {'os-migrate_volume': {'force_host_copy': True,
'lock_volume': True,
'host': 'fakehost'}} 'host': 'fakehost'}}
self.assert_called('POST', '/volumes/1234/action', body=expected) self.assert_called('POST', '/volumes/1234/action', body=expected)
def test_migrate_volume_bool_force_false(self):
# Set both --force-host-copy and --lock-volume to False.
self.run_command('migrate 1234 fakehost --force-host-copy=False '
'--lock-volume=False')
expected = {'os-migrate_volume': {'force_host_copy': 'False',
'lock_volume': 'False',
'host': 'fakehost'}}
self.assert_called('POST', '/volumes/1234/action', body=expected)
# Do not set the values to --force-host-copy and --lock-volume.
self.run_command('migrate 1234 fakehost')
expected = {'os-migrate_volume': {'force_host_copy': False,
'lock_volume': False,
'host': 'fakehost'}}
self.assert_called('POST', '/volumes/1234/action',
body=expected)
def test_snapshot_metadata_set(self): def test_snapshot_metadata_set(self):
self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2') self.run_command('snapshot-metadata 1234 set key1=val1 key2=val2')
self.assert_called('POST', '/snapshots/1234/metadata', self.assert_called('POST', '/snapshots/1234/metadata',

View File

@@ -166,8 +166,19 @@ class VolumesTest(utils.TestCase):
def test_migrate(self): def test_migrate(self):
v = cs.volumes.get('1234') v = cs.volumes.get('1234')
cs.volumes.migrate_volume(v, 'dest', False) cs.volumes.migrate_volume(v, 'dest', False, False)
cs.assert_called('POST', '/volumes/1234/action') cs.assert_called('POST', '/volumes/1234/action',
{'os-migrate_volume': {'host': 'dest',
'force_host_copy': False,
'lock_volume': False}})
def test_migrate_with_lock_volume(self):
v = cs.volumes.get('1234')
cs.volumes.migrate_volume(v, 'dest', False, True)
cs.assert_called('POST', '/volumes/1234/action',
{'os-migrate_volume': {'host': 'dest',
'force_host_copy': False,
'lock_volume': True}})
def test_metadata_update_all(self): def test_metadata_update_all(self):
cs.volumes.update_all_metadata(1234, {'k1': 'v1'}) cs.volumes.update_all_metadata(1234, {'k1': 'v1'})

View File

@@ -110,11 +110,14 @@ def _print(pt, order):
print(strutils.safe_encode(pt.get_string(sortby=order))) print(strutils.safe_encode(pt.get_string(sortby=order)))
def print_list(objs, fields, formatters=None, sortby_index=0): def print_list(objs, fields, exclude_unavailable=False, formatters=None,
sortby_index=0):
'''Prints a list of objects. '''Prints a list of objects.
@param objs: Objects to print @param objs: Objects to print
@param fields: Fields on each object to be printed @param fields: Fields on each object to be printed
@param exclude_unavailable: Boolean to decide if unavailable fields are
removed
@param formatters: Custom field formatters @param formatters: Custom field formatters
@param sortby_index: Results sorted against the key in the fields list at @param sortby_index: Results sorted against the key in the fields list at
this index; if None then the object order is not this index; if None then the object order is not
@@ -122,12 +125,14 @@ def print_list(objs, fields, formatters=None, sortby_index=0):
''' '''
formatters = formatters or {} formatters = formatters or {}
mixed_case_fields = ['serverId'] mixed_case_fields = ['serverId']
pt = prettytable.PrettyTable([f for f in fields], caching=False) removed_fields = []
pt.aligns = ['l' for f in fields] rows = []
for o in objs: for o in objs:
row = [] row = []
for field in fields: for field in fields:
if field in removed_fields:
continue
if field in formatters: if field in formatters:
row.append(formatters[field](o)) row.append(formatters[field](o))
else: else:
@@ -138,12 +143,24 @@ def print_list(objs, fields, formatters=None, sortby_index=0):
if type(o) == dict and field in o: if type(o) == dict and field in o:
data = o[field] data = o[field]
else: else:
data = getattr(o, field_name, '') if not hasattr(o, field_name) and exclude_unavailable:
removed_fields.append(field)
continue
else:
data = getattr(o, field_name, '')
if data is None: if data is None:
data = '-' data = '-'
if isinstance(data, six.string_types) and "\r" in data: if isinstance(data, six.string_types) and "\r" in data:
data = data.replace("\r", " ") data = data.replace("\r", " ")
row.append(data) row.append(data)
rows.append(row)
for f in removed_fields:
fields.remove(f)
pt = prettytable.PrettyTable((f for f in fields), caching=False)
pt.aligns = ['l' for f in fields]
for row in rows:
pt.add_row(row) pt.add_row(row)
if sortby_index is None: if sortby_index is None:

View File

@@ -160,6 +160,11 @@ def _extract_metadata(args):
metavar='<status>', metavar='<status>',
default=None, default=None,
help='Filters results by a status. Default=None.') help='Filters results by a status. Default=None.')
@utils.arg('--migration_status',
metavar='<migration_status>',
default=None,
help='Filters results by a migration status. Default=None. '
'Admin only.')
@utils.arg('--metadata', @utils.arg('--metadata',
type=str, type=str,
nargs='*', nargs='*',
@@ -212,6 +217,7 @@ def do_list(cs, args):
'project_id': args.tenant, 'project_id': args.tenant,
'name': args.name, 'name': args.name,
'status': args.status, 'status': args.status,
'migration_status': args.migration_status,
'metadata': _extract_metadata(args) if args.metadata else None, 'metadata': _extract_metadata(args) if args.metadata else None,
} }
@@ -233,18 +239,19 @@ def do_list(cs, args):
setattr(vol, 'attached_to', ','.join(map(str, servers))) setattr(vol, 'attached_to', ','.join(map(str, servers)))
if all_tenants: if all_tenants:
key_list = ['ID', 'Tenant ID', 'Status', 'Name', key_list = ['ID', 'Tenant ID', 'Status', 'Migration Status', 'Name',
'Size', 'Volume Type', 'Bootable', 'Multiattach', 'Size', 'Volume Type', 'Bootable', 'Multiattach',
'Attached to'] 'Attached to']
else: else:
key_list = ['ID', 'Status', 'Name', key_list = ['ID', 'Status', 'Migration Status', 'Name',
'Size', 'Volume Type', 'Bootable', 'Size', 'Volume Type', 'Bootable',
'Multiattach', 'Attached to'] 'Multiattach', 'Attached to']
if args.sort_key or args.sort_dir or args.sort: if args.sort_key or args.sort_dir or args.sort:
sortby_index = None sortby_index = None
else: else:
sortby_index = 0 sortby_index = 0
utils.print_list(volumes, key_list, sortby_index=sortby_index) utils.print_list(volumes, key_list, exclude_unavailable=True,
sortby_index=sortby_index)
@utils.arg('volume', @utils.arg('volume',
@@ -452,7 +459,8 @@ def do_force_delete(cs, args):
@utils.arg('--state', metavar='<state>', default='available', @utils.arg('--state', metavar='<state>', default='available',
help=('The state to assign to the volume. Valid values are ' help=('The state to assign to the volume. Valid values are '
'"available", "error", "creating", "deleting", "in-use", ' '"available", "error", "creating", "deleting", "in-use", '
'"attaching", "detaching" and "error_deleting". ' '"attaching", "detaching", "error_deleting" and '
'"maintenance". '
'NOTE: This command simply changes the state of the ' 'NOTE: This command simply changes the state of the '
'Volume in the DataBase with no regard to actual status, ' 'Volume in the DataBase with no regard to actual status, '
'exercise caution when using. Default=available.')) 'exercise caution when using. Default=available.'))
@@ -1153,11 +1161,31 @@ def do_upload_to_image(cs, args):
help='Enables or disables generic host-based ' help='Enables or disables generic host-based '
'force-migration, which bypasses driver ' 'force-migration, which bypasses driver '
'optimizations. Default=False.') 'optimizations. Default=False.')
@utils.arg('--lock-volume', metavar='<True|False>',
choices=['True', 'False'],
required=False,
const=True,
nargs='?',
default=False,
help='Enables or disables the termination of volume migration '
'caused by other commands. This option applies to the '
'available volume. True means it locks the volume '
'state and does not allow the migration to be aborted. The '
'volume status will be in maintenance during the '
'migration. False means it allows the volume migration '
'to be aborted. The volume status is still in the original '
'status. Default=False.')
@utils.service_type('volumev2') @utils.service_type('volumev2')
def do_migrate(cs, args): def do_migrate(cs, args):
"""Migrates volume to a new host.""" """Migrates volume to a new host."""
volume = utils.find_volume(cs, args.volume) volume = utils.find_volume(cs, args.volume)
volume.migrate_volume(args.host, args.force_host_copy) try:
volume.migrate_volume(args.host, args.force_host_copy,
args.lock_volume)
print("Request to migrate volume %s has been accepted." % (volume))
except Exception as e:
print("Migration for volume %s failed: %s." % (volume,
six.text_type(e)))
@utils.arg('volume', metavar='<volume>', @utils.arg('volume', metavar='<volume>',

View File

@@ -139,9 +139,9 @@ class Volume(base.Resource):
""" """
self.manager.extend(self, new_size) self.manager.extend(self, new_size)
def migrate_volume(self, host, force_host_copy): def migrate_volume(self, host, force_host_copy, lock_volume):
"""Migrate the volume to a new host.""" """Migrate the volume to a new host."""
self.manager.migrate_volume(self, host, force_host_copy) self.manager.migrate_volume(self, host, force_host_copy, lock_volume)
def retype(self, volume_type, policy): def retype(self, volume_type, policy):
"""Change a volume's type.""" """Change a volume's type."""
@@ -554,16 +554,19 @@ class VolumeManager(base.ManagerWithFind):
""" """
return self._get("/volumes/%s/encryption" % volume_id)._info return self._get("/volumes/%s/encryption" % volume_id)._info
def migrate_volume(self, volume, host, force_host_copy): def migrate_volume(self, volume, host, force_host_copy, lock_volume):
"""Migrate volume to new host. """Migrate volume to new host.
:param volume: The :class:`Volume` to migrate :param volume: The :class:`Volume` to migrate
:param host: The destination host :param host: The destination host
:param force_host_copy: Skip driver optimizations :param force_host_copy: Skip driver optimizations
:param lock_volume: Lock the volume and guarantee the migration
to finish
""" """
return self._action('os-migrate_volume', return self._action('os-migrate_volume',
volume, volume,
{'host': host, 'force_host_copy': force_host_copy}) {'host': host, 'force_host_copy': force_host_copy,
'lock_volume': lock_volume})
def migrate_volume_completion(self, old_volume, new_volume, error): def migrate_volume_completion(self, old_volume, new_volume, error):
"""Complete the migration from the old volume to the temp new one. """Complete the migration from the old volume to the temp new one.