diff --git a/tripleo_common/filters/__init__.py b/tripleo_common/filters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleo_common/filters/capabilities_filter.py b/tripleo_common/filters/capabilities_filter.py new file mode 100644 index 000000000..df65488c6 --- /dev/null +++ b/tripleo_common/filters/capabilities_filter.py @@ -0,0 +1,35 @@ +# Copyright 2016 Red Hat, Inc. +# +# 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 nova.scheduler import filters + + +class TripleOCapabilitiesFilter(filters.BaseHostFilter): + """Filter hosts based on capabilities in boot request + + The standard Nova ComputeCapabilitiesFilter does not respect capabilities + requested in the scheduler_hints field, so we need a custom one in order + to be able to do predictable placement of nodes. + """ + + # list of hosts doesn't change within a request + run_filter_once_per_request = True + + def host_passes(self, host_state, spec_obj): + host_node = host_state.stats.get('node') + instance_node = spec_obj.scheduler_hints.get('capabilities:node') + # The instance didn't request a specific node + if not instance_node: + return True + return host_node == instance_node[0] diff --git a/tripleo_common/filters/list.py b/tripleo_common/filters/list.py new file mode 100644 index 000000000..d125c92ca --- /dev/null +++ b/tripleo_common/filters/list.py @@ -0,0 +1,27 @@ +# Copyright 2016 Red Hat, Inc. +# +# 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 nova + +from tripleo_common.filters import capabilities_filter + + +def tripleo_filters(): + """Return a list of filter classes for TripleO + + This is a wrapper around the Nova all_filters function so we can add our + filters to the resulting list. + """ + nova_filters = nova.scheduler.filters.all_filters() + return (nova_filters + [capabilities_filter.TripleOCapabilitiesFilter]) diff --git a/tripleo_common/tests/fake_nova/README b/tripleo_common/tests/fake_nova/README new file mode 100644 index 000000000..894926aa6 --- /dev/null +++ b/tripleo_common/tests/fake_nova/README @@ -0,0 +1,4 @@ +We don't want to pull in all of Nova and, more importantly, all of its +numerous dependencies just for the sake of having one class to inherit +from in our custom filter. Instead, this module will be injected into +sys.modules as 'nova' when we run unit tests that rely on it. diff --git a/tripleo_common/tests/fake_nova/__init__.py b/tripleo_common/tests/fake_nova/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleo_common/tests/fake_nova/scheduler/__init__.py b/tripleo_common/tests/fake_nova/scheduler/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleo_common/tests/fake_nova/scheduler/filters.py b/tripleo_common/tests/fake_nova/scheduler/filters.py new file mode 100644 index 000000000..c80d4b846 --- /dev/null +++ b/tripleo_common/tests/fake_nova/scheduler/filters.py @@ -0,0 +1,18 @@ +# Copyright 2016 Red Hat, Inc. +# +# 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. + + +class BaseHostFilter(object): + pass diff --git a/tripleo_common/tests/test_filters.py b/tripleo_common/tests/test_filters.py new file mode 100644 index 000000000..854f71cc2 --- /dev/null +++ b/tripleo_common/tests/test_filters.py @@ -0,0 +1,68 @@ +# Copyright 2016 Red Hat, Inc. +# +# 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 sys + +import mock + +from tripleo_common.tests import base +from tripleo_common.tests import fake_nova + +# See the README file in the fake_nova module directory for details on why +# this is being done. +if 'nova' not in sys.modules: + sys.modules['nova'] = fake_nova +else: + raise RuntimeError('nova module already found in sys.modules. The ' + 'fake_nova injection should be removed.') +from tripleo_common.filters import capabilities_filter + + +class TestCapabilitiesFilter(base.TestCase): + def test_no_requested_node(self): + instance = capabilities_filter.TripleOCapabilitiesFilter() + host_state = mock.Mock() + host_state.stats.get.return_value = '' + spec_obj = mock.Mock() + spec_obj.scheduler_hints.get.return_value = [] + self.assertTrue(instance.host_passes(host_state, spec_obj)) + + def test_requested_node_matches(self): + def mock_host_get(key): + if key == 'node': + return 'compute-0' + else: + self.fail('Unexpected key requested by filter') + + def mock_spec_get(key): + if key == 'capabilities:node': + return ['compute-0'] + else: + self.fail('Unexpected key requested by filter') + + instance = capabilities_filter.TripleOCapabilitiesFilter() + host_state = mock.Mock() + host_state.stats.get.side_effect = mock_host_get + spec_obj = mock.Mock() + spec_obj.scheduler_hints.get.side_effect = mock_spec_get + self.assertTrue(instance.host_passes(host_state, spec_obj)) + + def test_requested_node_no_match(self): + instance = capabilities_filter.TripleOCapabilitiesFilter() + host_state = mock.Mock() + host_state.stats.get.return_value = 'controller-0' + spec_obj = mock.Mock() + spec_obj.scheduler_hints.get.return_value = ['compute-0'] + self.assertFalse(instance.host_passes(host_state, spec_obj))