diff --git a/manila_tempest_tests/common/constants.py b/manila_tempest_tests/common/constants.py index 88fe411b..27507603 100644 --- a/manila_tempest_tests/common/constants.py +++ b/manila_tempest_tests/common/constants.py @@ -97,6 +97,7 @@ MIN_SHARE_ACCESS_METADATA_MICROVERSION = '2.45' # Share servers SERVER_STATE_ACTIVE = 'active' +SERVER_STATE_INACTIVE = 'inactive' SERVER_STATE_CREATING = 'creating' SERVER_STATE_DELETING = 'deleting' SERVER_STATE_ERROR = 'error' @@ -104,3 +105,5 @@ SERVER_STATE_MANAGE_ERROR = 'manage_error' SERVER_STATE_MANAGE_STARTING = 'manage_starting' SERVER_STATE_UNMANAGE_ERROR = 'unmanage_error' SERVER_STATE_UNMANAGE_STARTING = 'unmanage_starting' +STATUS_SERVER_MIGRATING = 'server_migrating' +STATUS_SERVER_MIGRATING_TO = 'server_migrating_to' diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py index 2b6cfd4c..610ae51b 100644 --- a/manila_tempest_tests/config.py +++ b/manila_tempest_tests/config.py @@ -29,7 +29,7 @@ ShareGroup = [ help="The minimum api microversion is configured to be the " "value of the minimum microversion supported by Manila."), cfg.StrOpt("max_api_microversion", - default="2.56", + default="2.57", help="The maximum api microversion is configured to be the " "value of the latest microversion supported by Manila."), cfg.StrOpt("region", @@ -262,6 +262,10 @@ ShareGroup = [ help="Defines whether to run tests that create share from " "snapshots in another pool or az. Enable this " "option if the used driver supports it."), + cfg.BoolOpt("run_share_servers_migration_tests", + default=False, + help="Defines whether to run share servers migration tests. " + "Enable this option if the used driver supports it."), cfg.StrOpt("image_with_share_tools", default="manila-service-image-master", @@ -279,6 +283,10 @@ ShareGroup = [ default=1500, help="Time to wait for share migration before " "timing out (seconds)."), + cfg.IntOpt("share_server_migration_timeout", + default="1500", + help="Time to wait for share server migration before " + "timing out (seconds)."), cfg.StrOpt("default_share_type_name", help="Default share type name to use in tempest tests."), cfg.StrOpt("backend_replication_type", diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py index 050642b2..c1a0f3c4 100644 --- a/manila_tempest_tests/services/share/v2/json/shares_client.py +++ b/manila_tempest_tests/services/share/v2/json/shares_client.py @@ -578,7 +578,7 @@ class SharesV2Client(shares_client.SharesClient): time.sleep(self.build_interval) body = self.get_snapshot(snapshot_id, version=version) snapshot_status = body['status'] - if snapshot_status == status: + if snapshot_status in status: return if 'error' in snapshot_status: raise (share_exceptions. @@ -1522,7 +1522,7 @@ class SharesV2Client(shares_client.SharesClient): time.sleep(self.build_interval) body = self.show_share_server(server_id) server_status = body[status_attr] - if server_status == status: + if server_status in status: return elif constants.STATUS_ERROR in server_status.lower(): raise share_exceptions.ShareServerBuildErrorException( @@ -2107,3 +2107,90 @@ class SharesV2Client(shares_client.SharesClient): return body ############### + + def share_server_migration_check( + self, share_server_id, host, writable=False, + preserve_snapshots=False, nondisruptive=False, + new_share_network_id=None, version=LATEST_MICROVERSION): + body = { + 'migration_check': { + 'host': host, + 'writable': writable, + 'preserve_snapshots': preserve_snapshots, + 'nondisruptive': nondisruptive, + 'new_share_network_id': new_share_network_id, + } + } + + body = json.dumps(body) + resp, body = self.post('share-servers/%s/action' % share_server_id, + body, headers=EXPERIMENTAL, extra_headers=True, + version=version) + self.expected_success(200, resp.status) + + return json.loads(body) + + def share_server_migration_start(self, share_server_id, host, + writable=False, new_share_network_id=None, + preserve_snapshots=False, + nondisruptive=False, + version=LATEST_MICROVERSION): + body = { + 'migration_start': { + 'host': host, + 'writable': writable, + 'preserve_snapshots': preserve_snapshots, + 'nondisruptive': nondisruptive, + 'new_share_network_id': new_share_network_id, + } + } + + body = json.dumps(body) + resp, body = self.post('share-servers/%s/action' % share_server_id, + body, headers=EXPERIMENTAL, extra_headers=True, + version=version) + self.expected_success(202, resp.status) + + return body + + def share_server_migration_complete(self, share_server_id, + version=LATEST_MICROVERSION): + body = { + 'migration_complete': None + } + + body = json.dumps(body) + resp, body = self.post('share-servers/%s/action' % share_server_id, + body, headers=EXPERIMENTAL, extra_headers=True, + version=version) + self.expected_success(200, resp.status) + + return body + + def share_server_migration_cancel(self, share_server_id, + version=LATEST_MICROVERSION): + body = { + 'migration_cancel': None + } + + body = json.dumps(body) + resp, body = self.post('share-servers/%s/action' % share_server_id, + body, headers=EXPERIMENTAL, extra_headers=True, + version=version) + self.expected_success(202, resp.status) + + return body + + def share_server_migration_get_progress(self, share_server_id, + version=LATEST_MICROVERSION): + body = { + 'migration_get_progress': None + } + + body = json.dumps(body) + resp, body = self.post('share-servers/%s/action' % share_server_id, + body, headers=EXPERIMENTAL, extra_headers=True, + version=version) + self.expected_sucess(200, resp.status) + + return json.loads(body) diff --git a/manila_tempest_tests/share_exceptions.py b/manila_tempest_tests/share_exceptions.py index 67b4fca6..9466afeb 100644 --- a/manila_tempest_tests/share_exceptions.py +++ b/manila_tempest_tests/share_exceptions.py @@ -80,3 +80,8 @@ class ShareReplicationTypeException(exceptions.TempestException): class ShareServerBuildErrorException(exceptions.TempestException): message = ("Share server %(server_id)s failed to build and is in ERROR " "status") + + +class ShareServerMigrationException(exceptions.TempestException): + message = ("Share server %(server_id)s failed to migrate and is in ERROR " + "status") diff --git a/manila_tempest_tests/tests/api/admin/test_share_servers_migration.py b/manila_tempest_tests/tests/api/admin/test_share_servers_migration.py new file mode 100644 index 00000000..813cb76b --- /dev/null +++ b/manila_tempest_tests/tests/api/admin/test_share_servers_migration.py @@ -0,0 +1,407 @@ +# Copyright 2020 NetApp Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ddt +from tempest import config +from tempest.lib import decorators +from tempest.lib import exceptions +from testtools import testcase as tc + +from manila_tempest_tests.common import constants +from manila_tempest_tests.tests.api import base +from manila_tempest_tests import utils + +CONF = config.CONF + + +class MigrationShareServerBase(base.BaseSharesAdminTest): + protocol = None + + @classmethod + def skip_checks(cls): + super(MigrationShareServerBase, cls).skip_checks() + if cls.protocol not in CONF.share.enable_protocols: + raise cls.skipException('%s tests are disabled.' % cls.protocol) + if not CONF.share.multitenancy_enabled: + raise cls.skipException('Multitenancy tests are disabled.') + if not CONF.share.run_share_servers_migration_tests: + raise cls.skipException( + 'Share servers migration tests are disabled.') + utils.check_skip_if_microversion_lt('2.57') + + @classmethod + def resource_setup(cls): + super(MigrationShareServerBase, cls).resource_setup() + cls.all_hosts = cls.shares_v2_client.list_pools(detail=True) + cls.backends = set() + for pool in cls.all_hosts['pools']: + if pool['capabilities'].get('driver_handles_share_servers'): + cls.backends.add(pool['name'].split('#')[0]) + + if len(cls.backends) < 2: + msg = ("Could not find the necessary backends. At least two" + " are needed to run the tests of share server migration") + raise cls.skipException(msg) + + # create share type (generic) + cls.share_type = cls._create_share_type() + + def _setup_migration(self, share): + """Initial share server migration setup.""" + + share = self.shares_v2_client.get_share(share['id']) + server_id = share['share_server_id'] + + # (andrer) Verify if have at least one backend compatible with + # the specified share server. + dest_host, compatible = self._choose_matching_backend_for_share_server( + server_id) + + snapshot = False + if compatible['supported_capabilities']['preserve_snapshots']: + snapshot = self.create_snapshot_wait_for_active( + share['id'], cleanup_in_class=False)['id'] + + # (andrer) Check the share export locations. + old_exports = self.shares_v2_client.list_share_export_locations( + 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) + + # (andrer) Create the access rules, considering NFS and CIFS + # protocols. + access_rules = self._get_access_rule_data_for_protocols() + self.shares_v2_client.create_access_rule( + share['id'], access_type=access_rules[0].get('access_type'), + access_to=access_rules[0].get('access_to'), + access_level=access_rules[0].get('access_level')) + + self.shares_v2_client.wait_for_share_status( + share['id'], constants.RULE_STATE_ACTIVE, + status_attr='access_rules_status') + + if self.protocol == 'nfs': + self.shares_v2_client.create_access_rule( + share['id'], access_type=access_rules[1].get('access_type'), + access_to=access_rules[1].get('access_to'), + access_level=access_rules[1].get('access_level')) + + self.shares_v2_client.wait_for_share_status( + share['id'], constants.RULE_STATE_ACTIVE, + status_attr='access_rules_status') + + share = self.shares_v2_client.get_share(share['id']) + + return share, server_id, dest_host, snapshot + + def _validate_instances_states(self, share, instance_status, + snapshot_id): + """Validates the share and snapshot status.""" + statuses = ((instance_status,) + if not isinstance(instance_status, (tuple, list, set)) + else instance_status) + + share = self.shares_v2_client.get_share(share['id']) + self.assertIn(share['status'], statuses) + + if snapshot_id: + snapshot = self.shares_v2_client.get_snapshot(snapshot_id) + self.assertIn(snapshot['status'], statuses) + + def _validate_share_server_migration_complete( + self, share, dest_host, src_server_id, dest_server_id, + snapshot_id=None, share_network_id=None, + version=CONF.share.max_api_microversion): + """Validates the share server migration complete. """ + + # Check the export locations + 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) + + # Check the share host, share_network, share_server and status. + share = self.shares_v2_client.get_share(share['id']) + self.assertEqual(share['host'].split('#')[0], dest_host) + self.assertEqual(share_network_id, share['share_network_id']) + self.assertEqual(dest_server_id, share['share_server_id']) + self.assertEqual(share['status'], constants.STATUS_AVAILABLE) + + # Check the snapshot status if possible. + if snapshot_id: + self.shares_v2_client.wait_for_snapshot_status( + snapshot_id, constants.STATUS_AVAILABLE) + + # Check the share server destination status. + dest_server = self.shares_v2_client.show_share_server(dest_server_id) + self.assertIn(dest_server['task_state'], + constants.TASK_STATE_MIGRATION_SUCCESS) + + # Check if the access rules are in the share. + rules = self.shares_v2_client.list_access_rules(share['id']) + if self.protocol == 'cifs': + expected_rules = [{ + 'state': constants.RULE_STATE_ACTIVE, + 'access_to': CONF.share.username_for_user_rules, + 'access_type': 'user', + 'access_level': 'rw', + }] + elif self.protocol == 'nfs': + expected_rules = [{ + 'state': constants.RULE_STATE_ACTIVE, + 'access_to': '50.50.50.50', + 'access_type': 'ip', + 'access_level': 'rw', + }, { + 'state': constants.RULE_STATE_ACTIVE, + 'access_to': '51.51.51.51', + 'access_type': 'ip', + 'access_level': 'ro', + }] + + filtered_rules = [{'state': rule['state'], + 'access_to': rule['access_to'], + 'access_level': rule['access_level'], + 'access_type': rule['access_type']} + for rule in rules] + + for r in expected_rules: + self.assertIn(r, filtered_rules) + self.assertEqual(len(expected_rules), len(filtered_rules)) + + @classmethod + def _choose_matching_backend_for_share_server(self, server_id): + """Choose a compatible host for the share server migration.""" + for backend in self.backends: + # This try is necessary since if you try migrate the share server + # using the same backend and share network will raise an exception. + try: + compatibility = ( + self.admin_shares_v2_client.share_server_migration_check( + share_server_id=server_id, host=backend)) + except exceptions.Conflict or exceptions.ServerFault: + continue + if compatibility['compatible']: + return backend, compatibility + + raise self.skipException( + "Not found compatible host for the share server migration.") + + def _choose_incompatible_backend_for_share_server(self, server_id): + """Choose a not compatible host for the share server migration.""" + for backend in self.backends: + # This try is necessary since if you try migrate the share server + # using the same backend and share network will raise an exception. + try: + compatibility = ( + self.admin_shares_v2_client.share_server_migration_check( + share_server_id=server_id, host=backend)) + except exceptions.Conflict or exceptions.ServerFault: + continue + if not compatibility['compatible']: + return backend, compatibility + + raise self.skipException( + "Not found incompatible host for the share server migration.") + + def _get_share_server_destination_for_migration(self, src_server_id): + """Find the destination share server choosed for the migration.""" + params = {'source_share_server_id': src_server_id, + 'status': constants.STATUS_SERVER_MIGRATING_TO} + dest_server = self.admin_shares_v2_client.list_share_servers( + search_opts=params) + dest_server_id = dest_server[0]['id'] if dest_server else None + + return dest_server_id + + def _get_access_rule_data_for_protocols(self): + """Return fake data for access rules based on configured protocol.""" + if self.protocol == 'nfs': + return [{ + 'access_type': 'ip', + 'access_to': '50.50.50.50', + 'access_level': 'rw', + }, { + 'access_type': 'ip', + 'access_to': '51.51.51.51', + 'access_level': 'ro', + }] + elif self.protocol == 'cifs': + return [{ + 'access_type': 'user', + 'access_to': CONF.share.username_for_user_rules, + 'access_level': 'rw', + }] + else: + message = "Unrecognized protocol and access rules configuration" + raise self.skipException(message) + + +@ddt.ddt +class ShareServerMigrationBasicNFS(MigrationShareServerBase): + protocol = "nfs" + + @decorators.idempotent_id('5b84bcb6-17d8-4073-8e02-53b54aee6f8b') + @tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND) + def test_share_server_migration_cancel(self): + """Test the share server migration cancel.""" + share_network_id = self.provide_share_network( + self.shares_v2_client, self.networks_client) + share = self.create_share(share_protocol=self.protocol, + share_type_id=self.share_type['id'], + share_network_id=share_network_id, + cleanup_in_class=False) + share = self.shares_v2_client.get_share(share['id']) + + # Initial migration setup. + share, src_server_id, dest_host, snapshot_id = self._setup_migration( + share) + + preserve_snapshots = True if snapshot_id else False + + # Start share server migration. + self.shares_v2_client.share_server_migration_start( + src_server_id, dest_host, preserve_snapshots=preserve_snapshots) + + task_state = constants.TASK_STATE_MIGRATION_DRIVER_PHASE1_DONE + self.shares_v2_client.wait_for_share_server_status( + src_server_id, task_state, status_attr='task_state') + # Get for the destination share server. + dest_server_id = self._get_share_server_destination_for_migration( + src_server_id) + + dest_server = self.shares_v2_client.show_share_server(dest_server_id) + self.assertEqual(dest_host, dest_server['host']) + self.assertEqual(share_network_id, dest_server['share_network_id']) + + # Validate the share instances status. + share_status = constants.STATUS_SERVER_MIGRATING + self._validate_instances_states(share, share_status, snapshot_id) + + # Cancel the share server migration. + self.shares_v2_client.share_server_migration_cancel(src_server_id) + + # Wait for the migration cancelled status. + task_state = constants.TASK_STATE_MIGRATION_CANCELLED + self.shares_v2_client.wait_for_share_server_status( + src_server_id, task_state, status_attr='task_state') + + # After the cancel operation, we need to validate again the resources. + share_status = constants.STATUS_AVAILABLE + self._validate_instances_states(share, share_status, snapshot_id) + + @decorators.idempotent_id('99e439a8-a716-4205-bf5b-af50128cb908') + @tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND) + @ddt.data(False, True) + def test_share_server_migration_complete(self, new_share_network): + """Test the share server migration complete.""" + share_network_id = self.provide_share_network( + self.shares_v2_client, self.networks_client) + dest_share_network_id = share_network_id + if new_share_network: + src_share_network = self.shares_v2_client.get_share_network( + share_network_id) + share_net_info = ( + utils.share_network_get_default_subnet(src_share_network)) + dest_share_network_id = self.create_share_network( + neutron_net_id=share_net_info['neutron_net_id'], + neutron_subnet_id=share_net_info['neutron_subnet_id'], + cleanup_in_class=False)['id'] + + share = self.create_share(share_protocol=self.protocol, + share_type_id=self.share_type['id'], + share_network_id=share_network_id, + cleanup_in_class=False) + share = self.shares_v2_client.get_share(share['id']) + + # Initial migration setup. + share, src_server_id, dest_host, snapshot_id = self._setup_migration( + share) + + preserve_snapshots = True if snapshot_id else False + + # Start share server migration. + self.shares_v2_client.share_server_migration_start( + src_server_id, dest_host, + new_share_network_id=dest_share_network_id, + preserve_snapshots=preserve_snapshots) + + task_state = constants.TASK_STATE_MIGRATION_DRIVER_PHASE1_DONE + self.shares_v2_client.wait_for_share_server_status( + src_server_id, task_state, status_attr='task_state') + # Get for the destination share server. + dest_server_id = self._get_share_server_destination_for_migration( + src_server_id) + + dest_server = self.shares_v2_client.show_share_server(dest_server_id) + self.assertEqual(dest_host, dest_server['host']) + self.assertEqual(dest_share_network_id, + dest_server['share_network_id']) + + share_status = constants.STATUS_SERVER_MIGRATING + self._validate_instances_states(share, share_status, snapshot_id) + + # Share server migration complete. + self.shares_v2_client.share_server_migration_complete(src_server_id) + + # It's necessary wait for the migration success state and + # active status. + task_state = [constants.TASK_STATE_MIGRATION_SUCCESS, + constants.SERVER_STATE_INACTIVE] + self.shares_v2_client.wait_for_share_server_status( + src_server_id, task_state, status_attr='task_state') + + # Validate the share server migration complete. + share = self.shares_v2_client.get_share(share['id']) + self._validate_share_server_migration_complete( + share, dest_host, src_server_id, dest_server_id, + snapshot_id=snapshot_id, share_network_id=dest_share_network_id) + + @decorators.idempotent_id('52e154eb-2d39-45af-b5c1-49ea569ab804') + @tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND) + @ddt.data(True, False) + def test_share_server_migration_check(self, compatible): + """The share server migration check compatibility tests.""" + share = self.create_share(share_protocol=self.protocol, + share_type_id=self.share_type['id'], + cleanup_in_class=False) + share = self.shares_v2_client.get_share(share['id']) + # Find a backend compatible or not for the share server + # check compatibility operation. + if compatible: + dest_host, result = self._choose_matching_backend_for_share_server( + server_id=share['share_server_id']) + self.assertTrue(result['compatible']) + self.assertEqual(result['requested_capabilities']['host'], + dest_host) + else: + dest_host, result = ( + self._choose_incompatible_backend_for_share_server( + server_id=share['share_server_id'])) + if dest_host is None: + raise self.skipException( + "Not found any compatible destination host for the share " + "server %s." % share['share_server_id']) + self.assertFalse(result['compatible']) + self.assertEqual(result['requested_capabilities'].get('host'), + dest_host) + + +class ShareServerMigrationBasicCIFS(ShareServerMigrationBasicNFS): + protocol = "cifs" diff --git a/manila_tempest_tests/tests/api/admin/test_share_servers_migration_negative.py b/manila_tempest_tests/tests/api/admin/test_share_servers_migration_negative.py new file mode 100644 index 00000000..9924f820 --- /dev/null +++ b/manila_tempest_tests/tests/api/admin/test_share_servers_migration_negative.py @@ -0,0 +1,335 @@ +# Copyright 2020 NetApp Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest import config +from tempest.lib.common.utils import data_utils +from tempest.lib import decorators +from tempest.lib import exceptions as lib_exc +from testtools import testcase as tc + + +from manila_tempest_tests.common import constants +from manila_tempest_tests.tests.api.admin import test_share_servers_migration +from manila_tempest_tests.tests.api import base + +CONF = config.CONF + + +class MigrationShareServerNegative( + test_share_servers_migration.MigrationShareServerBase): + protocool = None + + @classmethod + def _setup_migration(self, cleanup_in_class=True): + """Setup migration for negative tests.""" + extra_specs = { + 'driver_handles_share_servers': CONF.share.multitenancy_enabled} + if CONF.share.capability_snapshot_support: + extra_specs['snapshot_support'] = True + share_type = self.create_share_type( + name=data_utils.rand_name("tempest-share-type"), + extra_specs=extra_specs, + cleanup_in_class=cleanup_in_class) + share = self.create_share(share_protocol=self.protocol, + share_type_id=share_type['share_type']['id'], + cleanup_in_class=cleanup_in_class) + share = self.shares_v2_client.get_share(share['id']) + share_server_id = share['share_server_id'] + dest_host, compatible = self._choose_matching_backend_for_share_server( + share_server_id) + + return share, share_server_id, dest_host + + +class ShareServerMigrationInvalidParametersNFS(MigrationShareServerNegative): + """Tests related to share server not found.""" + protocol = "nfs" + + @classmethod + def resource_setup(cls): + super(ShareServerMigrationInvalidParametersNFS, cls).resource_setup() + cls.share = cls.create_share( + share_protocol=cls.protocol, + share_type_id=cls.share_type['id']) + cls.share = cls.shares_v2_client.get_share(cls.share['id']) + cls.share_server_id = cls.share['share_server_id'] + cls.fake_server_id = 'fake_server_id' + cls.fake_host = 'fake_host@fake_backend' + + @decorators.idempotent_id('1be6ec2a-3118-4033-9cdb-ea6d199d97f4') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_server_migration_check(self): + """Not found share server in migration check.""" + self.assertRaises(lib_exc.NotFound, + self.shares_v2_client.share_server_migration_check, + self.fake_server_id, + self.fake_host) + + @decorators.idempotent_id('2aeffcfa-4e68-40e4-8a75-03b017503501') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_server_migration_cancel(self): + """Not found share server in migration cancel.""" + self.assertRaises(lib_exc.NotFound, + self.shares_v2_client.share_server_migration_cancel, + self.fake_server_id) + + @decorators.idempotent_id('52d23980-80e7-40de-8dba-1bb1382ef995') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_server_migration_start(self): + """Not found share server in migration start.""" + self.assertRaises(lib_exc.NotFound, + self.shares_v2_client.share_server_migration_start, + self.fake_server_id, + self.fake_host) + + @decorators.idempotent_id('47795631-eb50-424b-9fac-d2ee832cd01c') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_server_migration_get_progress(self): + """Not found share server in migration get progress.""" + self.assertRaises( + lib_exc.BadRequest, + self.shares_v2_client.share_server_migration_get_progress, + self.fake_server_id) + + @decorators.idempotent_id('3b464298-a4e4-417b-92d6-acfbd30ac45b') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_server_migration_complete(self): + """Not found share server in migration """ + self.assertRaises( + lib_exc.NotFound, + self.shares_v2_client.share_server_migration_complete, + self.fake_server_id) + + @decorators.idempotent_id('2d25cf84-0b5c-4a9f-ae20-9bec09bb6914') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_host_migration_start(self): + """Not found share server in migration start.""" + self.assertRaises(lib_exc.NotFound, + self.shares_v2_client.share_server_migration_start, + self.share_server_id, + self.fake_host) + + @decorators.idempotent_id('e7e2c19c-a0ed-41ab-b666-b2beae4a690c') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_host_migration_check(self): + """Not found share server in migration check.""" + self.assertRaises(lib_exc.NotFound, + self.shares_v2_client.share_server_migration_check, + self.share_server_id, + self.fake_host) + + +class ShareServerErrorStatusOperationNFS(MigrationShareServerNegative): + protocol = "nfs" + + @classmethod + def resource_setup(cls): + super(ShareServerErrorStatusOperationNFS, cls).resource_setup() + cls.share = cls.create_share( + share_protocol=cls.protocol, + share_type_id=cls.share_type['id']) + cls.share = cls.shares_v2_client.get_share(cls.share['id']) + cls.share_server_id = cls.share['share_server_id'] + cls.shares_v2_client.share_server_reset_state( + cls.share_server_id, status=constants.STATUS_ERROR) + cls.fake_host = 'fake_host@fake_backend' + + @decorators.idempotent_id('1f8d75c1-aa3c-465a-b2dd-9ad33933944f') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_operation_migration_check(self): + """Share server migration check invalid operation.""" + self.assertRaises(lib_exc.Conflict, + self.shares_v2_client.share_server_migration_check, + self.share_server_id, + self.fake_host) + + @decorators.idempotent_id('c256c5f5-b4d1-47b7-a1f4-af21f19ce600') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_operation_migration_start(self): + """Share server migration start invalid operation.""" + self.assertRaises(lib_exc.Conflict, + self.shares_v2_client.share_server_migration_start, + self.share_server_id, + self.fake_host) + + @decorators.idempotent_id('d2830fe4-8d13-40d2-b987-18d414bb6196') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_operation_migration_get_progress(self): + """Share server migration get progress invalid operation.""" + self.assertRaises( + lib_exc.BadRequest, + self.shares_v2_client.share_server_migration_get_progress, + self.share_server_id, + self.fake_host) + + @decorators.idempotent_id('245f39d7-bcbc-4711-afd7-651a5535a880') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_operation_migration_cancel(self): + """Share server migration cancel invalid operation.""" + self.assertRaises(lib_exc.BadRequest, + self.shares_v2_client.share_server_migration_cancel, + self.share_server_id, + self.fake_host) + + @decorators.idempotent_id('3db45440-2c70-4fa4-b5eb-75e3cb0204f8') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_invalid_operation_migration_complete(self): + """Share server migration complete invalid operation.""" + self.assertRaises( + lib_exc.BadRequest, + self.shares_v2_client.share_server_migration_complete, + self.share_server_id, + self.fake_host) + + +class ShareServerMigrationStartNegativesNFS(MigrationShareServerNegative): + protocol = "nfs" + + @classmethod + def resource_setup(cls): + super(ShareServerMigrationStartNegativesNFS, cls).resource_setup() + cls.share, cls.server_id, cls.dest_host = cls._setup_migration() + cls.shares_v2_client.share_server_migration_start( + cls.server_id, cls.dest_host) + + @classmethod + def resource_cleanup(cls): + states = [constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, + constants.TASK_STATE_MIGRATION_DRIVER_PHASE1_DONE] + cls.shares_v2_client.wait_for_share_server_status( + cls.server_id, status=states, status_attr="task_state") + cls.shares_v2_client.share_server_migration_cancel(cls.server_id) + cls.shares_v2_client.wait_for_share_status(cls.share['id'], + status="available") + super(ShareServerMigrationStartNegativesNFS, cls).resource_cleanup() + + @decorators.idempotent_id('5b904db3-fc36-4c35-a8ef-cf6b80315388') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_migration_start_try_create_snapshot(self): + """Try create snap during a server migration.""" + if not CONF.share.capability_snapshot_support: + raise self.skipException( + 'Snapshot tests are disabled or unsupported.') + self.assertRaises( + lib_exc.BadRequest, + self.shares_v2_client.create_snapshot, + self.share['id'] + ) + + @decorators.idempotent_id('93882b54-78d4-4c4e-95b5-993de0cdb25d') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_migration_start_try_create_access_rule(self): + """Try create access rule during a server migration.""" + self.assertRaises( + lib_exc.BadRequest, + self.shares_v2_client.create_access_rule, + self.share['id'] + ) + + @decorators.idempotent_id('7c74a4a8-61b2-4c55-bc4b-02eac73d2c6e') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_migration_start_try_delete_share_network(self): + """Try delete share network during a server migration.""" + self.assertRaises( + lib_exc.Conflict, + self.shares_v2_client.delete_share_network, + self.share['share_network_id'] + ) + + +class ShareServerMigrationStartInvalidStatesNFS(MigrationShareServerNegative): + protocol = "nfs" + + @decorators.idempotent_id('bcec0503-b2a9-4514-bf3f-a30d55f41e78') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_migration_start_invalid_network(self): + """Try server migration start with invalid network.""" + share, share_server_id, dest_host = self._setup_migration( + cleanup_in_class=False) + share_network = self.create_share_network(cleanup_in_class=False) + + self.assertRaises( + lib_exc.ServerFault, + self.shares_v2_client.share_server_migration_start, + share_server_id, + dest_host, + new_share_network_id=share_network) + + @decorators.idempotent_id('11374277-efcf-4992-ad94-c8f4a393d41b') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_migration_start_invalid_share_state(self): + """Try server migration start with invalid share state.""" + share, share_server_id, dest_host = self._setup_migration( + cleanup_in_class=False) + self.shares_v2_client.reset_state(share['id']) + + self.assertRaises( + lib_exc.Conflict, + self.shares_v2_client.share_server_migration_start, + share_server_id, + dest_host + ) + + @decorators.idempotent_id('ebe8da5b-ee9c-48c7-a7e4-9e71839f813f') + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + def test_share_server_migration_start_with_share_replica(self): + """Try server migration start with share replica.""" + if not CONF.share.backend_replication_type or ( + not CONF.share.run_replication_tests): + raise self.skipException( + 'Share replica tests are disabled or unsupported.') + extra_specs = { + 'driver_handles_share_servers': CONF.share.multitenancy_enabled, + 'replication_type': CONF.share.backend_replication_type + } + share_type = self.shares_v2_client.create_share_type( + name=data_utils.rand_name("tempest-share-type"), + extra_specs=extra_specs, + cleanup_in_class=False) + share = self.create_share(share_type_id=share_type['share_type']['id'], + share_protocol=self.protocol, + cleanup_in_class=False) + share = self.shares_v2_client.get_share(share['id']) + share_server_id = share['share_server_id'] + dest_host, _ = self._choose_matching_backend_for_share_server( + share_server_id) + self.create_share_replica( + share['id'], + cleanup_in_class=False) + self.assertRaises( + lib_exc.Conflict, + self.shares_v2_client.share_server_migration_start, + share_server_id, + dest_host + ) + + +class ShareServerMigrationInvalidParametersCIFS( + ShareServerMigrationInvalidParametersNFS): + protocol = "cifs" + + +class ShareServerErrorStatusOperationCIFS(ShareServerErrorStatusOperationNFS): + protocol = "cifs" + + +class ShareServerMigrationStartNegativesCIFS( + ShareServerMigrationStartNegativesNFS): + protocol = "cifs" + + +class ShareServerMigrationInvalidStatesCIFS( + ShareServerMigrationStartInvalidStatesNFS): + protocol = "cifs" diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py index 6bdc8e08..fe4616ee 100755 --- a/manila_tempest_tests/tests/api/base.py +++ b/manila_tempest_tests/tests/api/base.py @@ -677,8 +677,9 @@ class BaseSharesTest(test.BaseTestCase): return rep_domain, pools_in_rep_domain @classmethod - def create_share_replica(cls, share_id, availability_zone, client=None, - cleanup_in_class=False, cleanup=True, + def create_share_replica(cls, share_id, availability_zone=None, + client=None, cleanup_in_class=False, + cleanup=True, version=CONF.share.max_api_microversion): client = client or cls.shares_v2_client replica = client.create_share_replica( diff --git a/zuul.d/manila-tempest-jobs.yaml b/zuul.d/manila-tempest-jobs.yaml index 1dfe3f8b..cb7c0809 100644 --- a/zuul.d/manila-tempest-jobs.yaml +++ b/zuul.d/manila-tempest-jobs.yaml @@ -45,6 +45,7 @@ MANILA_DEFAULT_SHARE_TYPE_EXTRA_SPECS: 'snapshot_support=True create_share_from_snapshot_support=True' MANILA_CONFIGURE_DEFAULT_TYPES: true MANILA_SHARE_MIGRATION_PERIOD_TASK_INTERVAL: 1 + MANILA_SERVER_MIGRATION_PERIOD_TASK_INTERVAL: 10 MANILA_REPLICA_STATE_UPDATE_INTERVAL: 10 @@ -106,6 +107,7 @@ MANILA_INSTALL_TEMPEST_PLUGIN_SYSTEMWIDE: false MANILA_SERVICE_IMAGE_ENABLED: false MANILA_SHARE_MIGRATION_PERIOD_TASK_INTERVAL: 1 + MANILA_SERVER_MIGRATION_PERIOD_TASK_INTERVAL: 10 MANILA_REPLICA_STATE_UPDATE_INTERVAL: 10 devstack_services: tls-proxy: true @@ -241,6 +243,7 @@ multitenancy_enabled: true backend_names: LONDON,PARIS multi_backend: true + run_share_servers_migration_tests: true - job: name: manila-tempest-plugin-generic @@ -480,6 +483,7 @@ run_mount_snapshot_tests: true run_replication_tests: true run_revert_to_snapshot_tests: true + run_share_servers_migration_tests: true - job: name: manila-tempest-plugin-glusterfs-native