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 RRClass& rrclass, const RRType& rrtype,
                const RRTTL& ttl, ConstRdataPtr rdata,
                const RRTTL& ttl, ConstRdataPtr rdata,
                Message::ParseOptions options);
                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,
     void addEDNS(Message::Section section, const Name& name,
                  const RRClass& rrclass, const RRType& rrtype,
                  const RRClass& rrclass, const RRType& rrtype,
                  const RRTTL& ttl, const Rdata& rdata);
                  const RRTTL& ttl, const Rdata& rdata);
@@ -740,6 +745,17 @@ MessageImpl::parseSection(const Message::Section section,
         const RRClass rrclass(buffer.readUint16());
         const RRClass rrclass(buffer.readUint16());
         const RRTTL ttl(buffer.readUint32());
         const RRTTL ttl(buffer.readUint32());
         const size_t rdlen = buffer.readUint16();
         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);
         ConstRdataPtr rdata = createRdata(rrtype, rrclass, buffer, rdlen);
 
 
         if (rrtype == RRType::OPT()) {
         if (rrtype == RRType::OPT()) {
@@ -778,6 +794,24 @@ MessageImpl::addRR(Message::Section section, const Name& name,
 }
 }
 
 
 void
 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,
 MessageImpl::addEDNS(Message::Section section,  const Name& name,
                      const RRClass& rrclass, const RRType& rrtype,
                      const RRClass& rrclass, const RRType& rrtype,
                      const RRTTL& ttl, const Rdata& rdata)
                      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");
         NameComparisonResult::COMMONANCESTOR, "COMMONANCESTOR");
     addClassVariable(name_comparison_result_type, "NameRelation",
     addClassVariable(name_comparison_result_type, "NameRelation",
                      po_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",
     PyModule_AddObject(mod, "NameComparisonResult",
         reinterpret_cast<PyObject*>(&name_comparison_result_type));
         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.test_nsname = Name("ns.example.com")
         self.rrset_a = RRset(self.test_name, RRClass("IN"), RRType("A"), RRTTL(3600))
         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_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_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.rrset_ch_txt = RRset(self.test_domain, RRClass("CH"), RRType("TXT"), RRTTL(0))
         self.MAX_RDATA_COUNT = 100
         self.MAX_RDATA_COUNT = 100
@@ -90,6 +91,9 @@ class TestModuleSpec(unittest.TestCase):
 
 
         self.assertRaises(EmptyRRset, self.rrset_a_empty.to_text)
         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):
     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')
         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()
         buffer = bytearray()
@@ -99,6 +103,11 @@ class TestModuleSpec(unittest.TestCase):
         self.assertRaises(EmptyRRset, self.rrset_a_empty.to_wire, buffer);
         self.assertRaises(EmptyRRset, self.rrset_a_empty.to_wire, buffer);
         self.assertRaises(TypeError, self.rrset_a.to_wire, 1)
         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):
     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')
         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()
         mr = MessageRenderer()

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

@@ -43,14 +43,24 @@ AbstractRRset::toText() const {
     string s;
     string s;
     RdataIteratorPtr it = getRdataIterator();
     RdataIteratorPtr it = getRdataIterator();
 
 
+    // In the case of an empty rrset, just print name, ttl, class, and
+    // type
     if (it->isLast()) {
     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 {
     do {
         s += getName().toText() + " " + getTTL().toText() + " " +
         s += getName().toText() + " " + getTTL().toText() + " " +
-            getClass().toText() + " " + getType().toText() + " " +
-            it->getCurrent().toText() + "\n";
+             getClass().toText() + " " + getType().toText() + " " +
+             it->getCurrent().toText() + "\n";
         it->next();
         it->next();
     } while (!it->isLast());
     } while (!it->isLast());
 
 
@@ -65,7 +75,21 @@ rrsetToWire(const AbstractRRset& rrset, T& output, const size_t limit) {
     RdataIteratorPtr it = rrset.getRdataIterator();
     RdataIteratorPtr it = rrset.getRdataIterator();
 
 
     if (it->isLast()) {
     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
     // 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.
     /// the resulting string with a trailing newline character.
     /// (following the BIND9 convention)
     /// (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
     /// If resource allocation fails, a corresponding standard exception
     /// will be thrown.
     /// will be thrown.
     /// The default implementation may throw other exceptions if the
     /// The default implementation may throw other exceptions if the
@@ -299,8 +299,8 @@ public:
     ///
     ///
     /// If resource allocation fails, a corresponding standard exception
     /// If resource allocation fails, a corresponding standard exception
     /// will be thrown.
     /// 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
     /// The default implementation may throw other exceptions if the
     /// \c toWire() method of the RDATA objects throws.
     /// \c toWire() method of the RDATA objects throws.
     /// If a derived class of \c AbstractRRset overrides the default
     /// 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(test_name, RRClass::IN(), RRType::A(), RRTTL(3600)),
                   rrset_a_empty(test_name, RRClass::IN(), RRType::A(),
                   rrset_a_empty(test_name, RRClass::IN(), RRType::A(),
                                 RRTTL(3600)),
                                 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(),
                   rrset_ns(test_domain, RRClass::IN(), RRType::NS(),
                            RRTTL(86400)),
                            RRTTL(86400)),
                   rrset_ch_txt(test_domain, RRClass::CH(), RRType::TXT(),
                   rrset_ch_txt(test_domain, RRClass::CH(), RRType::TXT(),
@@ -62,6 +66,8 @@ protected:
     Name test_nsname;
     Name test_nsname;
     RRset rrset_a;
     RRset rrset_a;
     RRset rrset_a_empty;
     RRset rrset_a_empty;
+    RRset rrset_any_a_empty;
+    RRset rrset_none_a_empty;
     RRset rrset_ns;
     RRset rrset_ns;
     RRset rrset_ch_txt;
     RRset rrset_ch_txt;
     std::vector<unsigned char> wiredata;
     std::vector<unsigned char> wiredata;
@@ -193,8 +199,14 @@ TEST_F(RRsetTest, toText) {
               "test.example.com. 3600 IN A 192.0.2.2\n",
               "test.example.com. 3600 IN A 192.0.2.2\n",
               rrset_a.toText());
               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);
     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) {
 TEST_F(RRsetTest, toWireBuffer) {
@@ -207,6 +219,20 @@ TEST_F(RRsetTest, toWireBuffer) {
     // toWire() cannot be performed for an empty RRset.
     // toWire() cannot be performed for an empty RRset.
     buffer.clear();
     buffer.clear();
     EXPECT_THROW(rrset_a_empty.toWire(buffer), EmptyRRset);
     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) {
 TEST_F(RRsetTest, toWireRenderer) {
@@ -220,8 +246,24 @@ TEST_F(RRsetTest, toWireRenderer) {
                         renderer.getLength(), &wiredata[0], wiredata.size());
                         renderer.getLength(), &wiredata[0], wiredata.size());
 
 
     // toWire() cannot be performed for an empty RRset.
     // 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().
 // 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 += rrcode16_fromWire1 rrcode16_fromWire2
 EXTRA_DIST += rrcode32_fromWire1 rrcode32_fromWire2
 EXTRA_DIST += rrcode32_fromWire1 rrcode32_fromWire2
 EXTRA_DIST += rrset_toWire1 rrset_toWire2
 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_fromWire1.spec rdata_tsig_fromWire2.spec
 EXTRA_DIST += rdata_tsig_fromWire3.spec rdata_tsig_fromWire4.spec
 EXTRA_DIST += rdata_tsig_fromWire3.spec rdata_tsig_fromWire4.spec
 EXTRA_DIST += rdata_tsig_fromWire5.spec rdata_tsig_fromWire6.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
                 // increases the refcount and the container decreases it
                 // later. This way, it feels safer in case the build function
                 // later. This way, it feels safer in case the build function
                 // would fail.
                 // would fail.
-                return (Py_BuildValue("IO", r, list_container.get()));
+                return (Py_BuildValue("IOI", r, list_container.get(),
+                                      result_flags));
             } else {
             } else {
                 if (rrsp) {
                 if (rrsp) {
                     // Use N instead of O so the refcount isn't increased twice
                     // 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.
     # A success. It should return the list now.
     # This also tests we can ommit the options parameter
     # 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(ZoneFinder.SUCCESS, result)
     self.assertEqual(2, len(rrsets))
     self.assertEqual(2, len(rrsets))
     rrsets.sort(key=lambda rrset: rrset.get_type().to_text())
     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
 # No namespace declaration - these constants go in the global namespace
 # of the libddns_messages python module.
 # 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
 % LIBDDNS_UPDATE_ERROR update client %1 for zone %2: %3
 Debug message.  An error is found in processing a dynamic update
 Debug message.  An error is found in processing a dynamic update
 request.  This log message is used for general errors that are not
 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
 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 RFC2136, the receiving server will return a response with an RCODE
 of NOTAUTH.
 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:
         if self.__zname is None:
             return '(zone unknown/not determined)'
             return '(zone unknown/not determined)'
         return self.__zname.to_text(True) + '/' + self.__zclass.to_text()
         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 *
 from isc.dns import *
 import isc.ddns.zone_config
 import isc.ddns.zone_config
 from isc.log import *
 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 *
 from isc.log_messages.libddns_messages import *
+import copy
 
 
 # Result codes for UpdateSession.handle()
 # Result codes for UpdateSession.handle()
 UPDATE_SUCCESS = 0
 UPDATE_SUCCESS = 0
@@ -123,7 +125,11 @@ class UpdateSession:
         try:
         try:
             datasrc_client, zname, zclass = self.__get_update_zone()
             datasrc_client, zname, zclass = self.__get_update_zone()
             # conceptual code that would follow
             # 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.__check_update_acl()
             # self.__do_update()
             # self.__do_update()
             # self.__make_response(Rcode.NOERROR())
             # self.__make_response(Rcode.NOERROR())
@@ -187,3 +193,180 @@ class UpdateSession:
         self.__message.make_response()
         self.__message.make_response()
         self.__message.clear_section(SECTION_ZONE)
         self.__message.clear_section(SECTION_ZONE)
         self.__message.set_rcode(rcode)
         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_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]):
+def create_update_msg(zones=[TEST_ZONE_RECORD], prerequisites=[]):
     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())
     msg.set_rcode(Rcode.NOERROR())
     msg.set_rcode(Rcode.NOERROR())
     for z in zones:
     for z in zones:
         msg.add_question(z)
         msg.add_question(z)
+    for p in prerequisites:
+        msg.add_rrset(SECTION_PREREQUISITE, p)
 
 
     renderer = MessageRenderer()
     renderer = MessageRenderer()
     msg.to_wire(renderer)
     msg.to_wire(renderer)
@@ -148,6 +150,455 @@ 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 __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__":
 if __name__ == "__main__":
     isc.log.init("bind10")
     isc.log.init("bind10")
     isc.log.resetUnitTestRootLogger()
     isc.log.resetUnitTestRootLogger()

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