Browse Source

[master] Merge remote-tracking branch 'origin/trac1455'

Jelte Jansen 13 years ago
parent
commit
21d3d1aa2e

+ 34 - 0
src/lib/dns/message.cc

@@ -130,6 +130,11 @@ public:
                const RRClass& rrclass, const RRType& rrtype,
                const RRTTL& ttl, ConstRdataPtr rdata,
                Message::ParseOptions options);
+    // There are also times where an RR needs to be added that
+    // represents an empty RRset. There is no Rdata in that case
+    void addRR(Message::Section section, const Name& name,
+               const RRClass& rrclass, const RRType& rrtype,
+               const RRTTL& ttl, Message::ParseOptions options);
     void addEDNS(Message::Section section, const Name& name,
                  const RRClass& rrclass, const RRType& rrtype,
                  const RRTTL& ttl, const Rdata& rdata);
@@ -740,6 +745,17 @@ MessageImpl::parseSection(const Message::Section section,
         const RRClass rrclass(buffer.readUint16());
         const RRTTL ttl(buffer.readUint32());
         const size_t rdlen = buffer.readUint16();
+
+        // If class is ANY or NONE, rdlength may be zero, to signal
+        // an empty RRset.
+        // (the class check must be done to differentiate from RRTypes
+        // that can have zero length rdata
+        if ((rrclass == RRClass::ANY() || rrclass == RRClass::NONE()) &&
+            rdlen == 0) {
+            addRR(section, name, rrclass, rrtype, ttl, options);
+            ++added;
+            continue;
+        }
         ConstRdataPtr rdata = createRdata(rrtype, rrclass, buffer, rdlen);
 
         if (rrtype == RRType::OPT()) {
@@ -778,6 +794,24 @@ MessageImpl::addRR(Message::Section section, const Name& name,
 }
 
 void
+MessageImpl::addRR(Message::Section section, const Name& name,
+                   const RRClass& rrclass, const RRType& rrtype,
+                   const RRTTL& ttl, Message::ParseOptions options)
+{
+    if ((options & Message::PRESERVE_ORDER) == 0) {
+        vector<RRsetPtr>::iterator it =
+            find_if(rrsets_[section].begin(), rrsets_[section].end(),
+                    MatchRR(name, rrtype, rrclass));
+        if (it != rrsets_[section].end()) {
+            (*it)->setTTL(min((*it)->getTTL(), ttl));
+            return;
+        }
+    }
+    RRsetPtr rrset(new RRset(name, rrclass, rrtype, ttl));
+    rrsets_[section].push_back(rrset);
+}
+
+void
 MessageImpl::addEDNS(Message::Section section,  const Name& name,
                      const RRClass& rrclass, const RRType& rrtype,
                      const RRTTL& ttl, const Rdata& rdata)

+ 9 - 0
src/lib/dns/python/pydnspp.cc

@@ -221,6 +221,15 @@ initModulePart_Name(PyObject* mod) {
         NameComparisonResult::COMMONANCESTOR, "COMMONANCESTOR");
     addClassVariable(name_comparison_result_type, "NameRelation",
                      po_NameRelation);
+    // Add the constants themselves too
+    addClassVariable(name_comparison_result_type, "SUPERDOMAIN",
+                     Py_BuildValue("I", NameComparisonResult::SUPERDOMAIN));
+    addClassVariable(name_comparison_result_type, "SUBDOMAIN",
+                     Py_BuildValue("I", NameComparisonResult::SUBDOMAIN));
+    addClassVariable(name_comparison_result_type, "EQUAL",
+                     Py_BuildValue("I", NameComparisonResult::EQUAL));
+    addClassVariable(name_comparison_result_type, "COMMONANCESTOR",
+                     Py_BuildValue("I", NameComparisonResult::COMMONANCESTOR));
 
     PyModule_AddObject(mod, "NameComparisonResult",
         reinterpret_cast<PyObject*>(&name_comparison_result_type));

+ 9 - 0
src/lib/dns/python/tests/rrset_python_test.py

@@ -30,6 +30,7 @@ class TestModuleSpec(unittest.TestCase):
         self.test_nsname = Name("ns.example.com")
         self.rrset_a = RRset(self.test_name, RRClass("IN"), RRType("A"), RRTTL(3600))
         self.rrset_a_empty = RRset(self.test_name, RRClass("IN"), RRType("A"), RRTTL(3600))
+        self.rrset_any_a_empty = RRset(self.test_name, RRClass("ANY"), RRType("A"), RRTTL(3600))
         self.rrset_ns = RRset(self.test_domain, RRClass("IN"), RRType("NS"), RRTTL(86400))
         self.rrset_ch_txt = RRset(self.test_domain, RRClass("CH"), RRType("TXT"), RRTTL(0))
         self.MAX_RDATA_COUNT = 100
@@ -90,6 +91,9 @@ class TestModuleSpec(unittest.TestCase):
 
         self.assertRaises(EmptyRRset, self.rrset_a_empty.to_text)
 
+        self.assertEqual("test.example.com. 3600 ANY A\n",
+                         self.rrset_any_a_empty.to_text())
+
     def test_to_wire_buffer(self):
         exp_buffer = bytearray(b'\x04test\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x04\xc0\x00\x02\x01\x04test\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x04\xc0\x00\x02\x02')
         buffer = bytearray()
@@ -99,6 +103,11 @@ class TestModuleSpec(unittest.TestCase):
         self.assertRaises(EmptyRRset, self.rrset_a_empty.to_wire, buffer);
         self.assertRaises(TypeError, self.rrset_a.to_wire, 1)
 
+        exp_buffer = bytearray(b'\x04test\x07example\x03com\x00\x00\x01\x00\xff\x00\x00\x0e\x10\x00\x00')
+        buffer = bytearray()
+        self.rrset_any_a_empty.to_wire(buffer)
+        self.assertEqual(exp_buffer, buffer)
+
     def test_to_wire_renderer(self):
         exp_buffer = bytearray(b'\x04test\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x04\xc0\x00\x02\x01\xc0\x00\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x04\xc0\x00\x02\x02')
         mr = MessageRenderer()

+ 28 - 4
src/lib/dns/rrset.cc

@@ -43,14 +43,24 @@ AbstractRRset::toText() const {
     string s;
     RdataIteratorPtr it = getRdataIterator();
 
+    // In the case of an empty rrset, just print name, ttl, class, and
+    // type
     if (it->isLast()) {
-        isc_throw(EmptyRRset, "ToText() is attempted for an empty RRset");
+        // But only for class ANY or NONE
+        if (getClass() != RRClass::ANY() &&
+            getClass() != RRClass::NONE()) {
+            isc_throw(EmptyRRset, "toText() is attempted for an empty RRset");
+        }
+
+        s += getName().toText() + " " + getTTL().toText() + " " +
+             getClass().toText() + " " + getType().toText() + "\n";
+        return (s);
     }
 
     do {
         s += getName().toText() + " " + getTTL().toText() + " " +
-            getClass().toText() + " " + getType().toText() + " " +
-            it->getCurrent().toText() + "\n";
+             getClass().toText() + " " + getType().toText() + " " +
+             it->getCurrent().toText() + "\n";
         it->next();
     } while (!it->isLast());
 
@@ -65,7 +75,21 @@ rrsetToWire(const AbstractRRset& rrset, T& output, const size_t limit) {
     RdataIteratorPtr it = rrset.getRdataIterator();
 
     if (it->isLast()) {
-        isc_throw(EmptyRRset, "ToWire() is attempted for an empty RRset");
+        // empty rrsets are only allowed for classes ANY and NONE
+        if (rrset.getClass() != RRClass::ANY() &&
+            rrset.getClass() != RRClass::NONE()) {
+            isc_throw(EmptyRRset, "toWire() is attempted for an empty RRset");
+        }
+
+        // For an empty RRset, write the name, type, class and TTL once,
+        // followed by empty rdata.
+        rrset.getName().toWire(output);
+        rrset.getType().toWire(output);
+        rrset.getClass().toWire(output);
+        rrset.getTTL().toWire(output);
+        output.writeUint16(0);
+        // Still counts as 1 'rr'; it does show up in the message
+        return (1);
     }
 
     // sort the set of Rdata based on rrset-order and sortlist, and possible

+ 4 - 4
src/lib/dns/rrset.h

@@ -267,8 +267,8 @@ public:
     /// the resulting string with a trailing newline character.
     /// (following the BIND9 convention)
     ///
-    /// The RRset must contain some RDATA; otherwise, an exception of class
-    /// \c EmptyRRset will be thrown.
+    /// If the class is not ANY or NONE, the RRset must contain some RDATA;
+    /// otherwise, an exception of class \c EmptyRRset will be thrown.
     /// If resource allocation fails, a corresponding standard exception
     /// will be thrown.
     /// The default implementation may throw other exceptions if the
@@ -299,8 +299,8 @@ public:
     ///
     /// If resource allocation fails, a corresponding standard exception
     /// will be thrown.
-    /// The RRset must contain some RDATA; otherwise, an exception of class
-    /// \c EmptyRRset will be thrown.
+    /// If the class is not ANY or NONE, the RRset must contain some RDATA;
+    /// otherwise, an exception of class \c EmptyRRset will be thrown.
     /// The default implementation may throw other exceptions if the
     /// \c toWire() method of the RDATA objects throws.
     /// If a derived class of \c AbstractRRset overrides the default

+ 45 - 3
src/lib/dns/tests/rrset_unittest.cc

@@ -46,6 +46,10 @@ protected:
                   rrset_a(test_name, RRClass::IN(), RRType::A(), RRTTL(3600)),
                   rrset_a_empty(test_name, RRClass::IN(), RRType::A(),
                                 RRTTL(3600)),
+                  rrset_any_a_empty(test_name, RRClass::ANY(), RRType::A(),
+                                    RRTTL(3600)),
+                  rrset_none_a_empty(test_name, RRClass::NONE(), RRType::A(),
+                                     RRTTL(3600)),
                   rrset_ns(test_domain, RRClass::IN(), RRType::NS(),
                            RRTTL(86400)),
                   rrset_ch_txt(test_domain, RRClass::CH(), RRType::TXT(),
@@ -62,6 +66,8 @@ protected:
     Name test_nsname;
     RRset rrset_a;
     RRset rrset_a_empty;
+    RRset rrset_any_a_empty;
+    RRset rrset_none_a_empty;
     RRset rrset_ns;
     RRset rrset_ch_txt;
     std::vector<unsigned char> wiredata;
@@ -193,8 +199,14 @@ TEST_F(RRsetTest, toText) {
               "test.example.com. 3600 IN A 192.0.2.2\n",
               rrset_a.toText());
 
-    // toText() cannot be performed for an empty RRset.
+    // toText() cannot be performed for an empty RRset
     EXPECT_THROW(rrset_a_empty.toText(), EmptyRRset);
+
+    // Unless it is type ANY or NONE
+    EXPECT_EQ("test.example.com. 3600 ANY A\n",
+              rrset_any_a_empty.toText());
+    EXPECT_EQ("test.example.com. 3600 CLASS254 A\n",
+              rrset_none_a_empty.toText());
 }
 
 TEST_F(RRsetTest, toWireBuffer) {
@@ -207,6 +219,20 @@ TEST_F(RRsetTest, toWireBuffer) {
     // toWire() cannot be performed for an empty RRset.
     buffer.clear();
     EXPECT_THROW(rrset_a_empty.toWire(buffer), EmptyRRset);
+
+    // Unless it is type ANY or None
+    buffer.clear();
+    rrset_any_a_empty.toWire(buffer);
+    wiredata.clear();
+    UnitTestUtil::readWireData("rrset_toWire3", wiredata);
+    EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, buffer.getData(),
+                        buffer.getLength(), &wiredata[0], wiredata.size());
+    buffer.clear();
+    rrset_none_a_empty.toWire(buffer);
+    wiredata.clear();
+    UnitTestUtil::readWireData("rrset_toWire4", wiredata);
+    EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, buffer.getData(),
+                        buffer.getLength(), &wiredata[0], wiredata.size());
 }
 
 TEST_F(RRsetTest, toWireRenderer) {
@@ -220,8 +246,24 @@ TEST_F(RRsetTest, toWireRenderer) {
                         renderer.getLength(), &wiredata[0], wiredata.size());
 
     // toWire() cannot be performed for an empty RRset.
-    renderer.clear();
-    EXPECT_THROW(rrset_a_empty.toWire(renderer), EmptyRRset);
+    buffer.clear();
+    EXPECT_THROW(rrset_a_empty.toWire(buffer), EmptyRRset);
+
+    // Unless it is type ANY or None
+    // toWire() can also be performed for an empty RRset.
+    buffer.clear();
+    rrset_any_a_empty.toWire(buffer);
+    wiredata.clear();
+    UnitTestUtil::readWireData("rrset_toWire3", wiredata);
+    EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, buffer.getData(),
+                        buffer.getLength(), &wiredata[0], wiredata.size());
+
+    buffer.clear();
+    rrset_none_a_empty.toWire(buffer);
+    wiredata.clear();
+    UnitTestUtil::readWireData("rrset_toWire4", wiredata);
+    EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, buffer.getData(),
+                        buffer.getLength(), &wiredata[0], wiredata.size());
 }
 
 // test operator<<.  We simply confirm it appends the result of toText().

+ 1 - 0
src/lib/dns/tests/testdata/Makefile.am

@@ -146,6 +146,7 @@ EXTRA_DIST += rdata_txt_fromWire5.spec rdata_unknown_fromWire
 EXTRA_DIST += rrcode16_fromWire1 rrcode16_fromWire2
 EXTRA_DIST += rrcode32_fromWire1 rrcode32_fromWire2
 EXTRA_DIST += rrset_toWire1 rrset_toWire2
+EXTRA_DIST += rrset_toWire3 rrset_toWire4
 EXTRA_DIST += rdata_tsig_fromWire1.spec rdata_tsig_fromWire2.spec
 EXTRA_DIST += rdata_tsig_fromWire3.spec rdata_tsig_fromWire4.spec
 EXTRA_DIST += rdata_tsig_fromWire5.spec rdata_tsig_fromWire6.spec

+ 12 - 0
src/lib/dns/tests/testdata/rrset_toWire3

@@ -0,0 +1,12 @@
+#
+# Rendering an empty IN/A RRset
+#
+#(4) t  e  s  t (7) e  x  a  m  p  l  e (3) c  o  m  .
+ 04 74 65 73 74 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00
+# type/class: A = 1, ANY = 255
+00 01 00 ff
+# TTL: 3600
+00 00 0e 10
+#6  7
+# RDLENGTH: 0
+00 00

+ 12 - 0
src/lib/dns/tests/testdata/rrset_toWire4

@@ -0,0 +1,12 @@
+#
+# Rendering an empty IN/A RRset
+#
+#(4) t  e  s  t (7) e  x  a  m  p  l  e (3) c  o  m  .
+ 04 74 65 73 74 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00
+# type/class: A = 1, ANY = 255
+00 01 00 fe
+# TTL: 3600
+00 00 0e 10
+#6  7
+# RDLENGTH: 0
+00 00

+ 2 - 1
src/lib/python/isc/datasrc/finder_python.cc

@@ -147,7 +147,8 @@ PyObject* ZoneFinder_helper_all(ZoneFinder* finder, PyObject* args) {
                 // increases the refcount and the container decreases it
                 // later. This way, it feels safer in case the build function
                 // would fail.
-                return (Py_BuildValue("IO", r, list_container.get()));
+                return (Py_BuildValue("IOI", r, list_container.get(),
+                                      result_flags));
             } else {
                 if (rrsp) {
                     // Use N instead of O so the refcount isn't increased twice

+ 1 - 1
src/lib/python/isc/datasrc/tests/datasrc_test.py

@@ -83,7 +83,7 @@ def test_findall_common(self, tested):
 
     # A success. It should return the list now.
     # This also tests we can ommit the options parameter
-    result, rrsets = tested.find_all(isc.dns.Name("mix.example.com."))
+    result, rrsets, _ = tested.find_all(isc.dns.Name("mix.example.com."))
     self.assertEqual(ZoneFinder.SUCCESS, result)
     self.assertEqual(2, len(rrsets))
     rrsets.sort(key=lambda rrset: rrset.get_type().to_text())

+ 85 - 0
src/lib/python/isc/ddns/libddns_messages.mes

@@ -15,6 +15,84 @@
 # No namespace declaration - these constants go in the global namespace
 # of the libddns_messages python module.
 
+% LIBDDNS_PREREQ_FORMERR update client %1 for zone %2: Format error in prerequisite (%3). Non-zero TTL.
+The prerequisite with the given name, class and type is not well-formed.
+The specific prerequisite is shown. In this case, it has a non-zero TTL value.
+A FORMERR error response is sent to the client.
+
+% LIBDDNS_PREREQ_FORMERR_ANY update client %1 for zone %2: Format error in prerequisite (%3). Non-zero TTL or rdata found.
+The prerequisite with the given name, class and type is not well-formed.
+The specific prerequisite is shown. In this case, it either has a non-zero
+TTL value, or has rdata fields. A FORMERR error response is sent to the client.
+
+% LIBDDNS_PREREQ_FORMERR_CLASS update client %1 for zone %2: Format error in prerequisite (%3). Bad class.
+The prerequisite with the given name, class and type is not well-formed.
+The specific prerequisite is shown. In this case, the class of the
+prerequisite should either match the class of the zone in the Zone Section,
+or it should be ANY or NONE, and it is not. A FORMERR error response is sent
+to the client.
+
+% LIBDDNS_PREREQ_FORMERR_NONE update client %1 for zone %2: Format error in prerequisite (%3). Non-zero TTL or rdata found.
+The prerequisite with the given name, class and type is not well-formed.
+The specific prerequisite is shown. In this case, it either has a non-zero
+TTL value, or has rdata fields. A FORMERR error response is sent to the client.
+
+% LIBDDNS_PREREQ_NAME_IN_USE_FAILED update client %1 for zone %2: 'Name is in use' prerequisite not satisfied (%3), rcode: %4
+A DNS UPDATE prerequisite was not satisfied. The specific prerequisite that
+was not satisfied is shown. The client is sent an error response with the
+given rcode.
+In this case, the specific prerequisite is 'Name is in use'. From RFC2136:
+Name is in use.  At least one RR with a specified NAME (in
+the zone and class specified by the Zone Section) must exist.
+Note that this prerequisite is NOT satisfied by empty
+nonterminals.
+
+% LIBDDNS_PREREQ_NAME_NOT_IN_USE_FAILED update client %1 for zone %2: 'Name is not in use' (%3) prerequisite not satisfied, rcode: %4
+A DNS UPDATE prerequisite was not satisfied. The specific prerequisite that
+was not satisfied is shown. The client is sent an error response with the
+given rcode.
+In this case, the specific prerequisite is 'Name is not in use'.
+From RFC2136:
+Name is not in use.  No RR of any type is owned by a
+specified NAME.  Note that this prerequisite IS satisfied by
+empty nonterminals.
+
+% LIBDDNS_PREREQ_NOTZONE update client %1 for zone %2: prerequisite not in zone (%3)
+A DNS UPDATE prerequisite has a name that does not appear to be inside
+the zone specified in the Zone section of the UPDATE message.
+The specific prerequisite is shown. A NOTZONE error response is sent to
+the client.
+
+% LIBDDNS_PREREQ_RRSET_DOES_NOT_EXIST_FAILED update client %1 for zone %2: 'RRset does not exist' (%3) prerequisite not satisfied, rcode: %4
+A DNS UPDATE prerequisite was not satisfied. The specific prerequisite that
+was not satisfied is shown. The client is sent an error response with the
+given rcode.
+In this case, the specific prerequisite is 'RRset does not exist'.
+From RFC2136:
+RRset does not exist.  No RRs with a specified NAME and TYPE
+(in the zone and class denoted by the Zone Section) can exist.
+
+% LIBDDNS_PREREQ_RRSET_EXISTS_FAILED update client %1 for zone %2: 'RRset exists (value independent)' (%3) prerequisite not satisfied, rcode: %4
+A DNS UPDATE prerequisite was not satisfied. The specific prerequisite that
+was not satisfied is shown. The client is sent an error response with the
+given rcode.
+In this case, the specific prerequisite is 'RRset exists (value independent)'.
+From RFC2136:
+RRset exists (value dependent).  A set of RRs with a
+specified NAME and TYPE exists and has the same members
+with the same RDATAs as the RRset specified here in this
+Section.
+
+% LIBDDNS_PREREQ_RRSET_EXISTS_VAL_FAILED update client %1 for zone %2: 'RRset exists (value dependent)' (%3) prerequisite not satisfied, rcode: %4
+A DNS UPDATE prerequisite was not satisfied. The specific prerequisite that
+was not satisfied is shown. The client is sent an error response with the
+given rcode.
+In this case, the specific prerequisite is 'RRset exists (value dependent)'.
+From RFC2136:
+RRset exists (value independent).  At least one RR with a
+specified NAME and TYPE (in the zone and class specified by
+the Zone Section) must exist.
+
 % LIBDDNS_UPDATE_ERROR update client %1 for zone %2: %3
 Debug message.  An error is found in processing a dynamic update
 request.  This log message is used for general errors that are not
@@ -39,3 +117,10 @@ possible, you may want to check the implementation or configuration of
 those clients to suppress the requests.  As specified in Section 3.1
 of RFC2136, the receiving server will return a response with an RCODE
 of NOTAUTH.
+
+% LIBDDNS_UPDATE_PREREQUISITE_FAILED prerequisite failed in update update client %1 for zone %2: result code %3
+The handling of the prerequisite section (RFC2136 Section 3.2) found
+that one of the prerequisites was not satisfied. The result code
+should give more information on what prerequisite type failed.
+If the result code is FORMERR, the prerequisite section was not well-formed.
+An error response with the given result code is sent back to the client.

+ 25 - 0
src/lib/python/isc/ddns/logger.py

@@ -85,3 +85,28 @@ class ZoneFormatter:
         if self.__zname is None:
             return '(zone unknown/not determined)'
         return self.__zname.to_text(True) + '/' + self.__zclass.to_text()
+
+class RRsetFormatter:
+    """A utility class to convert rrsets to a short descriptive string.
+
+    This class is constructed with an rrset (isc.dns.RRset object).
+    Its text conversion method (__str__) converts it into a string
+    with only the name, class and type of the rrset.
+    This is used in logging so that the RRset can be identified, without
+    being completely printed, which would result in an unnecessary
+    multi-line message.
+
+    This class is designed to delay the conversion until it's explicitly
+    requested, so the conversion doesn't happen if the corresponding log
+    message is suppressed because of its log level.
+
+    See the note for the ClientFormatter class about overhead tradeoff.
+    This class shares the same discussion.
+    """
+    def __init__(self, rrset):
+        self.__rrset = rrset
+
+    def __str__(self):
+        return self.__rrset.get_name().to_text() + " " +\
+               self.__rrset.get_class().to_text() + " " +\
+               self.__rrset.get_type().to_text()

+ 185 - 2
src/lib/python/isc/ddns/session.py

@@ -16,8 +16,10 @@
 from isc.dns import *
 import isc.ddns.zone_config
 from isc.log import *
-from isc.ddns.logger import logger, ClientFormatter, ZoneFormatter
+from isc.ddns.logger import logger, ClientFormatter, ZoneFormatter,\
+                            RRsetFormatter
 from isc.log_messages.libddns_messages import *
+import copy
 
 # Result codes for UpdateSession.handle()
 UPDATE_SUCCESS = 0
@@ -123,7 +125,11 @@ class UpdateSession:
         try:
             datasrc_client, zname, zclass = self.__get_update_zone()
             # conceptual code that would follow
-            # self.__check_prerequisites()
+            prereq_result = self.__check_prerequisites(datasrc_client,
+                                                       zname, zclass)
+            if prereq_result != Rcode.NOERROR():
+                self.__make_response(prereq_result)
+                return UPDATE_ERROR, zname, zclass
             # self.__check_update_acl()
             # self.__do_update()
             # self.__make_response(Rcode.NOERROR())
@@ -187,3 +193,180 @@ class UpdateSession:
         self.__message.make_response()
         self.__message.clear_section(SECTION_ZONE)
         self.__message.set_rcode(rcode)
+
+    def __prereq_rrset_exists(self, datasrc_client, rrset):
+        '''Check whether an rrset with the given name and type exists. Class,
+           TTL, and Rdata (if any) of the given RRset are ignored.
+           RFC2136 Section 2.4.1.
+           Returns True if the prerequisite is satisfied, False otherwise.
+
+           Note: the only thing used in the call to find() here is the
+           result status. The actual data is immediately dropped. As
+           a future optimization, we may want to add a find() option to
+           only return what the result code would be (and not read/copy
+           any actual data).
+        '''
+        _, finder = datasrc_client.find_zone(rrset.get_name())
+        result, _, _ = finder.find(rrset.get_name(), rrset.get_type(),
+                                   finder.NO_WILDCARD | finder.FIND_GLUE_OK)
+        return result == finder.SUCCESS
+
+    def __prereq_rrset_exists_value(self, datasrc_client, rrset):
+        '''Check whether an rrset that matches name, type, and rdata(s) of the
+           given rrset exists.
+           RFC2136 Section 2.4.2
+           Returns True if the prerequisite is satisfied, False otherwise.
+        '''
+        _, finder = datasrc_client.find_zone(rrset.get_name())
+        result, found_rrset, _ = finder.find(rrset.get_name(), rrset.get_type(),
+                                             finder.NO_WILDCARD |
+                                             finder.FIND_GLUE_OK)
+        if result == finder.SUCCESS and\
+           rrset.get_name() == found_rrset.get_name() and\
+           rrset.get_type() == found_rrset.get_type():
+            # We need to match all actual RRs, unfortunately there is no
+            # direct order-independent comparison for rrsets, so this
+            # a slightly inefficient way to handle that.
+
+            # shallow copy of the rdata list, so we are sure that this
+            # loop does not mess with actual data.
+            found_rdata = copy.copy(found_rrset.get_rdata())
+            for rdata in rrset.get_rdata():
+                if rdata in found_rdata:
+                    found_rdata.remove(rdata)
+                else:
+                    return False
+            return len(found_rdata) == 0
+        return False
+
+    def __prereq_rrset_does_not_exist(self, datasrc_client, rrset):
+        '''Check whether no rrsets with the same name and type as the given
+           rrset exist.
+           RFC2136 Section 2.4.3.
+           Returns True if the prerequisite is satisfied, False otherwise.
+        '''
+        return not self.__prereq_rrset_exists(datasrc_client, rrset)
+
+    def __prereq_name_in_use(self, datasrc_client, rrset):
+        '''Check whether the name of the given RRset is in use (i.e. has
+           1 or more RRs).
+           RFC2136 Section 2.4.4
+           Returns True if the prerequisite is satisfied, False otherwise.
+
+           Note: the only thing used in the call to find_all() here is
+           the result status. The actual data is immediately dropped. As
+           a future optimization, we may want to add a find_all() option
+           to only return what the result code would be (and not read/copy
+           any actual data).
+        '''
+        _, 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):
+            return True
+        return False
+
+    def __prereq_name_not_in_use(self, datasrc_client, rrset):
+        '''Check whether the name of the given RRset is not in use (i.e. does
+           not exist at all, or is an empty nonterminal.
+           RFC2136 Section 2.4.5.
+           Returns True if the prerequisite is satisfied, False otherwise.
+        '''
+        return not self.__prereq_name_in_use(datasrc_client, rrset)
+
+    def __check_prerequisites(self, datasrc_client, zname, zclass):
+        '''Check the prerequisites section of the UPDATE Message.
+           RFC2136 Section 2.4.
+           Returns a dns Rcode signaling either no error (Rcode.NOERROR())
+           or that one of the prerequisites failed (any other Rcode).
+        '''
+        for rrset in self.__message.get_section(SECTION_PREREQUISITE):
+            # 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:
+                logger.info(LIBDDNS_PREREQ_NOTZONE,
+                            ClientFormatter(self.__client_addr),
+                            ZoneFormatter(zname, zclass),
+                            RRsetFormatter(rrset))
+                return Rcode.NOTZONE()
+
+            # Algorithm taken from RFC2136 Section 3.2
+            if rrset.get_class() == RRClass.ANY():
+                if rrset.get_ttl().get_value() != 0 or\
+                   rrset.get_rdata_count() != 0:
+                    logger.info(LIBDDNS_PREREQ_FORMERR_ANY,
+                                ClientFormatter(self.__client_addr),
+                                ZoneFormatter(zname, zclass),
+                                RRsetFormatter(rrset))
+                    return Rcode.FORMERR()
+                elif rrset.get_type() == RRType.ANY():
+                    if not self.__prereq_name_in_use(datasrc_client,
+                                                     rrset):
+                        rcode = Rcode.NXDOMAIN()
+                        logger.info(LIBDDNS_PREREQ_NAME_IN_USE_FAILED,
+                                    ClientFormatter(self.__client_addr),
+                                    ZoneFormatter(zname, zclass),
+                                    RRsetFormatter(rrset), rcode)
+                        return rcode
+                else:
+                    if not self.__prereq_rrset_exists(datasrc_client, rrset):
+                        rcode = Rcode.NXRRSET()
+                        logger.info(LIBDDNS_PREREQ_RRSET_EXISTS_FAILED,
+                                    ClientFormatter(self.__client_addr),
+                                    ZoneFormatter(zname, zclass),
+                                    RRsetFormatter(rrset), rcode)
+                        return rcode
+            elif rrset.get_class() == RRClass.NONE():
+                if rrset.get_ttl().get_value() != 0 or\
+                   rrset.get_rdata_count() != 0:
+                    logger.info(LIBDDNS_PREREQ_FORMERR_NONE,
+                                ClientFormatter(self.__client_addr),
+                                ZoneFormatter(zname, zclass),
+                                RRsetFormatter(rrset))
+                    return Rcode.FORMERR()
+                elif rrset.get_type() == RRType.ANY():
+                    if not self.__prereq_name_not_in_use(datasrc_client,
+                                                         rrset):
+                        rcode = Rcode.YXDOMAIN()
+                        logger.info(LIBDDNS_PREREQ_NAME_NOT_IN_USE_FAILED,
+                                    ClientFormatter(self.__client_addr),
+                                    ZoneFormatter(zname, zclass),
+                                    RRsetFormatter(rrset), rcode)
+                        return rcode
+                else:
+                    if not self.__prereq_rrset_does_not_exist(datasrc_client,
+                                                              rrset):
+                        rcode = Rcode.YXRRSET()
+                        logger.info(LIBDDNS_PREREQ_RRSET_DOES_NOT_EXIST_FAILED,
+                                    ClientFormatter(self.__client_addr),
+                                    ZoneFormatter(zname, zclass),
+                                    RRsetFormatter(rrset), rcode)
+                        return rcode
+            elif rrset.get_class() == zclass:
+                if rrset.get_ttl().get_value() != 0:
+                    logger.info(LIBDDNS_PREREQ_FORMERR,
+                                ClientFormatter(self.__client_addr),
+                                ZoneFormatter(zname, zclass),
+                                RRsetFormatter(rrset))
+                    return Rcode.FORMERR()
+                else:
+                    if not self.__prereq_rrset_exists_value(datasrc_client,
+                                                            rrset):
+                        rcode = Rcode.NXRRSET()
+                        logger.info(LIBDDNS_PREREQ_RRSET_EXISTS_VAL_FAILED,
+                                    ClientFormatter(self.__client_addr),
+                                    ZoneFormatter(zname, zclass),
+                                    RRsetFormatter(rrset), rcode)
+                        return rcode
+            else:
+                logger.info(LIBDDNS_PREREQ_FORMERR_CLASS,
+                            ClientFormatter(self.__client_addr),
+                            ZoneFormatter(zname, zclass),
+                            RRsetFormatter(rrset))
+                return Rcode.FORMERR()
+
+        # All prerequisites are satisfied
+        return Rcode.NOERROR()

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

@@ -36,13 +36,15 @@ TEST_ZONE_RECORD = Question(TEST_ZONE_NAME, TEST_RRCLASS, UPDATE_RRTYPE)
 TEST_CLIENT6 = ('2001:db8::1', 53, 0, 0)
 TEST_CLIENT4 = ('192.0.2.1', 53)
 
-def create_update_msg(zones=[TEST_ZONE_RECORD]):
+def create_update_msg(zones=[TEST_ZONE_RECORD], prerequisites=[]):
     msg = Message(Message.RENDER)
     msg.set_qid(5353)           # arbitrary chosen
     msg.set_opcode(Opcode.UPDATE())
     msg.set_rcode(Rcode.NOERROR())
     for z in zones:
         msg.add_question(z)
+    for p in prerequisites:
+        msg.add_rrset(SECTION_PREREQUISITE, p)
 
     renderer = MessageRenderer()
     msg.to_wire(renderer)
@@ -148,6 +150,455 @@ class SessionTest(unittest.TestCase):
         # zone class doesn't match
         self.check_notauth(Name('example.org'), RRClass.CH())
 
+    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 __check_prerequisite_exists_combined(self, method, rrclass, expected):
+        '''shared code for the checks for the very similar (but reversed
+           in behaviour) methods __prereq_rrset_exists and
+           __prereq_rrset_does_not_exist.
+           For rrset_exists, rrclass should be ANY, for rrset_does_not_exist,
+           it should be NONE.
+        '''
+        # Basic existence checks
+        # www.example.org should have an A, but not an MX
+        rrset = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                              rrclass, isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, expected, rrset)
+        rrset = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                              rrclass, isc.dns.RRType.MX(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, not expected, rrset)
+
+        # example.org should have an MX, but not an A
+        rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                              rrclass, isc.dns.RRType.MX(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, expected, rrset)
+        rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                              rrclass, isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, not expected, rrset)
+
+        # Also check the case where the name does not even exist
+        rrset = isc.dns.RRset(isc.dns.Name("doesnotexist.example.org"),
+                              rrclass, isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, not expected, rrset)
+
+        # Wildcard expansion should not be applied, but literal matches
+        # should work
+        rrset = isc.dns.RRset(isc.dns.Name("foo.wildcard.example.org"),
+                              rrclass, isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, not expected, rrset)
+
+        rrset = isc.dns.RRset(isc.dns.Name("*.wildcard.example.org"),
+                              rrclass, isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, expected, rrset)
+
+        # Likewise, CNAME directly should match, but what it points to should
+        # not
+        rrset = isc.dns.RRset(isc.dns.Name("cname.example.org"),
+                              rrclass, isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, not expected, rrset)
+
+        rrset = isc.dns.RRset(isc.dns.Name("cname.example.org"),
+                              rrclass, isc.dns.RRType.CNAME(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, expected, rrset)
+
+        # And also make sure a delegation (itself) is not treated as existing
+        # data
+        rrset = isc.dns.RRset(isc.dns.Name("foo.sub.example.org"),
+                              rrclass, isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, not expected, rrset)
+        # But the delegation data itself should match
+        rrset = isc.dns.RRset(isc.dns.Name("sub.example.org"),
+                              rrclass, isc.dns.RRType.NS(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, expected, rrset)
+        # As should glue
+        rrset = isc.dns.RRset(isc.dns.Name("ns.sub.example.org"),
+                              rrclass, isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, expected, rrset)
+
+    def test_check_prerequisite_exists(self):
+        method = self.__session._UpdateSession__prereq_rrset_exists
+        self.__check_prerequisite_exists_combined(method,
+                                                  isc.dns.RRClass.ANY(),
+                                                  True)
+
+    def test_check_prerequisite_does_not_exist(self):
+        method = self.__session._UpdateSession__prereq_rrset_does_not_exist
+        self.__check_prerequisite_exists_combined(method,
+                                                  isc.dns.RRClass.NONE(),
+                                                  False)
+
+    def test_check_prerequisite_exists_value(self):
+        method = self.__session._UpdateSession__prereq_rrset_exists_value
+
+        rrset = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                              isc.dns.RRClass.IN(), isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        # empty one should not match
+        self.__prereq_helper(method, False, rrset)
+
+        # When the rdata is added, it should match
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.A(),
+                                      isc.dns.RRClass.IN(),
+                                      "192.0.2.1"))
+        self.__prereq_helper(method, True, rrset)
+
+        # But adding more should not
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.A(),
+                                      isc.dns.RRClass.IN(),
+                                      "192.0.2.2"))
+        self.__prereq_helper(method, False, rrset)
+
+        # Also test one with more than one RR
+        rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                              isc.dns.RRClass.IN(), isc.dns.RRType.NS(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, False, rrset)
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.NS(),
+                                      isc.dns.RRClass.IN(),
+                                      "ns1.example.org."))
+        self.__prereq_helper(method, False, rrset)
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.NS(),
+                                      isc.dns.RRClass.IN(),
+                                      "ns2.example.org."))
+        self.__prereq_helper(method, False, rrset)
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.NS(),
+                                      isc.dns.RRClass.IN(),
+                                      "ns3.example.org."))
+        self.__prereq_helper(method, True, rrset)
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.NS(),
+                                      isc.dns.RRClass.IN(),
+                                      "ns4.example.org."))
+        self.__prereq_helper(method, False, rrset)
+
+        # Repeat that, but try a different order of Rdata addition
+        rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                              isc.dns.RRClass.IN(), isc.dns.RRType.NS(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, False, rrset)
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.NS(),
+                                      isc.dns.RRClass.IN(),
+                                      "ns3.example.org."))
+        self.__prereq_helper(method, False, rrset)
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.NS(),
+                                      isc.dns.RRClass.IN(),
+                                      "ns2.example.org."))
+        self.__prereq_helper(method, False, rrset)
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.NS(),
+                                      isc.dns.RRClass.IN(),
+                                      "ns1.example.org."))
+        self.__prereq_helper(method, True, rrset)
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.NS(),
+                                      isc.dns.RRClass.IN(),
+                                      "ns4.example.org."))
+        self.__prereq_helper(method, False, rrset)
+
+        # and test one where the name does not even exist
+        rrset = isc.dns.RRset(isc.dns.Name("doesnotexist.example.org"),
+                              isc.dns.RRClass.IN(), isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.A(),
+                                      isc.dns.RRClass.IN(),
+                                      "192.0.2.1"))
+        self.__prereq_helper(method, False, rrset)
+
+    def __check_prerequisite_name_in_use_combined(self, method, rrclass,
+                                                  expected):
+        '''shared code for the checks for the very similar (but reversed
+           in behaviour) methods __prereq_name_in_use and
+           __prereq_name_not_in_use
+        '''
+        rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                              rrclass, isc.dns.RRType.ANY(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, expected, rrset)
+
+        rrset = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                              rrclass, isc.dns.RRType.ANY(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, expected, rrset)
+
+        rrset = isc.dns.RRset(isc.dns.Name("doesnotexist.example.org"),
+                              rrclass, isc.dns.RRType.ANY(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, not expected, rrset)
+
+        rrset = isc.dns.RRset(isc.dns.Name("belowdelegation.sub.example.org"),
+                              rrclass, isc.dns.RRType.ANY(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, not expected, rrset)
+
+        rrset = isc.dns.RRset(isc.dns.Name("foo.wildcard.example.org"),
+                              rrclass, isc.dns.RRType.ANY(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, not expected, rrset)
+
+        # empty nonterminal should not match
+        rrset = isc.dns.RRset(isc.dns.Name("nonterminal.example.org"),
+                              rrclass, isc.dns.RRType.ANY(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, not expected, rrset)
+        rrset = isc.dns.RRset(isc.dns.Name("empty.nonterminal.example.org"),
+                              rrclass, isc.dns.RRType.ANY(),
+                              isc.dns.RRTTL(0))
+        self.__prereq_helper(method, expected, rrset)
+
+    def test_check_prerequisite_name_in_use(self):
+        method = self.__session._UpdateSession__prereq_name_in_use
+        self.__check_prerequisite_name_in_use_combined(method,
+                                                       isc.dns.RRClass.ANY(),
+                                                       True)
+
+    def test_check_prerequisite_name_not_in_use(self):
+        method = self.__session._UpdateSession__prereq_name_not_in_use
+        self.__check_prerequisite_name_in_use_combined(method,
+                                                       isc.dns.RRClass.NONE(),
+                                                       False)
+
+    def check_prerequisite_result(self, expected, prerequisites):
+        '''Helper method for checking the result of a prerequisite check;
+           creates an update session, and fills it with the list of rrsets
+           from 'prerequisites'. Then checks if __check_prerequisites()
+           returns the Rcode specified in 'expected'.'''
+        msg_data, msg = create_update_msg([TEST_ZONE_RECORD],
+                                          prerequisites)
+        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__check_prerequisites(self.__datasrc_client,
+                                                        TEST_ZONE_NAME,
+                                                        TEST_RRCLASS).to_text())
+        # 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)
+
+    def test_check_prerequisites(self):
+        # This test checks if the actual prerequisite-type-specific
+        # methods are called.
+        # It does test all types of prerequisites, but it does not test
+        # every possible result for those types (those are tested above,
+        # in the specific prerequisite type tests)
+
+        # Let's first define a number of prereq's that should succeed
+        rrset_exists_yes = isc.dns.RRset(isc.dns.Name("example.org"),
+                                         isc.dns.RRClass.ANY(),
+                                         isc.dns.RRType.SOA(),
+                                         isc.dns.RRTTL(0))
+
+        rrset_exists_value_yes = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                                               isc.dns.RRClass.IN(),
+                                               isc.dns.RRType.A(),
+                                               isc.dns.RRTTL(0))
+        rrset_exists_value_yes.add_rdata(isc.dns.Rdata(isc.dns.RRType.A(),
+                                                       isc.dns.RRClass.IN(),
+                                                       "192.0.2.1"))
+
+        rrset_does_not_exist_yes = isc.dns.RRset(isc.dns.Name("foo.example.org"),
+                                                 isc.dns.RRClass.NONE(),
+                                                 isc.dns.RRType.SOA(),
+                                                 isc.dns.RRTTL(0))
+
+        name_in_use_yes = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                                        isc.dns.RRClass.ANY(),
+                                        isc.dns.RRType.ANY(),
+                                        isc.dns.RRTTL(0))
+
+        name_not_in_use_yes = isc.dns.RRset(isc.dns.Name("foo.example.org"),
+                                            isc.dns.RRClass.NONE(),
+                                            isc.dns.RRType.ANY(),
+                                            isc.dns.RRTTL(0))
+
+        rrset_exists_value_1 = isc.dns.RRset(isc.dns.Name("example.org"),
+                                             isc.dns.RRClass.IN(),
+                                             isc.dns.RRType.NS(),
+                                             isc.dns.RRTTL(0))
+        rrset_exists_value_1.add_rdata(isc.dns.Rdata(isc.dns.RRType.NS(),
+                                                     isc.dns.RRClass.IN(),
+                                                     "ns1.example.org"))
+        rrset_exists_value_2 = isc.dns.RRset(isc.dns.Name("example.org"),
+                                             isc.dns.RRClass.IN(),
+                                             isc.dns.RRType.NS(),
+                                             isc.dns.RRTTL(0))
+        rrset_exists_value_2.add_rdata(isc.dns.Rdata(isc.dns.RRType.NS(),
+                                                     isc.dns.RRClass.IN(),
+                                                     "ns2.example.org"))
+        rrset_exists_value_3 = isc.dns.RRset(isc.dns.Name("example.org"),
+                                             isc.dns.RRClass.IN(),
+                                             isc.dns.RRType.NS(),
+                                             isc.dns.RRTTL(0))
+        rrset_exists_value_3.add_rdata(isc.dns.Rdata(isc.dns.RRType.NS(),
+                                                     isc.dns.RRClass.IN(),
+                                                     "ns3.example.org"))
+
+        # and a number that should not
+        rrset_exists_no = isc.dns.RRset(isc.dns.Name("foo.example.org"),
+                                        isc.dns.RRClass.ANY(),
+                                        isc.dns.RRType.SOA(),
+                                        isc.dns.RRTTL(0))
+
+
+        rrset_exists_value_no = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                                              isc.dns.RRClass.IN(),
+                                              isc.dns.RRType.A(),
+                                              isc.dns.RRTTL(0))
+        rrset_exists_value_no.add_rdata(isc.dns.Rdata(isc.dns.RRType.A(),
+                                                      isc.dns.RRClass.IN(),
+                                                      "192.0.2.2"))
+
+        rrset_does_not_exist_no = isc.dns.RRset(isc.dns.Name("example.org"),
+                                                isc.dns.RRClass.NONE(),
+                                                isc.dns.RRType.SOA(),
+                                                isc.dns.RRTTL(0))
+
+        name_in_use_no = isc.dns.RRset(isc.dns.Name("foo.example.org"),
+                                       isc.dns.RRClass.ANY(),
+                                       isc.dns.RRType.ANY(),
+                                       isc.dns.RRTTL(0))
+
+        name_not_in_use_no = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                                           isc.dns.RRClass.NONE(),
+                                           isc.dns.RRType.ANY(),
+                                           isc.dns.RRTTL(0))
+
+        # Create an UPDATE with all 5 'yes' prereqs
+        data, update = create_update_msg([TEST_ZONE_RECORD],
+                                         [
+                                          rrset_exists_yes,
+                                          rrset_does_not_exist_yes,
+                                          name_in_use_yes,
+                                          name_not_in_use_yes,
+                                          rrset_exists_value_yes,
+                                         ])
+        # check 'no' result codes
+        self.check_prerequisite_result(Rcode.NXRRSET(),
+                                       [ rrset_exists_no ])
+        self.check_prerequisite_result(Rcode.NXRRSET(),
+                                       [ rrset_exists_value_no ])
+        self.check_prerequisite_result(Rcode.YXRRSET(),
+                                       [ rrset_does_not_exist_no ])
+        self.check_prerequisite_result(Rcode.NXDOMAIN(),
+                                       [ name_in_use_no ])
+        self.check_prerequisite_result(Rcode.YXDOMAIN(),
+                                       [ name_not_in_use_no ])
+
+        # the 'yes' codes should result in ok
+        self.check_prerequisite_result(Rcode.NOERROR(),
+                                       [ rrset_exists_yes,
+                                         rrset_exists_value_yes,
+                                         rrset_does_not_exist_yes,
+                                         name_in_use_yes,
+                                         name_not_in_use_yes,
+                                         rrset_exists_value_1,
+                                         rrset_exists_value_2,
+                                         rrset_exists_value_3])
+
+        # try out a permutation, note that one rrset is split up,
+        # and the order of the RRs should not matter
+        self.check_prerequisite_result(Rcode.NOERROR(),
+                                       [ rrset_exists_value_3,
+                                         rrset_exists_yes,
+                                         rrset_exists_value_2,
+                                         name_in_use_yes,
+                                         rrset_exists_value_1])
+
+        # Should fail on the first error, even if most of the
+        # prerequisites are ok
+        self.check_prerequisite_result(Rcode.NXDOMAIN(),
+                                       [ rrset_exists_value_3,
+                                         rrset_exists_yes,
+                                         rrset_exists_value_2,
+                                         name_in_use_yes,
+                                         name_in_use_no,
+                                         rrset_exists_value_1])
+
+    def test_prerequisite_notzone(self):
+        rrset = isc.dns.RRset(isc.dns.Name("some.other.zone."),
+                              isc.dns.RRClass.ANY(),
+                              isc.dns.RRType.SOA(),
+                              isc.dns.RRTTL(0))
+        self.check_prerequisite_result(Rcode.NOTZONE(), [ rrset ])
+
+    def test_prerequisites_formerr(self):
+        # test for form errors in the prerequisite section
+
+        # Class ANY, non-zero TTL
+        rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                              isc.dns.RRClass.ANY(),
+                              isc.dns.RRType.SOA(),
+                              isc.dns.RRTTL(1))
+        self.check_prerequisite_result(Rcode.FORMERR(), [ rrset ])
+
+        # Class ANY, but with rdata
+        rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                              isc.dns.RRClass.ANY(),
+                              isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.A(),
+                                      isc.dns.RRClass.ANY(),
+                                      "\# 04 00 00 00 00"))
+        self.check_prerequisite_result(Rcode.FORMERR(), [ rrset ])
+
+        # Class NONE, non-zero TTL
+        rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                              isc.dns.RRClass.NONE(),
+                              isc.dns.RRType.SOA(),
+                              isc.dns.RRTTL(1))
+        self.check_prerequisite_result(Rcode.FORMERR(), [ rrset ])
+
+        # Class NONE, but with rdata
+        rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                              isc.dns.RRClass.NONE(),
+                              isc.dns.RRType.A(),
+                              isc.dns.RRTTL(0))
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.A(),
+                                      isc.dns.RRClass.NONE(),
+                                      "\# 04 00 00 00 00"))
+        self.check_prerequisite_result(Rcode.FORMERR(), [ rrset ])
+
+        # Matching class and type, but non-zero TTL
+        rrset = isc.dns.RRset(isc.dns.Name("www.example.org"),
+                              isc.dns.RRClass.IN(),
+                              isc.dns.RRType.A(),
+                              isc.dns.RRTTL(1))
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.A(),
+                                      isc.dns.RRClass.IN(),
+                                      "192.0.2.1"))
+        self.check_prerequisite_result(Rcode.FORMERR(), [ rrset ])
+
+        # Completely different class
+        rrset = isc.dns.RRset(isc.dns.Name("example.org"),
+                              isc.dns.RRClass.CH(),
+                              isc.dns.RRType.TXT(),
+                              isc.dns.RRTTL(0))
+        rrset.add_rdata(isc.dns.Rdata(isc.dns.RRType.TXT(),
+                                      isc.dns.RRClass.CH(),
+                                      "foo"))
+        self.check_prerequisite_result(Rcode.FORMERR(), [ rrset ])
+
 if __name__ == "__main__":
     isc.log.init("bind10")
     isc.log.resetUnitTestRootLogger()

BIN
src/lib/testutils/testdata/rwtest.sqlite3