Merge lp:~lamont/maas/bug-1356012 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: 4621
Proposed branch: lp:~lamont/maas/bug-1356012
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 661 lines (+238/-26)
14 files modified
src/maasserver/api/subnets.py (+10/-1)
src/maasserver/api/tests/test_subnets.py (+7/-0)
src/maasserver/dns/tests/test_config.py (+12/-6)
src/maasserver/dns/tests/test_zonegenerator.py (+53/-9)
src/maasserver/dns/zonegenerator.py (+38/-2)
src/maasserver/enum.py (+26/-0)
src/maasserver/forms_subnet.py (+5/-0)
src/maasserver/migrations/builtin/maasserver/0023_add_rdns_mode.py (+22/-0)
src/maasserver/models/subnet.py (+7/-0)
src/maasserver/models/tests/test_subnet.py (+5/-2)
src/maasserver/testing/factory.py (+3/-2)
src/maasserver/websockets/handlers/tests/test_subnet.py (+1/-0)
src/provisioningserver/dns/tests/test_zoneconfig.py (+4/-2)
src/provisioningserver/dns/zoneconfig.py (+45/-2)
To merge this branch: bzr merge lp:~lamont/maas/bug-1356012
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+284354@code.launchpad.net

Commit message

Generate rfc2317 glue zones in the DNS by default, allow the admin to disable that if desired.

Description of the change

If a subnet is smaller than a /24 (or /124 for IPv6 networks), then reverse DNS needs to be delegated from the respective /24 (/124) DNS zone. MAAS now defaults to doing that, and allows the admin to override that.

Additionally, the admin can now turn off DNS PTR zone (reverse dns) for an individual Subnet.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Overall looks good. Not going to block you on it. Would really like the see the missing documentation be done on the API calls before this lands.

review: Approve
Revision history for this message
LaMont Jones (lamont) wrote :

some replies, everything addressed.

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.1 MiB)

The attempt to merge lp:~lamont/maas/bug-1356012 into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://security.ubuntu.com/ubuntu xenial-security InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease [227 kB]
Hit:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main Sources [1,129 kB]
Get:6 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe Sources [7,635 kB]
Get:7 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/multiverse Sources [178 kB]
Get:8 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main amd64 Packages [1,481 kB]
Get:9 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main Translation-en [855 kB]
Get:10 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe amd64 Packages [7,220 kB]
Get:11 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe Translation-en [4,828 kB]
Get:12 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/multiverse amd64 Packages [139 kB]
Fetched 23.7 MB in 7s (3,134 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-apt python3-bson python3-convoy python3-coverage python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-mock python3-netaddr python3-netifaces python3-oauth python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-seamicroclient python3-simplejson python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-1ubuntu1).
authbind is already the newest version (2.1.1+nmu1).
bind9 is already the newest version (1:9.9.5.dfsg-12.1ubuntu1).
bind9utils is already the newest version (1:9.9.5.dfsg-12.1ubuntu1).
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu1).
debhelper is already the newest ve...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/api/subnets.py'
--- src/maasserver/api/subnets.py 2015-12-01 18:12:59 +0000
+++ src/maasserver/api/subnets.py 2016-01-29 18:06:17 +0000
@@ -26,6 +26,7 @@
26 'cidr',26 'cidr',
27 'gateway_ip',27 'gateway_ip',
28 'dns_servers',28 'dns_servers',
29 'rdns_mode',
29)30)
3031
3132
@@ -60,7 +61,14 @@
60 :param space: Space this subnet is in. Defaults to the default space.61 :param space: Space this subnet is in. Defaults to the default space.
61 :param cidr: The network CIDR for this subnet.62 :param cidr: The network CIDR for this subnet.
62 :param gateway_ip: The gateway IP address for this subnet.63 :param gateway_ip: The gateway IP address for this subnet.
63 :param dns_servers: Comma-seperated list of DNS servers for this \64 :param rdns_mode: How reverse DNS is handled for this subnet.
65 One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled means
66 no reverse zone is created; Enabled means generate the reverse
67 zone; RFC2317 extends Enabled to create the necessary parent zone
68 with the appropriate CNAME resource records for the network, if the
69 network is small enough to require the support described in
70 RFC2317.
71 :param dns_servers: Comma-seperated list of DNS servers for this
64 subnet.72 subnet.
65 """73 """
66 form = SubnetForm(data=request.data)74 form = SubnetForm(data=request.data)
@@ -111,6 +119,7 @@
111 :param space: Space this subnet is in.119 :param space: Space this subnet is in.
112 :param cidr: The network CIDR for this subnet.120 :param cidr: The network CIDR for this subnet.
113 :param gateway_ip: The gateway IP address for this subnet.121 :param gateway_ip: The gateway IP address for this subnet.
122 :param rdns_mode: How reverse DNS is handled for this subnet.
114 :param dns_servers: Comma-seperated list of DNS servers for this \123 :param dns_servers: Comma-seperated list of DNS servers for this \
115 subnet.124 subnet.
116125
117126
=== modified file 'src/maasserver/api/tests/test_subnets.py'
--- src/maasserver/api/tests/test_subnets.py 2016-01-25 14:10:29 +0000
+++ src/maasserver/api/tests/test_subnets.py 2016-01-29 18:06:17 +0000
@@ -15,6 +15,7 @@
15 IPADDRESS_TYPE,15 IPADDRESS_TYPE,
16 NODE_STATUS,16 NODE_STATUS,
17 NODEGROUP_STATUS,17 NODEGROUP_STATUS,
18 RDNS_MODE_CHOICES,
18)19)
19from maasserver.testing.api import (20from maasserver.testing.api import (
20 APITestCase,21 APITestCase,
@@ -82,6 +83,7 @@
82 space = factory.make_Space()83 space = factory.make_Space()
83 network = factory.make_ip4_or_6_network()84 network = factory.make_ip4_or_6_network()
84 cidr = str(network.cidr)85 cidr = str(network.cidr)
86 rdns_mode = factory.pick_choice(RDNS_MODE_CHOICES)
85 gateway_ip = factory.pick_ip_in_network(network)87 gateway_ip = factory.pick_ip_in_network(network)
86 dns_servers = []88 dns_servers = []
87 for _ in range(2):89 for _ in range(2):
@@ -96,6 +98,7 @@
96 "cidr": cidr,98 "cidr": cidr,
97 "gateway_ip": gateway_ip,99 "gateway_ip": gateway_ip,
98 "dns_servers": ','.join(dns_servers),100 "dns_servers": ','.join(dns_servers),
101 "rdns_mode": rdns_mode,
99 })102 })
100 self.assertEqual(103 self.assertEqual(
101 http.client.OK, response.status_code, response.content)104 http.client.OK, response.status_code, response.content)
@@ -107,6 +110,7 @@
107 self.assertEqual(cidr, created_subnet['cidr'])110 self.assertEqual(cidr, created_subnet['cidr'])
108 self.assertEqual(gateway_ip, created_subnet['gateway_ip'])111 self.assertEqual(gateway_ip, created_subnet['gateway_ip'])
109 self.assertEqual(dns_servers, created_subnet['dns_servers'])112 self.assertEqual(dns_servers, created_subnet['dns_servers'])
113 self.assertEqual(rdns_mode, created_subnet['rdns_mode'])
110114
111 def test_create_admin_only(self):115 def test_create_admin_only(self):
112 subnet_name = factory.make_name("subnet")116 subnet_name = factory.make_name("subnet")
@@ -185,9 +189,11 @@
185 self.become_admin()189 self.become_admin()
186 subnet = factory.make_Subnet()190 subnet = factory.make_Subnet()
187 new_name = factory.make_name("subnet")191 new_name = factory.make_name("subnet")
192 new_rdns_mode = factory.pick_choice(RDNS_MODE_CHOICES)
188 uri = get_subnet_uri(subnet)193 uri = get_subnet_uri(subnet)
189 response = self.client.put(uri, {194 response = self.client.put(uri, {
190 "name": new_name,195 "name": new_name,
196 "rdns_mode": new_rdns_mode,
191 })197 })
192 self.assertEqual(198 self.assertEqual(
193 http.client.OK, response.status_code, response.content)199 http.client.OK, response.status_code, response.content)
@@ -195,6 +201,7 @@
195 new_name, json.loads(201 new_name, json.loads(
196 response.content.decode(settings.DEFAULT_CHARSET))['name'])202 response.content.decode(settings.DEFAULT_CHARSET))['name'])
197 self.assertEqual(new_name, reload_object(subnet).name)203 self.assertEqual(new_name, reload_object(subnet).name)
204 self.assertEqual(new_rdns_mode, reload_object(subnet).rdns_mode)
198205
199 def test_update_admin_only(self):206 def test_update_admin_only(self):
200 subnet = factory.make_Subnet()207 subnet = factory.make_Subnet()
201208
=== modified file 'src/maasserver/dns/tests/test_config.py'
--- src/maasserver/dns/tests/test_config.py 2016-01-25 12:08:51 +0000
+++ src/maasserver/dns/tests/test_config.py 2016-01-29 18:06:17 +0000
@@ -558,8 +558,10 @@
558 commands=['-x', ip, '+short', '-%i' % version])558 commands=['-x', ip, '+short', '-%i' % version])
559 return output.split('\n')559 return output.split('\n')
560560
561 def assertDNSMatches(self, hostname, domain, ip, version=4):561 def assertDNSMatches(self, hostname, domain, ip, version=-1):
562 # A forward lookup on the hostname returns the IP address.562 # A forward lookup on the hostname returns the IP address.
563 if version == -1:
564 version = IPAddress(ip).version
563 fqdn = "%s.%s" % (hostname, domain)565 fqdn = "%s.%s" % (hostname, domain)
564 forward_lookup_result = self.dig_resolve(fqdn, version=version)566 forward_lookup_result = self.dig_resolve(fqdn, version=version)
565 self.expectThat(567 self.expectThat(
@@ -725,7 +727,8 @@
725727
726 def test_changing_interface_management_updates_DNS_zone(self):728 def test_changing_interface_management_updates_DNS_zone(self):
727 self.patch(settings, "DNS_CONNECT", True)729 self.patch(settings, "DNS_CONNECT", True)
728 network = IPNetwork('192.168.7.1/24')730 network = factory.make_ip4_or_6_network(
731 host_bits=random.randint(3, 10))
729 ip = factory.pick_ip_in_network(network)732 ip = factory.pick_ip_in_network(network)
730 nodegroup = factory.make_NodeGroup(733 nodegroup = factory.make_NodeGroup(
731 network=network, status=NODEGROUP_STATUS.ENABLED,734 network=network, status=NODEGROUP_STATUS.ENABLED,
@@ -737,7 +740,8 @@
737740
738 def test_delete_nodegroup_disables_DNS_zone(self):741 def test_delete_nodegroup_disables_DNS_zone(self):
739 self.patch(settings, "DNS_CONNECT", True)742 self.patch(settings, "DNS_CONNECT", True)
740 network = IPNetwork('192.168.7.1/24')743 network = factory.make_ip4_or_6_network(
744 host_bits=random.randint(3, 10))
741 ip = factory.pick_ip_in_network(network)745 ip = factory.pick_ip_in_network(network)
742 nodegroup = factory.make_NodeGroup(746 nodegroup = factory.make_NodeGroup(
743 network=network, status=NODEGROUP_STATUS.ENABLED,747 network=network, status=NODEGROUP_STATUS.ENABLED,
@@ -782,7 +786,8 @@
782786
783 def test_bind_configuration_includes_dynamic_ips_of_deployed_nodes(self):787 def test_bind_configuration_includes_dynamic_ips_of_deployed_nodes(self):
784 self.patch(settings, "DNS_CONNECT", True)788 self.patch(settings, "DNS_CONNECT", True)
785 network = IPNetwork('192.168.7.1/24')789 network = factory.make_ip4_or_6_network(
790 host_bits=random.randint(3, 10))
786 nodegroup = self.create_managed_nodegroup(network=network)791 nodegroup = self.create_managed_nodegroup(network=network)
787 [interface] = nodegroup.get_managed_interfaces()792 [interface] = nodegroup.get_managed_interfaces()
788 node = factory.make_Node(793 node = factory.make_Node(
@@ -805,7 +810,8 @@
805810
806 def test_dnsresources_are_in_the_dns(self):811 def test_dnsresources_are_in_the_dns(self):
807 self.patch(settings, "DNS_CONNECT", True)812 self.patch(settings, "DNS_CONNECT", True)
808 network = IPNetwork('192.168.7.1/24')813 network = factory.make_ip4_or_6_network(
814 host_bits=random.randint(3, 10))
809 nodegroup = self.create_managed_nodegroup(network=network)815 nodegroup = self.create_managed_nodegroup(network=network)
810 [interface] = nodegroup.get_managed_interfaces()816 [interface] = nodegroup.get_managed_interfaces()
811 node = factory.make_Node(817 node = factory.make_Node(
@@ -829,7 +835,7 @@
829835
830 def test_bind_configuration_includes_ipv6_zone(self):836 def test_bind_configuration_includes_ipv6_zone(self):
831 self.patch(settings, "DNS_CONNECT", True)837 self.patch(settings, "DNS_CONNECT", True)
832 network = IPNetwork('fe80::/16')838 network = factory.make_ipv6_network(slash=random.randint(118, 125))
833 nodegroup = self.create_managed_nodegroup(network=network)839 nodegroup = self.create_managed_nodegroup(network=network)
834 nodegroup, node, static = self.create_nodegroup_with_static_ip(840 nodegroup, node, static = self.create_nodegroup_with_static_ip(
835 nodegroup=nodegroup)841 nodegroup=nodegroup)
836842
=== modified file 'src/maasserver/dns/tests/test_zonegenerator.py'
--- src/maasserver/dns/tests/test_zonegenerator.py 2016-01-25 14:07:49 +0000
+++ src/maasserver/dns/tests/test_zonegenerator.py 2016-01-29 18:06:17 +0000
@@ -27,6 +27,7 @@
27 NODE_STATUS,27 NODE_STATUS,
28 NODEGROUP_STATUS,28 NODEGROUP_STATUS,
29 NODEGROUPINTERFACE_MANAGEMENT,29 NODEGROUPINTERFACE_MANAGEMENT,
30 RDNS_MODE,
30)31)
31from maasserver.models import (32from maasserver.models import (
32 Config,33 Config,
@@ -423,7 +424,8 @@
423 self.assertThat(424 self.assertThat(
424 zones, MatchesSetwise(425 zones, MatchesSetwise(
425 forward_zone("henry"),426 forward_zone("henry"),
426 reverse_zone(default_domain, "10/29")))427 reverse_zone(default_domain, "10/29"),
428 reverse_zone(default_domain, "10/24")))
427429
428 def test_with_one_nodegroup_with_node_yields_fwd_and_rev_zone(self):430 def test_with_one_nodegroup_with_node_yields_fwd_and_rev_zone(self):
429 self.useFixture(RegionConfigurationFixture())431 self.useFixture(RegionConfigurationFixture())
@@ -442,7 +444,8 @@
442 self.assertThat(444 self.assertThat(
443 zones, MatchesSetwise(445 zones, MatchesSetwise(
444 forward_zone("henry"),446 forward_zone("henry"),
445 reverse_zone(default_domain, "10/29")))447 reverse_zone(default_domain, "10/29"),
448 reverse_zone(default_domain, "10/24")))
446449
447 def test_returns_interface_ips_but_no_nulls(self):450 def test_returns_interface_ips_but_no_nulls(self):
448 self.useFixture(RegionConfigurationFixture())451 self.useFixture(RegionConfigurationFixture())
@@ -473,7 +476,8 @@
473 self.assertThat(476 self.assertThat(
474 zones, MatchesSetwise(477 zones, MatchesSetwise(
475 forward_zone("henry"),478 forward_zone("henry"),
476 reverse_zone(default_domain, "10/29")))479 reverse_zone(default_domain, "10/29"),
480 reverse_zone(default_domain, "10/24")))
477 self.assertEqual(481 self.assertEqual(
478 {node.hostname: (30, ['%s' % boot_ip.ip])}, zones[0]._mapping)482 {node.hostname: (30, ['%s' % boot_ip.ip])}, zones[0]._mapping)
479 self.assertEqual(483 self.assertEqual(
@@ -488,21 +492,37 @@
488 default_ttl, ['%s' % boot_ip.ip])},492 default_ttl, ['%s' % boot_ip.ip])},
489 zones[1]._mapping)493 zones[1]._mapping)
490494
495 def rfc2317_network(self, network):
496 """Returns the network that rfc2317 glue goes in, if any."""
497 net = network
498 if net.version == 4 and net.prefixlen > 24:
499 net = IPNetwork("%s/24" % net.network)
500 net = IPNetwork("%s/24" % net.network)
501 if net.version == 6 and net.prefixlen > 124:
502 net = IPNetwork("%s/124" % net.network)
503 net = IPNetwork("%s/124" % net.network)
504 if net != network:
505 return net
506 return None
507
491 def test_two_managed_interfaces_yields_one_forward_two_reverse_zones(self):508 def test_two_managed_interfaces_yields_one_forward_two_reverse_zones(self):
492 self.useFixture(RegionConfigurationFixture())509 self.useFixture(RegionConfigurationFixture())
493 nodegroup = self.make_node_group()510 nodegroup = self.make_node_group()
494 factory.make_NodeGroupInterface(511 factory.make_NodeGroupInterface(
495 nodegroup=nodegroup,512 nodegroup=nodegroup,
496 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)513 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
497 [interface1, interface2] = nodegroup.get_managed_interfaces()514 interfaces = nodegroup.get_managed_interfaces()
498 default_domain = Domain.objects.get_default_domain().name515 default_domain = Domain.objects.get_default_domain().name
499516
500 factory.make_Domain(name=nodegroup.name)517 factory.make_Domain(name=nodegroup.name)
501 expected_zones = [518 expected_zones = [forward_zone(nodegroup.name)] + [
502 forward_zone(nodegroup.name),519 reverse_zone(default_domain, iface.network)
503 reverse_zone(default_domain, interface1.network),520 for iface in interfaces
504 reverse_zone(default_domain, interface2.network),521 ] + [
505 ]522 reverse_zone(default_domain, self.rfc2317_network(iface.network))
523 for iface in interfaces
524 if self.rfc2317_network(iface.network) is not None
525 ]
506 domain = Domain.objects.get(name=nodegroup.name)526 domain = Domain.objects.get(name=nodegroup.name)
507 subnet = Subnet.objects.filter(527 subnet = Subnet.objects.filter(
508 nodegroupinterface__nodegroup=nodegroup)528 nodegroupinterface__nodegroup=nodegroup)
@@ -541,6 +561,30 @@
541 reverse_zone(default_domain.name, "11/29"),561 reverse_zone(default_domain.name, "11/29"),
542 reverse_zone(default_domain.name, "20/29"),562 reverse_zone(default_domain.name, "20/29"),
543 reverse_zone(default_domain.name, "21/29"),563 reverse_zone(default_domain.name, "21/29"),
564 reverse_zone(default_domain.name, "10/24"),
565 reverse_zone(default_domain.name, "11/24"),
566 reverse_zone(default_domain.name, "20/24"),
567 reverse_zone(default_domain.name, "21/24"),
568 )
569 self.assertThat(
570 ZoneGenerator(domains, subnets, serial_generator=Mock()).as_list(),
571 MatchesSetwise(*expected_zones))
572
573 def test_zone_generator_handles_rdns_mode_equal_enabled(self):
574 self.useFixture(RegionConfigurationFixture())
575 Domain.objects.get_or_create(name="one")
576 nodegroup = self.make_node_group(
577 name="one", network=IPNetwork("10/29"))
578 subnet = Subnet.objects.get(cidr="10.0.0.0/29")
579 subnet.rdns_mode = RDNS_MODE.ENABLED
580 subnet.save()
581 default_domain = Domain.objects.get_default_domain()
582 domains = Domain.objects.filter(name="one")
583 subnets = Subnet.objects.filter(
584 nodegroupinterface__nodegroup_id=nodegroup.id)
585 expected_zones = (
586 forward_zone("one"),
587 reverse_zone(default_domain.name, "10/29"),
544 )588 )
545 self.assertThat(589 self.assertThat(
546 ZoneGenerator(domains, subnets, serial_generator=Mock()).as_list(),590 ZoneGenerator(domains, subnets, serial_generator=Mock()).as_list(),
547591
=== modified file 'src/maasserver/dns/zonegenerator.py'
--- src/maasserver/dns/zonegenerator.py 2016-01-25 14:07:49 +0000
+++ src/maasserver/dns/zonegenerator.py 2016-01-29 18:06:17 +0000
@@ -16,7 +16,10 @@
16import socket16import socket
1717
18from maasserver import logger18from maasserver import logger
19from maasserver.enum import NODEGROUP_STATUS19from maasserver.enum import (
20 NODEGROUP_STATUS,
21 RDNS_MODE,
22)
20from maasserver.exceptions import MAASException23from maasserver.exceptions import MAASException
21from maasserver.models.config import Config24from maasserver.models.config import Config
22from maasserver.models.dnsresource import DNSResource25from maasserver.models.dnsresource import DNSResource
@@ -287,6 +290,24 @@
287 # 2. node: ip mapping(subnet), including DNSResource records for290 # 2. node: ip mapping(subnet), including DNSResource records for
288 # StaticIPAddresses in this subnet291 # StaticIPAddresses in this subnet
289 # 3. interfaces on any node that have IP addresses in this subnet292 # 3. interfaces on any node that have IP addresses in this subnet
293 rfc2317_glue = {}
294 for subnet in subnets:
295 network = IPNetwork(subnet.cidr)
296 # If this is a small subnet and we are doing RFC2317 glue for it,
297 # then we need to combine that with any other such subnets
298 # We need to know this before we start creating reverse DNS zones.
299 if subnet.rdns_mode == RDNS_MODE.RFC2317:
300 if network.version == 4 and network.prefixlen > 24:
301 # Turn 192.168.99.32/29 into 192.168.99.0/24
302 basenet = IPNetwork(
303 "%s/24" %
304 IPNetwork("%s/24" % network.network).network)
305 rfc2317_glue.setdefault(basenet, set()).add(network)
306 elif network.version == 6 and network.prefixlen > 124:
307 basenet = IPNetwork(
308 "%s/124" %
309 IPNetwork("%s/124" % network.network).network)
310 rfc2317_glue.setdefault(basenet, set()).add(network)
290 for subnet in subnets:311 for subnet in subnets:
291 network = IPNetwork(subnet.cidr)312 network = IPNetwork(subnet.cidr)
292 nodegroups = set(313 nodegroups = set(
@@ -333,12 +354,27 @@
333 })354 })
334355
335 # Use the default_domain as the name for the NS host in the reverse356 # Use the default_domain as the name for the NS host in the reverse
336 # zones.357 # zones. If this network is actually a parent rfc2317 glue
358 # network, then we need to generate the glue records.
359 if network in rfc2317_glue:
360 glue = rfc2317_glue[network]
361 del(rfc2317_glue[network])
362 else:
363 glue = set()
337 yield DNSReverseZoneConfig(364 yield DNSReverseZoneConfig(
338 Domain.objects.get_default_domain().name, serial=serial,365 Domain.objects.get_default_domain().name, serial=serial,
339 default_ttl=default_ttl,366 default_ttl=default_ttl,
340 mapping=mapping, network=network,367 mapping=mapping, network=network,
341 dynamic_ranges=dynamic_ranges,368 dynamic_ranges=dynamic_ranges,
369 rfc2317_ranges=glue,
370 )
371 # Now provide any remaining rfc2317 glue networks.
372 for network, ranges in rfc2317_glue.items():
373 yield DNSReverseZoneConfig(
374 Domain.objects.get_default_domain().name, serial=serial,
375 default_ttl=default_ttl,
376 network=network,
377 rfc2317_ranges=ranges,
342 )378 )
343379
344 def __iter__(self):380 def __iter__(self):
345381
=== modified file 'src/maasserver/enum.py'
--- src/maasserver/enum.py 2016-01-20 00:07:16 +0000
+++ src/maasserver/enum.py 2016-01-29 18:06:17 +0000
@@ -27,6 +27,9 @@
27 'PARTITION_TABLE_TYPE',27 'PARTITION_TABLE_TYPE',
28 'PARTITION_TABLE_TYPE_CHOICES',28 'PARTITION_TABLE_TYPE_CHOICES',
29 'PRESEED_TYPE',29 'PRESEED_TYPE',
30 'RDNS_MODE',
31 'RDNS_MODE_CHOICES',
32 'RDNS_MODE_CHOICES_DICT',
30 'USERDATA_TYPE',33 'USERDATA_TYPE',
31 ]34 ]
3235
@@ -221,6 +224,29 @@
221 OrderedDict(NODEGROUPINTERFACE_MANAGEMENT_CHOICES))224 OrderedDict(NODEGROUPINTERFACE_MANAGEMENT_CHOICES))
222225
223226
227class RDNS_MODE:
228 """The vocabulary of a `Subnet`'s possible reverse DNS modes."""
229 # By default, we do what we've always done: assume we rule the DNS world.
230 DEFAULT = 2
231 #: Do not generate reverse DNS for this Subnet.
232 DISABLED = 0
233 #: Generate reverse DNS only for the CIDR.
234 ENABLED = 1
235 #: Generate RFC2317 glue if needed (Subnet is too small for its own zone.)
236 RFC2317 = 2
237
238
239# Django choices for RDNS_MODE: sequence of tuples (key, UI representation.)
240RDNS_MODE_CHOICES = (
241 (RDNS_MODE.DISABLED, "Disabled"),
242 (RDNS_MODE.ENABLED, "Enabled"),
243 (RDNS_MODE.RFC2317, "Enabled, with rfc2317 glue zone."),
244)
245
246
247RDNS_MODE_CHOICES_DICT = OrderedDict(RDNS_MODE_CHOICES)
248
249
224class IPADDRESS_FAMILY:250class IPADDRESS_FAMILY:
225 """The vocabulary of possible IP family for `StaticIPAddress`."""251 """The vocabulary of possible IP family for `StaticIPAddress`."""
226 IPv4 = 4252 IPv4 = 4
227253
=== modified file 'src/maasserver/forms_subnet.py'
--- src/maasserver/forms_subnet.py 2015-12-23 17:21:20 +0000
+++ src/maasserver/forms_subnet.py 2016-01-29 18:06:17 +0000
@@ -8,6 +8,7 @@
8]8]
99
10from django import forms10from django import forms
11from maasserver.enum import RDNS_MODE_CHOICES
11from maasserver.fields import IPListFormField12from maasserver.fields import IPListFormField
12from maasserver.forms import MAASModelForm13from maasserver.forms import MAASModelForm
13from maasserver.models.fabric import Fabric14from maasserver.models.fabric import Fabric
@@ -33,6 +34,9 @@
33 space = forms.ModelChoiceField(34 space = forms.ModelChoiceField(
34 queryset=Space.objects.all(), required=False)35 queryset=Space.objects.all(), required=False)
3536
37 rdns_mode = forms.ChoiceField(
38 choices=RDNS_MODE_CHOICES, required=False)
39
36 class Meta:40 class Meta:
37 model = Subnet41 model = Subnet
38 fields = (42 fields = (
@@ -42,6 +46,7 @@
42 'cidr',46 'cidr',
43 'gateway_ip',47 'gateway_ip',
44 'dns_servers',48 'dns_servers',
49 'rdns_mode',
45 )50 )
4651
47 def __init__(self, *args, **kwargs):52 def __init__(self, *args, **kwargs):
4853
=== added file 'src/maasserver/migrations/builtin/maasserver/0023_add_rdns_mode.py'
--- src/maasserver/migrations/builtin/maasserver/0023_add_rdns_mode.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/builtin/maasserver/0023_add_rdns_mode.py 2016-01-29 18:06:17 +0000
@@ -0,0 +1,22 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import (
5 migrations,
6 models,
7)
8
9
10class Migration(migrations.Migration):
11
12 dependencies = [
13 ('maasserver', '0022_create_zone_serial_sequence'),
14 ]
15
16 operations = [
17 migrations.AddField(
18 model_name='subnet',
19 name='rdns_mode',
20 field=models.IntegerField(choices=[(0, 'Disabled'), (1, 'Enabled'), (2, 'Enabled, with rfc2317 glue zone.')], default=2),
21 ),
22 ]
023
=== modified file 'src/maasserver/models/subnet.py'
--- src/maasserver/models/subnet.py 2015-12-23 17:21:20 +0000
+++ src/maasserver/models/subnet.py 2016-01-29 18:06:17 +0000
@@ -18,6 +18,7 @@
18from django.db.models import (18from django.db.models import (
19 CharField,19 CharField,
20 ForeignKey,20 ForeignKey,
21 IntegerField,
21 Manager,22 Manager,
22 PROTECT,23 PROTECT,
23 Q,24 Q,
@@ -28,6 +29,8 @@
28from maasserver.enum import (29from maasserver.enum import (
29 NODEGROUP_STATUS,30 NODEGROUP_STATUS,
30 NODEGROUPINTERFACE_MANAGEMENT,31 NODEGROUPINTERFACE_MANAGEMENT,
32 RDNS_MODE,
33 RDNS_MODE_CHOICES,
31)34)
32from maasserver.fields import (35from maasserver.fields import (
33 CIDRField,36 CIDRField,
@@ -347,6 +350,10 @@
347 cidr = CIDRField(350 cidr = CIDRField(
348 blank=False, unique=True, editable=True, null=False)351 blank=False, unique=True, editable=True, null=False)
349352
353 rdns_mode = IntegerField(
354 choices=RDNS_MODE_CHOICES, editable=True,
355 default=RDNS_MODE.DEFAULT)
356
350 gateway_ip = MAASIPAddressField(blank=True, editable=True, null=True)357 gateway_ip = MAASIPAddressField(blank=True, editable=True, null=True)
351358
352 dns_servers = ArrayField(359 dns_servers = ArrayField(
353360
=== modified file 'src/maasserver/models/tests/test_subnet.py'
--- src/maasserver/models/tests/test_subnet.py 2015-12-01 18:12:59 +0000
+++ src/maasserver/models/tests/test_subnet.py 2016-01-29 18:06:17 +0000
@@ -17,6 +17,7 @@
17 NODE_PERMISSION,17 NODE_PERMISSION,
18 NODEGROUP_STATUS,18 NODEGROUP_STATUS,
19 NODEGROUPINTERFACE_MANAGEMENT,19 NODEGROUPINTERFACE_MANAGEMENT,
20 RDNS_MODE_CHOICES,
20)21)
21from maasserver.models.subnet import (22from maasserver.models.subnet import (
22 create_cidr,23 create_cidr,
@@ -417,14 +418,16 @@
417 dns_servers = [418 dns_servers = [
418 factory.make_ip_address()419 factory.make_ip_address()
419 for _ in range(random.randint(1, 3))]420 for _ in range(random.randint(1, 3))]
421 rdns_mode = factory.pick_choice(RDNS_MODE_CHOICES)
420 subnet = Subnet(422 subnet = Subnet(
421 name=name, vlan=vlan, cidr=cidr, gateway_ip=gateway_ip,423 name=name, vlan=vlan, cidr=cidr, gateway_ip=gateway_ip,
422 space=space, dns_servers=dns_servers)424 space=space, dns_servers=dns_servers, rdns_mode=rdns_mode)
423 subnet.save()425 subnet.save()
424 subnet_from_db = Subnet.objects.get(name=name)426 subnet_from_db = Subnet.objects.get(name=name)
425 self.assertThat(subnet_from_db, MatchesStructure.byEquality(427 self.assertThat(subnet_from_db, MatchesStructure.byEquality(
426 name=name, vlan=vlan, cidr=cidr, space=space,428 name=name, vlan=vlan, cidr=cidr, space=space,
427 gateway_ip=gateway_ip, dns_servers=dns_servers))429 gateway_ip=gateway_ip, dns_servers=dns_servers,
430 rdns_mode=rdns_mode))
428431
429 def test_validates_gateway_ip(self):432 def test_validates_gateway_ip(self):
430 error = self.assertRaises(433 error = self.assertRaises(
431434
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2016-01-27 17:44:54 +0000
+++ src/maasserver/testing/factory.py 2016-01-29 18:06:17 +0000
@@ -37,6 +37,7 @@
37 NODEGROUPINTERFACE_MANAGEMENT,37 NODEGROUPINTERFACE_MANAGEMENT,
38 PARTITION_TABLE_TYPE,38 PARTITION_TABLE_TYPE,
39 POWER_STATE,39 POWER_STATE,
40 RDNS_MODE,
40)41)
41from maasserver.fields import (42from maasserver.fields import (
42 LargeObjectFile,43 LargeObjectFile,
@@ -832,7 +833,7 @@
832833
833 def make_Subnet(self, name=None, vlan=None, space=None, cidr=None,834 def make_Subnet(self, name=None, vlan=None, space=None, cidr=None,
834 gateway_ip=None, dns_servers=None, host_bits=None,835 gateway_ip=None, dns_servers=None, host_bits=None,
835 fabric=None, vid=None):836 fabric=None, vid=None, rdns_mode=RDNS_MODE.DEFAULT):
836 if name is None:837 if name is None:
837 name = factory.make_name('name')838 name = factory.make_name('name')
838 if vlan is None:839 if vlan is None:
@@ -849,7 +850,7 @@
849 self.make_ip_address() for _ in range(random.randint(1, 3))]850 self.make_ip_address() for _ in range(random.randint(1, 3))]
850 subnet = Subnet(851 subnet = Subnet(
851 name=name, vlan=vlan, cidr=cidr, gateway_ip=gateway_ip,852 name=name, vlan=vlan, cidr=cidr, gateway_ip=gateway_ip,
852 space=space, dns_servers=dns_servers)853 space=space, dns_servers=dns_servers, rdns_mode=rdns_mode)
853 subnet.save()854 subnet.save()
854 return subnet855 return subnet
855856
856857
=== modified file 'src/maasserver/websockets/handlers/tests/test_subnet.py'
--- src/maasserver/websockets/handlers/tests/test_subnet.py 2015-12-01 18:12:59 +0000
+++ src/maasserver/websockets/handlers/tests/test_subnet.py 2016-01-29 18:06:17 +0000
@@ -25,6 +25,7 @@
25 "dns_servers": [server for server in subnet.dns_servers],25 "dns_servers": [server for server in subnet.dns_servers],
26 "vlan": subnet.vlan_id,26 "vlan": subnet.vlan_id,
27 "space": subnet.space_id,27 "space": subnet.space_id,
28 "rdns_mode": subnet.rdns_mode,
28 "cidr": subnet.cidr,29 "cidr": subnet.cidr,
29 "gateway_ip": subnet.gateway_ip,30 "gateway_ip": subnet.gateway_ip,
30 }31 }
3132
=== modified file 'src/provisioningserver/dns/tests/test_zoneconfig.py'
--- src/provisioningserver/dns/tests/test_zoneconfig.py 2016-01-19 22:34:22 +0000
+++ src/provisioningserver/dns/tests/test_zoneconfig.py 2016-01-29 18:06:17 +0000
@@ -359,7 +359,8 @@
359 factory.make_string(): factory.pick_ip_in_network(network),359 factory.make_string(): factory.pick_ip_in_network(network),
360 }360 }
361 expected = [361 expected = [
362 (IPAddress(ip).reverse_dns, 30, '%s.%s.' % (hostname, name))362 (IPAddress(ip).reverse_dns.split('.')[0], 30,
363 '%s.%s.' % (hostname, name))
363 for hostname, ip in hosts.items()364 for hostname, ip in hosts.items()
364 ]365 ]
365 mapping = {366 mapping = {
@@ -378,7 +379,8 @@
378 factory.make_string(): factory.pick_ip_in_network(network),379 factory.make_string(): factory.pick_ip_in_network(network),
379 }380 }
380 expected = [381 expected = [
381 (IPAddress(ip).reverse_dns, 30, '%s.%s.' % (hostname, name))382 (IPAddress(ip).reverse_dns.split('.')[0], 30,
383 '%s.%s.' % (hostname, name))
382 for hostname, ip in in_network_mapping.items()384 for hostname, ip in in_network_mapping.items()
383 ]385 ]
384 mapping = {386 mapping = {
385387
=== modified file 'src/provisioningserver/dns/zoneconfig.py'
--- src/provisioningserver/dns/zoneconfig.py 2016-01-19 22:34:22 +0000
+++ src/provisioningserver/dns/zoneconfig.py 2016-01-29 18:06:17 +0000
@@ -327,10 +327,13 @@
327 :param default_ttl: The default TTL for the zone.327 :param default_ttl: The default TTL for the zone.
328 :param network: The network that the mapping exists within.328 :param network: The network that the mapping exists within.
329 :type network: :class:`netaddr.IPNetwork`329 :type network: :class:`netaddr.IPNetwork`
330 :param rfc2317_ranges: List of ranges to generate RFC2317 CNAMEs for
331 :type rfc2317_ranges: [:class:`netaddr.IPNetwork`]
330 """332 """
331 self._mapping = kwargs.pop('mapping', {})333 self._mapping = kwargs.pop('mapping', {})
332 self._network = kwargs.pop("network", None)334 self._network = kwargs.pop("network", None)
333 self._dynamic_ranges = kwargs.pop('dynamic_ranges', [])335 self._dynamic_ranges = kwargs.pop('dynamic_ranges', [])
336 self._rfc2317_ranges = kwargs.pop('rfc2317_ranges', [])
334 zone_info = self.compose_zone_info(self._network)337 zone_info = self.compose_zone_info(self._network)
335 super(DNSReverseZoneConfig, self).__init__(338 super(DNSReverseZoneConfig, self).__init__(
336 domain, zone_info=zone_info, **kwargs)339 domain, zone_info=zone_info, **kwargs)
@@ -428,13 +431,23 @@
428 :param mapping: A hostname: (ttl, [ip-addresseses]) mapping for all431 :param mapping: A hostname: (ttl, [ip-addresseses]) mapping for all
429 known hosts in the reverse zone, to their FQDN (without trailing432 known hosts in the reverse zone, to their FQDN (without trailing
430 dot).433 dot).
431 :param network: DNS Zone's network.434 :param network: DNS Zone's network. (Not a supernet.)
432 :type network: :class:`netaddr.IPNetwork`435 :type network: :class:`netaddr.IPNetwork`
433 """436 """
437 def short_name(ip, network):
438 long_name = IPAddress(ip).reverse_dns
439 if network.version == 4:
440 short_name = ".".join(long_name.split('.')[
441 :(31 - network.prefixlen) // 8 + 1])
442 else:
443 short_name = ".".join(long_name.split('.')[
444 :(127 - network.prefixlen) // 4 + 1])
445 return short_name
446
434 if mapping is None:447 if mapping is None:
435 return ()448 return ()
436 return (449 return (
437 (IPAddress(ip).reverse_dns, ttl, '%s.' % (hostname))450 (short_name(ip, network), ttl, '%s.' % (hostname))
438 for hostname, ttl, ip in enumerate_mapping(mapping)451 for hostname, ttl, ip in enumerate_mapping(mapping)
439 # Filter out the IP addresses that are not in `network`.452 # Filter out the IP addresses that are not in `network`.
440 if IPAddress(ip) in network453 if IPAddress(ip) in network
@@ -479,6 +492,32 @@
479 return sorted(492 return sorted(
480 generate_directives, key=lambda directive: directive[2])493 generate_directives, key=lambda directive: directive[2])
481494
495 @classmethod
496 def get_rfc2317_GENERATE_directives(cls, network, rfc2317_ranges, domain):
497 """Return the GENERATE directives for any needed rfc2317 glue."""
498 # A non-empty rfc2317_ranges means that the network is the most
499 # specific that it can be (IPv4/24 or IPv6/124), so we can make some
500 # simplifications in the GENERATE directives.
501 generate_directives = set()
502 for subnet in rfc2317_ranges:
503 if network.version == 4:
504 iterator = "%d-%d" % (
505 (subnet.first & 0x000000ff),
506 (subnet.last & 0x000000ff))
507 hostname = "$.%d-%d" % (
508 (subnet.first & 0x000000ff),
509 subnet.prefixlen)
510 generate_directives.add((iterator, '$', hostname))
511 else:
512 iterator = "%x-%d" % (
513 (subnet.first & 0x0000000f),
514 (subnet.last & 0x0000000f))
515 hostname = "${0,1,x}.%x-%d" % (
516 (subnet.first & 0x0000000f),
517 subnet.prefixlen)
518 generate_directives.add((iterator, '${0,1,x}', hostname))
519 return sorted(generate_directives)
520
482 def write_config(self):521 def write_config(self):
483 """Write the zone file."""522 """Write the zone file."""
484 # Create GENERATE directives for IPv4 ranges.523 # Create GENERATE directives for IPv4 ranges.
@@ -502,6 +541,10 @@
502 'other_mapping': [],541 'other_mapping': [],
503 'generate_directives': {542 'generate_directives': {
504 'PTR': generate_directives,543 'PTR': generate_directives,
544 'CNAME': self.get_rfc2317_GENERATE_directives(
545 zi.subnetwork,
546 self._rfc2317_ranges,
547 self.domain),
505 }548 }
506 }549 }
507 )550 )