Browse Source

Merge #2436

Let isc::datasrc::ZoneLoader validate the data before committing by
isc::dns::checkZone. Also, remove the in-house validation from b10-loadzone,
since it's only a subset of the checkZone, which is now performed.

Import a merge fixup from mukund.
Michal 'vorner' Vaner 12 years ago
parent
commit
48d999f1cb

+ 1 - 15
src/bin/loadzone/b10-loadzone.xml

@@ -224,21 +224,7 @@
   <refsect1>
   <refsect1>
     <title>BUGS</title>
     <title>BUGS</title>
     <para>
     <para>
-      As of the initial implementation, the underlying library that
-      this tool uses does not fully validate the loaded zone; for
-      example, loading will succeed even if it doesn't have the SOA or
-      NS record at its origin name.  Such checks will be implemented
-      in a near future version, but until then, the
-      <command>b10-loadzone</command> performs the existence of the
-      SOA and NS records by itself.  However, <command>b10-loadzone</command>
-      only warns about it, and does not cancel the load itself.
-      If this warning message is produced, it's the user's
-      responsibility to fix the errors and reload it.  When the
-      library is updated with the post load checks, it will be more
-      sophisticated and the such zone won't be successfully loaded.
-    </para>
-    <para>
-      There are some other issues noted in the DESCRIPTION section.
+      There are some issues noted in the DESCRIPTION section.
     </para>
     </para>
   </refsect1>
   </refsect1>
 </refentry><!--
 </refentry><!--

+ 0 - 23
src/bin/loadzone/loadzone.py.in

@@ -259,28 +259,6 @@ class LoadZoneRunner:
                              self._zone_class)
                              self._zone_class)
             raise LoadFailure(str(ex))
             raise LoadFailure(str(ex))
 
 
-    def _post_load_checks(self):
-        '''Perform minimal validity checks on the loaded zone.
-
-        We do this ourselves because the underlying library currently
-        doesn't do any checks.  Once the library support post-load validation
-        this check should be removed.
-
-        '''
-        datasrc_client = DataSourceClient(self._datasrc_type,
-                                          self._datasrc_config)
-        _, finder = datasrc_client.find_zone(self._zone_name) # should succeed
-        result = finder.find(self._zone_name, RRType.SOA())[0]
-        if result is not finder.SUCCESS:
-            self._post_load_warning('zone has no SOA')
-        result = finder.find(self._zone_name, RRType.NS())[0]
-        if result is not finder.SUCCESS:
-            self._post_load_warning('zone has no NS')
-
-    def _post_load_warning(self, msg):
-        logger.warn(LOADZONE_POSTLOAD_ISSUE, self._zone_name,
-                    self._zone_class, msg)
-
     def _set_signal_handlers(self):
     def _set_signal_handlers(self):
         signal.signal(signal.SIGINT, self._interrupt_handler)
         signal.signal(signal.SIGINT, self._interrupt_handler)
         signal.signal(signal.SIGTERM, self._interrupt_handler)
         signal.signal(signal.SIGTERM, self._interrupt_handler)
@@ -298,7 +276,6 @@ class LoadZoneRunner:
             total_elapsed_txt = "%.2f" % (time.time() - self.__start_time)
             total_elapsed_txt = "%.2f" % (time.time() - self.__start_time)
             logger.info(LOADZONE_DONE, self.__loaded_rrs, self._zone_name,
             logger.info(LOADZONE_DONE, self.__loaded_rrs, self._zone_name,
                         self._zone_class, total_elapsed_txt)
                         self._zone_class, total_elapsed_txt)
-            self._post_load_checks()
             return 0
             return 0
         except BadArgument as ex:
         except BadArgument as ex:
             logger.error(LOADZONE_ARGUMENT_ERROR, ex)
             logger.error(LOADZONE_ARGUMENT_ERROR, ex)

+ 0 - 8
src/bin/loadzone/loadzone_messages.mes

@@ -46,14 +46,6 @@ in the zone file.  When this happens, the RRs loaded so far are
 effectively deleted from the zone, and the old version (if exists)
 effectively deleted from the zone, and the old version (if exists)
 will still remain valid for operations.
 will still remain valid for operations.
 
 
-% LOADZONE_POSTLOAD_ISSUE New version of zone %1/%2 has an issue: %3
-b10-loadzone detected a problem after a successful load of zone:
-either or both of SOA and NS records are missing at the zone origin.
-In the current implementation the load will not be canceled for such
-problems.  The operator will need to fix the issues and reload the
-zone; otherwise applications (such as b10-auth) that use this data
-source will not work as expected.
-
 % LOADZONE_SQLITE3_USING_DEFAULT_CONFIG Using default configuration with SQLite3 DB file %1
 % LOADZONE_SQLITE3_USING_DEFAULT_CONFIG Using default configuration with SQLite3 DB file %1
 The SQLite3 data source is specified as the data source type without a
 The SQLite3 data source is specified as the data source type without a
 data source configuration.  b10-loadzone uses the default
 data source configuration.  b10-loadzone uses the default

+ 3 - 11
src/bin/loadzone/tests/loadzone_test.py

@@ -237,7 +237,7 @@ class TestLoadZoneRunner(unittest.TestCase):
         self.__check_zone_soa(None, zone_name=Name('example.com'))
         self.__check_zone_soa(None, zone_name=Name('example.com'))
 
 
     def __common_post_load_setup(self, zone_file):
     def __common_post_load_setup(self, zone_file):
-        '''Common setup procedure for post load tests.'''
+        '''Common setup procedure for post load tests which should fail.'''
         # replace the LoadZoneRunner's original _post_load_warning() for
         # replace the LoadZoneRunner's original _post_load_warning() for
         # inspection
         # inspection
         self.__warnings = []
         self.__warnings = []
@@ -248,26 +248,18 @@ class TestLoadZoneRunner(unittest.TestCase):
         self.__common_load_setup()
         self.__common_load_setup()
         self.__runner._zone_file = zone_file
         self.__runner._zone_file = zone_file
         self.__check_zone_soa(ORIG_SOA_TXT)
         self.__check_zone_soa(ORIG_SOA_TXT)
-        self.__runner._do_load()
-        self.__runner._post_load_checks()
+        # It fails because there's problem with the zone data
+        self.assertRaises(LoadFailure, self.__runner._do_load)
 
 
     def test_load_post_check_fail_soa(self):
     def test_load_post_check_fail_soa(self):
         '''Load succeeds but warns about missing SOA, should cause warn'''
         '''Load succeeds but warns about missing SOA, should cause warn'''
-        self.__common_load_setup()
         self.__common_post_load_setup(LOCAL_TESTDATA_PATH +
         self.__common_post_load_setup(LOCAL_TESTDATA_PATH +
                                       '/example-nosoa.org.zone')
                                       '/example-nosoa.org.zone')
-        self.__check_zone_soa(False)
-        self.assertEqual(1, len(self.__warnings))
-        self.assertEqual('zone has no SOA', self.__warnings[0])
 
 
     def test_load_post_check_fail_ns(self):
     def test_load_post_check_fail_ns(self):
         '''Load succeeds but warns about missing NS, should cause warn'''
         '''Load succeeds but warns about missing NS, should cause warn'''
-        self.__common_load_setup()
         self.__common_post_load_setup(LOCAL_TESTDATA_PATH +
         self.__common_post_load_setup(LOCAL_TESTDATA_PATH +
                                       '/example-nons.org.zone')
                                       '/example-nons.org.zone')
-        self.__check_zone_soa(NEW_SOA_TXT)
-        self.assertEqual(1, len(self.__warnings))
-        self.assertEqual('zone has no NS', self.__warnings[0])
 
 
     def __interrupt_progress(self, loaded_rrs):
     def __interrupt_progress(self, loaded_rrs):
         '''A helper emulating a signal in the middle of loading.
         '''A helper emulating a signal in the middle of loading.

+ 13 - 0
src/lib/datasrc/datasrc_messages.mes

@@ -70,6 +70,19 @@ The maximum allowed number of items of the hotspot cache is set to the given
 number. If there are too many, some of them will be dropped. The size of 0
 number. If there are too many, some of them will be dropped. The size of 0
 means no limit.
 means no limit.
 
 
+% DATASRC_CHECK_ERROR post-load check of zone %1/%2 failed: %3
+The zone was loaded into the data source successfully, but the content fails
+basic sanity checks. See the message if you want to know what exactly is wrong
+with the data. The data can not be used and previous version, if any, will be
+preserved.
+
+% DATASRC_CHECK_WARNING %1/%2: %3
+The zone was loaded into the data source successfully, but there's some problem
+with the content. The problem does not stop the new version from being used
+(though there may be other problems that do, see DATASRC_CHECK_ERROR),
+but it should still be checked and fixed. See the message to know what exactly
+is wrong with the data.
+
 % DATASRC_DATABASE_COVER_NSEC_UNSUPPORTED %1 doesn't support DNSSEC when asked for NSEC data covering %2
 % DATASRC_DATABASE_COVER_NSEC_UNSUPPORTED %1 doesn't support DNSSEC when asked for NSEC data covering %2
 The datasource tried to provide an NSEC proof that the named domain does not
 The datasource tried to provide an NSEC proof that the named domain does not
 exist, but the database backend doesn't support DNSSEC. No proof is included
 exist, but the database backend doesn't support DNSSEC. No proof is included

+ 2 - 0
src/lib/datasrc/tests/Makefile.am

@@ -118,3 +118,5 @@ EXTRA_DIST += testdata/new_minor_schema.sqlite3
 EXTRA_DIST += testdata/newschema.sqlite3
 EXTRA_DIST += testdata/newschema.sqlite3
 EXTRA_DIST += testdata/oldschema.sqlite3
 EXTRA_DIST += testdata/oldschema.sqlite3
 EXTRA_DIST += testdata/static.zone
 EXTRA_DIST += testdata/static.zone
+EXTRA_DIST += testdata/novalidate.zone
+EXTRA_DIST += testdata/checkwarn.zone

+ 4 - 0
src/lib/datasrc/tests/testdata/checkwarn.zone

@@ -0,0 +1,4 @@
+example.org.			86400	IN	SOA	a.root-servers.net. nstld.verisign-grs.com. 2010030802 1800 900 604800 86400
+example.org.            86400   IN  NS  ns.example.org.
+; Missing the address for the nameserver. This should generate a warning, but not error.
+www.example.org.        3600    IN  A   192.0.2.1

+ 3 - 0
src/lib/datasrc/tests/testdata/novalidate.zone

@@ -0,0 +1,3 @@
+example.org.			86400	IN	SOA	a.root-servers.net. nstld.verisign-grs.com. 2010030802 1800 900 604800 86400
+; Missing the NS here, will generate an error in post-load check of the zone.
+www.example.org.        3600    IN  A   192.0.2.1

+ 135 - 35
src/lib/datasrc/tests/zone_loader_unittest.cc

@@ -27,6 +27,8 @@
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
 
 
 #include <boost/shared_ptr.hpp>
 #include <boost/shared_ptr.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <boost/foreach.hpp>
 #include <string>
 #include <string>
 #include <vector>
 #include <vector>
 
 
@@ -34,6 +36,7 @@ using isc::dns::RRClass;
 using isc::dns::Name;
 using isc::dns::Name;
 using isc::dns::RRType;
 using isc::dns::RRType;
 using isc::dns::ConstRRsetPtr;
 using isc::dns::ConstRRsetPtr;
+using isc::dns::RRsetPtr;
 using std::string;
 using std::string;
 using std::vector;
 using std::vector;
 using boost::shared_ptr;
 using boost::shared_ptr;
@@ -64,10 +67,9 @@ public:
     // since many client methods are const, but we still want to know they
     // since many client methods are const, but we still want to know they
     // were called.
     // were called.
     mutable vector<Name> provided_updaters_;
     mutable vector<Name> provided_updaters_;
-    // We store string representations of the RRsets. This is simpler than
-    // copying them and we can't really put them into shared pointers, because
-    // we get them as references.
-    vector<string> rrsets_;
+    vector<RRsetPtr> rrsets_;
+    // List of rrsets as texts, for easier manipulation
+    vector<string> rrset_texts_;
     bool commit_called_;
     bool commit_called_;
     // If set to true, getUpdater returns NULL
     // If set to true, getUpdater returns NULL
     bool missing_zone_;
     bool missing_zone_;
@@ -75,6 +77,26 @@ public:
     RRClass rrclass_;
     RRClass rrclass_;
 };
 };
 
 
+// Test implementation of RRsetCollectionBase.
+class TestRRsetCollection : public isc::datasrc::RRsetCollectionBase {
+public:
+    TestRRsetCollection(ZoneUpdater& updater,
+                        const isc::dns::RRClass& rrclass) :
+        isc::datasrc::RRsetCollectionBase(updater, rrclass)
+    {}
+
+    virtual ~TestRRsetCollection() {}
+
+protected:
+    virtual RRsetCollectionBase::IterPtr getBeginning() {
+        isc_throw(isc::NotImplemented, "This method is not implemented.");
+    }
+
+    virtual RRsetCollectionBase::IterPtr getEnd() {
+        isc_throw(isc::NotImplemented, "This method is not implemented.");
+    }
+};
+
 // The updater isn't really correct according to the API. For example,
 // The updater isn't really correct according to the API. For example,
 // the whole client can be committed only once in its lifetime. The
 // the whole client can be committed only once in its lifetime. The
 // updaters would influence each other if there were more. But we
 // updaters would influence each other if there were more. But we
@@ -82,21 +104,36 @@ public:
 // and this way, it is much simpler.
 // and this way, it is much simpler.
 class Updater : public ZoneUpdater {
 class Updater : public ZoneUpdater {
 public:
 public:
-    Updater(MockClient* client) :
+    Updater(MockClient* client, const Name& name) :
         client_(client),
         client_(client),
-        finder_(client_->rrclass_)
+        finder_(client_->rrclass_, name, client_->rrsets_)
     {}
     {}
     virtual ZoneFinder& getFinder() {
     virtual ZoneFinder& getFinder() {
         return (finder_);
         return (finder_);
     }
     }
     virtual isc::datasrc::RRsetCollectionBase& getRRsetCollection() {
     virtual isc::datasrc::RRsetCollectionBase& getRRsetCollection() {
-        isc_throw(isc::NotImplemented, "Method not used in tests");
+        if (!rrset_collection_) {
+            rrset_collection_.reset(new TestRRsetCollection(*this,
+                                                            client_->rrclass_));
+        }
+        return (*rrset_collection_);
     }
     }
     virtual void addRRset(const isc::dns::AbstractRRset& rrset) {
     virtual void addRRset(const isc::dns::AbstractRRset& rrset) {
         if (client_->commit_called_) {
         if (client_->commit_called_) {
             isc_throw(DataSourceError, "Add after commit");
             isc_throw(DataSourceError, "Add after commit");
         }
         }
-        client_->rrsets_.push_back(rrset.toText());
+        // We need to copy the RRset. We don't do it properly (we omit the
+        // signature, for example), because we don't need to.
+        RRsetPtr new_rrset(new isc::dns::BasicRRset(rrset.getName(),
+                                                    rrset.getClass(),
+                                                    rrset.getType(),
+                                                    rrset.getTTL()));
+        for (isc::dns::RdataIteratorPtr i(rrset.getRdataIterator());
+             !i->isLast(); i->next()) {
+            new_rrset->addRdata(i->getCurrent());
+        }
+        client_->rrsets_.push_back(new_rrset);
+        client_->rrset_texts_.push_back(rrset.toText());
     }
     }
     virtual void deleteRRset(const isc::dns::AbstractRRset&) {
     virtual void deleteRRset(const isc::dns::AbstractRRset&) {
         isc_throw(isc::NotImplemented, "Method not used in tests");
         isc_throw(isc::NotImplemented, "Method not used in tests");
@@ -106,21 +143,37 @@ public:
     }
     }
 private:
 private:
     MockClient* client_;
     MockClient* client_;
+    boost::scoped_ptr<TestRRsetCollection> rrset_collection_;
     class Finder : public ZoneFinder {
     class Finder : public ZoneFinder {
     public:
     public:
-        Finder(const RRClass& rrclass) :
-            class_(rrclass)
+        Finder(const RRClass& rrclass, const Name& name,
+               const vector<RRsetPtr>& rrsets) :
+            class_(rrclass),
+            name_(name),
+            rrsets_(rrsets)
         {}
         {}
         virtual RRClass getClass() const {
         virtual RRClass getClass() const {
             return (class_);
             return (class_);
         }
         }
         virtual Name getOrigin() const {
         virtual Name getOrigin() const {
-            isc_throw(isc::NotImplemented, "Method not used in tests");
+            return (name_);
         }
         }
-        virtual shared_ptr<Context> find(const Name&, const RRType&,
-                                         const FindOptions)
+        virtual shared_ptr<Context> find(const Name& name, const RRType& type,
+                                         const FindOptions options)
         {
         {
-            isc_throw(isc::NotImplemented, "Method not used in tests");
+            // The method is not completely correct. It ignores many special
+            // cases and also the options except for the result. But this is
+            // enough for the tests.  We care only about exact match here.
+            BOOST_FOREACH(const RRsetPtr& rrset, rrsets_) {
+                if (rrset->getName() == name && rrset->getType() == type) {
+                    return (shared_ptr<Context>(
+                        new GenericContext(*this, options,
+                                           ResultContext(SUCCESS, rrset))));
+                }
+            }
+            return (shared_ptr<Context>(
+                new GenericContext(*this, options,
+                                   ResultContext(NXRRSET, ConstRRsetPtr()))));
         }
         }
         virtual shared_ptr<Context> findAll(const Name&,
         virtual shared_ptr<Context> findAll(const Name&,
                                             vector<ConstRRsetPtr>&,
                                             vector<ConstRRsetPtr>&,
@@ -133,6 +186,8 @@ private:
         }
         }
     private:
     private:
         const RRClass class_;
         const RRClass class_;
+        const Name name_;
+        const vector<RRsetPtr>& rrsets_;
     } finder_;
     } finder_;
 };
 };
 
 
@@ -147,7 +202,7 @@ MockClient::getUpdater(const Name& name, bool replace, bool journaling) const {
     // const_cast is bad. But the const on getUpdater seems wrong in the first
     // const_cast is bad. But the const on getUpdater seems wrong in the first
     // place, since updater will be modifying the data there. And the updater
     // place, since updater will be modifying the data there. And the updater
     // wants to store data into the client so we can examine it later.
     // wants to store data into the client so we can examine it later.
-    return (ZoneUpdaterPtr(new Updater(const_cast<MockClient*>(this))));
+    return (ZoneUpdaterPtr(new Updater(const_cast<MockClient*>(this), name)));
 }
 }
 
 
 class ZoneLoaderTest : public ::testing::Test {
 class ZoneLoaderTest : public ::testing::Test {
@@ -160,10 +215,12 @@ protected:
     {}
     {}
     void prepareSource(const Name& zone, const char* filename) {
     void prepareSource(const Name& zone, const char* filename) {
         // TODO:
         // TODO:
-        // Currently, load uses an urelated implementation. In the long term,
-        // the method will probably be deprecated. At that time, we should
-        // probably prepare the data in some other way (using sqlite3 or
-        // something). This is simpler for now.
+        // Currently, source_client_ is of InMemoryClient and its load()
+        // uses a different code than the ZoneLoader (so we can cross-check
+        // the implementations). Currently, the load() doesn't perform any
+        // post-load checks. It will change in #2499, at which point the
+        // loading may start failing depending on details of the test data. We
+        // should prepare the data by some different method then.
         source_client_.load(zone, string(TEST_DATA_DIR) + "/" + filename);
         source_client_.load(zone, string(TEST_DATA_DIR) + "/" + filename);
     }
     }
 private:
 private:
@@ -198,12 +255,12 @@ TEST_F(ZoneLoaderTest, copyUnsigned) {
     // The count is 34 because we expect the RRs to be separated.
     // The count is 34 because we expect the RRs to be separated.
     EXPECT_EQ(34, destination_client_.rrsets_.size());
     EXPECT_EQ(34, destination_client_.rrsets_.size());
     // Ensure known order.
     // Ensure known order.
-    std::sort(destination_client_.rrsets_.begin(),
-              destination_client_.rrsets_.end());
+    std::sort(destination_client_.rrset_texts_.begin(),
+              destination_client_.rrset_texts_.end());
     EXPECT_EQ(". 518400 IN NS a.root-servers.net.\n",
     EXPECT_EQ(". 518400 IN NS a.root-servers.net.\n",
-              destination_client_.rrsets_.front());
+              destination_client_.rrset_texts_.front());
     EXPECT_EQ("m.root-servers.net. 3600000 IN AAAA 2001:dc3::35\n",
     EXPECT_EQ("m.root-servers.net. 3600000 IN AAAA 2001:dc3::35\n",
-              destination_client_.rrsets_.back());
+              destination_client_.rrset_texts_.back());
 
 
     // It isn't possible to try again now
     // It isn't possible to try again now
     EXPECT_THROW(loader.load(), isc::InvalidOperation);
     EXPECT_THROW(loader.load(), isc::InvalidOperation);
@@ -252,18 +309,18 @@ TEST_F(ZoneLoaderTest, copySigned) {
     EXPECT_EQ(14, destination_client_.rrsets_.size());
     EXPECT_EQ(14, destination_client_.rrsets_.size());
     EXPECT_TRUE(destination_client_.commit_called_);
     EXPECT_TRUE(destination_client_.commit_called_);
     // Same trick with sorting to know where they are
     // Same trick with sorting to know where they are
-    std::sort(destination_client_.rrsets_.begin(),
-              destination_client_.rrsets_.end());
+    std::sort(destination_client_.rrset_texts_.begin(),
+              destination_client_.rrset_texts_.end());
     // Due to the R at the beginning, this one should be last
     // Due to the R at the beginning, this one should be last
     EXPECT_EQ("09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN NSEC3 "
     EXPECT_EQ("09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN NSEC3 "
               "1 0 10 AABBCCDD RKOF8QMFRB5F2V9EJHFBVB2JPVSA0DJD A RRSIG\n",
               "1 0 10 AABBCCDD RKOF8QMFRB5F2V9EJHFBVB2JPVSA0DJD A RRSIG\n",
-              destination_client_.rrsets_[0]);
+              destination_client_.rrset_texts_[0]);
     EXPECT_EQ("09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN RRSIG "
     EXPECT_EQ("09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN RRSIG "
               "NSEC3 7 3 1200 20120301040838 20120131040838 19562 example.org."
               "NSEC3 7 3 1200 20120301040838 20120131040838 19562 example.org."
               " EdwMeepLf//lV+KpCAN+213Scv1rrZyj4i2OwoCP4XxxS3CWGSuvYuKOyfZc8w"
               " EdwMeepLf//lV+KpCAN+213Scv1rrZyj4i2OwoCP4XxxS3CWGSuvYuKOyfZc8w"
               "KRcrD/4YG6nZVXE0s5O8NahjBJmDIyVt4WkfZ6QthxGg8ggLVvcD3dFksPyiKHf"
               "KRcrD/4YG6nZVXE0s5O8NahjBJmDIyVt4WkfZ6QthxGg8ggLVvcD3dFksPyiKHf"
               "/WrTOZPSsxvN5m/i1Ey6+YWS01Gf3WDCMWDauC7Nmh3CTM=\n",
               "/WrTOZPSsxvN5m/i1Ey6+YWS01Gf3WDCMWDauC7Nmh3CTM=\n",
-              destination_client_.rrsets_[1]);
+              destination_client_.rrset_texts_[1]);
 }
 }
 
 
 // If the destination zone does not exist, it throws
 // If the destination zone does not exist, it throws
@@ -304,12 +361,12 @@ TEST_F(ZoneLoaderTest, loadUnsigned) {
     // The count is 34 because we expect the RRs to be separated.
     // The count is 34 because we expect the RRs to be separated.
     EXPECT_EQ(34, destination_client_.rrsets_.size());
     EXPECT_EQ(34, destination_client_.rrsets_.size());
     // Ensure known order.
     // Ensure known order.
-    std::sort(destination_client_.rrsets_.begin(),
-              destination_client_.rrsets_.end());
+    std::sort(destination_client_.rrset_texts_.begin(),
+              destination_client_.rrset_texts_.end());
     EXPECT_EQ(". 518400 IN NS a.root-servers.net.\n",
     EXPECT_EQ(". 518400 IN NS a.root-servers.net.\n",
-              destination_client_.rrsets_.front());
+              destination_client_.rrset_texts_.front());
     EXPECT_EQ("m.root-servers.net. 3600000 IN AAAA 2001:dc3::35\n",
     EXPECT_EQ("m.root-servers.net. 3600000 IN AAAA 2001:dc3::35\n",
-              destination_client_.rrsets_.back());
+              destination_client_.rrset_texts_.back());
 
 
     // It isn't possible to try again now
     // It isn't possible to try again now
     EXPECT_THROW(loader.load(), isc::InvalidOperation);
     EXPECT_THROW(loader.load(), isc::InvalidOperation);
@@ -362,18 +419,18 @@ TEST_F(ZoneLoaderTest, loadSigned) {
     EXPECT_EQ(14, destination_client_.rrsets_.size());
     EXPECT_EQ(14, destination_client_.rrsets_.size());
     EXPECT_TRUE(destination_client_.commit_called_);
     EXPECT_TRUE(destination_client_.commit_called_);
     // Same trick with sorting to know where they are
     // Same trick with sorting to know where they are
-    std::sort(destination_client_.rrsets_.begin(),
-              destination_client_.rrsets_.end());
+    std::sort(destination_client_.rrset_texts_.begin(),
+              destination_client_.rrset_texts_.end());
     // Due to the R at the beginning, this one should be last
     // Due to the R at the beginning, this one should be last
     EXPECT_EQ("09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN NSEC3 "
     EXPECT_EQ("09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN NSEC3 "
               "1 0 10 AABBCCDD RKOF8QMFRB5F2V9EJHFBVB2JPVSA0DJD A RRSIG\n",
               "1 0 10 AABBCCDD RKOF8QMFRB5F2V9EJHFBVB2JPVSA0DJD A RRSIG\n",
-              destination_client_.rrsets_[0]);
+              destination_client_.rrset_texts_[0]);
     EXPECT_EQ("09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN RRSIG "
     EXPECT_EQ("09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN RRSIG "
               "NSEC3 7 3 1200 20120301040838 20120131040838 19562 example.org."
               "NSEC3 7 3 1200 20120301040838 20120131040838 19562 example.org."
               " EdwMeepLf//lV+KpCAN+213Scv1rrZyj4i2OwoCP4XxxS3CWGSuvYuKOyfZc8w"
               " EdwMeepLf//lV+KpCAN+213Scv1rrZyj4i2OwoCP4XxxS3CWGSuvYuKOyfZc8w"
               "KRcrD/4YG6nZVXE0s5O8NahjBJmDIyVt4WkfZ6QthxGg8ggLVvcD3dFksPyiKHf"
               "KRcrD/4YG6nZVXE0s5O8NahjBJmDIyVt4WkfZ6QthxGg8ggLVvcD3dFksPyiKHf"
               "/WrTOZPSsxvN5m/i1Ey6+YWS01Gf3WDCMWDauC7Nmh3CTM=\n",
               "/WrTOZPSsxvN5m/i1Ey6+YWS01Gf3WDCMWDauC7Nmh3CTM=\n",
-              destination_client_.rrsets_[1]);
+              destination_client_.rrset_texts_[1]);
 }
 }
 
 
 // Test it throws when there's no such file
 // Test it throws when there's no such file
@@ -395,4 +452,47 @@ TEST_F(ZoneLoaderTest, loadSyntaxError) {
     EXPECT_FALSE(destination_client_.commit_called_);
     EXPECT_FALSE(destination_client_.commit_called_);
 }
 }
 
 
+// Test there's validation of the data in the zone loader.
+TEST_F(ZoneLoaderTest, loadCheck) {
+    ZoneLoader loader(destination_client_, Name("example.org"),
+                      TEST_DATA_DIR "/novalidate.zone");
+    EXPECT_THROW(loader.loadIncremental(10), ZoneContentError);
+    // The messages go to the log. We don't have an easy way to examine them.
+    EXPECT_FALSE(destination_client_.commit_called_);
+}
+
+// The same test, but for copying from other data source
+TEST_F(ZoneLoaderTest, copyCheck) {
+    prepareSource(Name("example.org"), "novalidate.zone");
+    ZoneLoader loader(destination_client_, Name("example.org"),
+                      source_client_);
+
+    EXPECT_THROW(loader.loadIncremental(10), ZoneContentError);
+    // The messages go to the log. We don't have an easy way to examine them.
+    EXPECT_FALSE(destination_client_.commit_called_);
+}
+
+// Check a warning doesn't disrupt the loading of the zone
+TEST_F(ZoneLoaderTest, loadCheckWarn) {
+    ZoneLoader loader(destination_client_, Name("example.org"),
+                      TEST_DATA_DIR "/checkwarn.zone");
+    EXPECT_TRUE(loader.loadIncremental(10));
+    // The messages go to the log. We don't have an easy way to examine them.
+    // But the zone was committed and contains all 3 RRs
+    EXPECT_TRUE(destination_client_.commit_called_);
+    EXPECT_EQ(3, destination_client_.rrsets_.size());
+}
+
+TEST_F(ZoneLoaderTest, copyCheckWarn) {
+    prepareSource(Name("example.org"), "checkwarn.zone");
+    ZoneLoader loader(destination_client_, Name("example.org"),
+                      source_client_);
+    EXPECT_TRUE(loader.loadIncremental(10));
+    // The messages go to the log. We don't have an easy way to examine them.
+    // But the zone was committed and contains all 3 RRs
+    EXPECT_TRUE(destination_client_.commit_called_);
+    EXPECT_EQ(3, destination_client_.rrsets_.size());
+
+}
+
 }
 }

+ 38 - 0
src/lib/datasrc/zone_loader.cc

@@ -19,8 +19,17 @@
 #include <datasrc/data_source.h>
 #include <datasrc/data_source.h>
 #include <datasrc/iterator.h>
 #include <datasrc/iterator.h>
 #include <datasrc/zone.h>
 #include <datasrc/zone.h>
+#include <datasrc/logger.h>
+#include <datasrc/rrset_collection_base.h>
 
 
 #include <dns/rrset.h>
 #include <dns/rrset.h>
+#include <dns/zone_checker.h>
+#include <dns/name.h>
+#include <dns/rrclass.h>
+
+#include <boost/bind.hpp>
+
+#include <string>
 
 
 using isc::dns::Name;
 using isc::dns::Name;
 using isc::dns::ConstRRsetPtr;
 using isc::dns::ConstRRsetPtr;
@@ -99,6 +108,22 @@ copyRRsets(const ZoneUpdaterPtr& destination, const ZoneIteratorPtr& source,
     return (false); // Not yet, there may be more
     return (false); // Not yet, there may be more
 }
 }
 
 
+void
+logWarning(const dns::Name* zone_name, const dns::RRClass* rrclass,
+           const std::string& reason)
+{
+    LOG_WARN(logger, DATASRC_CHECK_WARNING).arg(*zone_name).arg(*rrclass).
+        arg(reason);
+}
+
+void
+logError(const dns::Name* zone_name, const dns::RRClass* rrclass,
+         const std::string& reason)
+{
+    LOG_ERROR(logger, DATASRC_CHECK_ERROR).arg(*zone_name).arg(*rrclass).
+        arg(reason);
+}
+
 } // end unnamed namespace
 } // end unnamed namespace
 
 
 bool
 bool
@@ -123,6 +148,19 @@ ZoneLoader::loadIncremental(size_t limit) {
     }
     }
 
 
     if (complete_) {
     if (complete_) {
+        // Everything is loaded. Perform some basic sanity checks on the zone.
+        RRsetCollectionBase& collection = updater_->getRRsetCollection();
+        const dns::Name& zone_name(updater_->getFinder().getOrigin());
+        const dns::RRClass& zone_class(updater_->getFinder().getClass());
+        const dns::ZoneCheckerCallbacks
+            callbacks(boost::bind(&logError, &zone_name, &zone_class, _1),
+                      boost::bind(&logWarning, &zone_name, &zone_class, _1));
+        if (!dns::checkZone(zone_name, zone_class, collection, callbacks)) {
+            // The post-load check failed.
+            loaded_ok_ = false;
+            isc_throw(ZoneContentError, "Errors found when validating zone " <<
+                      zone_name << "/" << zone_class);
+        }
         updater_->commit();
         updater_->commit();
     }
     }
     return (complete_);
     return (complete_);

+ 19 - 0
src/lib/datasrc/zone_loader.h

@@ -48,6 +48,17 @@ public:
     {}
     {}
 };
 };
 
 
+/// \brief Exception thrown when the zone doesn't pass post-load check.
+///
+/// This is thrown by the ZoneLoader when the zone is loaded, but it
+/// doesn't pass basic sanity checks.
+class ZoneContentError : public DataSourceError {
+public:
+    ZoneContentError(const char* file, size_t line, const char* what) :
+        DataSourceError(file, line, what)
+    {}
+};
+
 /// \brief Class to load data into a data source client.
 /// \brief Class to load data into a data source client.
 ///
 ///
 /// This is a small wrapper class that is able to load data into a data source.
 /// This is a small wrapper class that is able to load data into a data source.
@@ -107,6 +118,7 @@ public:
     /// \throw DataSourceError in case some error (possibly low-level) happens.
     /// \throw DataSourceError in case some error (possibly low-level) happens.
     /// \throw MasterFileError when the master_file is badly formatted or some
     /// \throw MasterFileError when the master_file is badly formatted or some
     ///     similar problem is found when loading the master file.
     ///     similar problem is found when loading the master file.
+    /// \throw ZoneContentError when the zone doesn't pass sanity check.
     void load() {
     void load() {
         while (!loadIncremental(1000)) { // 1000 is arbitrary largish number
         while (!loadIncremental(1000)) { // 1000 is arbitrary largish number
             // Body intentionally left blank.
             // Body intentionally left blank.
@@ -123,6 +135,12 @@ public:
     /// pauses in the loading for some purposes (for example reporting
     /// pauses in the loading for some purposes (for example reporting
     /// progress).
     /// progress).
     ///
     ///
+    /// After the last RR is loaded, a sanity check of the zone is performed by
+    /// isc::dns::validateZone. It reports errors and warnings by logging them
+    /// directly. If there are any errors, a ZoneContentError exception is
+    /// thrown and the load is aborted (preserving the old version of zone, if
+    /// any).
+    ///
     /// \param limit The maximum allowed number of RRs to be loaded during this
     /// \param limit The maximum allowed number of RRs to be loaded during this
     ///     call.
     ///     call.
     /// \return True in case the loading is completed, false if there's more
     /// \return True in case the loading is completed, false if there's more
@@ -133,6 +151,7 @@ public:
     /// \throw DataSourceError in case some error (possibly low-level) happens.
     /// \throw DataSourceError in case some error (possibly low-level) happens.
     /// \throw MasterFileError when the master_file is badly formatted or some
     /// \throw MasterFileError when the master_file is badly formatted or some
     ///     similar problem is found when loading the master file.
     ///     similar problem is found when loading the master file.
+    /// \throw ZoneContentError when the zone doesn't pass sanity check.
     /// \note If the limit is exactly the number of RRs available to be loaded,
     /// \note If the limit is exactly the number of RRs available to be loaded,
     ///     the method still returns false and true'll be returned on the next
     ///     the method still returns false and true'll be returned on the next
     ///     call (which will load 0 RRs). This is because the end of iterator or
     ///     call (which will load 0 RRs). This is because the end of iterator or