launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #12709
[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),
),