Browse Source

[1457] initial update handling

Still needs a lot of cleanup, logging and more tests, but the basics are there
Jelte Jansen 13 years ago
parent
commit
1e0e7c3c3a

+ 1 - 0
src/lib/dns/python/rdata_python.cc

@@ -116,6 +116,7 @@ Rdata_init(PyObject* self_p, PyObject* args, PyObject*) {
             return (0);
             return (0);
         } else if (PyArg_ParseTuple(args, "O!O!y#", &rrtype_type, &rrtype,
         } else if (PyArg_ParseTuple(args, "O!O!y#", &rrtype_type, &rrtype,
                                     &rrclass_type, &rrclass, &data, &len)) {
                                     &rrclass_type, &rrclass, &data, &len)) {
+            PyErr_Clear();
             InputBuffer input_buffer(data, len);
             InputBuffer input_buffer(data, len);
             self->cppobj = createRdata(PyRRType_ToRRType(rrtype),
             self->cppobj = createRdata(PyRRType_ToRRType(rrtype),
                                        PyRRClass_ToRRClass(rrclass),
                                        PyRRClass_ToRRClass(rrclass),

+ 276 - 5
src/lib/python/isc/ddns/session.py

@@ -19,6 +19,7 @@ from isc.log import *
 from isc.ddns.logger import logger, ClientFormatter, ZoneFormatter,\
 from isc.ddns.logger import logger, ClientFormatter, ZoneFormatter,\
                             RRsetFormatter
                             RRsetFormatter
 from isc.log_messages.libddns_messages import *
 from isc.log_messages.libddns_messages import *
+import isc.xfrin.diff
 import copy
 import copy
 
 
 # Result codes for UpdateSession.handle()
 # Result codes for UpdateSession.handle()
@@ -57,6 +58,58 @@ class UpdateError(Exception):
         self.rcode = rcode
         self.rcode = rcode
         self.nolog = nolog
         self.nolog = nolog
 
 
+def rrset_as_rrs(rrset, method, *kwargs):
+    '''Helper function. For DDNS, in a number of cases, we need to
+       treat the various RRs in a single RRset separately.
+       Our libdns++ has no concept of RRs, so in that case,
+       what we do is create a temporary 1-RR RRset for each Rdata
+       in the RRset object.
+       This method then calls the given method with the given args
+       for each of the temporary rrsets (the rrset in *wargs is
+       replaced by the temporary one)
+       Note: if this method is useful in more places, we may want
+       to move it out of ddns.
+       Example:
+       Say you have a method that prints a prexif string and an
+       rrset, def my_print(prefix, rrset)
+       Given an rrset my_rrset, you'd print the entire rrset
+       with my_print("foo", rrset)
+       And with this helper function, to print each rr invidually,
+       you'd call
+       rrset_as_rrset(rrset, my_print, "foo", rrset)
+       Note the rrset is needed twice, the first to identify it,
+       the second as the 'real' argument to my_print (which is replaced
+       by this function.
+    '''
+    #result = None
+    for rdata in rrset.get_rdata():
+        tmp_rrset = isc.dns.RRset(rrset.get_name(),
+                                  rrset.get_class(),
+                                  rrset.get_type(),
+                                  rrset.get_ttl())
+        tmp_rrset.add_rdata(rdata)
+        a = []
+        # Replace the rrset in the original arguments by our rrset
+        args = [arg if arg != rrset else tmp_rrset for arg in kwargs]
+        result = method(*args)
+    return result
+
+def rrset_class_conversion(rrset, rrclass):
+    '''Returns a (new) rrset with the data from the given rrset,
+       but of the given class. Useful to convert from NONE and ANY to
+       a real class'''
+    # QUERY, do we want to do this as a special case of the rrset_as_rrs?
+    # or would that make it too complicated?
+    new_rrset = isc.dns.RRset(rrset.get_name(), rrclass, rrset.get_type(),
+                              rrset.get_ttl())
+    for rdata in rrset.get_rdata():
+        # Rdata class is nof modifiable, and must match rrset's
+        # class, so we need to to some ugly conversion here.
+        # And we cannot use to_text() (since the class may be unknown)
+        wire = rdata.to_wire(bytes())
+        new_rrset.add_rdata(isc.dns.Rdata(rrset.get_type(), rrclass, wire))
+    return new_rrset
+
 class UpdateSession:
 class UpdateSession:
     '''Protocol handling for a single dynamic update request.
     '''Protocol handling for a single dynamic update request.
 
 
@@ -93,6 +146,7 @@ class UpdateSession:
         self.__message = req_message
         self.__message = req_message
         self.__client_addr = client_addr
         self.__client_addr = client_addr
         self.__zone_config = zone_config
         self.__zone_config = zone_config
+        self.__added_soa = None
 
 
     def get_message(self):
     def get_message(self):
         '''Return the update message.
         '''Return the update message.
@@ -131,8 +185,11 @@ class UpdateSession:
                 self.__make_response(prereq_result)
                 self.__make_response(prereq_result)
                 return UPDATE_ERROR, zname, zclass
                 return UPDATE_ERROR, zname, zclass
             # self.__check_update_acl()
             # self.__check_update_acl()
-            # self.__do_update()
-            # self.__make_response(Rcode.NOERROR())
+            update_result = self.__do_update(datasrc_client, zname, zclass)
+            if update_result != Rcode.NOERROR():
+                self.__make_response(update_result)
+                return UPDATE_ERROR, zname, zclass
+            self.__make_response(Rcode.NOERROR())
             return UPDATE_SUCCESS, zname, zclass
             return UPDATE_SUCCESS, zname, zclass
         except UpdateError as e:
         except UpdateError as e:
             if not e.nolog:
             if not e.nolog:
@@ -276,6 +333,12 @@ class UpdateSession:
         '''
         '''
         return not self.__prereq_name_in_use(datasrc_client, rrset)
         return not self.__prereq_name_in_use(datasrc_client, rrset)
 
 
+    def __check_in_zone(self, rrset, zname):
+        '''Returns true if the RRset is in the zone'''
+        relation = rrset.get_name().compare(zname).get_relation()
+        return relation == NameComparisonResult.SUBDOMAIN or\
+               relation == NameComparisonResult.EQUAL
+
     def __check_prerequisites(self, datasrc_client, zname, zclass):
     def __check_prerequisites(self, datasrc_client, zname, zclass):
         '''Check the prerequisites section of the UPDATE Message.
         '''Check the prerequisites section of the UPDATE Message.
            RFC2136 Section 2.4.
            RFC2136 Section 2.4.
@@ -284,9 +347,7 @@ class UpdateSession:
         '''
         '''
         for rrset in self.__message.get_section(SECTION_PREREQUISITE):
         for rrset in self.__message.get_section(SECTION_PREREQUISITE):
             # First check if the name is in the zone
             # First check if the name is in the zone
-            relation = rrset.get_name().compare(zname).get_relation()
-            if relation != NameComparisonResult.SUBDOMAIN and\
-               relation != NameComparisonResult.EQUAL:
+            if not self.__check_in_zone(rrset, zname):
                 logger.info(LIBDDNS_PREREQ_NOTZONE,
                 logger.info(LIBDDNS_PREREQ_NOTZONE,
                             ClientFormatter(self.__client_addr),
                             ClientFormatter(self.__client_addr),
                             ZoneFormatter(zname, zclass),
                             ZoneFormatter(zname, zclass),
@@ -370,3 +431,213 @@ class UpdateSession:
 
 
         # All prerequisites are satisfied
         # All prerequisites are satisfied
         return Rcode.NOERROR()
         return Rcode.NOERROR()
+
+    def __set_soa_rrset(self, rrset):
+        self.__added_soa = rrset
+
+    def __do_prescan(self, datasrc_client, zname, zclass):
+        '''Perform the prescan as defined in RFC2136 section 3.4.1.
+           This method has a side-effect; it sets self._new_soa if
+           it encounters the addition of a SOA record in the update
+           list (so serial can be checked by update later, etc.).
+           It puts the added SOA in self.__added_soa.
+        '''
+        for rrset in self.__message.get_section(SECTION_UPDATE):
+            if not self.__check_in_zone(rrset, zname):
+                return Rcode.NOTZONE()
+            if rrset.get_class() == zclass:
+                # In fact, all metatypes are in a specific range,
+                # so one check can test TKEY to ANY
+                # (some value check is needed anyway, since we do
+                # not have defined RRtypes for MAILA and MAILB)
+                if rrset.get_type().get_code() >=  249:
+                    return Rcode.FORMERR()
+                if rrset.get_type() == RRType.SOA():
+                    # In case there's multiple soa records in the update
+                    # somehow, just take the last
+                    rrset_as_rrs(rrset, self.__set_soa_rrset, rrset)
+            elif rrset.get_class() == RRClass.ANY():
+                if rrset.get_ttl().get_value() != 0:
+                    return Rcode.FORMERR()
+                if rrset.get_rdata_count() > 0:
+                    return Rcode.FORMERR()
+                if rrset.get_type().get_code() >= 249 and\
+                   rrset.get_type().get_code() <= 254:
+                    return Rcode.FORMERR()
+            elif rrset.get_class() == RRClass.NONE():
+                if rrset.get_ttl().get_value() != 0:
+                    return Rcode.FORMERR()
+                if rrset.get_type().get_code() >= 249:
+                    return Rcode.FORMERR()
+            else:
+                return Rcode.FORMERR()
+        return Rcode.NOERROR()
+
+    def __do_update_add_rrs_to_rrset(self, datasrc_client, diff, rrset):
+        # For a number of cases, we may need to remove data in the zone
+        # (note; SOA is handled separately by __do_update, so that one
+        # is not explicitely ignored here)
+        if rrset.get_type() == RRType.SOA():
+            return
+        _, finder = datasrc_client.find_zone(rrset.get_name())
+        result, orig_rrset, _ = finder.find(rrset.get_name(),
+                                            rrset.get_type(),
+                                            finder.NO_WILDCARD |
+                                            finder.FIND_GLUE_OK)
+        if result == finder.SUCCESS:
+            # if update is cname, and zone rr is not, ignore
+            if rrset.get_type() == RRType.CNAME():
+                # can the orig_rrset be of different type?
+                if orig_rrset.get_type() == RRType.CNAME():
+                    diff.remove_data(orig_rrset)
+                else:
+                    # ignore
+                    return
+            elif orig_rrset.get_type() == RRType.CNAME():
+                # ignore
+                return
+            # We do not have WKS support at this time, but if there
+            # are special Update equality rules such as for WKS, and
+            # we do have support for the type, this is where the check
+            # (and potential delete) would go.
+        rrset_as_rrs(rrset, diff.add_data, rrset)
+
+    def __do_update_delete_rrset(self, datasrc_client, zname, diff, rrset):
+        _, finder = datasrc_client.find_zone(rrset.get_name())
+        result, to_delete, _ = finder.find(rrset.get_name(),
+                                           rrset.get_type(),
+                                           finder.NO_WILDCARD |
+                                           finder.FIND_GLUE_OK)
+        if to_delete.get_name() == zname and\
+           (to_delete.get_type() == RRType.SOA() or\
+            to_delete.get_type() == RRType.NS()):
+            # ignore
+            return
+        rrset_as_rrs(to_delete, diff.delete_data, to_delete)
+
+    def __ns_deleter_helper(self, datasrc_client, zname, diff, rrset):
+        _, finder = datasrc_client.find_zone(rrset.get_name())
+        result, orig_rrset, _ = finder.find(rrset.get_name(),
+                                            rrset.get_type(),
+                                            finder.NO_WILDCARD |
+                                            finder.FIND_GLUE_OK)
+        # Even a real rrset comparison wouldn't help here...
+        # The goal is to make sure that after deletion of the
+        # given rrset, at least 1 NS record is left.
+        # So we make a (shallow) copy of the existing rrset,
+        # and for each rdata in the to_delete set, we check if it wouldn't
+        # delete the last one. If it would, that specific one is ignored.
+        # If it would not, the rdata is removed from the temporary list
+        orig_rrset_rdata = copy.copy(orig_rrset.get_rdata())
+        for rdata in rrset.get_rdata():
+            if len(orig_rrset_rdata) == 1 and rdata == orig_rrset_rdata[0]:
+                # ignore
+                continue
+            else:
+                # create an individual RRset for deletion
+                to_delete = isc.dns.RRset(rrset.get_name(),
+                                          rrset.get_class(),
+                                          rrset.get_type(),
+                                          rrset.get_ttl())
+                to_delete.add_rdata(rdata)
+                orig_rrset_rdata.remove(rdata)
+                diff.delete_data(to_delete)
+
+    def __do_update_delete_name(self, datasrc_client, zname, diff, rrset):
+        _, finder = datasrc_client.find_zone(rrset.get_name())
+        result, rrsets, flags = finder.find_all(rrset.get_name(),
+                                                finder.NO_WILDCARD |
+                                                finder.FIND_GLUE_OK)
+        if result == finder.SUCCESS and\
+           (flags & finder.RESULT_WILDCARD == 0):
+            for to_delete in rrsets:
+                # if name == zname and type is soa or ns, don't delete!
+                if to_delete.get_name() == zname and\
+                   (to_delete.get_type() == RRType.SOA() or
+                    to_delete.get_type() == RRType.NS()):
+                    continue
+                else:
+                    rrset_as_rrs(to_delete, diff.delete_data, to_delete)
+
+    def __do_update_delete_rr_from_rrset(self, datasrc_client, zname, zclass, diff, rrset):
+        # Delete all rrs in the rrset, except if name=zname and type=soa, or
+        # type = ns and there is only one left (...)
+
+        # The delete does not want class NONE, we would not have gotten here
+        # if it wasn't, but now is a good time to change it to the zclass.
+        to_delete = rrset_class_conversion(rrset, zclass)
+
+        if rrset.get_name() == zname:
+            if rrset.get_type() == RRType.SOA():
+                # ignore
+                return
+            elif rrset.get_type() == RRType.NS():
+                # hmm. okay. annoying. There must be at least one left,
+                # delegate to helper method
+                self.__ns_deleter_helper(datasrc_client, zname, diff, to_delete)
+                return
+        rrset_as_rrs(to_delete, diff.delete_data, to_delete)
+
+    def __update_soa(self, datasrc_client, zname, zclass, diff):
+        # Get the existing SOA
+        # if a new soa was specified, add that one, otherwise, do the
+        # serial magic and add the newly created one
+
+        # get it from DS and to increment and stuff
+        _, finder = datasrc_client.find_zone(zname)
+        result, old_soa, _ = finder.find(zname, RRType.SOA(),
+                                         finder.NO_WILDCARD |
+                                         finder.FIND_GLUE_OK)
+
+        if self.__added_soa is not None:
+            new_soa = self.__added_soa
+            # serial check goes here
+        else:
+            new_soa = old_soa
+            # increment goes here
+
+        diff.delete_data(old_soa)
+        diff.add_data(new_soa)
+
+    def __do_update(self, datasrc_client, zname, zclass):
+        # prescan
+        prescan_result = self.__do_prescan(datasrc_client, zname, zclass)
+        if prescan_result != Rcode.NOERROR():
+            return prescan_result
+
+        # update
+        # TODO: catchall? any error should result in abort and servfail...
+        # Don't like catchalls much, though
+
+        # create an ixfr-out-friendly diff structure to work on
+        diff = isc.xfrin.diff.Diff(datasrc_client, zname, journaling=True, single_update_mode=True)
+
+        # Do special handling for SOA first
+        self.__update_soa(datasrc_client, zname, zclass, diff)
+
+        # Algorithm from RFC2136 Section 3.4
+        # Note that this works on full rrsets, not individual RRs.
+        # Some checks might be easier with individual RRs, but only if we
+        # would use the ZoneUpdater directly (so we can query the
+        # 'zone-as-it-would-be-so-far'. However, due to the current use
+        # of the Diff class, this is not the case, and therefore it
+        # is easier to work with full rrsets for the most parts
+        # (less lookups needed; conversion to individual rrs is
+        # the same offort whether it is done here or in the several
+        # do_update statements)
+        for rrset in self.__message.get_section(SECTION_UPDATE):
+            if rrset.get_class() == zclass:
+                self.__do_update_add_rrs_to_rrset(datasrc_client, diff, rrset)
+            elif rrset.get_class() == RRClass.ANY():
+                if rrset.get_type() == RRType.ANY():
+                    self.__do_update_delete_name(datasrc_client, zname, diff, rrset)
+                else:
+                    self.__do_update_delete_rrset(datasrc_client, zname, diff, rrset)
+            elif rrset.get_class() == RRClass.NONE():
+                self.__do_update_delete_rr_from_rrset(datasrc_client, zname, zclass, diff, rrset)
+
+        #try:
+        diff.commit()
+        return Rcode.NOERROR()
+        #except isc.datasrc.Error:
+        #    return Rcode.SERVFAIL()

+ 491 - 1
src/lib/python/isc/ddns/tests/session_tests.py

@@ -36,7 +36,8 @@ TEST_ZONE_RECORD = Question(TEST_ZONE_NAME, TEST_RRCLASS, UPDATE_RRTYPE)
 TEST_CLIENT6 = ('2001:db8::1', 53, 0, 0)
 TEST_CLIENT6 = ('2001:db8::1', 53, 0, 0)
 TEST_CLIENT4 = ('192.0.2.1', 53)
 TEST_CLIENT4 = ('192.0.2.1', 53)
 
 
-def create_update_msg(zones=[TEST_ZONE_RECORD], prerequisites=[]):
+def create_update_msg(zones=[TEST_ZONE_RECORD], prerequisites=[],
+                      updates=[]):
     msg = Message(Message.RENDER)
     msg = Message(Message.RENDER)
     msg.set_qid(5353)           # arbitrary chosen
     msg.set_qid(5353)           # arbitrary chosen
     msg.set_opcode(Opcode.UPDATE())
     msg.set_opcode(Opcode.UPDATE())
@@ -45,6 +46,8 @@ def create_update_msg(zones=[TEST_ZONE_RECORD], prerequisites=[]):
         msg.add_question(z)
         msg.add_question(z)
     for p in prerequisites:
     for p in prerequisites:
         msg.add_rrset(SECTION_PREREQUISITE, p)
         msg.add_rrset(SECTION_PREREQUISITE, p)
+    for u in updates:
+        msg.add_rrset(SECTION_UPDATE, u)
 
 
     renderer = MessageRenderer()
     renderer = MessageRenderer()
     msg.to_wire(renderer)
     msg.to_wire(renderer)
@@ -150,6 +153,47 @@ class SessionTest(unittest.TestCase):
         # zone class doesn't match
         # zone class doesn't match
         self.check_notauth(Name('example.org'), RRClass.CH())
         self.check_notauth(Name('example.org'), RRClass.CH())
 
 
+    def rrset_as_rrs_helper(self, rr, l):
+        l.append(rr.to_text())
+
+    def test_rrset_as_rrset(self):
+        rrset = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                                       TEST_RRCLASS,
+                                       isc.dns.RRType.A(),
+                                       isc.dns.RRTTL(3600))
+        rrset.add_rdata(isc.dns.Rdata(rrset.get_type(),
+                                      rrset.get_class(),
+                                               "192.0.2.1"))
+
+        l = []
+        rrset_as_rrs(rrset, self.rrset_as_rrs_helper, rrset, l)
+        self.assertEqual(["www.example.org. 3600 IN A 192.0.2.1\n"], l)
+
+        rrset.add_rdata(isc.dns.Rdata(rrset.get_type(),
+                                      rrset.get_class(),
+                                      "192.0.2.2"))
+        rrset.add_rdata(isc.dns.Rdata(rrset.get_type(),
+                                      rrset.get_class(),
+                                      "192.0.2.3"))
+
+        # if the helper is called directly, the list should have
+        # one entry, with a multiline string
+        # but through the helper, there should be several 1-line entries
+        l = []
+        self.rrset_as_rrs_helper(rrset, l)
+        self.assertEqual(["www.example.org. 3600 IN A 192.0.2.1\n" +
+                          "www.example.org. 3600 IN A 192.0.2.2\n" +
+                          "www.example.org. 3600 IN A 192.0.2.3\n"
+                         ], l)
+
+        # but through the helper, there should be several 1-line entries
+        l = []
+        rrset_as_rrs(rrset, self.rrset_as_rrs_helper, rrset, l)
+        self.assertEqual(["www.example.org. 3600 IN A 192.0.2.1\n",
+                          "www.example.org. 3600 IN A 192.0.2.2\n",
+                          "www.example.org. 3600 IN A 192.0.2.3\n",
+                         ], l)
+
     def __prereq_helper(self, method, expected, rrset):
     def __prereq_helper(self, method, expected, rrset):
         '''Calls the given method with self.__datasrc_client
         '''Calls the given method with self.__datasrc_client
            and the given rrset, and compares the return value.
            and the given rrset, and compares the return value.
@@ -396,6 +440,60 @@ class SessionTest(unittest.TestCase):
         else:
         else:
             self.assertEqual(UPDATE_ERROR, result)
             self.assertEqual(UPDATE_ERROR, result)
 
 
+    # TODO: remove dupe with above one
+    def check_prescan_result(self, expected, updates, expected_soa = None):
+        '''Helper method for checking the result of a prerequisite check;
+           creates an update session, and fills it with the list of rrsets
+           from 'updates'. Then checks if __do_prescan()
+           returns the Rcode specified in 'expected'.'''
+        msg_data, msg = create_update_msg([TEST_ZONE_RECORD],
+                                          [], updates)
+        zconfig = ZoneConfig([], TEST_RRCLASS, self.__datasrc_client)
+        session = UpdateSession(msg, msg_data, TEST_CLIENT4, zconfig)
+        # compare the to_text output of the rcodes (nicer error messages)
+        # This call itself should also be done by handle(),
+        # but just for better failures, it is first called on its own
+        self.assertEqual(expected.to_text(),
+            session._UpdateSession__do_prescan(self.__datasrc_client,
+                                               TEST_ZONE_NAME,
+                                               TEST_RRCLASS).to_text())
+        # If there is an expected soa, check it
+        self.assertEqual(str(expected_soa),
+                         str(session._UpdateSession__added_soa))
+
+        # REMOVED, don't mess with actual data during prescan tests
+        # Now see if handle finds the same result
+        #(result, _, _) = session.handle()
+        #self.assertEqual(expected,
+        #                 session._UpdateSession__message.get_rcode())
+        ## And that the result looks right
+        #if expected == Rcode.NOERROR():
+        #    self.assertEqual(UPDATE_SUCCESS, result)
+        #else:
+        #    self.assertEqual(UPDATE_ERROR, result)
+
+    # TODO XXX: remove dupe with above
+    def check_full_handle_result(self, expected, updates):
+        '''Helper method for checking the result of a full handle;
+           creates an update session, and fills it with the list of rrsets
+           from 'updates'. Then checks if __handle()
+           results in a response with rcode 'expected'.'''
+        msg_data, msg = create_update_msg([TEST_ZONE_RECORD],
+                                          [], updates)
+        zconfig = ZoneConfig([], TEST_RRCLASS, self.__datasrc_client)
+        session = UpdateSession(msg, msg_data, TEST_CLIENT4, zconfig)
+
+        # Now see if handle finds the same result
+        (result, _, _) = session.handle()
+        self.assertEqual(expected.to_text(),
+                         session._UpdateSession__message.get_rcode().to_text())
+        # And that the result looks right
+        if expected == Rcode.NOERROR():
+            self.assertEqual(UPDATE_SUCCESS, result)
+        else:
+            self.assertEqual(UPDATE_ERROR, result)
+
+
     def test_check_prerequisites(self):
     def test_check_prerequisites(self):
         # This test checks if the actual prerequisite-type-specific
         # This test checks if the actual prerequisite-type-specific
         # methods are called.
         # methods are called.
@@ -599,6 +697,398 @@ class SessionTest(unittest.TestCase):
                                       "foo"))
                                       "foo"))
         self.check_prerequisite_result(Rcode.FORMERR(), [ rrset ])
         self.check_prerequisite_result(Rcode.FORMERR(), [ rrset ])
 
 
+
+    def __prereq_helper(self, method, expected, rrset):
+        '''Calls the given method with self.__datasrc_client
+           and the given rrset, and compares the return value.
+           Function does not do much but makes the code look nicer'''
+        self.assertEqual(expected, method(self.__datasrc_client, rrset))
+
+    def initialize_update_rrsets(self):
+        '''Prepare a number of RRsets to be used in several update tests
+           The rrsets are stored in self'''
+        rrset_update_a = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                                            TEST_RRCLASS,
+                                            isc.dns.RRType.A(),
+                                            isc.dns.RRTTL(3600))
+        rrset_update_a.add_rdata(isc.dns.Rdata(rrset_update_a.get_type(),
+                                               rrset_update_a.get_class(),
+                                               "192.0.2.2"))
+        rrset_update_a.add_rdata(isc.dns.Rdata(rrset_update_a.get_type(),
+                                               rrset_update_a.get_class(),
+                                               "192.0.2.3"))
+        self.rrset_update_a = rrset_update_a
+
+        rrset_update_soa = isc.dns.RRset(isc.dns.Name("example.org"),
+                                         TEST_RRCLASS,
+                                         isc.dns.RRType.SOA(),
+                                         isc.dns.RRTTL(3600))
+        rrset_update_soa.add_rdata(isc.dns.Rdata(rrset_update_soa.get_type(),
+                                                 rrset_update_soa.get_class(),
+                                                 "ns1.example.org. " +
+                                                 "admin.example.org. " +
+                                                 "1233 3600 1800 2419200 7200"))
+        self.rrset_update_soa = rrset_update_soa
+
+        rrset_update_soa_del = isc.dns.RRset(isc.dns.Name("example.org"),
+                                             isc.dns.RRClass.NONE(),
+                                             isc.dns.RRType.SOA(),
+                                             isc.dns.RRTTL(0))
+        rrset_update_soa_del.add_rdata(isc.dns.Rdata(rrset_update_soa_del.get_type(),
+                                                 rrset_update_soa_del.get_class(),
+                                                 "ns1.example.org. " +
+                                                 "admin.example.org. " +
+                                                 "1233 3600 1800 2419200 7200"))
+        self.rrset_update_soa_del = rrset_update_soa_del
+
+
+        rrset_update_soa2 = isc.dns.RRset(isc.dns.Name("example.org"),
+                                         TEST_RRCLASS,
+                                         isc.dns.RRType.SOA(),
+                                         isc.dns.RRTTL(3600))
+        rrset_update_soa2.add_rdata(isc.dns.Rdata(rrset_update_soa.get_type(),
+                                                 rrset_update_soa.get_class(),
+                                                 "ns1.example.org. " +
+                                                 "admin.example.org. " +
+                                                 "4000 3600 1800 2419200 7200"))
+        self.rrset_update_soa2 = rrset_update_soa2
+
+        rrset_update_del_name = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                                                   isc.dns.RRClass.ANY(),
+                                                   isc.dns.RRType.ANY(),
+                                                   isc.dns.RRTTL(0))
+        self.rrset_update_del_name = rrset_update_del_name
+
+        rrset_update_del_name_apex = isc.dns.RRset(isc.dns.Name("example.org"),
+                                                   isc.dns.RRClass.ANY(),
+                                                   isc.dns.RRType.ANY(),
+                                                   isc.dns.RRTTL(0))
+        self.rrset_update_del_name_apex = rrset_update_del_name_apex
+
+        rrset_update_del_rrset = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                                                   isc.dns.RRClass.ANY(),
+                                                   isc.dns.RRType.A(),
+                                                   isc.dns.RRTTL(0))
+        self.rrset_update_del_rrset = rrset_update_del_rrset
+
+        rrset_update_del_rrset_apex = isc.dns.RRset(isc.dns.Name("example.org"),
+                                                    isc.dns.RRClass.ANY(),
+                                                    isc.dns.RRType.A(),
+                                                    isc.dns.RRTTL(0))
+        self.rrset_update_del_rrset_apex = rrset_update_del_rrset_apex
+
+        rrset_update_del_rrset_part = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                                                    isc.dns.RRClass.NONE(),
+                                                    isc.dns.RRType.A(),
+                                                    isc.dns.RRTTL(0))
+        rrset_update_del_rrset_part.add_rdata(isc.dns.Rdata(rrset_update_a.get_type(),
+                                               rrset_update_del_rrset_part.get_class(),
+                                               "\# 04 c0 00 02 02"))
+        rrset_update_del_rrset_part.add_rdata(isc.dns.Rdata(rrset_update_a.get_type(),
+                                               rrset_update_del_rrset_part.get_class(),
+                                               "\# 04 c0 00 02 03"))
+        self.rrset_update_del_rrset_part = rrset_update_del_rrset_part
+
+        rrset_update_del_rrset_ns = isc.dns.RRset(isc.dns.Name("example.org"),
+                                                    isc.dns.RRClass.NONE(),
+                                                    isc.dns.RRType.NS(),
+                                                    isc.dns.RRTTL(0))
+        rrset_update_del_rrset_ns.add_rdata(isc.dns.Rdata(rrset_update_del_rrset_ns.get_type(),
+                                            rrset_update_del_rrset_ns.get_class(),
+                                            b'\x03ns1\x07example\x03org\x00'))
+        rrset_update_del_rrset_ns.add_rdata(isc.dns.Rdata(rrset_update_del_rrset_ns.get_type(),
+                                            rrset_update_del_rrset_ns.get_class(),
+                                            b'\x03ns2\x07example\x03org\x00'))
+        rrset_update_del_rrset_ns.add_rdata(isc.dns.Rdata(rrset_update_del_rrset_ns.get_type(),
+                                            rrset_update_del_rrset_ns.get_class(),
+                                            b'\x03ns3\x07example\x03org\x00'))
+        self.rrset_update_del_rrset_ns = rrset_update_del_rrset_ns
+
+
+    def test_prescan(self):
+        '''Test whether the prescan succeeds on data that is ok, and whether
+           if notices the SOA if present'''
+        # prepare a set of correct update statements
+        self.initialize_update_rrsets()
+
+        self.check_prescan_result(Rcode.NOERROR(), [ self.rrset_update_a ])
+
+        # check if soa is noticed
+        self.check_prescan_result(Rcode.NOERROR(), [ self.rrset_update_soa ],
+                                  self.rrset_update_soa)
+
+        # Other types of succesful prechecks
+        self.check_prescan_result(Rcode.NOERROR(), [ self.rrset_update_soa2 ],
+                                  self.rrset_update_soa2)
+        self.check_prescan_result(Rcode.NOERROR(),
+                                  [ self.rrset_update_del_name ])
+        self.check_prescan_result(Rcode.NOERROR(),
+                                  [ self.rrset_update_del_name_apex ])
+        self.check_prescan_result(Rcode.NOERROR(), [ self.rrset_update_del_rrset ])
+        self.check_prescan_result(Rcode.NOERROR(), [ self.rrset_update_del_rrset_apex ])
+        self.check_prescan_result(Rcode.NOERROR(), [ self.rrset_update_del_rrset_part ])
+
+        # and check a few permutations of the above
+        # all of them (with one of the soas)
+        self.check_prescan_result(Rcode.NOERROR(),
+                                  [
+                                    self.rrset_update_a,
+                                    self.rrset_update_soa,
+                                    self.rrset_update_del_name,
+                                    self.rrset_update_del_name_apex,
+                                    self.rrset_update_del_rrset,
+                                    self.rrset_update_del_rrset_apex,
+                                    self.rrset_update_del_rrset_part
+                                  ],
+                                  self.rrset_update_soa)
+
+        # Two soas. Should we reject or simply use the last?
+        # (RFC is not really explicit on this, but between the lines I read
+        # use the last)
+        # TODO this fails ;)
+        self.check_prescan_result(Rcode.NOERROR(),
+                                  [ self.rrset_update_soa,
+                                    self.rrset_update_soa2 ],
+                                    self.rrset_update_soa2)
+        self.check_prescan_result(Rcode.NOERROR(),
+                                  [ self.rrset_update_soa2,
+                                    self.rrset_update_soa ],
+                                  self.rrset_update_soa)
+
+        self.check_prescan_result(Rcode.NOERROR(),
+                                  [
+                                    self.rrset_update_del_rrset_apex,
+                                    self.rrset_update_del_name,
+                                    self.rrset_update_del_name_apex,
+                                    self.rrset_update_del_rrset_part,
+                                    self.rrset_update_a,
+                                    self.rrset_update_del_rrset,
+                                    self.rrset_update_soa
+                                  ],
+                                  self.rrset_update_soa)
+
+    def test_prescan_failures(self):
+        '''Test whether prescan fails on bad data'''
+        # out of zone data
+        rrset = isc.dns.RRset(isc.dns.Name("different.zone"),
+                              isc.dns.RRClass.ANY(),
+                              isc.dns.RRType.TXT(),
+                              isc.dns.RRTTL(0));
+        self.check_prescan_result(Rcode.NOTZONE(), [ rrset ])
+
+
+        # forbidden type, zone class
+        rrset = isc.dns.RRset(TEST_ZONE_NAME,
+                              TEST_RRCLASS,
+                              isc.dns.RRType.ANY(),
+                              isc.dns.RRTTL(0));
+        rrset.add_rdata(isc.dns.Rdata(rrset.get_type(), rrset.get_class(),
+                        "\# 00"))
+        self.check_prescan_result(Rcode.FORMERR(), [ rrset ])
+
+        # non-zero TTL, class ANY
+        rrset = isc.dns.RRset(TEST_ZONE_NAME,
+                              isc.dns.RRClass.ANY(),
+                              isc.dns.RRType.TXT(),
+                              isc.dns.RRTTL(1));
+        self.check_prescan_result(Rcode.FORMERR(), [ rrset ])
+
+        # non-zero Rdata, class ANY
+        rrset = isc.dns.RRset(TEST_ZONE_NAME,
+                              isc.dns.RRClass.ANY(),
+                              isc.dns.RRType.TXT(),
+                              isc.dns.RRTTL(0));
+        rrset.add_rdata(isc.dns.Rdata(rrset.get_type(), rrset.get_class(),
+                                      "foo"))
+        self.check_prescan_result(Rcode.FORMERR(), [ rrset ])
+
+        # forbidden type, class ANY
+        rrset = isc.dns.RRset(TEST_ZONE_NAME,
+                              isc.dns.RRClass.ANY(),
+                              isc.dns.RRType.AXFR(),
+                              isc.dns.RRTTL(0));
+        rrset.add_rdata(isc.dns.Rdata(rrset.get_type(), rrset.get_class(),
+                                      "\# 00"))
+        self.check_prescan_result(Rcode.FORMERR(), [ rrset ])
+
+        # non-zero TTL, class NONE
+        rrset = isc.dns.RRset(TEST_ZONE_NAME,
+                              isc.dns.RRClass.NONE(),
+                              isc.dns.RRType.TXT(),
+                              isc.dns.RRTTL(1));
+        self.check_prescan_result(Rcode.FORMERR(), [ rrset ])
+
+        # forbidden type, class NONE
+        rrset = isc.dns.RRset(TEST_ZONE_NAME,
+                              isc.dns.RRClass.NONE(),
+                              isc.dns.RRType.AXFR(),
+                              isc.dns.RRTTL(0));
+        rrset.add_rdata(isc.dns.Rdata(rrset.get_type(), rrset.get_class(),
+                                      "\# 00"))
+        self.check_prescan_result(Rcode.FORMERR(), [ rrset ])
+
+    def check_inzone_data(self, expected_result, name, rrtype,
+                          expected_rrset = None):
+        '''Does a find on TEST_ZONE for the given rrset's name and type,
+           then checks if the result matches the expected result.
+           If so, and if expected_rrset is given, they are compared as
+           well.'''
+        _, finder = self.__datasrc_client.find_zone(TEST_ZONE_NAME)
+        result, found_rrset, _ = finder.find(name, rrtype,
+                                             finder.NO_WILDCARD |
+                                             finder.FIND_GLUE_OK)
+        self.assertEqual(expected_result, result)
+        # Sigh. Need rrsets.compare() again.
+        # To be sure, compare name, class, type, and ttl
+        if expected_rrset is not None:
+            self.assertEqual(expected_rrset.get_name(), found_rrset.get_name())
+            self.assertEqual(expected_rrset.get_class(), found_rrset.get_class())
+            self.assertEqual(expected_rrset.get_type(), found_rrset.get_type())
+            self.assertEqual(expected_rrset.get_ttl().to_text(), found_rrset.get_ttl().to_text())
+            expected_rdata = [ rdata.to_text() for rdata in expected_rrset.get_rdata() ]
+            found_rdata = [ rdata.to_text() for rdata in found_rrset.get_rdata() ]
+            expected_rdata.sort()
+            found_rdata.sort()
+            self.assertEqual(expected_rdata, found_rdata)
+
+    def check_inzone_data_all(self, expected_result, expected_rrset):
+        pass
+
+    def test_update_add_delete_rrset(self):
+        self.initialize_update_rrsets()
+
+        # initially, the www should only contain one rr
+        orig_a_rrset = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                                     TEST_RRCLASS,
+                                     isc.dns.RRType.A(),
+                                     isc.dns.RRTTL(3600))
+        orig_a_rrset.add_rdata(isc.dns.Rdata(orig_a_rrset.get_type(),
+                                             orig_a_rrset.get_class(),
+                                            "192.0.2.1"))
+
+        # during this test, we will extend it at some point
+        extended_a_rrset = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                                         TEST_RRCLASS,
+                                         isc.dns.RRType.A(),
+                                         isc.dns.RRTTL(3600))
+        extended_a_rrset.add_rdata(isc.dns.Rdata(extended_a_rrset.get_type(),
+                                                 extended_a_rrset.get_class(),
+                                                 "192.0.2.1"))
+        extended_a_rrset.add_rdata(isc.dns.Rdata(extended_a_rrset.get_type(),
+                                                 extended_a_rrset.get_class(),
+                                                 "192.0.2.2"))
+        extended_a_rrset.add_rdata(isc.dns.Rdata(extended_a_rrset.get_type(),
+                                                 extended_a_rrset.get_class(),
+                                                 "192.0.2.3"))
+
+        # Sanity check, make sure original data is really there before updates
+        self.check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
+                               isc.dns.Name("www.example.org"),
+                               isc.dns.RRType.A(),
+                               orig_a_rrset)
+
+        # Add two rrs
+        self.check_full_handle_result(Rcode.NOERROR(), [ self.rrset_update_a ])
+
+        self.check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
+                               isc.dns.Name("www.example.org"),
+                               isc.dns.RRType.A(),
+                               extended_a_rrset)
+
+        # Now delete those two again
+        self.check_full_handle_result(Rcode.NOERROR(), [ self.rrset_update_del_rrset_part ])
+        self.check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
+                               isc.dns.Name("www.example.org"),
+                               isc.dns.RRType.A(),
+                               orig_a_rrset)
+
+
+        # Check that if we update the SOA, it is updated to our value
+        self.check_full_handle_result(Rcode.NOERROR(), [ self.rrset_update_soa2 ])
+        self.check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
+                               isc.dns.Name("example.org"),
+                               isc.dns.RRType.SOA(),
+                               self.rrset_update_soa2)
+
+    def test_update_delete_name(self):
+        self.initialize_update_rrsets()
+
+        # And delete the entire name
+        self.check_full_handle_result(Rcode.NOERROR(), [ self.rrset_update_del_name ])
+        self.check_inzone_data(isc.datasrc.ZoneFinder.NXDOMAIN,
+                               isc.dns.Name("www.example.org"),
+                               isc.dns.RRType.A())
+
+    def test_update_apex_special_cases(self):
+        self.initialize_update_rrsets()
+
+        # the original SOA
+        orig_soa_rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                                       TEST_RRCLASS,
+                                       isc.dns.RRType.SOA(),
+                                       isc.dns.RRTTL(3600))
+        orig_soa_rrset.add_rdata(isc.dns.Rdata(orig_soa_rrset.get_type(),
+                                               orig_soa_rrset.get_class(),
+                                               "ns1.example.org. " +
+                                               "admin.example.org. " +
+                                               "1234 3600 1800 2419200 7200"))
+
+        # We will delete some of the NS records
+        orig_ns_rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                                      TEST_RRCLASS,
+                                      isc.dns.RRType.NS(),
+                                      isc.dns.RRTTL(3600))
+        orig_ns_rrset.add_rdata(isc.dns.Rdata(orig_ns_rrset.get_type(),
+                                              orig_ns_rrset.get_class(),
+                                              "ns1.example.org."))
+        orig_ns_rrset.add_rdata(isc.dns.Rdata(orig_ns_rrset.get_type(),
+                                              orig_ns_rrset.get_class(),
+                                              "ns2.example.org."))
+        orig_ns_rrset.add_rdata(isc.dns.Rdata(orig_ns_rrset.get_type(),
+                                              orig_ns_rrset.get_class(),
+                                              "ns3.example.org."))
+
+        short_ns_rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                                       TEST_RRCLASS,
+                                       isc.dns.RRType.NS(),
+                                       isc.dns.RRTTL(3600))
+        short_ns_rrset.add_rdata(isc.dns.Rdata(short_ns_rrset.get_type(),
+                                               short_ns_rrset.get_class(),
+                                               "ns3.example.org."))
+
+        # Sanity check, make sure original data is really there before updates
+        self.check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
+                               isc.dns.Name("example.org"),
+                               isc.dns.RRType.NS(),
+                               orig_ns_rrset)
+
+        # Check that we cannot delete the SOA record by direction deletion
+        #self.check_full_handle_result(Rcode.NOERROR(), [ self.rrset_update_soa_del ])
+        self.check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
+                               isc.dns.Name("example.org"),
+                               isc.dns.RRType.SOA(),
+                               orig_soa_rrset)
+
+        # And that soa and NS at apex are not deleted if you delete the apex by name
+        #self.check_full_handle_result(Rcode.NOERROR(), [ self.rrset_update_del_name_apex ])
+        self.check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
+                               isc.dns.Name("example.org"),
+                               isc.dns.RRType.SOA(),
+                               orig_soa_rrset)
+        self.check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
+                               isc.dns.Name("example.org"),
+                               isc.dns.RRType.NS(),
+                               orig_ns_rrset)
+
+        # And if we delete the NS at the apex specifically, it should still
+        # keep one record
+        self.check_full_handle_result(Rcode.NOERROR(), [ self.rrset_update_del_rrset_ns ])
+        self.check_inzone_data(isc.datasrc.ZoneFinder.SUCCESS,
+                               isc.dns.Name("example.org"),
+                               isc.dns.RRType.NS(),
+                               short_ns_rrset)
+
+
 if __name__ == "__main__":
 if __name__ == "__main__":
     isc.log.init("bind10")
     isc.log.init("bind10")
     isc.log.resetUnitTestRootLogger()
     isc.log.resetUnitTestRootLogger()