diff --git a/doc/source/filter_scheduler.rst b/doc/source/filter_scheduler.rst index bbd94019f2c5..ba1a143c1db3 100644 --- a/doc/source/filter_scheduler.rst +++ b/doc/source/filter_scheduler.rst @@ -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 ` .. |TypeAffinityFilter| replace:: :class:`TypeAffinityFilter ` .. |AggregateTypeAffinityFilter| replace:: :class:`AggregateTypeAffinityFilter ` +.. |AggregateTypeExtraSpecsAffinityFilter| replace:: :class:`AggregateTypeAffinityFilter ` .. |ServerGroupAntiAffinityFilter| replace:: :class:`ServerGroupAntiAffinityFilter ` .. |ServerGroupAffinityFilter| replace:: :class:`ServerGroupAffinityFilter ` .. |AggregateInstanceExtraSpecsFilter| replace:: :class:`AggregateInstanceExtraSpecsFilter ` diff --git a/nova/scheduler/filters/type_filter.py b/nova/scheduler/filters/type_filter.py index e593cf071a59..ab6728d99c7f 100644 --- a/nova/scheduler/filters/type_filter.py +++ b/nova/scheduler/filters/type_filter.py @@ -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 diff --git a/nova/tests/unit/scheduler/filters/test_type_filters.py b/nova/tests/unit/scheduler/filters/test_type_filters.py index 8e05005b0f47..1c4242664c70 100644 --- a/nova/tests/unit/scheduler/filters/test_type_filters.py +++ b/nova/tests/unit/scheduler/filters/test_type_filters.py @@ -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))