From fd6314dfbdc6cd076bdb6099f558f481b9adbac2 Mon Sep 17 00:00:00 2001 From: Goutham Pacha Ravi Date: Mon, 7 Jan 2019 01:45:44 -0800 Subject: [PATCH] Add tests for export location changes in APIv 2.47 - Share export locations API from version 2.47 will not supply export locations of non-active secondary replicas. - New APIs GET /share-replicas/{share_replica_id}/export-locations and /share-replicas/{share_replica_id}/export-locations/{export_id} provide replica export locations at tenant level by virtue of default policy and provide necessary information for tenant consumption. Depends-On: https://review.openstack.org/#/c/628069/ Change-Id: I64dd04c9fa8a429e568e219aac175d43c8c57ec7 Implements: bp export-locations-az --- manila_tempest_tests/config.py | 2 +- .../services/share/v2/json/shares_client.py | 20 ++ .../api/test_replication_export_locations.py | 240 ++++++++++++++++++ ...t_replication_export_locations_negative.py | 110 ++++++++ 4 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 manila_tempest_tests/tests/api/test_replication_export_locations.py create mode 100644 manila_tempest_tests/tests/api/test_replication_export_locations_negative.py diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py index c24a50ed..b49fd324 100644 --- a/manila_tempest_tests/config.py +++ b/manila_tempest_tests/config.py @@ -30,7 +30,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.46", + default="2.47", help="The maximum api microversion is configured to be the " "value of the latest microversion supported by Manila."), cfg.StrOpt("region", 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 a5a816af..32c863b6 100644 --- a/manila_tempest_tests/services/share/v2/json/shares_client.py +++ b/manila_tempest_tests/services/share/v2/json/shares_client.py @@ -1525,6 +1525,26 @@ class SharesV2Client(shares_client.SharesClient): self.expected_success(expected_status, resp.status) return self._parse_resp(body) + def list_share_replica_export_locations(self, replica_id, + expected_status=200, + version=LATEST_MICROVERSION): + uri = "share-replicas/%s/export-locations" % replica_id + resp, body = self.get(uri, headers=EXPERIMENTAL, + extra_headers=True, version=version) + self.expected_success(expected_status, resp.status) + return self._parse_resp(body) + + def get_share_replica_export_location(self, replica_id, + export_location_id, + expected_status=200, + version=LATEST_MICROVERSION): + uri = "share-replicas/%s/export-locations/%s" % (replica_id, + export_location_id) + resp, body = self.get(uri, headers=EXPERIMENTAL, + extra_headers=True, version=version) + self.expected_success(expected_status, resp.status) + return self._parse_resp(body) + def wait_for_share_replica_status(self, replica_id, expected_status, status_attr='status'): """Waits for a replica's status_attr to reach a given status.""" diff --git a/manila_tempest_tests/tests/api/test_replication_export_locations.py b/manila_tempest_tests/tests/api/test_replication_export_locations.py new file mode 100644 index 00000000..4313edb6 --- /dev/null +++ b/manila_tempest_tests/tests/api/test_replication_export_locations.py @@ -0,0 +1,240 @@ +# 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.common.utils import data_utils +import testtools +from testtools import testcase as tc + +from manila_tempest_tests.common import constants +from manila_tempest_tests import share_exceptions +from manila_tempest_tests.tests.api import base +from manila_tempest_tests import utils + +CONF = config.CONF +LATEST_MICROVERSION = CONF.share.max_api_microversion + + +@testtools.skipUnless(CONF.share.run_replication_tests, + 'Replication tests are disabled.') +@ddt.ddt +class ReplicationExportLocationsTest(base.BaseSharesMixedTest): + + @classmethod + def resource_setup(cls): + super(ReplicationExportLocationsTest, cls).resource_setup() + # Create share_type + name = data_utils.rand_name(constants.TEMPEST_MANILA_PREFIX) + cls.admin_client = cls.admin_shares_v2_client + cls.replication_type = CONF.share.backend_replication_type + + if cls.replication_type not in constants.REPLICATION_TYPE_CHOICES: + raise share_exceptions.ShareReplicationTypeException( + replication_type=cls.replication_type + ) + cls.extra_specs = cls.add_extra_specs_to_dict( + {"replication_type": cls.replication_type}) + share_type = cls.create_share_type( + name, + extra_specs=cls.extra_specs, + client=cls.admin_client) + cls.share_type = share_type["share_type"] + cls.zones = cls.get_availability_zones_matching_share_type( + cls.share_type) + cls.share_zone = cls.zones[0] + cls.replica_zone = cls.zones[-1] + + @staticmethod + def _remove_admin_only_exports(all_exports): + return [e for e in all_exports if not e['is_admin_only']] + + def _create_share_and_replica_get_exports(self, cleanup_replica=True): + share = self.create_share(share_type_id=self.share_type['id'], + availability_zone=self.share_zone) + replica = self.create_share_replica(share['id'], self.replica_zone, + cleanup=cleanup_replica) + replicas = self.shares_v2_client.list_share_replicas( + share_id=share['id']) + primary_replica = [r for r in replicas if r['id'] != replica['id']][0] + + # Refresh share and replica + share = self.shares_v2_client.get_share(share['id']) + replica = self.shares_v2_client.get_share_replica(replica['id']) + + # Grab export locations of the share instances using admin API + replica_exports = self._remove_admin_only_exports( + self.admin_client.list_share_instance_export_locations( + replica['id'])) + primary_replica_exports = self._remove_admin_only_exports( + self.admin_client.list_share_instance_export_locations( + primary_replica['id'])) + + return share, replica, primary_replica_exports, replica_exports + + def _validate_export_location_api_behavior(self, replica, replica_exports, + primary_replica_exports, + share_exports, version): + share_export_paths = [e['path'] for e in share_exports] + + # Expectations + expected_number_of_exports = len(primary_replica_exports + + replica_exports) + expected_exports = replica_exports + primary_replica_exports + # In and beyond version 2.47, secondary "non-active" replica exports + # are not expected to be present in the share export locations. + # Secondary replicas can be "active" only in in "writable" + # replication. In other types of replication, secondary replicas are + # either "in_sync" or "out_of_sync" + replica_is_non_active = (replica['replica_state'] != + constants.REPLICATION_STATE_ACTIVE) + if utils.is_microversion_ge(version, '2.47') and replica_is_non_active: + expected_number_of_exports = len(primary_replica_exports) + expected_exports = primary_replica_exports + + # Assertions + self.assertEqual(expected_number_of_exports, len(share_exports)) + for export in expected_exports: + self.assertIn(export['path'], share_export_paths) + + @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND) + @ddt.data(*set(['2.46', '2.47', LATEST_MICROVERSION])) + def test_replicated_share_export_locations(self, version): + """Test behavior changes in the share export locations API at 2.47""" + self.skip_if_microversion_not_supported(version) + share, replica, primary_replica_exports, replica_exports = ( + self._create_share_and_replica_get_exports() + ) + + # Share export locations list API + share_exports = self.shares_v2_client.list_share_export_locations( + share['id'], version=version) + + self._validate_export_location_api_behavior(replica, replica_exports, + primary_replica_exports, + share_exports, version) + + @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND) + @ddt.data(*set(['2.46', '2.47', LATEST_MICROVERSION])) + @testtools.skipUnless( + CONF.share.backend_replication_type in + (constants.REPLICATION_STYLE_READABLE, constants.REPLICATION_STYLE_DR), + 'Promotion of secondary not supported in writable replication style.') + def test_replicated_share_export_locations_with_promotion(self, version): + self.skip_if_microversion_not_supported(version) + share, replica, primary_replica_exports, replica_exports = ( + self._create_share_and_replica_get_exports(cleanup_replica=False) + ) + primary_replica = self.shares_v2_client.get_share_replica( + primary_replica_exports[0]['share_instance_id']) + self.shares_v2_client.wait_for_share_replica_status( + replica['id'], constants.REPLICATION_STATE_IN_SYNC, + status_attr='replica_state') + + # Share export locations list API + share_exports = self.shares_v2_client.list_share_export_locations( + share['id'], version=version) + + # Validate API behavior + self._validate_export_location_api_behavior(replica, replica_exports, + primary_replica_exports, + share_exports, version) + + # Promote share replica + self.promote_share_replica(replica['id']) + + # Refresh for verification + current_secondary_replica = self.shares_v2_client.get_share_replica( + primary_replica['id']) + current_primary_replica_exports = self._remove_admin_only_exports( + self.admin_client.list_share_instance_export_locations( + replica['id'], version=version)) + current_secondary_replica_exports = self._remove_admin_only_exports( + self.admin_client.list_share_instance_export_locations( + primary_replica['id'], version=version)) + share_exports = self.shares_v2_client.list_share_export_locations( + share['id'], version=version) + + # Validate API behavior + self._validate_export_location_api_behavior( + current_secondary_replica, current_secondary_replica_exports, + current_primary_replica_exports, share_exports, version) + + # Delete the secondary (the 'active' replica before promotion) + self.delete_share_replica(primary_replica['id']) + + @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND) + @utils.skip_if_microversion_not_supported('2.47') + def test_replica_export_locations(self): + """Validates exports from the replica export locations APIs""" + el_summary_keys = ['id', 'path', 'replica_state', + 'availability_zone', 'preferred'] + el_detail_keys = el_summary_keys + ['created_at', 'updated_at'] + share, replica, expected_primary_exports, expected_replica_exports = ( + self._create_share_and_replica_get_exports() + ) + primary_replica = self.shares_v2_client.get_share_replica( + expected_primary_exports[0]['share_instance_id']) + expected_primary_export_paths = [e['path'] for e in + expected_primary_exports] + expected_replica_export_paths = [e['path'] for e in + expected_replica_exports] + + # For the primary replica + actual_primary_exports = ( + self.shares_v2_client.list_share_replica_export_locations( + primary_replica['id']) + ) + + self.assertEqual(len(expected_primary_exports), + len(actual_primary_exports)) + for export in actual_primary_exports: + self.assertIn(export['path'], expected_primary_export_paths) + self.assertEqual(constants.REPLICATION_STATE_ACTIVE, + export['replica_state']) + self.assertEqual(share['availability_zone'], + export['availability_zone']) + self.assertEqual(sorted(el_summary_keys), sorted(export.keys())) + + export_location_details = ( + self.shares_v2_client.get_share_replica_export_location( + primary_replica['id'], export['id']) + ) + self.assertEqual(sorted(el_detail_keys), + sorted(export_location_details.keys())) + for key in el_summary_keys: + self.assertEqual(export[key], export_location_details[key]) + + # For the secondary replica + actual_replica_exports = ( + self.shares_v2_client.list_share_replica_export_locations( + replica['id']) + ) + + self.assertEqual(len(expected_replica_exports), + len(actual_replica_exports)) + for export in actual_replica_exports: + self.assertIn(export['path'], expected_replica_export_paths) + self.assertEqual(replica['replica_state'], + export['replica_state']) + self.assertEqual(replica['availability_zone'], + export['availability_zone']) + self.assertEqual(sorted(el_summary_keys), sorted(export.keys())) + + export_location_details = ( + self.shares_v2_client.get_share_replica_export_location( + replica['id'], export['id']) + ) + self.assertEqual(sorted(el_detail_keys), + sorted(export_location_details.keys())) + for key in el_summary_keys: + self.assertEqual(export[key], export_location_details[key]) diff --git a/manila_tempest_tests/tests/api/test_replication_export_locations_negative.py b/manila_tempest_tests/tests/api/test_replication_export_locations_negative.py new file mode 100644 index 00000000..af2b1b3e --- /dev/null +++ b/manila_tempest_tests/tests/api/test_replication_export_locations_negative.py @@ -0,0 +1,110 @@ +# 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 exceptions as lib_exc +import testtools +from testtools import testcase as tc + +from manila_tempest_tests.common import constants +from manila_tempest_tests import share_exceptions +from manila_tempest_tests.tests.api import base +from manila_tempest_tests import utils + +CONF = config.CONF + + +@testtools.skipUnless(CONF.share.run_replication_tests, + 'Replication tests are disabled.') +class ReplicationExportLocationsNegativeTest(base.BaseSharesMixedTest): + + @classmethod + def resource_setup(cls): + super(ReplicationExportLocationsNegativeTest, cls).resource_setup() + # Create share_type + name = data_utils.rand_name(constants.TEMPEST_MANILA_PREFIX) + cls.admin_client = cls.admin_shares_v2_client + cls.replication_type = CONF.share.backend_replication_type + + if cls.replication_type not in constants.REPLICATION_TYPE_CHOICES: + raise share_exceptions.ShareReplicationTypeException( + replication_type=cls.replication_type + ) + cls.extra_specs = cls.add_extra_specs_to_dict( + {"replication_type": cls.replication_type}) + share_type = cls.create_share_type( + name, + extra_specs=cls.extra_specs, + client=cls.admin_client) + cls.share_type = share_type["share_type"] + cls.zones = cls.get_availability_zones_matching_share_type( + cls.share_type) + cls.share_zone = cls.zones[0] + cls.replica_zone = cls.zones[-1] + + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + @utils.skip_if_microversion_not_supported('2.47') + @testtools.skipUnless( + CONF.share.backend_replication_type in + (constants.REPLICATION_STYLE_READABLE, constants.REPLICATION_STYLE_DR), + 'Test is not appropriate for writable replication style.') + def test_get_share_export_location_for_secondary_replica(self): + """Is NotFound raised with share el API for non-active replicas""" + share = self.create_share(share_type_id=self.share_type['id'], + availability_zone=self.share_zone) + replica = self.create_share_replica(share['id'], self.replica_zone) + replica_exports = ( + self.shares_v2_client.list_share_replica_export_locations( + replica['id']) + ) + + for export in replica_exports: + self.assertRaises(lib_exc.NotFound, + self.shares_v2_client.get_share_export_location, + share['id'], export['id']) + + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + @utils.skip_if_microversion_not_supported('2.47') + def test_get_replica_export_location_for_non_replica(self): + """Is NotFound raised for non-replica share instances""" + # Create a share type with no support for replication + share_type = self._create_share_type() + share = self.create_share(share_type_id=share_type['id'], + availability_zone=self.share_zone) + share_instances = self.admin_client.get_instances_of_share(share['id']) + for instance in share_instances: + self.assertRaises( + lib_exc.NotFound, + self.shares_v2_client.list_share_replica_export_locations, + instance['id']) + + @tc.attr(base.TAG_NEGATIVE, base.TAG_API) + @utils.skip_if_microversion_not_supported('2.47') + def test_list_replica_export_locations_for_invalid_replica(self): + """Is NotFound raised for invalid replica ID""" + self.assertRaises( + lib_exc.NotFound, + self.shares_v2_client.list_share_replica_export_locations, + 'invalid-replica-id') + + @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND) + @utils.skip_if_microversion_not_supported('2.47') + def test_get_replica_export_location_for_invalid_export_id(self): + """Is NotFound raised for invalid replica export location ID""" + share = self.create_share(share_type_id=self.share_type['id'], + availability_zone=self.share_zone) + replica = self.create_share_replica(share['id'], self.replica_zone) + self.assertRaises( + lib_exc.NotFound, + self.shares_v2_client.get_share_replica_export_location, + replica['id'], 'invalid-export-location-id')