132
136
(ip, [(hostname, )]),
133
137
(dns.get_dns_server_address(nodegroup), resolver.extract_args()))
139
def test_warn_loopback_warns_about_IPv4_loopback(self):
140
logger = self.patch(dns, 'logger')
141
loopback = '127.0.0.1'
142
dns.warn_loopback(loopback)
144
logger.warn, MockCalledOnceWith(dns.WARNING_MESSAGE % loopback))
146
def test_warn_loopback_warns_about_any_IPv4_loopback(self):
147
logger = self.patch(dns, 'logger')
148
loopback = '127.254.100.99'
149
dns.warn_loopback(loopback)
150
self.assertThat(logger.warn, MockCalledOnceWith(ANY))
152
def test_warn_loopback_warns_about_IPv6_loopback(self):
153
logger = self.patch(dns, 'logger')
155
dns.warn_loopback(loopback)
156
self.assertThat(logger.warn, MockCalledOnceWith(ANY))
158
def test_warn_loopback_does_not_warn_about_sensible_IPv4(self):
159
logger = self.patch(dns, 'logger')
160
dns.warn_loopback('10.1.2.3')
161
self.assertThat(logger.warn, MockNotCalled())
163
def test_warn_loopback_does_not_warn_about_sensible_IPv6(self):
164
logger = self.patch(dns, 'logger')
165
dns.warn_loopback('1::9')
166
self.assertThat(logger.warn, MockNotCalled())
136
169
class TestLazyDict(TestCase):
137
170
"""Tests for `lazydict`."""
164
197
self.assertEqual({key1: key1, key2: key2}, value_dict)
167
class TestDNSConfigModifications(MAASServerTestCase):
170
("celery", FixtureResource(CeleryFixture())),
200
class TestGetHostnameIPMapping(MAASServerTestCase):
201
"""Test for `get_hostname_ip_mapping`."""
203
def test_get_hostname_ip_mapping_combines_mappings(self):
204
nodegroup = factory.make_node_group()
205
# Create dynamic mapping for an allocated node.
206
node1 = factory.make_node(
207
nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED)
208
mac = factory.make_mac_address(node=node1)
209
lease = factory.make_dhcp_lease(
210
nodegroup=nodegroup, mac=mac.mac_address)
211
# Create static mapping for an allocated node.
212
node2 = factory.make_node_with_mac_attached_to_nodegroupinterface(
214
staticip = factory.make_staticipaddress(mac=node2.get_primary_mac())
217
node1.hostname: lease.ip,
218
node2.hostname: staticip.ip,
221
expected_mapping, dns.get_hostname_ip_mapping(nodegroup))
223
def test_get_hostname_ip_mapping_gives_precedence_to_static_mappings(self):
224
nodegroup = factory.make_node_group()
225
# Create dynamic mapping for an allocated node.
226
node = factory.make_node(
227
nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED)
228
mac = factory.make_mac_address(node=node)
229
factory.make_dhcp_lease(
230
nodegroup=nodegroup, mac=mac.mac_address)
231
# Create static mapping for the *same* node.
232
staticip = factory.make_staticipaddress(mac=node.get_primary_mac())
235
node.hostname: staticip.ip,
238
expected_mapping, dns.get_hostname_ip_mapping(nodegroup))
241
class TestDNSServer(MAASServerTestCase):
242
"""A base class to perform real-world DNS-related tests.
244
The class starts a BINDServer for every test and provides a set of
245
helper methods to perform DNS queries.
247
Because of the overhead added by starting and stopping the DNS
248
server, new tests in this class and its descendants are expensive.
174
super(TestDNSConfigModifications, self).setUp()
252
super(TestDNSServer, self).setUp()
175
253
self.bind = self.useFixture(BINDServer())
176
254
self.patch(conf, 'DNS_CONFIG_DIR', self.bind.config.homedir)
190
268
self.bind.runner.rndc('reload')
192
def create_managed_nodegroup(self):
270
def create_managed_nodegroup(self, network=None):
272
network = IPNetwork('192.168.0.1/24')
193
273
return factory.make_node_group(
194
network=IPNetwork('192.168.0.1/24'),
195
275
status=NODEGROUP_STATUS.ACCEPTED,
196
276
management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
198
def create_nodegroup_with_lease(self, lease_number=1, nodegroup=None):
278
def create_nodegroup_with_static_ip(self, lease_number=1, nodegroup=None):
199
279
if nodegroup is None:
200
280
nodegroup = self.create_managed_nodegroup()
201
281
[interface] = nodegroup.get_managed_interfaces()
202
282
node = factory.make_node(
203
283
nodegroup=nodegroup)
204
mac = factory.make_mac_address(node=node)
205
ips = IPRange(interface.ip_range_low, interface.ip_range_high)
206
lease_ip = unicode(islice(ips, lease_number, lease_number + 1).next())
207
lease = factory.make_dhcp_lease(
208
nodegroup=nodegroup, mac=mac.mac_address, ip=lease_ip)
209
# Simulate that this lease was created by
210
# DHCPLease.objects.update_leases: update its DNS config.
284
mac = factory.make_mac_address(node=node, cluster_interface=interface)
286
interface.static_ip_range_low, interface.static_ip_range_high)
287
static_ip = unicode(islice(ips, lease_number, lease_number + 1).next())
288
staticaddress = factory.make_staticipaddress(ip=static_ip, mac=mac)
211
289
dns.change_dns_zones([nodegroup])
212
return nodegroup, node, lease
290
return nodegroup, node, staticaddress
214
def dig_resolve(self, fqdn):
292
def dig_resolve(self, fqdn, version=4):
215
293
"""Resolve `fqdn` using dig. Returns a list of results."""
294
# Using version=6 has two effects:
295
# - it changes the type of query from 'A' to 'AAAA';
296
# - it forces dig to only use IPv6 query transport.
297
record_type = 'AAAA' if version == 6 else 'A'
298
commands = [fqdn, '+short', '-%i' % version, record_type]
217
300
port=self.bind.config.port,
218
commands=[fqdn, '+short']).split('\n')
301
commands=commands).split('\n')
220
def dig_reverse_resolve(self, ip):
303
def dig_reverse_resolve(self, ip, version=4):
221
304
"""Reverse resolve `ip` using dig. Returns a list of results."""
223
306
port=self.bind.config.port,
224
commands=['-x', ip, '+short']).split('\n')
307
commands=['-x', ip, '+short', '-%i' % version]).split('\n')
226
def assertDNSMatches(self, hostname, domain, ip):
309
def assertDNSMatches(self, hostname, domain, ip, version=4):
310
# A forward lookup on the hostname returns the IP address.
227
311
fqdn = "%s.%s" % (hostname, domain)
228
autogenerated_hostname = '%s.' % generated_hostname(ip, domain)
229
forward_lookup_result = self.dig_resolve(fqdn)
230
if '%s.' % fqdn == autogenerated_hostname:
231
# If the fqdn is an autogenerated hostname, it resolves to the IP
232
# address (A record).
233
expected_results = [ip]
235
# If the fqdn is a custom hostname, it resolves to the
236
# autogenerated hostname (CNAME record) and the IP address
238
expected_results = [autogenerated_hostname, ip]
312
forward_lookup_result = self.dig_resolve(fqdn, version=version)
239
313
self.assertEqual(
240
expected_results, forward_lookup_result,
314
[ip], forward_lookup_result,
241
315
"Failed to resolve '%s' (results: '%s')." % (
242
316
fqdn, ','.join(forward_lookup_result)))
243
# A reverse lookup on the IP returns the autogenerated
245
reverse_lookup_result = self.dig_reverse_resolve(ip)
317
# A reverse lookup on the IP address returns the hostname.
318
reverse_lookup_result = self.dig_reverse_resolve(
246
320
self.assertEqual(
247
[autogenerated_hostname], reverse_lookup_result,
321
["%s." % fqdn], reverse_lookup_result,
248
322
"Failed to reverse resolve '%s' (results: '%s')." % (
249
323
fqdn, ','.join(reverse_lookup_result)))
326
class TestDNSConfigModifications(TestDNSServer):
251
328
def test_add_zone_loads_dns_zone(self):
252
nodegroup, node, lease = self.create_nodegroup_with_lease()
329
nodegroup, node, static = self.create_nodegroup_with_static_ip()
253
330
self.patch(settings, 'DNS_CONNECT', True)
254
331
dns.add_zone(nodegroup)
255
self.assertDNSMatches(node.hostname, nodegroup.name, lease.ip)
332
self.assertDNSMatches(node.hostname, nodegroup.name, static.ip)
257
334
def test_change_dns_zone_changes_dns_zone(self):
258
nodegroup, _, _ = self.create_nodegroup_with_lease()
335
nodegroup, _, _ = self.create_nodegroup_with_static_ip()
259
336
self.patch(settings, 'DNS_CONNECT', True)
260
337
dns.write_full_dns_config()
261
nodegroup, new_node, new_lease = (
262
self.create_nodegroup_with_lease(
338
nodegroup, new_node, new_static = (
339
self.create_nodegroup_with_static_ip(
263
340
nodegroup=nodegroup, lease_number=2))
264
341
dns.change_dns_zones(nodegroup)
265
self.assertDNSMatches(new_node.hostname, nodegroup.name, new_lease.ip)
342
self.assertDNSMatches(new_node.hostname, nodegroup.name, new_static.ip)
267
344
def test_is_dns_enabled_return_false_if_DNS_CONNECT_False(self):
268
345
self.patch(settings, 'DNS_CONNECT', False)
329
406
port=self.bind.config.port, commands=[ns_record, '+short'])
330
407
self.assertEqual(ip, ip_of_ns_record)
332
def test_add_nodegroup_creates_DNS_zone(self):
333
self.patch(settings, "DNS_CONNECT", True)
334
network = IPNetwork('192.168.7.1/24')
335
ip = factory.getRandomIPInNetwork(network)
336
nodegroup = factory.make_node_group(
337
network=network, status=NODEGROUP_STATUS.ACCEPTED,
338
management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
339
self.assertDNSMatches(generated_hostname(ip), nodegroup.name, ip)
341
409
def test_edit_nodegroupinterface_updates_DNS_zone(self):
342
410
self.patch(settings, "DNS_CONNECT", True)
343
411
old_network = IPNetwork('192.168.7.1/24')
344
old_ip = factory.getRandomIPInNetwork(old_network)
345
412
nodegroup = factory.make_node_group(
346
413
network=old_network, status=NODEGROUP_STATUS.ACCEPTED,
347
414
management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
348
415
[interface] = nodegroup.get_managed_interfaces()
416
_, node, lease = self.create_nodegroup_with_static_ip(
419
["%s." % node.fqdn], self.dig_reverse_resolve(lease.ip))
349
420
# Edit nodegroup's network information to '192.168.44.1/24'
350
421
interface.ip = '192.168.44.7'
351
422
interface.router_ip = '192.168.44.14'
352
423
interface.broadcast_ip = '192.168.44.255'
353
424
interface.netmask = '255.255.255.0'
354
425
interface.ip_range_low = '192.168.44.0'
355
interface.ip_range_high = '192.168.44.255'
426
interface.ip_range_high = '192.168.44.128'
427
interface.static_ip_range_low = '192.168.44.129'
428
interface.static_ip_range_high = '192.168.44.255'
357
ip = factory.getRandomIPInNetwork(IPNetwork('192.168.44.1/24'))
358
# The ip from the old network does not resolve anymore.
359
self.assertEqual([''], self.dig_resolve(generated_hostname(old_ip)))
360
self.assertEqual([''], self.dig_reverse_resolve(old_ip))
361
# The ip from the new network resolves.
362
self.assertDNSMatches(generated_hostname(ip), nodegroup.name, ip)
430
# The IP from the old network does not resolve anymore.
431
self.assertEqual([''], self.dig_reverse_resolve(lease.ip))
432
# A lease in the new network resolves.
433
_, node, lease = self.create_nodegroup_with_static_ip(
436
IPAddress(lease.ip) in interface.network,
437
"The lease IP Address is not in the new network")
439
["%s." % node.fqdn], self.dig_reverse_resolve(lease.ip))
364
441
def test_changing_interface_management_updates_DNS_zone(self):
365
442
self.patch(settings, "DNS_CONNECT", True)
382
458
network=network, status=NODEGROUP_STATUS.ACCEPTED,
383
459
management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
384
460
nodegroup.delete()
385
self.assertEqual([''], self.dig_resolve(generated_hostname(ip)))
386
461
self.assertEqual([''], self.dig_reverse_resolve(ip))
388
463
def test_add_node_updates_zone(self):
389
464
self.patch(settings, "DNS_CONNECT", True)
390
nodegroup, node, lease = self.create_nodegroup_with_lease()
391
self.assertDNSMatches(node.hostname, nodegroup.name, lease.ip)
465
nodegroup, node, static = self.create_nodegroup_with_static_ip()
466
self.assertDNSMatches(node.hostname, nodegroup.name, static.ip)
393
468
def test_delete_node_updates_zone(self):
394
469
self.patch(settings, "DNS_CONNECT", True)
395
nodegroup, node, lease = self.create_nodegroup_with_lease()
470
nodegroup, node, static = self.create_nodegroup_with_static_ip()
396
471
# Prevent omshell task dispatch.
397
472
self.patch(node_module, "remove_dhcp_host_map")
416
491
self.assertEqual(0, recorder.call_count)
419
def forward_zone(domain, *networks):
494
class TestDNSBackwardCompat(TestDNSServer):
495
"""Allocated nodes with IP addresses in the dynamic range get a DNS
499
def test_bind_configuration_includes_dynamic_ips_of_allocated_nodes(self):
500
self.patch(settings, "DNS_CONNECT", True)
501
network = IPNetwork('192.168.7.1/24')
502
nodegroup = self.create_managed_nodegroup(network=network)
503
[interface] = nodegroup.get_managed_interfaces()
504
node = factory.make_node(
505
nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED)
506
mac = factory.make_mac_address(node=node, cluster_interface=interface)
507
# Get an IP in the dynamic range.
509
interface.ip_range_low, interface.ip_range_high)
510
ip = "%s" % random.choice(ip_range)
511
lease = factory.make_dhcp_lease(
512
nodegroup=nodegroup, mac=mac.mac_address, ip=ip)
513
dns.change_dns_zones([nodegroup])
514
self.assertDNSMatches(node.hostname, nodegroup.name, lease.ip)
517
class TestIPv6DNS(TestDNSServer):
519
def test_bind_configuration_includes_ipv6_zone(self):
520
self.patch(settings, "DNS_CONNECT", True)
521
network = IPNetwork('fe80::/64')
522
nodegroup = self.create_managed_nodegroup(network=network)
523
nodegroup, node, static = self.create_nodegroup_with_static_ip(
525
self.assertDNSMatches(
526
node.hostname, nodegroup.name, static.ip, version=6)
529
def forward_zone(domain):
421
531
Returns a matcher for a :class:`DNSForwardZoneConfig` with the given
424
networks = {IPNetwork(network) for network in networks}
425
534
return MatchesAll(
426
535
IsInstance(DNSForwardZoneConfig),
427
MatchesStructure.byEquality(
428
domain=domain, _networks=networks))
536
MatchesStructure.byEquality(domain=domain))
431
539
def reverse_zone(domain, network):