Merge "OnlyHostFilter allows user to specify host during share create."
This commit is contained in:
commit
d1509256da
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
@ -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):
|
||||||
|
33
manila/scheduler/filters/host.py
Normal file
33
manila/scheduler/filters/host.py
Normal 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
|
@ -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',
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
63
manila/tests/scheduler/filters/test_host.py
Normal file
63
manila/tests/scheduler/filters/test_host.py
Normal 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))
|
@ -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',
|
||||||
|
10
releasenotes/notes/hostonly-filter-1a17a70dd0aafb86.yaml
Normal file
10
releasenotes/notes/hostonly-filter-1a17a70dd0aafb86.yaml
Normal 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.
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user