Update Share Migration CLI

Implemented new parameters:
- Added 'Writable' API parameter: user chooses whether share must
remain writable during migration.
- Added 'Preserve Metadata' API parameter: user chooses whether
share must preserve all file metadata on migration.
- Added 'Non-disruptive' API parameter: user chooses whether
migration of share must be performed non-disruptively.
- Removed 'Notify' parameter.
- Renamed existing 'Force Host Copy' parameter to
'Skip Optimized Migration'.
- Added 'New Share Network' API parameter: user chooses new
share network to migrate to if desired.

Implements: blueprint newton-migration-improvements

Depends-On: Ief49a46c86ed3c22d3b31021aff86a9ce0ecbe3b
Change-Id: I6e7a1d854d9a09f10511c5d47715b1979813f947
This commit is contained in:
Rodrigo Barbieri 2016-06-10 10:14:35 -03:00
parent 283c96e9c5
commit 800b8003b1
10 changed files with 199 additions and 104 deletions

View File

@ -27,7 +27,7 @@ from manilaclient import utils
LOG = logging.getLogger(__name__)
MAX_VERSION = '2.21'
MAX_VERSION = '2.22'
MIN_VERSION = '2.0'
DEPRECATED_VERSION = '1.0'
_VERSIONED_METHOD_MAP = {}

View File

@ -896,6 +896,13 @@ class ManilaCLIClient(base.CLIClient):
"within the required time (%s s)." % self.build_timeout)
raise tempest_lib_exc.TimeoutException(message)
def reset_task_state(self, share_id, state, version=None):
state = '--task_state %s' % state if state else ''
return self.manila('reset-task-state %(state)s %(share)s' % {
'state': state,
'share': share_id,
}, microversion=version)
def create_security_service(self, type='ldap', name=None, description=None,
dns_ip=None, server=None, domain=None,
user=None, password=None, microversion=None):

View File

@ -13,10 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.
import ddt
from tempest.lib.common.utils import data_utils
from manilaclient import config
from manilaclient.tests.functional import base
from manilaclient.tests.functional import utils
CONF = config.CONF
@ -89,6 +92,28 @@ class SharesReadWriteBase(base.BaseTestCase):
self.assertTrue(get.get('export_locations', []) > 0)
@ddt.ddt
class SharesTestMigration(base.BaseTestCase):
@classmethod
def setUpClass(cls):
super(SharesTestMigration, cls).setUpClass()
cls.share = cls.create_share(
share_protocol='nfs',
size=1,
name=data_utils.rand_name('autotest_share_name'),
client=cls.get_user_client(),
cleanup_in_class=True)
@utils.skip_if_microversion_not_supported('2.22')
@ddt.data('migration_error', 'migration_success', 'None')
def test_reset_task_state(self, state):
self.admin_client.reset_task_state(self.share['id'], state)
share = self.admin_client.get_share(self.share['id'])
self.assertEqual(state, share['task_state'])
class NFSSharesReadWriteTest(SharesReadWriteBase):
protocol = 'nfs'

View File

@ -51,10 +51,13 @@ class FakeClient(object):
if body is not None:
actual = self.client.callstack[pos][2]
assert actual == body, "Expected %(b)s; got %(a)s" % {
'b': body,
'a': actual
}
if isinstance(actual, dict) and isinstance(body, dict):
assert sorted(list(actual)) == sorted(list(body))
else:
assert actual == body, "Expected %(b)s; got %(a)s" % {
'b': body,
'a': actual
}
def assert_called_anytime(self, method, url, body=None):
"""Assert than an API method was called anytime in the test."""

View File

@ -367,6 +367,19 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
assert body[action]['new_size'] is not None
elif action in ('unmanage', ):
assert body[action] is None
elif action in (
'migration_cancel', 'migration_complete',
'migration_get_progress'):
assert body[action] is None
if 'migration_get_progress' == action:
_body = {'total_progress': 50}
return 200, {}, _body
elif action in (
'os-migrate_share', 'migrate_share',
'migration_start'):
assert 'host' in body[action]
elif action == 'reset_task_state':
assert 'task_state' in body[action]
else:
raise AssertionError("Unexpected share action: %s" % action)
return (resp, {}, _body)

View File

@ -521,33 +521,26 @@ class SharesTest(utils.TestCase):
cs.shares.list_instances(share)
cs.assert_called('GET', '/shares/1234/instances')
@ddt.data(
("2.6", "os-migrate_share"),
("2.7", "migrate_share"),
("2.14", "migrate_share"),
("2.15", "migration_start"),
)
@ddt.unpack
def test_migration_start(self, microversion, action_name):
def test_migration_start(self):
share = "fake_share"
host = "fake_host"
force_host_copy = "fake_force_host_copy"
version = api_versions.APIVersion(microversion)
version = api_versions.APIVersion('2.22')
manager = shares.ShareManager(
api=fakes.FakeClient(api_version=version))
with mock.patch.object(manager, "_action",
mock.Mock(return_value="fake")):
if version < api_versions.APIVersion('2.15'):
result = manager.migration_start(share, host, force_host_copy)
else:
result = manager.migration_start(share, host, force_host_copy,
True)
result = manager.migration_start(share, host, True)
manager._action.assert_called_once_with(
action_name, share,
{"host": host, "force_host_copy": force_host_copy,
"notify": True})
'migration_start', share, {
"host": host,
"force_host_assisted_migration": True,
"preserve_metadata": True,
"writable": True,
"nondisruptive": False,
"new_share_network_id": None,
})
self.assertEqual("fake", result)
def test_migration_complete(self):

View File

@ -1871,3 +1871,35 @@ class ShellTest(test_utils.TestCase):
expected = {'reset_status': {'status': 'available'}}
self.assert_called('POST', '/snapshot-instances/1234/action',
body=expected)
def test_migration_start(self):
command = ("migration-start --preserve-metadata False --writable False"
" --force-host-assisted-migration True "
"--non-disruptive True --new-share-network 1111 "
"1234 host@backend#pool")
self.run_command(command)
expected = {'migration_start': {
'host': 'host@backend#pool',
'force_host_assisted_migration': 'True',
'preserve-metadata': 'False',
'writable': 'False',
'nondisruptive': 'True',
'new_share_network_id': '1111',
}}
self.assert_called('POST', '/shares/1234/action', body=expected)
@ddt.data('migration-complete', 'migration-get-progress',
'migration-cancel')
def test_migration_others(self, method):
command = ' '.join((method, '1234'))
self.run_command(command)
expected = {method.replace('-', '_'): None}
self.assert_called('POST', '/shares/1234/action', body=expected)
@ddt.data('migration_error', 'migration_success', None)
def test_reset_task_state(self, param):
command = ' '.join(('reset-task-state --state', six.text_type(param),
'1234'))
self.run_command(command)
expected = {'reset_task_state': {'task_state': param}}
self.assert_called('POST', '/shares/1234/action', body=expected)

View File

@ -43,9 +43,13 @@ class Share(common_base.Resource):
"""Unmanage this share."""
self.manager.unmanage(self, **kwargs)
def migration_start(self, host, force_host_copy, notify=True):
def migration_start(self, host, force_host_assisted_migration,
preserve_metadata=True, writable=True,
nondisruptive=False, new_share_network_id=None):
"""Migrate the share to a new host."""
self.manager.migration_start(self, host, force_host_copy, notify)
self.manager.migration_start(self, host, force_host_assisted_migration,
preserve_metadata, writable,
nondisruptive, new_share_network_id)
def migration_complete(self):
"""Complete migration of a share."""
@ -144,42 +148,22 @@ class ShareManager(base.ManagerWithFind):
}
return self._create('/shares', {'share': body}, 'share')
def _do_migrate_start(self, share, host, force_host_copy, notify,
action_name):
"""Migrate share to new host and pool.
:param share: The :class:'share' to migrate
:param host: The destination host and pool
:param force_host_copy: Skip driver optimizations
:param notify: whether migration completion should be notified
:param action_name: action name to be used in request. Changes
according to desired microversion.
"""
@api_versions.wraps("2.22")
@api_versions.experimental_api
def migration_start(self, share, host, force_host_assisted_migration,
preserve_metadata=True, writable=True,
nondisruptive=False, new_share_network_id=None):
return self._action(
action_name, share,
{"host": host, "force_host_copy": force_host_copy,
"notify": notify})
"migration_start", share, {
"host": host,
"force_host_assisted_migration": force_host_assisted_migration,
"preserve_metadata": preserve_metadata,
"writable": writable,
"nondisruptive": nondisruptive,
"new_share_network_id": new_share_network_id,
})
@api_versions.wraps("2.5", "2.6")
@api_versions.experimental_api
def migration_start(self, share, host, force_host_copy):
return self._do_migrate_start(
share, host, force_host_copy, True, "os-migrate_share")
@api_versions.wraps("2.7", "2.14") # noqa
@api_versions.experimental_api
def migration_start(self, share, host, force_host_copy):
return self._do_migrate_start(
share, host, force_host_copy, True, "migrate_share")
@api_versions.wraps("2.15") # noqa
@api_versions.experimental_api
def migration_start(self, share, host, force_host_copy, notify):
return self._do_migrate_start(
share, host, force_host_copy, notify, "migration_start")
@api_versions.wraps("2.15")
@api_versions.wraps("2.22")
@api_versions.experimental_api
def reset_task_state(self, share, task_state):
"""Update the provided share with the provided task state.
@ -190,7 +174,7 @@ class ShareManager(base.ManagerWithFind):
return self._action('reset_task_state', share,
{"task_state": task_state})
@api_versions.wraps("2.15")
@api_versions.wraps("2.22")
@api_versions.experimental_api
def migration_complete(self, share):
"""Completes migration for a given share.
@ -199,7 +183,7 @@ class ShareManager(base.ManagerWithFind):
"""
return self._action('migration_complete', share)
@api_versions.wraps("2.15")
@api_versions.wraps("2.22")
@api_versions.experimental_api
def migration_cancel(self, share):
"""Attempts to cancel migration for a given share.
@ -208,7 +192,7 @@ class ShareManager(base.ManagerWithFind):
"""
return self._action('migration_cancel', share)
@api_versions.wraps("2.15")
@api_versions.wraps("2.22")
@api_versions.experimental_api
def migration_get_progress(self, share):
"""Obtains progress of share migration for a given share.

View File

@ -621,68 +621,82 @@ def do_create(cs, args):
_print_share(cs, share)
@api_versions.wraps("2.22")
@cliutils.arg(
'share',
metavar='<share>',
help='Name or ID of share to migrate.')
@cliutils.arg(
'host',
metavar='<host#pool>',
help='Destination host and pool.')
metavar='<host@backend#pool>',
help="Destination host, backend and pool in format 'host@backend#pool'.")
@cliutils.arg(
'--force-host-copy',
'--force_host_copy',
'--force_host_assisted_migration',
'--force-host-assisted-migration',
metavar='<True|False>',
choices=['True', 'False'],
required=False,
action='single_alias',
help='Enables or disables generic host-based force-migration, which '
'bypasses driver optimizations. Default=False.',
'bypasses driver optimizations. Default=False. '
'Renamed from "force_host_copy" in version 2.22.',
default=False)
@api_versions.wraps("2.5", "2.14")
def do_migrate(cs, args):
"""(Deprecated) Migrates share to a new host (Admin only, Experimental)."""
share = _find_share(cs, args.share)
share.migration_start(args.host, args.force_host_copy, True)
@cliutils.arg(
'share',
metavar='<share>',
help='Name or ID of share to migrate.')
@cliutils.arg(
'host',
metavar='<host#pool>',
help='Destination host and pool.')
@cliutils.arg(
'--force-host-copy',
'--force_host_copy',
'--preserve-metadata',
'--preserve_metadata',
action='single_alias',
metavar='<True|False>',
choices=['True', 'False'],
required=False,
help='Enables or disables generic host-based force-migration, which '
'bypasses driver optimizations. Default=False.',
default=False)
@cliutils.arg(
'--notify',
metavar='<True|False>',
choices=['True', 'False'],
required=False,
help='Enables or disables notification of data copying completed. '
'Default=True.',
help='Chooses whether migration should be forced to preserve all file '
'metadata when moving its contents. Default=True. '
'Introduced in version 2.22.',
default=True)
@api_versions.wraps("2.15")
@cliutils.arg(
'--writable',
metavar='<True|False>',
choices=['True', 'False'],
required=False,
help='Chooses whether migration should be forced to remain writable '
'while contents are being moved. Default=True. '
'Introduced in version 2.22.',
default=True)
@cliutils.arg(
'--non-disruptive',
'--non_disruptive',
action='single_alias',
metavar='<True|False>',
choices=['True', 'False'],
required=False,
help='Chooses whether migration should only be performed if it is not '
'disruptive. Default=False. Introduced in version 2.22.',
default=False)
@cliutils.arg(
'--new_share_network',
'--new-share-network',
metavar='<new_share_network>',
action='single_alias',
required=False,
help='Specifies a new share network if desired to change. Default=None. '
'Introduced in version 2.22.',
default=None)
def do_migration_start(cs, args):
"""Migrates share to a new host (Admin only, Experimental)."""
share = _find_share(cs, args.share)
share.migration_start(args.host, args.force_host_copy, args.notify)
new_share_net_id = None
if args.new_share_network:
share_net = _find_share_network(cs, args.new_share_network)
new_share_net_id = share_net.id if share_net else None
share.migration_start(args.host, args.force_host_assisted_migration,
args.preserve_metadata, args.writable,
args.non_disruptive, new_share_net_id)
@cliutils.arg(
'share',
metavar='<share>',
help='Name or ID of share to complete migration.')
@api_versions.wraps("2.15")
@api_versions.wraps("2.22")
def do_migration_complete(cs, args):
"""Completes migration for a given share (Admin only, Experimental)."""
share = _find_share(cs, args.share)
@ -693,7 +707,7 @@ def do_migration_complete(cs, args):
'share',
metavar='<share>',
help='Name or ID of share to cancel migration.')
@api_versions.wraps("2.15")
@api_versions.wraps("2.22")
def do_migration_cancel(cs, args):
"""Cancels migration of a given share when copying
@ -712,8 +726,9 @@ def do_migration_cancel(cs, args):
'--task_state',
'--state',
metavar='<task_state>',
default='migration_error',
default='None',
action='single_alias',
required=False,
help=('Indicate which task state to assign the share. Options include '
'migration_starting, migration_in_progress, migration_completing, '
'migration_success, migration_error, migration_cancelled, '
@ -721,15 +736,18 @@ def do_migration_cancel(cs, args):
'data_copying_starting, data_copying_in_progress, '
'data_copying_completing, data_copying_completed, '
'data_copying_cancelled, data_copying_error. If no value is '
'provided, migration_error will be used.'))
@api_versions.wraps("2.15")
'provided, None will be used.'))
@api_versions.wraps("2.22")
def do_reset_task_state(cs, args):
"""Explicitly update the task state of a share
(Admin only, Experimental).
"""
state = args.task_state
if args.task_state == 'None':
state = None
share = _find_share(cs, args.share)
share.reset_task_state(args.task_state)
share.reset_task_state(state)
@cliutils.arg(
@ -737,7 +755,7 @@ def do_reset_task_state(cs, args):
metavar='<share>',
help='Name or ID of the share to get share migration progress '
'information.')
@api_versions.wraps("2.15")
@api_versions.wraps("2.22")
def do_migration_get_progress(cs, args):
"""Gets migration progress of a given share when copying

View File

@ -0,0 +1,20 @@
---
prelude: >
- Added new parameters to Share Migration experimental API.
features:
- Share Migration now has parameters to force share migration
procedure to maintain the share writable, preserve its metadata
and be non-disruptive when migrating.
- Added parameter to change share network when performing
migration.
deprecations:
- Renamed Share Migration 'force_host_copy' parameter
to 'force_host_assisted_migration', to better represent
the parameter's functionality.
- API version 2.22 is now required for all Share Migration APIs.
upgrades:
- Removed Share Migration 'notify' parameter, it is no longer
possible to perform a 1-phase migration.
- Removed 'migrate_share' API support.
- Added 'None' to 'reset_task_state' API possible values so it
can unset the task_state.