Merge lp:~lamont/maas/dns-testfailures-check-soa 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: 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
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.

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

Tested this on the lander; noted some failures while running bin/test.region maasserver.dns.tests -

https://paste.ubuntu.com/15270806/

Revision history for this message
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.

Revision history for this message
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.

review: Needs Fixing
Revision history for this message
Gavin Panella (allenap) wrote :

Tip top.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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.**",