Merge lp:~lamont/maas/dns-testfailures-check-soa into lp:~maas-committers/maas/trunk
- dns-testfailures-check-soa
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | LaMont Jones |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4750 |
Proposed branch: | lp:~lamont/maas/dns-testfailures-check-soa |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
773 lines (+193/-159) 11 files modified
required-packages/dev (+1/-0) src/maasserver/dns/config.py (+4/-3) src/maasserver/dns/tests/test_config.py (+76/-30) src/maasserver/dns/tests/test_zonegenerator.py (+19/-17) src/maasserver/dns/zonegenerator.py (+3/-8) src/maasserver/sequence.py (+24/-0) src/maasserver/tests/test_sequence.py (+31/-1) src/provisioningserver/testing/bindfixture.py (+2/-1) src/provisioningserver/utils/fs.py (+16/-53) src/provisioningserver/utils/tests/test_fs.py (+16/-46) utilities/check-imports (+1/-0) |
To merge this branch: | bzr merge lp:~lamont/maas/dns-testfailures-check-soa |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email: mp+287861@code.launchpad.net |
Commit message
Have Sequence remember what it last handed out via next(), and make sure that's initialized. Update dns test suite to check for the expected SOA for a zone before it actually tests the answer it wants. Remove extra (incorrect) bumping of the SOA.
Description of the change
Have Sequence remember what it last handed out via next(), and make sure that's initialized. Update dns test suite to check for the expected SOA for a zone before it actually tests the answer it wants. Remove extra (incorrect) bumping of the SOA.
Mike Pontillo (mpontillo) wrote : | # |
LaMont Jones (lamont) wrote : | # |
With bind9 1:9.10.3.dfsg.P2-5 (in xenial since last night), and this branch, 199 iterations of test_config.py running on the lander yielded 73128 total lines of output, and zero failures.
Gavin Panella (allenap) wrote : | # |
This is awesome; thank you for fixing those tests, and for the BIND work.
I do have some serious concerns about the code in here, particularly around the changes to Sequence. However, I don't think anything invalidates your approach, it just needs fixing.
I wrote some code as I reviewed this. Where I've proposed new or altered code, you can get it by merging lp:~allenap/maas/dns-testfailures-check-soa if you want.
Preview Diff
1 | === modified file 'required-packages/dev' |
2 | --- required-packages/dev 2016-02-17 16:12:30 +0000 |
3 | +++ required-packages/dev 2016-03-08 17:17:00 +0000 |
4 | @@ -15,5 +15,6 @@ |
5 | npm |
6 | python-pocket-lint |
7 | python3-django-nose |
8 | +python3-dnspython |
9 | socat |
10 | xvfb |
11 | |
12 | === modified file 'src/maasserver/dns/config.py' |
13 | --- src/maasserver/dns/config.py 2016-02-11 23:37:45 +0000 |
14 | +++ src/maasserver/dns/config.py 2016-03-08 17:17:00 +0000 |
15 | @@ -89,12 +89,12 @@ |
16 | return |
17 | |
18 | default_ttl = Config.objects.get_config('default_dns_ttl') |
19 | + serial = next_zone_serial() |
20 | zones_to_write = ZoneGenerator( |
21 | domains, subnets, default_ttl, |
22 | - serial_generator=next_zone_serial).as_list() |
23 | + serial).as_list() |
24 | if len(zones_to_write) == 0: |
25 | return None |
26 | - serial = next_zone_serial() |
27 | # Compute non-None zones. |
28 | zones = ZoneGenerator( |
29 | Domain.objects.all(), Subnet.objects.all(), |
30 | @@ -232,9 +232,10 @@ |
31 | domains = Domain.objects.filter(authoritative=True) |
32 | subnets = Subnet.objects.exclude(rdns_mode=RDNS_MODE.DISABLED) |
33 | default_ttl = Config.objects.get_config('default_dns_ttl') |
34 | + serial = next_zone_serial() |
35 | zones = ZoneGenerator( |
36 | domains, subnets, default_ttl, |
37 | - serial_generator=next_zone_serial).as_list() |
38 | + serial).as_list() |
39 | bind_write_zones(zones) |
40 | |
41 | # We should not be calling bind_write_options() here; call-sites should be |
42 | |
43 | === modified file 'src/maasserver/dns/tests/test_config.py' |
44 | --- src/maasserver/dns/tests/test_config.py 2016-03-02 16:01:57 +0000 |
45 | +++ src/maasserver/dns/tests/test_config.py 2016-03-08 17:17:00 +0000 |
46 | @@ -7,10 +7,10 @@ |
47 | |
48 | import random |
49 | import time |
50 | -from unittest import SkipTest |
51 | |
52 | from django.conf import settings |
53 | from django.core.management import call_command |
54 | +import dns.resolver |
55 | from maasserver import locks |
56 | from maasserver.config import RegionConfiguration |
57 | from maasserver.dns import config as dns_config_module |
58 | @@ -72,6 +72,7 @@ |
59 | BINDServer, |
60 | ) |
61 | from provisioningserver.testing.tests.test_bindfixture import dig_call |
62 | +from provisioningserver.utils.twisted import retries |
63 | from testtools.matchers import ( |
64 | Contains, |
65 | FileContains, |
66 | @@ -84,12 +85,6 @@ |
67 | from twisted.internet.defer import Deferred |
68 | |
69 | |
70 | -def setUp(): |
71 | - raise SkipTest( |
72 | - "XXX: GavinPanella 2016-03-02 bug=1550540: DNS test suite needs " |
73 | - "to do better lockstep on checking that DNS updates happened.") |
74 | - |
75 | - |
76 | def get_expected_names(node, ip): |
77 | expected_names = [ |
78 | "%s.%s." % (iface.name, node.fqdn) |
79 | @@ -489,6 +484,10 @@ |
80 | self.patch(dns_config_module, "DNS_DEFER_UPDATES", False) |
81 | # Create a DNS server. |
82 | self.bind = self.useFixture(BINDServer()) |
83 | + # Use the dnspython resolver for at least some queries. |
84 | + self.resolver = dns.resolver.Resolver() |
85 | + self.resolver.nameservers = ['127.0.0.1'] |
86 | + self.resolver.port = self.bind.config.port |
87 | patch_dns_config_path(self, self.bind.config.homedir) |
88 | # Use a random port for rndc. |
89 | patch_dns_rndc_port(self, allocate_ports("localhost")[0]) |
90 | @@ -522,11 +521,66 @@ |
91 | dns_update_zones_now([domain], [subnet]) |
92 | return node, static_ip |
93 | |
94 | - def dig_resolve(self, fqdn, version=4): |
95 | + def dns_wait_soa(self, fqdn, removing=False): |
96 | + # Get the serial number for the zone containing the FQDN by asking DNS |
97 | + # nicely for the SOA for the FQDN. If it's top-of-zone, we get an |
98 | + # answer, if it's not, we get the SOA in authority. |
99 | + |
100 | + if not fqdn.endswith('.'): |
101 | + fqdn = fqdn + '.' |
102 | + |
103 | + for elapsed, remaining, wait in retries(15, 0.02): |
104 | + query_name = fqdn |
105 | + |
106 | + # Loop until we have a value for serial, be that numeric or None. |
107 | + serial = undefined = object() |
108 | + while serial is undefined: |
109 | + try: |
110 | + ans = self.resolver.query( |
111 | + query_name, 'SOA', raise_on_no_answer=False) |
112 | + except dns.resolver.NXDOMAIN: |
113 | + if removing: |
114 | + # The zone has gone; we're done. |
115 | + return |
116 | + elif "." in query_name: |
117 | + # Query the parent domain for the SOA record. |
118 | + # For most things, this will be the correct DNS zone. |
119 | + # In the case of SRV records, we'll actually need to |
120 | + # strip more, hence the loop. |
121 | + query_name = query_name.split('.', 1)[1] |
122 | + else: |
123 | + # We've hit the root zone; no SOA found. |
124 | + serial = None |
125 | + except dns.resolver.NoNameservers: |
126 | + # No DNS service as yet. |
127 | + serial = None |
128 | + else: |
129 | + # If we got here, then we either have (1) a situation where |
130 | + # the LHS exists in the DNS, but no SOA RR exists for that |
131 | + # LHS (because it's a node with an A or AAAA RR, and not |
132 | + # the domain...) or (2) an answer to our SOA query. |
133 | + # Either way, we get exactly one SOA in the reply: in the |
134 | + # first case, it's in the Authority section, in the second, |
135 | + # it's in the Answer section. |
136 | + if ans.rrset is None: |
137 | + serial = ans.response.authority[0].items[0].serial |
138 | + else: |
139 | + serial = ans.rrset.items[0].serial |
140 | + |
141 | + if serial == zone_serial.current(): |
142 | + # The zone is up-to-date; we're done. |
143 | + return |
144 | + else: |
145 | + time.sleep(wait) |
146 | + |
147 | + self.fail("Timed-out waiting for %s to update." % fqdn) |
148 | + |
149 | + def dig_resolve(self, fqdn, version=4, removing=False): |
150 | """Resolve `fqdn` using dig. Returns a list of results.""" |
151 | # Using version=6 has two effects: |
152 | # - it changes the type of query from 'A' to 'AAAA'; |
153 | # - it forces dig to only use IPv6 query transport. |
154 | + self.dns_wait_soa(fqdn, removing) |
155 | record_type = 'AAAA' if version == 6 else 'A' |
156 | commands = [fqdn, '+short', '-%i' % version, record_type] |
157 | output = dig_call( |
158 | @@ -534,8 +588,9 @@ |
159 | commands=commands) |
160 | return output.split('\n') |
161 | |
162 | - def dig_reverse_resolve(self, ip, version=4): |
163 | + def dig_reverse_resolve(self, ip, version=4, removing=False): |
164 | """Reverse resolve `ip` using dig. Returns a list of results.""" |
165 | + self.dns_wait_soa(IPAddress(ip).reverse_dns, removing) |
166 | output = dig_call( |
167 | port=self.bind.config.port, |
168 | commands=['-x', ip, '+short', '-%i' % version]) |
169 | @@ -546,9 +601,6 @@ |
170 | if version == -1: |
171 | version = IPAddress(ip).version |
172 | fqdn = "%s.%s" % (hostname, domain) |
173 | - start = time.time() |
174 | - now = time.time() |
175 | - forward_lookup_result = [''] |
176 | # Give BIND enough time to process the rndc request. |
177 | # XXX 2016-03-01 lamont bug=1550540 We should really query DNS for the |
178 | # SOA that we (can) know to be the correct one, and wait for that |
179 | @@ -556,21 +608,14 @@ |
180 | # all of our tests go from having no answer for forward and/or reverse, |
181 | # to having the expected answer, and just wait for a non-empty return, |
182 | # or timeout (15 seconds because of slow jenkins sometimes.) |
183 | - while forward_lookup_result == [''] and now - start < 15: |
184 | - forward_lookup_result = self.dig_resolve(fqdn, version=version) |
185 | - now = time.time() |
186 | - time.sleep(0.05) |
187 | + forward_lookup_result = self.dig_resolve(fqdn, version=version) |
188 | self.assertThat( |
189 | forward_lookup_result, Contains(ip), |
190 | "Failed to resolve '%s' (results: '%s')." % ( |
191 | fqdn, ','.join(forward_lookup_result))) |
192 | # A reverse lookup on the IP address returns the hostname. |
193 | - reverse_lookup_result = [''] |
194 | - while reverse_lookup_result == [''] and now - start < 15: |
195 | - reverse_lookup_result = self.dig_reverse_resolve( |
196 | - ip, version=version) |
197 | - now = time.time() |
198 | - time.sleep(0.05) |
199 | + reverse_lookup_result = self.dig_reverse_resolve( |
200 | + ip, version=version) |
201 | self.assertThat( |
202 | reverse_lookup_result, Contains("%s." % fqdn), |
203 | "Failed to reverse resolve '%s' missing '%s' (results: '%s')." % ( |
204 | @@ -701,16 +746,15 @@ |
205 | FileContains(matcher=Contains(trusted_network))) |
206 | |
207 | def test_dns_config_has_NS_record(self): |
208 | + self.patch(settings, 'DNS_CONNECT', True) |
209 | ip = factory.make_ipv4_address() |
210 | with RegionConfiguration.open_for_update() as config: |
211 | config.maas_url = 'http://%s/' % ip |
212 | domain = factory.make_Domain() |
213 | node, static = self.create_node_with_static_ip(domain=domain) |
214 | - self.patch(settings, 'DNS_CONNECT', True) |
215 | - dns_update_all_zones_now() |
216 | - # Sleep half a second to make sure bind is fully-ready. This is not the |
217 | - # best, but it does prevent this tests from failing randomly. |
218 | - time.sleep(0.5) |
219 | + # Creating the domain triggered writing the zone file and updating the |
220 | + # DNS. |
221 | + self.dns_wait_soa(domain.name) |
222 | # Get the NS record for the zone 'domain.name'. |
223 | ns_record = dig_call( |
224 | port=self.bind.config.port, |
225 | @@ -718,6 +762,7 @@ |
226 | self.assertGreater( |
227 | len(ns_record), 0, "No NS record for domain.name.") |
228 | # Resolve that hostname. |
229 | + self.dns_wait_soa(ns_record) |
230 | ip_of_ns_record = dig_call( |
231 | port=self.bind.config.port, commands=[ns_record, '+short']) |
232 | self.assertEqual(ip, ip_of_ns_record) |
233 | @@ -736,7 +781,8 @@ |
234 | subnet.dns_servers = [] |
235 | subnet.save() |
236 | # The IP from the old network does not resolve anymore. |
237 | - self.assertEqual([''], self.dig_reverse_resolve(lease.ip)) |
238 | + self.assertEqual([''], self.dig_reverse_resolve( |
239 | + lease.ip, removing=True)) |
240 | # A lease in the new network resolves. |
241 | node, lease = self.create_node_with_static_ip(subnet=subnet) |
242 | self.assertTrue( |
243 | @@ -752,7 +798,7 @@ |
244 | ip = factory.pick_ip_in_network(network) |
245 | domain = factory.make_Domain() |
246 | domain.delete() |
247 | - self.assertEqual([''], self.dig_reverse_resolve(ip)) |
248 | + self.assertEqual([''], self.dig_reverse_resolve(ip, removing=True)) |
249 | |
250 | def test_add_node_updates_zone(self): |
251 | self.patch(settings, "DNS_CONNECT", True) |
252 | @@ -764,7 +810,7 @@ |
253 | node, static = self.create_node_with_static_ip() |
254 | node.delete() |
255 | fqdn = "%s.%s" % (node.hostname, node.domain.name) |
256 | - self.assertEqual([''], self.dig_resolve(fqdn)) |
257 | + self.assertEqual([''], self.dig_resolve(fqdn, removing=True)) |
258 | |
259 | def test_change_node_hostname_updates_zone(self): |
260 | self.patch(settings, "DNS_CONNECT", True) |
261 | |
262 | === modified file 'src/maasserver/dns/tests/test_zonegenerator.py' |
263 | --- src/maasserver/dns/tests/test_zonegenerator.py 2016-02-18 17:31:45 +0000 |
264 | +++ src/maasserver/dns/tests/test_zonegenerator.py 2016-03-08 17:17:00 +0000 |
265 | @@ -284,10 +284,10 @@ |
266 | def test_empty_yields_nothing(self): |
267 | self.assertEqual( |
268 | [], |
269 | - ZoneGenerator((), (), serial_generator=Mock()).as_list()) |
270 | + ZoneGenerator((), (), serial=random.randint(0, 65535)).as_list()) |
271 | |
272 | def test_defaults_ttl(self): |
273 | - zonegen = ZoneGenerator((), (), serial_generator=Mock()) |
274 | + zonegen = ZoneGenerator((), (), serial=random.randint(0, 65535)) |
275 | self.assertEqual( |
276 | Config.objects.get_config('default_dns_ttl'), |
277 | zonegen.default_ttl) |
278 | @@ -296,7 +296,7 @@ |
279 | def test_accepts_default_ttl(self): |
280 | default_ttl = random.randint(10, 1000) |
281 | zonegen = ZoneGenerator( |
282 | - (), (), default_ttl=default_ttl, serial_generator=Mock()) |
283 | + (), (), default_ttl=default_ttl, serial=random.randint(0, 65535)) |
284 | self.assertEqual(default_ttl, zonegen.default_ttl) |
285 | |
286 | def test_yields_forward_and_reverse_zone(self): |
287 | @@ -304,7 +304,7 @@ |
288 | domain = factory.make_Domain(name='henry') |
289 | subnet = factory.make_Subnet(cidr=str(IPNetwork("10/29").cidr)) |
290 | zones = ZoneGenerator( |
291 | - domain, subnet, serial_generator=Mock()).as_list() |
292 | + domain, subnet, serial=random.randint(0, 65535)).as_list() |
293 | self.assertThat( |
294 | zones, MatchesSetwise( |
295 | forward_zone("henry"), |
296 | @@ -318,7 +318,7 @@ |
297 | factory.make_Node_with_Interface_on_Subnet( |
298 | subnet=subnet, vlan=subnet.vlan, fabric=subnet.vlan.fabric) |
299 | zones = ZoneGenerator( |
300 | - domain, subnet, serial_generator=Mock()).as_list() |
301 | + domain, subnet, serial=random.randint(0, 65535)).as_list() |
302 | self.assertThat( |
303 | zones, MatchesSetwise( |
304 | forward_zone("henry"), |
305 | @@ -347,7 +347,7 @@ |
306 | Config.objects.set_config('default_dns_ttl', default_ttl) |
307 | zones = ZoneGenerator( |
308 | domain, subnet, default_ttl=default_ttl, |
309 | - serial_generator=Mock()).as_list() |
310 | + serial=random.randint(0, 65535)).as_list() |
311 | self.assertThat( |
312 | zones, MatchesSetwise( |
313 | forward_zone("henry"), |
314 | @@ -404,7 +404,7 @@ |
315 | Config.objects.set_config('default_dns_ttl', default_ttl) |
316 | zones = ZoneGenerator( |
317 | domain, [subnet1, subnet2], default_ttl=default_ttl, |
318 | - serial_generator=Mock()).as_list() |
319 | + serial=random.randint(0, 65535)).as_list() |
320 | self.assertThat( |
321 | zones, MatchesSetwise( |
322 | forward_zone(domain.name), |
323 | @@ -436,7 +436,8 @@ |
324 | ] |
325 | self.assertThat( |
326 | ZoneGenerator( |
327 | - domain, [subnet1, subnet2], serial_generator=Mock()).as_list(), |
328 | + domain, [subnet1, subnet2], |
329 | + serial=random.randint(0, 65535)).as_list(), |
330 | MatchesSetwise(*expected_zones)) |
331 | |
332 | def test_with_many_yields_many_zones(self): |
333 | @@ -459,7 +460,7 @@ |
334 | expected_zones.add( |
335 | reverse_zone(default_domain.name, rfc2317_net.cidr)) |
336 | actual_zones = ZoneGenerator( |
337 | - domains, subnets, serial_generator=Mock()).as_list() |
338 | + domains, subnets, serial=random.randint(0, 65535)).as_list() |
339 | self.assertThat(actual_zones, MatchesSetwise(*expected_zones)) |
340 | |
341 | def test_zone_generator_handles_rdns_mode_equal_enabled(self): |
342 | @@ -475,7 +476,8 @@ |
343 | reverse_zone(default_domain.name, "10/29"), |
344 | ) |
345 | self.assertThat( |
346 | - ZoneGenerator(domains, subnets, serial_generator=Mock()).as_list(), |
347 | + ZoneGenerator( |
348 | + domains, subnets, serial=random.randint(0, 65535)).as_list(), |
349 | MatchesSetwise(*expected_zones)) |
350 | |
351 | |
352 | @@ -502,7 +504,7 @@ |
353 | node.system_id, domain.ttl, {boot_ip.ip}, node.node_type)} |
354 | zones = ZoneGenerator( |
355 | domain, subnet, default_ttl=global_ttl, |
356 | - serial_generator=Mock()).as_list() |
357 | + serial=random.randint(0, 65535)).as_list() |
358 | self.assertItemsEqual( |
359 | expected_forward.items(), zones[0]._mapping.items()) |
360 | self.assertItemsEqual( |
361 | @@ -532,7 +534,7 @@ |
362 | node.node_type)} |
363 | zones = ZoneGenerator( |
364 | domain, subnet, default_ttl=global_ttl, |
365 | - serial_generator=Mock()).as_list() |
366 | + serial=random.randint(0, 65535)).as_list() |
367 | self.assertItemsEqual( |
368 | expected_forward.items(), zones[0]._mapping.items()) |
369 | self.assertItemsEqual( |
370 | @@ -568,7 +570,7 @@ |
371 | node.node_type)} |
372 | zones = ZoneGenerator( |
373 | domain, subnet, default_ttl=global_ttl, |
374 | - serial_generator=Mock()).as_list() |
375 | + serial=random.randint(0, 65535)).as_list() |
376 | self.assertItemsEqual( |
377 | expected_forward.items(), zones[0]._mapping.items()) |
378 | self.assertItemsEqual( |
379 | @@ -608,7 +610,7 @@ |
380 | node.node_type)} |
381 | zones = ZoneGenerator( |
382 | domain, subnet, default_ttl=global_ttl, |
383 | - serial_generator=Mock()).as_list() |
384 | + serial=random.randint(0, 65535)).as_list() |
385 | self.assertItemsEqual( |
386 | expected_forward.items(), zones[0]._mapping.items()) |
387 | self.assertItemsEqual( |
388 | @@ -629,7 +631,7 @@ |
389 | None, {(global_ttl, dnsdata.rrtype, dnsdata.rrdata)})} |
390 | zones = ZoneGenerator( |
391 | domain, subnet, default_ttl=global_ttl, |
392 | - serial_generator=Mock()).as_list() |
393 | + serial=random.randint(0, 65535)).as_list() |
394 | self.assertItemsEqual( |
395 | expected_forward.items(), zones[0]._other_mapping.items()) |
396 | self.assertItemsEqual([], zones[0]._mapping.items()) |
397 | @@ -651,7 +653,7 @@ |
398 | None, {(domain.ttl, dnsdata.rrtype, dnsdata.rrdata)})} |
399 | zones = ZoneGenerator( |
400 | domain, subnet, default_ttl=global_ttl, |
401 | - serial_generator=Mock()).as_list() |
402 | + serial=random.randint(0, 65535)).as_list() |
403 | self.assertItemsEqual( |
404 | expected_forward.items(), zones[0]._other_mapping.items()) |
405 | self.assertItemsEqual([], zones[0]._mapping.items()) |
406 | @@ -673,7 +675,7 @@ |
407 | None, {(dnsdata.ttl, dnsdata.rrtype, dnsdata.rrdata)})} |
408 | zones = ZoneGenerator( |
409 | domain, subnet, default_ttl=global_ttl, |
410 | - serial_generator=Mock()).as_list() |
411 | + serial=random.randint(0, 65535)).as_list() |
412 | self.assertItemsEqual( |
413 | expected_forward.items(), zones[0]._other_mapping.items()) |
414 | self.assertItemsEqual([], zones[0]._mapping.items()) |
415 | |
416 | === modified file 'src/maasserver/dns/zonegenerator.py' |
417 | --- src/maasserver/dns/zonegenerator.py 2016-03-07 15:15:36 +0000 |
418 | +++ src/maasserver/dns/zonegenerator.py 2016-03-08 17:17:00 +0000 |
419 | @@ -152,12 +152,9 @@ |
420 | We generate zones for the domains (forward), and subnets (reverse) passed. |
421 | """ |
422 | |
423 | - def __init__(self, domains, subnets, default_ttl=None, |
424 | - serial=None, serial_generator=None): |
425 | + def __init__(self, domains, subnets, default_ttl=None, serial=None): |
426 | """ |
427 | :param serial: A serial number to reuse when creating zones in bulk. |
428 | - :param serial_generator: As an alternative to `serial`, a callback |
429 | - that returns a fresh serial number on every call. |
430 | """ |
431 | self.domains = sequence(domains) |
432 | self.subnets = sequence(subnets) |
433 | @@ -166,7 +163,6 @@ |
434 | else: |
435 | self.default_ttl = default_ttl |
436 | self.serial = serial |
437 | - self.serial_generator = serial_generator |
438 | |
439 | @staticmethod |
440 | def _get_mappings(): |
441 | @@ -356,12 +352,11 @@ |
442 | """ |
443 | # For testing and such it's fine if we don't have a serial, but once |
444 | # we get to this point, we really need one. |
445 | - assert not (self.serial is None and self.serial_generator is None), ( |
446 | - "No serial number or serial number generator specified.") |
447 | + assert not (self.serial is None), ("No serial number specified.") |
448 | |
449 | mappings = self._get_mappings() |
450 | rrset_mappings = self._get_rrset_mappings() |
451 | - serial = self.serial or self.serial_generator() |
452 | + serial = self.serial |
453 | default_ttl = self.default_ttl |
454 | return chain( |
455 | self._gen_forward_zones( |
456 | |
457 | === modified file 'src/maasserver/sequence.py' |
458 | --- src/maasserver/sequence.py 2016-01-25 11:43:50 +0000 |
459 | +++ src/maasserver/sequence.py 2016-03-08 17:17:00 +0000 |
460 | @@ -20,6 +20,7 @@ |
461 | from provisioningserver.utils import typed |
462 | from psycopg2.errorcodes import ( |
463 | DUPLICATE_TABLE, |
464 | + OBJECT_NOT_IN_PREREQUISITE_STATE, |
465 | UNDEFINED_TABLE, |
466 | ) |
467 | |
468 | @@ -153,6 +154,29 @@ |
469 | else: |
470 | raise |
471 | |
472 | + def current(self): |
473 | + """Return the current value of this sequence, or `None`. |
474 | + |
475 | + :return: The sequence value, or None if there is no current value for |
476 | + the sequence in the database session or if the sequence does not |
477 | + exist. |
478 | + :rtype: int / None |
479 | + """ |
480 | + try: |
481 | + with transaction.atomic(): |
482 | + with connection.cursor() as cursor: |
483 | + cursor.execute("SELECT currval(%s)", [self.name]) |
484 | + return cursor.fetchone()[0] |
485 | + except (utils.OperationalError, utils.ProgrammingError) as error: |
486 | + if is_postgres_error(error, OBJECT_NOT_IN_PREREQUISITE_STATE): |
487 | + # There is no current value for the sequence. |
488 | + return None |
489 | + elif is_postgres_error(error, UNDEFINED_TABLE): |
490 | + # The sequence does not exist, hence has no current value. |
491 | + return None |
492 | + else: |
493 | + raise |
494 | + |
495 | |
496 | def is_postgres_error(error, *pgcodes): |
497 | # Unwrap Django's lowest-common-denominator exception. |
498 | |
499 | === modified file 'src/maasserver/tests/test_sequence.py' |
500 | --- src/maasserver/tests/test_sequence.py 2016-01-25 11:43:50 +0000 |
501 | +++ src/maasserver/tests/test_sequence.py 2016-03-08 17:17:00 +0000 |
502 | @@ -43,7 +43,7 @@ |
503 | seq.create() |
504 | self.assertRaisesRegex(ProgrammingError, "already exists", seq.create) |
505 | |
506 | - def test_create_if_not_exists_does_not_fails_if_sequence_exists(self): |
507 | + def test_create_if_not_exists_does_not_fail_if_sequence_exists(self): |
508 | name = factory.make_name('seq', sep='') |
509 | seq = Sequence(name) |
510 | seq.create() |
511 | @@ -151,3 +151,33 @@ |
512 | seq.create() |
513 | self.assertSequenceEqual( |
514 | list(range(1, 11)), list(islice(seq, 10))) |
515 | + |
516 | + def test_current_returns_none_when_table_does_not_exist(self): |
517 | + name = factory.make_name('seq', sep='') |
518 | + seq = Sequence(name) |
519 | + self.assertEqual(None, seq.current()) |
520 | + |
521 | + def test_current_returns_none_when_no_current_value(self): |
522 | + name = factory.make_name('seq', sep='') |
523 | + seq = Sequence(name) |
524 | + seq.create() |
525 | + self.assertEqual(None, seq.current()) |
526 | + |
527 | + def test_current_returns_current_value(self): |
528 | + name = factory.make_name('seq', sep='') |
529 | + seq = Sequence(name) |
530 | + seq.create() |
531 | + expected = next(seq) |
532 | + self.assertEqual(expected, seq.current()) |
533 | + self.assertEqual(expected, seq.current()) |
534 | + |
535 | + def test_current_returns_correct_value(self): |
536 | + name = factory.make_name('seq', sep='') |
537 | + seq = Sequence(name) |
538 | + seq.create() |
539 | + expected = next(seq) |
540 | + self.assertEqual(expected, seq.current()) |
541 | + self.assertEqual(expected, seq.current()) |
542 | + expected = next(seq) |
543 | + self.assertEqual(expected, seq.current()) |
544 | + self.assertEqual(expected, seq.current()) |
545 | |
546 | === modified file 'src/provisioningserver/testing/bindfixture.py' |
547 | --- src/provisioningserver/testing/bindfixture.py 2015-12-01 18:12:59 +0000 |
548 | +++ src/provisioningserver/testing/bindfixture.py 2016-03-08 17:17:00 +0000 |
549 | @@ -119,8 +119,9 @@ |
550 | logging{ |
551 | channel simple_log { |
552 | file "{{log_file}}"; |
553 | - severity info; |
554 | + severity debug; |
555 | print-severity yes; |
556 | + print-time yes; |
557 | }; |
558 | category default{ |
559 | simple_log; |
560 | |
561 | === modified file 'src/provisioningserver/utils/fs.py' |
562 | --- src/provisioningserver/utils/fs.py 2016-03-08 11:44:02 +0000 |
563 | +++ src/provisioningserver/utils/fs.py 2016-03-08 17:17:00 +0000 |
564 | @@ -42,10 +42,7 @@ |
565 | ) |
566 | import tempfile |
567 | import threading |
568 | -from time import ( |
569 | - sleep, |
570 | - time, |
571 | -) |
572 | +from time import sleep |
573 | |
574 | from provisioningserver.utils import sudo |
575 | from provisioningserver.utils.shell import ExternalProcessError |
576 | @@ -201,61 +198,27 @@ |
577 | raise |
578 | |
579 | |
580 | -def pick_new_mtime(old_mtime=None): |
581 | - """Choose a new modification time for a file that needs it updated. |
582 | - |
583 | - This function is used to manage the modification time of files |
584 | - for which we need to see an increment in the modification time |
585 | - each time the file is modified. This is the case for DNS zone |
586 | - files which only get properly reloaded if BIND sees that the |
587 | - modification time is > to the time it has in its database. |
588 | - |
589 | - Modification time can have a resolution as low as one second in |
590 | - some relevant environments (we have observed this with ext3). |
591 | - To produce mtime changes regardless, we set a file's modification |
592 | - time in the past when it is first written, and |
593 | - increment it by 1 second on each subsequent write. |
594 | - |
595 | - :param old_mtime: File's previous modification time, as a number |
596 | - with a unity of one second, or None if it did not previously |
597 | - exist. |
598 | - """ |
599 | - now = time() |
600 | - if old_mtime is None or old_mtime + 1 < now: |
601 | - # File is new. Set modification time in the past to have room for |
602 | - # sub-second modifications. |
603 | - return now |
604 | - else: |
605 | - # Let the file timestamp march into the future, so that BIND will |
606 | - # reload the zone. Our future selves will notice that mtime is enough |
607 | - # before the then-now to go back to having it match us. |
608 | - return old_mtime + 1 |
609 | - |
610 | - |
611 | def incremental_write(content, filename, mode=0o600): |
612 | - """Write the given `content` into the file `filename` and |
613 | - increment the modification time by 1 sec. |
614 | + """Write the given `content` into the file `filename`. In the past, this |
615 | + would potentially change modification time to arbitrary values. |
616 | |
617 | :type content: `bytes` |
618 | :param mode: Access permissions for the file. |
619 | """ |
620 | - old_mtime = get_mtime(filename) |
621 | + # We used to change modification time on the files, in an attempt to out |
622 | + # smart BIND into loading zones on file systems where time was not granular |
623 | + # enough. BIND got smarter about how it loads zones and remembers when it |
624 | + # last loaded the zone: either the then-current time, or the mtime of the |
625 | + # file, whichever is later. When BIND then gets a reload request, it |
626 | + # compares the current time to the loadtime for the domain, and skips it if |
627 | + # the file is not new. If we set mtime in the past, then zones don't load. |
628 | + # If we set it in the future, then we WILL sometimes hit the race condition |
629 | + # where BIND looks at the time after we atomic_write, but before we manage |
630 | + # to set the time into the future. N.B.: /etc on filesystems with 1-second |
631 | + # granularity are no longer supported by MAAS. The good news is that since |
632 | + # 2.6, linux has supported nanosecond-granular time. As of bind9 |
633 | + # 1:9.10.3.dfsg.P2-5, BIND even uses it. |
634 | atomic_write(content, filename, mode=mode) |
635 | - new_mtime = pick_new_mtime(old_mtime) |
636 | - os.utime(filename, (new_mtime, new_mtime)) |
637 | - |
638 | - |
639 | -def get_mtime(filename): |
640 | - """Return a file's modification time, or None if it does not exist.""" |
641 | - try: |
642 | - return os.stat(filename).st_mtime |
643 | - except OSError as e: |
644 | - if e.errno == errno.ENOENT: |
645 | - # File does not exist. Be helpful, return None. |
646 | - return None |
647 | - else: |
648 | - # Other failure. The caller will want to know. |
649 | - raise |
650 | |
651 | |
652 | def sudo_write_file(filename, contents, mode=0o644): |
653 | |
654 | === modified file 'src/provisioningserver/utils/tests/test_fs.py' |
655 | --- src/provisioningserver/utils/tests/test_fs.py 2016-03-07 14:35:29 +0000 |
656 | +++ src/provisioningserver/utils/tests/test_fs.py 2016-03-08 17:17:00 +0000 |
657 | @@ -8,7 +8,6 @@ |
658 | from base64 import urlsafe_b64encode |
659 | import os |
660 | import os.path |
661 | -from random import randint |
662 | import re |
663 | from shutil import rmtree |
664 | import stat |
665 | @@ -21,7 +20,6 @@ |
666 | |
667 | from maastesting import root |
668 | from maastesting.factory import factory |
669 | -from maastesting.fakemethod import FakeMethod |
670 | from maastesting.matchers import ( |
671 | FileContains, |
672 | MockCalledOnceWith, |
673 | @@ -43,9 +41,7 @@ |
674 | ensure_dir, |
675 | FileLock, |
676 | get_maas_provision_command, |
677 | - get_mtime, |
678 | incremental_write, |
679 | - pick_new_mtime, |
680 | read_text_file, |
681 | RunLock, |
682 | sudo_delete_file, |
683 | @@ -250,7 +246,7 @@ |
684 | class TestIncrementalWrite(MAASTestCase): |
685 | """Test `incremental_write`.""" |
686 | |
687 | - def test_incremental_write_increments_modification_time(self): |
688 | + def test_incremental_write_updates_modification_time(self): |
689 | content = factory.make_bytes() |
690 | filename = self.make_file(contents=factory.make_string()) |
691 | # Pretend that this file is older than it is. So that |
692 | @@ -259,8 +255,21 @@ |
693 | os.utime(filename, (old_mtime, old_mtime)) |
694 | incremental_write(content, filename) |
695 | new_time = time.time() |
696 | - self.assertAlmostEqual( |
697 | - os.stat(filename).st_mtime, new_time, delta=0.01) |
698 | + # should be much closer to new_time than to old_mtime. |
699 | + self.assertAlmostEqual( |
700 | + os.stat(filename).st_mtime, new_time, delta=2.0) |
701 | + |
702 | + def test_incremental_write_does_not_set_future_time(self): |
703 | + content = factory.make_bytes() |
704 | + filename = self.make_file(contents=factory.make_string()) |
705 | + # Pretend that this file is older than it is. So that |
706 | + # incrementing its mtime won't put it in the future. |
707 | + old_mtime = os.stat(filename).st_mtime + 10 |
708 | + os.utime(filename, (old_mtime, old_mtime)) |
709 | + incremental_write(content, filename) |
710 | + new_time = time.time() |
711 | + self.assertAlmostEqual( |
712 | + os.stat(filename).st_mtime, new_time, delta=2.0) |
713 | |
714 | def test_incremental_write_sets_permissions(self): |
715 | atomic_file = self.make_file() |
716 | @@ -269,45 +278,6 @@ |
717 | self.assertEqual(mode, stat.S_IMODE(os.stat(atomic_file).st_mode)) |
718 | |
719 | |
720 | -class TestGetMTime(MAASTestCase): |
721 | - """Test `get_mtime`.""" |
722 | - |
723 | - def test_get_mtime_returns_None_for_nonexistent_file(self): |
724 | - nonexistent_file = os.path.join( |
725 | - self.make_dir(), factory.make_name('nonexistent-file')) |
726 | - self.assertIsNone(get_mtime(nonexistent_file)) |
727 | - |
728 | - def test_get_mtime_returns_mtime(self): |
729 | - existing_file = self.make_file() |
730 | - mtime = os.stat(existing_file).st_mtime - randint(0, 100) |
731 | - os.utime(existing_file, (mtime, mtime)) |
732 | - # Some small rounding/representation errors can happen here. |
733 | - # That's just the way of floating-point numbers. According to |
734 | - # Gavin there's a conversion to fixed-point along the way, which |
735 | - # would raise representability issues. |
736 | - self.assertAlmostEqual(mtime, get_mtime(existing_file), delta=0.00001) |
737 | - |
738 | - def test_get_mtime_passes_on_other_error(self): |
739 | - forbidden_file = self.make_file() |
740 | - self.patch(os, 'stat', FakeMethod(failure=OSError("Forbidden file"))) |
741 | - self.assertRaises(OSError, get_mtime, forbidden_file) |
742 | - |
743 | - |
744 | -class TestPickNewMTime(MAASTestCase): |
745 | - """Test `pick_new_mtime`.""" |
746 | - |
747 | - def test_pick_new_mtime_increments_mtime_if_possible(self): |
748 | - now = time.time() |
749 | - new_time = pick_new_mtime(now - 2) |
750 | - self.assertAlmostEqual(new_time, now, delta=0.0001) |
751 | - |
752 | - def test_pick_new_mtime_happily_moves_mtime_into_the_future(self): |
753 | - # Race condition: this will fail if the test gets held up for |
754 | - # a second between readings of the clock. |
755 | - now = time.time() |
756 | - self.assertEqual(now + 1, pick_new_mtime(now)) |
757 | - |
758 | - |
759 | class TestGetMAASProvisionCommand(MAASTestCase): |
760 | |
761 | def test__returns_just_command_for_production(self): |
762 | |
763 | === modified file 'utilities/check-imports' |
764 | --- utilities/check-imports 2016-02-29 14:42:45 +0000 |
765 | +++ utilities/check-imports 2016-03-08 17:17:00 +0000 |
766 | @@ -148,6 +148,7 @@ |
767 | map("{0}|{0}.**".format, python_standard_libs)) |
768 | TestingLibraries = Pattern( |
769 | "django_nose|django_nose.**", |
770 | + "dns|dns.**", |
771 | "fixtures|fixtures.**", |
772 | "hypothesis|hypothesis.**", |
773 | "maastesting|maastesting.**", |
Tested this on the lander; noted some failures while running bin/test.region maasserver. dns.tests -
https:/ /paste. ubuntu. com/15270806/