From 746fb7e2df142ba490dc5457ac6a0f47c1d5cc8e Mon Sep 17 00:00:00 2001 From: Kiran Pawar Date: Sun, 10 Oct 2021 08:06:23 +0000 Subject: [PATCH] OnlyHostFilter allows user to specify host during share create. e.g. manila create NFS 1 --name Share1 --share-network net1 \ --scheduler_hint="only_host=host1@generic1#GENERIC1" Since there is no way to create share server in manila, we can use a workaround of creating first share on specific host (e.g. host@backend#pool). This will then create the share server automatically on that host and admin can use idle host when other hosts are overloaded. New microversion 2.67 introduced. DocImpact Closes-Bug: #1946462 Change-Id: I603434cac246e2c0946672d3f0fe469ed5423fa4 --- api-ref/source/parameters.yaml | 7 ++- manila/api/openstack/api_version_request.py | 7 ++- .../openstack/rest_api_version_history.rst | 6 ++ manila/api/v2/shares.py | 3 + manila/scheduler/drivers/filter.py | 52 +++++++-------- manila/scheduler/filters/host.py | 33 ++++++++++ manila/scheduler/host_manager.py | 1 + manila/share/api.py | 13 ++-- manila/tests/scheduler/filters/test_host.py | 63 +++++++++++++++++++ manila/tests/share/test_api.py | 13 ++-- .../hostonly-filter-1a17a70dd0aafb86.yaml | 10 +++ setup.cfg | 1 + 12 files changed, 167 insertions(+), 42 deletions(-) create mode 100644 manila/scheduler/filters/host.py create mode 100644 manila/tests/scheduler/filters/test_host.py create mode 100644 releasenotes/notes/hostonly-filter-1a17a70dd0aafb86.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index aebf1db924..faae2cb54c 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -2323,9 +2323,10 @@ revert_to_snapshot_support_share_capability: min_version: 2.27 scheduler_hints: description: | - One or more scheduler_hints key and value pairs as a dictionary - of strings. e.g. keys are same_host, different_host and values must be - a comma separated list of Share IDs. + One or more scheduler_hints key and value pairs as a dictionary of + strings. Accepted hints are: + - ``same_host`` or ``different_host``: values must be a comma separated list of Share IDs + - ``only_host``: value must be a manage-share service host in ``host@backend#POOL`` format (admin only) in: body required: false type: object diff --git a/manila/api/openstack/api_version_request.py b/manila/api/openstack/api_version_request.py index 876ba4ccf9..cdf64cf5de 100644 --- a/manila/api/openstack/api_version_request.py +++ b/manila/api/openstack/api_version_request.py @@ -170,15 +170,18 @@ REST_API_VERSION_HISTORY = """ actions on the share network's endpoint: 'update_security_service', 'update_security_service_check' and 'add_security_service_check'. - * 2.65 - Added ability to set scheduler hints via the share create API. + * 2.65 - Added ability to set affinity scheduler hints via the share + create API. * 2.66 - Added filter search by group spec for share group type list. + * 2.67 - Added ability to set 'only_host' scheduler hint via the share + create API. """ # The minimum and maximum versions of the API supported # The default api version request is defined to be the # minimum version of the API supported. _MIN_API_VERSION = "2.0" -_MAX_API_VERSION = "2.66" +_MAX_API_VERSION = "2.67" DEFAULT_API_VERSION = _MIN_API_VERSION diff --git a/manila/api/openstack/rest_api_version_history.rst b/manila/api/openstack/rest_api_version_history.rst index be56611565..eec55b98b6 100644 --- a/manila/api/openstack/rest_api_version_history.rst +++ b/manila/api/openstack/rest_api_version_history.rst @@ -367,3 +367,9 @@ user documentation. 2.66 ---- Added filter search by group spec for share group type list. + +2.67 +____ + Added supprot for 'only_host' key in "scheduler_hints" in the request body + of the POST/shares request. This hint will invoke OnlyHost scheduler + filter during share creation. diff --git a/manila/api/v2/shares.py b/manila/api/v2/shares.py index bc00524f8f..a35e3f318b 100644 --- a/manila/api/v2/shares.py +++ b/manila/api/v2/shares.py @@ -186,6 +186,9 @@ class ShareController(shares.ShareMixin, share = body['share'] scheduler_hints = share.pop('scheduler_hints', None) + if req.api_version_request < api_version.APIVersionRequest("2.67"): + if scheduler_hints: + scheduler_hints.pop('only_host', None) return self._create(req, body, check_create_share_from_snapshot_support=True, check_availability_zones_extra_spec=True, diff --git a/manila/scheduler/drivers/filter.py b/manila/scheduler/drivers/filter.py index 334da6166d..1402cf3002 100644 --- a/manila/scheduler/drivers/filter.py +++ b/manila/scheduler/drivers/filter.py @@ -23,7 +23,6 @@ filters and weighing functions. from oslo_config import cfg from oslo_log import log -from manila.db import api as db_api from manila import exception from manila.i18n import _ from manila.message import api as message_api @@ -322,15 +321,29 @@ class FilterScheduler(base.Scheduler): "exc": "exc" }) - def populate_filter_properties_share_scheduler_hint(self, context, - share_id, hints, - key, hint): - try: - result = db_api.share_metadata_get_item(context, share_id, key) - except exception.ShareMetadataNotFound: - pass + def _populate_scheduler_hint(self, request_spec, hints, key, hint): + share_properties = request_spec.get('share_properties', {}) + value = share_properties.get('metadata', {}).get(key, None) + if value: + hints.update({hint: value}) + + def populate_filter_properties_scheduler_hints(self, context, request_spec, + filter_properties): + share_id = request_spec.get('share_id', None) + if not share_id: + filter_properties['scheduler_hints'] = {} + return else: - hints.update({hint: result.get(key)}) + if filter_properties.get('scheduler_hints', None): + return + hints = {} + self._populate_scheduler_hint(request_spec, hints, + AFFINITY_KEY, + AFFINITY_HINT) + self._populate_scheduler_hint(request_spec, hints, + ANTI_AFFINITY_KEY, + ANTI_AFFINITY_HINT) + filter_properties['scheduler_hints'] = hints def populate_filter_properties_share(self, context, request_spec, filter_properties): @@ -347,25 +360,8 @@ class FilterScheduler(base.Scheduler): filter_properties['user_id'] = shr.get('user_id') filter_properties['metadata'] = shr.get('metadata') filter_properties['snapshot_id'] = shr.get('snapshot_id') - - share_id = request_spec.get('share_id', None) - if not share_id: - filter_properties['scheduler_hints'] = {} - return - - try: - db_api.share_get(context, share_id) - except exception.NotFound: - filter_properties['scheduler_hints'] = {} - else: - hints = {} - self.populate_filter_properties_share_scheduler_hint( - context, share_id, hints, - AFFINITY_KEY, AFFINITY_HINT) - self.populate_filter_properties_share_scheduler_hint( - context, share_id, hints, - ANTI_AFFINITY_KEY, ANTI_AFFINITY_HINT) - filter_properties['scheduler_hints'] = hints + self.populate_filter_properties_scheduler_hints(context, request_spec, + filter_properties) def schedule_create_share_group(self, context, share_group_id, request_spec, filter_properties): diff --git a/manila/scheduler/filters/host.py b/manila/scheduler/filters/host.py new file mode 100644 index 0000000000..ba54e90d96 --- /dev/null +++ b/manila/scheduler/filters/host.py @@ -0,0 +1,33 @@ +# Copyright 2021 Cloudification GmbH. +# 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 manila.scheduler.filters import base_host + + +class OnlyHostFilter(base_host.BaseHostFilter): + """Filters Hosts by 'only_host' scheduler_hint.""" + + def host_passes(self, host_state, filter_properties): + context = filter_properties['context'] + if not context.is_admin: + return True + hints = filter_properties.get('scheduler_hints') + if hints is None: + return True + requested_host = hints.get('only_host', None) + if requested_host is None: + return True + # e.g. "only_host=hostname@generic2#GENERIC2" + return host_state.host == requested_host diff --git a/manila/scheduler/host_manager.py b/manila/scheduler/host_manager.py index 6a719838eb..4a24666d1f 100644 --- a/manila/scheduler/host_manager.py +++ b/manila/scheduler/host_manager.py @@ -42,6 +42,7 @@ from manila import utils host_manager_opts = [ cfg.ListOpt('scheduler_default_filters', default=[ + 'OnlyHostFilter', 'AvailabilityZoneFilter', 'CapacityFilter', 'CapabilitiesFilter', diff --git a/manila/share/api.py b/manila/share/api.py index 754c4cc8f3..8726b0a1b2 100644 --- a/manila/share/api.py +++ b/manila/share/api.py @@ -884,7 +884,8 @@ class API(base.Base): export_location_path) request_spec = self._get_request_spec_dict( - share, share_type, size=0, share_proto=share_data['share_proto'], + share, share_type, context, size=0, + share_proto=share_data['share_proto'], host=share_data['host']) # NOTE(ganso): Scheduler is called to validate if share type @@ -895,7 +896,7 @@ class API(base.Base): return self.db.share_get(context, share['id']) - def _get_request_spec_dict(self, share, share_type, **kwargs): + def _get_request_spec_dict(self, share, share_type, context, **kwargs): if share is None: share = {'instance': {}} @@ -908,6 +909,8 @@ class API(base.Base): 'size': kwargs.get('size', share.get('size')), 'user_id': kwargs.get('user_id', share.get('user_id')), 'project_id': kwargs.get('project_id', share.get('project_id')), + 'metadata': self.db.share_metadata_get( + context, share_instance.get('share_id')), 'snapshot_support': kwargs.get( 'snapshot_support', share_type.get('extra_specs', {}).get('snapshot_support') @@ -1548,6 +1551,7 @@ class API(base.Base): request_spec = self._get_request_spec_dict( share, share_type, + context, availability_zone_id=service['availability_zone_id'], share_network_id=new_share_network_id) @@ -2323,7 +2327,8 @@ class API(base.Base): else: share_type = share_types.get_share_type( context, share['instance']['share_type_id']) - request_spec = self._get_request_spec_dict(share, share_type) + request_spec = self._get_request_spec_dict(share, share_type, + context) self.scheduler_rpcapi.extend_share(context, share['id'], new_size, reservations, request_spec) LOG.info("Extend share request issued successfully.", @@ -2474,7 +2479,7 @@ class API(base.Base): share_type_id = share_instance['share_type_id'] share_type = share_types.get_share_type(context, share_type_id) req_spec = self._get_request_spec_dict(share_instance, - share_type, + share_type, context, **kwargs) shares_req_spec.append(req_spec) diff --git a/manila/tests/scheduler/filters/test_host.py b/manila/tests/scheduler/filters/test_host.py new file mode 100644 index 0000000000..4ea537c7c5 --- /dev/null +++ b/manila/tests/scheduler/filters/test_host.py @@ -0,0 +1,63 @@ +# Copyright 2021 Cloudification GmbH. +# 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 manila import context +from manila.scheduler.filters import host +from manila import test +from manila.tests.scheduler import fakes + + +fake_host1 = fakes.FakeHostState('host1', {}) +fake_host2 = fakes.FakeHostState('host2', {}) + + +@ddt.ddt +class OnlyHostFilterTestCase(test.TestCase): + """Test case for OnlyHostFilter.""" + + def setUp(self): + super(OnlyHostFilterTestCase, self).setUp() + self.filter = host.OnlyHostFilter() + self.user_context = context.RequestContext('user', 'project') + self.admin_context = context.RequestContext('user', 'project', + is_admin=True) + + def _make_filter_properties(self, hint): + return { + 'context': self.admin_context, + 'scheduler_hints': hint, + } + + @ddt.data((fake_host1, {'scheduler_hints': None}), + (fake_host1, {'scheduler_hints': {}}), + (fake_host1, + {'scheduler_hints': {'only_host': fake_host2.host}})) + @ddt.unpack + def test_only_host_filter_user_context(self, host, filter_properties): + context = {'context': self.user_context} + filter_properties.update(context) + self.assertTrue(self.filter.host_passes(host, filter_properties)) + + @ddt.data((fake_host1, None, True), + (fake_host1, {}, True), + (fake_host1, {'only_host': fake_host1.host}, True), + (fake_host2, {'only_host': fake_host1.host}, False)) + @ddt.unpack + def test_only_host_filter_admin_context(self, host, hint, host_passes): + filter_properties = self._make_filter_properties(hint) + self.assertEqual(host_passes, + self.filter.host_passes(host, filter_properties)) diff --git a/manila/tests/share/test_api.py b/manila/tests/share/test_api.py index c7a301111a..6fe193e6d6 100644 --- a/manila/tests/share/test_api.py +++ b/manila/tests/share/test_api.py @@ -1170,7 +1170,8 @@ class ShareAPITestCase(test.TestCase): }) expected_request_spec = self._get_request_spec_dict( - share, fake_type, size=0, share_proto=share_data['share_proto'], + share, fake_type, self.context, size=0, + share_proto=share_data['share_proto'], host=share_data['host']) if dhss: @@ -1378,7 +1379,7 @@ class ShareAPITestCase(test.TestCase): self.assertRaises(exception.InvalidShare, self.api.manage, self.context, share_data, driver_options) - def _get_request_spec_dict(self, share, share_type, **kwargs): + def _get_request_spec_dict(self, share, share_type, context, **kwargs): if share is None: share = {'instance': {}} @@ -1389,6 +1390,7 @@ class ShareAPITestCase(test.TestCase): 'size': kwargs.get('size', share.get('size')), 'user_id': kwargs.get('user_id', share.get('user_id')), 'project_id': kwargs.get('project_id', share.get('project_id')), + 'metadata': db_api.share_metadata_get(context, share['id']), 'snapshot_support': kwargs.get( 'snapshot_support', share_type['extra_specs']['snapshot_support']), @@ -3314,7 +3316,8 @@ class ShareAPITestCase(test.TestCase): share_network_id=share_network_id) request_spec = self._get_request_spec_dict( - share, fake_type_2, size=0, availability_zone_id='fake_az_id', + share, fake_type_2, self.context, size=0, + availability_zone_id='fake_az_id', share_network_id=share_network_id) self.mock_object(self.scheduler_rpcapi, 'migrate_share_to_host') @@ -3551,7 +3554,7 @@ class ShareAPITestCase(test.TestCase): share_type_id=fake_type['id']) request_spec = self._get_request_spec_dict( - share, fake_type, availability_zone_id='fake_az_id') + share, fake_type, self.context, availability_zone_id='fake_az_id') self.api.migration_start(self.context, share, host, False, True, True, True, True) @@ -5093,7 +5096,7 @@ class ShareAPITestCase(test.TestCase): get_type_calls.append( mock.call(self.context, instance['share_type_id'])) get_request_spec_calls.append( - mock.call(instance, fake_share_type)) + mock.call(instance, fake_share_type, self.context)) mock_get_type = self.mock_object( share_types, 'get_share_type', diff --git a/releasenotes/notes/hostonly-filter-1a17a70dd0aafb86.yaml b/releasenotes/notes/hostonly-filter-1a17a70dd0aafb86.yaml new file mode 100644 index 0000000000..d119b251a0 --- /dev/null +++ b/releasenotes/notes/hostonly-filter-1a17a70dd0aafb86.yaml @@ -0,0 +1,10 @@ +--- +features: + - Add OnlyHostFilter to manila's scheduler. This filter needs admin to + specify host@backend#pool to "share.scheduler_hints.only_host" in the + request payload when creating a manila share. The hint is used only + for share creation and not stored as share metadata. For non-admin users + the OnlyHostFilter will always be ignored. +upgrade: + - To add OnlyHostFilter to an active deployment, its reference must be + enabled in manila.conf. diff --git a/setup.cfg b/setup.cfg index c717388169..7960d91dfc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ wsgi_scripts = manila.scheduler.filters = AffinityFilter = manila.scheduler.filters.affinity:AffinityFilter AntiAffinityFilter = manila.scheduler.filters.affinity:AntiAffinityFilter + OnlyHostFilter = manila.scheduler.filters.host:OnlyHostFilter AvailabilityZoneFilter = manila.scheduler.filters.availability_zone:AvailabilityZoneFilter CapabilitiesFilter = manila.scheduler.filters.capabilities:CapabilitiesFilter CapacityFilter = manila.scheduler.filters.capacity:CapacityFilter