Merge lp:~jtv/maas/delegate-write_config 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: 1841
Proposed branch: lp:~jtv/maas/delegate-write_config
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 329 lines (+47/-89)
4 files modified
src/maasserver/dns.py (+3/-3)
src/provisioningserver/dns/config.py (+33/-46)
src/provisioningserver/dns/tests/test_config.py (+9/-35)
src/provisioningserver/tests/test_tasks.py (+2/-5)
To merge this branch: bzr merge lp:~jtv/maas/delegate-write_config
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+202595@code.launchpad.net

Commit message

Untangle data flows between the DNSZoneConfig classes.

Description of the change

This is where my cleanups for the dns config module really come together. To understand what's going on, imagine the class hierarchy: there's an abstract DNSZoneConfigBase class, with two concrete child classes DNSForwardZoneConfig and DNSReverseZoneConfig. The most important thing they do is write a zone config file. This is implemented in the base class, but calls on various abstract methods and properties to get specific information from the concrete class. It is that convoluted, bidirectional data flow which makes it so hard to understand and change the module.

In this branch you'll see:
 * the "mapping" attribute move from the base class to just one child class. It was passed in other places too, but never actually used.
 * zone_name become a simple instance variable, explicitly computed by the derived-class constructor and passed to the base-class constructor.
 * template_path disappear, because render_dns_template already knows how to compute this path.
 * get_context be demoted from an abstract method with 2 implementations to a pair of merely similar helpers.
 * write_config() delegating explicitly to a base-class method, after gathering its data up-front.
 * zone composition on DNSReverseZoneConfig become a class method, ruling out hidden data flows.

There's a bit more polish coming, but not much: the implicit data flows through instance variables within the concrete classes can now be replaced with explicit parameters, and perhaps one or two instance variables can be retired altogether. (Actually it looks as if several are currently only used for inspection by tests.) But the module is now in a state where we can see what data is needed, and what for. We can then start answering questions like "how are we going to deal with hostnames that are in multiple networks?"

Jeroen

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

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

 review: approve
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.15 (GNU/Linux)
Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/

iEYEARECAAYFAlLfaZIACgkQWhGlTF8G/HcYQgCglwEnMn+KP6JyY7IRuBjrpAyp
k18AnAo2zkY3w85NacqQDA/jjcQ7YajA
=BA2Q
-----END PGP SIGNATURE-----

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.py'
2--- src/maasserver/dns.py 2014-01-21 16:39:55 +0000
3+++ src/maasserver/dns.py 2014-01-22 04:15:05 +0000
4@@ -226,14 +226,14 @@
5 )
6
7 @staticmethod
8- def _gen_reverse_zones(nodegroups, serial, mappings, networks):
9+ def _gen_reverse_zones(nodegroups, serial, networks):
10 """Generator of reverse zones, sorted by network."""
11 get_domain = lambda nodegroup: nodegroup.name
12 reverse_nodegroups = sorted(nodegroups, key=networks.get)
13 for nodegroup in reverse_nodegroups:
14 yield DNSReverseZoneConfig(
15 get_domain(nodegroup), serial=serial,
16- mapping=mappings[nodegroup], network=networks[nodegroup])
17+ network=networks[nodegroup])
18
19 def __iter__(self):
20 forward_nodegroups = self._get_forward_nodegroups(self.nodegroups)
21@@ -245,7 +245,7 @@
22 self._gen_forward_zones(
23 forward_nodegroups, serial, mappings, networks),
24 self._gen_reverse_zones(
25- reverse_nodegroups, serial, mappings, networks),
26+ reverse_nodegroups, serial, networks),
27 )
28
29 def as_list(self):
30
31=== modified file 'src/provisioningserver/dns/config.py'
32--- src/provisioningserver/dns/config.py 2014-01-22 02:32:04 +0000
33+++ src/provisioningserver/dns/config.py 2014-01-22 04:15:05 +0000
34@@ -21,11 +21,7 @@
35 ]
36
37
38-from abc import (
39- ABCMeta,
40- abstractmethod,
41- abstractproperty,
42- )
43+from abc import ABCMeta
44 from contextlib import contextmanager
45 from datetime import datetime
46 import errno
47@@ -252,14 +248,13 @@
48 :raises DNSConfigDirectoryMissing: if the DNS configuration directory
49 does not exist.
50 """
51- template_path = locate_config(TEMPLATES_DIR, self.template_file_name)
52 context = {
53 'zones': self.zones,
54 'DNS_CONFIG_DIR': conf.DNS_CONFIG_DIR,
55 'named_rndc_conf_path': get_named_rndc_conf_path(),
56 'modified': unicode(datetime.today()),
57 }
58- content = render_dns_template(template_path, kwargs, context)
59+ content = render_dns_template(self.template_file_name, kwargs, context)
60 target_path = compose_config_path(self.target_file_name)
61 with report_missing_config_dir():
62 atomic_write(content, target_path, overwrite=overwrite, mode=0644)
63@@ -279,68 +274,50 @@
64
65 template_file_name = 'zone.template'
66
67- def __init__(self, domain, serial=None, mapping=None):
68+ def __init__(self, domain, zone_name, serial=None):
69 """
70 :param domain: The domain name of the forward zone.
71+ :param zone_name: Fully-qualified zone name.
72 :param serial: The serial to use in the zone file. This must increment
73 on each change.
74- :param mapping: A hostname:ip-address mapping for all known hosts in
75- the zone.
76 """
77 self.domain = domain
78+ self.zone_name = zone_name
79 self.serial = serial
80- self.mapping = {} if mapping is None else mapping
81-
82- @abstractproperty
83- def zone_name(self):
84- """Return the zone's fully-qualified name."""
85-
86- @property
87- def template_path(self):
88- return locate_config(TEMPLATES_DIR, self.template_file_name)
89-
90- @property
91- def target_path(self):
92- """Return the full path of the DNS zone file."""
93- return compose_config_path('zone.%s' % self.zone_name)
94-
95- @abstractmethod
96- def get_context(self):
97- """Dictionary containing parameters to be included in the
98- parameters used when rendering the template."""
99-
100- def write_config(self):
101- """Write out this DNS zone file.
102+ self.target_path = compose_config_path('zone.%s' % self.zone_name)
103+
104+ @classmethod
105+ def write_zone_file(cls, output_file, parameters):
106+ """Write a zone file based on the zone file template.
107
108 There is a subtlety with zone files: their filesystem timestamp must
109 increase with every rewrite. Some filesystems (ext3?) only seem to
110 support a resolution of one second, and so this method may set an
111 unexpected modification time in order to maintain that property.
112 """
113- content = render_dns_template(self.template_path, self.get_context())
114+ content = render_dns_template(cls.template_file_name, parameters)
115 with report_missing_config_dir():
116- incremental_write(content, self.target_path, mode=0644)
117+ incremental_write(content, output_file, mode=0644)
118
119
120 class DNSForwardZoneConfig(DNSZoneConfigBase):
121 """Writes forward zone files."""
122
123- def __init__(self, *args, **kwargs):
124+ def __init__(self, domain, **kwargs):
125 """See `DNSZoneConfigBase.__init__`.
126
127 :param networks: The networks that the mapping exists within.
128 :type networks: Sequence of :class:`netaddr.IPNetwork`
129 :param dns_ip: The IP address of the DNS server authoritative for this
130 zone.
131+ :param mapping: A hostname:ip-address mapping for all known hosts in
132+ the zone.
133 """
134 self.networks = kwargs.pop('networks', [])
135 self.dns_ip = kwargs.pop('dns_ip', None)
136- super(DNSForwardZoneConfig, self).__init__(*args, **kwargs)
137-
138- @property
139- def zone_name(self):
140- """Return the name of the forward zone."""
141- return self.domain
142+ self.mapping = kwargs.pop('mapping', {})
143+ super(DNSForwardZoneConfig, self).__init__(
144+ domain, zone_name=domain, **kwargs)
145
146 def get_cname_mapping(self):
147 """Return a generator with the mapping: hostname->generated hostname.
148@@ -384,23 +361,29 @@
149 }
150 }
151
152+ def write_config(self):
153+ """Write the zone file."""
154+ self.write_zone_file(self.target_path, self.get_context())
155+
156
157 class DNSReverseZoneConfig(DNSZoneConfigBase):
158 """Writes reverse zone files."""
159
160- def __init__(self, *args, **kwargs):
161+ def __init__(self, domain, **kwargs):
162 """See `DNSZoneConfigBase.__init__`.
163
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- super(DNSReverseZoneConfig, self).__init__(*args, **kwargs)
169+ zone_name = self.compose_zone_name(self.network)
170+ super(DNSReverseZoneConfig, self).__init__(
171+ domain, zone_name=zone_name, **kwargs)
172
173- @property
174- def zone_name(self):
175+ @classmethod
176+ def compose_zone_name(cls, network):
177 """Return the name of the reverse zone."""
178- broadcast, netmask = self.network.broadcast, self.network.netmask
179+ broadcast, netmask = network.broadcast, network.netmask
180 octets = broadcast.words[:netmask.words.count(255)]
181 return '%s.in-addr.arpa' % '.'.join(imap(unicode, reversed(octets)))
182
183@@ -445,3 +428,7 @@
184 'PTR': self.get_static_mapping(),
185 }
186 }
187+
188+ def write_config(self):
189+ """Write the zone file."""
190+ self.write_zone_file(self.target_path, self.get_context())
191
192=== modified file 'src/provisioningserver/dns/tests/test_config.py'
193--- src/provisioningserver/dns/tests/test_config.py 2014-01-22 02:32:04 +0000
194+++ src/provisioningserver/dns/tests/test_config.py 2014-01-22 04:15:05 +0000
195@@ -52,11 +52,9 @@
196 report_missing_config_dir,
197 set_up_options_conf,
198 setup_rndc,
199- TEMPLATES_DIR,
200 uncomment_named_conf,
201 )
202 from provisioningserver.dns.utils import generated_hostname
203-from provisioningserver.utils import locate_config
204 from testtools.matchers import (
205 Contains,
206 ContainsAll,
207@@ -339,9 +337,7 @@
208 forward_zone = DNSForwardZoneConfig(
209 domain, mapping={factory.getRandomString(): ip},
210 networks=[network])
211- reverse_zone = DNSReverseZoneConfig(
212- domain, mapping={factory.getRandomString(): ip},
213- network=network)
214+ reverse_zone = DNSReverseZoneConfig(domain, network=network)
215 dnsconfig = DNSConfig((forward_zone, reverse_zone))
216 dnsconfig.write_config()
217 self.assertThat(
218@@ -385,7 +381,7 @@
219 ip = factory.getRandomIPInNetwork(network)
220 mapping = {hostname: ip}
221 dns_zone_config = DNSForwardZoneConfig(
222- domain, serial, mapping, networks=[network])
223+ domain, serial=serial, mapping=mapping, networks=[network])
224 self.assertThat(
225 dns_zone_config,
226 MatchesStructure.byEquality(
227@@ -400,14 +396,8 @@
228 domain = factory.make_name('zone')
229 dns_zone_config = DNSForwardZoneConfig(domain)
230 self.assertEqual(
231- (
232- locate_config(TEMPLATES_DIR, 'zone.template'),
233- os.path.join(conf.DNS_CONFIG_DIR, 'zone.%s' % domain),
234- ),
235- (
236- dns_zone_config.template_path,
237- dns_zone_config.target_path,
238- ))
239+ os.path.join(conf.DNS_CONFIG_DIR, 'zone.%s' % domain),
240+ dns_zone_config.target_path)
241
242 def test_forward_zone_get_cname_mapping_returns_iterator(self):
243 name = factory.getRandomString()
244@@ -536,18 +526,14 @@
245 def test_fields(self):
246 domain = factory.getRandomString()
247 serial = random.randint(1, 200)
248- hostname = factory.getRandomString()
249 network = factory.getRandomNetwork()
250- ip = factory.getRandomIPInNetwork(network)
251- mapping = {hostname: ip}
252 dns_zone_config = DNSReverseZoneConfig(
253- domain, serial, mapping, network=network)
254+ domain, serial=serial, network=network)
255 self.assertThat(
256 dns_zone_config,
257 MatchesStructure.byEquality(
258 domain=domain,
259 serial=serial,
260- mapping=mapping,
261 network=network,
262 )
263 )
264@@ -576,24 +562,15 @@
265 dns_zone_config = DNSReverseZoneConfig(
266 domain, network=IPNetwork("192.168.0.0/22"))
267 self.assertEqual(
268- (
269- locate_config(TEMPLATES_DIR, 'zone.template'),
270- os.path.join(conf.DNS_CONFIG_DIR, reverse_file_name)
271- ),
272- (
273- dns_zone_config.template_path,
274- dns_zone_config.target_path,
275- ))
276+ os.path.join(conf.DNS_CONFIG_DIR, reverse_file_name),
277+ dns_zone_config.target_path)
278
279 def test_reverse_data_slash_24(self):
280 # DNSReverseZoneConfig calculates the reverse data correctly for
281 # a /24 network.
282 domain = factory.make_name('zone')
283- hostname = factory.getRandomString()
284- ip = '192.168.0.5'
285 network = IPNetwork('192.168.0.1/24')
286- dns_zone_config = DNSReverseZoneConfig(
287- domain, mapping={hostname: ip}, network=network)
288+ dns_zone_config = DNSReverseZoneConfig(domain, network=network)
289 self.assertEqual(
290 '0.168.192.in-addr.arpa',
291 dns_zone_config.zone_name)
292@@ -602,11 +579,8 @@
293 # DNSReverseZoneConfig calculates the reverse data correctly for
294 # a /22 network.
295 domain = factory.getRandomString()
296- hostname = factory.getRandomString()
297- ip = '192.168.0.10'
298 network = IPNetwork('192.168.0.1/22')
299- dns_zone_config = DNSReverseZoneConfig(
300- domain, mapping={hostname: ip}, network=network)
301+ dns_zone_config = DNSReverseZoneConfig(domain, network=network)
302 self.assertEqual(
303 '168.192.in-addr.arpa',
304 dns_zone_config.zone_name)
305
306=== modified file 'src/provisioningserver/tests/test_tasks.py'
307--- src/provisioningserver/tests/test_tasks.py 2014-01-21 08:12:06 +0000
308+++ src/provisioningserver/tests/test_tasks.py 2014-01-22 04:15:05 +0000
309@@ -347,8 +347,7 @@
310 domain, serial=random.randint(1, 100),
311 mapping={factory.getRandomString(): ip}, networks=[network])
312 reverse_zone = DNSReverseZoneConfig(
313- domain, serial=random.randint(1, 100),
314- mapping={factory.getRandomString(): ip}, network=network)
315+ domain, serial=random.randint(1, 100), network=network)
316 result = write_dns_zone_config.delay(
317 zones=[forward_zone, reverse_zone],
318 callback=rndc_command.subtask(args=[command]))
319@@ -461,9 +460,7 @@
320 mapping={factory.getRandomString(): ip},
321 networks=[network]),
322 DNSReverseZoneConfig(
323- domain, serial=random.randint(1, 100),
324- mapping={factory.getRandomString(): ip},
325- network=network),
326+ domain, serial=random.randint(1, 100), network=network),
327 ]
328 command = factory.getRandomString()
329 result = write_full_dns_config.delay(