diff --git a/designate/scheduler/filters/attribute_filter.py b/designate/scheduler/filters/attribute_filter.py index 62a5d1e2d..cdf3fae4b 100644 --- a/designate/scheduler/filters/attribute_filter.py +++ b/designate/scheduler/filters/attribute_filter.py @@ -11,15 +11,18 @@ # 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 six from oslo_log import log as logging +from oslo_utils.strutils import bool_from_string +from designate.objects import PoolAttributeList from designate.scheduler.filters.base import Filter LOG = logging.getLogger(__name__) class AttributeFilter(Filter): - """This allows users top choose the pool by supplying hints to this filter. + """This allows users to choose the pool by supplying hints to this filter. These are provided as attributes as part of the zone object provided at zone create time. @@ -29,7 +32,7 @@ class AttributeFilter(Filter): { "attributes": { "pool_level": "gold", - "fast_ttl": True, + "fast_ttl": "true", "pops": "global", }, "email": "user@example.com", @@ -39,11 +42,6 @@ class AttributeFilter(Filter): The zone attributes are matched against the potential pool candiates, and any pools that do not match **all** hints are removed. - .. warning:: - - This filter is disabled currently, and should not be used. - It will be enabled at a later date. - .. warning:: This should be uses in conjunction with the @@ -58,5 +56,57 @@ class AttributeFilter(Filter): """ def filter(self, context, pools, zone): - # FIXME (graham) actually filter on attributes + + zone_attributes = zone.attributes.to_dict() + + def evaluate_pool(pool): + + pool_attributes = pool.attributes.to_dict() + + # Check if the keys requested exist in this pool + if not {key for key in six.iterkeys(pool_attributes)}.issuperset( + zone_attributes): + msg = "%(pool)s did not match list of requested attribute "\ + "keys - removing from list. Requested: %(r_key)s. Pool:"\ + "%(p_key)s" + + LOG.debug( + msg, + { + 'pool': pool, + 'r_key': zone_attributes, + 'p_key': pool_attributes + } + ) + # Missing required keys - remove from the list + return False + + for key in six.iterkeys(zone_attributes): + LOG.debug("Checking value of %s for %s", key, pool) + + pool_attr = bool_from_string(pool_attributes[key], + default=pool_attributes[key]) + zone_attr = bool_from_string(zone_attributes[key], + default=zone_attributes[key]) + + if not pool_attr == zone_attr: + LOG.debug( + "%(pool)s did not match requested value of %(key)s. " + "Requested: %(r_val)s. Pool: %(p_val)s", + { + 'pool': pool, + 'key': key, + 'r_val': zone_attr, + 'p_val': pool_attr + }) + # Value didn't match - remove from the list + return False + + # Pool matches list of attributes - keep + return True + + pool_list = [pool for pool in pools if evaluate_pool(pool)] + + pools = PoolAttributeList(objects=pool_list) + return pools diff --git a/designate/service_status.py b/designate/service_status.py index 0ce7a2af7..f2445fbf7 100644 --- a/designate/service_status.py +++ b/designate/service_status.py @@ -67,7 +67,7 @@ class HeartBeatEmitter(plugin.DriverPlugin): if not self._running: return - LOG.debug("Emitting heartbeat...") + LOG.trace("Emitting heartbeat...") status, stats, capabilities = self._get_status() diff --git a/designate/tests/unit/test_scheduler/test_filters.py b/designate/tests/unit/test_scheduler/test_filters.py index 8e1d9217f..39688b62a 100644 --- a/designate/tests/unit/test_scheduler/test_filters.py +++ b/designate/tests/unit/test_scheduler/test_filters.py @@ -22,6 +22,7 @@ from oslotest import base as test from designate.scheduler.filters import default_pool_filter from designate.scheduler.filters import fallback_filter from designate.scheduler.filters import pool_id_attribute_filter +from designate.scheduler.filters import attribute_filter from designate import objects from designate import context from designate import policy @@ -202,3 +203,221 @@ class SchedulerPoolIDAttributeFilterTest(SchedulerFilterTest): 'zone_create_forced_pool', self.context, pools[0]) + + +class SchedulerAttributeFilterTest(SchedulerFilterTest): + + FILTER = attribute_filter.AttributeFilter + + def setUp(self): + super(SchedulerAttributeFilterTest, self).setUp() + + self.zone = objects.Zone( + name="example.com.", + type="PRIMARY", + email="hostmaster@example.com", + attributes=objects.ZoneAttributeList.from_list( + [ + { + "key": "attribute_one", + "value": "True" + }, + { + "key": "attribute_two", + "value": "False" + }, + { + "key": "attribute_three", + "value": "foo" + } + ] + ) + ) + + def test_default_operation(self): + pools = objects.PoolList.from_list( + [ + { + "id": "6c346011-e581-429b-a7a2-6cdf0aba91c3", + } + ] + ) + + pools[0].attributes = objects.PoolAttributeList.from_list( + [ + { + "key": "attribute_one", + "value": "True" + }, + { + "key": "attribute_two", + "value": "False" + }, + { + "key": "attribute_three", + "value": "foo" + } + ]) + + pools = self.test_filter.filter(self.context, pools, self.zone) + + self.assertEqual("6c346011-e581-429b-a7a2-6cdf0aba91c3", pools[0].id) + + def test_multiple_pools_all_match(self): + pools = objects.PoolList.from_list( + [ + {"id": "6c346011-e581-429b-a7a2-6cdf0aba91c3"}, + {"id": "5fabcd37-262c-4cf3-8625-7f419434b6df"} + ] + + ) + + attributes = objects.PoolAttributeList.from_list( + [ + { + "key": "attribute_one", + "value": "True" + }, + { + "key": "attribute_two", + "value": "False" + }, + { + "key": "attribute_three", + "value": "foo" + } + ]) + + pools[0].attributes = attributes + pools[1].attributes = attributes + + pools = self.test_filter.filter(self.context, pools, self.zone) + + self.assertEqual(2, len(pools)) + + def test_multiple_pools_one_match(self): + pools = objects.PoolList.from_list( + [ + {"id": "6c346011-e581-429b-a7a2-6cdf0aba91c3"}, + {"id": "5fabcd37-262c-4cf3-8625-7f419434b6df"} + ] + + ) + + pool_0_attributes = objects.PoolAttributeList.from_list( + [ + { + "key": "attribute_one", + "value": "True" + }, + { + "key": "attribute_two", + "value": "False" + }, + { + "key": "attribute_three", + "value": "foo" + } + ]) + + pool_1_attributes = objects.PoolAttributeList.from_list( + [ + { + "key": "attribute_four", + "value": "True" + }, + { + "key": "attribute_five", + "value": "False" + }, + { + "key": "attribute_three", + "value": "foo" + } + ]) + + pools[0].attributes = pool_0_attributes + pools[1].attributes = pool_1_attributes + + pools = self.test_filter.filter(self.context, pools, self.zone) + + self.assertEqual(1, len(pools)) + self.assertEqual("6c346011-e581-429b-a7a2-6cdf0aba91c3", pools[0].id) + + def test_multiple_pools_no_match(self): + pools = objects.PoolList.from_list( + [ + {"id": "6c346011-e581-429b-a7a2-6cdf0aba91c3"}, + {"id": "5fabcd37-262c-4cf3-8625-7f419434b6df"} + ] + + ) + + pool_0_attributes = objects.PoolAttributeList.from_list( + [ + { + "key": "attribute_six", + "value": "True" + }, + { + "key": "attribute_two", + "value": "False" + }, + { + "key": "attribute_seven", + "value": "foo" + } + ]) + + pool_1_attributes = objects.PoolAttributeList.from_list( + [ + { + "key": "attribute_four", + "value": "True" + }, + { + "key": "attribute_five", + "value": "False" + }, + { + "key": "attribute_three", + "value": "foo" + } + ]) + + pools[0].attributes = pool_0_attributes + pools[1].attributes = pool_1_attributes + + pools = self.test_filter.filter(self.context, pools, self.zone) + + self.assertEqual(0, len(pools)) + + def test_no_match_non_bool(self): + pools = objects.PoolList.from_list( + [ + {"id": "6c346011-e581-429b-a7a2-6cdf0aba91c3"}, + ] + + ) + + pool_0_attributes = objects.PoolAttributeList.from_list( + [ + { + "key": "attribute_one", + "value": "True" + }, + { + "key": "attribute_two", + "value": "False" + }, + { + "key": "attribute_three", + "value": "bar" + } + ]) + + pools[0].attributes = pool_0_attributes + + pools = self.test_filter.filter(self.context, pools, self.zone) + + self.assertEqual(0, len(pools)) diff --git a/releasenotes/notes/attribute-filter-f06a53b61f5fd111.yaml b/releasenotes/notes/attribute-filter-f06a53b61f5fd111.yaml new file mode 100644 index 000000000..6ea1dff13 --- /dev/null +++ b/releasenotes/notes/attribute-filter-f06a53b61f5fd111.yaml @@ -0,0 +1,5 @@ +--- +features: + - Addition of the "attribute" filter for scheduling zones across pools. + This can be enabled in the ``[service:central]`` section of the config + by adding ``attribute`` to the list of values in the ``filters`` option. diff --git a/setup.cfg b/setup.cfg index a4046444c..cf081792c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -104,6 +104,7 @@ designate.quota = designate.scheduler.filters = fallback = designate.scheduler.filters.fallback_filter:FallbackFilter + attribute = designate.scheduler.filters.attribute_filter:AttributeFilter random = designate.scheduler.filters.random_filter:RandomFilter pool_id_attribute = designate.scheduler.filters.pool_id_attribute_filter:PoolIDAttributeFilter default_pool = designate.scheduler.filters.default_pool_filter:DefaultPoolFilter