From 0ca6b817587bfb69a31fcc3a47d62171f5911caf Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Thu, 14 Nov 2013 16:53:18 +0000 Subject: [PATCH] Aggregate: Hosts isolation based on image properties Isolates hosts based on image properties and aggregate metadata - If a host doesn't belong to any aggregate it can create instances from all images. - if a host belongs to an aggregate and if this aggregate defines metadata that match with the image properties then the host is a candidate to boot the instance. DocImpact Change-Id: I3b9325e2e103f3bb6eed66789ac2c82941e94397 Implements: blueprint aggregate-host-isolation-based-image-properties --- doc/source/devref/filter_scheduler.rst | 4 +- etc/nova/nova.conf.sample | 13 +++ .../aggregate_image_properties_isolation.py | 68 ++++++++++++ nova/tests/scheduler/test_host_filters.py | 102 ++++++++++++++++++ 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 nova/scheduler/filters/aggregate_image_properties_isolation.py diff --git a/doc/source/devref/filter_scheduler.rst b/doc/source/devref/filter_scheduler.rst index dc217099f677..693d292f16f3 100644 --- a/doc/source/devref/filter_scheduler.rst +++ b/doc/source/devref/filter_scheduler.rst @@ -107,6 +107,8 @@ There are some standard filter classes to use (:mod:`nova.scheduler.filters`): * |GroupAffinityFilter| - ensures that each instance in group is on a same host with one of the instance host in a group. * |AggregateMultiTenancyIsolation| - isolate tenants in specific aggregates. +* |AggregateImagePropertiesIsolation| - isolates hosts based on image + properties and aggregate metadata. Now we can focus on these standard filter classes in details. I will pass the simplest ones, such as |AllHostsFilter|, |CoreFilter| and |RamFilter| are, @@ -330,5 +332,5 @@ in :mod:``nova.tests.scheduler``. .. |AggregateTypeAffinityFilter| replace:: :class:`AggregateTypeAffinityFilter ` .. |AggregateInstanceExtraSpecsFilter| replace:: :class:`AggregateInstanceExtraSpecsFilter ` .. |AggregateMultiTenancyIsolation| replace:: :class:`AggregateMultiTenancyIsolation ` - .. |RamWeigher| replace:: :class:`RamWeigher ` +.. |AggregateImagePropertiesIsolation| replace:: :class:`AggregateImagePropertiesIsolation ` diff --git a/etc/nova/nova.conf.sample b/etc/nova/nova.conf.sample index fd4cc4c3cf8b..eba27156ce89 100644 --- a/etc/nova/nova.conf.sample +++ b/etc/nova/nova.conf.sample @@ -1735,6 +1735,19 @@ #scheduler_host_subset_size=1 +# +# Options defined in nova.scheduler.filters.aggregate_image_properties_isolation +# + +# Force the filter to consider only keys matching the given +# namespace. (string value) +#aggregate_image_properties_isolation_namespace= + +# The separator used between the namespace and keys (string +# value) +#aggregate_image_properties_isolation_separator=. + + # # Options defined in nova.scheduler.filters.core_filter # diff --git a/nova/scheduler/filters/aggregate_image_properties_isolation.py b/nova/scheduler/filters/aggregate_image_properties_isolation.py new file mode 100644 index 000000000000..2101cf0e9391 --- /dev/null +++ b/nova/scheduler/filters/aggregate_image_properties_isolation.py @@ -0,0 +1,68 @@ +# Copyright (c) 2013 Cloudwatt +# 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.config import cfg + +from nova import db +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import log as logging +from nova.scheduler import filters + +opts = [ + cfg.StrOpt('aggregate_image_properties_isolation_namespace', + help='Force the filter to consider only keys matching ' + 'the given namespace.'), + cfg.StrOpt('aggregate_image_properties_isolation_separator', + default=".", + help='The separator used between the namespace and keys'), +] +CONF = cfg.CONF +CONF.register_opts(opts) + +LOG = logging.getLogger(__name__) + + +class AggregateImagePropertiesIsolation(filters.BaseHostFilter): + """AggregateImagePropertiesIsolation works with image properties.""" + + # Aggregate data and instance type does not change within a request + run_filter_once_per_request = True + + def host_passes(self, host_state, filter_properties): + """Checks a host in an aggregate that metadata key/value match + with image properties. + """ + cfg_namespace = CONF.aggregate_image_properties_isolation_namespace + cfg_separator = CONF.aggregate_image_properties_isolation_separator + + spec = filter_properties.get('request_spec', {}) + image_props = spec.get('image', {}).get('properties', {}) + context = filter_properties['context'].elevated() + metadata = db.aggregate_metadata_get_by_host(context, host_state.host) + + for key, options in metadata.iteritems(): + if (cfg_namespace and + not key.startswith(cfg_namespace + cfg_separator)): + continue + prop = image_props.get(key) + if prop and prop not in options: + LOG.debug(_("%(host_state)s fails image aggregate properties " + "requirements. Property %(prop)s does not " + "match %(options)s."), + {'host_state': host_state, + 'prop': prop, + 'options': options}) + return False + return True diff --git a/nova/tests/scheduler/test_host_filters.py b/nova/tests/scheduler/test_host_filters.py index 52b44e13cd4d..c5e11ccbb42c 100644 --- a/nova/tests/scheduler/test_host_filters.py +++ b/nova/tests/scheduler/test_host_filters.py @@ -1643,3 +1643,105 @@ class HostFiltersTestCase(test.NoDBTestCase): self.pci_request_result = True self.assertRaises(AttributeError, filt_cls.host_passes, host, filter_properties) + + def test_aggregate_image_properties_isolation_passes(self): + self._stub_service_is_up(True) + filt_cls = self.class_map['AggregateImagePropertiesIsolation']() + aggr_meta = {'foo': 'bar'} + self._create_aggregate_with_host(name='fake1', + metadata=aggr_meta, + hosts=['host1']) + filter_properties = {'context': self.context, + 'request_spec': { + 'image': { + 'properties': {'foo': 'bar'}}}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertTrue(filt_cls.host_passes(host, filter_properties)) + + def test_aggregate_image_properties_isolation_multi_props_passes(self): + self._stub_service_is_up(True) + filt_cls = self.class_map['AggregateImagePropertiesIsolation']() + aggr_meta = {'foo': 'bar', 'foo2': 'bar2'} + self._create_aggregate_with_host(name='fake1', + metadata=aggr_meta, + hosts=['host1']) + filter_properties = {'context': self.context, + 'request_spec': { + 'image': { + 'properties': {'foo': 'bar', + 'foo2': 'bar2'}}}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertTrue(filt_cls.host_passes(host, filter_properties)) + + def test_aggregate_image_properties_isolation_props_with_meta_passes(self): + self._stub_service_is_up(True) + filt_cls = self.class_map['AggregateImagePropertiesIsolation']() + aggr_meta = {'foo': 'bar'} + self._create_aggregate_with_host(name='fake1', + metadata=aggr_meta, + hosts=['host1']) + filter_properties = {'context': self.context, + 'request_spec': { + 'image': { + 'properties': {}}}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertTrue(filt_cls.host_passes(host, filter_properties)) + + def test_aggregate_image_properties_isolation_props_imgprops_passes(self): + self._stub_service_is_up(True) + filt_cls = self.class_map['AggregateImagePropertiesIsolation']() + aggr_meta = {} + self._create_aggregate_with_host(name='fake1', + metadata=aggr_meta, + hosts=['host1']) + filter_properties = {'context': self.context, + 'request_spec': { + 'image': { + 'properties': {'foo': 'bar'}}}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertTrue(filt_cls.host_passes(host, filter_properties)) + + def test_aggregate_image_properties_isolation_props_not_match_fails(self): + self._stub_service_is_up(True) + filt_cls = self.class_map['AggregateImagePropertiesIsolation']() + aggr_meta = {'foo': 'bar'} + self._create_aggregate_with_host(name='fake1', + metadata=aggr_meta, + hosts=['host1']) + filter_properties = {'context': self.context, + 'request_spec': { + 'image': { + 'properties': {'foo': 'no-bar'}}}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertFalse(filt_cls.host_passes(host, filter_properties)) + + def test_aggregate_image_properties_isolation_props_not_match2_fails(self): + self._stub_service_is_up(True) + filt_cls = self.class_map['AggregateImagePropertiesIsolation']() + aggr_meta = {'foo': 'bar', 'foo2': 'bar2'} + self._create_aggregate_with_host(name='fake1', + metadata=aggr_meta, + hosts=['host1']) + filter_properties = {'context': self.context, + 'request_spec': { + 'image': { + 'properties': {'foo': 'bar', + 'foo2': 'bar3'}}}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertFalse(filt_cls.host_passes(host, filter_properties)) + + def test_aggregate_image_properties_isolation_props_namespace(self): + self._stub_service_is_up(True) + filt_cls = self.class_map['AggregateImagePropertiesIsolation']() + self.flags(aggregate_image_properties_isolation_namespace="np") + aggr_meta = {'np.foo': 'bar', 'foo2': 'bar2'} + self._create_aggregate_with_host(name='fake1', + metadata=aggr_meta, + hosts=['host1']) + filter_properties = {'context': self.context, + 'request_spec': { + 'image': { + 'properties': {'np.foo': 'bar', + 'foo2': 'bar3'}}}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertTrue(filt_cls.host_passes(host, filter_properties))