def test_writes_reverse_dns_zone_config(self): target_dir = patch_dns_config_path(self) domain = factory.make_string() ns_host_name = factory.make_name("ns") network = IPNetwork('192.168.0.1/22') dynamic_network = IPNetwork('192.168.0.1/28') dns_zone_config = DNSReverseZoneConfig(domain, serial=random.randint(1, 100), network=network, ns_host_name=ns_host_name, dynamic_ranges=[ IPRange( dynamic_network.first, dynamic_network.last) ]) dns_zone_config.write_config() for sub in range(4): reverse_file_name = 'zone.%d.168.192.in-addr.arpa' % sub expected_GEN_direct = dns_zone_config.get_GENERATE_directives( dynamic_network, domain, DomainInfo(IPNetwork('192.168.%d.0/24' % sub), "%d.168.192.in-addr.arpa" % sub)) expected = ContainsAll(['30 IN NS %s.' % ns_host_name] + [ '$GENERATE %s %s IN PTR %s' % (iterator_values, reverse_dns, hostname) for iterator_values, reverse_dns, hostname in expected_GEN_direct ]) self.assertThat(os.path.join(target_dir, reverse_file_name), FileContains(matcher=expected))
def test_writes_reverse_dns_zone_config_for_small_network(self): target_dir = patch_dns_config_path(self) domain = factory.make_string() ns_host_name = factory.make_name("ns") network = IPNetwork("192.168.0.1/26") dynamic_network = IPNetwork("192.168.0.1/28") dns_zone_config = DNSReverseZoneConfig( domain, serial=random.randint(1, 100), network=network, ns_host_name=ns_host_name, dynamic_ranges=[ IPRange(dynamic_network.first, dynamic_network.last) ], ) dns_zone_config.write_config() reverse_zone_name = "0-26.0.168.192.in-addr.arpa" reverse_file_name = "zone.0-26.0.168.192.in-addr.arpa" expected_GEN_direct = dns_zone_config.get_GENERATE_directives( dynamic_network, domain, DomainInfo(network, reverse_zone_name)) expected = ContainsAll(["30 IN NS %s." % ns_host_name] + [ "$GENERATE %s %s IN PTR %s" % (iterator_values, reverse_dns, hostname) for iterator_values, reverse_dns, hostname in expected_GEN_direct ]) self.assertThat( os.path.join(target_dir, reverse_file_name), FileContains(matcher=expected), )
def test_sorts_output_by_hostname(self): network = IPNetwork("10.0.0.1/23") domain = factory.make_string() expected_hostname = "10-0-%s-$." + domain + "." expected_rdns = "$.%s.0.10.in-addr.arpa." directives = list( DNSReverseZoneConfig.get_GENERATE_directives( network, domain, DomainInfo(IPNetwork("10.0.0.0/24"), "0.0.10.in-addr.arpa"), )) self.expectThat( directives[0], Equals(("0-255", expected_rdns % "0", expected_hostname % "0")), ) expected_hostname = "10-0-%s-$." + domain + "." expected_rdns = "$.%s.0.10.in-addr.arpa." directives = list( DNSReverseZoneConfig.get_GENERATE_directives( network, domain, DomainInfo(IPNetwork("10.0.1.0/24"), "1.0.10.in-addr.arpa"), )) self.expectThat( directives[0], Equals(("0-255", expected_rdns % "1", expected_hostname % "1")), )
def test_reverse_config_file_is_world_readable(self): patch_dns_config_path(self) dns_zone_config = DNSReverseZoneConfig( factory.make_string(), serial=random.randint(1, 100), network=factory.make_ipv4_network()) dns_zone_config.write_config() for tgt in [zi.target_path for zi in dns_zone_config.zone_info]: filepath = FilePath(tgt) self.assertTrue(filepath.getPermissions().other.read)
def test_writes_dns_zone_config_with_NS_record(self): target_dir = patch_dns_config_path(self) network = factory.make_ipv4_network() ns_host_name = factory.make_name("ns") dns_zone_config = DNSReverseZoneConfig(factory.make_string(), serial=random.randint(1, 100), ns_host_name=ns_host_name, network=network) dns_zone_config.write_config() for zone_name in [zi.zone_name for zi in dns_zone_config.zone_info]: self.assertThat( os.path.join(target_dir, 'zone.%s' % zone_name), FileContains(matcher=Contains('30 IN NS %s.' % ns_host_name)))
def test_bind_write_configuration_writes_file(self): domain = factory.make_string() zones = [ DNSReverseZoneConfig(domain, serial=random.randint(1, 100), network=factory.make_ipv4_network()), DNSReverseZoneConfig(domain, serial=random.randint(1, 100), network=factory.make_ipv6_network()), ] actions.bind_write_configuration(zones=zones, trusted_networks=[]) self.assertThat(os.path.join(self.dns_conf_dir, MAAS_NAMED_CONF_NAME), FileExists())
def test_bind_write_zones_writes_file(self): domain = factory.make_string() network = IPNetwork("192.168.0.3/24") dns_ip_list = [factory.pick_ip_in_network(network)] ip = factory.pick_ip_in_network(network) ttl = random.randint(10, 1000) forward_zone = DNSForwardZoneConfig( domain, serial=random.randint(1, 100), mapping={ factory.make_string(): HostnameIPMapping(None, ttl, {ip}) }, dns_ip_list=dns_ip_list, ) reverse_zone = DNSReverseZoneConfig( domain, serial=random.randint(1, 100), network=network ) actions.bind_write_zones(zones=[forward_zone, reverse_zone]) forward_file_name = "zone.%s" % domain reverse_file_name = "zone.0.168.192.in-addr.arpa" expected_files = [ join(self.dns_conf_dir, forward_file_name), join(self.dns_conf_dir, reverse_file_name), ] self.assertThat(expected_files, AllMatch(FileExists()))
def test_get_ptr_mapping_drops_IPs_not_in_network(self): name = factory.make_string() network = IPNetwork("192.12.0.1/30") in_network_mapping = { factory.make_string(): factory.pick_ip_in_network(network), factory.make_string(): factory.pick_ip_in_network(network), } expected = [ ( IPAddress(ip).reverse_dns.split(".")[0], 30, "%s.%s." % (hostname, name), ) for hostname, ip in in_network_mapping.items() ] mapping = { "%s.%s" % (hostname, name): HostnameIPMapping(None, 30, [ip]) for hostname, ip in in_network_mapping.items() } extra_mapping = { factory.make_string(): HostnameIPMapping(None, 30, ["192.50.0.2"]), factory.make_string(): HostnameIPMapping(None, 30, ["192.70.0.2"]), } mapping.update(extra_mapping) self.assertItemsEqual( expected, DNSReverseZoneConfig.get_PTR_mapping(mapping, network) )
def test_returns_single_entry_for_weird_small_range(self): ip_range = IPRange('10.0.0.1', '10.0.0.255') domain = factory.make_string() directives = DNSReverseZoneConfig.get_GENERATE_directives( ip_range, domain, DomainInfo(IPNetwork('10.0.0.0/24'), '0.0.10.in-addr.arpa')) self.expectThat(directives, HasLength(1))
def test_computes_dns_config_file_paths_for_small_network(self): domain = factory.make_name('zone') reverse_file_name = 'zone.192-27.0.168.192.in-addr.arpa' dns_zone_config = DNSReverseZoneConfig( domain, network=IPNetwork("192.168.0.192/27")) self.assertEqual(1, len(dns_zone_config.zone_info)) self.assertEqual(os.path.join(get_dns_config_dir(), reverse_file_name), dns_zone_config.zone_info[0].target_path)
def test_ignores_generate_directives_for_v6_dynamic_ranges(self): patch_dns_config_path(self) domain = factory.make_string() network = IPNetwork("192.168.0.1/22") dynamic_network = IPNetwork("%s/64" % factory.make_ipv6_address()) dns_zone_config = DNSReverseZoneConfig( domain, serial=random.randint(1, 100), network=network, dynamic_ranges=[ IPRange(dynamic_network.first, dynamic_network.last) ], ) get_generate_directives = self.patch(dns_zone_config, "get_GENERATE_directives") dns_zone_config.write_config() self.assertThat(get_generate_directives, MockNotCalled())
def test_ignores_networks_that_span_slash_16s(self): # If the upper and lower bounds of a range span two /16 networks # (but contain between them no more than 65536 addresses), # get_GENERATE_directives() will return early ip_range = IPRange('10.0.0.55', '10.1.0.54') directives = DNSReverseZoneConfig.get_GENERATE_directives( ip_range, factory.make_string(), DomainInfo(IPNetwork('10.0.0.0/15'), "do not care")) self.assertEqual([], directives)
def test_ignores_network_larger_than_slash_16(self): network = IPNetwork("%s/15" % factory.make_ipv4_address()) self.assertEqual( [], DNSReverseZoneConfig.get_GENERATE_directives( network, factory.make_string(), DomainInfo(network, "do not care"), ), )
def test_returns_single_entry_for_slash_24_network(self): network = IPNetwork("%s/24" % factory.make_ipv4_address()) reverse = ".".join(IPAddress(network).reverse_dns.split(".")[1:-1]) domain = factory.make_string() expected_generate_directives = self.get_expected_generate_directives( network, domain) directives = DNSReverseZoneConfig.get_GENERATE_directives( network, domain, DomainInfo(network, reverse)) self.expectThat(directives, HasLength(1)) self.assertItemsEqual(expected_generate_directives, directives)
def test_computes_dns_config_file_paths(self): domain = factory.make_name('zone') reverse_file_name = [ 'zone.%d.168.192.in-addr.arpa' % i for i in range(4) ] dns_zone_config = DNSReverseZoneConfig( domain, network=IPNetwork("192.168.0.0/22")) for i in range(4): self.assertEqual( os.path.join(get_dns_config_dir(), reverse_file_name[i]), dns_zone_config.zone_info[i].target_path)
def test_returns_256_entries_for_slash_16_network(self): network = IPNetwork(factory.make_ipv4_network(slash=16)) reverse = IPAddress(network.first).reverse_dns.split(".")[2:-1] reverse = ".".join(reverse) domain = factory.make_string() expected_generate_directives = self.get_expected_generate_directives( network, domain) directives = DNSReverseZoneConfig.get_GENERATE_directives( network, domain, DomainInfo(network, reverse)) self.expectThat(directives, HasLength(256)) self.assertItemsEqual(expected_generate_directives, directives)
def test_fields(self): domain = factory.make_string() serial = random.randint(1, 200) network = factory.make_ipv4_network() dns_zone_config = DNSReverseZoneConfig(domain, serial=serial, network=network) self.assertThat( dns_zone_config, MatchesStructure.byEquality(domain=domain, serial=serial, _network=network), )
def test_get_ptr_mapping(self): name = factory.make_string() network = IPNetwork('192.12.0.1/30') hosts = { factory.make_string(): factory.pick_ip_in_network(network), factory.make_string(): factory.pick_ip_in_network(network), } expected = [(IPAddress(ip).reverse_dns.split('.')[0], 30, '%s.%s.' % (hostname, name)) for hostname, ip in hosts.items()] mapping = { "%s.%s" % (hostname, name): HostnameIPMapping(None, 30, {ip}) for hostname, ip in hosts.items() } self.assertItemsEqual( expected, DNSReverseZoneConfig.get_PTR_mapping(mapping, network))
def test_write_config_writes_config(self): target_dir = patch_dns_config_path(self) domain = factory.make_string() network = IPNetwork('192.168.0.3/24') ip = factory.pick_ip_in_network(network) forward_zone = DNSForwardZoneConfig( domain, mapping={factory.make_string(): ip}) reverse_zone = DNSReverseZoneConfig(domain, network=network) dnsconfig = DNSConfig((forward_zone, reverse_zone)) dnsconfig.write_config() self.assertThat( os.path.join(target_dir, MAAS_NAMED_CONF_NAME), FileContains(matcher=ContainsAll([ 'zone.%s' % domain, 'zone.0.168.192.in-addr.arpa', MAAS_NAMED_RNDC_CONF_NAME, ])))
def test_excplicitly(self): # The other tests in this TestCase rely on # get_expected_generate_directives(), which is quite dense. Here # we test get_GENERATE_directives() explicitly. ip_range = IPRange('192.168.0.55', '192.168.2.128') expected_directives = [ ("55-255", "$.0.168.192.in-addr.arpa.", "192-168-0-$.domain."), ("0-255", "$.1.168.192.in-addr.arpa.", "192-168-1-$.domain."), ("0-128", "$.2.168.192.in-addr.arpa.", "192-168-2-$.domain."), ] self.assertItemsEqual( expected_directives, DNSReverseZoneConfig.get_GENERATE_directives( ip_range, domain="domain", zone_info=DomainInfo(IPNetwork('192.168.0.0/16'), "168.192.in-addr.arpa")))
def test_no_overlap(self): expected = [ ( [( IPNetwork("10.232.32.0/21"), [IPNetwork("10.232.36.0/24")], )], [ { "excludes": [ "36.232.10.in-addr.arpa", ], "includes": [ "32.232.10.in-addr.arpa", ], }, ], ), ( [( IPNetwork("10.232.36.0/24"), [IPNetwork("10.232.32.0/21")], )], [ { "includes": [ "36.232.10.in-addr.arpa", ], }, ], ), ( [( IPNetwork("10.232.32.0/21"), [ IPNetwork("10.232.35.0/24"), IPNetwork("10.232.36.0/24"), ], )], [ { "excludes": [ "35.232.10.in-addr.arpa", "36.232.10.in-addr.arpa", ], "includes": [ "32.232.10.in-addr.arpa", ], }, ], ), ] for t_case in expected: nets = t_case[0] expected = t_case[1] domain = factory.make_name("zone") for (idx, net) in enumerate(nets): network = net[0] other_subnets = net[1] dns_zone_config = DNSReverseZoneConfig(domain, network=network, exclude=other_subnets) zone_names = set( [z.zone_name for z in dns_zone_config.zone_info]) for exclude in expected[idx].get("excludes", []): self.assertFalse(exclude in zone_names) for include in expected[idx].get("includes", []): self.assertTrue(include in zone_names)
def test_reverse_zone_file(self): # DNSReverseZoneConfig calculates the reverse zone file name # correctly for IPv4 and IPv6 networks. # As long as the network size ends on a "nice" boundary (multiple of # 8 for IPv4, multiple of 4 for IPv6), then there will be one reverse # zone for the subnet. When it isn't, then there will be 2-128 # individual reverse zones for the subnet. # A special case is the small subnet (less than 256 hosts for IPv4, # less than 16 hosts for IPv6), in which case, we follow RFC2317 with # the modern adjustment of using '-' instead of '/'. zn = "%d.0.0.0.0.0.0.0.0.0.0.0.4.0.1.f.1.0.8.a.b.0.1.0.0.2.ip6.arpa" expected = [ # IPv4 networks. # /22 ==> 4 /24 reverse zones ( IPNetwork("192.168.0.1/22"), [ DomainInfo( IPNetwork("192.168.%d.0/24" % i), "%d.168.192.in-addr.arpa" % i, ) for i in range(4) ], ), # /24 ==> 1 reverse zone ( IPNetwork("192.168.0.1/24"), [ DomainInfo(IPNetwork("192.168.0.0/24"), "0.168.192.in-addr.arpa") ], ), # /29 ==> 1 reverse zones, per RFC2317 ( IPNetwork("192.168.0.241/29"), [ DomainInfo( IPNetwork("192.168.0.240/29"), "240-29.0.168.192.in-addr.arpa", ) ], ), # IPv6 networks. # /32, 48, 56, 64 ==> 1 reverse zones ( IPNetwork("3ffe:801::/32"), [ DomainInfo(IPNetwork("3ffe:801::32"), "1.0.8.0.e.f.f.3.ip6.arpa") ], ), ( IPNetwork("2001:db8:0::/48"), [ DomainInfo( IPNetwork("2001:db8:0::/48"), "0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", ) ], ), ( IPNetwork("2001:ba8:1f1:400::/56"), [ DomainInfo( IPNetwork("2001:ba8:1f1:400::/56"), "4.0.1.f.1.0.8.a.b.0.1.0.0.2.ip6.arpa", ) ], ), ( IPNetwork("2610:8:6800:1::/64"), [ DomainInfo( IPNetwork("2610:8:6800:1::/64"), "1.0.0.0.0.0.8.6.8.0.0.0.0.1.6.2.ip6.arpa", ) ], ), # /2 with hex digits ==> 4 /4 reverse zones ( IPNetwork("8000::/2"), [ DomainInfo(IPNetwork("8000::/4"), "8.ip6.arpa"), DomainInfo(IPNetwork("9000::/4"), "9.ip6.arpa"), DomainInfo(IPNetwork("a000::/4"), "a.ip6.arpa"), DomainInfo(IPNetwork("b000::/4"), "b.ip6.arpa"), ], ), # /103 ==> 2 /104 reverse zones ( IPNetwork("2001:ba8:1f1:400::/103"), [ DomainInfo( IPNetwork("2001:ba8:1f1:400:0:0:%d00:0000/104" % i), zn % i, ) for i in range(2) ], ), # /125 ==> 1 reverse zone, based on RFC2317 ( IPNetwork("2001:ba8:1f1:400::/125"), [ DomainInfo( IPNetwork("2001:ba8:1f1:400::/125"), "0-125.%s" % (IPAddress("2001:ba8:1f1:400::").reverse_dns[2:-1]), ) ], ), ] results = [] for network, _ in expected: domain = factory.make_name("zone") dns_zone_config = DNSReverseZoneConfig(domain, network=network) results.append((network, dns_zone_config.zone_info)) # Make sure we have the right number of elements. self.assertEqual(len(expected), len(results)) # And that the zone names chosen for each element are correct. for net in range(len(expected)): for zi in range(len(expected[net][1])): self.assertItemsEqual( expected[net][1][zi].zone_name, results[net][1][zi].zone_name, )
def _gen_reverse_zones(subnets, serial, ns_host_name, mappings, default_ttl): """Generator of reverse zones, sorted by network.""" subnets = set(subnets) # Generate the list of parent networks for rfc2317 glue. Note that we # need to handle the case where we are controlling both the small net # and a bigger network containing the /24, not just a /24 network. rfc2317_glue = {} for subnet in subnets: network = IPNetwork(subnet.cidr) if subnet.rdns_mode == RDNS_MODE.RFC2317: # If this is a small subnet and we are doing RFC2317 glue for # it, then we need to combine that with any other such subnets # We need to know this before we start creating reverse DNS # zones. if network.version == 4 and network.prefixlen > 24: # Turn 192.168.99.32/29 into 192.168.99.0/24 basenet = IPNetwork( "%s/24" % IPNetwork("%s/24" % network.network).network) rfc2317_glue.setdefault(basenet, set()).add(network) elif network.version == 6 and network.prefixlen > 124: basenet = IPNetwork( "%s/124" % IPNetwork("%s/124" % network.network).network) rfc2317_glue.setdefault(basenet, set()).add(network) # Since get_hostname_ip_mapping(Subnet) ignores Subnet.id, so we can # just do it once and be happy. LP#1600259 if len(subnets): mappings['reverse'] = mappings[Subnet.objects.first()] # For each of the zones that we are generating (one or more per # subnet), compile the zone from: # 1. Dynamic ranges on this subnet. # 2. Node: ip mapping(subnet), including DNSResource records for # StaticIPAddresses in this subnet. # All of this needs to be done smallest to largest so that we can # correctly gather the rfc2317 glue that we need. Failure to sort # means that we wind up grabbing (and deleting) the rfc2317 glue info # while processing the wrong network. for subnet in sorted( subnets, key=lambda subnet: IPNetwork(subnet.cidr).prefixlen, reverse=True): network = IPNetwork(subnet.cidr) if subnet.rdns_mode == RDNS_MODE.DISABLED: # If we are not doing reverse dns for this subnet, then just # skip to the next subnet. logger.debug("%s disabled subnet in DNS config list" % subnet.cidr) continue # 1. Figure out the dynamic ranges. dynamic_ranges = [ ip_range.netaddr_iprange for ip_range in subnet.get_dynamic_ranges() ] # 2. Start with the map of all of the nodes, including all # DNSResource-associated addresses. We will prune this to just # entries for the subnet when we actually generate the zonefile. # If we get here, then we have subnets, so we noticed that above # and created mappings['reverse']. LP#1600259 mapping = mappings['reverse'] # Use the default_domain as the name for the NS host in the reverse # zones. If this network is actually a parent rfc2317 glue # network, then we need to generate the glue records. # We need to detect the need for glue in our networks that are # big. if ((network.version == 6 and network.prefixlen < 124) or network.prefixlen < 24): glue = set() # This is the reason for needing the subnets sorted in # increasing order of size. for net in rfc2317_glue.copy().keys(): if net in network: glue.update(rfc2317_glue[net]) del (rfc2317_glue[net]) elif network in rfc2317_glue: glue = rfc2317_glue[network] del (rfc2317_glue[network]) else: glue = set() yield DNSReverseZoneConfig( ns_host_name, serial=serial, default_ttl=default_ttl, ns_host_name=ns_host_name, mapping=mapping, network=IPNetwork(subnet.cidr), dynamic_ranges=dynamic_ranges, rfc2317_ranges=glue, ) # Now provide any remaining rfc2317 glue networks. for network, ranges in rfc2317_glue.items(): yield DNSReverseZoneConfig( ns_host_name, serial=serial, default_ttl=default_ttl, network=network, ns_host_name=ns_host_name, rfc2317_ranges=ranges, )