From 953492904772933f5f8e265d1ae6cc1e6385fcc6 Mon Sep 17 00:00:00 2001 From: Jorge Niedbalski Date: Thu, 28 May 2020 14:57:49 -0400 Subject: [PATCH] Update zones masters using pool target masters. This change enforces the update of the zone masters for all zones that belongs to a particular pool, using the pool's defined target(s) masters and forcing a update_zone call. This change also, moves the backend base class update_zone method as an abstract method, allowing to each backend implementation to create its own update logic. For the case of bind9 its extended to allow running a rndc modzone with the new given masters for the zone fixing the behavior exposed on LP: #1879798. Fixes-Bug: #1879798 Change-Id: I9dddd4130a0cbb29311eeb52e077e216c8c03f3a Signed-off-by: Jorge Niedbalski (cherry picked from commit 3756fc51e71aaf0ba7cfb9155ca5d1de26ab78bc) --- designate/backend/impl_bind9.py | 37 +++++++++++ designate/manage/pool.py | 28 ++++++++ designate/tests/test_manage/__init__.py | 5 ++ .../tests/test_manage/test_update_pool.py | 65 +++++++++++++++++++ designate/tests/unit/backend/test_bind9.py | 12 ++++ 5 files changed, 147 insertions(+) create mode 100644 designate/tests/test_manage/__init__.py create mode 100644 designate/tests/test_manage/test_update_pool.py diff --git a/designate/backend/impl_bind9.py b/designate/backend/impl_bind9.py index 0c1c853c2..b27d18352 100644 --- a/designate/backend/impl_bind9.py +++ b/designate/backend/impl_bind9.py @@ -127,6 +127,43 @@ class Bind9Backend(base.Backend): LOG.warning('RNDC call failure: %s', e) raise + def update_zone(self, context, zone): + """ + Update a DNS zone. + + This will execute a rndc modzone as the zone + already exists but masters might need to be refreshed. + + :param context: Security context information. + :param zone: the DNS zone. + """ + LOG.debug('Update Zone') + + masters = [] + for master in self.masters: + host = master['host'] + port = master['port'] + masters.append('%s port %s' % (host, port)) + + # Ensure different MiniDNS instances are targeted for AXFRs + random.shuffle(masters) + + view = 'in %s' % self._view if self._view else '' + + rndc_op = [ + 'modzone', + '%s %s { type slave; masters { %s;}; file "slave.%s%s"; };' % + (zone['name'].rstrip('.'), view, '; '.join(masters), zone['name'], + zone['id']), + ] + + try: + self._execute_rndc(rndc_op) + except exceptions.Backend as e: + LOG.warning("Error updating zone: %s", e) + pass + super(Bind9Backend, self).update_zone(context, zone) + def _execute_rndc(self, rndc_op): """Execute rndc diff --git a/designate/manage/pool.py b/designate/manage/pool.py index 40d4aab3e..7d11777cf 100644 --- a/designate/manage/pool.py +++ b/designate/manage/pool.py @@ -23,6 +23,7 @@ import oslo_messaging as messaging from designate import exceptions from designate import rpc from designate import objects +from designate import policy from designate.central import rpcapi as central_rpcapi from designate.manage import base from designate.objects.adapters import DesignateAdapter @@ -43,6 +44,30 @@ class PoolCommands(base.Commands): rpc.init(cfg.CONF) self.central_api = central_rpcapi.CentralAPI() + def _update_zones(self, pool): + LOG.info("Updating zone masters for pool: {}".format(pool.id)) + + def __get_masters_from_pool(pool): + masters = [] + for target in pool.targets: + for master in target.get("masters", []): + masters.append({'host': master['host'], + 'port': master['port']}) + return masters + + policy.init() + + self.context.all_tenants = True + zones = self.central_api.find_zones( + self.context, + criterion={'pool_id': pool.id}) + + for zone in zones: + zone.masters = objects.ZoneMasterList().from_list( + __get_masters_from_pool(pool)) + self.central_api.update_zone(self.context, + zone) + @base.args('--file', help='The path to the file the yaml output should be ' 'written to', default='/etc/designate/pools.yaml') @@ -138,6 +163,9 @@ class PoolCommands(base.Commands): output_msg.append("Update Pool: %s" % pool) else: pool = self.central_api.update_pool(self.context, pool) + # Bug: Changes in the pool targets should trigger a + # zone masters update LP: #1879798. + self._update_zones(pool) except exceptions.PoolNotFound: pool = DesignateAdapter.parse('YAML', xpool, objects.Pool()) diff --git a/designate/tests/test_manage/__init__.py b/designate/tests/test_manage/__init__.py new file mode 100644 index 000000000..dd671f437 --- /dev/null +++ b/designate/tests/test_manage/__init__.py @@ -0,0 +1,5 @@ +from designate.tests import TestCase + + +class DesignateManageTestCase(TestCase): + pass diff --git a/designate/tests/test_manage/test_update_pool.py b/designate/tests/test_manage/test_update_pool.py new file mode 100644 index 000000000..d5b21fbcd --- /dev/null +++ b/designate/tests/test_manage/test_update_pool.py @@ -0,0 +1,65 @@ +# 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_log import log as logging + +import mock +from designate.tests.test_manage import DesignateManageTestCase +from designate.manage.pool import PoolCommands +from designate.tests import fixtures +from designate import objects + +LOG = logging.getLogger(__name__) + + +class UpdatePoolTestCase(DesignateManageTestCase): + def setUp(self): + super(DesignateManageTestCase, self).setUp() + self.stdlog = fixtures.StandardLogging() + self.useFixture(self.stdlog) + + def hydrate_pool_targets(self, targets): + pool_targets = objects.PoolTargetList() + masters = objects.PoolTargetMasterList() + for target in targets: + masters.append(target) + target = objects.PoolTarget(masters=masters) + target.masters = masters + pool_targets.append(target) + return pool_targets + + def test_update_pools_zones(self): + values = dict( + name='example.com.', + email='info@example.com', + type='PRIMARY' + ) + + zone = self.central_service.create_zone( + self.admin_context, zone=objects.Zone.from_dict(values)) + + # Ensure the correct NS Records are in place + pool = self.central_service.get_pool( + self.admin_context, zone.pool_id) + + pool.targets = self.hydrate_pool_targets([objects.PoolTargetMaster( + pool_target_id=pool.id, + host="127.0.0.1", + port="53")]) + + command = PoolCommands() + command.context = self.admin_context + command.central_api = self.central_service + + with mock.patch.object(self.central_service, + "update_zone") as mock_update_zone: + command._update_zones(pool) + mock_update_zone.assert_called_once() diff --git a/designate/tests/unit/backend/test_bind9.py b/designate/tests/unit/backend/test_bind9.py index 6934bec11..4a8654a6c 100644 --- a/designate/tests/unit/backend/test_bind9.py +++ b/designate/tests/unit/backend/test_bind9.py @@ -65,6 +65,18 @@ class Bind9BackendTestCase(designate.tests.TestCase): ] ) + @mock.patch.object(impl_bind9.Bind9Backend, '_execute_rndc') + def test_update_zone(self, mock_execute): + with fixtures.random_seed(0): + self.backend.update_zone(self.admin_context, self.zone) + + mock_execute.assert_called_with( + [ + 'modzone', + 'example.com { type slave; masters { 192.168.1.1 port 53; 192.168.1.2 port 35;}; file "slave.example.com.cca7908b-dad4-4c50-adba-fb67d4c556e8"; };' # noqa + ] + ) + @mock.patch.object(impl_bind9.Bind9Backend, '_execute_rndc') def test_create_zone_with_view(self, mock_execute): self.target['options'].append(