← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~julian-edwards/maas/backport-r1300 into lp:maas/1.2

 

Julian Edwards has proposed merging lp:~julian-edwards/maas/backport-r1300 into lp:maas/1.2.

Commit message:
Write the DNS config only if at least one of the interfaces is configured to manage DNS. (backport r1300 from trunk)

Requested reviews:
  MAAS Maintainers (maas-maintainers)
Related bugs:
  Bug #1067977 in MAAS: "write_full_dns_config runs on a fresh installation even before dns is installed or configured"
  https://bugs.launchpad.net/maas/+bug/1067977

For more details, see:
https://code.launchpad.net/~julian-edwards/maas/backport-r1300/+merge/137494
-- 
https://code.launchpad.net/~julian-edwards/maas/backport-r1300/+merge/137494
Your team MAAS Maintainers is requested to review the proposed merge of lp:~julian-edwards/maas/backport-r1300 into lp:maas/1.2.
=== modified file 'src/maasserver/dhcp_connect.py'
--- src/maasserver/dhcp_connect.py	2012-10-03 07:48:03 +0000
+++ src/maasserver/dhcp_connect.py	2012-12-03 07:32:19 +0000
@@ -31,7 +31,7 @@
     configure_dhcp(instance.nodegroup)
 
 
-def dhcp_post_edit_status_NodeGroup(instance, old_field):
+def dhcp_post_edit_status_NodeGroup(instance, old_field, **kwargs):
     """The status of a NodeGroup changed."""
     # This could be optimized a bit by detecting if the status change is
     # actually a change from 'do not manage DHCP' to 'manage DHCP'.

=== modified file 'src/maasserver/dns.py'
--- src/maasserver/dns.py	2012-10-01 16:34:43 +0000
+++ src/maasserver/dns.py	2012-12-03 07:32:19 +0000
@@ -37,6 +37,7 @@
 from maasserver.models import (
     DHCPLease,
     NodeGroup,
+    NodeGroupInterface,
     )
 from maasserver.sequence import (
     INT_MAX,
@@ -64,6 +65,14 @@
     return '%0.10d' % zone_serial.nextval()
 
 
+def is_dns_in_use():
+    """Is there at least one interface configured to manage DNS?"""
+    interfaces_with_dns = (
+        NodeGroupInterface.objects.filter(
+             management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS))
+    return interfaces_with_dns.exists()
+
+
 def is_dns_enabled():
     """Is MAAS configured to manage DNS?"""
     return settings.DNS_CONNECT
@@ -82,17 +91,18 @@
         interface.management == NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
 
 
+WARNING_MESSAGE = (
+    "The DNS server will use the address '%s',  which is inside the "
+    "loopback network.  This may not be a problem if you're not using "
+    "MAAS's DNS features or if you don't rely on this information.  "
+    "Be sure to configure the DEFAULT_MAAS_URL setting in MAAS's "
+    "/etc/maas/maas_local_settings.py.")
+
+
 def warn_loopback(ip):
     """Warn if the given IP address is in the loopback network."""
     if IPAddress(ip) in IPNetwork('127.0.0.1/8'):
-        logging.getLogger('maas').warn(
-            "The DNS server will use the address '%s',  which is inside the "
-            "loopback network.  This may not be a problem if you're not using "
-            "MAAS's DNS features or if you don't rely on this information.  "
-            "Be sure to configure the DEFAULT_MAAS_URL setting in MAAS's "
-            "settings.py (or demo.py/development.py if you are running a "
-            "development system)."
-            % ip)
+        logging.getLogger('maas').warn(WARNING_MESSAGE % ip)
 
 
 def get_dns_server_address():
@@ -251,7 +261,7 @@
         zone should be updated.
     :type nodegroups: list (or :class:`NodeGroup`)
     """
-    if not is_dns_enabled():
+    if not (is_dns_enabled() and is_dns_in_use()):
         return
     serial = next_zone_serial()
     for zone in ZoneGenerator(nodegroups, serial):
@@ -271,7 +281,7 @@
     :param nodegroup: The nodegroup for which the zone should be added.
     :type nodegroup: :class:`NodeGroup`
     """
-    if not is_dns_enabled():
+    if not (is_dns_enabled() and is_dns_in_use()):
         return
     zones_to_write = ZoneGenerator(nodegroup).as_list()
     if len(zones_to_write) == 0:
@@ -286,23 +296,21 @@
         zones=zones_to_write, callback=write_dns_config_subtask)
 
 
-def write_full_dns_config(active=True, reload_retry=False):
+def write_full_dns_config(reload_retry=False, force=False):
     """Write the DNS configuration.
 
-    :param active: If True, write the DNS config for all the nodegroups.
-        Otherwise write an empty DNS config (with no zones).  Defaults
-        to `True`.
-    :type active: bool
     :param reload_retry: Should the reload rndc command be retried in case
         of failure?  Defaults to `False`.
     :type reload_retry: bool
+    :param force: Write the configuration even if no interface is
+        configured to manage DNS.
+    :type force: bool
     """
-    if not is_dns_enabled():
+    write_conf = (
+        is_dns_enabled() and (force or is_dns_in_use()))
+    if not write_conf:
         return
-    if active:
-        zones = ZoneGenerator(NodeGroup.objects.all()).as_list()
-    else:
-        zones = []
+    zones = ZoneGenerator(NodeGroup.objects.all()).as_list()
     tasks.write_full_dns_config.delay(
         zones=zones,
         callback=tasks.rndc_command.subtask(

=== modified file 'src/maasserver/dns_connect.py'
--- src/maasserver/dns_connect.py	2012-10-08 12:50:32 +0000
+++ src/maasserver/dns_connect.py	2012-12-03 07:32:19 +0000
@@ -19,6 +19,7 @@
     post_save,
     )
 from django.dispatch import receiver
+from maasserver.enum import NODEGROUPINTERFACE_MANAGEMENT
 from maasserver.models import (
     Node,
     NodeGroup,
@@ -50,11 +51,19 @@
         write_full_dns_config()
 
 
-@receiver(post_delete, sender=NodeGroup)
-def dns_post_delete_NodeGroup(sender, instance, **kwargs):
-    """Delete DNS zones related to the nodegroup."""
+def dns_post_edit_management_NodeGroupInterface(instance, old_field, deleted):
+    """Delete DNS zones related to the interface."""
     from maasserver.dns import write_full_dns_config
-    write_full_dns_config()
+    if old_field == NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS:
+        # Force the dns config to be written as this might have been
+        # triggered by the last DNS-enabled interface being deleted
+        # or switched off (i.e. management set to DHCP or UNMANAGED).
+        write_full_dns_config(force=True)
+
+
+connect_to_field_change(
+    dns_post_edit_management_NodeGroupInterface,
+    NodeGroupInterface, 'management', delete=True)
 
 
 @receiver(post_delete, sender=Node)
@@ -70,7 +79,7 @@
         pass
 
 
-def dns_post_edit_hostname_Node(instance, old_field):
+def dns_post_edit_hostname_Node(instance, old_field, **kwargs):
     """When a Node has been flagged, update the related zone."""
     from maasserver.dns import change_dns_zones
     change_dns_zones(instance.nodegroup)

=== modified file 'src/maasserver/signals.py'
--- src/maasserver/signals.py	2012-08-03 18:57:08 +0000
+++ src/maasserver/signals.py	2012-12-03 07:32:19 +0000
@@ -15,13 +15,15 @@
     ]
 
 from django.db.models.signals import (
+    post_delete,
     post_init,
     post_save,
+    pre_delete,
     pre_save,
     )
 
 
-def connect_to_field_change(callback, model, field_name):
+def connect_to_field_change(callback, model, field_name, delete=False):
     """Call the provided callback when a field is modified on a model.
 
     The provided `callback` method will be called when the value of the field
@@ -29,13 +31,24 @@
 
     The signature of the callback method should be the following:
 
-    >>> def callback(instance, old_value):
+    >>> def callback(instance, old_value, deleted):
         ...
         pass
 
-    Where `instance` is the object which has just being saved to the database
-    and `old_value` is the old value of the field (different from the value of
-    the field in `instance`).
+    Where `instance` is the object which has just being saved to the
+    database, `old_value` is the old value of the field (different from the
+    value of the field in `instance`) and `deleted` indicates whether or not
+    the callback was called because the object was deleted.
+
+    :param callback: The callback function.
+    :type callback: callable
+    :param model: Specifies a particular sender to receive signals from.
+    :type model: class
+    :param field_name: Name of the field to monitor.
+    :type field_name: basestring
+    :param delete: Should the deletion of an object be considered a change
+        in the field?
+    :type delete: bool
     """
     last_seen_flag = '_field_last_seen_value_%s' % field_name
     delta_flag = '_field_delta_%s' % field_name
@@ -47,17 +60,27 @@
     post_init.connect(post_init_callback, sender=model, weak=False)
 
     # Set 'delta_flag' with the new and the old value of the field.
-    def pre_save_callback(sender, instance, **kwargs):
+    def record_delta_flag(sender, instance, **kwargs):
         original_value = getattr(instance, last_seen_flag)
         new_value = getattr(instance, field_name)
         setattr(instance, delta_flag, (new_value, original_value))
-    pre_save.connect(pre_save_callback, sender=model, weak=False)
+    pre_save.connect(record_delta_flag, sender=model, weak=False)
 
     # Call the `callback` if the field has changed.
     def post_save_callback(sender, instance, created, **kwargs):
         (new_value, original_value) = getattr(instance, delta_flag)
         # Call the callback method is the field has changed.
         if original_value != new_value:
-            callback(instance, original_value)
+            callback(instance, original_value, deleted=False)
         setattr(instance, last_seen_flag, new_value)
+
+    if delete:
+        pre_delete.connect(record_delta_flag, sender=model, weak=False)
+
+        def post_delete_callback(sender, instance, **kwargs):
+            (new_value, original_value) = getattr(instance, delta_flag)
+            callback(instance, original_value, deleted=True)
+
+        post_delete.connect(post_delete_callback, sender=model, weak=False)
+
     post_save.connect(post_save_callback, sender=model, weak=False)

=== modified file 'src/maasserver/tests/test_dns.py'
--- src/maasserver/tests/test_dns.py	2012-11-16 13:50:43 +0000
+++ src/maasserver/tests/test_dns.py	2012-12-03 07:32:19 +0000
@@ -15,6 +15,7 @@
 
 from functools import partial
 from itertools import islice
+import logging
 import socket
 
 from celery.task import task
@@ -35,6 +36,10 @@
 from maastesting.celery import CeleryFixture
 from maastesting.fakemethod import FakeMethod
 from maastesting.tests.test_bindfixture import dig_call
+from mock import (
+    call,
+    Mock,
+    )
 from netaddr import (
     IPNetwork,
     IPRange,
@@ -99,6 +104,16 @@
         self.patch_DEFAULT_MAAS_URL_with_random_values()
         self.assertRaises(dns.DNSException, dns.get_dns_server_address)
 
+    def test_get_dns_server_address_logs_warning_if_ip_is_localhost(self):
+        logger = self.patch(logging, 'getLogger')
+        self.patch(
+            dns, 'get_maas_facing_server_address',
+            Mock(return_value='127.0.0.1'))
+        dns.get_dns_server_address()
+        self.assertEqual(
+            call(dns.WARNING_MESSAGE % '127.0.0.1'),
+            logger.return_value.warn.call_args)
+
     def test_is_dns_managed(self):
         nodegroups_with_expected_results = {
             factory.make_node_group(
@@ -149,12 +164,15 @@
         # Reload BIND.
         self.bind.runner.rndc('reload')
 
+    def create_managed_nodegroup(self):
+        return factory.make_node_group(
+            network=IPNetwork('192.168.0.1/24'),
+            status=NODEGROUP_STATUS.ACCEPTED,
+            management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
+
     def create_nodegroup_with_lease(self, lease_number=1, nodegroup=None):
         if nodegroup is None:
-            nodegroup = factory.make_node_group(
-                network=IPNetwork('192.168.0.1/24'),
-                status=NODEGROUP_STATUS.ACCEPTED,
-                management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
+            nodegroup = self.create_managed_nodegroup()
         interface = nodegroup.get_managed_interface()
         node = factory.make_node(
             nodegroup=nodegroup)
@@ -225,25 +243,27 @@
         self.patch(settings, 'DNS_CONNECT', False)
         self.assertFalse(dns.is_dns_enabled())
 
-    def test_is_dns_enabled_return_True(self):
+    def test_is_dns_enabled_return_True_if_DNS_CONNECT_True(self):
         self.patch(settings, 'DNS_CONNECT', True)
         self.assertTrue(dns.is_dns_enabled())
 
+    def test_is_dns_in_use_return_False_no_configured_interface(self):
+        self.assertFalse(dns.is_dns_in_use())
+
+    def test_is_dns_in_use_return_True_if_configured_interface(self):
+        self.create_managed_nodegroup()
+        self.assertTrue(dns.is_dns_in_use())
+
     def test_write_full_dns_loads_full_dns_config(self):
         nodegroup, node, lease = self.create_nodegroup_with_lease()
         self.patch(settings, 'DNS_CONNECT', True)
         dns.write_full_dns_config()
         self.assertDNSMatches(node.hostname, nodegroup.name, lease.ip)
 
-    def test_write_full_dns_can_write_inactive_config(self):
-        nodegroup, node, lease = self.create_nodegroup_with_lease()
-        self.patch(settings, 'DNS_CONNECT', True)
-        dns.write_full_dns_config(active=False)
-        self.assertEqual([''], self.dig_resolve(generated_hostname(lease.ip)))
-
     def test_write_full_dns_passes_reload_retry_parameter(self):
         self.patch(settings, 'DNS_CONNECT', True)
         recorder = FakeMethod()
+        self.create_managed_nodegroup()
 
         @task
         def recorder_task(*args, **kwargs):
@@ -253,6 +273,12 @@
         self.assertEqual(
             ([(['reload'], True)]), recorder.extract_args())
 
+    def test_write_full_dns_doesnt_call_task_it_no_interface_configured(self):
+        self.patch(settings, 'DNS_CONNECT', True)
+        patched_task = self.patch(tasks, 'write_full_dns_config')
+        dns.write_full_dns_config()
+        self.assertEqual(0, patched_task.call_count)
+
     def test_dns_config_has_NS_record(self):
         ip = factory.getRandomIPAddress()
         self.patch(settings, 'DEFAULT_MAAS_URL', 'http://%s/' % ip)
@@ -268,11 +294,6 @@
             port=self.bind.config.port, commands=[ns_record, '+short'])
         self.assertEqual(ip, ip_of_ns_record)
 
-    def test_is_dns_enabled_follows_DNS_CONNECT(self):
-        rand_bool = factory.getRandomBoolean()
-        self.patch(settings, "DNS_CONNECT", rand_bool)
-        self.assertEqual(rand_bool, dns.is_dns_enabled())
-
     def test_add_nodegroup_creates_DNS_zone(self):
         self.patch(settings, "DNS_CONNECT", True)
         network = IPNetwork('192.168.7.1/24')
@@ -305,6 +326,19 @@
         # The ip from the new network resolves.
         self.assertDNSMatches(generated_hostname(ip), nodegroup.name, ip)
 
+    def test_changing_interface_management_updates_DNS_zone(self):
+        self.patch(settings, "DNS_CONNECT", True)
+        network = IPNetwork('192.168.7.1/24')
+        ip = factory.getRandomIPInNetwork(network)
+        nodegroup = factory.make_node_group(
+                network=network, status=NODEGROUP_STATUS.ACCEPTED,
+                management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
+        interface = nodegroup.get_managed_interface()
+        interface.management = NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED
+        interface.save()
+        self.assertEqual([''], self.dig_resolve(generated_hostname(ip)))
+        self.assertEqual([''], self.dig_reverse_resolve(ip))
+
     def test_delete_nodegroup_disables_DNS_zone(self):
         self.patch(settings, "DNS_CONNECT", True)
         network = IPNetwork('192.168.7.1/24')

=== modified file 'src/maasserver/tests/test_signals.py'
--- src/maasserver/tests/test_signals.py	2012-08-03 18:57:08 +0000
+++ src/maasserver/tests/test_signals.py	2012-12-03 07:32:19 +0000
@@ -16,7 +16,10 @@
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import TestModelTestCase
 from maasserver.tests.models import FieldChangeTestModel
-from maastesting.fakemethod import FakeMethod
+from mock import (
+    call,
+    Mock,
+    )
 
 
 class ConnectToFieldChangeTest(TestModelTestCase):
@@ -25,7 +28,7 @@
     app = 'maasserver.tests'
 
     def test_connect_to_field_change_calls_callback(self):
-        callback = FakeMethod()
+        callback = Mock()
         connect_to_field_change(callback, FieldChangeTestModel, 'name1')
         old_name1_value = factory.getRandomString()
         obj = FieldChangeTestModel(name1=old_name1_value)
@@ -33,11 +36,11 @@
         obj.name1 = factory.getRandomString()
         obj.save()
         self.assertEqual(
-            (1, [(obj, old_name1_value)]),
-            (callback.call_count, callback.extract_args()))
+            (1, call(obj, old_name1_value, deleted=False)),
+            (callback.call_count, callback.call_args))
 
     def test_connect_to_field_change_calls_callback_for_each_save(self):
-        callback = FakeMethod()
+        callback = Mock()
         connect_to_field_change(callback, FieldChangeTestModel, 'name1')
         old_name1_value = factory.getRandomString()
         obj = FieldChangeTestModel(name1=old_name1_value)
@@ -49,7 +52,7 @@
         self.assertEqual(2, callback.call_count)
 
     def test_connect_to_field_change_calls_callback_for_each_real_save(self):
-        callback = FakeMethod()
+        callback = Mock()
         connect_to_field_change(callback, FieldChangeTestModel, 'name1')
         old_name1_value = factory.getRandomString()
         obj = FieldChangeTestModel(name1=old_name1_value)
@@ -60,9 +63,9 @@
         self.assertEqual(1, callback.call_count)
 
     def test_connect_to_field_change_calls_multiple_callbacks(self):
-        callback1 = FakeMethod()
+        callback1 = Mock()
         connect_to_field_change(callback1, FieldChangeTestModel, 'name1')
-        callback2 = FakeMethod()
+        callback2 = Mock()
         connect_to_field_change(callback2, FieldChangeTestModel, 'name1')
         old_name1_value = factory.getRandomString()
         obj = FieldChangeTestModel(name1=old_name1_value)
@@ -74,15 +77,35 @@
     def test_connect_to_field_change_ignores_changes_to_other_fields(self):
         obj = FieldChangeTestModel(name2=factory.getRandomString())
         obj.save()
-        callback = FakeMethod()
+        callback = Mock()
         connect_to_field_change(callback, FieldChangeTestModel, 'name1')
         obj.name2 = factory.getRandomString()
         obj.save()
         self.assertEqual(0, callback.call_count)
 
     def test_connect_to_field_change_ignores_object_creation(self):
-        callback = FakeMethod()
+        callback = Mock()
         connect_to_field_change(callback, FieldChangeTestModel, 'name1')
         obj = FieldChangeTestModel(name1=factory.getRandomString())
         obj.save()
         self.assertEqual(0, callback.call_count)
+
+    def test_connect_to_field_change_ignores_deletion_by_default(self):
+        obj = FieldChangeTestModel(name2=factory.getRandomString())
+        obj.save()
+        callback = Mock()
+        connect_to_field_change(callback, FieldChangeTestModel, 'name1')
+        obj.delete()
+        self.assertEqual(0, callback.call_count)
+
+    def test_connect_to_field_change_listens_to_deletion_if_delete_True(self):
+        old_name1_value = factory.getRandomString()
+        obj = FieldChangeTestModel(name1=old_name1_value)
+        obj.save()
+        callback = Mock()
+        connect_to_field_change(
+            callback, FieldChangeTestModel, 'name1', delete=True)
+        obj.delete()
+        self.assertEqual(
+            (1, call(obj, old_name1_value, deleted=True)),
+            (callback.call_count, callback.call_args))


Follow ups