diff --git a/manila/scheduler/drivers/filter.py b/manila/scheduler/drivers/filter.py index fbc45c2b92..b8e8e3503f 100644 --- a/manila/scheduler/drivers/filter.py +++ b/manila/scheduler/drivers/filter.py @@ -181,6 +181,18 @@ class FilterScheduler(base.Scheduler): if cg_host: cg_support = cg_host.consistency_group_support + # NOTE(gouthamr): If 'active_replica_host' is present in the request + # spec, pass that host's 'replication_domain' to the + # ShareReplication filter. + active_replica_host = request_spec.get('active_replica_host') + replication_domain = None + if active_replica_host: + temp_hosts = self.host_manager.get_all_host_states_share(elevated) + ar_host = next((host for host in temp_hosts + if host.host == active_replica_host), None) + if ar_host: + replication_domain = ar_host.replication_domain + if filter_properties is None: filter_properties = {} self._populate_retry_share(filter_properties, resource_properties) @@ -192,6 +204,7 @@ class FilterScheduler(base.Scheduler): 'resource_type': resource_type, 'cg_support': cg_support, 'consistency_group': cg, + 'replication_domain': replication_domain, }) self.populate_filter_properties_share(request_spec, filter_properties) diff --git a/manila/scheduler/filters/share_replication.py b/manila/scheduler/filters/share_replication.py new file mode 100644 index 0000000000..039f8b1795 --- /dev/null +++ b/manila/scheduler/filters/share_replication.py @@ -0,0 +1,75 @@ +# Copyright (c) 2016 Goutham Pacha Ravi +# 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 oslo_log import log + +from manila.scheduler.filters import base_host + +LOG = log.getLogger(__name__) + + +class ShareReplicationFilter(base_host.BaseHostFilter): + """ShareReplicationFilter filters hosts based on replication support.""" + + def host_passes(self, host_state, filter_properties): + """Return True if 'active' replica's host can replicate with host. + + Design of this filter: + - Share replication is symmetric. All backends that can + replicate between each other must share the same + 'replication_domain'. + - For scheduling a share that can be replicated in the future, + this filter checks for 'replication_domain' capability. + - For scheduling a replica, it checks for the + 'replication_domain' compatibility. + """ + active_replica_host = filter_properties.get('request_spec', {}).get( + 'active_replica_host') + replication_type = filter_properties.get('resource_type', {}).get( + 'extra_specs', {}).get('replication_type') + active_replica_replication_domain = filter_properties.get( + 'replication_domain') + host_replication_domain = host_state.replication_domain + + if replication_type is None: + # NOTE(gouthamr): You're probably not creating a replicated + # share or a replica, then this host obviously passes. Also, + # avoid creating a replica on the same host. + return True + elif host_replication_domain is None: + msg = "Replication is not enabled on host %s." + LOG.debug(msg, host_state.host) + return False + elif active_replica_host is None: + # 'replication_type' filtering will be handled by the + # capabilities filter, since it is a share-type extra-spec. + return True + + # Scheduler filtering by replication_domain for a replica + if active_replica_replication_domain != host_replication_domain: + msg = ("The replication domain of Host %(host)s is " + "'%(host_domain)s' and it does not match the replication " + "domain of the 'active' replica's host: " + "%(active_replica_host)s, which is '%(arh_domain)s'. ") + kwargs = { + "host": host_state.host, + "host_domain": host_replication_domain, + "active_replica_host": active_replica_host, + "arh_domain": active_replica_replication_domain, + } + LOG.debug(msg, kwargs) + return False + + return True diff --git a/manila/scheduler/host_manager.py b/manila/scheduler/host_manager.py index c4339d59fb..51ffacd8a2 100644 --- a/manila/scheduler/host_manager.py +++ b/manila/scheduler/host_manager.py @@ -46,6 +46,7 @@ host_manager_opts = [ 'CapacityFilter', 'CapabilitiesFilter', 'ConsistencyGroupFilter', + 'ShareReplicationFilter', ], help='Which filter class names to use for filtering hosts ' 'when not specified in the request.'), @@ -128,6 +129,7 @@ class HostState(object): self.dedupe = False self.compression = False self.replication_type = None + self.replication_domain = None # PoolState for all pools self.pools = {} @@ -296,6 +298,9 @@ class HostState(object): if not pool_cap.get('replication_type'): pool_cap['replication_type'] = self.replication_type + if not pool_cap.get('replication_domain'): + pool_cap['replication_domain'] = self.replication_domain + def update_backend(self, capability): self.share_backend_name = capability.get('share_backend_name') self.vendor_name = capability.get('vendor_name') @@ -308,6 +313,7 @@ class HostState(object): 'consistency_group_support', False) self.updated = capability['timestamp'] self.replication_type = capability.get('replication_type') + self.replication_domain = capability.get('replication_domain') def consume_from_share(self, share): """Incrementally update host state from an share.""" @@ -372,6 +378,8 @@ class PoolState(HostState): 'compression', False) self.replication_type = capability.get( 'replication_type', self.replication_type) + self.replication_domain = capability.get( + 'replication_domain') def update_pools(self, capability): # Do nothing, since we don't have pools within pool, yet diff --git a/manila/share/api.py b/manila/share/api.py index 93add1725f..53f93ceb56 100644 --- a/manila/share/api.py +++ b/manila/share/api.py @@ -375,8 +375,10 @@ class API(base.Base): self._check_is_share_busy(share) - if not self.db.share_replicas_get_available_active_replica( - context, share['id']): + active_replica = self.db.share_replicas_get_available_active_replica( + context, share['id']) + + if not active_replica: msg = _("Share %s does not have any active replica in available " "state.") raise exception.ReplicationException(reason=msg % share['id']) @@ -386,6 +388,8 @@ class API(base.Base): context, share, availability_zone=availability_zone, share_network_id=share_network_id)) + request_spec['active_replica_host'] = active_replica['host'] + self.db.share_replica_update( context, share_replica['id'], {'replica_state': constants.REPLICA_STATE_OUT_OF_SYNC}) diff --git a/manila/share/driver.py b/manila/share/driver.py index 4832339bb3..557c9e9253 100644 --- a/manila/share/driver.py +++ b/manila/share/driver.py @@ -117,6 +117,16 @@ share_opts = [ "define network plugin config options in some separate config " "group and set its name here. Used only with another " "option 'driver_handles_share_servers' set to 'True'."), + # Replication option/s + cfg.StrOpt( + "replication_domain", + default=None, + help="A string specifying the replication domain that the backend " + "belongs to. This option needs to be specified the same in the " + "configuration sections of all backends that support " + "replication between each other. If this option is not " + "specified in the group, it means that replication is not " + "enabled on the backend."), ] ssh_opts = [ @@ -275,6 +285,12 @@ class ShareDriver(object): return self.configuration.safe_get('driver_handles_share_servers') return CONF.driver_handles_share_servers + @property + def replication_domain(self): + if self.configuration: + return self.configuration.safe_get('replication_domain') + return CONF.replication_domain + def _verify_share_server_handling(self, driver_handles_share_servers): """Verifies driver_handles_share_servers and given configuration.""" if not isinstance(self.driver_handles_share_servers, bool): @@ -899,6 +915,7 @@ class ShareDriver(object): qos=False, pools=self.pools or None, snapshot_support=self.snapshots_are_supported, + replication_domain=self.replication_domain, ) if isinstance(data, dict): common.update(data) diff --git a/manila/share/utils.py b/manila/share/utils.py index 4e69e41b68..9ad66875db 100644 --- a/manila/share/utils.py +++ b/manila/share/utils.py @@ -32,8 +32,8 @@ def extract_host(host, level='backend', use_default_pool_name=False): :param host: String for host, which could include host@backend#pool info :param level: Indicate which level of information should be extracted - from host string. Level can be 'host', 'backend' or 'pool', - default value is 'backend' + from host string. Level can be 'host', 'backend', 'pool', + or 'backend_name', default value is 'backend' :param use_default_pool_name: This flag specifies what to do if level == 'pool' and there is no 'pool' info encoded in host string. default_pool_name=True @@ -50,7 +50,8 @@ def extract_host(host, level='backend', use_default_pool_name=False): # ret is 'HostA@BackendB' ret = extract_host(host, 'pool') # ret is 'PoolC' - + ret = extract_host(host, 'backend_name') + # ret is 'BackendB' host = 'HostX@BackendY' ret = extract_host(host, 'pool') # ret is None @@ -61,6 +62,9 @@ def extract_host(host, level='backend', use_default_pool_name=False): # Make sure pool is not included hst = host.split('#')[0] return hst.split('@')[0] + if level == 'backend_name': + hst = host.split('#')[0] + return hst.split('@')[1] elif level == 'backend': return host.split('#')[0] elif level == 'pool': diff --git a/manila/tests/scheduler/drivers/test_filter.py b/manila/tests/scheduler/drivers/test_filter.py index 44e0566537..0a62630116 100644 --- a/manila/tests/scheduler/drivers/test_filter.py +++ b/manila/tests/scheduler/drivers/test_filter.py @@ -39,6 +39,27 @@ class FilterSchedulerTestCase(test_base.SchedulerTestCase): driver_cls = filter.FilterScheduler + def test___format_filter_properties_active_replica_host_is_provided(self): + sched = fakes.FakeFilterScheduler() + fake_context = context.RequestContext('user', 'project') + request_spec = { + 'share_properties': {'project_id': 1, 'size': 1}, + 'share_instance_properties': {}, + 'share_type': {'name': 'NFS'}, + 'share_id': ['fake-id1'], + 'active_replica_host': 'fake_ar_host', + } + hosts = [fakes.FakeHostState(host, {'replication_domain': 'xyzzy'}) + for host in ('fake_ar_host', 'fake_host_2')] + self.mock_object(sched.host_manager, 'get_all_host_states_share', + mock.Mock(return_value=hosts)) + self.mock_object(sched, 'populate_filter_properties_share') + + retval = sched._format_filter_properties( + fake_context, {}, request_spec) + + self.assertTrue('replication_domain' in retval[0]) + def test_create_share_no_hosts(self): # Ensure empty hosts/child_zones result in NoValidHosts exception. sched = fakes.FakeFilterScheduler() diff --git a/manila/tests/scheduler/fakes.py b/manila/tests/scheduler/fakes.py index 4a85d28efe..12c339aa24 100644 --- a/manila/tests/scheduler/fakes.py +++ b/manila/tests/scheduler/fakes.py @@ -193,6 +193,7 @@ class FakeHostManager(host_manager.HostManager): 'timestamp': None, 'snapshot_support': True, 'replication_type': 'writable', + 'replication_domain': 'endor', }, 'host2': {'total_capacity_gb': 2048, 'free_capacity_gb': 300, @@ -204,6 +205,7 @@ class FakeHostManager(host_manager.HostManager): 'timestamp': None, 'snapshot_support': True, 'replication_type': 'readable', + 'replication_domain': 'kashyyyk', }, 'host3': {'total_capacity_gb': 512, 'free_capacity_gb': 256, @@ -226,6 +228,7 @@ class FakeHostManager(host_manager.HostManager): 'timestamp': None, 'snapshot_support': True, 'replication_type': 'dr', + 'replication_domain': 'naboo', }, 'host5': {'total_capacity_gb': 2048, 'free_capacity_gb': 500, @@ -256,6 +259,10 @@ class FakeHostState(host_manager.HostState): for (key, val) in attribute_dict.items(): setattr(self, key, val) +FAKE_HOST_STRING_1 = 'openstack@BackendA#PoolX' +FAKE_HOST_STRING_2 = 'openstack@BackendB#PoolY' +FAKE_HOST_STRING_3 = 'openstack@BackendC#PoolZ' + def mock_host_manager_db_calls(mock_obj, disabled=None): services = [ diff --git a/manila/tests/scheduler/filters/test_share_replication.py b/manila/tests/scheduler/filters/test_share_replication.py new file mode 100644 index 0000000000..c25eef3c6e --- /dev/null +++ b/manila/tests/scheduler/filters/test_share_replication.py @@ -0,0 +1,116 @@ +# 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. + +""" +Tests for the ShareReplicationFilter. +""" +import ddt + +from oslo_context import context + +from manila.scheduler.filters import share_replication +from manila import test +from manila.tests.scheduler import fakes + + +@ddt.ddt +class ShareReplicationFilterTestCase(test.TestCase): + """Test case for ShareReplicationFilter.""" + + def setUp(self): + super(ShareReplicationFilterTestCase, self).setUp() + self.filter = share_replication.ShareReplicationFilter() + self.debug_log = self.mock_object(share_replication.LOG, 'debug') + + @staticmethod + def _create_replica_request(replication_domain='kashyyyk', + replication_type='dr', + active_replica_host=fakes.FAKE_HOST_STRING_1, + is_admin=False): + ctxt = context.RequestContext('fake', 'fake', is_admin=is_admin) + return { + 'context': ctxt, + 'request_spec': { + 'active_replica_host': active_replica_host, + }, + 'resource_type': { + 'extra_specs': { + 'replication_type': replication_type, + }, + }, + 'replication_domain': replication_domain, + } + + @ddt.data('tatooine', '') + def test_share_replication_filter_fails_incompatible_domain(self, domain): + request = self._create_replica_request() + + host = fakes.FakeHostState('host1', + { + 'replication_domain': domain, + }) + + self.assertFalse(self.filter.host_passes(host, request)) + self.assertTrue(self.debug_log.called) + + def test_share_replication_filter_fails_no_replication_domain(self): + request = self._create_replica_request() + + host = fakes.FakeHostState('host1', + { + 'replication_domain': None, + }) + + self.assertFalse(self.filter.host_passes(host, request)) + self.assertTrue(self.debug_log.called) + + def test_share_replication_filter_passes_no_replication_type(self): + request = self._create_replica_request(replication_type=None) + + host = fakes.FakeHostState('host1', + { + 'replication_domain': 'tatooine', + }) + + self.assertTrue(self.filter.host_passes(host, request)) + + def test_share_replication_filter_passes_no_active_replica_host(self): + request = self._create_replica_request(active_replica_host=None) + + host = fakes.FakeHostState('host1', + { + 'replication_domain': 'tatooine', + }) + + self.assertTrue(self.filter.host_passes(host, request)) + + def test_share_replication_filter_passes_happy_day(self): + request = self._create_replica_request() + + host = fakes.FakeHostState('host1', + { + 'replication_domain': 'kashyyyk', + }) + + self.assertTrue(self.filter.host_passes(host, request)) + + def test_share_replication_filter_empty(self): + request = {} + + host = fakes.FakeHostState('host1', + { + 'replication_domain': 'naboo', + }) + + self.assertTrue(self.filter.host_passes(host, request)) diff --git a/manila/tests/scheduler/test_host_manager.py b/manila/tests/scheduler/test_host_manager.py index d61af2de8a..37a1f6fc42 100644 --- a/manila/tests/scheduler/test_host_manager.py +++ b/manila/tests/scheduler/test_host_manager.py @@ -200,6 +200,7 @@ class HostManagerTestCase(test.TestCase): 'dedupe': False, 'compression': False, 'replication_type': None, + 'replication_domain': None, }, }, { 'name': 'host2@back1#BBB', @@ -224,6 +225,7 @@ class HostManagerTestCase(test.TestCase): 'dedupe': False, 'compression': False, 'replication_type': None, + 'replication_domain': None, }, }, { 'name': 'host2@back2#CCC', @@ -248,6 +250,7 @@ class HostManagerTestCase(test.TestCase): 'dedupe': False, 'compression': False, 'replication_type': None, + 'replication_domain': None, }, }, ] @@ -294,6 +297,7 @@ class HostManagerTestCase(test.TestCase): 'dedupe': False, 'compression': False, 'replication_type': None, + 'replication_domain': None, }, }, { 'name': 'host2@BBB#pool2', @@ -319,6 +323,7 @@ class HostManagerTestCase(test.TestCase): 'dedupe': False, 'compression': False, 'replication_type': None, + 'replication_domain': None, }, }, { 'name': 'host3@CCC#pool3', @@ -344,6 +349,7 @@ class HostManagerTestCase(test.TestCase): 'dedupe': False, 'compression': False, 'replication_type': None, + 'replication_domain': None, }, }, { 'name': 'host4@DDD#pool4a', @@ -369,6 +375,7 @@ class HostManagerTestCase(test.TestCase): 'dedupe': False, 'compression': False, 'replication_type': None, + 'replication_domain': None, }, }, { 'name': 'host4@DDD#pool4b', @@ -394,6 +401,7 @@ class HostManagerTestCase(test.TestCase): 'dedupe': False, 'compression': False, 'replication_type': None, + 'replication_domain': None, }, }, ] @@ -452,6 +460,7 @@ class HostManagerTestCase(test.TestCase): 'dedupe': False, 'compression': False, 'replication_type': None, + 'replication_domain': None, }, }, { 'name': 'host2@back1#BBB', @@ -476,6 +485,7 @@ class HostManagerTestCase(test.TestCase): 'dedupe': False, 'compression': False, 'replication_type': None, + 'replication_domain': None, }, }, ] @@ -526,6 +536,7 @@ class HostManagerTestCase(test.TestCase): 'dedupe': False, 'compression': False, 'replication_type': None, + 'replication_domain': None, }, }, ] diff --git a/manila/tests/share/drivers/emc/test_driver.py b/manila/tests/share/drivers/emc/test_driver.py index 4e86e4b707..b2f68dcd60 100644 --- a/manila/tests/share/drivers/emc/test_driver.py +++ b/manila/tests/share/drivers/emc/test_driver.py @@ -124,6 +124,7 @@ class EMCShareFrameworkTestCase(test.TestCase): data['qos'] = False data['pools'] = None data['snapshot_support'] = True + data['replication_domain'] = None self.assertEqual(data, self.driver._stats) def _fake_safe_get(self, value): diff --git a/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py b/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py index b603d7ec62..8a43fca869 100644 --- a/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py +++ b/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py @@ -520,6 +520,7 @@ class HPE3ParDriverTestCase(test.TestCase): 'total_capacity_gb': 0, 'vendor_name': 'HPE', 'pools': None, + 'replication_domain': None, } result = self.driver.get_share_stats(refresh=True) @@ -577,6 +578,7 @@ class HPE3ParDriverTestCase(test.TestCase): 'hpe3par_flash_cache': False, 'hp3par_flash_cache': False, 'snapshot_support': True, + 'replication_domain': None, } result = self.driver.get_share_stats(refresh=True) @@ -610,6 +612,7 @@ class HPE3ParDriverTestCase(test.TestCase): 'total_capacity_gb': 0, 'vendor_name': 'HPE', 'snapshot_support': True, + 'replication_domain': None, } result = self.driver.get_share_stats(refresh=True) diff --git a/manila/tests/share/drivers/huawei/test_huawei_nas.py b/manila/tests/share/drivers/huawei/test_huawei_nas.py index e6d59db198..2a1e68a7f8 100644 --- a/manila/tests/share/drivers/huawei/test_huawei_nas.py +++ b/manila/tests/share/drivers/huawei/test_huawei_nas.py @@ -718,6 +718,7 @@ class HuaweiShareDriverTestCase(test.TestCase): self.configuration.huawei_share_backend = 'V3' self.configuration.max_over_subscription_ratio = 1 self.configuration.driver_handles_share_servers = False + self.configuration.replication_domain = None self.tmp_dir = tempfile.mkdtemp() self.fake_conf_file = self.tmp_dir + '/manila_huawei_conf.xml' @@ -2011,6 +2012,7 @@ class HuaweiShareDriverTestCase(test.TestCase): expected['free_capacity_gb'] = 0.0 expected['qos'] = True expected["snapshot_support"] = True + expected['replication_domain'] = None expected["pools"] = [] pool = dict( pool_name='OpenStack_Pool', diff --git a/manila/tests/share/drivers/test_glusterfs_native.py b/manila/tests/share/drivers/test_glusterfs_native.py index 2c497301f0..731f06ae92 100644 --- a/manila/tests/share/drivers/test_glusterfs_native.py +++ b/manila/tests/share/drivers/test_glusterfs_native.py @@ -325,6 +325,7 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase): 'free_capacity_gb': 'unknown', 'pools': None, 'snapshot_support': True, + 'replication_domain': None, } self.assertEqual(test_data, self._driver._stats) diff --git a/manila/tests/share/test_api.py b/manila/tests/share/test_api.py index 29ad76f394..0d250bc49b 100644 --- a/manila/tests/share/test_api.py +++ b/manila/tests/share/test_api.py @@ -1835,7 +1835,7 @@ class ShareAPITestCase(test.TestCase): fake_replica = fakes.fake_replica(replica['id']) fake_request_spec = fakes.fake_replica_request_spec() self.mock_object(db_api, 'share_replicas_get_available_active_replica', - mock.Mock(return_value='FAKE_ACTIVE_REPLICA')) + mock.Mock(return_value={'host': 'fake_ar_host'})) self.mock_object( share_api.API, '_create_share_instance_and_get_request_spec', mock.Mock(return_value=(fake_request_spec, fake_replica))) diff --git a/manila/tests/share/test_share_utils.py b/manila/tests/share/test_share_utils.py index b3e216ecf9..2071a5a7df 100644 --- a/manila/tests/share/test_share_utils.py +++ b/manila/tests/share/test_share_utils.py @@ -52,6 +52,18 @@ class ShareUtilsTestCase(test.TestCase): self.assertEqual( 'Host', share_utils.extract_host(host)) + def test_extract_host_only_return_backend_name(self): + host = 'Host@Backend#Pool' + self.assertEqual( + 'Backend', share_utils.extract_host(host, 'backend_name')) + + def test_extract_host_only_return_backend_name_index_error(self): + host = 'Host#Pool' + + self.assertRaises(IndexError, + share_utils.extract_host, + host, 'backend_name') + def test_extract_host_missing_backend(self): host = 'Host#Pool' self.assertEqual( diff --git a/setup.cfg b/setup.cfg index d5ba6062cb..7ada079508 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ manila.scheduler.filters = IgnoreAttemptedHostsFilter = manila.scheduler.filters.ignore_attempted_hosts:IgnoreAttemptedHostsFilter JsonFilter = manila.scheduler.filters.json:JsonFilter RetryFilter = manila.scheduler.filters.retry:RetryFilter + ShareReplicationFilter = manila.scheduler.filters.share_replication:ShareReplicationFilter manila.scheduler.weighers = CapacityWeigher = manila.scheduler.weighers.capacity:CapacityWeigher PoolWeigher = manila.scheduler.weighers.pool:PoolWeigher