Merge lp:~jtv/maas/dns-disable-ipv4 into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 2738
Proposed branch: lp:~jtv/maas/dns-disable-ipv4
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 216 lines (+121/-16)
4 files modified
src/maasserver/dns/tests/test_config.py (+3/-7)
src/maasserver/dns/tests/test_zonegenerator.py (+50/-0)
src/maasserver/models/staticipaddress.py (+24/-9)
src/maasserver/models/tests/test_staticipaddress.py (+44/-0)
To merge this branch: bzr merge lp:~jtv/maas/dns-disable-ipv4
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+231148@code.launchpad.net

Commit message

Allow a node to have IPv4 and IPv6 addresses in DNS at the same time; suppress the IPv4 address if node has disable_ipv4 set.

Description of the change

Most of the work for this went into preparatory branches, so the branch you see here is simple. It allows a node to have one static IPv4 address and one static IPv6 address (even on different MACs, although I doubt that matters) and map them both in DNS.

The diff in src/maasserver/models/staticipaddress.py comes out in two chunks for me, but it may help to know that it's all in the same method: get_hostname_ip_mapping. All the relevant code was in that one SQL query there, and as luck would have it, both changes were minor. I hope the query is still clear enough to your fresh eyes. If not, let me know what you'd like me to clarify in comments.

Jeroen

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Looks good, but you need more tests for the SQL changes, particularly all the scenarios around mixing v4/v6 and same/different MACs. I trust you will get those right since you're probably the best test writer on the team, which pains me to write this review :)

review: Approve
Revision history for this message
Julian Edwards (julian-edwards) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/dns/tests/test_config.py'
2--- src/maasserver/dns/tests/test_config.py 2014-08-15 09:44:42 +0000
3+++ src/maasserver/dns/tests/test_config.py 2014-08-18 06:29:42 +0000
4@@ -77,10 +77,6 @@
5 ['%0.10d' % i for i in range(initial + 1, initial + 11)],
6 [next_zone_serial() for i in range(initial, initial + 10)])
7
8- def patch_DEFAULT_MAAS_URL(self, hostname=None):
9- self.patch(
10- settings, 'DEFAULT_MAAS_URL', factory.make_url(netloc=hostname))
11-
12
13 class TestDNSServer(MAASServerTestCase):
14 """A base class to perform real-world DNS-related tests.
15@@ -124,8 +120,7 @@
16 if nodegroup is None:
17 nodegroup = self.create_managed_nodegroup()
18 [interface] = nodegroup.get_managed_interfaces()
19- node = factory.make_node(
20- nodegroup=nodegroup)
21+ node = factory.make_node(nodegroup=nodegroup, disable_ipv4=False)
22 mac = factory.make_mac_address(node=node, cluster_interface=interface)
23 ips = IPRange(
24 interface.static_ip_range_low, interface.static_ip_range_high)
25@@ -347,7 +342,8 @@
26 nodegroup = self.create_managed_nodegroup(network=network)
27 [interface] = nodegroup.get_managed_interfaces()
28 node = factory.make_node(
29- nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED)
30+ nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED,
31+ disable_ipv4=False)
32 mac = factory.make_mac_address(node=node, cluster_interface=interface)
33 # Get an IP in the dynamic range.
34 ip_range = IPRange(
35
36=== modified file 'src/maasserver/dns/tests/test_zonegenerator.py'
37--- src/maasserver/dns/tests/test_zonegenerator.py 2014-08-15 11:49:09 +0000
38+++ src/maasserver/dns/tests/test_zonegenerator.py 2014-08-18 06:29:42 +0000
39@@ -210,6 +210,56 @@
40 {node.hostname: [staticip.ip]},
41 get_hostname_ip_mapping(nodegroup))
42
43+ def test_get_hostname_ip_mapping_includes_IPv4_and_IPv6_by_default(self):
44+ # A node can have static IP addresses for IPv4 and IPv6 both.
45+ # (It can't have DHCP leases for IPv6, since we don't parse or use
46+ # those.)
47+ node = factory.make_node_with_mac_attached_to_nodegroupinterface(
48+ status=NODE_STATUS.ALLOCATED, disable_ipv4=False)
49+ mac = node.get_primary_mac()
50+ ipv4 = factory.getRandomIPAddress()
51+ ipv6 = factory.make_ipv6_address()
52+ factory.make_staticipaddress(mac=mac, ip=ipv4)
53+ factory.make_staticipaddress(mac=mac, ip=ipv6)
54+
55+ self.assertItemsEqual(
56+ [ipv4, ipv6],
57+ get_hostname_ip_mapping(node.nodegroup)[node.hostname])
58+
59+ def test_get_hostname_ip_mapping_excludes_IPv4_if_disabled_on_node(self):
60+ node = factory.make_node_with_mac_attached_to_nodegroupinterface(
61+ status=NODE_STATUS.ALLOCATED, disable_ipv4=True)
62+ mac = node.get_primary_mac()
63+ ipv4 = factory.getRandomIPAddress()
64+ ipv6 = factory.make_ipv6_address()
65+ factory.make_staticipaddress(mac=mac, ip=ipv4)
66+ factory.make_staticipaddress(mac=mac, ip=ipv6)
67+
68+ self.assertItemsEqual(
69+ [ipv6],
70+ get_hostname_ip_mapping(node.nodegroup)[node.hostname])
71+
72+ def test_get_hostname_ip_mapping_disables_IPv4_per_individual_node(self):
73+ nodegroup = factory.make_node_group()
74+ node_with_ipv4 = (
75+ factory.make_node_with_mac_attached_to_nodegroupinterface(
76+ nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED,
77+ disable_ipv4=False))
78+ node_without_ipv4 = (
79+ factory.make_node_with_mac_attached_to_nodegroupinterface(
80+ nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED,
81+ disable_ipv4=True))
82+ ipv4 = factory.getRandomIPAddress()
83+ factory.make_staticipaddress(
84+ mac=node_with_ipv4.get_primary_mac(), ip=ipv4)
85+ factory.make_staticipaddress(
86+ mac=node_without_ipv4.get_primary_mac())
87+
88+ mappings = get_hostname_ip_mapping(nodegroup)
89+ # The node with IPv4 enabled gets its IPv4 address mapped.
90+ # The node with IPv4 disabled gets no mapping.
91+ self.assertEqual({node_with_ipv4.hostname: [ipv4]}, mappings)
92+
93
94 def forward_zone(domain):
95 """Create a matcher for a :class:`DNSForwardZoneConfig`.
96
97=== modified file 'src/maasserver/models/staticipaddress.py'
98--- src/maasserver/models/staticipaddress.py 2014-08-16 08:53:23 +0000
99+++ src/maasserver/models/staticipaddress.py 2014-08-18 06:29:42 +0000
100@@ -23,6 +23,8 @@
101 'StaticIPAddress',
102 ]
103
104+from collections import defaultdict
105+
106 from django.contrib.auth.models import User
107 from django.db import (
108 connection,
109@@ -190,8 +192,13 @@
110 return self._deallocate(qs)
111
112 def get_hostname_ip_mapping(self, nodegroup):
113- """Return a mapping {hostnames -> ips} for current `StaticIPAddress`es
114- for the nodes in `nodegroup`.
115+ """Return hostname mappings for `StaticIPAddress` entries.
116+
117+ Returns a mapping `{hostnames -> [ips]}` corresponding to current
118+ `StaticIPAddress` objects for the nodes in `nodegroup`.
119+
120+ At most one IPv4 address and one IPv6 address will be returned per
121+ node, each the one for whichever `MACAddress` was created first.
122
123 Any domain will be stripped from the hostnames.
124 """
125@@ -200,8 +207,10 @@
126 # DISTINCT ON returns the first matching row for any given
127 # hostname, using the query's ordering. Here, we're trying to
128 # return the IP for the oldest MAC address.
129+ #
130+ # For nodes that have disable_ipv4 set, leave out any IPv4 address.
131 cursor.execute("""
132- SELECT DISTINCT ON (node.hostname)
133+ SELECT DISTINCT ON (node.hostname, family(staticip.ip))
134 node.hostname, staticip.ip
135 FROM maasserver_macaddress AS mac
136 JOIN maasserver_node AS node ON
137@@ -210,13 +219,19 @@
138 link.mac_address_id = mac.id
139 JOIN maasserver_staticipaddress AS staticip ON
140 staticip.id = link.ip_address_id
141- WHERE node.nodegroup_id = %s
142- ORDER BY node.hostname, mac.id
143+ WHERE
144+ node.nodegroup_id = %s AND
145+ (
146+ node.disable_ipv4 IS FALSE OR
147+ family(staticip.ip) <> 4
148+ )
149+ ORDER BY node.hostname, family(staticip.ip), mac.id
150 """, (nodegroup.id,))
151- return {
152- strip_domain(hostname): [ip]
153- for hostname, ip in cursor.fetchall()
154- }
155+ mapping = defaultdict(list)
156+ for hostname, ip in cursor.fetchall():
157+ hostname = strip_domain(hostname)
158+ mapping[hostname].append(ip)
159+ return mapping
160
161
162 class StaticIPAddress(CleanSave, TimestampedModel):
163
164=== modified file 'src/maasserver/models/tests/test_staticipaddress.py'
165--- src/maasserver/models/tests/test_staticipaddress.py 2014-08-16 12:08:22 +0000
166+++ src/maasserver/models/tests/test_staticipaddress.py 2014-08-18 06:29:42 +0000
167@@ -283,6 +283,50 @@
168 node.nodegroup)
169 self.assertEqual({node.hostname: [ip_for_older_mac.ip]}, mapping)
170
171+ def test_get_hostname_ip_mapping_combines_IPv4_and_IPv6_addresses(self):
172+ node = factory.make_node(mac=True, disable_ipv4=False)
173+ mac = node.get_primary_mac()
174+ ipv4_address = factory.make_staticipaddress(
175+ mac=mac,
176+ ip=factory.pick_ip_in_network(factory.getRandomNetwork()))
177+ ipv6_address = factory.make_staticipaddress(
178+ mac=mac,
179+ ip=factory.pick_ip_in_network(factory.make_ipv6_network()))
180+ mapping = StaticIPAddress.objects.get_hostname_ip_mapping(
181+ node.nodegroup)
182+ self.assertItemsEqual(
183+ [ipv4_address.ip, ipv6_address.ip],
184+ mapping[node.hostname])
185+
186+ def test_get_hostname_ip_mapping_combines_MACs_for_same_node(self):
187+ # A node's preferred static IPv4 and IPv6 addresses may be on
188+ # different MACs.
189+ node = factory.make_node(disable_ipv4=False)
190+ ipv4_address = factory.make_staticipaddress(
191+ mac=factory.make_mac_address(node=node),
192+ ip=factory.pick_ip_in_network(factory.getRandomNetwork()))
193+ ipv6_address = factory.make_staticipaddress(
194+ mac=factory.make_mac_address(node=node),
195+ ip=factory.pick_ip_in_network(factory.make_ipv6_network()))
196+ mapping = StaticIPAddress.objects.get_hostname_ip_mapping(
197+ node.nodegroup)
198+ self.assertItemsEqual(
199+ [ipv4_address.ip, ipv6_address.ip],
200+ mapping[node.hostname])
201+
202+ def test_get_hostname_ip_mapping_skips_ipv4_if_disable_ipv4_set(self):
203+ node = factory.make_node(mac=True, disable_ipv4=True)
204+ mac = node.get_primary_mac()
205+ factory.make_staticipaddress(
206+ mac=mac,
207+ ip=factory.pick_ip_in_network(factory.getRandomNetwork()))
208+ ipv6_address = factory.make_staticipaddress(
209+ mac=mac,
210+ ip=factory.pick_ip_in_network(factory.make_ipv6_network()))
211+ mapping = StaticIPAddress.objects.get_hostname_ip_mapping(
212+ node.nodegroup)
213+ self.assertEqual({node.hostname: [ipv6_address.ip]}, mapping)
214+
215
216 class StaticIPAddressTest(MAASServerTestCase):
217