Merge "OnlyHostFilter allows user to specify host during share create."

This commit is contained in:
Zuul 2022-01-14 04:38:59 +00:00 committed by Gerrit Code Review
commit d1509256da
12 changed files with 167 additions and 42 deletions

View File

@ -2323,9 +2323,10 @@ revert_to_snapshot_support_share_capability:
min_version: 2.27 min_version: 2.27
scheduler_hints: scheduler_hints:
description: | description: |
One or more scheduler_hints key and value pairs as a dictionary One or more scheduler_hints key and value pairs as a dictionary of
of strings. e.g. keys are same_host, different_host and values must be strings. Accepted hints are:
a comma separated list of Share IDs. - ``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 in: body
required: false required: false
type: object type: object

View File

@ -170,15 +170,18 @@ REST_API_VERSION_HISTORY = """
actions on the share network's endpoint: actions on the share network's endpoint:
'update_security_service', 'update_security_service_check' and 'update_security_service', 'update_security_service_check' and
'add_security_service_check'. '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.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 minimum and maximum versions of the API supported
# The default api version request is defined to be the # The default api version request is defined to be the
# minimum version of the API supported. # minimum version of the API supported.
_MIN_API_VERSION = "2.0" _MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.66" _MAX_API_VERSION = "2.67"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -367,3 +367,9 @@ user documentation.
2.66 2.66
---- ----
Added filter search by group spec for share group type list. 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.

View File

@ -186,6 +186,9 @@ class ShareController(shares.ShareMixin,
share = body['share'] share = body['share']
scheduler_hints = share.pop('scheduler_hints', None) 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, return self._create(req, body,
check_create_share_from_snapshot_support=True, check_create_share_from_snapshot_support=True,
check_availability_zones_extra_spec=True, check_availability_zones_extra_spec=True,

View File

@ -23,7 +23,6 @@ filters and weighing functions.
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from manila.db import api as db_api
from manila import exception from manila import exception
from manila.i18n import _ from manila.i18n import _
from manila.message import api as message_api from manila.message import api as message_api
@ -322,15 +321,29 @@ class FilterScheduler(base.Scheduler):
"exc": "exc" "exc": "exc"
}) })
def populate_filter_properties_share_scheduler_hint(self, context, def _populate_scheduler_hint(self, request_spec, hints, key, hint):
share_id, hints, share_properties = request_spec.get('share_properties', {})
key, hint): value = share_properties.get('metadata', {}).get(key, None)
try: if value:
result = db_api.share_metadata_get_item(context, share_id, key) hints.update({hint: value})
except exception.ShareMetadataNotFound:
pass 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: 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, def populate_filter_properties_share(self, context, request_spec,
filter_properties): filter_properties):
@ -347,25 +360,8 @@ class FilterScheduler(base.Scheduler):
filter_properties['user_id'] = shr.get('user_id') filter_properties['user_id'] = shr.get('user_id')
filter_properties['metadata'] = shr.get('metadata') filter_properties['metadata'] = shr.get('metadata')
filter_properties['snapshot_id'] = shr.get('snapshot_id') filter_properties['snapshot_id'] = shr.get('snapshot_id')
self.populate_filter_properties_scheduler_hints(context, request_spec,
share_id = request_spec.get('share_id', None) filter_properties)
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
def schedule_create_share_group(self, context, share_group_id, def schedule_create_share_group(self, context, share_group_id,
request_spec, filter_properties): request_spec, filter_properties):

View File

@ -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

View File

@ -42,6 +42,7 @@ from manila import utils
host_manager_opts = [ host_manager_opts = [
cfg.ListOpt('scheduler_default_filters', cfg.ListOpt('scheduler_default_filters',
default=[ default=[
'OnlyHostFilter',
'AvailabilityZoneFilter', 'AvailabilityZoneFilter',
'CapacityFilter', 'CapacityFilter',
'CapabilitiesFilter', 'CapabilitiesFilter',

View File

@ -884,7 +884,8 @@ class API(base.Base):
export_location_path) export_location_path)
request_spec = self._get_request_spec_dict( 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']) host=share_data['host'])
# NOTE(ganso): Scheduler is called to validate if share type # 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']) 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: if share is None:
share = {'instance': {}} share = {'instance': {}}
@ -908,6 +909,8 @@ class API(base.Base):
'size': kwargs.get('size', share.get('size')), 'size': kwargs.get('size', share.get('size')),
'user_id': kwargs.get('user_id', share.get('user_id')), 'user_id': kwargs.get('user_id', share.get('user_id')),
'project_id': kwargs.get('project_id', share.get('project_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': kwargs.get(
'snapshot_support', 'snapshot_support',
share_type.get('extra_specs', {}).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( request_spec = self._get_request_spec_dict(
share, share,
share_type, share_type,
context,
availability_zone_id=service['availability_zone_id'], availability_zone_id=service['availability_zone_id'],
share_network_id=new_share_network_id) share_network_id=new_share_network_id)
@ -2323,7 +2327,8 @@ class API(base.Base):
else: else:
share_type = share_types.get_share_type( share_type = share_types.get_share_type(
context, share['instance']['share_type_id']) 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, self.scheduler_rpcapi.extend_share(context, share['id'], new_size,
reservations, request_spec) reservations, request_spec)
LOG.info("Extend share request issued successfully.", 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_id = share_instance['share_type_id']
share_type = share_types.get_share_type(context, share_type_id) share_type = share_types.get_share_type(context, share_type_id)
req_spec = self._get_request_spec_dict(share_instance, req_spec = self._get_request_spec_dict(share_instance,
share_type, share_type, context,
**kwargs) **kwargs)
shares_req_spec.append(req_spec) shares_req_spec.append(req_spec)

View File

@ -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))

View File

@ -1170,7 +1170,8 @@ class ShareAPITestCase(test.TestCase):
}) })
expected_request_spec = self._get_request_spec_dict( 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']) host=share_data['host'])
if dhss: if dhss:
@ -1378,7 +1379,7 @@ class ShareAPITestCase(test.TestCase):
self.assertRaises(exception.InvalidShare, self.api.manage, self.assertRaises(exception.InvalidShare, self.api.manage,
self.context, share_data, driver_options) 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: if share is None:
share = {'instance': {}} share = {'instance': {}}
@ -1389,6 +1390,7 @@ class ShareAPITestCase(test.TestCase):
'size': kwargs.get('size', share.get('size')), 'size': kwargs.get('size', share.get('size')),
'user_id': kwargs.get('user_id', share.get('user_id')), 'user_id': kwargs.get('user_id', share.get('user_id')),
'project_id': kwargs.get('project_id', share.get('project_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': kwargs.get(
'snapshot_support', 'snapshot_support',
share_type['extra_specs']['snapshot_support']), share_type['extra_specs']['snapshot_support']),
@ -3314,7 +3316,8 @@ class ShareAPITestCase(test.TestCase):
share_network_id=share_network_id) share_network_id=share_network_id)
request_spec = self._get_request_spec_dict( 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) share_network_id=share_network_id)
self.mock_object(self.scheduler_rpcapi, 'migrate_share_to_host') self.mock_object(self.scheduler_rpcapi, 'migrate_share_to_host')
@ -3551,7 +3554,7 @@ class ShareAPITestCase(test.TestCase):
share_type_id=fake_type['id']) share_type_id=fake_type['id'])
request_spec = self._get_request_spec_dict( 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, self.api.migration_start(self.context, share, host, False, True, True,
True, True) True, True)
@ -5093,7 +5096,7 @@ class ShareAPITestCase(test.TestCase):
get_type_calls.append( get_type_calls.append(
mock.call(self.context, instance['share_type_id'])) mock.call(self.context, instance['share_type_id']))
get_request_spec_calls.append( 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( mock_get_type = self.mock_object(
share_types, 'get_share_type', share_types, 'get_share_type',

View File

@ -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.

View File

@ -46,6 +46,7 @@ wsgi_scripts =
manila.scheduler.filters = manila.scheduler.filters =
AffinityFilter = manila.scheduler.filters.affinity:AffinityFilter AffinityFilter = manila.scheduler.filters.affinity:AffinityFilter
AntiAffinityFilter = manila.scheduler.filters.affinity:AntiAffinityFilter AntiAffinityFilter = manila.scheduler.filters.affinity:AntiAffinityFilter
OnlyHostFilter = manila.scheduler.filters.host:OnlyHostFilter
AvailabilityZoneFilter = manila.scheduler.filters.availability_zone:AvailabilityZoneFilter AvailabilityZoneFilter = manila.scheduler.filters.availability_zone:AvailabilityZoneFilter
CapabilitiesFilter = manila.scheduler.filters.capabilities:CapabilitiesFilter CapabilitiesFilter = manila.scheduler.filters.capabilities:CapabilitiesFilter
CapacityFilter = manila.scheduler.filters.capacity:CapacityFilter CapacityFilter = manila.scheduler.filters.capacity:CapacityFilter