← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/maas/tasks-with-forward-and-reverse-zones into lp:maas

 

Gavin Panella has proposed merging lp:~allenap/maas/tasks-with-forward-and-reverse-zones into lp:maas with lp:~allenap/maas/split-forward-and-reverse-zones as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/maas/tasks-with-forward-and-reverse-zones/+merge/127182

This branch set out to do one thing: update gen_zones() and
DNSForwardZoneConfig to handle correctly the situation where more than
one nodegroup shares a domain name.

However, in order to do this safely and correctly, I felt that it was
necessary to add validation to NodeGroupInterface. By the time I
realised this was becoming a large piece of work it was beyond the
point that breaking it out into a new branch would be easy. The change
itself was relatively easy, but the test fall-out, from tests using
semi-garbage data, was substantial.

The good news is that the fall-out is mostly mechanical stuff, easy to
review, and its bittiness is a large contributor to the line-count
here.

I've also made more use of the netaddr library, which makes light work
of a lot of fiddly IP address stuff.

$ bzr diff -r :prev | diffstat
 maasserver/dns.py                                    |  116 ++++++++++++-------
 maasserver/models/nodegroupinterface.py              |   54 ++++++++
 maasserver/testing/factory.py                        |   25 +---
 maasserver/tests/test_api.py                         |    7 -
 maasserver/tests/test_commands_config_master_dhcp.py |   17 +-
 maasserver/tests/test_dhcp.py                        |   12 +
 maasserver/tests/test_dns.py                         |  109 +++++++++++++----
 maasserver/tests/test_fields.py                      |   12 -
 maasserver/tests/test_forms.py                       |   22 +--
 maasserver/tests/test_nodegroup.py                   |   30 ++--
 provisioningserver/dns/config.py                     |   33 +++--
 provisioningserver/dns/tests/test_config.py          |   69 ++++++-----
 provisioningserver/tasks.py                          |    9 -
 provisioningserver/testing/__init__.py               |   29 ----
 provisioningserver/tests/test_tasks.py               |   43 ++++---
 15 files changed, 367 insertions(+), 220 deletions(-)

-- 
https://code.launchpad.net/~allenap/maas/tasks-with-forward-and-reverse-zones/+merge/127182
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/maas/tasks-with-forward-and-reverse-zones into lp:maas.
=== modified file 'src/maasserver/dns.py'
--- src/maasserver/dns.py	2012-09-20 10:24:32 +0000
+++ src/maasserver/dns.py	2012-10-01 00:25:26 +0000
@@ -21,6 +21,7 @@
 
 
 import collections
+from itertools import groupby
 import logging
 import socket
 
@@ -44,7 +45,10 @@
     IPNetwork,
     )
 from provisioningserver import tasks
-from provisioningserver.dns.config import DNSZoneConfig
+from provisioningserver.dns.config import (
+    DNSForwardZoneConfig,
+    DNSReverseZoneConfig,
+    )
 
 # 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
@@ -108,39 +112,71 @@
     return ip
 
 
-def get_zone(nodegroup, serial=None):
-    """Create a :class:`DNSZoneConfig` object from a nodegroup.
-
-    Return a :class:`DNSZoneConfig` if DHCP is enabled on the
-    nodegroup or None if it is not the case.
+def gen_zones(nodegroups, serial=None):
+    """Generate zones describing those relating to the given node groups.
 
     This method also accepts a serial to reuse the same serial when
-    we are creating DNSZoneConfig objects in bulk.
+    we are creating config objects in bulk.
     """
-    if not is_dns_managed(nodegroup):
-        return None
-    interface = nodegroup.get_managed_interface()
-
-    if serial is None:
-        serial = next_zone_serial()
+    get_domain = lambda nodegroup: nodegroup.name
+    # Generate forward zones for all managed nodegroups with the same domain
+    # as the domain of any of the given nodegroups.
+    forward_nodegroups = {
+        nodegroup for nodegroup in NodeGroup.objects.filter(
+            name__in={get_domain(nodegroup) for nodegroup in nodegroups})
+        if is_dns_managed(nodegroup)
+        }
+    # Generate only reverse zones for the given nodegroups; no searching for
+    # overlapping networks or anything like that... for now.
+    reverse_nodegroups = {
+        nodegroup for nodegroup in nodegroups
+        if is_dns_managed(nodegroup)
+        }
+    # Skip out now if there are no nodegroups to deal with.
+    if len(forward_nodegroups) == 0 and len(reverse_nodegroups) == 0:
+        return
+    # Assemble the set of all nodegroups to be operated on.
+    nodegroups = set().union(forward_nodegroups, reverse_nodegroups)
+    # Caches for various things.
+    mappings = {
+        nodegroup: DHCPLease.objects.get_hostname_ip_mapping(nodegroup)
+        for nodegroup in nodegroups
+        }
+    interfaces = {
+        nodegroup: nodegroup.get_managed_interface()
+        for nodegroup in nodegroups
+        }
+    networks = {
+        nodegroup: interface.network
+        for nodegroup, interface in interfaces.items()
+        }
+    # Useful stuff.
+    serial = next_zone_serial() if serial is None else serial
     dns_ip = get_dns_server_address()
-    return DNSZoneConfig(
-        zone_name=nodegroup.name, serial=serial, dns_ip=dns_ip,
-        subnet_mask=interface.subnet_mask,
-        broadcast_ip=interface.broadcast_ip,
-        ip_range_low=interface.ip_range_low,
-        ip_range_high=interface.ip_range_high,
-        mapping=DHCPLease.objects.get_hostname_ip_mapping(nodegroup))
-
-
-def get_zones(nodegroups, serial):
-    """Return a list of non-None :class:`DNSZoneConfig` from nodegroups."""
-    return filter(
-        None,
-        [
-            get_zone(nodegroup, serial)
-            for nodegroup in nodegroups
-        ])
+    # Forward zones, collated by domain name.
+    forward_nodegroups = sorted(forward_nodegroups, key=get_domain)
+    for domain, nodegroups in groupby(forward_nodegroups, get_domain):
+        nodegroups = list(nodegroups)
+        # A forward zone encompassing all nodes in the same domain.
+        yield DNSForwardZoneConfig(
+            domain, serial=serial, dns_ip=dns_ip,
+            mapping={
+                hostname: ip
+                for nodegroup in nodegroups
+                for hostname, ip in mappings[nodegroup].items()
+                },
+            networks={
+                networks[nodegroup]
+                for nodegroup in nodegroups
+                },
+            )
+    # Reverse zones, sorted by network.
+    reverse_nodegroups = sorted(reverse_nodegroups, key=networks.get)
+    for nodegroup in reverse_nodegroups:
+        yield DNSReverseZoneConfig(
+            get_domain(nodegroup), serial=serial, dns_ip=dns_ip,
+            mapping=mappings[nodegroup],
+            network=networks[nodegroup])
 
 
 def change_dns_zones(nodegroups):
@@ -155,15 +191,12 @@
     if not isinstance(nodegroups, collections.Iterable):
         nodegroups = [nodegroups]
     serial = next_zone_serial()
-    zones = get_zones(nodegroups, serial)
+    zones = gen_zones(nodegroups, serial)
     for zone in zones:
-        reverse_zone_reload_subtask = tasks.rndc_command.subtask(
-            args=[['reload', zone.reverse_zone_name]])
         zone_reload_subtask = tasks.rndc_command.subtask(
-            args=[['reload', zone.zone_name]],
-            callback=reverse_zone_reload_subtask)
+            args=[['reload', zone.zone_name]])
         tasks.write_dns_zone_config.delay(
-            zone=zone, callback=zone_reload_subtask)
+            zones=[zone], callback=zone_reload_subtask)
 
 
 def add_zone(nodegroup):
@@ -178,17 +211,17 @@
     """
     if not is_dns_enabled():
         return
-    zone = get_zone(nodegroup)
-    if zone is None:
+    zones_to_write = list(gen_zones([nodegroup]))
+    if len(zones_to_write) == 0:
         return None
     serial = next_zone_serial()
     # Compute non-None zones.
-    zones = get_zones(NodeGroup.objects.all(), serial)
+    zones = list(gen_zones(NodeGroup.objects.all(), serial))
     reconfig_subtask = tasks.rndc_command.subtask(args=[['reconfig']])
     write_dns_config_subtask = tasks.write_dns_config.subtask(
         zones=zones, callback=reconfig_subtask)
     tasks.write_dns_zone_config.delay(
-        zone=zone, callback=write_dns_config_subtask)
+        zones=zones_to_write, callback=write_dns_config_subtask)
 
 
 def write_full_dns_config(active=True, reload_retry=False):
@@ -205,8 +238,7 @@
     if not is_dns_enabled():
         return
     if active:
-        serial = next_zone_serial()
-        zones = get_zones(NodeGroup.objects.all(), serial)
+        zones = list(gen_zones(NodeGroup.objects.all()))
     else:
         zones = []
     tasks.write_full_dns_config.delay(

=== modified file 'src/maasserver/models/nodegroupinterface.py'
--- src/maasserver/models/nodegroupinterface.py	2012-09-28 11:14:48 +0000
+++ src/maasserver/models/nodegroupinterface.py	2012-10-01 00:25:26 +0000
@@ -15,6 +15,9 @@
     ]
 
 
+from collections import defaultdict
+
+from django.core.exceptions import ValidationError
 from django.db.models import (
     CharField,
     ForeignKey,
@@ -26,10 +29,15 @@
     NODEGROUPINTERFACE_MANAGEMENT,
     NODEGROUPINTERFACE_MANAGEMENT_CHOICES,
     )
+from maasserver.models.cleansave import CleanSave
 from maasserver.models.timestampedmodel import TimestampedModel
-
-
-class NodeGroupInterface(TimestampedModel):
+from netaddr import (
+    IPAddress,
+    IPNetwork,
+    )
+
+
+class NodeGroupInterface(CleanSave, TimestampedModel):
 
     class Meta(DefaultMeta):
         unique_together = ('nodegroup', 'interface')
@@ -59,5 +67,45 @@
     ip_range_high = GenericIPAddressField(
         editable=True, unique=True, blank=True, null=True, default=None)
 
+    @property
+    def network(self):
+        """Return the network defined by the broadcast address and net mask.
+
+        If either the broadcast address or the subnet mask is unset, returns
+        None.
+
+        :return: :class:`IPNetwork`
+        """
+        network = self.broadcast_ip, self.subnet_mask
+        return IPNetwork("%s/%s" % network) if all(network) else None
+
     def __repr__(self):
         return "<NodeGroupInterface %r,%s>" % (self.nodegroup, self.interface)
+
+    def clean_network(self):
+        """Ensure that the network settings are all congruent.
+
+        Specifically, it ensures that the interface address, router address,
+        and the address range, all fall within the network defined by the
+        broadcast address and subnet mask.
+        """
+        network = self.network
+        if network is None:
+            return
+        network_settings = (
+            ("ip", self.ip),
+            ("router_ip", self.router_ip),
+            ("ip_range_low", self.ip_range_low),
+            ("ip_range_high", self.ip_range_high),
+            )
+        network_errors = defaultdict(list)
+        for field, address in network_settings:
+            if address and IPAddress(address) not in network:
+                network_errors[field].append(
+                    "%s not in the %s network" % (address, network))
+        if len(network_errors) != 0:
+            raise ValidationError(network_errors)
+
+    def clean(self, *args, **kwargs):
+        super(NodeGroupInterface, self).clean(*args, **kwargs)
+        self.clean_network()

=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py	2012-09-25 11:21:25 +0000
+++ src/maasserver/testing/factory.py	2012-10-01 00:25:26 +0000
@@ -131,9 +131,6 @@
         subnet_mask, broadcast_ip, ip_range_low, ip_range_high, router_ip and
         worker_ip. This is a convenience to setup a coherent network all in
         one go.
-
-        Otherwise, use the provided values for these values or use random IP
-        addresses if they are not provided.
         """
         if status is None:
             status = factory.getRandomEnum(NODEGROUP_STATUS)
@@ -143,26 +140,20 @@
             name = self.make_name('nodegroup')
         if uuid is None:
             uuid = factory.getRandomUUID()
-        if network is not None:
+        if network is None:
+            network = factory.getRandomNetwork()
+        if subnet_mask is None:
             subnet_mask = str(network.netmask)
+        if broadcast_ip is None:
             broadcast_ip = str(network.broadcast)
+        if ip_range_low is None:
             ip_range_low = str(IPAddress(network.first))
+        if ip_range_high is None:
             ip_range_high = str(IPAddress(network.last))
+        if router_ip is None:
             router_ip = factory.getRandomIPInNetwork(network)
+        if ip is None:
             ip = factory.getRandomIPInNetwork(network)
-        else:
-            if subnet_mask is None:
-                subnet_mask = self.getRandomIPAddress()
-            if broadcast_ip is None:
-                broadcast_ip = self.getRandomIPAddress()
-            if ip_range_low is None:
-                ip_range_low = self.getRandomIPAddress()
-            if ip_range_high is None:
-                ip_range_high = self.getRandomIPAddress()
-            if router_ip is None:
-                router_ip = self.getRandomIPAddress()
-            if ip is None:
-                ip = self.getRandomIPAddress()
         if interface is None:
             interface = self.make_name('interface')
         ng = NodeGroup.objects.new(

=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py	2012-09-29 21:27:52 +0000
+++ src/maasserver/tests/test_api.py	2012-10-01 00:25:26 +0000
@@ -22,6 +22,7 @@
     datetime,
     timedelta,
     )
+from functools import partial
 import httplib
 from itertools import izip
 import json
@@ -3162,7 +3163,11 @@
         self.become_admin()
         nodegroup = factory.make_node_group()
         interface = nodegroup.get_managed_interface()
-        new_ip_range_high = factory.getRandomIPAddress()
+        get_ip_in_network = partial(
+            factory.getRandomIPInNetwork, interface.network)
+        new_ip_range_high = next(
+            ip for ip in iter(get_ip_in_network, None)
+            if ip != interface.ip_range_high)
         response = self.client.put(
             reverse(
                 'nodegroupinterface_handler',

=== modified file 'src/maasserver/tests/test_commands_config_master_dhcp.py'
--- src/maasserver/tests/test_commands_config_master_dhcp.py	2012-09-20 15:10:21 +0000
+++ src/maasserver/tests/test_commands_config_master_dhcp.py	2012-10-01 00:25:26 +0000
@@ -34,22 +34,21 @@
 
 def make_dhcp_settings():
     """Return an arbitrary dict of DHCP settings."""
+    network = factory.getRandomNetwork()
     return {
-        'ip': '10.111.123.10',
+        'ip': factory.getRandomIPInNetwork(network),
         'interface': factory.make_name('interface'),
-        'subnet_mask': '255.255.0.0',
-        'broadcast_ip': '10.111.255.255',
-        'router_ip': factory.getRandomIPAddress(),
-        'ip_range_low': '10.111.123.9',
-        'ip_range_high': '10.111.244.18',
+        'subnet_mask': str(network.netmask),
+        'broadcast_ip': str(network.broadcast),
+        'router_ip': factory.getRandomIPInNetwork(network),
+        'ip_range_low': factory.getRandomIPInNetwork(network),
+        'ip_range_high': factory.getRandomIPInNetwork(network),
     }
 
 
 def make_cleared_dhcp_settings():
     """Return dict of cleared DHCP settings."""
-    return {
-        setting: None
-        for setting in make_dhcp_settings().keys()}
+    return dict.fromkeys(make_dhcp_settings())
 
 
 class TestConfigMasterDHCP(TestCase):

=== modified file 'src/maasserver/tests/test_dhcp.py'
--- src/maasserver/tests/test_dhcp.py	2012-09-25 14:45:59 +0000
+++ src/maasserver/tests/test_dhcp.py	2012-10-01 00:25:26 +0000
@@ -12,6 +12,8 @@
 __metaclass__ = type
 __all__ = []
 
+from functools import partial
+
 from django.conf import settings
 from maasserver import dhcp
 from maasserver.dhcp import (
@@ -24,6 +26,7 @@
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import TestCase
 from maastesting.celery import CeleryFixture
+from netaddr import IPNetwork
 from provisioningserver import tasks
 from testresources import FixtureResource
 
@@ -60,8 +63,7 @@
             status=NODEGROUP_STATUS.ACCEPTED,
             dhcp_key=factory.getRandomString(),
             interface=factory.make_name('eth'),
-            ip_range_low='192.168.102.1', ip_range_high='192.168.103.254',
-            subnet_mask='255.255.252.0', broadcast_ip='192.168.103.255')
+            network=IPNetwork("192.168.102.0/22"))
 
         self.patch(settings, "DHCP_CONNECT", True)
         configure_dhcp(nodegroup)
@@ -134,7 +136,11 @@
         interface = nodegroup.get_managed_interface()
         self.patch(settings, "DHCP_CONNECT", True)
         self.patch(dhcp, 'write_dhcp_config')
-        new_router_ip = factory.getRandomIPAddress()
+        get_ip_in_network = partial(
+            factory.getRandomIPInNetwork, interface.network)
+        new_router_ip = next(
+            ip for ip in iter(get_ip_in_network, None)
+            if ip != interface.router_ip)
         interface.router_ip = new_router_ip
         interface.save()
         args, kwargs = dhcp.write_dhcp_config.apply_async.call_args

=== modified file 'src/maasserver/tests/test_dns.py'
--- src/maasserver/tests/test_dns.py	2012-09-20 10:24:32 +0000
+++ src/maasserver/tests/test_dns.py	2012-10-01 00:25:26 +0000
@@ -13,8 +13,8 @@
 __all__ = []
 
 
+from functools import partial
 from itertools import islice
-import random
 import socket
 
 from celery.task import task
@@ -28,7 +28,6 @@
     NODEGROUP_STATUS,
     NODEGROUPINTERFACE_MANAGEMENT,
     )
-from maasserver.models.dhcplease import DHCPLease
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import TestCase
 from maastesting.bindfixture import BINDServer
@@ -42,12 +41,18 @@
 from provisioningserver import tasks
 from provisioningserver.dns.config import (
     conf,
-    DNSZoneConfig,
+    DNSForwardZoneConfig,
+    DNSReverseZoneConfig,
+    DNSZoneConfigBase,
     )
 from provisioningserver.dns.utils import generated_hostname
 from rabbitfixture.server import allocate_ports
 from testresources import FixtureResource
-from testtools.matchers import MatchesStructure
+from testtools.matchers import (
+    IsInstance,
+    MatchesAll,
+    MatchesStructure,
+    )
 
 
 class TestDNSUtilities(TestCase):
@@ -92,25 +97,6 @@
         self.patch_DEFAULT_MAAS_URL_with_random_values()
         self.assertRaises(dns.DNSException, dns.get_dns_server_address)
 
-    def test_get_zone_creates_DNSZoneConfig(self):
-        nodegroup = factory.make_node_group(
-            status=NODEGROUP_STATUS.ACCEPTED,
-            management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
-        interface = nodegroup.get_managed_interface()
-        serial = random.randint(1, 100)
-        zone = dns.get_zone(nodegroup, serial)
-        self.assertAttributes(
-            zone,
-            dict(
-                zone_name=nodegroup.name,
-                serial=serial,
-                subnet_mask=interface.subnet_mask,
-                broadcast_ip=interface.broadcast_ip,
-                ip_range_low=interface.ip_range_low,
-                ip_range_high=interface.ip_range_high,
-                mapping=DHCPLease.objects.get_hostname_ip_mapping(nodegroup),
-                ))
-
     def test_is_dns_managed(self):
         nodegroups_with_expected_results = {
             factory.make_node_group(
@@ -303,6 +289,8 @@
                 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
         interface = nodegroup.get_managed_interface()
         # Edit nodegroup's network information to '192.168.44.1/24'
+        interface.ip = '192.168.44.7'
+        interface.router_ip = '192.168.44.14'
         interface.broadcast_ip = '192.168.44.255'
         interface.netmask = '255.255.255.0'
         interface.ip_range_low = '192.168.44.0'
@@ -349,7 +337,80 @@
         self.patch(settings, "DNS_CONNECT", True)
         nodegroup, node, lease = self.create_nodegroup_with_lease()
         recorder = FakeMethod()
-        self.patch(DNSZoneConfig, 'write_config', recorder)
+        self.patch(DNSZoneConfigBase, 'write_config', recorder)
         node.error = factory.getRandomString()
         node.save()
         self.assertEqual(0, recorder.call_count)
+
+
+class TestZoneGeneration(TestCase):
+    """Tests for `dns.gen_zones`."""
+
+    def test_gen_zones_with_no_nodegroups_yields_nothing(self):
+        self.assertEqual([], list(dns.gen_zones(())))
+
+    def test_gen_zones_with_many_nodegroups(self):
+        # This demonstrates gen_zones in all-singing all-dancing mode.
+        make_node_group = partial(
+            factory.make_node_group, status=NODEGROUP_STATUS.ACCEPTED,
+            management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
+        nodegroups = [
+            make_node_group(name="one", network=IPNetwork("10/32")),
+            make_node_group(name="one", network=IPNetwork("11/32")),
+            make_node_group(name="two", network=IPNetwork("20/32")),
+            make_node_group(name="two", network=IPNetwork("21/32")),
+            ]
+        [  # Other nodegroups.
+            make_node_group(name="one", network=IPNetwork("12/32")),
+            make_node_group(name="two", network=IPNetwork("22/32")),
+            ]
+        zones = list(dns.gen_zones(nodegroups))
+        self.assertEqual(6, len(zones), zones)
+        f_one, f_two, r_one_0, r_one_1, r_two_0, r_two_1 = zones
+        # For the forward zones, all nodegroups sharing a domain name, even
+        # those not passed into gen_zones(), are consolidated into a single
+        # forward zone description.
+        self.assertThat(
+            f_one, MatchesAll(
+                IsInstance(DNSForwardZoneConfig),
+                MatchesStructure.byEquality(
+                    domain="one",
+                    networks={
+                        IPNetwork("10/32"),
+                        IPNetwork("11/32"),
+                        IPNetwork("12/32"),
+                        }
+                    )))
+        self.assertThat(
+            f_two, MatchesAll(
+                IsInstance(DNSForwardZoneConfig),
+                MatchesStructure.byEquality(
+                    domain="two",
+                    networks={
+                        IPNetwork("20/32"),
+                        IPNetwork("21/32"),
+                        IPNetwork("22/32"),
+                        }
+                    )))
+        # For the reverse zones, a single reverse zone description is
+        # generated for each nodegroup passed in, in network order.
+        self.assertThat(
+            r_one_0, MatchesAll(
+                IsInstance(DNSReverseZoneConfig),
+                MatchesStructure.byEquality(
+                    domain="one", network=IPNetwork("10/32"))))
+        self.assertThat(
+            r_one_1, MatchesAll(
+                IsInstance(DNSReverseZoneConfig),
+                MatchesStructure.byEquality(
+                    domain="one", network=IPNetwork("11/32"))))
+        self.assertThat(
+            r_two_0, MatchesAll(
+                IsInstance(DNSReverseZoneConfig),
+                MatchesStructure.byEquality(
+                    domain="two", network=IPNetwork("20/32"))))
+        self.assertThat(
+            r_two_1, MatchesAll(
+                IsInstance(DNSReverseZoneConfig),
+                MatchesStructure.byEquality(
+                    domain="two", network=IPNetwork("21/32"))))

=== modified file 'src/maasserver/tests/test_fields.py'
--- src/maasserver/tests/test_fields.py	2012-09-21 09:06:21 +0000
+++ src/maasserver/tests/test_fields.py	2012-10-01 00:25:26 +0000
@@ -32,6 +32,7 @@
     JSONFieldModel,
     XMLFieldModel,
     )
+from netaddr import IPNetwork
 
 
 class TestNodeGroupFormField(TestCase):
@@ -70,7 +71,7 @@
 
     def test_clean_finds_nodegroup_by_network_address(self):
         nodegroup = factory.make_node_group(
-            ip='192.168.28.1', subnet_mask='255.255.255.0')
+            network=IPNetwork("192.168.28.1/24"))
         self.assertEqual(
             nodegroup,
             NodeGroupFormField().clean('192.168.28.0'))
@@ -83,7 +84,7 @@
 
     def test_find_nodegroup_accepts_any_ip_in_nodegroup_subnet(self):
         nodegroup = factory.make_node_group(
-            ip='192.168.41.1', subnet_mask='255.255.255.0')
+            network=IPNetwork("192.168.41.0/24"))
         self.assertEqual(
             nodegroup,
             NodeGroupFormField().clean('192.168.41.199'))
@@ -95,15 +96,14 @@
             factory.getRandomIPAddress())
 
     def test_find_nodegroup_reports_if_multiple_matches(self):
-        factory.make_node_group(ip='10.0.0.1', subnet_mask='255.0.0.0')
-        factory.make_node_group(ip='10.1.1.1', subnet_mask='255.255.255.0')
+        factory.make_node_group(network=IPNetwork("10/8"))
+        factory.make_node_group(network=IPNetwork("10.1.1/24"))
         self.assertRaises(
             NodeGroup.MultipleObjectsReturned,
             NodeGroupFormField().clean, '10.1.1.2')
 
     def test_find_nodegroup_handles_multiple_matches_on_same_nodegroup(self):
-        nodegroup = factory.make_node_group(
-            ip='10.0.0.1', subnet_mask='255.0.0.0')
+        nodegroup = factory.make_node_group(network=IPNetwork("10/8"))
         NodeGroupInterface.objects.create(
             nodegroup=nodegroup, ip='10.0.0.2', subnet_mask='255.0.0.0',
             interface='eth71')

=== modified file 'src/maasserver/tests/test_forms.py'
--- src/maasserver/tests/test_forms.py	2012-09-28 11:14:48 +0000
+++ src/maasserver/tests/test_forms.py	2012-10-01 00:25:26 +0000
@@ -65,6 +65,7 @@
 from maasserver.testing import reload_object
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import TestCase
+from netaddr import IPNetwork
 from provisioningserver.enum import POWER_TYPE_CHOICES
 from testtools.matchers import (
     AllMatch,
@@ -199,8 +200,8 @@
 
     def test_sets_nodegroup_on_new_node_if_requested(self):
         nodegroup = factory.make_node_group(
-            ip_range_low='192.168.14.2', ip_range_high='192.168.14.254',
-            ip='192.168.14.1', subnet_mask='255.255.255.0')
+            network=IPNetwork("192.168.14.0/24"), ip_range_low='192.168.14.2',
+            ip_range_high='192.168.14.254', ip='192.168.14.1')
         form = NodeWithMACAddressesForm(
             self.make_params(nodegroup=nodegroup.get_managed_interface().ip))
         self.assertEqual(nodegroup, form.save().nodegroup)
@@ -210,9 +211,7 @@
         # nodes.  You can't change it later.
         original_nodegroup = factory.make_node_group()
         node = factory.make_node(nodegroup=original_nodegroup)
-        factory.make_node_group(
-            ip_range_low='10.0.0.1', ip_range_high='10.0.0.2',
-            ip='10.0.0.1', subnet_mask='255.0.0.0')
+        factory.make_node_group(network=IPNetwork("10.0.0.0/8"))
         form = NodeWithMACAddressesForm(
             self.make_params(nodegroup='10.0.0.1'), instance=node)
         form.save()
@@ -630,14 +629,15 @@
 
 def make_interface_settings():
     """Create a dict of arbitrary interface configuration parameters."""
+    network = factory.getRandomNetwork()
     return {
-        'ip': factory.getRandomIPAddress(),
+        'ip': factory.getRandomIPInNetwork(network),
         'interface': factory.make_name('interface'),
-        'subnet_mask': factory.getRandomIPAddress(),
-        'broadcast_ip': factory.getRandomIPAddress(),
-        'router_ip': factory.getRandomIPAddress(),
-        'ip_range_low': factory.getRandomIPAddress(),
-        'ip_range_high': factory.getRandomIPAddress(),
+        'subnet_mask': str(network.netmask),
+        'broadcast_ip': str(network.broadcast),
+        'router_ip': factory.getRandomIPInNetwork(network),
+        'ip_range_low': factory.getRandomIPInNetwork(network),
+        'ip_range_high': factory.getRandomIPInNetwork(network),
         'management': factory.getRandomEnum(NODEGROUPINTERFACE_MANAGEMENT),
     }
 

=== modified file 'src/maasserver/tests/test_nodegroup.py'
--- src/maasserver/tests/test_nodegroup.py	2012-09-27 09:23:58 +0000
+++ src/maasserver/tests/test_nodegroup.py	2012-10-01 00:25:26 +0000
@@ -38,15 +38,16 @@
 
 
 def make_dhcp_settings():
-    """Create a dict of arbitrary nodegroup configuration parameters."""
-    return {
+    """Return an arbitrary dict of DHCP settings."""
+    network = factory.getRandomNetwork()
+    return network, {
         'interface': factory.make_name('interface'),
-        'subnet_mask': '255.0.0.0',
-        'broadcast_ip': '10.255.255.255',
-        'router_ip': factory.getRandomIPAddress(),
-        'ip_range_low': '10.0.0.1',
-        'ip_range_high': '10.254.254.254',
-    }
+        'subnet_mask': str(network.netmask),
+        'broadcast_ip': str(network.broadcast),
+        'router_ip': factory.getRandomIPInNetwork(network),
+        'ip_range_low': factory.getRandomIPInNetwork(network),
+        'ip_range_high': factory.getRandomIPInNetwork(network),
+        }
 
 
 class TestNodeGroupManager(TestCase):
@@ -65,11 +66,10 @@
         uuid = factory.getRandomUUID()
         ip = factory.getRandomIPAddress()
         nodegroup = NodeGroup.objects.new(name, uuid, ip)
+        dhcp_network, dhcp_settings = make_dhcp_settings()
         self.assertThat(
-            nodegroup,
-            MatchesStructure.fromExample({
-                item: None
-                for item in make_dhcp_settings().keys()}))
+            nodegroup, MatchesStructure.fromExample(
+                dict.fromkeys(dhcp_settings)))
 
     def test_new_requires_all_dhcp_settings_or_none(self):
         name = factory.make_name('nodegroup')
@@ -82,8 +82,8 @@
     def test_new_creates_nodegroup_with_given_dhcp_settings(self):
         name = factory.make_name('nodegroup')
         uuid = factory.make_name('uuid')
-        ip = factory.getRandomIPAddress()
-        dhcp_settings = make_dhcp_settings()
+        dhcp_network, dhcp_settings = make_dhcp_settings()
+        ip = factory.getRandomIPInNetwork(dhcp_network)
         nodegroup = NodeGroup.objects.new(name, uuid, ip, **dhcp_settings)
         nodegroup = reload_object(nodegroup)
         self.assertEqual(name, nodegroup.name)
@@ -118,7 +118,7 @@
             NodeGroup.objects.ensure_master(),
             MatchesStructure.fromExample({
                 'name': 'master',
-                'workder_id': 'master',
+                'worker_id': 'master',
                 'worker_ip': '127.0.0.1',
                 'subnet_mask': None,
                 'broadcast_ip': None,

=== modified file 'src/provisioningserver/dns/config.py'
--- src/provisioningserver/dns/config.py	2012-10-01 00:25:25 +0000
+++ src/provisioningserver/dns/config.py	2012-10-01 00:25:26 +0000
@@ -24,6 +24,7 @@
     )
 from datetime import datetime
 from itertools import (
+    chain,
     imap,
     islice,
     )
@@ -218,8 +219,7 @@
 
     template_file_name = 'zone.template'
 
-    def __init__(self, domain, serial=None, mapping=None, dns_ip=None,
-                 network=None):
+    def __init__(self, domain, serial=None, mapping=None, dns_ip=None):
         """
         :param domain: The domain name of the forward zone.
         :param serial: The serial to use in the zone file. This must increment
@@ -228,14 +228,12 @@
             the zone.
         :param dns_ip: The IP address of the DNS server authoritative for this
             zone.
-        :param network: The network that the mapping exists within.
-        :type network: :class:`netaddr.IPNetwork`
         """
+        super(DNSZoneConfigBase, self).__init__()
         self.domain = domain
         self.serial = serial
         self.mapping = {} if mapping is None else mapping
         self.dns_ip = dns_ip
-        self.network = network
 
     @abstractproperty
     def zone_name(self):
@@ -263,6 +261,16 @@
 class DNSForwardZoneConfig(DNSZoneConfigBase):
     """Writes forward zone files."""
 
+    def __init__(self, *args, **kwargs):
+        """See `DNSZoneConfigBase.__init__`.
+
+        :param networks: The networks that the mapping exists within.
+        :type networks: Sequence of :class:`netaddr.IPNetwork`
+        """
+        networks = kwargs.pop("networks", None)
+        self.networks = [] if networks is None else networks
+        super(DNSForwardZoneConfig, self).__init__(*args, **kwargs)
+
     @property
     def zone_name(self):
         """Return the name of the forward zone."""
@@ -281,10 +289,8 @@
         The generated mapping is the mapping between the generated hostnames
         and the IP addresses for all the possible IP addresses in zone.
         """
-        return {
-            generated_hostname(ip): ip
-            for ip in imap(str, self.network)
-        }
+        ips = imap(unicode, chain.from_iterable(self.networks))
+        return {generated_hostname(ip): ip for ip in ips}
 
     def get_context(self):
         """Return the dict used to render the DNS zone file.
@@ -308,6 +314,15 @@
 class DNSReverseZoneConfig(DNSZoneConfigBase):
     """Writes reverse zone files."""
 
+    def __init__(self, *args, **kwargs):
+        """See `DNSZoneConfigBase.__init__`.
+
+        :param network: The network that the mapping exists within.
+        :type network: :class:`netaddr.IPNetwork`
+        """
+        self.network = kwargs.pop("network", None)
+        super(DNSReverseZoneConfig, self).__init__(*args, **kwargs)
+
     @property
     def zone_name(self):
         """Return the name of the reverse zone."""

=== modified file 'src/provisioningserver/dns/tests/test_config.py'
--- src/provisioningserver/dns/tests/test_config.py	2012-10-01 00:25:25 +0000
+++ src/provisioningserver/dns/tests/test_config.py	2012-10-01 00:25:26 +0000
@@ -156,7 +156,7 @@
         ip = factory.getRandomIPInNetwork(network)
         forward_zone = DNSForwardZoneConfig(
             domain, mapping={factory.getRandomString(): ip},
-            network=network)
+            networks=[network])
         reverse_zone = DNSReverseZoneConfig(
             domain, mapping={factory.getRandomString(): ip},
             network=network)
@@ -214,7 +214,7 @@
 class TestDNSForwardZoneConfig(TestCase):
     """Tests for DNSForwardZoneConfig."""
 
-    def test_DNSForwardZoneConfig_fields(self):
+    def test_fields(self):
         domain = factory.getRandomString()
         serial = random.randint(1, 200)
         hostname = factory.getRandomString()
@@ -222,18 +222,18 @@
         ip = factory.getRandomIPInNetwork(network)
         mapping = {hostname: ip}
         dns_zone_config = DNSForwardZoneConfig(
-            domain, serial, mapping, network=network)
+            domain, serial, mapping, networks=[network])
         self.assertThat(
             dns_zone_config,
             MatchesStructure.byEquality(
                 domain=domain,
                 serial=serial,
                 mapping=mapping,
-                network=network,
+                networks=[network],
                 )
             )
 
-    def test_DNSForwardZoneConfig_computes_dns_config_file_paths(self):
+    def test_computes_dns_config_file_paths(self):
         domain = factory.make_name('zone')
         dns_zone_config = DNSForwardZoneConfig(domain)
         self.assertEqual(
@@ -246,10 +246,10 @@
                 dns_zone_config.target_path,
             ))
 
-    def test_DNSForwardZoneConfig_get_generated_mapping(self):
+    def test_get_generated_mapping(self):
         name = factory.getRandomString()
         network = IPNetwork('192.12.0.1/30')
-        dns_zone_config = DNSForwardZoneConfig(name, network=network)
+        dns_zone_config = DNSForwardZoneConfig(name, networks=[network])
         self.assertEqual(
             {
                 generated_hostname('192.12.0.0'): '192.12.0.0',
@@ -260,7 +260,21 @@
             dns_zone_config.get_generated_mapping(),
             )
 
-    def test_DNSForwardZoneConfig_writes_dns_zone_config(self):
+    def test_get_generated_mapping_multiple_networks(self):
+        name = factory.getRandomString()
+        networks = IPNetwork('11.11.11.11/31'), IPNetwork('22.22.22.22/31')
+        dns_zone_config = DNSForwardZoneConfig(name, networks=networks)
+        self.assertEqual(
+            {
+                generated_hostname('11.11.11.10'): '11.11.11.10',
+                generated_hostname('11.11.11.11'): '11.11.11.11',
+                generated_hostname('22.22.22.22'): '22.22.22.22',
+                generated_hostname('22.22.22.23'): '22.22.22.23',
+             },
+            dns_zone_config.get_generated_mapping(),
+            )
+
+    def test_writes_dns_zone_config(self):
         target_dir = self.make_dir()
         self.patch(DNSForwardZoneConfig, 'target_dir', target_dir)
         domain = factory.getRandomString()
@@ -269,7 +283,7 @@
         ip = factory.getRandomIPInNetwork(network)
         dns_zone_config = DNSForwardZoneConfig(
             domain, serial=random.randint(1, 100),
-            mapping={hostname: ip}, network=network)
+            mapping={hostname: ip}, networks=[network])
         dns_zone_config.write_config()
         self.assertThat(
             os.path.join(target_dir, 'zone.%s' % domain),
@@ -280,14 +294,14 @@
                         '%s IN A %s' % (generated_hostname(ip), ip),
                     ])))
 
-    def test_DNSForwardZoneConfig_writes_dns_zone_config_with_NS_record(self):
+    def test_writes_dns_zone_config_with_NS_record(self):
         target_dir = self.make_dir()
         self.patch(DNSForwardZoneConfig, 'target_dir', target_dir)
         network = factory.getRandomNetwork()
         dns_ip = factory.getRandomIPAddress()
         dns_zone_config = DNSForwardZoneConfig(
             factory.getRandomString(), serial=random.randint(1, 100),
-            dns_ip=dns_ip, network=network)
+            dns_ip=dns_ip, networks=[network])
         dns_zone_config.write_config()
         self.assertThat(
             os.path.join(target_dir, 'zone.%s' % dns_zone_config.domain),
@@ -298,12 +312,12 @@
                         '%s. IN A %s' % (dns_zone_config.domain, dns_ip),
                     ])))
 
-    def test_DNSForwardZoneConfig_config_file_is_world_readable(self):
+    def test_config_file_is_world_readable(self):
         self.patch(DNSForwardZoneConfig, 'target_dir', self.make_dir())
         network = factory.getRandomNetwork()
         dns_zone_config = DNSForwardZoneConfig(
             factory.getRandomString(), serial=random.randint(1, 100),
-            dns_ip=factory.getRandomIPAddress(), network=network)
+            dns_ip=factory.getRandomIPAddress(), networks=[network])
         dns_zone_config.write_config()
         filepath = FilePath(dns_zone_config.target_path)
         self.assertTrue(filepath.getPermissions().other.read)
@@ -312,7 +326,7 @@
 class TestDNSReverseZoneConfig(TestCase):
     """Tests for DNSReverseZoneConfig."""
 
-    def test_DNSReverseZoneConfig_fields(self):
+    def test_fields(self):
         domain = factory.getRandomString()
         serial = random.randint(1, 200)
         hostname = factory.getRandomString()
@@ -331,7 +345,7 @@
                 )
             )
 
-    def test_DNSReverseZoneConfig_computes_dns_config_file_paths(self):
+    def test_computes_dns_config_file_paths(self):
         domain = factory.make_name('zone')
         reverse_file_name = 'zone.168.192.in-addr.arpa'
         dns_zone_config = DNSReverseZoneConfig(
@@ -346,7 +360,7 @@
                 dns_zone_config.target_path,
             ))
 
-    def test_DNSReverseZoneConfig_reverse_data_slash_24(self):
+    def test_reverse_data_slash_24(self):
         # DNSReverseZoneConfig calculates the reverse data correctly for
         # a /24 network.
         domain = factory.make_name('zone')
@@ -359,7 +373,7 @@
             '0.168.192.in-addr.arpa',
             dns_zone_config.zone_name)
 
-    def test_DNSReverseZoneConfig_reverse_data_slash_22(self):
+    def test_reverse_data_slash_22(self):
         # DNSReverseZoneConfig calculates the reverse data correctly for
         # a /22 network.
         domain = factory.getRandomString()
@@ -372,7 +386,7 @@
             '168.192.in-addr.arpa',
             dns_zone_config.zone_name)
 
-    def test_DNSReverseZoneConfig_get_generated_mapping(self):
+    def test_get_generated_mapping(self):
         name = factory.getRandomString()
         network = IPNetwork('192.12.0.1/30')
         dns_zone_config = DNSReverseZoneConfig(name, network=network)
@@ -386,7 +400,7 @@
             dns_zone_config.get_generated_mapping(),
             )
 
-    def test_DNSReverseZoneConfig_writes_dns_zone_config_with_NS_record(self):
+    def test_writes_dns_zone_config_with_NS_record(self):
         target_dir = self.make_dir()
         self.patch(DNSReverseZoneConfig, 'target_dir', target_dir)
         network = factory.getRandomNetwork()
@@ -401,7 +415,7 @@
             FileContains(
                 matcher=Contains('IN  NS  %s.' % dns_zone_config.domain)))
 
-    def test_DNSReverseZoneConfig_writes_reverse_dns_zone_config(self):
+    def test_writes_reverse_dns_zone_config(self):
         target_dir = self.make_dir()
         self.patch(DNSReverseZoneConfig, 'target_dir', target_dir)
         domain = factory.getRandomString()
@@ -410,20 +424,13 @@
             domain, serial=random.randint(1, 100), network=network)
         dns_zone_config.write_config()
         reverse_file_name = 'zone.168.192.in-addr.arpa'
+        expected = Contains(
+            '10.0 IN PTR %s' % generated_hostname('192.168.0.10'))
         self.assertThat(
             os.path.join(target_dir, reverse_file_name),
-            FileContains(
-                matcher=ContainsAll(
-                    ['%s IN PTR %s' % (
-                        '10.0',
-                        generated_hostname('192.168.0.10'),
-                        )
-                    ]
-                )
-            )
-        )
+            FileContains(matcher=expected))
 
-    def test_DNSReverseZoneConfig_reverse_config_file_is_world_readable(self):
+    def test_reverse_config_file_is_world_readable(self):
         self.patch(DNSReverseZoneConfig, 'target_dir', self.make_dir())
         dns_zone_config = DNSReverseZoneConfig(
             factory.getRandomString(), serial=random.randint(1, 100),

=== modified file 'src/provisioningserver/tasks.py'
--- src/provisioningserver/tasks.py	2012-09-28 10:16:59 +0000
+++ src/provisioningserver/tasks.py	2012-10-01 00:25:26 +0000
@@ -190,7 +190,6 @@
     if zones is not None:
         for zone in zones:
             zone.write_config()
-            zone.write_reverse_config()
     # Write main config file.
     dns_config = DNSConfig(zones=zones)
     dns_config.write_config(**kwargs)
@@ -216,8 +215,8 @@
 
 
 @task(queue=celery_config.WORKER_QUEUE_DNS)
-def write_dns_zone_config(zone, callback=None, **kwargs):
-    """Write out a DNS zone configuration file.
+def write_dns_zone_config(zones, callback=None, **kwargs):
+    """Write out DNS zones.
 
     :param zone: The zone data to write the configuration for.
     :type zone: :class:`DNSZoneData`
@@ -225,8 +224,8 @@
     :type callback: callable
     :param **kwargs: Keyword args passed to DNSZoneConfig.write_config()
     """
-    zone.write_config()
-    zone.write_reverse_config()
+    for zone in zones:
+        zone.write_config()
     if callback is not None:
         callback.delay()
 

=== modified file 'src/provisioningserver/testing/__init__.py'
--- src/provisioningserver/testing/__init__.py	2012-07-25 08:20:23 +0000
+++ src/provisioningserver/testing/__init__.py	2012-10-01 00:25:26 +0000
@@ -1,29 +0,0 @@
-# Copyright 2012 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-from __future__ import (
-    absolute_import,
-    print_function,
-    unicode_literals,
-    )
-
-"""Testing tools for `provisioningserver`."""
-
-__metaclass__ = type
-__all__ = [
-    "network_infos",
-    ]
-
-
-def network_infos(network):
-    """Return a dict of info about a network.
-
-    :param network: The network object from which to extract the data.
-    :type network: IPNetwork.
-    """
-    return dict(
-        subnet_mask=str(network.netmask),
-        broadcast_ip=str(network.broadcast),
-        ip_range_low=str(network.first),
-        ip_range_high=str(network.last),
-    )

=== modified file 'src/provisioningserver/tests/test_tasks.py'
--- src/provisioningserver/tests/test_tasks.py	2012-09-28 10:16:59 +0000
+++ src/provisioningserver/tests/test_tasks.py	2012-10-01 00:25:26 +0000
@@ -46,7 +46,8 @@
     )
 from provisioningserver.dns.config import (
     conf,
-    DNSZoneConfig,
+    DNSForwardZoneConfig,
+    DNSReverseZoneConfig,
     MAAS_NAMED_CONF_NAME,
     MAAS_NAMED_RNDC_CONF_NAME,
     MAAS_RNDC_CONF_NAME,
@@ -71,7 +72,6 @@
     write_dns_zone_config,
     write_full_dns_config,
     )
-from provisioningserver.testing import network_infos
 from provisioningserver.testing.boot_images import make_boot_image_params
 from provisioningserver.testing.config import ConfigFixture
 from provisioningserver.testing.testcase import PservTestCase
@@ -317,20 +317,25 @@
 
     def test_write_dns_zone_config_writes_file(self):
         command = factory.getRandomString()
-        zone_name = factory.getRandomString()
+        domain = factory.getRandomString()
         network = IPNetwork('192.168.0.3/24')
         ip = factory.getRandomIPInNetwork(network)
-        zone = DNSZoneConfig(
-            zone_name, serial=random.randint(1, 100),
-            mapping={factory.getRandomString(): ip}, **network_infos(network))
+        forward_zone = DNSForwardZoneConfig(
+            domain, serial=random.randint(1, 100),
+            mapping={factory.getRandomString(): ip}, networks=[network])
+        reverse_zone = DNSReverseZoneConfig(
+            domain, serial=random.randint(1, 100),
+            mapping={factory.getRandomString(): ip}, network=network)
         result = write_dns_zone_config.delay(
-            zone=zone, callback=rndc_command.subtask(args=[command]))
+            zones=[forward_zone, reverse_zone],
+            callback=rndc_command.subtask(args=[command]))
 
-        reverse_file_name = 'zone.rev.0.168.192.in-addr.arpa'
+        forward_file_name = 'zone.%s' % domain
+        reverse_file_name = 'zone.0.168.192.in-addr.arpa'
         self.assertThat(
             (
                 result.successful(),
-                os.path.join(self.dns_conf_dir, 'zone.%s' % zone_name),
+                os.path.join(self.dns_conf_dir, forward_file_name),
                 os.path.join(self.dns_conf_dir, reverse_file_name),
                 self.rndc_recorder.calls,
             ),
@@ -421,23 +426,31 @@
     def test_write_full_dns_config_sets_up_config(self):
         # write_full_dns_config writes the config file, writes
         # the zone files, and reloads the dns service.
-        zone_name = factory.getRandomString()
+        domain = factory.getRandomString()
         network = IPNetwork('192.168.0.3/24')
         ip = factory.getRandomIPInNetwork(network)
-        zones = [DNSZoneConfig(
-            zone_name, serial=random.randint(1, 100),
-            mapping={factory.getRandomString(): ip}, **network_infos(network))]
+        zones = [
+            DNSForwardZoneConfig(
+                domain, serial=random.randint(1, 100),
+                mapping={factory.getRandomString(): ip},
+                networks=[network]),
+            DNSReverseZoneConfig(
+                domain, serial=random.randint(1, 100),
+                mapping={factory.getRandomString(): ip},
+                network=network),
+            ]
         command = factory.getRandomString()
         result = write_full_dns_config.delay(
             zones=zones,
             callback=rndc_command.subtask(args=[command]))
 
-        reverse_file_name = 'zone.rev.0.168.192.in-addr.arpa'
+        forward_file_name = 'zone.%s' % domain
+        reverse_file_name = 'zone.0.168.192.in-addr.arpa'
         self.assertThat(
             (
                 result.successful(),
                 self.rndc_recorder.calls,
-                os.path.join(self.dns_conf_dir, 'zone.%s' % zone_name),
+                os.path.join(self.dns_conf_dir, forward_file_name),
                 os.path.join(self.dns_conf_dir, reverse_file_name),
                 os.path.join(self.dns_conf_dir, MAAS_NAMED_CONF_NAME),
             ),