Merge lp:~lamont/maas/bug-1600259 into lp:~maas-committers/maas/trunk

Proposed by LaMont Jones
Status: Merged
Approved by: LaMont Jones
Approved revision: no longer in the source branch.
Merged at revision: 5164
Proposed branch: lp:~lamont/maas/bug-1600259
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 248 lines (+81/-31)
3 files modified
src/maasserver/dns/zonegenerator.py (+11/-3)
src/maasserver/models/staticipaddress.py (+49/-28)
src/maasserver/models/tests/test_staticipaddress.py (+21/-0)
To merge this branch: bzr merge lp:~lamont/maas/bug-1600259
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+299584@code.launchpad.net

Commit message

When generating reverse DNS, we have to consider all of the forward zones, not just our subnet.

Description of the change

When generating reverse DNS, we have to consider all of the forward zones, not just our subnet.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

I hereby approve this branch... reluctantly. This function is a real nightmare. But I don't have any constructive criticism, so let's go ahead and ship it. We may want to consider writing or using some kind of a framework for generating SQL queries dynamically if this continues...

I would also urge you to measure the code coverage of this function to ensure our tests are covering everything. (I see you added a unit test, too, so that's great.)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/dns/zonegenerator.py'
2--- src/maasserver/dns/zonegenerator.py 2016-07-07 23:21:57 +0000
3+++ src/maasserver/dns/zonegenerator.py 2016-07-08 19:43:07 +0000
4@@ -251,6 +251,11 @@
5 IPNetwork("%s/124" % network.network).network)
6 rfc2317_glue.setdefault(basenet, set()).add(network)
7
8+ # Since get_hostname_ip_mapping(Subnet) ignores Subnet.id, so we can
9+ # just do it once and be happy. LP#1600259
10+ if len(subnets):
11+ mappings['reverse'] = mappings[Subnet.objects.first()]
12+
13 # For each of the zones that we are generating (one or more per
14 # subnet), compile the zone from:
15 # 1. Dynamic ranges on this subnet.
16@@ -278,9 +283,12 @@
17 for ip_range in subnet.get_dynamic_ranges()
18 ]
19
20- # 2. Start with the map of all of the nodes on this subnet,
21- # including all DNSResource-associated addresses.
22- mapping = mappings[subnet]
23+ # 2. Start with the map of all of the nodes, including all
24+ # DNSResource-associated addresses. We will prune this to just
25+ # entries for the subnet when we actually generate the zonefile.
26+ # If we get here, then we have subnets, so we noticed that above
27+ # and created mappings['reverse']. LP#1600259
28+ mapping = mappings['reverse']
29
30 # Use the default_domain as the name for the NS host in the reverse
31 # zones. If this network is actually a parent rfc2317 glue
32
33=== modified file 'src/maasserver/models/staticipaddress.py'
34--- src/maasserver/models/staticipaddress.py 2016-07-07 23:21:57 +0000
35+++ src/maasserver/models/staticipaddress.py 2016-07-08 19:43:07 +0000
36@@ -52,7 +52,6 @@
37 from maasserver.models.domain import Domain
38 from maasserver.models.subnet import Subnet
39 from maasserver.models.timestampedmodel import TimestampedModel
40-from maasserver.utils import strip_domain
41 from maasserver.utils.dns import get_ip_based_hostname
42 from maasserver.utils.orm import (
43 request_transaction_retry,
44@@ -229,8 +228,10 @@
45 qs = self.filter(
46 Q(alloc_type=IPADDRESS_TYPE.USER_RESERVED) |
47 Q(dnsresource__isnull=False))
48+ # If this is a subnet, we need to ignore subnet.id, as per
49+ # get_hostname_ip_mapping(). LP#1600259
50 if isinstance(domain_or_subnet, Subnet):
51- qs = qs.filter(subnet_id=domain_or_subnet.id)
52+ pass
53 elif isinstance(domain_or_subnet, Domain):
54 qs = qs.filter(dnsresource__domain_id=domain_or_subnet.id)
55 qs = qs.prefetch_related("dnsresource_set")
56@@ -310,20 +311,6 @@
57 # return the IPs for the oldest Interface address.
58 #
59 # For nodes that have disable_ipv4 set, leave out any IPv4 address.
60- if isinstance(domain_or_subnet, Subnet):
61- search_term = "staticip.subnet_id"
62- elif isinstance(domain_or_subnet, Domain):
63- # The model has nodes in the parent domain, but they actually live
64- # in the child domain. And the parent needs the glue. So we
65- # return such nodes addresses in _BOTH_ the parent and the child
66- # domains. domain2.name will be non-null if this host's fqdn is the
67- # name of a domain in MAAS.
68- # n.b. The SQL says: WHERE ... $search_term = %s ...
69- # we overload that to accomplish our objectives.
70- search_term = """domain2.name IS NOT NULL OR node.domain_id"""
71- else:
72- raise ValueError('bad object passed to get_hostname_ip_mapping')
73-
74 default_ttl = "%d" % Config.objects.get_config('default_dns_ttl')
75 if raw_ttl:
76 ttl_clause = """node.address_ttl"""
77@@ -335,11 +322,11 @@
78 %s)""" % default_ttl
79 sql_query = """
80 SELECT DISTINCT ON (node.hostname, is_boot, family(staticip.ip))
81- node.hostname,
82+ CONCAT(node.hostname, '.', domain.name) AS fqdn,
83 node.system_id,
84 node.node_type,
85 """ + ttl_clause + """ AS ttl,
86- domain.name, staticip.ip,
87+ staticip.ip,
88 COALESCE(
89 node.boot_interface_id IS NOT NULL AND
90 (
91@@ -362,15 +349,35 @@
92 link.interface_id = interface.id
93 JOIN maasserver_staticipaddress AS staticip ON
94 staticip.id = link.staticipaddress_id
95+ """
96+ if isinstance(domain_or_subnet, Domain):
97+ # The model has nodes in the parent domain, but they actually live
98+ # in the child domain. And the parent needs the glue. So we
99+ # return such nodes addresses in _BOTH_ the parent and the child
100+ # domains. domain2.name will be non-null if this host's fqdn is the
101+ # name of a domain in MAAS.
102+ sql_query += """
103 LEFT JOIN maasserver_domain as domain2 ON
104 /* Pick up another copy of domain looking for instances of
105 * nodes a the top of a domain.
106 */
107 domain2.name = CONCAT(node.hostname, '.', domain.name)
108 WHERE
109+ (domain2.name IS NOT NULL OR node.domain_id = %s) AND
110+ """
111+ query_parms = [domain_or_subnet.id, ]
112+ else:
113+ # For subnets, we need ALL the names, so that we can correctly
114+ # identify which ones should have the FQDN. dns/zonegenerator.py
115+ # optimizes based on this, and only calls once with a subnet,
116+ # expecting to get all the subnets back in one table.
117+ sql_query += """
118+ WHERE
119+ """
120+ query_parms = []
121+ sql_query += """
122 staticip.ip IS NOT NULL AND
123 host(staticip.ip) != '' AND
124- (""" + search_term + """ = %s) AND
125 (
126 node.disable_ipv4 IS FALSE OR
127 family(staticip.ip) <> 4
128@@ -412,11 +419,11 @@
129 """
130 iface_sql_query = """
131 SELECT
132- node.hostname,
133+ CONCAT(node.hostname, '.', domain.name) AS fqdn,
134 node.system_id,
135 node.node_type,
136 """ + ttl_clause + """ AS ttl,
137- domain.name, staticip.ip,
138+ staticip.ip,
139 interface.name
140 FROM
141 maasserver_interface AS interface
142@@ -428,6 +435,10 @@
143 link.interface_id = interface.id
144 JOIN maasserver_staticipaddress AS staticip ON
145 staticip.id = link.staticipaddress_id
146+ """
147+ if isinstance(domain_or_subnet, Domain):
148+ # This logic is similar to the logic in sql_query above.
149+ iface_sql_query += """
150 LEFT JOIN maasserver_domain as domain2 ON
151 /* Pick up another copy of domain looking for instances of
152 * the name as the top of a domain.
153@@ -435,9 +446,19 @@
154 domain2.name = CONCAT(
155 interface.name, '.', node.hostname, '.', domain.name)
156 WHERE
157+ (domain2.name IS NOT NULL OR node.domain_id = %s) AND
158+ """
159+ else:
160+ # For subnets, we need ALL the names, so that we can correctly
161+ # identify which ones should have the FQDN. dns/zonegenerator.py
162+ # optimizes based on this, and only calls once with a subnet,
163+ # expecting to get all the subnets back in one table.
164+ iface_sql_query += """
165+ WHERE
166+ """
167+ iface_sql_query += """
168 staticip.ip IS NOT NULL AND
169 host(staticip.ip) != '' AND
170- (""" + search_term + """ = %s) AND
171 (
172 node.disable_ipv4 IS FALSE OR
173 family(staticip.ip) <> 4
174@@ -449,19 +470,20 @@
175 # We get user reserved et al mappings first, so that we can overwrite
176 # TTL as we process the return from the SQL horror above.
177 mapping = self._get_user_reserved_mappings(domain_or_subnet)
178+ # All of the mappings that we got mean that we will only want to add
179+ # addresses for the boot interface (is_boot == True).
180 iface_is_boot = defaultdict(bool, {
181 hostname: True for hostname in mapping.keys()
182 })
183- cursor.execute(sql_query, (domain_or_subnet.id,))
184+ cursor.execute(sql_query, query_parms)
185 # The records from the query provide, for each hostname (after
186 # stripping domain), the boot and non-boot interface ip address in ipv4
187 # and ipv6. Our task: if there are boot interace IPs, they win. If
188 # there are none, then whatever we got wins. The ORDER BY means that
189 # we will see all of the boot interfaces before we see any non-boot
190 # interface IPs. See Bug#1584850
191- for (node_name, system_id, node_type, ttl, domain_name,
192+ for (fqdn, system_id, node_type, ttl,
193 ip, is_boot) in cursor.fetchall():
194- fqdn = "%s.%s" % (strip_domain(node_name), domain_name)
195 mapping[fqdn].node_type = node_type
196 mapping[fqdn].system_id = system_id
197 mapping[fqdn].ttl = ttl
198@@ -473,9 +495,8 @@
199 # Next, get all the addresses, on all the interfaces, and add the ones
200 # that are not already present on the FQDN as $IFACE.$FQDN.
201 cursor.execute(iface_sql_query, (domain_or_subnet.id,))
202- for (node_name, system_id, node_type, ttl,
203- domain_name, ip, iface_name) in cursor.fetchall():
204- fqdn = "%s.%s" % (strip_domain(node_name), domain_name)
205+ for (fqdn, system_id, node_type, ttl,
206+ ip, iface_name) in cursor.fetchall():
207 if ip not in mapping[fqdn].ips:
208 name = "%s.%s" % (iface_name, fqdn)
209 mapping[name].node_type = node_type
210
211=== modified file 'src/maasserver/models/tests/test_staticipaddress.py'
212--- src/maasserver/models/tests/test_staticipaddress.py 2016-07-07 23:21:57 +0000
213+++ src/maasserver/models/tests/test_staticipaddress.py 2016-07-08 19:43:07 +0000
214@@ -33,6 +33,7 @@
215 HostnameIPMapping,
216 StaticIPAddress,
217 )
218+from maasserver.models.subnet import Subnet
219 from maasserver.testing.factory import factory
220 from maasserver.testing.testcase import (
221 MAASServerTestCase,
222@@ -356,6 +357,26 @@
223 mapping = StaticIPAddress.objects.get_hostname_ip_mapping(domain)
224 self.assertEqual(expected_mapping, mapping)
225
226+ def test_get_hostname_ip_mapping_returns_all_mappings_for_subnet(self):
227+ domain = Domain.objects.get_default_domain()
228+ expected_mapping = {}
229+ for _ in range(3):
230+ node = factory.make_Node(interface=True, disable_ipv4=False)
231+ boot_interface = node.get_boot_interface()
232+ subnet = factory.make_Subnet()
233+ staticip = factory.make_StaticIPAddress(
234+ alloc_type=IPADDRESS_TYPE.STICKY,
235+ ip=factory.pick_ip_in_Subnet(subnet),
236+ subnet=subnet, interface=boot_interface)
237+ full_hostname = "%s.%s" % (node.hostname, domain.name)
238+ expected_mapping[full_hostname] = HostnameIPMapping(
239+ node.system_id, 30, {staticip.ip}, node.node_type)
240+ # See also LP#1600259. It doesn't matter what subnet is passed in, you
241+ # get all of them.
242+ mapping = StaticIPAddress.objects.get_hostname_ip_mapping(
243+ Subnet.objects.first())
244+ self.assertEqual(expected_mapping, mapping)
245+
246 def test_get_hostname_ip_mapping_returns_fqdn_and_other(self):
247 hostname = factory.make_name('hostname')
248 domainname = factory.make_name('domain')