Merge lp:~jtv/maas/dns-zones-explicit-data-flow 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: 1843
Proposed branch: lp:~jtv/maas/dns-zones-explicit-data-flow
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~jtv/maas/delegate-write_config
Diff against target: 366 lines (+86/-90)
3 files modified
src/maasserver/tests/test_dns.py (+3/-3)
src/provisioningserver/dns/config.py (+70/-54)
src/provisioningserver/dns/tests/test_config.py (+13/-33)
To merge this branch: bzr merge lp:~jtv/maas/dns-zones-explicit-data-flow
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+202620@code.launchpad.net

This proposal supersedes a proposal from 2014-01-22.

Commit message

Make data-passing in the DNS zone config classes more explicit.

Description of the change

This completes my refactoring of the DNS config module. It makes private some instance variables that are not effectively part of the classes' interfaces. Helper methods become class methods and receive their inputs as parameters, so that hopefully it'll be easier to assess the impact of future changes. It also makes them a bit easier to test, but that's just a happy coincidence. The main thing is improving our ability to understand and change the code.

A new method make_parameters does the common part of what the two get_context methods did. The remainder of what those methods did is now inlined into the write_config methods.

Jeroen

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

[0]

82 - def get_cname_mapping(self):
83 + @classmethod
84 + def get_cname_mapping(cls, mapping):
85 """Return a generator with the mapping: hostname->generated hostname.
86
87 The mapping only contains hosts for which the two host names differ.

98 - def get_static_mapping(self):
99 + @classmethod
100 + def get_static_mapping(cls, domain, networks, dns_ip):
101 """Return a generator with the mapping fqdn->ip for the generated ips.

Both these methods have been turned into class-level utilities, it might be worth updating their docstrings to describe the parameters.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Updated.

Revision history for this message
MAAS Lander (maas-lander) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/tests/test_dns.py'
2--- src/maasserver/tests/test_dns.py 2013-12-21 16:36:41 +0000
3+++ src/maasserver/tests/test_dns.py 2014-01-22 11:01:40 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2012, 2013 Canonical Ltd. This software is licensed under the
6+# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Test DNS module."""
10@@ -416,7 +416,7 @@
11 return MatchesAll(
12 IsInstance(DNSForwardZoneConfig),
13 MatchesStructure.byEquality(
14- domain=domain, networks=networks))
15+ domain=domain, _networks=networks))
16
17
18 def reverse_zone(domain, network):
19@@ -428,7 +428,7 @@
20 return MatchesAll(
21 IsInstance(DNSReverseZoneConfig),
22 MatchesStructure.byEquality(
23- domain=domain, network=network))
24+ domain=domain, _network=network))
25
26
27 class TestZoneGenerator(MAASServerTestCase):
28
29=== modified file 'src/provisioningserver/dns/config.py'
30--- src/provisioningserver/dns/config.py 2014-01-22 11:01:40 +0000
31+++ src/provisioningserver/dns/config.py 2014-01-22 11:01:40 +0000
32@@ -286,8 +286,16 @@
33 self.serial = serial
34 self.target_path = compose_config_path('zone.%s' % self.zone_name)
35
36+ def make_parameters(self):
37+ """Return a dict of the common template parameters."""
38+ return {
39+ 'domain': self.domain,
40+ 'serial': self.serial,
41+ 'modified': unicode(datetime.today()),
42+ }
43+
44 @classmethod
45- def write_zone_file(cls, output_file, parameters):
46+ def write_zone_file(cls, output_file, *parameters):
47 """Write a zone file based on the zone file template.
48
49 There is a subtlety with zone files: their filesystem timestamp must
50@@ -295,7 +303,7 @@
51 support a resolution of one second, and so this method may set an
52 unexpected modification time in order to maintain that property.
53 """
54- content = render_dns_template(cls.template_file_name, parameters)
55+ content = render_dns_template(cls.template_file_name, *parameters)
56 with report_missing_config_dir():
57 incremental_write(content, output_file, mode=0644)
58
59@@ -306,6 +314,9 @@
60 def __init__(self, domain, **kwargs):
61 """See `DNSZoneConfigBase.__init__`.
62
63+ :param domain: The domain name of the forward zone.
64+ :param serial: The serial to use in the zone file. This must increment
65+ on each change.
66 :param networks: The networks that the mapping exists within.
67 :type networks: Sequence of :class:`netaddr.IPNetwork`
68 :param dns_ip: The IP address of the DNS server authoritative for this
69@@ -313,57 +324,59 @@
70 :param mapping: A hostname:ip-address mapping for all known hosts in
71 the zone.
72 """
73- self.networks = kwargs.pop('networks', [])
74- self.dns_ip = kwargs.pop('dns_ip', None)
75- self.mapping = kwargs.pop('mapping', {})
76+ self._networks = kwargs.pop('networks', [])
77+ self._dns_ip = kwargs.pop('dns_ip', None)
78+ self._mapping = kwargs.pop('mapping', {})
79 super(DNSForwardZoneConfig, self).__init__(
80 domain, zone_name=domain, **kwargs)
81
82- def get_cname_mapping(self):
83- """Return a generator with the mapping: hostname->generated hostname.
84+ @classmethod
85+ def get_cname_mapping(cls, mapping):
86+ """Return a generator mapping hostnames to generated hostnames.
87
88 The mapping only contains hosts for which the two host names differ.
89
90+ :param mapping: A dict mapping host names to IP addresses.
91 :return: A generator of tuples: (host name, generated host name).
92 """
93 # We filter out cases where the two host names are identical: it
94 # would be wrong to define a CNAME that maps to itself.
95- for hostname, ip in self.mapping.items():
96+ for hostname, ip in mapping.items():
97 generated_name = generated_hostname(ip)
98 if generated_name != hostname:
99 yield (hostname, generated_name)
100
101- def get_static_mapping(self):
102- """Return a generator with the mapping fqdn->ip for the generated ips.
103+ @classmethod
104+ def get_static_mapping(cls, domain, networks, dns_ip):
105+ """Return a generator mapping a network's generated fqdns to ips.
106
107 The generated mapping is the mapping between the generated hostnames
108 and the IP addresses for all the possible IP addresses in zone.
109- Note that we return a list of tuples instead of a dictionary in order
110- to be able to return a generator.
111+ The return type is a sequence of tuples, not a dictionary, so that we
112+ don't have to generate the whole thing at once.
113+
114+ :param domain: Zone's domain name.
115+ :param networks: Sequence of :class:`netaddr.IPNetwork` describing
116+ the networks whose IP-based generated host names should be mapped
117+ to the corresponding IP addresses.
118+ :param dns_ip: IP address for the zone's authoritative DNS server.
119 """
120- ips = imap(unicode, chain.from_iterable(self.networks))
121+ ips = imap(unicode, chain.from_iterable(networks))
122 static_mapping = ((generated_hostname(ip), ip) for ip in ips)
123 # Add A record for the name server's IP.
124- return chain([('%s.' % self.domain, self.dns_ip)], static_mapping)
125-
126- def get_context(self):
127- """Return the dict used to render the DNS zone file.
128-
129- That context dict is used to render the DNS zone file.
130- """
131- return {
132- 'domain': self.domain,
133- 'serial': self.serial,
134- 'modified': unicode(datetime.today()),
135- 'mappings': {
136- 'CNAME': self.get_cname_mapping(),
137- 'A': self.get_static_mapping(),
138- }
139- }
140+ return chain([('%s.' % domain, dns_ip)], static_mapping)
141
142 def write_config(self):
143 """Write the zone file."""
144- self.write_zone_file(self.target_path, self.get_context())
145+ self.write_zone_file(
146+ self.target_path, self.make_parameters(),
147+ {
148+ 'mappings': {
149+ 'CNAME': self.get_cname_mapping(self._mapping),
150+ 'A': self.get_static_mapping(
151+ self.domain, self._networks, self._dns_ip),
152+ },
153+ })
154
155
156 class DNSReverseZoneConfig(DNSZoneConfigBase):
157@@ -372,11 +385,14 @@
158 def __init__(self, domain, **kwargs):
159 """See `DNSZoneConfigBase.__init__`.
160
161+ :param domain: The domain name of the forward zone.
162+ :param serial: The serial to use in the zone file. This must increment
163+ on each change.
164 :param network: The network that the mapping exists within.
165 :type network: :class:`netaddr.IPNetwork`
166 """
167- self.network = kwargs.pop("network", None)
168- zone_name = self.compose_zone_name(self.network)
169+ self._network = kwargs.pop("network", None)
170+ zone_name = self.compose_zone_name(self._network)
171 super(DNSReverseZoneConfig, self).__init__(
172 domain, zone_name=zone_name, **kwargs)
173
174@@ -402,33 +418,33 @@
175 significant_octets = islice(reversed(ip.words), byte_num)
176 return '.'.join(imap(unicode, significant_octets))
177
178- def get_static_mapping(self):
179- """Return the reverse generated mapping: (shortened) ip->fqdn.
180+ @classmethod
181+ def get_static_mapping(cls, domain, network):
182+ """Return reverse mapping: shortened IPs to generated fqdns.
183
184 The reverse generated mapping is the mapping between the IP addresses
185 and the generated hostnames for all the possible IP addresses in zone.
186+
187+ :param domain: Zone's domain name.
188+ :param network: Network whose IP addresses should be mapped to their
189+ corresponding generated hostnames.
190+ :type network: :class:`netaddr.IPNetwork`
191 """
192- byte_num = 4 - self.network.netmask.words.count(255)
193+ byte_num = 4 - network.netmask.words.count(255)
194 return (
195- (self.shortened_reversed_ip(ip, byte_num),
196- '%s.%s.' % (generated_hostname(ip), self.domain))
197- for ip in self.network
198- )
199-
200- def get_context(self):
201- """Return the dict used to render the DNS reverse zone file.
202-
203- That context dict is used to render the DNS reverse zone file.
204- """
205- return {
206- 'domain': self.domain,
207- 'serial': self.serial,
208- 'modified': unicode(datetime.today()),
209- 'mappings': {
210- 'PTR': self.get_static_mapping(),
211- }
212- }
213+ (
214+ cls.shortened_reversed_ip(ip, byte_num),
215+ '%s.%s.' % (generated_hostname(ip), domain),
216+ )
217+ for ip in network
218+ )
219
220 def write_config(self):
221 """Write the zone file."""
222- self.write_zone_file(self.target_path, self.get_context())
223+ self.write_zone_file(
224+ self.target_path, self.make_parameters(),
225+ {
226+ 'mappings': {
227+ 'PTR': self.get_static_mapping(self.domain, self._network),
228+ },
229+ })
230
231=== modified file 'src/provisioningserver/dns/tests/test_config.py'
232--- src/provisioningserver/dns/tests/test_config.py 2014-01-22 11:01:40 +0000
233+++ src/provisioningserver/dns/tests/test_config.py 2014-01-22 11:01:40 +0000
234@@ -387,8 +387,8 @@
235 MatchesStructure.byEquality(
236 domain=domain,
237 serial=serial,
238- mapping=mapping,
239- networks=[network],
240+ _mapping=mapping,
241+ _networks=[network],
242 )
243 )
244
245@@ -400,15 +400,9 @@
246 dns_zone_config.target_path)
247
248 def test_forward_zone_get_cname_mapping_returns_iterator(self):
249- name = factory.getRandomString()
250- network = IPNetwork('192.12.0.1/30')
251- dns_ip = factory.getRandomIPInNetwork(network)
252- dns_zone_config = DNSForwardZoneConfig(
253- name, networks=[network], dns_ip=dns_ip,
254- mapping={
255- factory.make_name('hostname'): factory.getRandomIPAddress()})
256 self.assertThat(
257- dns_zone_config.get_cname_mapping(),
258+ DNSForwardZoneConfig.get_cname_mapping(
259+ {factory.make_name('host'): factory.getRandomIPAddress()}),
260 MatchesAll(
261 IsInstance(Iterable), Not(IsInstance(Sequence))))
262
263@@ -416,24 +410,17 @@
264 # We don't write cname records to map host names to themselves.
265 # Without this, a node would get an invalid cname record upon
266 # enlistment.
267- zone = factory.make_name('zone')
268 network = IPNetwork('10.250.99.0/24')
269 ip = factory.getRandomIPInNetwork(network)
270 generated_name = generated_hostname(ip)
271- dns_zone_config = DNSForwardZoneConfig(
272- zone, networks=[network],
273- dns_ip=factory.getRandomIPInNetwork(network),
274- mapping={generated_name: ip})
275 self.assertNotIn(
276 generated_name,
277- dict(dns_zone_config.get_cname_mapping()))
278+ dict(DNSForwardZoneConfig.get_cname_mapping({generated_name: ip})))
279
280 def test_get_static_mapping(self):
281 name = factory.getRandomString()
282 network = IPNetwork('192.12.0.1/30')
283 dns_ip = factory.getRandomIPInNetwork(network)
284- dns_zone_config = DNSForwardZoneConfig(
285- name, networks=[network], dns_ip=dns_ip)
286 self.assertItemsEqual(
287 [
288 ('%s.' % name, dns_ip),
289@@ -442,17 +429,14 @@
290 (generated_hostname('192.12.0.2'), '192.12.0.2'),
291 (generated_hostname('192.12.0.3'), '192.12.0.3'),
292 ],
293- dns_zone_config.get_static_mapping(),
294- )
295+ DNSForwardZoneConfig.get_static_mapping(name, [network], dns_ip))
296
297 def test_forward_zone_get_static_mapping_returns_iterator(self):
298 name = factory.getRandomString()
299 network = IPNetwork('192.12.0.1/30')
300 dns_ip = factory.getRandomIPInNetwork(network)
301- dns_zone_config = DNSForwardZoneConfig(
302- name, networks=[network], dns_ip=dns_ip)
303 self.assertThat(
304- dns_zone_config.get_static_mapping(),
305+ DNSForwardZoneConfig.get_static_mapping(name, [network], dns_ip),
306 MatchesAll(
307 IsInstance(Iterable), Not(IsInstance(Sequence))))
308
309@@ -460,8 +444,6 @@
310 name = factory.getRandomString()
311 networks = IPNetwork('11.11.11.11/31'), IPNetwork('22.22.22.22/31')
312 dns_ip = factory.getRandomIPInNetwork(networks[0])
313- dns_zone_config = DNSForwardZoneConfig(
314- name, networks=networks, dns_ip=dns_ip)
315 self.assertItemsEqual(
316 [
317 ('%s.' % name, dns_ip),
318@@ -470,7 +452,7 @@
319 (generated_hostname('22.22.22.22'), '22.22.22.22'),
320 (generated_hostname('22.22.22.23'), '22.22.22.23'),
321 ],
322- dns_zone_config.get_static_mapping(),
323+ DNSForwardZoneConfig.get_static_mapping(name, networks, dns_ip),
324 )
325
326 def test_writes_dns_zone_config(self):
327@@ -534,7 +516,7 @@
328 MatchesStructure.byEquality(
329 domain=domain,
330 serial=serial,
331- network=network,
332+ _network=network,
333 )
334 )
335
336@@ -586,17 +568,16 @@
337 dns_zone_config.zone_name)
338
339 def test_get_static_mapping_returns_iterator(self):
340- dns_zone_config = DNSReverseZoneConfig(
341- factory.getRandomString(), network=IPNetwork('192.12.0.1/30'))
342 self.assertThat(
343- dns_zone_config.get_static_mapping(),
344+ DNSReverseZoneConfig.get_static_mapping(
345+ factory.make_name('zone'),
346+ network=IPNetwork('192.12.0.1/30')),
347 MatchesAll(
348 IsInstance(Iterable), Not(IsInstance(Sequence))))
349
350 def test_get_static_mapping(self):
351 name = factory.getRandomString()
352 network = IPNetwork('192.12.0.1/30')
353- dns_zone_config = DNSReverseZoneConfig(name, network=network)
354 self.assertItemsEqual(
355 [
356 ('0', '%s.' % generated_hostname('192.12.0.0', name)),
357@@ -604,8 +585,7 @@
358 ('2', '%s.' % generated_hostname('192.12.0.2', name)),
359 ('3', '%s.' % generated_hostname('192.12.0.3', name)),
360 ],
361- dns_zone_config.get_static_mapping(),
362- )
363+ DNSReverseZoneConfig.get_static_mapping(name, network))
364
365 def test_writes_dns_zone_config_with_NS_record(self):
366 target_dir = patch_dns_config_path(self)