Added new scheduler filter: AggregateTypeExtraSpecsAffinityFilter
The flavor_extra_spec metadata pair will be consumed by the AggregateTypeExtraSpecsAfinityFilter to allow operators to define a set of extra specs key value pairs that are required to schedule to the aggregate, e.g.: standard memory backing aggregate: flavor_extra_spec: "mem_page_size=small,mem_page_size=any" high bandwidth memory backing aggregate: flavor_extra_spec: "mem_page_size=2M,mem_page_size=1G,mem_page_size=large" Added unit test for this feature. DocImpact Implements: blueprint aggregate-extra-specs-filter Change-Id: Id3a9918cf9f83b2a9b1dfbcd91803b5b1b2bcc78
This commit is contained in:
@@ -166,6 +166,10 @@ There are some standard filter classes to use (:mod:`nova.scheduler.filters`):
|
||||
the available metrics are passed.
|
||||
* |NUMATopologyFilter| - filters hosts based on the NUMA topology requested by the
|
||||
instance, if any.
|
||||
* |AggregateTypeExtraSpecsAffinityFilter| - filters only hosts aggregated inside
|
||||
host aggregates containing "flavor_extra_spec" metadata. Keys inside this
|
||||
variable must match with the instance extra specs. Eg. 'flavor_extra_spec=\
|
||||
mem_page_size=any,mem_page_size=~,mem_page_size=small'
|
||||
|
||||
Now we can focus on these standard filter classes in details. I will pass the
|
||||
simplest ones, such as |AllHostsFilter|, |CoreFilter| and |RamFilter| are,
|
||||
@@ -409,6 +413,7 @@ in :mod:``nova.tests.scheduler``.
|
||||
.. |TrustedFilter| replace:: :class:`TrustedFilter <nova.scheduler.filters.trusted_filter.TrustedFilter>`
|
||||
.. |TypeAffinityFilter| replace:: :class:`TypeAffinityFilter <nova.scheduler.filters.type_filter.TypeAffinityFilter>`
|
||||
.. |AggregateTypeAffinityFilter| replace:: :class:`AggregateTypeAffinityFilter <nova.scheduler.filters.type_filter.AggregateTypeAffinityFilter>`
|
||||
.. |AggregateTypeExtraSpecsAffinityFilter| replace:: :class:`AggregateTypeAffinityFilter <nova.scheduler.filters.type_filter.AggregateTypeExtraSpecsAffinityFilter>`
|
||||
.. |ServerGroupAntiAffinityFilter| replace:: :class:`ServerGroupAntiAffinityFilter <nova.scheduler.filters.affinity_filter.ServerGroupAntiAffinityFilter>`
|
||||
.. |ServerGroupAffinityFilter| replace:: :class:`ServerGroupAffinityFilter <nova.scheduler.filters.affinity_filter.ServerGroupAffinityFilter>`
|
||||
.. |AggregateInstanceExtraSpecsFilter| replace:: :class:`AggregateInstanceExtraSpecsFilter <nova.scheduler.filters.aggregate_instance_extra_specs.AggregateInstanceExtraSpecsFilter>`
|
||||
|
||||
@@ -13,9 +13,13 @@
|
||||
# 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 collections
|
||||
|
||||
from nova.scheduler import filters
|
||||
from nova.scheduler.filters import utils
|
||||
from oslo_log import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TypeAffinityFilter(filters.BaseHostFilter):
|
||||
@@ -59,3 +63,100 @@ class AggregateTypeAffinityFilter(filters.BaseHostFilter):
|
||||
[x.strip() for x in val.split(',')]):
|
||||
return True
|
||||
return not aggregate_vals
|
||||
|
||||
|
||||
class AggregateTypeExtraSpecsAffinityFilter(filters.BaseHostFilter):
|
||||
"""AggregateTypeExtraSpecsAffinityFilter filters only hosts aggregated
|
||||
inside host aggregates containing "flavor_extra_spec" metadata. Keys inside
|
||||
this variable must match with the instance extra specs.
|
||||
"""
|
||||
|
||||
def _group_extra_specs(self, aggregate_extraspec):
|
||||
"""Group all key/data from aggregate_extraspec.
|
||||
|
||||
:param aggregate_extraspec: string containing a list of required
|
||||
instance flavor extra specs, separated by commas, with format
|
||||
"key=value"
|
||||
:type aggregate: basestring.
|
||||
:return: dictionary with the values parsed and grouped by keys.
|
||||
:type: dict.
|
||||
"""
|
||||
extra_specs = collections.defaultdict(set)
|
||||
kv_list = aggregate_extraspec.split(',')
|
||||
|
||||
# Create an entry in a dict for every new key/value.
|
||||
for kv_element in kv_list:
|
||||
key, value = kv_element.split('=')[:2]
|
||||
extra_specs[key].add(value)
|
||||
|
||||
return extra_specs
|
||||
|
||||
def _test_extra_specs_elements(self,
|
||||
aggregate_extra_spec,
|
||||
instance_extra_specs):
|
||||
"""Analyze and execute all aggregate_extra_spec parsed.
|
||||
|
||||
:param aggregate_extra_spec: dictionary with the values parsed and
|
||||
grouped by keys
|
||||
:type aggregate_extra_spec: dict.
|
||||
:return: True if all parameters executed correctly; False is there
|
||||
is any error.
|
||||
:type: boolean.
|
||||
"""
|
||||
for key, value in aggregate_extra_spec.iteritems():
|
||||
if '*' in value:
|
||||
if key not in instance_extra_specs:
|
||||
LOG.debug("'flavor_extra_spec' key: %(key)s "
|
||||
"is not present", {'key': key})
|
||||
return False
|
||||
else:
|
||||
continue
|
||||
|
||||
if '!' in value:
|
||||
if key in instance_extra_specs:
|
||||
LOG.debug("'flavor_extra_spec' key: %(key)s "
|
||||
"is present", {'key': key})
|
||||
return False
|
||||
else:
|
||||
continue
|
||||
|
||||
if '~' in value:
|
||||
if key not in instance_extra_specs:
|
||||
continue
|
||||
else:
|
||||
value.discard('~')
|
||||
|
||||
for element in value:
|
||||
if key not in instance_extra_specs:
|
||||
LOG.debug("'flavor_extra_spec' key: %(key)s "
|
||||
"is not present", {'key': key})
|
||||
return False
|
||||
if instance_extra_specs[key] not in value:
|
||||
LOG.debug("The following 'flavor_extra_spec' "
|
||||
"key/value: %(key)s/%(value)s doesn't "
|
||||
"match", {'key': key, 'value': value})
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def host_passes(self, host_state, filter_properties):
|
||||
# Read extra specs needed by instance.
|
||||
instance_type = filter_properties.get('instance_type')
|
||||
instance_extra_specs = instance_type.get('extra_specs')
|
||||
|
||||
# Read metadata from host: find "flavor_extra_spec" in host
|
||||
# aggregates metadata, each separately.
|
||||
aggregate_extra_spec_list = utils.aggregate_values_from_key(host_state,
|
||||
'flavor_extra_spec')
|
||||
|
||||
for aggregate_extra_spec in aggregate_extra_spec_list:
|
||||
# Recover and group all key/data from aggregate_extraspec.
|
||||
aggregate_extra_spec = \
|
||||
self._group_extra_specs(aggregate_extra_spec)
|
||||
|
||||
# Loop all aggregate flavor extra specs and compare to
|
||||
# instance extra specs.
|
||||
if self._test_extra_specs_elements(aggregate_extra_spec,
|
||||
instance_extra_specs) is False:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -119,3 +119,151 @@ class TestTypeFilter(test.NoDBTestCase):
|
||||
self.assertTrue(self.filt_cls.host_passes(host, filter2_properties))
|
||||
# False as instance_type is not allowed for aggregate
|
||||
self.assertFalse(self.filt_cls.host_passes(host, filter3_properties))
|
||||
|
||||
|
||||
class TestAggregateTypeExtraSpecsAffinityFilter(test.NoDBTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestAggregateTypeExtraSpecsAffinityFilter, self).setUp()
|
||||
self.filt_cls = type_filter.AggregateTypeExtraSpecsAffinityFilter()
|
||||
|
||||
self.aggr_no_extraspecs = objects.Aggregate(context='test')
|
||||
self.aggr_no_extraspecs.metadata = {}
|
||||
|
||||
self.aggr_invalid_extraspecs = objects.Aggregate(context='test')
|
||||
self.aggr_invalid_extraspecs.metadata = {'flavor_extra_spec':
|
||||
'mem_page_size=large'}
|
||||
|
||||
self.aggr_valid_extraspecs = objects.Aggregate(context='test')
|
||||
self.aggr_valid_extraspecs.metadata = {'flavor_extra_spec':
|
||||
'mem_page_size=small'}
|
||||
|
||||
self.aggr_optional_large = objects.Aggregate(context='test')
|
||||
self.aggr_optional_large.metadata = {'flavor_extra_spec':
|
||||
'mem_page_size=large,mem_page_size=~'}
|
||||
|
||||
self.aggr_optional_small = objects.Aggregate(context='test')
|
||||
self.aggr_optional_small.metadata = {'flavor_extra_spec':
|
||||
'mem_page_size=small,mem_page_size=~'}
|
||||
|
||||
self.aggr_optional_first = objects.Aggregate(context='test')
|
||||
self.aggr_optional_first.metadata = {'flavor_extra_spec':
|
||||
'mem_page_size=~,mem_page_size=small,mem_page_size=any'}
|
||||
|
||||
self.aggr_optional_last = objects.Aggregate(context='test')
|
||||
self.aggr_optional_last.metadata = {'flavor_extra_spec':
|
||||
'mem_page_size=any,mem_page_size=small,mem_page_size=~'}
|
||||
|
||||
self.aggr_optional_middle = objects.Aggregate(context='test')
|
||||
self.aggr_optional_middle.metadata = {'flavor_extra_spec':
|
||||
'mem_page_size=any,mem_page_size=~,mem_page_size=small'}
|
||||
|
||||
self.aggr_any = objects.Aggregate(context='test')
|
||||
self.aggr_any.metadata = {'flavor_extra_spec': 'mem_page_size=*'}
|
||||
|
||||
self.aggr_nopresent = objects.Aggregate(context='test')
|
||||
self.aggr_nopresent.metadata = {'flavor_extra_spec':
|
||||
'mem_page_size=!'}
|
||||
|
||||
self.filter_properties = {'instance_type': {'extra_specs':
|
||||
{'mem_page_size': 'small'}}}
|
||||
self.filter_properties_noextraspecs = {'instance_type':
|
||||
{'extra_specs': {}}}
|
||||
|
||||
def _do_test_compute_filter_extra_specs(self, host_state,
|
||||
filter_properties, passes):
|
||||
assertion = self.assertTrue if passes else self.assertFalse
|
||||
assertion(self.filt_cls.host_passes(host_state, filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_ha_no_extraspecs(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_no_extraspecs)
|
||||
self.assertTrue(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_ha_wrong_extraspecs(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_invalid_extraspecs)
|
||||
self.assertFalse(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_ha_right_extraspecs(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_valid_extraspecs)
|
||||
self.assertTrue(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_2ha_noextra_wrong(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_no_extraspecs)
|
||||
host_state.aggregates.append(self.aggr_invalid_extraspecs)
|
||||
self.assertFalse(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_2ha_noextra_right(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_no_extraspecs)
|
||||
host_state.aggregates.append(self.aggr_valid_extraspecs)
|
||||
self.assertTrue(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_2ha_wrong_right(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_invalid_extraspecs)
|
||||
host_state.aggregates.append(self.aggr_valid_extraspecs)
|
||||
self.assertFalse(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_any_and_more_large(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_optional_large)
|
||||
self.assertFalse(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_any_and_more_small(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_optional_small)
|
||||
self.assertTrue(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_any_and_more_no_extraspecs(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_optional_large)
|
||||
self._do_test_compute_filter_extra_specs(host_state,
|
||||
self.filter_properties_noextraspecs, True)
|
||||
|
||||
def test_aggregate_type_extra_specs_any_order(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_optional_first)
|
||||
self.assertTrue(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
host_state.aggregates = [self.aggr_optional_last]
|
||||
self.assertTrue(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
host_state.aggregates = [self.aggr_optional_middle]
|
||||
self.assertTrue(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_kleene(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_any)
|
||||
self.assertTrue(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_kleene_no_extraspecs(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_any)
|
||||
self.assertFalse(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties_noextraspecs))
|
||||
|
||||
def test_aggregate_type_extra_specs_nospec(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_nopresent)
|
||||
self.assertFalse(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties))
|
||||
|
||||
def test_aggregate_type_extra_specs_nospec_no_extraspecs(self):
|
||||
host_state = fakes.FakeHostState('host1', 'compute', {})
|
||||
host_state.aggregates.append(self.aggr_nopresent)
|
||||
self.assertTrue(self.filt_cls.host_passes(host_state,
|
||||
self.filter_properties_noextraspecs))
|
||||
|
||||
Reference in New Issue
Block a user