Share migration Newton improvements
At Austin 2016 summit there were several improvements to
Share migration feature discussed. This patch implements
these changes.
Changes are:
- 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 existing 'Notify', thus removing 1-phase migration
possibility.
- Renamed existing 'Force Host Copy' parameter to 'Force
Host-assisted Migration'.
- Renamed all 'migration_info' and 'migration_get_info' entries to
'connection_info' and 'connection_get_info'.
- Updated driver interfaces with the new API parameters, drivers
must respect them.
- Changed share/api => scheduler RPCAPI back to asynchronous.
- Added optional SHA-256 validation to perform additional check if
bytes were corrupted during copying.
- Added mount options configuration to Data Service so CIFS shares
can be mounted.
- Driver may override _get_access_mapping if supports a different
access_type/protocol combination than what is defined by default.
- Added CIFS share protocol support and 'user' access type
support to Data Service.
- Reset Task State API now allows task_state to be unset using
'None' value.
- Added possibility to change share-network when migrating a share.
- Bumped microversion to 2.22.
- Removed support of all previous versions of Share Migration APIs.
APIImpact
DocImpact
Implements: blueprint newton-migration-improvements
Change-Id: Ief49a46c86ed3c22d3b31021aff86a9ce0ecbe3b
diff --git a/manila_tempest_tests/tests/api/admin/test_admin_actions.py b/manila_tempest_tests/tests/api/admin/test_admin_actions.py
index 40b2460..ac862d4 100644
--- a/manila_tempest_tests/tests/api/admin/test_admin_actions.py
+++ b/manila_tempest_tests/tests/api/admin/test_admin_actions.py
@@ -30,7 +30,7 @@
super(AdminActionsTest, cls).resource_setup()
cls.states = ["error", "available"]
cls.task_states = ["migration_starting", "data_copying_in_progress",
- "migration_success"]
+ "migration_success", None]
cls.bad_status = "error_deleting"
cls.sh = cls.create_share()
cls.sh_instance = (
@@ -120,7 +120,7 @@
self.shares_v2_client.wait_for_resource_deletion(snapshot_id=sn["id"])
@test.attr(type=[base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND])
- @base.skip_if_microversion_lt("2.15")
+ @base.skip_if_microversion_lt("2.22")
def test_reset_share_task_state(self):
for task_state in self.task_states:
self.shares_v2_client.reset_task_state(self.sh["id"], task_state)
diff --git a/manila_tempest_tests/tests/api/admin/test_admin_actions_negative.py b/manila_tempest_tests/tests/api/admin/test_admin_actions_negative.py
index 67444de..735c65d 100644
--- a/manila_tempest_tests/tests/api/admin/test_admin_actions_negative.py
+++ b/manila_tempest_tests/tests/api/admin/test_admin_actions_negative.py
@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import ddt
from tempest import config
from tempest.lib import exceptions as lib_exc
from tempest import test
@@ -124,20 +125,14 @@
self.sh['id'])
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
- @base.skip_if_microversion_lt("2.15")
- def test_reset_task_state_empty(self):
- self.assertRaises(
- lib_exc.BadRequest, self.admin_client.reset_task_state,
- self.sh['id'], None)
-
- @test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
- @base.skip_if_microversion_lt("2.15")
+ @base.skip_if_microversion_lt("2.22")
def test_reset_task_state_invalid_state(self):
self.assertRaises(
lib_exc.BadRequest, self.admin_client.reset_task_state,
self.sh['id'], 'fake_state')
+@ddt.ddt
class AdminActionsAPIOnlyNegativeTest(base.BaseSharesMixedTest):
@classmethod
@@ -153,7 +148,7 @@
self.member_client.list_share_instances)
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API])
- @base.skip_if_microversion_lt("2.15")
+ @base.skip_if_microversion_lt("2.22")
def test_reset_task_state_share_not_found(self):
self.assertRaises(
lib_exc.NotFound, self.admin_client.reset_task_state,
@@ -196,3 +191,20 @@
def test_reset_nonexistent_snapshot_state(self):
self.assertRaises(lib_exc.NotFound, self.admin_client.reset_state,
"fake", s_type="snapshots")
+
+ @test.attr(type=[base.TAG_NEGATIVE, base.TAG_API])
+ @ddt.data('migrate_share', 'migration_complete', 'reset_task_state',
+ 'migration_get_progress', 'migration_cancel')
+ def test_migration_API_invalid_microversion(self, method_name):
+ if method_name == 'migrate_share':
+ self.assertRaises(
+ lib_exc.NotFound, getattr(self.shares_v2_client, method_name),
+ 'fake_share', 'fake_host', version='2.21')
+ elif method_name == 'reset_task_state':
+ self.assertRaises(
+ lib_exc.NotFound, getattr(self.shares_v2_client, method_name),
+ 'fake_share', 'fake_task_state', version='2.21')
+ else:
+ self.assertRaises(
+ lib_exc.NotFound, getattr(self.shares_v2_client, method_name),
+ 'fake_share', version='2.21')
diff --git a/manila_tempest_tests/tests/api/admin/test_migration.py b/manila_tempest_tests/tests/api/admin/test_migration.py
index c5fef44..ed643ff 100644
--- a/manila_tempest_tests/tests/api/admin/test_migration.py
+++ b/manila_tempest_tests/tests/api/admin/test_migration.py
@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import ddt
from tempest import config
from tempest import test
@@ -23,10 +24,27 @@
CONF = config.CONF
+@ddt.ddt
class MigrationNFSTest(base.BaseSharesAdminTest):
- """Tests Share Migration.
+ """Tests Share Migration for NFS shares.
Tests share migration in multi-backend environment.
+
+ This class covers:
+ 1) Driver-assisted migration: force_host_assisted_migration, nondisruptive,
+ writable and preserve-metadata are False.
+ 2) Host-assisted migration: force_host_assisted_migration is True,
+ nondisruptive, writable and preserve-metadata are False.
+ 3) 2-phase migration of both Host-assisted and Driver-assisted.
+
+ No need to test with writable, preserve-metadata and non-disruptive as
+ True, values are supplied to the driver which decides what to do. Test
+ should be positive, so not being writable, not preserving metadata and
+ being disruptive is less restrictive for drivers, which would abort if they
+ cannot handle them.
+
+ Drivers that implement driver-assisted migration should enable the
+ configuration flag to be tested.
"""
protocol = "nfs"
@@ -35,80 +53,89 @@
def resource_setup(cls):
super(MigrationNFSTest, cls).resource_setup()
if cls.protocol not in CONF.share.enable_protocols:
- message = "%s tests are disabled" % cls.protocol
+ message = "%s tests are disabled." % cls.protocol
raise cls.skipException(message)
- if not CONF.share.run_migration_tests:
+ if not (CONF.share.run_host_assisted_migration_tests or
+ CONF.share.run_driver_assisted_migration_tests):
raise cls.skipException("Share migration tests are disabled.")
@test.attr(type=[base.TAG_POSITIVE, base.TAG_BACKEND])
- @base.skip_if_microversion_lt("2.15")
- def test_migration_cancel(self):
+ @base.skip_if_microversion_lt("2.22")
+ @ddt.data(True, False)
+ def test_migration_cancel(self, force_host_assisted):
+
+ self._check_migration_enabled(force_host_assisted)
share, dest_pool = self._setup_migration()
old_exports = self.shares_v2_client.list_share_export_locations(
- share['id'], version='2.15')
+ share['id'])
self.assertNotEmpty(old_exports)
old_exports = [x['path'] for x in old_exports
if x['is_admin_only'] is False]
self.assertNotEmpty(old_exports)
- task_states = (constants.TASK_STATE_DATA_COPYING_COMPLETED,
- constants.TASK_STATE_MIGRATION_DRIVER_PHASE1_DONE)
+ task_state = (constants.TASK_STATE_DATA_COPYING_COMPLETED
+ if force_host_assisted
+ else constants.TASK_STATE_MIGRATION_DRIVER_PHASE1_DONE)
share = self.migrate_share(
- share['id'], dest_pool, version='2.15', notify=False,
- wait_for_status=task_states)
+ share['id'], dest_pool, wait_for_status=task_state,
+ force_host_assisted_migration=force_host_assisted)
self._validate_migration_successful(
- dest_pool, share, task_states, '2.15', notify=False)
+ dest_pool, share, task_state, complete=False)
share = self.migration_cancel(share['id'], dest_pool)
self._validate_migration_successful(
dest_pool, share, constants.TASK_STATE_MIGRATION_CANCELLED,
- '2.15', notify=False)
+ complete=False)
@test.attr(type=[base.TAG_POSITIVE, base.TAG_BACKEND])
- @base.skip_if_microversion_lt("2.5")
- def test_migration_empty_v2_5(self):
+ @base.skip_if_microversion_lt("2.22")
+ @ddt.data(True, False)
+ def test_migration_2phase(self, force_host_assisted):
- share, dest_pool = self._setup_migration()
-
- share = self.migrate_share(share['id'], dest_pool, version='2.5')
-
- self._validate_migration_successful(
- dest_pool, share, constants.TASK_STATE_MIGRATION_SUCCESS,
- version='2.5')
-
- @test.attr(type=[base.TAG_POSITIVE, base.TAG_BACKEND])
- @base.skip_if_microversion_lt("2.15")
- def test_migration_completion_empty_v2_15(self):
+ self._check_migration_enabled(force_host_assisted)
share, dest_pool = self._setup_migration()
old_exports = self.shares_v2_client.list_share_export_locations(
- share['id'], version='2.15')
+ share['id'])
self.assertNotEmpty(old_exports)
old_exports = [x['path'] for x in old_exports
if x['is_admin_only'] is False]
self.assertNotEmpty(old_exports)
- task_states = (constants.TASK_STATE_DATA_COPYING_COMPLETED,
- constants.TASK_STATE_MIGRATION_DRIVER_PHASE1_DONE)
+ task_state = (constants.TASK_STATE_DATA_COPYING_COMPLETED
+ if force_host_assisted
+ else constants.TASK_STATE_MIGRATION_DRIVER_PHASE1_DONE)
+
+ old_share_network_id = share['share_network_id']
+ new_share_network_id = self._create_secondary_share_network(
+ old_share_network_id)
share = self.migrate_share(
- share['id'], dest_pool, version='2.15', notify=False,
- wait_for_status=task_states)
+ share['id'], dest_pool,
+ force_host_assisted_migration=force_host_assisted,
+ wait_for_status=task_state,
+ new_share_network_id=new_share_network_id)
self._validate_migration_successful(
- dest_pool, share, task_states, '2.15', notify=False)
+ dest_pool, share, task_state,
+ complete=False, share_network_id=old_share_network_id)
- share = self.migration_complete(share['id'], dest_pool, version='2.15')
+ progress = self.shares_v2_client.migration_get_progress(share['id'])
+
+ self.assertEqual(task_state, progress['task_state'])
+ self.assertEqual(100, progress['total_progress'])
+
+ share = self.migration_complete(share['id'], dest_pool)
self._validate_migration_successful(
dest_pool, share, constants.TASK_STATE_MIGRATION_SUCCESS,
- version='2.15')
+ complete=True, share_network_id=new_share_network_id)
def _setup_migration(self):
@@ -145,28 +172,59 @@
return share, dest_pool
- def _validate_migration_successful(self, dest_pool, share,
- status_to_wait, version, notify=True):
+ def _validate_migration_successful(self, dest_pool, share, status_to_wait,
+ version=CONF.share.max_api_microversion,
+ complete=True, share_network_id=None):
statuses = ((status_to_wait,)
if not isinstance(status_to_wait, (tuple, list, set))
else status_to_wait)
- if utils.is_microversion_lt(version, '2.9'):
- new_exports = share['export_locations']
- self.assertNotEmpty(new_exports)
- else:
- new_exports = self.shares_v2_client.list_share_export_locations(
- share['id'], version='2.9')
- self.assertNotEmpty(new_exports)
- new_exports = [x['path'] for x in new_exports if
- x['is_admin_only'] is False]
- self.assertNotEmpty(new_exports)
+ new_exports = self.shares_v2_client.list_share_export_locations(
+ share['id'], version=version)
+ self.assertNotEmpty(new_exports)
+ new_exports = [x['path'] for x in new_exports if
+ x['is_admin_only'] is False]
+ self.assertNotEmpty(new_exports)
+
+ self.assertIn(share['task_state'], statuses)
+ if share_network_id:
+ self.assertEqual(share_network_id, share['share_network_id'])
# Share migrated
- if notify:
+ if complete:
self.assertEqual(dest_pool, share['host'])
+ self.shares_v2_client.delete_share(share['id'])
+ self.shares_v2_client.wait_for_resource_deletion(
+ share_id=share['id'])
# Share not migrated yet
else:
self.assertNotEqual(dest_pool, share['host'])
- self.assertIn(share['task_state'], statuses)
+
+ def _check_migration_enabled(self, force_host_assisted):
+
+ if force_host_assisted:
+ if not CONF.share.run_host_assisted_migration_tests:
+ raise self.skipException(
+ "Host-assisted migration tests are disabled.")
+ else:
+ if not CONF.share.run_driver_assisted_migration_tests:
+ raise self.skipException(
+ "Driver-assisted migration tests are disabled.")
+
+ def _create_secondary_share_network(self, old_share_network_id):
+ if (utils.is_microversion_ge(
+ CONF.share.max_api_microversion, "2.22") and
+ CONF.share.multitenancy_enabled):
+
+ old_share_network = self.shares_v2_client.get_share_network(
+ old_share_network_id)
+
+ new_share_network = self.create_share_network(
+ cleanup_in_class=True,
+ neutron_net_id=old_share_network['neutron_net_id'],
+ neutron_subnet_id=old_share_network['neutron_subnet_id'])
+
+ return new_share_network['id']
+ else:
+ return None
diff --git a/manila_tempest_tests/tests/api/admin/test_migration_negative.py b/manila_tempest_tests/tests/api/admin/test_migration_negative.py
index 3dfc31a..94450d2 100644
--- a/manila_tempest_tests/tests/api/admin/test_migration_negative.py
+++ b/manila_tempest_tests/tests/api/admin/test_migration_negative.py
@@ -26,7 +26,7 @@
CONF = config.CONF
-class MigrationNFSTest(base.BaseSharesAdminTest):
+class MigrationTest(base.BaseSharesAdminTest):
"""Tests Share Migration.
Tests share migration in multi-backend environment.
@@ -36,8 +36,12 @@
@classmethod
def resource_setup(cls):
- super(MigrationNFSTest, cls).resource_setup()
- if not CONF.share.run_migration_tests:
+ super(MigrationTest, cls).resource_setup()
+ if cls.protocol not in CONF.share.enable_protocols:
+ message = "%s tests are disabled." % cls.protocol
+ raise cls.skipException(message)
+ if not (CONF.share.run_host_assisted_migration_tests or
+ CONF.share.run_driver_assisted_migration_tests):
raise cls.skipException("Share migration tests are disabled.")
pools = cls.shares_client.list_pools(detail=True)['pools']
@@ -62,56 +66,112 @@
cls.dest_pool = dest_pool['name']
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
- @base.skip_if_microversion_lt("2.15")
+ @base.skip_if_microversion_lt("2.22")
def test_migration_cancel_invalid(self):
self.assertRaises(
lib_exc.BadRequest, self.shares_v2_client.migration_cancel,
self.share['id'])
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
- @base.skip_if_microversion_lt("2.15")
- def test_migration_get_progress_invalid(self):
+ @base.skip_if_microversion_lt("2.22")
+ def test_migration_get_progress_None(self):
+ self.shares_v2_client.reset_task_state(self.share["id"], None)
+ self.shares_v2_client.wait_for_share_status(
+ self.share["id"], None, 'task_state')
self.assertRaises(
lib_exc.BadRequest, self.shares_v2_client.migration_get_progress,
self.share['id'])
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
- @base.skip_if_microversion_lt("2.15")
+ @base.skip_if_microversion_lt("2.22")
def test_migration_complete_invalid(self):
self.assertRaises(
lib_exc.BadRequest, self.shares_v2_client.migration_complete,
self.share['id'])
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
- @base.skip_if_microversion_lt("2.5")
+ @base.skip_if_microversion_lt("2.22")
+ def test_migration_cancel_not_found(self):
+ self.assertRaises(
+ lib_exc.NotFound, self.shares_v2_client.migration_cancel,
+ 'invalid_share_id')
+
+ @test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+ @base.skip_if_microversion_lt("2.22")
+ def test_migration_get_progress_not_found(self):
+ self.assertRaises(
+ lib_exc.NotFound, self.shares_v2_client.migration_get_progress,
+ 'invalid_share_id')
+
+ @test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+ @base.skip_if_microversion_lt("2.22")
+ def test_migration_complete_not_found(self):
+ self.assertRaises(
+ lib_exc.NotFound, self.shares_v2_client.migration_complete,
+ 'invalid_share_id')
+
+ @test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+ @base.skip_if_microversion_lt("2.22")
@testtools.skipUnless(CONF.share.run_snapshot_tests,
"Snapshot tests are disabled.")
- def test_migrate_share_with_snapshot_v2_5(self):
+ def test_migrate_share_with_snapshot(self):
snap = self.create_snapshot_wait_for_active(self.share['id'])
self.assertRaises(
lib_exc.BadRequest, self.shares_v2_client.migrate_share,
- self.share['id'], self.dest_pool, True, version='2.5')
+ self.share['id'], self.dest_pool)
self.shares_client.delete_snapshot(snap['id'])
self.shares_client.wait_for_resource_deletion(snapshot_id=snap["id"])
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
- @base.skip_if_microversion_lt("2.5")
- def test_migrate_share_same_host_v2_5(self):
+ @base.skip_if_microversion_lt("2.22")
+ def test_migrate_share_same_host(self):
self.assertRaises(
lib_exc.BadRequest, self.shares_v2_client.migrate_share,
- self.share['id'], self.share['host'], True, version='2.5')
+ self.share['id'], self.share['host'])
@test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
- @base.skip_if_microversion_lt("2.5")
- def test_migrate_share_not_available_v2_5(self):
- self.shares_client.reset_state(
- self.share['id'], constants.STATUS_ERROR)
+ @base.skip_if_microversion_lt("2.22")
+ def test_migrate_share_host_invalid(self):
+ self.assertRaises(
+ lib_exc.NotFound, self.shares_v2_client.migrate_share,
+ self.share['id'], 'invalid_host')
+
+ @test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+ @base.skip_if_microversion_lt("2.22")
+ def test_migrate_share_host_assisted_not_allowed(self):
+ self.shares_v2_client.migrate_share(
+ self.share['id'], self.dest_pool,
+ force_host_assisted_migration=True, writable=True,
+ preserve_metadata=True)
+ self.shares_v2_client.wait_for_migration_status(
+ self.share['id'], self.dest_pool, 'migration_error')
+
+ @test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+ @base.skip_if_microversion_lt("2.22")
+ def test_migrate_share_not_found(self):
+ self.assertRaises(
+ lib_exc.NotFound, self.shares_v2_client.migrate_share,
+ 'invalid_share_id', self.dest_pool)
+
+ @test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+ @base.skip_if_microversion_lt("2.22")
+ def test_migrate_share_not_available(self):
+ self.shares_client.reset_state(self.share['id'],
+ constants.STATUS_ERROR)
self.shares_client.wait_for_share_status(self.share['id'],
constants.STATUS_ERROR)
self.assertRaises(
lib_exc.BadRequest, self.shares_v2_client.migrate_share,
- self.share['id'], self.dest_pool, True, version='2.5')
+ self.share['id'], self.dest_pool)
self.shares_client.reset_state(self.share['id'],
constants.STATUS_AVAILABLE)
self.shares_client.wait_for_share_status(self.share['id'],
constants.STATUS_AVAILABLE)
+
+ @test.attr(type=[base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND])
+ @base.skip_if_microversion_lt("2.22")
+ def test_migrate_share_invalid_share_network(self):
+ self.assertRaises(
+ lib_exc.NotFound, self.shares_v2_client.migrate_share,
+ self.share['id'], self.dest_pool,
+ new_share_network_id='invalid_net_id')
diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py
index 82135bf..6d0eb25 100644
--- a/manila_tempest_tests/tests/api/base.py
+++ b/manila_tempest_tests/tests/api/base.py
@@ -401,13 +401,19 @@
return share
@classmethod
- def migrate_share(cls, share_id, dest_host, client=None, notify=True,
- wait_for_status='migration_success', **kwargs):
+ def migrate_share(
+ cls, share_id, dest_host, wait_for_status, client=None,
+ force_host_assisted_migration=False, new_share_network_id=None,
+ **kwargs):
client = client or cls.shares_v2_client
- client.migrate_share(share_id, dest_host, notify, **kwargs)
+ client.migrate_share(
+ share_id, dest_host,
+ force_host_assisted_migration=force_host_assisted_migration,
+ new_share_network_id=new_share_network_id,
+ writable=False, preserve_metadata=False, nondisruptive=False,
+ **kwargs)
share = client.wait_for_migration_status(
- share_id, dest_host, wait_for_status,
- version=kwargs.get('version'))
+ share_id, dest_host, wait_for_status, **kwargs)
return share
@classmethod
@@ -415,8 +421,7 @@
client = client or cls.shares_v2_client
client.migration_complete(share_id, **kwargs)
share = client.wait_for_migration_status(
- share_id, dest_host, 'migration_success',
- version=kwargs.get('version'))
+ share_id, dest_host, 'migration_success', **kwargs)
return share
@classmethod