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
=== modified file 'required-packages/dev'
--- required-packages/dev 2016-02-17 16:12:30 +0000
+++ required-packages/dev 2016-03-08 17:17:00 +0000
@@ -15,5 +15,6 @@
15npm15npm
16python-pocket-lint16python-pocket-lint
17python3-django-nose17python3-django-nose
18python3-dnspython
18socat19socat
19xvfb20xvfb
2021
=== modified file 'src/maasserver/dns/config.py'
--- src/maasserver/dns/config.py 2016-02-11 23:37:45 +0000
+++ src/maasserver/dns/config.py 2016-03-08 17:17:00 +0000
@@ -89,12 +89,12 @@
89 return89 return
9090
91 default_ttl = Config.objects.get_config('default_dns_ttl')91 default_ttl = Config.objects.get_config('default_dns_ttl')
92 serial = next_zone_serial()
92 zones_to_write = ZoneGenerator(93 zones_to_write = ZoneGenerator(
93 domains, subnets, default_ttl,94 domains, subnets, default_ttl,
94 serial_generator=next_zone_serial).as_list()95 serial).as_list()
95 if len(zones_to_write) == 0:96 if len(zones_to_write) == 0:
96 return None97 return None
97 serial = next_zone_serial()
98 # Compute non-None zones.98 # Compute non-None zones.
99 zones = ZoneGenerator(99 zones = ZoneGenerator(
100 Domain.objects.all(), Subnet.objects.all(),100 Domain.objects.all(), Subnet.objects.all(),
@@ -232,9 +232,10 @@
232 domains = Domain.objects.filter(authoritative=True)232 domains = Domain.objects.filter(authoritative=True)
233 subnets = Subnet.objects.exclude(rdns_mode=RDNS_MODE.DISABLED)233 subnets = Subnet.objects.exclude(rdns_mode=RDNS_MODE.DISABLED)
234 default_ttl = Config.objects.get_config('default_dns_ttl')234 default_ttl = Config.objects.get_config('default_dns_ttl')
235 serial = next_zone_serial()
235 zones = ZoneGenerator(236 zones = ZoneGenerator(
236 domains, subnets, default_ttl,237 domains, subnets, default_ttl,
237 serial_generator=next_zone_serial).as_list()238 serial).as_list()
238 bind_write_zones(zones)239 bind_write_zones(zones)
239240
240 # We should not be calling bind_write_options() here; call-sites should be241 # We should not be calling bind_write_options() here; call-sites should be
241242
=== modified file 'src/maasserver/dns/tests/test_config.py'
--- src/maasserver/dns/tests/test_config.py 2016-03-02 16:01:57 +0000
+++ src/maasserver/dns/tests/test_config.py 2016-03-08 17:17:00 +0000
@@ -7,10 +7,10 @@
77
8import random8import random
9import time9import time
10from unittest import SkipTest
1110
12from django.conf import settings11from django.conf import settings
13from django.core.management import call_command12from django.core.management import call_command
13import dns.resolver
14from maasserver import locks14from maasserver import locks
15from maasserver.config import RegionConfiguration15from maasserver.config import RegionConfiguration
16from maasserver.dns import config as dns_config_module16from maasserver.dns import config as dns_config_module
@@ -72,6 +72,7 @@
72 BINDServer,72 BINDServer,
73)73)
74from provisioningserver.testing.tests.test_bindfixture import dig_call74from provisioningserver.testing.tests.test_bindfixture import dig_call
75from provisioningserver.utils.twisted import retries
75from testtools.matchers import (76from testtools.matchers import (
76 Contains,77 Contains,
77 FileContains,78 FileContains,
@@ -84,12 +85,6 @@
84from twisted.internet.defer import Deferred85from twisted.internet.defer import Deferred
8586
8687
87def setUp():
88 raise SkipTest(
89 "XXX: GavinPanella 2016-03-02 bug=1550540: DNS test suite needs "
90 "to do better lockstep on checking that DNS updates happened.")
91
92
93def get_expected_names(node, ip):88def get_expected_names(node, ip):
94 expected_names = [89 expected_names = [
95 "%s.%s." % (iface.name, node.fqdn)90 "%s.%s." % (iface.name, node.fqdn)
@@ -489,6 +484,10 @@
489 self.patch(dns_config_module, "DNS_DEFER_UPDATES", False)484 self.patch(dns_config_module, "DNS_DEFER_UPDATES", False)
490 # Create a DNS server.485 # Create a DNS server.
491 self.bind = self.useFixture(BINDServer())486 self.bind = self.useFixture(BINDServer())
487 # Use the dnspython resolver for at least some queries.
488 self.resolver = dns.resolver.Resolver()
489 self.resolver.nameservers = ['127.0.0.1']
490 self.resolver.port = self.bind.config.port
492 patch_dns_config_path(self, self.bind.config.homedir)491 patch_dns_config_path(self, self.bind.config.homedir)
493 # Use a random port for rndc.492 # Use a random port for rndc.
494 patch_dns_rndc_port(self, allocate_ports("localhost")[0])493 patch_dns_rndc_port(self, allocate_ports("localhost")[0])
@@ -522,11 +521,66 @@
522 dns_update_zones_now([domain], [subnet])521 dns_update_zones_now([domain], [subnet])
523 return node, static_ip522 return node, static_ip
524523
525 def dig_resolve(self, fqdn, version=4):524 def dns_wait_soa(self, fqdn, removing=False):
525 # Get the serial number for the zone containing the FQDN by asking DNS
526 # nicely for the SOA for the FQDN. If it's top-of-zone, we get an
527 # answer, if it's not, we get the SOA in authority.
528
529 if not fqdn.endswith('.'):
530 fqdn = fqdn + '.'
531
532 for elapsed, remaining, wait in retries(15, 0.02):
533 query_name = fqdn
534
535 # Loop until we have a value for serial, be that numeric or None.
536 serial = undefined = object()
537 while serial is undefined:
538 try:
539 ans = self.resolver.query(
540 query_name, 'SOA', raise_on_no_answer=False)
541 except dns.resolver.NXDOMAIN:
542 if removing:
543 # The zone has gone; we're done.
544 return
545 elif "." in query_name:
546 # Query the parent domain for the SOA record.
547 # For most things, this will be the correct DNS zone.
548 # In the case of SRV records, we'll actually need to
549 # strip more, hence the loop.
550 query_name = query_name.split('.', 1)[1]
551 else:
552 # We've hit the root zone; no SOA found.
553 serial = None
554 except dns.resolver.NoNameservers:
555 # No DNS service as yet.
556 serial = None
557 else:
558 # If we got here, then we either have (1) a situation where
559 # the LHS exists in the DNS, but no SOA RR exists for that
560 # LHS (because it's a node with an A or AAAA RR, and not
561 # the domain...) or (2) an answer to our SOA query.
562 # Either way, we get exactly one SOA in the reply: in the
563 # first case, it's in the Authority section, in the second,
564 # it's in the Answer section.
565 if ans.rrset is None:
566 serial = ans.response.authority[0].items[0].serial
567 else:
568 serial = ans.rrset.items[0].serial
569
570 if serial == zone_serial.current():
571 # The zone is up-to-date; we're done.
572 return
573 else:
574 time.sleep(wait)
575
576 self.fail("Timed-out waiting for %s to update." % fqdn)
577
578 def dig_resolve(self, fqdn, version=4, removing=False):
526 """Resolve `fqdn` using dig. Returns a list of results."""579 """Resolve `fqdn` using dig. Returns a list of results."""
527 # Using version=6 has two effects:580 # Using version=6 has two effects:
528 # - it changes the type of query from 'A' to 'AAAA';581 # - it changes the type of query from 'A' to 'AAAA';
529 # - it forces dig to only use IPv6 query transport.582 # - it forces dig to only use IPv6 query transport.
583 self.dns_wait_soa(fqdn, removing)
530 record_type = 'AAAA' if version == 6 else 'A'584 record_type = 'AAAA' if version == 6 else 'A'
531 commands = [fqdn, '+short', '-%i' % version, record_type]585 commands = [fqdn, '+short', '-%i' % version, record_type]
532 output = dig_call(586 output = dig_call(
@@ -534,8 +588,9 @@
534 commands=commands)588 commands=commands)
535 return output.split('\n')589 return output.split('\n')
536590
537 def dig_reverse_resolve(self, ip, version=4):591 def dig_reverse_resolve(self, ip, version=4, removing=False):
538 """Reverse resolve `ip` using dig. Returns a list of results."""592 """Reverse resolve `ip` using dig. Returns a list of results."""
593 self.dns_wait_soa(IPAddress(ip).reverse_dns, removing)
539 output = dig_call(594 output = dig_call(
540 port=self.bind.config.port,595 port=self.bind.config.port,
541 commands=['-x', ip, '+short', '-%i' % version])596 commands=['-x', ip, '+short', '-%i' % version])
@@ -546,9 +601,6 @@
546 if version == -1:601 if version == -1:
547 version = IPAddress(ip).version602 version = IPAddress(ip).version
548 fqdn = "%s.%s" % (hostname, domain)603 fqdn = "%s.%s" % (hostname, domain)
549 start = time.time()
550 now = time.time()
551 forward_lookup_result = ['']
552 # Give BIND enough time to process the rndc request.604 # Give BIND enough time to process the rndc request.
553 # XXX 2016-03-01 lamont bug=1550540 We should really query DNS for the605 # XXX 2016-03-01 lamont bug=1550540 We should really query DNS for the
554 # SOA that we (can) know to be the correct one, and wait for that606 # SOA that we (can) know to be the correct one, and wait for that
@@ -556,21 +608,14 @@
556 # all of our tests go from having no answer for forward and/or reverse,608 # all of our tests go from having no answer for forward and/or reverse,
557 # to having the expected answer, and just wait for a non-empty return,609 # to having the expected answer, and just wait for a non-empty return,
558 # or timeout (15 seconds because of slow jenkins sometimes.)610 # or timeout (15 seconds because of slow jenkins sometimes.)
559 while forward_lookup_result == [''] and now - start < 15:611 forward_lookup_result = self.dig_resolve(fqdn, version=version)
560 forward_lookup_result = self.dig_resolve(fqdn, version=version)
561 now = time.time()
562 time.sleep(0.05)
563 self.assertThat(612 self.assertThat(
564 forward_lookup_result, Contains(ip),613 forward_lookup_result, Contains(ip),
565 "Failed to resolve '%s' (results: '%s')." % (614 "Failed to resolve '%s' (results: '%s')." % (
566 fqdn, ','.join(forward_lookup_result)))615 fqdn, ','.join(forward_lookup_result)))
567 # A reverse lookup on the IP address returns the hostname.616 # A reverse lookup on the IP address returns the hostname.
568 reverse_lookup_result = ['']617 reverse_lookup_result = self.dig_reverse_resolve(
569 while reverse_lookup_result == [''] and now - start < 15:618 ip, version=version)
570 reverse_lookup_result = self.dig_reverse_resolve(
571 ip, version=version)
572 now = time.time()
573 time.sleep(0.05)
574 self.assertThat(619 self.assertThat(
575 reverse_lookup_result, Contains("%s." % fqdn),620 reverse_lookup_result, Contains("%s." % fqdn),
576 "Failed to reverse resolve '%s' missing '%s' (results: '%s')." % (621 "Failed to reverse resolve '%s' missing '%s' (results: '%s')." % (
@@ -701,16 +746,15 @@
701 FileContains(matcher=Contains(trusted_network)))746 FileContains(matcher=Contains(trusted_network)))
702747
703 def test_dns_config_has_NS_record(self):748 def test_dns_config_has_NS_record(self):
749 self.patch(settings, 'DNS_CONNECT', True)
704 ip = factory.make_ipv4_address()750 ip = factory.make_ipv4_address()
705 with RegionConfiguration.open_for_update() as config:751 with RegionConfiguration.open_for_update() as config:
706 config.maas_url = 'http://%s/' % ip752 config.maas_url = 'http://%s/' % ip
707 domain = factory.make_Domain()753 domain = factory.make_Domain()
708 node, static = self.create_node_with_static_ip(domain=domain)754 node, static = self.create_node_with_static_ip(domain=domain)
709 self.patch(settings, 'DNS_CONNECT', True)755 # Creating the domain triggered writing the zone file and updating the
710 dns_update_all_zones_now()756 # DNS.
711 # Sleep half a second to make sure bind is fully-ready. This is not the757 self.dns_wait_soa(domain.name)
712 # best, but it does prevent this tests from failing randomly.
713 time.sleep(0.5)
714 # Get the NS record for the zone 'domain.name'.758 # Get the NS record for the zone 'domain.name'.
715 ns_record = dig_call(759 ns_record = dig_call(
716 port=self.bind.config.port,760 port=self.bind.config.port,
@@ -718,6 +762,7 @@
718 self.assertGreater(762 self.assertGreater(
719 len(ns_record), 0, "No NS record for domain.name.")763 len(ns_record), 0, "No NS record for domain.name.")
720 # Resolve that hostname.764 # Resolve that hostname.
765 self.dns_wait_soa(ns_record)
721 ip_of_ns_record = dig_call(766 ip_of_ns_record = dig_call(
722 port=self.bind.config.port, commands=[ns_record, '+short'])767 port=self.bind.config.port, commands=[ns_record, '+short'])
723 self.assertEqual(ip, ip_of_ns_record)768 self.assertEqual(ip, ip_of_ns_record)
@@ -736,7 +781,8 @@
736 subnet.dns_servers = []781 subnet.dns_servers = []
737 subnet.save()782 subnet.save()
738 # The IP from the old network does not resolve anymore.783 # The IP from the old network does not resolve anymore.
739 self.assertEqual([''], self.dig_reverse_resolve(lease.ip))784 self.assertEqual([''], self.dig_reverse_resolve(
785 lease.ip, removing=True))
740 # A lease in the new network resolves.786 # A lease in the new network resolves.
741 node, lease = self.create_node_with_static_ip(subnet=subnet)787 node, lease = self.create_node_with_static_ip(subnet=subnet)
742 self.assertTrue(788 self.assertTrue(
@@ -752,7 +798,7 @@
752 ip = factory.pick_ip_in_network(network)798 ip = factory.pick_ip_in_network(network)
753 domain = factory.make_Domain()799 domain = factory.make_Domain()
754 domain.delete()800 domain.delete()
755 self.assertEqual([''], self.dig_reverse_resolve(ip))801 self.assertEqual([''], self.dig_reverse_resolve(ip, removing=True))
756802
757 def test_add_node_updates_zone(self):803 def test_add_node_updates_zone(self):
758 self.patch(settings, "DNS_CONNECT", True)804 self.patch(settings, "DNS_CONNECT", True)
@@ -764,7 +810,7 @@
764 node, static = self.create_node_with_static_ip()810 node, static = self.create_node_with_static_ip()
765 node.delete()811 node.delete()
766 fqdn = "%s.%s" % (node.hostname, node.domain.name)812 fqdn = "%s.%s" % (node.hostname, node.domain.name)
767 self.assertEqual([''], self.dig_resolve(fqdn))813 self.assertEqual([''], self.dig_resolve(fqdn, removing=True))
768814
769 def test_change_node_hostname_updates_zone(self):815 def test_change_node_hostname_updates_zone(self):
770 self.patch(settings, "DNS_CONNECT", True)816 self.patch(settings, "DNS_CONNECT", True)
771817
=== modified file 'src/maasserver/dns/tests/test_zonegenerator.py'
--- src/maasserver/dns/tests/test_zonegenerator.py 2016-02-18 17:31:45 +0000
+++ src/maasserver/dns/tests/test_zonegenerator.py 2016-03-08 17:17:00 +0000
@@ -284,10 +284,10 @@
284 def test_empty_yields_nothing(self):284 def test_empty_yields_nothing(self):
285 self.assertEqual(285 self.assertEqual(
286 [],286 [],
287 ZoneGenerator((), (), serial_generator=Mock()).as_list())287 ZoneGenerator((), (), serial=random.randint(0, 65535)).as_list())
288288
289 def test_defaults_ttl(self):289 def test_defaults_ttl(self):
290 zonegen = ZoneGenerator((), (), serial_generator=Mock())290 zonegen = ZoneGenerator((), (), serial=random.randint(0, 65535))
291 self.assertEqual(291 self.assertEqual(
292 Config.objects.get_config('default_dns_ttl'),292 Config.objects.get_config('default_dns_ttl'),
293 zonegen.default_ttl)293 zonegen.default_ttl)
@@ -296,7 +296,7 @@
296 def test_accepts_default_ttl(self):296 def test_accepts_default_ttl(self):
297 default_ttl = random.randint(10, 1000)297 default_ttl = random.randint(10, 1000)
298 zonegen = ZoneGenerator(298 zonegen = ZoneGenerator(
299 (), (), default_ttl=default_ttl, serial_generator=Mock())299 (), (), default_ttl=default_ttl, serial=random.randint(0, 65535))
300 self.assertEqual(default_ttl, zonegen.default_ttl)300 self.assertEqual(default_ttl, zonegen.default_ttl)
301301
302 def test_yields_forward_and_reverse_zone(self):302 def test_yields_forward_and_reverse_zone(self):
@@ -304,7 +304,7 @@
304 domain = factory.make_Domain(name='henry')304 domain = factory.make_Domain(name='henry')
305 subnet = factory.make_Subnet(cidr=str(IPNetwork("10/29").cidr))305 subnet = factory.make_Subnet(cidr=str(IPNetwork("10/29").cidr))
306 zones = ZoneGenerator(306 zones = ZoneGenerator(
307 domain, subnet, serial_generator=Mock()).as_list()307 domain, subnet, serial=random.randint(0, 65535)).as_list()
308 self.assertThat(308 self.assertThat(
309 zones, MatchesSetwise(309 zones, MatchesSetwise(
310 forward_zone("henry"),310 forward_zone("henry"),
@@ -318,7 +318,7 @@
318 factory.make_Node_with_Interface_on_Subnet(318 factory.make_Node_with_Interface_on_Subnet(
319 subnet=subnet, vlan=subnet.vlan, fabric=subnet.vlan.fabric)319 subnet=subnet, vlan=subnet.vlan, fabric=subnet.vlan.fabric)
320 zones = ZoneGenerator(320 zones = ZoneGenerator(
321 domain, subnet, serial_generator=Mock()).as_list()321 domain, subnet, serial=random.randint(0, 65535)).as_list()
322 self.assertThat(322 self.assertThat(
323 zones, MatchesSetwise(323 zones, MatchesSetwise(
324 forward_zone("henry"),324 forward_zone("henry"),
@@ -347,7 +347,7 @@
347 Config.objects.set_config('default_dns_ttl', default_ttl)347 Config.objects.set_config('default_dns_ttl', default_ttl)
348 zones = ZoneGenerator(348 zones = ZoneGenerator(
349 domain, subnet, default_ttl=default_ttl,349 domain, subnet, default_ttl=default_ttl,
350 serial_generator=Mock()).as_list()350 serial=random.randint(0, 65535)).as_list()
351 self.assertThat(351 self.assertThat(
352 zones, MatchesSetwise(352 zones, MatchesSetwise(
353 forward_zone("henry"),353 forward_zone("henry"),
@@ -404,7 +404,7 @@
404 Config.objects.set_config('default_dns_ttl', default_ttl)404 Config.objects.set_config('default_dns_ttl', default_ttl)
405 zones = ZoneGenerator(405 zones = ZoneGenerator(
406 domain, [subnet1, subnet2], default_ttl=default_ttl,406 domain, [subnet1, subnet2], default_ttl=default_ttl,
407 serial_generator=Mock()).as_list()407 serial=random.randint(0, 65535)).as_list()
408 self.assertThat(408 self.assertThat(
409 zones, MatchesSetwise(409 zones, MatchesSetwise(
410 forward_zone(domain.name),410 forward_zone(domain.name),
@@ -436,7 +436,8 @@
436 ]436 ]
437 self.assertThat(437 self.assertThat(
438 ZoneGenerator(438 ZoneGenerator(
439 domain, [subnet1, subnet2], serial_generator=Mock()).as_list(),439 domain, [subnet1, subnet2],
440 serial=random.randint(0, 65535)).as_list(),
440 MatchesSetwise(*expected_zones))441 MatchesSetwise(*expected_zones))
441442
442 def test_with_many_yields_many_zones(self):443 def test_with_many_yields_many_zones(self):
@@ -459,7 +460,7 @@
459 expected_zones.add(460 expected_zones.add(
460 reverse_zone(default_domain.name, rfc2317_net.cidr))461 reverse_zone(default_domain.name, rfc2317_net.cidr))
461 actual_zones = ZoneGenerator(462 actual_zones = ZoneGenerator(
462 domains, subnets, serial_generator=Mock()).as_list()463 domains, subnets, serial=random.randint(0, 65535)).as_list()
463 self.assertThat(actual_zones, MatchesSetwise(*expected_zones))464 self.assertThat(actual_zones, MatchesSetwise(*expected_zones))
464465
465 def test_zone_generator_handles_rdns_mode_equal_enabled(self):466 def test_zone_generator_handles_rdns_mode_equal_enabled(self):
@@ -475,7 +476,8 @@
475 reverse_zone(default_domain.name, "10/29"),476 reverse_zone(default_domain.name, "10/29"),
476 )477 )
477 self.assertThat(478 self.assertThat(
478 ZoneGenerator(domains, subnets, serial_generator=Mock()).as_list(),479 ZoneGenerator(
480 domains, subnets, serial=random.randint(0, 65535)).as_list(),
479 MatchesSetwise(*expected_zones))481 MatchesSetwise(*expected_zones))
480482
481483
@@ -502,7 +504,7 @@
502 node.system_id, domain.ttl, {boot_ip.ip}, node.node_type)}504 node.system_id, domain.ttl, {boot_ip.ip}, node.node_type)}
503 zones = ZoneGenerator(505 zones = ZoneGenerator(
504 domain, subnet, default_ttl=global_ttl,506 domain, subnet, default_ttl=global_ttl,
505 serial_generator=Mock()).as_list()507 serial=random.randint(0, 65535)).as_list()
506 self.assertItemsEqual(508 self.assertItemsEqual(
507 expected_forward.items(), zones[0]._mapping.items())509 expected_forward.items(), zones[0]._mapping.items())
508 self.assertItemsEqual(510 self.assertItemsEqual(
@@ -532,7 +534,7 @@
532 node.node_type)}534 node.node_type)}
533 zones = ZoneGenerator(535 zones = ZoneGenerator(
534 domain, subnet, default_ttl=global_ttl,536 domain, subnet, default_ttl=global_ttl,
535 serial_generator=Mock()).as_list()537 serial=random.randint(0, 65535)).as_list()
536 self.assertItemsEqual(538 self.assertItemsEqual(
537 expected_forward.items(), zones[0]._mapping.items())539 expected_forward.items(), zones[0]._mapping.items())
538 self.assertItemsEqual(540 self.assertItemsEqual(
@@ -568,7 +570,7 @@
568 node.node_type)}570 node.node_type)}
569 zones = ZoneGenerator(571 zones = ZoneGenerator(
570 domain, subnet, default_ttl=global_ttl,572 domain, subnet, default_ttl=global_ttl,
571 serial_generator=Mock()).as_list()573 serial=random.randint(0, 65535)).as_list()
572 self.assertItemsEqual(574 self.assertItemsEqual(
573 expected_forward.items(), zones[0]._mapping.items())575 expected_forward.items(), zones[0]._mapping.items())
574 self.assertItemsEqual(576 self.assertItemsEqual(
@@ -608,7 +610,7 @@
608 node.node_type)}610 node.node_type)}
609 zones = ZoneGenerator(611 zones = ZoneGenerator(
610 domain, subnet, default_ttl=global_ttl,612 domain, subnet, default_ttl=global_ttl,
611 serial_generator=Mock()).as_list()613 serial=random.randint(0, 65535)).as_list()
612 self.assertItemsEqual(614 self.assertItemsEqual(
613 expected_forward.items(), zones[0]._mapping.items())615 expected_forward.items(), zones[0]._mapping.items())
614 self.assertItemsEqual(616 self.assertItemsEqual(
@@ -629,7 +631,7 @@
629 None, {(global_ttl, dnsdata.rrtype, dnsdata.rrdata)})}631 None, {(global_ttl, dnsdata.rrtype, dnsdata.rrdata)})}
630 zones = ZoneGenerator(632 zones = ZoneGenerator(
631 domain, subnet, default_ttl=global_ttl,633 domain, subnet, default_ttl=global_ttl,
632 serial_generator=Mock()).as_list()634 serial=random.randint(0, 65535)).as_list()
633 self.assertItemsEqual(635 self.assertItemsEqual(
634 expected_forward.items(), zones[0]._other_mapping.items())636 expected_forward.items(), zones[0]._other_mapping.items())
635 self.assertItemsEqual([], zones[0]._mapping.items())637 self.assertItemsEqual([], zones[0]._mapping.items())
@@ -651,7 +653,7 @@
651 None, {(domain.ttl, dnsdata.rrtype, dnsdata.rrdata)})}653 None, {(domain.ttl, dnsdata.rrtype, dnsdata.rrdata)})}
652 zones = ZoneGenerator(654 zones = ZoneGenerator(
653 domain, subnet, default_ttl=global_ttl,655 domain, subnet, default_ttl=global_ttl,
654 serial_generator=Mock()).as_list()656 serial=random.randint(0, 65535)).as_list()
655 self.assertItemsEqual(657 self.assertItemsEqual(
656 expected_forward.items(), zones[0]._other_mapping.items())658 expected_forward.items(), zones[0]._other_mapping.items())
657 self.assertItemsEqual([], zones[0]._mapping.items())659 self.assertItemsEqual([], zones[0]._mapping.items())
@@ -673,7 +675,7 @@
673 None, {(dnsdata.ttl, dnsdata.rrtype, dnsdata.rrdata)})}675 None, {(dnsdata.ttl, dnsdata.rrtype, dnsdata.rrdata)})}
674 zones = ZoneGenerator(676 zones = ZoneGenerator(
675 domain, subnet, default_ttl=global_ttl,677 domain, subnet, default_ttl=global_ttl,
676 serial_generator=Mock()).as_list()678 serial=random.randint(0, 65535)).as_list()
677 self.assertItemsEqual(679 self.assertItemsEqual(
678 expected_forward.items(), zones[0]._other_mapping.items())680 expected_forward.items(), zones[0]._other_mapping.items())
679 self.assertItemsEqual([], zones[0]._mapping.items())681 self.assertItemsEqual([], zones[0]._mapping.items())
680682
=== modified file 'src/maasserver/dns/zonegenerator.py'
--- src/maasserver/dns/zonegenerator.py 2016-03-07 15:15:36 +0000
+++ src/maasserver/dns/zonegenerator.py 2016-03-08 17:17:00 +0000
@@ -152,12 +152,9 @@
152 We generate zones for the domains (forward), and subnets (reverse) passed.152 We generate zones for the domains (forward), and subnets (reverse) passed.
153 """153 """
154154
155 def __init__(self, domains, subnets, default_ttl=None,155 def __init__(self, domains, subnets, default_ttl=None, serial=None):
156 serial=None, serial_generator=None):
157 """156 """
158 :param serial: A serial number to reuse when creating zones in bulk.157 :param serial: A serial number to reuse when creating zones in bulk.
159 :param serial_generator: As an alternative to `serial`, a callback
160 that returns a fresh serial number on every call.
161 """158 """
162 self.domains = sequence(domains)159 self.domains = sequence(domains)
163 self.subnets = sequence(subnets)160 self.subnets = sequence(subnets)
@@ -166,7 +163,6 @@
166 else:163 else:
167 self.default_ttl = default_ttl164 self.default_ttl = default_ttl
168 self.serial = serial165 self.serial = serial
169 self.serial_generator = serial_generator
170166
171 @staticmethod167 @staticmethod
172 def _get_mappings():168 def _get_mappings():
@@ -356,12 +352,11 @@
356 """352 """
357 # For testing and such it's fine if we don't have a serial, but once353 # For testing and such it's fine if we don't have a serial, but once
358 # we get to this point, we really need one.354 # we get to this point, we really need one.
359 assert not (self.serial is None and self.serial_generator is None), (355 assert not (self.serial is None), ("No serial number specified.")
360 "No serial number or serial number generator specified.")
361356
362 mappings = self._get_mappings()357 mappings = self._get_mappings()
363 rrset_mappings = self._get_rrset_mappings()358 rrset_mappings = self._get_rrset_mappings()
364 serial = self.serial or self.serial_generator()359 serial = self.serial
365 default_ttl = self.default_ttl360 default_ttl = self.default_ttl
366 return chain(361 return chain(
367 self._gen_forward_zones(362 self._gen_forward_zones(
368363
=== modified file 'src/maasserver/sequence.py'
--- src/maasserver/sequence.py 2016-01-25 11:43:50 +0000
+++ src/maasserver/sequence.py 2016-03-08 17:17:00 +0000
@@ -20,6 +20,7 @@
20from provisioningserver.utils import typed20from provisioningserver.utils import typed
21from psycopg2.errorcodes import (21from psycopg2.errorcodes import (
22 DUPLICATE_TABLE,22 DUPLICATE_TABLE,
23 OBJECT_NOT_IN_PREREQUISITE_STATE,
23 UNDEFINED_TABLE,24 UNDEFINED_TABLE,
24)25)
2526
@@ -153,6 +154,29 @@
153 else:154 else:
154 raise155 raise
155156
157 def current(self):
158 """Return the current value of this sequence, or `None`.
159
160 :return: The sequence value, or None if there is no current value for
161 the sequence in the database session or if the sequence does not
162 exist.
163 :rtype: int / None
164 """
165 try:
166 with transaction.atomic():
167 with connection.cursor() as cursor:
168 cursor.execute("SELECT currval(%s)", [self.name])
169 return cursor.fetchone()[0]
170 except (utils.OperationalError, utils.ProgrammingError) as error:
171 if is_postgres_error(error, OBJECT_NOT_IN_PREREQUISITE_STATE):
172 # There is no current value for the sequence.
173 return None
174 elif is_postgres_error(error, UNDEFINED_TABLE):
175 # The sequence does not exist, hence has no current value.
176 return None
177 else:
178 raise
179
156180
157def is_postgres_error(error, *pgcodes):181def is_postgres_error(error, *pgcodes):
158 # Unwrap Django's lowest-common-denominator exception.182 # Unwrap Django's lowest-common-denominator exception.
159183
=== modified file 'src/maasserver/tests/test_sequence.py'
--- src/maasserver/tests/test_sequence.py 2016-01-25 11:43:50 +0000
+++ src/maasserver/tests/test_sequence.py 2016-03-08 17:17:00 +0000
@@ -43,7 +43,7 @@
43 seq.create()43 seq.create()
44 self.assertRaisesRegex(ProgrammingError, "already exists", seq.create)44 self.assertRaisesRegex(ProgrammingError, "already exists", seq.create)
4545
46 def test_create_if_not_exists_does_not_fails_if_sequence_exists(self):46 def test_create_if_not_exists_does_not_fail_if_sequence_exists(self):
47 name = factory.make_name('seq', sep='')47 name = factory.make_name('seq', sep='')
48 seq = Sequence(name)48 seq = Sequence(name)
49 seq.create()49 seq.create()
@@ -151,3 +151,33 @@
151 seq.create()151 seq.create()
152 self.assertSequenceEqual(152 self.assertSequenceEqual(
153 list(range(1, 11)), list(islice(seq, 10)))153 list(range(1, 11)), list(islice(seq, 10)))
154
155 def test_current_returns_none_when_table_does_not_exist(self):
156 name = factory.make_name('seq', sep='')
157 seq = Sequence(name)
158 self.assertEqual(None, seq.current())
159
160 def test_current_returns_none_when_no_current_value(self):
161 name = factory.make_name('seq', sep='')
162 seq = Sequence(name)
163 seq.create()
164 self.assertEqual(None, seq.current())
165
166 def test_current_returns_current_value(self):
167 name = factory.make_name('seq', sep='')
168 seq = Sequence(name)
169 seq.create()
170 expected = next(seq)
171 self.assertEqual(expected, seq.current())
172 self.assertEqual(expected, seq.current())
173
174 def test_current_returns_correct_value(self):
175 name = factory.make_name('seq', sep='')
176 seq = Sequence(name)
177 seq.create()
178 expected = next(seq)
179 self.assertEqual(expected, seq.current())
180 self.assertEqual(expected, seq.current())
181 expected = next(seq)
182 self.assertEqual(expected, seq.current())
183 self.assertEqual(expected, seq.current())
154184
=== modified file 'src/provisioningserver/testing/bindfixture.py'
--- src/provisioningserver/testing/bindfixture.py 2015-12-01 18:12:59 +0000
+++ src/provisioningserver/testing/bindfixture.py 2016-03-08 17:17:00 +0000
@@ -119,8 +119,9 @@
119 logging{119 logging{
120 channel simple_log {120 channel simple_log {
121 file "{{log_file}}";121 file "{{log_file}}";
122 severity info;122 severity debug;
123 print-severity yes;123 print-severity yes;
124 print-time yes;
124 };125 };
125 category default{126 category default{
126 simple_log;127 simple_log;
127128
=== modified file 'src/provisioningserver/utils/fs.py'
--- src/provisioningserver/utils/fs.py 2016-03-08 11:44:02 +0000
+++ src/provisioningserver/utils/fs.py 2016-03-08 17:17:00 +0000
@@ -42,10 +42,7 @@
42)42)
43import tempfile43import tempfile
44import threading44import threading
45from time import (45from time import sleep
46 sleep,
47 time,
48)
4946
50from provisioningserver.utils import sudo47from provisioningserver.utils import sudo
51from provisioningserver.utils.shell import ExternalProcessError48from provisioningserver.utils.shell import ExternalProcessError
@@ -201,61 +198,27 @@
201 raise198 raise
202199
203200
204def pick_new_mtime(old_mtime=None):
205 """Choose a new modification time for a file that needs it updated.
206
207 This function is used to manage the modification time of files
208 for which we need to see an increment in the modification time
209 each time the file is modified. This is the case for DNS zone
210 files which only get properly reloaded if BIND sees that the
211 modification time is > to the time it has in its database.
212
213 Modification time can have a resolution as low as one second in
214 some relevant environments (we have observed this with ext3).
215 To produce mtime changes regardless, we set a file's modification
216 time in the past when it is first written, and
217 increment it by 1 second on each subsequent write.
218
219 :param old_mtime: File's previous modification time, as a number
220 with a unity of one second, or None if it did not previously
221 exist.
222 """
223 now = time()
224 if old_mtime is None or old_mtime + 1 < now:
225 # File is new. Set modification time in the past to have room for
226 # sub-second modifications.
227 return now
228 else:
229 # Let the file timestamp march into the future, so that BIND will
230 # reload the zone. Our future selves will notice that mtime is enough
231 # before the then-now to go back to having it match us.
232 return old_mtime + 1
233
234
235def incremental_write(content, filename, mode=0o600):201def incremental_write(content, filename, mode=0o600):
236 """Write the given `content` into the file `filename` and202 """Write the given `content` into the file `filename`. In the past, this
237 increment the modification time by 1 sec.203 would potentially change modification time to arbitrary values.
238204
239 :type content: `bytes`205 :type content: `bytes`
240 :param mode: Access permissions for the file.206 :param mode: Access permissions for the file.
241 """207 """
242 old_mtime = get_mtime(filename)208 # We used to change modification time on the files, in an attempt to out
209 # smart BIND into loading zones on file systems where time was not granular
210 # enough. BIND got smarter about how it loads zones and remembers when it
211 # last loaded the zone: either the then-current time, or the mtime of the
212 # file, whichever is later. When BIND then gets a reload request, it
213 # compares the current time to the loadtime for the domain, and skips it if
214 # the file is not new. If we set mtime in the past, then zones don't load.
215 # If we set it in the future, then we WILL sometimes hit the race condition
216 # where BIND looks at the time after we atomic_write, but before we manage
217 # to set the time into the future. N.B.: /etc on filesystems with 1-second
218 # granularity are no longer supported by MAAS. The good news is that since
219 # 2.6, linux has supported nanosecond-granular time. As of bind9
220 # 1:9.10.3.dfsg.P2-5, BIND even uses it.
243 atomic_write(content, filename, mode=mode)221 atomic_write(content, filename, mode=mode)
244 new_mtime = pick_new_mtime(old_mtime)
245 os.utime(filename, (new_mtime, new_mtime))
246
247
248def get_mtime(filename):
249 """Return a file's modification time, or None if it does not exist."""
250 try:
251 return os.stat(filename).st_mtime
252 except OSError as e:
253 if e.errno == errno.ENOENT:
254 # File does not exist. Be helpful, return None.
255 return None
256 else:
257 # Other failure. The caller will want to know.
258 raise
259222
260223
261def sudo_write_file(filename, contents, mode=0o644):224def sudo_write_file(filename, contents, mode=0o644):
262225
=== modified file 'src/provisioningserver/utils/tests/test_fs.py'
--- src/provisioningserver/utils/tests/test_fs.py 2016-03-07 14:35:29 +0000
+++ src/provisioningserver/utils/tests/test_fs.py 2016-03-08 17:17:00 +0000
@@ -8,7 +8,6 @@
8from base64 import urlsafe_b64encode8from base64 import urlsafe_b64encode
9import os9import os
10import os.path10import os.path
11from random import randint
12import re11import re
13from shutil import rmtree12from shutil import rmtree
14import stat13import stat
@@ -21,7 +20,6 @@
2120
22from maastesting import root21from maastesting import root
23from maastesting.factory import factory22from maastesting.factory import factory
24from maastesting.fakemethod import FakeMethod
25from maastesting.matchers import (23from maastesting.matchers import (
26 FileContains,24 FileContains,
27 MockCalledOnceWith,25 MockCalledOnceWith,
@@ -43,9 +41,7 @@
43 ensure_dir,41 ensure_dir,
44 FileLock,42 FileLock,
45 get_maas_provision_command,43 get_maas_provision_command,
46 get_mtime,
47 incremental_write,44 incremental_write,
48 pick_new_mtime,
49 read_text_file,45 read_text_file,
50 RunLock,46 RunLock,
51 sudo_delete_file,47 sudo_delete_file,
@@ -250,7 +246,7 @@
250class TestIncrementalWrite(MAASTestCase):246class TestIncrementalWrite(MAASTestCase):
251 """Test `incremental_write`."""247 """Test `incremental_write`."""
252248
253 def test_incremental_write_increments_modification_time(self):249 def test_incremental_write_updates_modification_time(self):
254 content = factory.make_bytes()250 content = factory.make_bytes()
255 filename = self.make_file(contents=factory.make_string())251 filename = self.make_file(contents=factory.make_string())
256 # Pretend that this file is older than it is. So that252 # Pretend that this file is older than it is. So that
@@ -259,8 +255,21 @@
259 os.utime(filename, (old_mtime, old_mtime))255 os.utime(filename, (old_mtime, old_mtime))
260 incremental_write(content, filename)256 incremental_write(content, filename)
261 new_time = time.time()257 new_time = time.time()
262 self.assertAlmostEqual(258 # should be much closer to new_time than to old_mtime.
263 os.stat(filename).st_mtime, new_time, delta=0.01)259 self.assertAlmostEqual(
260 os.stat(filename).st_mtime, new_time, delta=2.0)
261
262 def test_incremental_write_does_not_set_future_time(self):
263 content = factory.make_bytes()
264 filename = self.make_file(contents=factory.make_string())
265 # Pretend that this file is older than it is. So that
266 # incrementing its mtime won't put it in the future.
267 old_mtime = os.stat(filename).st_mtime + 10
268 os.utime(filename, (old_mtime, old_mtime))
269 incremental_write(content, filename)
270 new_time = time.time()
271 self.assertAlmostEqual(
272 os.stat(filename).st_mtime, new_time, delta=2.0)
264273
265 def test_incremental_write_sets_permissions(self):274 def test_incremental_write_sets_permissions(self):
266 atomic_file = self.make_file()275 atomic_file = self.make_file()
@@ -269,45 +278,6 @@
269 self.assertEqual(mode, stat.S_IMODE(os.stat(atomic_file).st_mode))278 self.assertEqual(mode, stat.S_IMODE(os.stat(atomic_file).st_mode))
270279
271280
272class TestGetMTime(MAASTestCase):
273 """Test `get_mtime`."""
274
275 def test_get_mtime_returns_None_for_nonexistent_file(self):
276 nonexistent_file = os.path.join(
277 self.make_dir(), factory.make_name('nonexistent-file'))
278 self.assertIsNone(get_mtime(nonexistent_file))
279
280 def test_get_mtime_returns_mtime(self):
281 existing_file = self.make_file()
282 mtime = os.stat(existing_file).st_mtime - randint(0, 100)
283 os.utime(existing_file, (mtime, mtime))
284 # Some small rounding/representation errors can happen here.
285 # That's just the way of floating-point numbers. According to
286 # Gavin there's a conversion to fixed-point along the way, which
287 # would raise representability issues.
288 self.assertAlmostEqual(mtime, get_mtime(existing_file), delta=0.00001)
289
290 def test_get_mtime_passes_on_other_error(self):
291 forbidden_file = self.make_file()
292 self.patch(os, 'stat', FakeMethod(failure=OSError("Forbidden file")))
293 self.assertRaises(OSError, get_mtime, forbidden_file)
294
295
296class TestPickNewMTime(MAASTestCase):
297 """Test `pick_new_mtime`."""
298
299 def test_pick_new_mtime_increments_mtime_if_possible(self):
300 now = time.time()
301 new_time = pick_new_mtime(now - 2)
302 self.assertAlmostEqual(new_time, now, delta=0.0001)
303
304 def test_pick_new_mtime_happily_moves_mtime_into_the_future(self):
305 # Race condition: this will fail if the test gets held up for
306 # a second between readings of the clock.
307 now = time.time()
308 self.assertEqual(now + 1, pick_new_mtime(now))
309
310
311class TestGetMAASProvisionCommand(MAASTestCase):281class TestGetMAASProvisionCommand(MAASTestCase):
312282
313 def test__returns_just_command_for_production(self):283 def test__returns_just_command_for_production(self):
314284
=== modified file 'utilities/check-imports'
--- utilities/check-imports 2016-02-29 14:42:45 +0000
+++ utilities/check-imports 2016-03-08 17:17:00 +0000
@@ -148,6 +148,7 @@
148 map("{0}|{0}.**".format, python_standard_libs))148 map("{0}|{0}.**".format, python_standard_libs))
149TestingLibraries = Pattern(149TestingLibraries = Pattern(
150 "django_nose|django_nose.**",150 "django_nose|django_nose.**",
151 "dns|dns.**",
151 "fixtures|fixtures.**",152 "fixtures|fixtures.**",
152 "hypothesis|hypothesis.**",153 "hypothesis|hypothesis.**",
153 "maastesting|maastesting.**",154 "maastesting|maastesting.**",