Browse Source

[master] Merge branch 'trac1456'

Jelte Jansen 13 years ago
parent
commit
a45a2bd0f8
2 changed files with 254 additions and 37 deletions
  1. 149 34
      src/lib/python/isc/xfrin/diff.py
  2. 105 3
      src/lib/python/isc/xfrin/tests/diff_tests.py

+ 149 - 34
src/lib/python/isc/xfrin/diff.py

@@ -15,11 +15,12 @@
 
 
 """
 """
 This helps the XFR in process with accumulating parts of diff and applying
 This helps the XFR in process with accumulating parts of diff and applying
-it to the datasource.
+it to the datasource. It also has a 'single update mode' which is useful
+for DDNS.
 
 
 The name of the module is not yet fully decided. We might want to move it
 The name of the module is not yet fully decided. We might want to move it
-under isc.datasrc or somewhere else, because we might want to reuse it with
-future DDNS process. But until then, it lives here.
+under isc.datasrc or somewhere else, because we are reusing it with DDNS.
+But for now, it lives here.
 """
 """
 
 
 import isc.dns
 import isc.dns
@@ -59,7 +60,8 @@ class Diff:
     the changes to underlying data source right away, but keeps them for
     the changes to underlying data source right away, but keeps them for
     a while.
     a while.
     """
     """
-    def __init__(self, ds_client, zone, replace=False, journaling=False):
+    def __init__(self, ds_client, zone, replace=False, journaling=False,
+                 single_update_mode=False):
         """
         """
         Initializes the diff to a ready state. It checks the zone exists
         Initializes the diff to a ready state. It checks the zone exists
         in the datasource and if not, NoSuchZone is raised. This also creates
         in the datasource and if not, NoSuchZone is raised. This also creates
@@ -76,6 +78,25 @@ class Diff:
         incoming updates but does not support journaling, the Diff object
         incoming updates but does not support journaling, the Diff object
         will still continue applying the diffs with disabling journaling.
         will still continue applying the diffs with disabling journaling.
 
 
+        If single_update_mode is true, the update is expected to only contain
+        1 set of changes (i.e. one set of additions, and one set of deletions).
+        If so, the additions and deletions are kept separately, and applied
+        in one go upon commit() or apply(). In this mode, additions and
+        deletions can be done in any order. The first addition and the
+        first deletion still have to be the new and old SOA records,
+        respectively. Once apply() or commit() has been called, this
+        requirement is renewed (since the diff object is essentialy reset).
+
+        In this single_update_mode, upon commit, the deletions are performed
+        first, and then the additions. With the previously mentioned
+        restrictions, this means that the actual update looks like a single
+        IXFR changeset (which can then be journaled). Apart from those
+        restrictions, this class does not do any checking of data; it is
+        the caller's responsibility to keep the data 'sane', and this class
+        does not presume to have any knowledge of DNS zone content sanity.
+        For instance, though it enforces the SOA to be deleted first, and
+        added first, it does no checks on the SERIAL value.
+
         You can also expect isc.datasrc.Error or isc.datasrc.NotImplemented
         You can also expect isc.datasrc.Error or isc.datasrc.NotImplemented
         exceptions.
         exceptions.
         """
         """
@@ -91,7 +112,12 @@ class Diff:
             raise NoSuchZone("Zone " + str(zone) +
             raise NoSuchZone("Zone " + str(zone) +
                              " does not exist in the data source " +
                              " does not exist in the data source " +
                              str(ds_client))
                              str(ds_client))
-        self.__buffer = []
+        self.__single_update_mode = single_update_mode
+        if single_update_mode:
+            self.__additions = []
+            self.__deletions = []
+        else:
+            self.__buffer = []
 
 
     def __check_commited(self):
     def __check_commited(self):
         """
         """
@@ -103,12 +129,45 @@ class Diff:
             raise ValueError("The diff is already commited or it has raised " +
             raise ValueError("The diff is already commited or it has raised " +
                              "an exception, you come late")
                              "an exception, you come late")
 
 
+    def __append_with_soa_check(self, buf, operation, rr):
+        """
+        Helper method for __data_common().
+        Add the given rr to the given buffer, but with a SOA check;
+        - if the buffer is empty, the RRType of the rr must be SOA
+        - if the buffer is not empty, the RRType must not be SOA
+        Raises a ValueError if these rules are not satisified.
+        If they are, the RR is appended to the buffer.
+        Arguments:
+        buf: buffer to add to
+        operation: operation to perform (either 'add' or 'delete')
+        rr: RRset to add to the buffer
+        """
+        # first add or delete must be of type SOA
+        if len(buf) == 0 and\
+           rr.get_type() != isc.dns.RRType.SOA():
+            raise ValueError("First " + operation +
+                             " in single update mode must be of type SOA")
+        # And later adds or deletes may not
+        elif len(buf) != 0 and\
+           rr.get_type() == isc.dns.RRType.SOA():
+            raise ValueError("Multiple SOA records in single " +
+                             "update mode " + operation)
+        buf.append((operation, rr))
+
     def __data_common(self, rr, operation):
     def __data_common(self, rr, operation):
         """
         """
         Schedules an operation with rr.
         Schedules an operation with rr.
 
 
         It does all the real work of add_data and delete_data, including
         It does all the real work of add_data and delete_data, including
         all checks.
         all checks.
+
+        Raises a ValueError in several cases:
+        - if the rrset contains multiple rrs
+        - if the class of the rrset does not match that of the update
+        - in single_update_mode if the first rr is not of type SOA (both
+          for addition and deletion)
+        - in single_update_mode if any later rr is of type SOA (both for
+          addition and deletion)
         """
         """
         self.__check_commited()
         self.__check_commited()
         if rr.get_rdata_count() != 1:
         if rr.get_rdata_count() != 1:
@@ -118,10 +177,17 @@ class Diff:
             raise ValueError("The rrset's class " + str(rr.get_class()) +
             raise ValueError("The rrset's class " + str(rr.get_class()) +
                              " does not match updater's " +
                              " does not match updater's " +
                              str(self.__updater.get_class()))
                              str(self.__updater.get_class()))
-        self.__buffer.append((operation, rr))
-        if len(self.__buffer) >= DIFF_APPLY_TRESHOLD:
-            # Time to auto-apply, so the data don't accumulate too much
-            self.apply()
+        if self.__single_update_mode:
+            if operation == 'add':
+                self.__append_with_soa_check(self.__additions, operation, rr)
+            elif operation == 'delete':
+                self.__append_with_soa_check(self.__deletions, operation, rr)
+        else:
+            self.__buffer.append((operation, rr))
+            if len(self.__buffer) >= DIFF_APPLY_TRESHOLD:
+                # Time to auto-apply, so the data don't accumulate too much
+                # This is not done for DDNS type data
+                self.apply()
 
 
     def add_data(self, rr):
     def add_data(self, rr):
         """
         """
@@ -175,23 +241,34 @@ class Diff:
             sigdata2 = rrset2.get_rdata()[0].to_text().split()[0]
             sigdata2 = rrset2.get_rdata()[0].to_text().split()[0]
             return sigdata1 == sigdata2
             return sigdata1 == sigdata2
 
 
-        buf = []
-        for (op, rrset) in self.__buffer:
-            old = buf[-1][1] if len(buf) > 0 else None
-            if old is None or op != buf[-1][0] or \
-                rrset.get_name() != old.get_name() or \
-                (not same_type(rrset, old)):
-                buf.append((op, isc.dns.RRset(rrset.get_name(),
-                                              rrset.get_class(),
-                                              rrset.get_type(),
-                                              rrset.get_ttl())))
-            if rrset.get_ttl() != buf[-1][1].get_ttl():
-                logger.warn(LIBXFRIN_DIFFERENT_TTL, rrset.get_ttl(),
-                            buf[-1][1].get_ttl(), rrset.get_name(),
-                            rrset.get_class(), rrset.get_type())
-            for rdatum in rrset.get_rdata():
-                buf[-1][1].add_rdata(rdatum)
-        self.__buffer = buf
+        def compact_buffer(buffer_to_compact):
+            '''Internal helper function for compacting buffers, compacts the
+               given buffer.
+               Returns the compacted buffer.
+            '''
+            buf = []
+            for (op, rrset) in buffer_to_compact:
+                old = buf[-1][1] if len(buf) > 0 else None
+                if old is None or op != buf[-1][0] or \
+                    rrset.get_name() != old.get_name() or \
+                    (not same_type(rrset, old)):
+                    buf.append((op, isc.dns.RRset(rrset.get_name(),
+                                                  rrset.get_class(),
+                                                  rrset.get_type(),
+                                                  rrset.get_ttl())))
+                if rrset.get_ttl() != buf[-1][1].get_ttl():
+                    logger.warn(LIBXFRIN_DIFFERENT_TTL, rrset.get_ttl(),
+                                buf[-1][1].get_ttl(), rrset.get_name(),
+                                rrset.get_class(), rrset.get_type())
+                for rdatum in rrset.get_rdata():
+                    buf[-1][1].add_rdata(rdatum)
+            return buf
+
+        if self.__single_update_mode:
+            self.__additions = compact_buffer(self.__additions)
+            self.__deletions = compact_buffer(self.__deletions)
+        else:
+            self.__buffer = compact_buffer(self.__buffer)
 
 
     def apply(self):
     def apply(self):
         """
         """
@@ -209,25 +286,41 @@ class Diff:
         It also can raise isc.datasrc.Error. If that happens, you should stop
         It also can raise isc.datasrc.Error. If that happens, you should stop
         using this object and abort the modification.
         using this object and abort the modification.
         """
         """
-        self.__check_commited()
-        # First, compact the data
-        self.compact()
-        try:
-            # Then pass the data inside the data source
-            for (operation, rrset) in self.__buffer:
+        def apply_buffer(buf):
+            '''
+            Helper method to apply all operations in the given buffer
+            '''
+            for (operation, rrset) in buf:
                 if operation == 'add':
                 if operation == 'add':
                     self.__updater.add_rrset(rrset)
                     self.__updater.add_rrset(rrset)
                 elif operation == 'delete':
                 elif operation == 'delete':
                     self.__updater.delete_rrset(rrset)
                     self.__updater.delete_rrset(rrset)
                 else:
                 else:
                     raise ValueError('Unknown operation ' + operation)
                     raise ValueError('Unknown operation ' + operation)
+
+        self.__check_commited()
+        # First, compact the data
+        self.compact()
+        try:
+            # Then pass the data inside the data source
+            if self.__single_update_mode:
+                apply_buffer(self.__deletions)
+                apply_buffer(self.__additions)
+            else:
+                apply_buffer(self.__buffer)
+
             # As everything is already in, drop the buffer
             # As everything is already in, drop the buffer
         except:
         except:
             # If there's a problem, we can't continue.
             # If there's a problem, we can't continue.
             self.__updater = None
             self.__updater = None
             raise
             raise
 
 
-        self.__buffer = []
+        # all went well, reset state of buffers
+        if self.__single_update_mode:
+            self.__additions = []
+            self.__deletions = []
+        else:
+            self.__buffer = []
 
 
     def commit(self):
     def commit(self):
         """
         """
@@ -259,5 +352,27 @@ class Diff:
 
 
         Probably useful only for testing and introspection purposes. Don't
         Probably useful only for testing and introspection purposes. Don't
         modify the list.
         modify the list.
+
+        Raises a ValueError if the buffer is in single_update_mode.
+        """
+        if self.__single_update_mode:
+            raise ValueError("Compound buffer requested in single-update mode")
+        else:
+            return self.__buffer
+
+    def get_single_update_buffers(self):
+        """
+        Returns the current buffers of changes not yet passed into the data
+        source. It is a tuple of the current deletions and additions, which
+        each are in a form like [('delete', rrset), ('delete', rrset), ...],
+        and [('add', rrset), ('add', rrset), ..].
+
+        Probably useful only for testing and introspection purposes. Don't
+        modify the lists.
+
+        Raises a ValueError if the buffer is not in single_update_mode.
         """
         """
-        return self.__buffer
+        if not self.__single_update_mode:
+            raise ValueError("Separate buffers requested in single-update mode")
+        else:
+            return (self.__deletions, self.__additions)

+ 105 - 3
src/lib/python/isc/xfrin/tests/diff_tests.py

@@ -478,14 +478,116 @@ class DiffTest(unittest.TestCase):
         diff.compact()
         diff.compact()
         self.assertEqual(2, len(diff.get_buffer()))
         self.assertEqual(2, len(diff.get_buffer()))
 
 
-    def test_relpace(self):
-        """
+    def test_replace(self):
+        '''
         Test that when we want to replace the whole zone, it is propagated.
         Test that when we want to replace the whole zone, it is propagated.
-        """
+        '''
         self.__should_replace = True
         self.__should_replace = True
         diff = Diff(self, "example.org.", True)
         diff = Diff(self, "example.org.", True)
         self.assertTrue(self.__updater_requested)
         self.assertTrue(self.__updater_requested)
 
 
+    def test_get_buffer(self):
+        '''
+        Test that the getters raise when used in the wrong mode
+        '''
+        diff_multi = Diff(self, Name('example.org.'), single_update_mode=False)
+        self.assertRaises(ValueError, diff_multi.get_single_update_buffers)
+        self.assertEqual([], diff_multi.get_buffer())
+
+        diff_single = Diff(self, Name('example.org.'), single_update_mode=True)
+        self.assertRaises(ValueError, diff_single.get_buffer)
+        self.assertEqual(([], []), diff_single.get_single_update_buffers())
+
+    def test_single_update_mode(self):
+        '''
+        Test single-update mode. In this mode, updates and deletes can
+        be done in any order, but there may only be one changeset.
+        For both updates and deletes, exactly one SOA rr must be given,
+        and it must be the first change.
+        '''
+
+        # First create some RRsets to play with
+        soa = RRset(Name('example.org.'), self.__rrclass, RRType.SOA(),
+                    RRTTL(3600))
+        soa.add_rdata(Rdata(soa.get_type(), soa.get_class(),
+                      "ns.example.org. foo.example.org. 1234 28800 "+
+                      "7200 604800 3600"))
+
+        a = RRset(Name('www.example.org.'), self.__rrclass, RRType.A(),
+                      RRTTL(3600))
+        a.add_rdata(Rdata(a.get_type(), a.get_class(),
+                        "192.0.2.1"))
+
+        a2 = RRset(Name('www.example.org.'), self.__rrclass, RRType.A(),
+                      RRTTL(3600))
+        a2.add_rdata(Rdata(a2.get_type(), a2.get_class(),
+                        "192.0.2.2"))
+
+        # full rrset for A (to check compact())
+        a_1_2 = RRset(Name('www.example.org.'), self.__rrclass, RRType.A(),
+                      RRTTL(3600))
+        a_1_2.add_rdata(Rdata(a_1_2.get_type(), a_1_2.get_class(),
+                        "192.0.2.1"))
+        a_1_2.add_rdata(Rdata(a_1_2.get_type(), a_1_2.get_class(),
+                        "192.0.2.2"))
+
+        diff = Diff(self, Name('example.org.'), single_update_mode=True)
+
+        # adding a first should fail
+        self.assertRaises(ValueError, diff.add_data, a)
+        # But soa should work
+        diff.add_data(soa)
+        # And then A should as well
+        diff.add_data(a)
+        diff.add_data(a2)
+        # But another SOA should fail again
+        self.assertRaises(ValueError, diff.add_data, soa)
+
+        # Same for delete
+        self.assertRaises(ValueError, diff.delete_data, a)
+        diff.delete_data(soa)
+        diff.delete_data(a)
+        diff.delete_data(a2)
+        self.assertRaises(ValueError, diff.delete_data, soa)
+
+        # Not compacted yet, so the buffers should be as we
+        # filled them
+        (delbuf, addbuf) = diff.get_single_update_buffers()
+        self.assertEqual([('delete', soa), ('delete', a), ('delete', a2)], delbuf)
+        self.assertEqual([('add', soa), ('add', a), ('add', a2)], addbuf)
+
+        # Compact should compact the A records in both buffers
+        diff.compact()
+        (delbuf, addbuf) = diff.get_single_update_buffers()
+        # need rrset equality again :/
+        self.assertEqual(2, len(delbuf))
+        self.assertEqual(2, len(delbuf[0]))
+        self.assertEqual('delete', delbuf[0][0])
+        self.assertEqual(soa.to_text(), delbuf[0][1].to_text())
+        self.assertEqual(2, len(delbuf[1]))
+        self.assertEqual('delete', delbuf[1][0])
+        self.assertEqual(a_1_2.to_text(), delbuf[1][1].to_text())
+
+        self.assertEqual(2, len(addbuf))
+        self.assertEqual(2, len(addbuf[0]))
+        self.assertEqual('add', addbuf[0][0])
+        self.assertEqual(soa.to_text(), addbuf[0][1].to_text())
+        self.assertEqual(2, len(addbuf[1]))
+        self.assertEqual('add', addbuf[1][0])
+        self.assertEqual(a_1_2.to_text(), addbuf[1][1].to_text())
+
+        # Apply should reset the buffers
+        diff.apply()
+        (delbuf, addbuf) = diff.get_single_update_buffers()
+        self.assertEqual([], delbuf)
+        self.assertEqual([], addbuf)
+
+        # Now the change has been applied, and the buffers are cleared,
+        # Adding non-SOA records should fail again.
+        self.assertRaises(ValueError, diff.add_data, a)
+        self.assertRaises(ValueError, diff.delete_data, a)
+
+
 if __name__ == "__main__":
 if __name__ == "__main__":
     isc.log.init("bind10")
     isc.log.init("bind10")
     isc.log.resetUnitTestRootLogger()
     isc.log.resetUnitTestRootLogger()