← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/maas-dns3 into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/maas-dns3 into lp:maas with lp:~rvb/maas/maas-dns-utils as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/maas-dns3/+merge/114832

This branch is the highlight in a series of branches to add DNS support to MAAS.  It finally ties it all together and adds (among other things) a test which: sets up a BIND server (with the fixture), registers MAAS' BIND configuration inside it, reloads the BIND server, creates a nodegroup, a node, a MAC Address and a valid lease, writes the config (the main config + the new zone), reloads the BIND server and - finally -, uses dig to request the IP for the node's hostname and compares it with the lease's information.

= Notes =

The main changes of this branch are the new methods in src/maasserver/dns.py (and the related tests in src/maasserver/tests/test_dns.py).

This branch fixes lots of small problems (which where uncovered by the fact that the new tests in test_dns.py finally makes real use of the BIND templates and, like I said earlier, ties it all together):

- add a setting DNS_RNDC_PORT to specify the RNDC port used by the MAAS-specific RNDC configuration (this is required to be able to set the RNDC config in tests using a non-privileged port).

- I've reduced the size of the generated node hostname (otherwise it's rejected by BIND).

- I got rid of InactiveDNSConfig for two reasons:
    - an inactive config can be generated creating DNSConfig with zone_names=() and reverse_zone_names=();
    - even when we write an inactive config we need the RNDC configuration (to be able to reload the config after it has been changed back to contain real zones).

- I've fixed test_next_zone_serial_returns_sequence to cope with the fact that the sequence doesn't get destructed and reconstructed between each test.  That's suboptimal I know but that branch is already big.

- I've used incremental_age=True when calling atomic_write in DNSZoneConfig.write, see https://code.launchpad.net/~rvb/maas/maas-dns-utils/+merge/114837 for a more detailed explanation about that.

- I've removed 'reload_dns_config' and 'reload_zone_config' and added a more generic 'rndc_command' because it turned out that we need to use quite a few rndc commands after all.

- I've changed most of the conf writing tasks to accept a callback argument.  This seems to be a pattern suggested by Celery to chain tasks.  We could also have used a Chord but that is really discouraged by the Celery documentation (see http://celery.github.com/celery/userguide/tasksets.html#chords for details).  This allow us to reload config or chain tasks together in a nice and intuitive way.
-- 
https://code.launchpad.net/~rvb/maas/maas-dns3/+merge/114832
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-dns3 into lp:maas.
=== modified file 'etc/celeryconfig.py'
--- etc/celeryconfig.py	2012-07-11 09:06:57 +0000
+++ etc/celeryconfig.py	2012-07-13 16:16:28 +0000
@@ -32,6 +32,10 @@
 # Location of MAAS' bind configuration files.
 DNS_CONFIG_DIR = '/var/cache/bind/maas'
 
+# RNDC port to be configured by MAAS to communicate with the BIND
+# server.
+DNS_RNDC_PORT = 954
+
 # DHCP leases file, as maintained by ISC dhcpd.
 DHCP_LEASES_FILE = '/var/lib/dhcp/dhcpd.leases'
 

=== modified file 'etc/democeleryconfig.py'
--- etc/democeleryconfig.py	2012-07-12 15:18:10 +0000
+++ etc/democeleryconfig.py	2012-07-13 16:16:28 +0000
@@ -26,3 +26,7 @@
 
 DNS_CONFIG_DIR = os.path.join(
     DEV_ROOT_DIRECTORY, 'run/named/')
+
+
+DNS_RNDC_PORT = 9154
+

=== modified file 'src/maasserver/dns.py'
--- src/maasserver/dns.py	2012-06-29 16:00:51 +0000
+++ src/maasserver/dns.py	2012-07-13 16:16:28 +0000
@@ -11,14 +11,22 @@
 
 __metaclass__ = type
 __all__ = [
+    'add_zone',
+    'change_dns_zone',
     'next_zone_serial',
+    'write_full_dns_config',
     ]
 
 
+from maasserver.models import (
+    DHCPLease,
+    NodeGroup,
+    )
 from maasserver.sequence import (
     INT_MAX,
     Sequence,
     )
+from provisioningserver import tasks
 
 # A DNS zone's serial is a 32-bit integer.  Also, we start with the
 # value 1 because 0 has special meaning for some DNS servers.  Even if
@@ -29,3 +37,48 @@
 
 def next_zone_serial():
     return '%0.10d' % zone_serial.nextval()
+
+
+def change_dns_zone(nodegroup):
+    mapping = DHCPLease.objects.get_hostname_ip_mapping(nodegroup)
+    zone_name = nodegroup.name
+    zone_reload_subtask = tasks.rndc_command.subtask(
+        args=['reload', zone_name])
+    tasks.write_dns_zone_config.delay(
+        zone_name=zone_name, domain=zone_name,
+        serial=next_zone_serial(), hostname_ip_mapping=mapping,
+        callback=zone_reload_subtask)
+
+
+def add_zone(nodegroup):
+    zone_names = [
+        result[0]
+        for result in NodeGroup.objects.all().values_list('name')]
+    tasks.write_dns_config(zone_names=zone_names)
+    mapping = DHCPLease.objects.get_hostname_ip_mapping(nodegroup)
+    zone_name = nodegroup.name
+    reconfig_subtask = tasks.rndc_command.subtask(args=['reconfig'])
+    write_dns_config_subtask = tasks.write_dns_config.subtask(
+        zone_names=zone_names, callback=reconfig_subtask)
+    tasks.write_dns_zone_config.delay(
+        zone_name=zone_name, domain=zone_name,
+        serial=next_zone_serial(), hostname_ip_mapping=mapping,
+        callback=write_dns_config_subtask)
+
+
+def write_full_dns_config():
+    groups = NodeGroup.objects.all()
+    serial = next_zone_serial()
+    zones = {
+        group.name: {
+            'serial': serial,
+            'zone_name': group.name,
+            'domain': group.name,
+            'hostname_ip_mapping': (
+                DHCPLease.objects.get_hostname_ip_mapping(
+                    group))
+            }
+        for group in groups
+        }
+    tasks.write_full_dns_config(
+        zones,  callback=tasks.rndc_command.subtask(args=['reload']))

=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py	2012-07-10 15:17:21 +0000
+++ src/maasserver/testing/factory.py	2012-07-13 16:16:28 +0000
@@ -90,7 +90,7 @@
                   created=None, **kwargs):
         # hostname=None is a valid value, hence the set_hostname trick.
         if hostname is '' and set_hostname:
-            hostname = self.getRandomString(255)
+            hostname = self.getRandomString(20)
         if status is None:
             status = NODE_STATUS.DEFAULT_STATUS
         node = Node(

=== modified file 'src/maasserver/tests/test_dns.py'
--- src/maasserver/tests/test_dns.py	2012-06-29 16:00:51 +0000
+++ src/maasserver/tests/test_dns.py	2012-07-13 16:16:28 +0000
@@ -12,11 +12,26 @@
 __metaclass__ = type
 __all__ = []
 
+
+from django.core.management import call_command
 from maasserver.dns import (
+    add_zone,
+    change_dns_zone,
     next_zone_serial,
+    write_full_dns_config,
     zone_serial,
     )
+from maasserver.testing.factory import factory
 from maasserver.testing.testcase import TestCase
+from maastesting.bindfixture import BINDServer
+from maastesting.celery import CeleryFixture
+from maastesting.tests.test_bindfixture import dig_call
+from provisioningserver.dns.config import conf
+from provisioningserver.tasks import (
+    setup_rndc_configuration,
+    write_dns_config,
+    )
+from testresources import FixtureResource
 from testtools.matchers import MatchesStructure
 
 
@@ -33,6 +48,66 @@
             )
 
     def test_next_zone_serial_returns_sequence(self):
+        initial = int(next_zone_serial())
         self.assertSequenceEqual(
-            ['%0.10d' % i for i in range(1, 11)],
-            [next_zone_serial() for i in range(10)])
+            ['%0.10d' % i for i in range(initial + 1, initial + 11)],
+            [next_zone_serial() for i in range(initial, initial + 10)])
+
+
+class TestDNSConfigModifications(TestCase):
+
+    resources = (
+        ("celery", FixtureResource(CeleryFixture())),
+        )
+
+    def setUp(self):
+        super(TestDNSConfigModifications, self).setUp()
+        self.bind = self.useFixture(BINDServer())
+        self.patch(conf, 'DNS_CONFIG_DIR', self.bind.config.homedir)
+
+        call_command(
+            'get_named_conf', edit=True,
+            config_path=self.bind.config.conf_file)
+        setup_rndc_configuration()
+        write_dns_config(inactive=True)
+        self.bind.runner.rndc('reload')
+
+    def create_nodegroup_with_lease(self, nodegroup=None):
+        if nodegroup is None:
+            nodegroup = factory.make_node_group()
+        node = factory.make_node(
+            nodegroup=nodegroup, set_hostname=True)
+        mac = factory.make_mac_address(node=node)
+        lease = factory.make_dhcp_lease(
+            nodegroup=nodegroup, mac=mac.mac_address)
+        return nodegroup, node, lease
+
+    def dig_resolve(self, fqdn):
+        return dig_call(
+            port=self.bind.config.port,
+            commands=[fqdn, '+short'])
+
+    def test_add_zone_loads_dns_zone(self):
+        nodegroup, node, lease = self.create_nodegroup_with_lease()
+        add_zone(nodegroup)
+
+        fqdn = "%s.%s" % (node.hostname, nodegroup.name)
+        self.assertEqual(lease.ip, self.dig_resolve(fqdn))
+
+    def test_change_zone_changes_dns_zone(self):
+        nodegroup, _, _ = self.create_nodegroup_with_lease()
+        write_full_dns_config()
+        nodegroup, new_node, new_lease = (
+            self.create_nodegroup_with_lease(nodegroup=nodegroup))
+        change_dns_zone(nodegroup)
+
+        fqdn = "%s.%s" % (new_node.hostname, nodegroup.name)
+        self.assertEqual(new_lease.ip, self.dig_resolve(fqdn))
+
+    def test_write_full_dns_loads_full_dns_config(self):
+        nodegroup, node, lease = self.create_nodegroup_with_lease()
+
+        write_full_dns_config()
+
+        fqdn = "%s.%s" % (node.hostname, nodegroup.name)
+        self.assertEqual(lease.ip, self.dig_resolve(fqdn))

=== modified file 'src/maastesting/tests/test_bindfixture.py'
--- src/maastesting/tests/test_bindfixture.py	2012-07-09 14:01:13 +0000
+++ src/maastesting/tests/test_bindfixture.py	2012-07-13 16:16:28 +0000
@@ -32,7 +32,7 @@
 from testtools.testcase import gather_details
 
 
-def dig_call(port=53, server='127.0.0.1', command=''):
+def dig_call(port=53, server='127.0.0.1', commands=None):
     """Call `dig` with the given command.
 
     Note that calling dig without a command will perform an NS
@@ -42,16 +42,19 @@
     :param port: Port of the queried DNS server (defaults to 53).
     :param server: IP address of the queried DNS server (defaults
         to '127.0.0.1').
-    :param command: Dig command to run (defaults to '').
+    :param commands: List of dig commands to run (defaults to None
+        which will perform an NS query for "." (the root)).
     :return: The output as a string.
     :rtype: basestring
     """
     cmd = [
         'dig', '+time=1', '+tries=1', '@%s' % server, '-p',
         '%d' % port]
-    if command != '':
-        cmd.append(command)
-    return check_output(cmd)
+    if commands != None:
+        if not isinstance(commands, list):
+            commands = (commands, )
+        cmd.extend(commands)
+    return check_output(cmd).strip()
 
 
 class TestBINDFixture(TestCase):

=== modified file 'src/provisioningserver/dns/config.py'
--- src/provisioningserver/dns/config.py	2012-07-11 15:14:48 +0000
+++ src/provisioningserver/dns/config.py	2012-07-13 16:16:28 +0000
@@ -13,7 +13,6 @@
 __all__ = [
     'DNSConfig',
     'DNSZoneConfig',
-    'InactiveDNSConfig',
     'setup_rndc',
     ]
 
@@ -22,6 +21,7 @@
     ABCMeta,
     abstractproperty,
     )
+from datetime import datetime
 import os.path
 from subprocess import (
     check_call,
@@ -29,7 +29,10 @@
     )
 
 from celery.conf import conf
-from provisioningserver.utils import atomic_write
+from provisioningserver.utils import (
+    atomic_write,
+    incremental_write,
+    )
 import tempita
 
 
@@ -83,7 +86,8 @@
     MAAS_RNDC_CONF_NAME and MAAS_NAMED_RNDC_CONF_NAME, both stored in
     conf.DNS_CONFIG_DIR.
     """
-    rndc_content, named_content = generate_rndc()
+    rndc_content, named_content = generate_rndc(
+        conf.DNS_RNDC_PORT)
 
     target_file = get_rndc_conf_path()
     with open(target_file, "wb") as f:
@@ -98,7 +102,10 @@
     """Execute a rndc command."""
     rndc_conf = os.path.join(
         conf.DNS_CONFIG_DIR, MAAS_RNDC_CONF_NAME)
-    check_call(['rndc', '-c', rndc_conf] + map(str, arguments))
+    with open(os.devnull, "ab") as devnull:
+        check_call(
+        ['rndc', '-c', rndc_conf] + map(str, arguments),
+        stdout=devnull)
 
 
 # Directory where the DNS configuration template files can be found.
@@ -108,6 +115,8 @@
 class DNSConfigBase:
     __metaclass__ = ABCMeta
 
+    incremental_write = False
+
     @abstractproperty
     def template_path(self):
         """Return the full path of the template to be used."""
@@ -144,7 +153,10 @@
         template = self.get_template()
         kwargs.update(self.get_extra_context())
         rendered = self.render_template(template, **kwargs)
-        atomic_write(rendered, self.target_path)
+        if self.incremental_write:
+            atomic_write(rendered, self.target_path)
+        else:
+            incremental_write(rendered, self.target_path)
 
 
 class DNSConfig(DNSConfigBase):
@@ -177,29 +189,21 @@
                 RevDNSZoneConfig(reverse_zone_name)
                 for reverse_zone_name in self.reverse_zone_names],
             'DNS_CONFIG_DIR': conf.DNS_CONFIG_DIR,
-            'named_rndc_conf_path':  get_named_rndc_conf_path()
+            'named_rndc_conf_path':  get_named_rndc_conf_path(),
+            'modified': unicode(datetime.today()),
         }
 
     def get_include_snippet(self):
         return '\ninclude "%s";\n' % self.target_path
 
 
-class InactiveDNSConfig(DNSConfig):
-    """A specialized version of DNSConfig that simply writes a blank/empty
-    configuration file.
-    """
-
-    def get_template(self):
-        """Return an empty template."""
-        return tempita.Template('', 'empty template')
-
-
 class DNSZoneConfig(DNSConfig):
     """A specialized version of DNSConfig that writes zone files."""
 
     template_file_name = 'zone.template'
     zone_name_string = '%s'
     zone_filename_string = 'zone.%s'
+    incremental_write = True
 
     def __init__(self, zone_name):
         self.zone_name = zone_name
@@ -218,7 +222,9 @@
             self.target_dir, self.zone_filename_string % self.zone_name)
 
     def get_extra_context(self):
-        return {}
+        return {
+            'modified': unicode(datetime.today()),
+        }
 
 
 class RevDNSZoneConfig(DNSZoneConfig):

=== modified file 'src/provisioningserver/dns/templates/zone.template'
--- src/provisioningserver/dns/templates/zone.template	2012-07-11 11:13:24 +0000
+++ src/provisioningserver/dns/templates/zone.template	2012-07-13 16:16:28 +0000
@@ -1,3 +1,7 @@
+; Zone file modified: {{modified}}.
+; Note that the modification time of this file doesn't reflect
+; the actual modification time.  MAAS controls the modification time
+; of this file to be able to force the zone to be reloaded by BIND.
 $TTL    300
 @   IN    SOA {{domain}}. nobody.example.com. (
               {{serial}} ; serial
@@ -8,6 +12,7 @@
               )
 
     IN  NS  {{domain}}.
-{{for host in hosts}}
-{{host['hostname']}} IN A {{host['ip']}}
+{{domain}}.  IN A 127.0.0.1; TODO: use master worker IP.
+{{for hostname, ip in hostname_ip_mapping.items()}}
+{{hostname}} IN A {{ip}}
 {{endfor}}

=== modified file 'src/provisioningserver/dns/tests/test_config.py'
--- src/provisioningserver/dns/tests/test_config.py	2012-07-11 15:14:48 +0000
+++ src/provisioningserver/dns/tests/test_config.py	2012-07-13 16:16:28 +0000
@@ -30,7 +30,6 @@
     DNSZoneConfig,
     execute_rndc_command,
     generate_rndc,
-    InactiveDNSConfig,
     MAAS_NAMED_CONF_NAME,
     MAAS_NAMED_RNDC_CONF_NAME,
     MAAS_RNDC_CONF_NAME,
@@ -77,9 +76,7 @@
         execute_rndc_command(command)
         rndc_conf_path = os.path.join(fake_dir, MAAS_RNDC_CONF_NAME)
         expected_command = ['rndc', '-c', rndc_conf_path, command]
-        self.assertSequenceEqual(
-            [((expected_command,), {})],
-            recorder.calls)
+        self.assertEqual((expected_command,), recorder.calls[0][0])
 
 
 class TestDNSConfig(TestCase):
@@ -147,19 +144,6 @@
                 Contains('include "%s"' % dnsconfig.target_path)))
 
 
-class TestInactiveDNSConfig(TestCase):
-    """Tests for InactiveDNSConfig."""
-
-    def test_write_config_writes_empty_config(self):
-        target_dir = self.make_dir()
-        self.patch(InactiveDNSConfig, 'target_dir', target_dir)
-        dnsconfig = InactiveDNSConfig()
-        dnsconfig.write_config()
-        self.assertThat(
-            os.path.join(target_dir, MAAS_NAMED_CONF_NAME),
-            FileContains(''))
-
-
 class TestDNSZoneConfig(TestCase):
     """Tests for DNSZoneConfig."""
 
@@ -185,17 +169,16 @@
         dnszoneconfig = DNSZoneConfig(zone_name)
         domain = factory.getRandomString()
         serial = random.randint(1, 100)
-        hosts = [{
-            'ip': factory.getRandomIPAddress(),
-            'hostname': factory.getRandomString()}]
+        hostname = factory.getRandomString()
+        ip = factory.getRandomIPAddress()
         dnszoneconfig.write_config(
-            domain=domain, serial=serial, hosts=hosts)
+            domain=domain, serial=serial,
+            hostname_ip_mapping={hostname: ip})
         self.assertThat(
             os.path.join(target_dir, 'zone.%s' % zone_name),
             FileContains(
                 matcher=ContainsAll(
                     [
                         'IN  NS  %s.' % domain,
-                        '%s IN A %s' % (
-                            hosts[0]['hostname'], hosts[0]['ip']),
+                        '%s IN A %s' % (hostname, ip),
                     ])))

=== modified file 'src/provisioningserver/tasks.py'
--- src/provisioningserver/tasks.py	2012-07-11 11:25:25 +0000
+++ src/provisioningserver/tasks.py	2012-07-13 16:16:28 +0000
@@ -13,22 +13,19 @@
 __all__ = [
     'power_off',
     'power_on',
-    'reload_dns_config',
-    'reload_zone_config',
+    'rndc_command',
+    'setup_rndc_configuration',
     'write_dns_config',
     'write_dns_zone_config',
     'write_full_dns_config',
-    'setup_rndc_configuration',
     ]
 
 
 from celery.task import task
-from celery.task.sets import subtask
 from provisioningserver.dns.config import (
     DNSConfig,
     DNSZoneConfig,
     execute_rndc_command,
-    InactiveDNSConfig,
     setup_rndc,
     )
 from provisioningserver.power.poweraction import (
@@ -97,20 +94,14 @@
 
 
 @task
-def reload_dns_config():
-    """Use rndc to reload the DNS configuration."""
-    execute_rndc_command('reload')
-
-
-@task
-def reload_zone_config(zone_name):
-    """Use rndc to reload the DNS configuration for a zone."""
-    execute_rndc_command('reload', zone_name)
+def rndc_command(*arguments):
+    """Use rndc to execute a command."""
+    execute_rndc_command(*arguments)
 
 
 @task
 def write_full_dns_config(zones=None, reverse_zones=None,
-                          **kwargs):
+                          callback=None, **kwargs):
     """Write out the DNS configuration files: the main configuration
     file and the zone files.
     :param zones: Mapping between zone names and the zone data used
@@ -120,6 +111,8 @@
         reverse zone data used to write the reverse zone config
         files.
     :type reverse_zones: dict
+    :param callback: Callback subtask.
+    :type callback: callable
     :param **kwargs: Keyword args passed to DNSConfig.write_config()
     """
     if zones is None:
@@ -137,52 +130,57 @@
         zone_names=list(zones),
         reverse_zone_names=list(reverse_zones))
     config.write_config(**kwargs)
-    subtask(reload_dns_config.subtask()).delay()
+    if callback is not None:
+        callback.delay()
 
 
 @task
-def write_dns_config(inactive=False, zone_names=(),
-                     reverse_zone_names=(), **kwargs):
+def write_dns_config(zone_names=(), reverse_zone_names=(),
+                     callback=None, **kwargs):
     """Write out the DNS configuration file.
 
-    :param inactive: Whether or not an inactive (i.e. blank)
-        configuration should be written. False by default.
-    :type inactive: boolean
     :param zone_names: List of zone names to include as part of the
         main config.
     :type zone_names: list
     :param reverse_zone_names: List of reverse zone names to include as part of
         the main config.
     :type reverse_zone_names: list
+    :param callback: Callback subtask.
+    :type callback: callable
     :param **kwargs: Keyword args passed to DNSConfig.write_config()
     """
-    if inactive:
-        InactiveDNSConfig().write_config()
-    else:
-        config = DNSConfig(
-            zone_names=zone_names,
-            reverse_zone_names=reverse_zone_names)
-        config.write_config(**kwargs)
-    subtask(reload_dns_config.subtask()).delay()
+    config = DNSConfig(
+        zone_names=zone_names,
+        reverse_zone_names=reverse_zone_names)
+    config.write_config(**kwargs)
+    if callback is not None:
+        callback.delay()
 
 
 @task
-def write_dns_zone_config(zone_name, **kwargs):
+def write_dns_zone_config(zone_name, callback=None, **kwargs):
     """Write out a DNS zone configuration file.
 
     :param zone_name: The identifier of the zone to write the configuration
         for.
     :type zone_name: basestring
+    :param callback: Callback subtask.
+    :type callback: callable
     :param **kwargs: Keyword args passed to DNSZoneConfig.write_config()
     """
     DNSZoneConfig(zone_name).write_config(**kwargs)
-    subtask(reload_zone_config.subtask(args=[zone_name])).delay()
+    if callback is not None:
+        callback.delay()
 
 
 @task
-def setup_rndc_configuration():
+def setup_rndc_configuration(callback=None):
     """Write out the two rndc configuration files (rndc.conf and
     named.conf.rndc).
+
+    :param callback: Callback subtask.
+    :type callback: callable
     """
     setup_rndc()
-    subtask(reload_dns_config.subtask()).delay()
+    if callback is not None:
+        callback.delay()

=== modified file 'src/provisioningserver/tests/test_tasks.py'
--- src/provisioningserver/tests/test_tasks.py	2012-07-11 15:25:05 +0000
+++ src/provisioningserver/tests/test_tasks.py	2012-07-13 16:16:28 +0000
@@ -33,8 +33,7 @@
 from provisioningserver.tasks import (
     power_off,
     power_on,
-    reload_dns_config,
-    reload_zone_config,
+    rndc_command,
     setup_rndc_configuration,
     write_dns_config,
     write_dns_zone_config,
@@ -130,7 +129,10 @@
 
     def test_write_dns_config_writes_file(self):
         zone_names = [random.randint(1, 100), random.randint(1, 100)]
-        result = write_dns_config.delay(inactive=False, zone_names=zone_names)
+        command = factory.getRandomString()
+        result = write_dns_config.delay(
+            zone_names=zone_names,
+            callback=rndc_command.subtask(args=[command]))
 
         self.assertThat(
             (
@@ -142,32 +144,17 @@
                 (
                     Equals(True),
                     FileExists(),
-                    Equals([(('reload',), {})]),
-                )),
-            result)
-
-    def test_write_dns_config_with_inactive_True(self):
-        result = write_dns_config.delay(inactive=True)
-
-        self.assertThat(
-            (
-                result.successful(),
-                os.path.join(self.dns_conf_dir, MAAS_NAMED_CONF_NAME),
-                self.rndc_recorder.calls,
-            ),
-            MatchesListwise(
-                (
-                    Equals(True),
-                    FileContains(''),
-                    Equals([(('reload',), {})]),
+                    Equals([((command,), {})]),
                 )),
             result)
 
     def test_write_dns_zone_config_writes_file(self):
+        command = factory.getRandomString()
         zone_name = factory.getRandomString()
         result = write_dns_zone_config.delay(
             zone_name=zone_name, domain=factory.getRandomString(),
-            serial=random.randint(1, 100), hosts=[])
+            serial=random.randint(1, 100), hostname_ip_mapping={},
+            callback=rndc_command.subtask(args=[command]))
 
         self.assertThat(
             (
@@ -179,12 +166,14 @@
                 (
                     Equals(True),
                     FileExists(),
-                    Equals([(('reload', zone_name), {})]),
+                    Equals([((command, ), {})]),
                 )),
             result)
 
     def test_setup_rndc_configuration_writes_files(self):
-        result = setup_rndc_configuration.delay()
+        command = factory.getRandomString()
+        result = setup_rndc_configuration.delay(
+            callback=rndc_command.subtask(args=[command]))
 
         self.assertThat(
             (
@@ -199,31 +188,20 @@
                     Equals(True),
                     FileExists(),
                     FileExists(),
-                    Equals([(('reload',), {})]),
+                    Equals([((command,), {})]),
                 )),
             result)
 
-    def test_reload_dns_config_issues_reload_command(self):
-        result = reload_dns_config.delay()
-
-        self.assertThat(
-            (result.successful(), self.rndc_recorder.calls),
-            MatchesListwise(
-                (
-                    Equals(True),
-                    Equals([(('reload',), {})]),
-                )))
-
-    def test_reload_zone_config_issues_zone_reload_command(self):
-        zone_name = factory.getRandomString()
-        result = reload_zone_config.delay(zone_name)
-
-        self.assertThat(
-            (result.successful(), self.rndc_recorder.calls),
-            MatchesListwise(
-                (
-                    Equals(True),
-                    Equals([(('reload', zone_name), {})]),
+    def test_rndc_command_execute_command(self):
+        command = factory.getRandomString()
+        result = rndc_command.delay(command)
+
+        self.assertThat(
+            (result.successful(), self.rndc_recorder.calls),
+            MatchesListwise(
+                (
+                    Equals(True),
+                    Equals([((command,), {})]),
                 )))
 
     def test_write_full_dns_config_sets_up_config(self):
@@ -236,11 +214,14 @@
         zones = {
             zone_name: {
                 'serial': random.randint(1, 100),
-                'hosts': [{'hostname': hostname, 'ip': ip}],
+                'hostname_ip_mapping': {hostname: ip},
                 'domain': domain,
             }
         }
-        result = write_full_dns_config.delay(zones=zones)
+        command = factory.getRandomString()
+        result = write_full_dns_config.delay(
+            zones=zones,
+            callback=rndc_command.subtask(args=[command]))
 
         self.assertThat(
             (
@@ -254,7 +235,7 @@
             MatchesListwise(
                 (
                     Equals(True),
-                    Equals([(('reload',), {})]),
+                    Equals([((command,), {})]),
                     FileExists(),
                     FileExists(),
                 )))