Add Share Affinity/Anti-Affinity Scheduler Filters

This patch implements hard affinity and anti-affinity filter for
manila scheduler. Users can specify affinity/anti-affinity share
ids to the field "share.scheduler_hints.same_host" or
"share.scheduler_hints.different_host" in the request payload
when creating a manila share. The scheduler_hints are stored as
share metadata. The filter properties are populated from this
metadata during share migration and so filters will be applied
for share migration as well.

Both fields can be a single share UUID or multiple uuids
separated by comma. For example,

`{
    "share": {
        "scheduler_hints": {
            "same_host": "share_uuid_1,share_uuid_2",
            "different_host": "share_uuid_3"
        }
    }
}`

Implements: bp/affinity-antiaffinity-filter

Change-Id: Ic42d8a0c1d22e77ae64e0ca014607b28fd336467
Co-authored-by: Maurice Escher <maurice.escher@sap.com>
This commit is contained in:
Chuan Miao 2021-02-09 20:57:34 +01:00 committed by kpdev
parent d955928947
commit 7e7ec7337c
20 changed files with 490 additions and 20 deletions

View File

@ -2200,6 +2200,15 @@ revert_to_snapshot_support_share_capability:
required: true
type: boolean
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.
in: body
required: false
type: object
min_version: 2.65
security_service_dns_ip:
description: |
The DNS IP address that is used inside the project network.

View File

@ -12,6 +12,10 @@
"metadata": {
"project": "my_app",
"aim": "doc"
},
"scheduler_hints": {
"same_host": "d9c66489-cf02-4156-b0f2-527f3211b243,4ffee55f-ba98-42d2-a8ce-e7cecb169182",
"different_host": "903685eb-f242-4105-903d-4bef2db94be4"
}
}
}

View File

@ -370,6 +370,7 @@ Request
- metadata: metadata
- share_network_id: share_network_id_request
- availability_zone: availability_zone_request
- scheduler_hints: scheduler_hints
Request example
---------------

View File

@ -171,13 +171,14 @@ 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.
"""
# 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.63"
_MAX_API_VERSION = "2.65"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -358,3 +358,8 @@ user documentation.
Added 'force' field to extend share api, which can extend share directly
without go through share scheduler.
2.65
----
Added ability to specify "scheduler_hints" in the request body of the POST
/shares request. These hints will invoke Affinity/Anti-Affinity scheduler
filters during share creation and share migration.

View File

@ -235,7 +235,8 @@ class ShareMixin(object):
@wsgi.Controller.authorize('create')
def _create(self, req, body,
check_create_share_from_snapshot_support=False,
check_availability_zones_extra_spec=False):
check_availability_zones_extra_spec=False,
scheduler_hints=None):
"""Creates a new share."""
context = req.environ['manila.context']
@ -412,6 +413,9 @@ class ShareMixin(object):
kwargs['share_type'] = share_type
if share_network_id:
kwargs['share_network_id'] = share_network_id
kwargs['scheduler_hints'] = scheduler_hints
new_share = self.share_api.create(context,
share_proto,
size,

View File

@ -178,8 +178,20 @@ class ShareController(shares.ShareMixin,
return data
@wsgi.Controller.api_version("2.48")
@wsgi.Controller.api_version("2.65")
def create(self, req, body):
if not self.is_valid_body(body, 'share'):
raise exc.HTTPUnprocessableEntity()
share = body['share']
scheduler_hints = share.pop('scheduler_hints', None)
return self._create(req, body,
check_create_share_from_snapshot_support=True,
check_availability_zones_extra_spec=True,
scheduler_hints=scheduler_hints)
@wsgi.Controller.api_version("2.48", "2.64") # noqa
def create(self, req, body): # pylint: disable=function-redefined # noqa F811
return self._create(req, body,
check_create_share_from_snapshot_support=True,
check_availability_zones_extra_spec=True)

View File

@ -766,13 +766,16 @@ def security_service_get_all_by_project(context, project_id):
####################
def share_metadata_get(context, share_id):
"""Get all metadata for a share."""
return IMPL.share_metadata_get(context, share_id)
def share_metadata_get_item(context, share_id, key):
"""Get metadata item for given key and for a given share.."""
return IMPL.share_metadata_get_item(context, share_id, key)
def share_metadata_delete(context, share_id, key):
"""Delete the given metadata item."""
IMPL.share_metadata_delete(context, share_id, key)
@ -783,6 +786,11 @@ def share_metadata_update(context, share, metadata, delete):
IMPL.share_metadata_update(context, share, metadata, delete)
def share_metadata_update_item(context, share_id, item):
"""update meta item containing key and value for given share."""
IMPL.share_metadata_update_item(context, share_id, item)
###################
def share_export_location_get_by_uuid(context, export_location_uuid,

View File

@ -3416,6 +3416,22 @@ def share_metadata_get(context, share_id):
return _share_metadata_get(context, share_id)
@require_context
@require_share_exists
def share_metadata_get_item(context, share_id, key, session=None):
session = session or get_session()
try:
row = _share_metadata_get_item(context, share_id, key,
session=session)
except exception.ShareMetadataNotFound:
raise exception.ShareMetadataNotFound()
result = {}
result[row['key']] = row['value']
return result
@require_context
@require_share_exists
def share_metadata_delete(context, share_id, key):
@ -3429,6 +3445,12 @@ def share_metadata_update(context, share_id, metadata, delete):
return _share_metadata_update(context, share_id, metadata, delete)
@require_context
@require_share_exists
def share_metadata_update_item(context, share_id, item, session=None):
return _share_metadata_update(context, share_id, item, delete=False)
def _share_metadata_get_query(context, share_id, session=None):
return (model_query(context, models.ShareMetadata, session=session,
read_deleted="no").

View File

@ -175,7 +175,7 @@ class InvalidParameterValue(Invalid):
class InvalidUUID(Invalid):
message = _("Expected a uuid but received %(uuid)s.")
message = _("%(uuid)s is not a valid uuid.")
class InvalidDriverMode(Invalid):
@ -546,6 +546,10 @@ class ShareNotFound(NotFound):
message = _("Share %(share_id)s could not be found.")
class ShareInstanceNotFound(NotFound):
message = _("Share instance %(share_instance_id)s could not be found.")
class ShareSnapshotNotFound(NotFound):
message = _("Snapshot %(snapshot_id)s could not be found.")

View File

@ -23,6 +23,7 @@ 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
@ -34,6 +35,11 @@ from manila.share import share_types
CONF = cfg.CONF
LOG = log.getLogger(__name__)
AFFINITY_HINT = 'same_host'
ANTI_AFFINITY_HINT = 'different_host'
AFFINITY_KEY = "__affinity_same_host"
ANTI_AFFINITY_KEY = "__affinity_different_host"
class FilterScheduler(base.Scheduler):
"""Scheduler that can be used for filtering and weighing."""
@ -214,7 +220,8 @@ class FilterScheduler(base.Scheduler):
'replication_domain': replication_domain,
})
self.populate_filter_properties_share(request_spec, filter_properties)
self.populate_filter_properties_share(context, request_spec,
filter_properties)
return filter_properties, share_properties
@ -315,7 +322,17 @@ class FilterScheduler(base.Scheduler):
"exc": "exc"
})
def populate_filter_properties_share(self, request_spec,
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
else:
hints.update({hint: result.get(key)})
def populate_filter_properties_share(self, context, request_spec,
filter_properties):
"""Stuff things into filter_properties.
@ -331,6 +348,25 @@ class FilterScheduler(base.Scheduler):
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
def schedule_create_share_group(self, context, share_group_id,
request_spec, filter_properties):

View File

@ -0,0 +1,121 @@
# Copyright (c) 2021 SAP.
# 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 import exception
from manila.scheduler.filters import base_host
from manila.share import api
LOG = log.getLogger(__name__)
AFFINITY_FILTER = 'same_host'
ANTI_AFFINITY_FILTER = 'different_host'
class AffinityBaseFilter(base_host.BaseHostFilter):
"""Base class of affinity filters"""
_filter_type = None
def __init__(self):
self.share_api = api.API()
def filter_all(self, filter_obj_list, filter_properties):
# _filter_type should be defined in subclass
if self._filter_type is None:
raise AffinityFilterTypeNotSetError
try:
filter_properties = self._validate(filter_properties)
except SchedulerHintsNotSet:
# AffinityFilter/AntiAffinityFilter is skipped if corresponding
# hint is not set. If the "scheduler_hints" is not set, both
# filters are skipped.
return filter_obj_list
except (exception.InvalidUUID,
exception.ShareNotFound,
exception.ShareInstanceNotFound) as e:
# Stop scheduling share when above errors are caught
LOG.error('%(filter_name)s: %(error)s', {
'filter_name': self.__class__.__name__,
'error': e})
return None
else:
# Return list of hosts which pass the function host_passes()
# overriden in AffinityFilter and AntiAffinityFilter.
return [obj for obj in filter_obj_list
if self._filter_one(obj, filter_properties)]
def _validate(self, filter_properties):
context = filter_properties['context']
hints = filter_properties.get('scheduler_hints')
if hints is None:
raise SchedulerHintsNotSet
else:
share_uuids = hints.get(self._filter_type)
if share_uuids is None:
raise SchedulerHintsNotSet
share_uuids = share_uuids.split(",")
filter_properties['scheduler_hints'][self._filter_type] = []
filtered_hosts = []
for uuid in share_uuids:
try:
share = self.share_api.get(context, uuid)
except exception.NotFound:
raise exception.ShareNotFound(uuid)
instances = share.get('instances')
if len(instances) == 0:
raise exception.ShareInstanceNotFound(share_instance_id=uuid)
filtered_hosts.append(
[instance.get('host') for instance in instances])
if self._filter_type == AFFINITY_FILTER:
filter_properties['scheduler_hints'][self._filter_type] = list(
set.intersection(*map(set, filtered_hosts)))
else:
filter_properties['scheduler_hints'][self._filter_type] = list(
set.union(*map(set, filtered_hosts)))
return filter_properties
class AffinityFilter(AffinityBaseFilter):
_filter_type = AFFINITY_FILTER
def host_passes(self, host_state, filter_properties):
allowed_hosts = \
filter_properties['scheduler_hints'][self._filter_type]
return host_state.host in allowed_hosts
class AntiAffinityFilter(AffinityBaseFilter):
_filter_type = ANTI_AFFINITY_FILTER
def host_passes(self, host_state, filter_properties):
forbidden_hosts = \
filter_properties['scheduler_hints'][self._filter_type]
return host_state.host not in forbidden_hosts
class SchedulerHintsNotSet(Exception):
pass
class AffinityFilterTypeNotSetError(Exception):
pass

View File

@ -48,6 +48,8 @@ host_manager_opts = [
'DriverFilter',
'ShareReplicationFilter',
'CreateFromSnapshotFilter',
'AffinityFilter',
'AntiAffinityFilter',
],
help='Which filter class names to use for filtering hosts '
'when not specified in the request.'),

View File

@ -26,6 +26,7 @@ from oslo_log import log
from oslo_utils import excutils
from oslo_utils import strutils
from oslo_utils import timeutils
from oslo_utils import uuidutils
import six
from manila.api import common as api_common
@ -64,6 +65,11 @@ LOG = log.getLogger(__name__)
GB = 1048576 * 1024
QUOTAS = quota.QUOTAS
AFFINITY_HINT = 'same_host'
ANTI_AFFINITY_HINT = 'different_host'
AFFINITY_KEY = "__affinity_same_host"
ANTI_AFFINITY_KEY = "__affinity_different_host"
def locked_security_service_update_operation(operation):
"""Lock decorator for security service operation.
@ -184,7 +190,7 @@ class API(base.Base):
snapshot_id=None, availability_zone=None, metadata=None,
share_network_id=None, share_type=None, is_public=False,
share_group_id=None, share_group_snapshot_member=None,
availability_zones=None):
availability_zones=None, scheduler_hints=None):
"""Create new share."""
self._check_metadata_properties(metadata)
@ -367,6 +373,8 @@ class API(base.Base):
QUOTAS.rollback(
context, reservations, share_type_id=share_type_id)
self.save_scheduler_hints(context, share, scheduler_hints)
host = None
snapshot_host = None
if snapshot:
@ -384,7 +392,7 @@ class API(base.Base):
availability_zone=availability_zone, share_group=share_group,
share_group_snapshot_member=share_group_snapshot_member,
share_type_id=share_type_id, availability_zones=availability_zones,
snapshot_host=snapshot_host)
snapshot_host=snapshot_host, scheduler_hints=scheduler_hints)
# Retrieve the share with instance details
share = self.db.share_get(context, share['id'])
@ -461,7 +469,7 @@ class API(base.Base):
host=None, availability_zone=None,
share_group=None, share_group_snapshot_member=None,
share_type_id=None, availability_zones=None,
snapshot_host=None):
snapshot_host=None, scheduler_hints=None):
request_spec, share_instance = (
self.create_share_instance_and_get_request_spec(
context, share, availability_zone=availability_zone,
@ -493,14 +501,17 @@ class API(base.Base):
share_instance,
host,
request_spec=request_spec,
filter_properties={},
filter_properties={'scheduler_hints': scheduler_hints},
snapshot_id=share['snapshot_id'],
)
else:
# Create share instance from scratch or from snapshot could happen
# on hosts other than the source host.
self.scheduler_rpcapi.create_share_instance(
context, request_spec=request_spec, filter_properties={})
context,
request_spec=request_spec,
filter_properties={'scheduler_hints': scheduler_hints},
)
return share_instance
@ -961,6 +972,7 @@ class API(base.Base):
'terminated_at': timeutils.utcnow()}
share_ref = self.db.share_update(context, share['id'], update_data)
self.delete_scheduler_hints(context, share)
self.share_rpcapi.unmanage_share(context, share_ref)
# NOTE(u_glide): We should update 'updated_at' timestamp of
@ -1170,6 +1182,8 @@ class API(base.Base):
"members.") % share_group_snapshot_members_count)
raise exception.InvalidShare(reason=msg)
self._check_is_share_busy(share)
self.delete_scheduler_hints(context, share)
for share_instance in share.instances:
if share_instance['host']:
self.delete_instance(context, share_instance, force=force)
@ -2052,6 +2066,81 @@ class API(base.Base):
"""Delete the given metadata item from a share."""
self.db.share_metadata_delete(context, share['id'], key)
def _validate_scheduler_hints(self, context, share, share_uuids):
for uuid in share_uuids:
if not uuidutils.is_uuid_like(uuid):
raise exception.InvalidUUID(uuid=uuid)
try:
self.get(context, uuid)
except (exception.NotFound, exception.PolicyNotAuthorized):
raise exception.ShareNotFound(share_id=uuid)
def _save_scheduler_hints(self, context, share, share_uuids, key):
share_uuids = share_uuids.split(",")
self._validate_scheduler_hints(context, share, share_uuids)
val_uuids = None
for uuid in share_uuids:
try:
result = self.db.share_metadata_get_item(context, uuid, key)
except exception.ShareMetadataNotFound:
item = {key: share['id']}
else:
existing_uuids = result.get(key, "")
item = {key:
','.join(existing_uuids.split(',') + [share['id']])}
self.db.share_metadata_update_item(context, uuid, item)
if not val_uuids:
val_uuids = uuid
else:
val_uuids = val_uuids + "," + uuid
if val_uuids:
item = {key: val_uuids}
self.db.share_metadata_update_item(context, share['id'], item)
def save_scheduler_hints(self, context, share, scheduler_hints=None):
if scheduler_hints is None:
return
same_host_uuids = scheduler_hints.get(AFFINITY_HINT, None)
different_host_uuids = scheduler_hints.get(ANTI_AFFINITY_HINT, None)
if same_host_uuids:
self._save_scheduler_hints(context, share, same_host_uuids,
AFFINITY_KEY)
if different_host_uuids:
self._save_scheduler_hints(context, share, different_host_uuids,
ANTI_AFFINITY_KEY)
def _delete_scheduler_hints(self, context, share, key):
try:
result = self.db.share_metadata_get_item(context, share['id'],
key)
except exception.ShareMetadataNotFound:
return
share_uuids = result.get(key, "").split(",")
for uuid in share_uuids:
try:
result = self.db.share_metadata_get_item(context, uuid, key)
except exception.ShareMetadataNotFound:
continue
new_val_uuids = [val_uuid for val_uuid
in result.get(key, "").split(",")
if val_uuid != share['id']]
if not new_val_uuids:
self.db.share_metadata_delete(context, uuid, key)
else:
item = {key: ','.join(new_val_uuids)}
self.db.share_metadata_update_item(context, uuid, item)
self.db.share_metadata_delete(context, share['id'], key)
def delete_scheduler_hints(self, context, share):
self._delete_scheduler_hints(context, share, AFFINITY_KEY)
self._delete_scheduler_hints(context, share, ANTI_AFFINITY_KEY)
def _check_is_share_busy(self, share):
"""Raises an exception if share is busy with an active task."""
if share.is_busy:

View File

@ -560,7 +560,8 @@ class ShareAPITest(test.TestCase):
is_public=False,
metadata=None,
snapshot_id=None,
availability_zone=az_name)
availability_zone=az_name,
scheduler_hints=None)
def test_share_create_with_sg_and_different_availability_zone(self):
sg_id = 'fake_sg_id'

View File

@ -152,7 +152,7 @@ class FilterSchedulerTestCase(test_base.SchedulerTestCase):
'share_properties': {'project_id': 1, 'size': 1},
'share_instance_properties': {},
'share_type': {'name': 'NFS'},
'share_id': ['fake-id1'],
'share_id': 'fake-id1',
}
self.assertRaises(exception.NoValidHost, sched.schedule_create_share,
fake_context, request_spec, {})
@ -177,7 +177,7 @@ class FilterSchedulerTestCase(test_base.SchedulerTestCase):
'share_properties': {'project_id': 1, 'size': 1},
'share_instance_properties': {},
'share_type': {'name': 'NFS'},
'share_id': ['fake-id1'],
'share_id': 'fake-id1',
}
self.assertRaises(exception.NoValidHost, sched.schedule_create_share,
fake_context, request_spec, {})

View File

@ -0,0 +1,132 @@
# Copyright (c) 2021 SAP.
# 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 unittest import mock
from manila import exception
from manila.scheduler.filters import affinity
from manila import test
from manila.tests.scheduler import fakes
fake_hosts = [
fakes.FakeHostState('host1', {}),
fakes.FakeHostState('host2', {}),
fakes.FakeHostState('host3', {}),
]
fake_shares_1 = {
'abb6e0ac-7c3e-4ce0-8a69-5a166d246882': {
'instances': [
{'host': fake_hosts[0].host}
]
},
'4de0cc74-450c-4468-8159-52128cf03407': {
'instances': [
{'host': fake_hosts[0].host}
]
},
}
fake_shares_2 = {
'c920fb61-e250-4c3c-a25d-1fdd9ca7cbc3': {
'instances': [
{'host': fake_hosts[1].host}
]
},
}
fake_shares_3 = {
'3923bebf-9825-4a66-971e-6092a9fe2dbb': {
'instances': [
{'host': fake_hosts[2].host}
]
},
}
@ddt.ddt
class AffinityFilterTestCase(test.TestCase):
"""Test case for AffinityFilter."""
def setUp(self):
super(AffinityFilterTestCase, self).setUp()
self.filter = affinity.AffinityFilter()
self.anti_filter = affinity.AntiAffinityFilter()
def _make_filter_hints(self, *hints):
return {
'context': None,
'scheduler_hints': {'same_host': ','.join(list(hints))},
}
def _make_anti_filter_hints(self, *hints):
return {
'context': None,
'scheduler_hints': {'different_host': ','.join(list(hints))},
}
def _fake_get(self, context, uuid):
if uuid in fake_shares_1.keys():
return fake_shares_1[uuid]
if uuid in fake_shares_2.keys():
return fake_shares_2[uuid]
if uuid in fake_shares_3.keys():
return fake_shares_3[uuid]
raise exception.ShareNotFound(uuid)
@ddt.data('b5c207da-ac0b-43b0-8691-c6c9e860199d')
@mock.patch('manila.share.api.API.get')
def test_affinity_share_not_found(self, unknown_id, mock_share_get):
mock_share_get.side_effect = self._fake_get
self.assertRaises(exception.ShareNotFound,
self.filter._validate,
self._make_filter_hints(unknown_id))
@ddt.data(
{'context': None},
{'context': None, 'scheduler_hints': None},
{'context': None, 'scheduler_hints': {}},
)
def test_affinity_scheduler_hint_not_set(self, hints):
self.assertRaises(affinity.SchedulerHintsNotSet,
self.filter._validate, hints)
@ mock.patch('manila.share.api.API.get')
def test_affinity_filter(self, mock_share_get):
mock_share_get.side_effect = self._fake_get
share_ids = fake_shares_1.keys()
hints = self._make_filter_hints(*share_ids)
valid_hosts = self.filter.filter_all(fake_hosts, hints)
valid_hosts = [h.host for h in valid_hosts]
self.assertIn('host1', valid_hosts)
self.assertNotIn('host2', valid_hosts)
self.assertNotIn('host3', valid_hosts)
@ mock.patch('manila.share.api.API.get')
def test_anti_affinity_filter(self, mock_share_get):
mock_share_get.side_effect = self._fake_get
share_ids = fake_shares_2.keys()
hints = self._make_anti_filter_hints(*share_ids)
valid_hosts = self.anti_filter.filter_all(fake_hosts, hints)
valid_hosts = [h.host for h in valid_hosts]
self.assertIn('host1', valid_hosts)
self.assertIn('host3', valid_hosts)
self.assertNotIn('host2', valid_hosts)

View File

@ -825,7 +825,8 @@ class ShareAPITestCase(test.TestCase):
self.context, share, share_network_id=fake_share_network_id,
host=None, availability_zone=None, share_group=None,
share_group_snapshot_member=None, share_type_id=None,
availability_zones=expected_azs, snapshot_host=None
availability_zones=expected_azs, snapshot_host=None,
scheduler_hints=None
)
db_api.share_get.assert_called_once()
@ -980,7 +981,7 @@ class ShareAPITestCase(test.TestCase):
share_instance,
host,
request_spec=mock.ANY,
filter_properties={},
filter_properties={'scheduler_hints': None},
snapshot_id=share['snapshot_id'],
)
self.assertFalse(
@ -993,7 +994,8 @@ class ShareAPITestCase(test.TestCase):
(self.api.scheduler_rpcapi.create_share_instance.
assert_called_once_with(
self.context, request_spec=mock.ANY, filter_properties={}))
self.context, request_spec=mock.ANY,
filter_properties={'scheduler_hints': None}))
self.assertFalse(self.api.share_rpcapi.create_share_instance.called)
def test_create_share_instance_from_snapshot(self):
@ -2273,7 +2275,8 @@ class ShareAPITestCase(test.TestCase):
availability_zone=az,
share_group=None, share_group_snapshot_member=None,
availability_zones=None,
snapshot_host=snapshot['share']['instance']['host'])
snapshot_host=snapshot['share']['instance']['host'],
scheduler_hints=None)
share_api.policy.check_policy.assert_called_once_with(
self.context, 'share_snapshot', 'get_snapshot')
quota.QUOTAS.reserve.assert_called_once_with(

View File

@ -0,0 +1,14 @@
---
features:
- Add AffinityFilter and AntiAffinityFilter to manila's scheduler.
These hard affinity and anti-affinity filter needs user to specify
affinity/anti-affinity share ids to the field
"share.scheduler_hints.same_host" or
"share.scheduler_hints.different_host" in the request payload when
creating a manila share. The hints are stored as share metadata. The
filter properties are populated from this metadata during share
migration and so filters will be applied when migrating a manila share.
upgrade:
- To add AffinityFilter and AntiAffinityFilter to an active deployment,
their references must be added to the manila.scheduler.filters section in
setup.cfg and must be enabled in manila.conf.

View File

@ -44,6 +44,8 @@ console_scripts =
wsgi_scripts =
manila-wsgi = manila.wsgi.wsgi:initialize_application
manila.scheduler.filters =
AffinityFilter = manila.scheduler.filters.affinity:AffinityFilter
AntiAffinityFilter = manila.scheduler.filters.affinity:AntiAffinityFilter
AvailabilityZoneFilter = manila.scheduler.filters.availability_zone:AvailabilityZoneFilter
CapabilitiesFilter = manila.scheduler.filters.capabilities:CapabilitiesFilter
CapacityFilter = manila.scheduler.filters.capacity:CapacityFilter