Browse Source

Merge remote-tracking branch 'origin/trac1976-cont-2' into work/merge

Michal 'vorner' Vaner 13 years ago
parent
commit
7d5399cb03

+ 0 - 4
src/bin/auth/auth.spec.pre.in

@@ -125,10 +125,6 @@
 	  {
             "item_name": "origin", "item_type": "string",
             "item_optional": false, "item_default": ""
-          },
-	  {
-            "item_name": "datasrc", "item_type": "string",
-            "item_optional": true, "item_default": "memory"
           }
         ]
       }

+ 41 - 132
src/bin/auth/command.cc

@@ -17,8 +17,7 @@
 #include <auth/auth_srv.h>
 
 #include <cc/data.h>
-#include <datasrc/memory_datasrc.h>
-#include <datasrc/factory.h>
+#include <datasrc/client_list.h>
 #include <config/ccsession.h>
 #include <exceptions/exceptions.h>
 #include <dns/rrclass.h>
@@ -144,161 +143,74 @@ public:
 // Handle the "loadzone" command.
 class LoadZoneCommand : public AuthCommand {
 public:
+    LoadZoneCommand() :
+        zone_class_(RRClass::IN()), // We need to have something to compile
+        origin_(Name::ROOT_NAME())
+    {}
     virtual void exec(AuthSrv& server, isc::data::ConstElementPtr args) {
         // parse and validate the args.
-        if (!validate(server, args)) {
+        if (!validate(args)) {
             return;
         }
 
-        const ConstElementPtr zone_config = getZoneConfig(server);
+        const boost::shared_ptr<isc::datasrc::ConfigurableClientList>
+            list(server.getClientList(zone_class_));
 
-        // Load a new zone and replace the current zone with the new one.
-        // TODO: eventually this should be incremental or done in some way
-        // that doesn't block other server operations.
-        // TODO: we may (should?) want to check the "last load time" and
-        // the timestamp of the file and skip loading if the file isn't newer.
-        const ConstElementPtr type(zone_config->get("filetype"));
-        boost::shared_ptr<InMemoryZoneFinder> zone_finder(
-            new InMemoryZoneFinder(old_zone_finder_->getClass(),
-                                   old_zone_finder_->getOrigin()));
-        if (type && type->stringValue() == "sqlite3") {
-            scoped_ptr<DataSourceClientContainer>
-                container(new DataSourceClientContainer("sqlite3",
-                                                        Element::fromJSON(
-                    "{\"database_file\": \"" +
-                    zone_config->get("file")->stringValue() + "\"}")));
-            zone_finder->load(*container->getInstance().getIterator(
-                old_zone_finder_->getOrigin()));
-        } else {
-            zone_finder->load(old_zone_finder_->getFileName());
+        if (!list) {
+            isc_throw(AuthCommandError, "There's no client list for "
+                      "class " << zone_class_);
+        }
+
+        switch (list->reload(origin_)) {
+            case ConfigurableClientList::ZONE_RELOADED:
+                // Everything worked fine.
+                LOG_DEBUG(auth_logger, DBG_AUTH_OPS, AUTH_LOAD_ZONE)
+                    .arg(zone_class_).arg(origin_);
+                return;
+            case ConfigurableClientList::ZONE_NOT_FOUND:
+                isc_throw(AuthCommandError, "Zone " << origin_ << "/" <<
+                          zone_class_ << " was not found in any configure "
+                          "data source. Configure it first.");
+            case ConfigurableClientList::ZONE_NOT_CACHED:
+                isc_throw(AuthCommandError, "Zone " << origin_ << "/" <<
+                          zone_class_ << " is not served from memory, but "
+                          "direcly from the data source. It is not possible "
+                          "to reload into memory, configure it to be cached "
+                          "first.");
+            case ConfigurableClientList::CACHE_DISABLED:
+                // This is an internal error. Auth server must have the cache
+                // enabled.
+                isc_throw(isc::Unexpected, "Cache disabled in client list of "
+                          "class " << zone_class_);
         }
-        old_zone_finder_->swap(*zone_finder);
-        LOG_DEBUG(auth_logger, DBG_AUTH_OPS, AUTH_LOAD_ZONE)
-                  .arg(zone_finder->getOrigin()).arg(zone_finder->getClass());
     }
 
 private:
-    // zone finder to be updated with the new file.
-    boost::shared_ptr<InMemoryZoneFinder> old_zone_finder_;
+    // Parsed arguments
+    RRClass zone_class_;
+    Name origin_;
 
     // A helper private method to parse and validate command parameters.
     // On success, it sets 'old_zone_finder_' to the zone to be updated.
     // It returns true if everything is okay; and false if the command is
     // valid but there's no need for further process.
-    bool validate(AuthSrv& , isc::data::ConstElementPtr ) {
-#if 0
-        TODO: Rewrite
+    bool validate(isc::data::ConstElementPtr args) {
         if (args == NULL) {
             isc_throw(AuthCommandError, "Null argument");
         }
 
-        // In this initial implementation, we assume memory data source
-        // for class IN by default.
-        ConstElementPtr datasrc_elem = args->get("datasrc");
-        if (datasrc_elem) {
-            if (datasrc_elem->stringValue() == "sqlite3") {
-                LOG_DEBUG(auth_logger, DBG_AUTH_OPS, AUTH_SQLITE3);
-                return (false);
-            } else if (datasrc_elem->stringValue() != "memory") {
-                // (note: at this point it's guaranteed that datasrc_elem
-                // is of string type)
-                isc_throw(AuthCommandError,
-                          "Data source type " << datasrc_elem->stringValue()
-                          << " is not supported");
-            }
-        }
-
         ConstElementPtr class_elem = args->get("class");
-        const RRClass zone_class =
-            class_elem ? RRClass(class_elem->stringValue()) : RRClass::IN();
-
-        isc::datasrc::DataSourceClient* datasrc(
-            server.getInMemoryClient(zone_class));
-        if (datasrc == NULL) {
-            isc_throw(AuthCommandError, "Memory data source is disabled");
-        }
+        zone_class_ = class_elem ? RRClass(class_elem->stringValue()) :
+            RRClass::IN();
 
         ConstElementPtr origin_elem = args->get("origin");
         if (!origin_elem) {
             isc_throw(AuthCommandError, "Zone origin is missing");
         }
-        const Name origin = Name(origin_elem->stringValue());
-
-        // Get the current zone
-        const DataSourceClient::FindResult result = datasrc->findZone(origin);
-        if (result.code != result::SUCCESS) {
-            isc_throw(AuthCommandError, "Zone " << origin <<
-                      " is not found in data source");
-        }
-
-        // It would appear that dynamic_cast does not work on all systems;
-        // it seems to confuse the RTTI system, resulting in NULL return
-        // values. So we use the more dangerous static_pointer_cast here.
-        old_zone_finder_ = boost::static_pointer_cast<InMemoryZoneFinder>(
-            result.zone_finder);
+        origin_ = Name(origin_elem->stringValue());
 
-#endif
         return (true);
     }
-
-    ConstElementPtr getZoneConfig(const AuthSrv &server) {
-        if (!server.getConfigSession()) {
-            // FIXME: This is a hack to make older tests pass. We should
-            // update these tests as well sometime and remove this hack.
-            // (note that under normal situation, the
-            // server.getConfigSession() does not return NULL)
-
-            // We provide an empty map, which means no configuration --
-            // defaults.
-            return (ConstElementPtr(new MapElement()));
-        }
-
-        // Find the config corresponding to the zone.
-        // We expect the configuration to be valid, as we have it and we
-        // accepted it before, therefore it must be validated.
-        const ConstElementPtr config(server.getConfigSession()->
-                                     getValue("datasources"));
-        ConstElementPtr zone_list;
-        // Unfortunately, we need to walk the list to find the correct data
-        // source.
-        // TODO: Make it named sets. These lists are uncomfortable.
-        for (size_t i(0); i < config->size(); ++i) {
-            // We use the getValue to get defaults as well
-            const ConstElementPtr dsrc_config(config->get(i));
-            const ConstElementPtr class_config(dsrc_config->get("class"));
-            const string class_type(class_config ?
-                                    class_config->stringValue() : "IN");
-            // It is in-memory and our class matches.
-            // FIXME: Is it allowed to have two datasources for the same
-            // type and class at once? It probably would not work now
-            // anyway and we may want to change the configuration of
-            // datasources somehow.
-            if (dsrc_config->get("type")->stringValue() == "memory" &&
-                RRClass(class_type) == old_zone_finder_->getClass()) {
-                zone_list = dsrc_config->get("zones");
-                break;
-            }
-        }
-
-        if (!zone_list) {
-            isc_throw(AuthCommandError,
-                      "Corresponding data source configuration was not found");
-        }
-
-        // Now we need to walk the zones and find the correct one.
-        for (size_t i(0); i < zone_list->size(); ++i) {
-            const ConstElementPtr zone_config(zone_list->get(i));
-            if (Name(zone_config->get("origin")->stringValue()) ==
-                old_zone_finder_->getOrigin()) {
-                // The origins are the same, so we consider this config to be
-                // for the zone.
-                return (zone_config);
-            }
-        }
-
-        isc_throw(AuthCommandError,
-                  "Corresponding zone configuration was not found");
-    }
 };
 
 // The factory of command objects.
@@ -310,11 +222,8 @@ createAuthCommand(const string& command_id) {
         return (new ShutdownCommand());
     } else if (command_id == "sendstats") {
         return (new SendStatsCommand());
-#if 0
-    // FIXME: The loadzone command will use #2046
     } else if (command_id == "loadzone") {
         return (new LoadZoneCommand());
-#endif
     } else if (false && command_id == "_throw_exception") {
         // This is for testing purpose only and should not appear in the
         // actual configuration syntax.

+ 83 - 207
src/bin/auth/tests/command_unittest.cc

@@ -19,6 +19,7 @@
 #include <auth/auth_srv.h>
 #include <auth/auth_config.h>
 #include <auth/command.h>
+#include <auth/datasrc_configurator.h>
 
 #include <dns/name.h>
 #include <dns/rrclass.h>
@@ -69,7 +70,9 @@ protected:
     {
         server_.setStatisticsSession(&statistics_session_);
     }
-    void checkAnswer(const int expected_code) {
+    void checkAnswer(const int expected_code, const char* name = "") {
+        SCOPED_TRACE(name);
+
         parseAnswer(rcode_, result_);
         EXPECT_EQ(expected_code, rcode_) << result_->str();
     }
@@ -177,29 +180,24 @@ TEST_F(AuthCommandTest, shutdownIncorrectPID) {
     EXPECT_EQ(0, rcode_);
 }
 
-#if 0
-TODO: This needs to be rewritten
-Also, reenable everywhere
 // A helper function commonly used for the "loadzone" command tests.
 // It configures the server with a memory data source containing two
 // zones, and checks the zones are correctly loaded.
 void
 zoneChecks(AuthSrv& server) {
-    EXPECT_TRUE(server.getInMemoryClient(RRClass::IN()));
-    EXPECT_EQ(ZoneFinder::SUCCESS, server.getInMemoryClient(RRClass::IN())->
-              findZone(Name("ns.test1.example")).zone_finder->
+    EXPECT_EQ(ZoneFinder::SUCCESS, server.getClientList(RRClass::IN())->
+              find(Name("ns.test1.example")).finder_->
               find(Name("ns.test1.example"), RRType::A())->code);
-    EXPECT_EQ(ZoneFinder::NXRRSET, server.getInMemoryClient(RRClass::IN())->
-              findZone(Name("ns.test1.example")).zone_finder->
+    EXPECT_EQ(ZoneFinder::NXRRSET, server.getClientList(RRClass::IN())->
+              find(Name("ns.test1.example")).finder_->
               find(Name("ns.test1.example"), RRType::AAAA())->code);
-    EXPECT_EQ(ZoneFinder::SUCCESS, server.getInMemoryClient(RRClass::IN())->
-              findZone(Name("ns.test2.example")).zone_finder->
+    EXPECT_EQ(ZoneFinder::SUCCESS, server.getClientList(RRClass::IN())->
+              find(Name("ns.test2.example")).finder_->
               find(Name("ns.test2.example"), RRType::A())->code);
-    EXPECT_EQ(ZoneFinder::NXRRSET, server.getInMemoryClient(RRClass::IN())->
-              findZone(Name("ns.test2.example")).zone_finder->
+    EXPECT_EQ(ZoneFinder::NXRRSET, server.getClientList(RRClass::IN())->
+              find(Name("ns.test2.example")).finder_->
               find(Name("ns.test2.example"), RRType::AAAA())->code);
 }
-#endif
 
 void
 configureZones(AuthSrv& server) {
@@ -207,51 +205,44 @@ configureZones(AuthSrv& server) {
                         TEST_DATA_BUILDDIR "/test1.zone.copied"));
     ASSERT_EQ(0, system(INSTALL_PROG " -c " TEST_DATA_DIR "/test2.zone.in "
                         TEST_DATA_BUILDDIR "/test2.zone.copied"));
-    configureAuthServer(server, Element::fromJSON(
-                            "{\"datasources\": "
-                            " [{\"type\": \"memory\","
-                            "   \"zones\": "
-                            "[{\"origin\": \"test1.example\","
-                            "  \"file\": \""
-                               TEST_DATA_BUILDDIR "/test1.zone.copied\"},"
-                            " {\"origin\": \"test2.example\","
-                            "  \"file\": \""
-                               TEST_DATA_BUILDDIR "/test2.zone.copied\"}"
-                            "]}]}"));
-    //TODO: zoneChecks(server);
+
+    const ConstElementPtr config(Element::fromJSON("{"
+        "\"IN\": [{"
+        "   \"type\": \"MasterFiles\","
+        "   \"params\": {"
+        "       \"test1.example\": \"" +
+                string(TEST_DATA_BUILDDIR "/test1.zone.copied") + "\","
+        "       \"test2.example\": \"" +
+                string(TEST_DATA_BUILDDIR "/test2.zone.copied") + "\""
+        "   },"
+        "   \"cache-enable\": true"
+        "}]}"));
+
+    DataSourceConfigurator::testReconfigure(&server, config);
+
+    zoneChecks(server);
 }
 
-#if 0
-TODO: This needs to be rewritten and re-enabled
 void
 newZoneChecks(AuthSrv& server) {
-    EXPECT_TRUE(server.getInMemoryClient(RRClass::IN()));
-    EXPECT_EQ(ZoneFinder::SUCCESS, server.getInMemoryClient(RRClass::IN())->
-              findZone(Name("ns.test1.example")).zone_finder->
+    EXPECT_EQ(ZoneFinder::SUCCESS, server.getClientList(RRClass::IN())->
+              find(Name("ns.test1.example")).finder_->
               find(Name("ns.test1.example"), RRType::A())->code);
     // now test1.example should have ns/AAAA
-    EXPECT_EQ(ZoneFinder::SUCCESS, server.getInMemoryClient(RRClass::IN())->
-              findZone(Name("ns.test1.example")).zone_finder->
+    EXPECT_EQ(ZoneFinder::SUCCESS, server.getClientList(RRClass::IN())->
+              find(Name("ns.test1.example")).finder_->
               find(Name("ns.test1.example"), RRType::AAAA())->code);
 
     // test2.example shouldn't change
-    EXPECT_EQ(ZoneFinder::SUCCESS, server.getInMemoryClient(RRClass::IN())->
-              findZone(Name("ns.test2.example")).zone_finder->
+    EXPECT_EQ(ZoneFinder::SUCCESS, server.getClientList(RRClass::IN())->
+              find(Name("ns.test2.example")).finder_->
               find(Name("ns.test2.example"), RRType::A())->code);
-    EXPECT_EQ(ZoneFinder::NXRRSET, server.getInMemoryClient(RRClass::IN())->
-              findZone(Name("ns.test2.example")).zone_finder->
+    EXPECT_EQ(ZoneFinder::NXRRSET, server.getClientList(RRClass::IN())->
+              find(Name("ns.test2.example")).finder_->
               find(Name("ns.test2.example"), RRType::AAAA())->code);
 }
-#endif
 
-TEST_F(AuthCommandTest,
-#ifdef USE_STATIC_LINK
-       DISABLED_loadZone
-#else
-       DISABLED_loadZone // Needs #2046
-#endif
-    )
-{
+TEST_F(AuthCommandTest, loadZone) {
     configureZones(server_);
 
     ASSERT_EQ(0, system(INSTALL_PROG " -c " TEST_DATA_DIR
@@ -265,62 +256,36 @@ TEST_F(AuthCommandTest,
                                     Element::fromJSON(
                                         "{\"origin\": \"test1.example\"}"));
     checkAnswer(0);
-    //TODO: newZoneChecks(server_);
+    newZoneChecks(server_);
 }
 
 TEST_F(AuthCommandTest,
 #ifdef USE_STATIC_LINK
        DISABLED_loadZoneSQLite3
 #else
-       DISABLED_loadZoneSQLite3 // Needs #2044
+       loadZoneSQLite3
 #endif
     )
 {
-    const char* const SPEC_FILE = AUTH_OBJ_DIR "/auth.spec";
-
     // Prepare the database first
     const string test_db = TEST_DATA_BUILDDIR "/auth_test.sqlite3.copied";
     const string bad_db = TEST_DATA_BUILDDIR "/does-not-exist.sqlite3";
     stringstream ss("example.org. 3600 IN SOA . . 0 0 0 0 0\n");
     createSQLite3DB(RRClass::IN(), Name("example.org"), test_db.c_str(), ss);
-
-    // Then store a config of the zone to the auth server
-    // This omits many config options of the auth server, but these are
-    // not read now.
-    isc::testutils::MockSession session;
-    // The session should not take care of anything or start anything, we
-    // need it only to hold the config we're going to put into it.
-    ModuleCCSession module_session(SPEC_FILE, session, NULL, NULL, false,
-                                  false);
     // This describes the data source in the configuration
-    const ElementPtr
-        map(Element::fromJSON("{\"datasources\": ["
-                              "  {"
-                              "    \"type\": \"memory\","
-                              "    \"zones\": ["
-                              "      {"
-                              "        \"origin\": \"example.org\","
-                              "        \"file\": \"" + test_db + "\","
-                              "        \"filetype\": \"sqlite3\""
-                              "      }"
-                              "    ]"
-                              "  }"
-                              "],"
-                              " \"database_file\": \"" + test_db + "\""
-                              "}"));
-    module_session.setLocalConfig(map);
-    server_.setConfigSession(&module_session);
-
-    server_.updateConfig(map);
-
-#if 0
-    FIXME: This needs to be done slightly differently
+    const ConstElementPtr config(Element::fromJSON("{"
+        "\"IN\": [{"
+        "    \"type\": \"sqlite3\","
+        "    \"params\": {\"database_file\": \"" + test_db + "\"},"
+        "    \"cache-enable\": true,"
+        "    \"cache-zones\": [\"example.org\"]"
+        "}]}"));
+    DataSourceConfigurator::testReconfigure(&server_, config);
+
     // Check that the A record at www.example.org does not exist
-    ASSERT_TRUE(server_.hasInMemoryClient());
-    EXPECT_EQ(ZoneFinder::NXDOMAIN, server_.getInMemoryClient(RRClass::IN())->
-              findZone(Name("example.org")).zone_finder->
+    EXPECT_EQ(ZoneFinder::NXDOMAIN, server_.getClientList(RRClass::IN())->
+              find(Name("example.org")).finder_->
               find(Name("www.example.org"), RRType::A())->code);
-#endif
 
     // Add the record to the underlying sqlite database, by loading
     // it as a separate datasource, and updating it
@@ -338,107 +303,52 @@ TEST_F(AuthCommandTest,
     sql_updater->addRRset(*rrset);
     sql_updater->commit();
 
-#if 0
-    TODO:
-    // This new record is in the database now, but should not be in the
-    // memory-datasource yet, so check again
-    EXPECT_EQ(ZoneFinder::NXDOMAIN, server_.getInMemoryClient(RRClass::IN())->
-              findZone(Name("example.org")).zone_finder->
+    EXPECT_EQ(ZoneFinder::NXDOMAIN, server_.getClientList(RRClass::IN())->
+              find(Name("example.org")).finder_->
               find(Name("www.example.org"), RRType::A())->code);
-#endif
 
     // Now send the command to reload it
     result_ = execAuthServerCommand(server_, "loadzone",
                                     Element::fromJSON(
                                         "{\"origin\": \"example.org\"}"));
-    checkAnswer(0);
+    checkAnswer(0, "Successfull load");
 
-#if 0
     // And now it should be present too.
-    EXPECT_EQ(ZoneFinder::SUCCESS, server_.getInMemoryClient(RRClass::IN())->
-              findZone(Name("example.org")).zone_finder->
+    EXPECT_EQ(ZoneFinder::SUCCESS, server_.getClientList(RRClass::IN())->
+              find(Name("example.org")).finder_->
               find(Name("www.example.org"), RRType::A())->code);
-#endif
 
     // Some error cases. First, the zone has no configuration. (note .com here)
     result_ = execAuthServerCommand(server_, "loadzone",
         Element::fromJSON("{\"origin\": \"example.com\"}"));
-    checkAnswer(1);
-#if 0
-    FIXME
+    checkAnswer(1, "example.com");
+
     // The previous zone is not hurt in any way
-    EXPECT_EQ(ZoneFinder::SUCCESS, server_.getInMemoryClient(RRClass::IN())->
-              findZone(Name("example.org")).zone_finder->
+    EXPECT_EQ(ZoneFinder::SUCCESS, server_.getClientList(RRClass::IN())->
+              find(Name("example.org")).finder_->
               find(Name("example.org"), RRType::SOA())->code);
-#endif
 
-    module_session.setLocalConfig(Element::fromJSON("{\"datasources\": []}"));
-    result_ = execAuthServerCommand(server_, "loadzone",
-                                    Element::fromJSON(
-                                        "{\"origin\": \"example.org\"}"));
-    checkAnswer(1);
+    const ConstElementPtr config2(Element::fromJSON("{"
+        "\"IN\": [{"
+        "    \"type\": \"sqlite3\","
+        "    \"params\": {\"database_file\": \"" + bad_db + "\"},"
+        "    \"cache-enable\": true,"
+        "    \"cache-zones\": [\"example.com\"]"
+        "}]}"));
+    EXPECT_THROW(DataSourceConfigurator::testReconfigure(&server_, config2),
+                 ConfigurableClientList::ConfigurationError);
 
-#if 0
-    FIXME
-    // The previous zone is not hurt in any way
-    EXPECT_EQ(ZoneFinder::SUCCESS, server_.getInMemoryClient(RRClass::IN())->
-              findZone(Name("example.org")).zone_finder->
-              find(Name("example.org"), RRType::SOA())->code);
-#endif
-    // Configure an unreadable zone. Should fail, but leave the original zone
-    // data there
-    const ElementPtr
-        mapBad(Element::fromJSON("{\"datasources\": ["
-                                 "  {"
-                                 "    \"type\": \"memory\","
-                                 "    \"zones\": ["
-                                 "      {"
-                                 "        \"origin\": \"example.org\","
-                                 "        \"file\": \"" + bad_db + "\","
-                                 "        \"filetype\": \"sqlite3\""
-                                 "      }"
-                                 "    ]"
-                                 "  }"
-                                 "]}"));
-    module_session.setLocalConfig(mapBad);
     result_ = execAuthServerCommand(server_, "loadzone",
         Element::fromJSON("{\"origin\": \"example.com\"}"));
-    checkAnswer(1);
-#if 0
-    FIXME
-    // The previous zone is not hurt in any way
-    EXPECT_EQ(ZoneFinder::SUCCESS, server_.getInMemoryClient(RRClass::IN())->
-              findZone(Name("example.org")).zone_finder->
-              find(Name("example.org"), RRType::SOA())->code);
-#endif
+    checkAnswer(1, "Unreadable");
 
-    // Broken configuration (not valid against the spec)
-    const ElementPtr
-        broken(Element::fromJSON("{\"datasources\": ["
-                                 "  {"
-                                 "    \"type\": \"memory\","
-                                 "    \"zones\": [[]]"
-                                 "  }"
-                                 "]}"));
-    module_session.setLocalConfig(broken);
-    checkAnswer(1);
-#if 0
-    FIXME
     // The previous zone is not hurt in any way
-    EXPECT_EQ(ZoneFinder::SUCCESS, server_.getInMemoryClient(RRClass::IN())->
-              findZone(Name("example.org")).zone_finder->
+    EXPECT_EQ(ZoneFinder::SUCCESS, server_.getClientList(RRClass::IN())->
+              find(Name("example.org")).finder_->
               find(Name("example.org"), RRType::SOA())->code);
-#endif
 }
 
-TEST_F(AuthCommandTest,
-#ifdef USE_STATIC_LINK
-       DISABLED_loadBrokenZone
-#else
-       DISABLED_loadBrokenZone // Needs #2046
-#endif
-    )
-{
+TEST_F(AuthCommandTest, loadBrokenZone) {
     configureZones(server_);
 
     ASSERT_EQ(0, system(INSTALL_PROG " -c " TEST_DATA_DIR
@@ -448,17 +358,10 @@ TEST_F(AuthCommandTest,
                                     Element::fromJSON(
                                         "{\"origin\": \"test1.example\"}"));
     checkAnswer(1);
-    //zoneChecks(server_);     // zone shouldn't be replaced
+    zoneChecks(server_);     // zone shouldn't be replaced
 }
 
-TEST_F(AuthCommandTest,
-#ifdef USE_STATIC_LINK
-       DISABLED_loadUnreadableZone
-#else
-       DISABLED_loadUnreadableZone // Needs #2046
-#endif
-    )
-{
+TEST_F(AuthCommandTest, loadUnreadableZone) {
     configureZones(server_);
 
     // install the zone file as unreadable
@@ -469,7 +372,7 @@ TEST_F(AuthCommandTest,
                                     Element::fromJSON(
                                         "{\"origin\": \"test1.example\"}"));
     checkAnswer(1);
-    //zoneChecks(server_);     // zone shouldn't be replaced
+    zoneChecks(server_);     // zone shouldn't be replaced
 }
 
 TEST_F(AuthCommandTest, loadZoneWithoutDataSrc) {
@@ -481,72 +384,45 @@ TEST_F(AuthCommandTest, loadZoneWithoutDataSrc) {
     checkAnswer(1);
 }
 
-TEST_F(AuthCommandTest,
-#ifdef USE_STATIC_LINK
-       DISABLED_loadZoneInvalidParams
-#else
-       DISABLED_loadZoneInvalidParams // Needs #2046
-#endif
-    )
-{
+TEST_F(AuthCommandTest, loadZoneInvalidParams) {
     configureZones(server_);
 
     // null arg
     result_ = execAuthServerCommand(server_, "loadzone", ElementPtr());
-    checkAnswer(1);
+    checkAnswer(1, "Null arg");
 
     // zone class is bogus
     result_ = execAuthServerCommand(server_, "loadzone",
                                     Element::fromJSON(
                                         "{\"origin\": \"test1.example\","
                                         " \"class\": \"no_such_class\"}"));
-    checkAnswer(1);
+    checkAnswer(1, "No such class");
 
     result_ = execAuthServerCommand(server_, "loadzone",
                                     Element::fromJSON(
                                         "{\"origin\": \"test1.example\","
                                         " \"class\": 1}"));
-    checkAnswer(1);
+    checkAnswer(1, "Integral class");
 
-    // unsupported zone class
-    result_ = execAuthServerCommand(server_, "loadzone",
-                                    Element::fromJSON(
-                                        "{\"origin\": \"test1.example\","
-                                        " \"class\": \"CH\"}"));
-    checkAnswer(1);
-
-    // unsupported data source class
-    result_ = execAuthServerCommand(server_, "loadzone",
-                                    Element::fromJSON(
-                                        "{\"origin\": \"test1.example\","
-                                        " \"datasrc\": \"not supported\"}"));
-    checkAnswer(1);
-
-    // data source is bogus
-    result_ = execAuthServerCommand(server_, "loadzone",
-                                    Element::fromJSON(
-                                        "{\"origin\": \"test1.example\","
-                                        " \"datasrc\": 0}"));
-    checkAnswer(1);
 
     // origin is missing
     result_ = execAuthServerCommand(server_, "loadzone",
                                     Element::fromJSON("{}"));
-    checkAnswer(1);
+    checkAnswer(1, "Missing origin");
 
     // zone doesn't exist in the data source
     result_ = execAuthServerCommand(server_, "loadzone",
                                     Element::fromJSON("{\"origin\": \"xx\"}"));
-    checkAnswer(1);
+    checkAnswer(1, "No such zone");
 
     // origin is bogus
     result_ = execAuthServerCommand(server_, "loadzone",
                                     Element::fromJSON(
                                         "{\"origin\": \"...\"}"));
-    checkAnswer(1);
+    checkAnswer(1, "Wrong name");
 
     result_ = execAuthServerCommand(server_, "loadzone",
                                     Element::fromJSON("{\"origin\": 10}"));
-    checkAnswer(1);
+    checkAnswer(1, "Integral name");
 }
 }

+ 82 - 24
src/lib/datasrc/client_list.cc

@@ -157,37 +157,49 @@ ConfigurableClientList::configure(const ConstElementPtr& config,
         // the scope.
         data_sources_.swap(new_data_sources);
         configuration_ = config;
+        allow_cache_ = allow_cache;
     } catch (const TypeError& te) {
         isc_throw(ConfigurationError, "Malformed configuration at data source "
                   "no. " << i << ": " << te.what());
     }
 }
 
+// We have this class as a temporary storage, as the FindResult can't be
+// assigned.
+struct ConfigurableClientList::MutableResult {
+    MutableResult() :
+        datasrc_client(NULL),
+        matched_labels(0),
+        matched(false),
+        exact(false),
+        info(NULL)
+    {}
+    DataSourceClient* datasrc_client;
+    ZoneFinderPtr finder;
+    uint8_t matched_labels;
+    bool matched;
+    bool exact;
+    const DataSourceInfo* info;
+    operator FindResult() const {
+        // Conversion to the right result.
+        return (FindResult(datasrc_client, finder, exact));
+    }
+};
+
 ClientList::FindResult
 ConfigurableClientList::find(const dns::Name& name, bool want_exact_match,
-                            bool) const
+                             bool want_finder) const
 {
-    // Nothing found yet.
-    //
-    // We have this class as a temporary storage, as the FindResult can't be
-    // assigned.
-    struct MutableResult {
-        MutableResult() :
-            datasrc_client(NULL),
-            matched_labels(0),
-            matched(false)
-        {}
-        DataSourceClient* datasrc_client;
-        ZoneFinderPtr finder;
-        uint8_t matched_labels;
-        bool matched;
-        operator FindResult() const {
-            // Conversion to the right result. If we return this, there was
-            // a partial match at best.
-            return (FindResult(datasrc_client, finder, false));
-        }
-    } candidate;
+    MutableResult result;
+    findInternal(result, name, want_exact_match, want_finder);
+    return (result);
+}
 
+void
+ConfigurableClientList::findInternal(MutableResult& candidate,
+                                     const dns::Name& name,
+                                     bool want_exact_match, bool) const
+{
     BOOST_FOREACH(const DataSourceInfo& info, data_sources_) {
         DataSourceClient* client(info.cache_ ? info.cache_.get() :
                                  info.data_src_client_);
@@ -201,8 +213,12 @@ ConfigurableClientList::find(const dns::Name& name, bool want_exact_match,
 
                 // TODO: In case we have only the datasource and not the finder
                 // and the need_updater parameter is true, get the zone there.
-                return (FindResult(client, result.zone_finder,
-                                   true));
+                candidate.datasrc_client = client;
+                candidate.finder = result.zone_finder;
+                candidate.matched = true;
+                candidate.exact = true;
+                candidate.info = &info;
+                return;
             case result::PARTIALMATCH:
                 if (!want_exact_match) {
                     // In case we have a partial match, check if it is better
@@ -226,6 +242,7 @@ ConfigurableClientList::find(const dns::Name& name, bool want_exact_match,
                         candidate.finder = result.zone_finder;
                         candidate.matched_labels = labels;
                         candidate.matched = true;
+                        candidate.info = &info;
                     }
                 }
                 break;
@@ -240,7 +257,48 @@ ConfigurableClientList::find(const dns::Name& name, bool want_exact_match,
 
     // Return the partial match we have. In case we didn't want a partial
     // match, this surely contains the original empty result.
-    return (candidate);
+}
+
+ConfigurableClientList::ReloadResult
+ConfigurableClientList::reload(const Name& name) {
+    if (!allow_cache_) {
+        return (CACHE_DISABLED);
+    }
+    // Try to find the correct zone.
+    MutableResult result;
+    findInternal(result, name, true, true);
+    if (!result.finder) {
+        return (ZONE_NOT_FOUND);
+    }
+    // Try to convert the finder to in-memory one. If it is the cache,
+    // it should work.
+    shared_ptr<InMemoryZoneFinder>
+        finder(dynamic_pointer_cast<InMemoryZoneFinder>(result.finder));
+    const DataSourceInfo* info(result.info);
+    // It is of a different type or there's no cache.
+    if (!info->cache_ || !finder) {
+        return (ZONE_NOT_CACHED);
+    }
+    DataSourceClient* client(info->data_src_client_);
+    if (client) {
+        // Now do the final reload. If it does not exist in client,
+        // DataSourceError is thrown, which is exactly the result what we
+        // want, so no need to handle it.
+        ZoneIteratorPtr iterator(client->getIterator(name));
+        if (!iterator) {
+            isc_throw(isc::Unexpected, "Null iterator from " << name);
+        }
+        finder->load(*iterator);
+    } else {
+        // The MasterFiles special case
+        const string filename(finder->getFileName());
+        if (filename.empty()) {
+            isc_throw(isc::Unexpected, "Confused about missing both filename "
+                      "and data source");
+        }
+        finder->load(filename);
+    }
+    return (ZONE_RELOADED);
 }
 
 // NOTE: This function is not tested, it would be complicated. However, the

+ 34 - 0
src/lib/datasrc/client_list.h

@@ -238,6 +238,28 @@ public:
         return (configuration_);
     }
 
+    /// \brief Result of the reload() method.
+    enum ReloadResult {
+        CACHE_DISABLED,     ///< The cache is not enabled in this list.
+        ZONE_NOT_CACHED,    ///< Zone is served directly, not from cache.
+        ZONE_NOT_FOUND,     ///< Zone does not exist or not cached.
+        ZONE_RELOADED       ///< The zone was successfully reloaded.
+    };
+
+    /// \brief Reloads a cached zone.
+    ///
+    /// This method finds a zone which is loaded into a cache and reloads it.
+    /// This may be used to renew the cache when the underlying data source
+    /// changes.
+    ///
+    /// \param zone The origin of the zone to reload.
+    /// \return A status if the command worked.
+    /// \throw DataSourceError or anything else that the data source
+    ///      containing the zone might throw is propagated.
+    /// \throw DataSourceError if something unexpected happens, like when
+    ///      the original data source no longer contains the cached zone.
+    ReloadResult reload(const dns::Name& zone);
+
     /// \brief Implementation of the ClientList::find.
     virtual FindResult find(const dns::Name& zone,
                             bool want_exact_match = false,
@@ -300,9 +322,21 @@ public:
     /// hide it).
     const DataSources& getDataSources() const { return (data_sources_); }
 private:
+    struct MutableResult;
+    /// \brief Internal implementation of find.
+    ///
+    /// The class itself needs to do some internal searches in other methods,
+    /// so the implementation is shared.
+    ///
+    /// The result is returned as parameter because MutableResult is not
+    /// defined in the header file.
+    void findInternal(MutableResult& result, const dns::Name& name,
+                      bool want_exact_match, bool want_finder) const;
     const isc::dns::RRClass rrclass_;
     /// \brief Currently active configuration.
     isc::data::ConstElementPtr configuration_;
+    /// \brief The last set value of allow_cache.
+    bool allow_cache_;
 };
 
 } // namespace datasrc

+ 161 - 1
src/lib/datasrc/tests/client_list_unittest.cc

@@ -225,6 +225,12 @@ public:
             "{"
             "   \"type\": \"test_type\","
             "   \"params\": {}"
+            "}]")),
+        config_elem_zones_(Element::fromJSON("["
+            "{"
+            "   \"type\": \"test_type\","
+            "   \"params\": [\"example.org\", \"example.com\", "
+            "                \"noiter.org\", \"null.org\"]"
             "}]"))
     {
         for (size_t i(0); i < ds_count; ++ i) {
@@ -235,6 +241,24 @@ public:
                 DataSourceClientContainerPtr(), false));
         }
     }
+    void prepareCache(size_t index, const Name& zone, bool prefill = false) {
+        const shared_ptr<InMemoryClient> cache(new InMemoryClient());
+        const shared_ptr<InMemoryZoneFinder>
+            finder(new InMemoryZoneFinder(RRClass::IN(), zone));
+        if (prefill) {
+            RRsetPtr soa(new RRset(zone, RRClass::IN(), RRType::SOA(),
+                                   RRTTL(3600)));
+            // The RData here is bogus, but it is not used to anything. There
+            // just needs to be some.
+            soa->addRdata(rdata::generic::SOA(Name::ROOT_NAME(),
+                                              Name::ROOT_NAME(),
+                                              0, 0, 0, 0, 0));
+            finder->add(soa);
+        }
+        // We leave the zone empty, so we can check it was reloaded.
+        cache->addZone(finder);
+        list_->getDataSources()[index].cache_ = cache;
+    }
     // Check the positive result is as we expect it.
     void positiveResult(const ClientList::FindResult& result,
                         const shared_ptr<MockDataSourceClient>& dsrc,
@@ -303,7 +327,7 @@ public:
     const ClientList::FindResult negativeResult_;
     vector<shared_ptr<MockDataSourceClient> > ds_;
     vector<ConfigurableClientList::DataSourceInfo> ds_info_;
-    const ConstElementPtr config_elem_;
+    const ConstElementPtr config_elem_, config_elem_zones_;
 };
 
 // Test the test itself
@@ -750,4 +774,140 @@ TEST_F(ListTest, masterFiles) {
     EXPECT_EQ(0, list_->getDataSources().size());
 }
 
+// Test we can reload a zone
+TEST_F(ListTest, reloadSuccess) {
+    list_->configure(config_elem_zones_, true);
+    Name name("example.org");
+    prepareCache(0, name);
+    // Not there yet. It would be NXDOMAIN, but it is in apex and
+    // it returns NXRRSET instead.
+    EXPECT_EQ(ZoneFinder::NXRRSET,
+              list_->find(name).finder_->find(name, RRType::SOA())->code);
+    // Now reload. It should be there now.
+    EXPECT_EQ(ConfigurableClientList::ZONE_RELOADED, list_->reload(name));
+    EXPECT_EQ(ZoneFinder::SUCCESS,
+              list_->find(name).finder_->find(name, RRType::SOA())->code);
+}
+
+// The cache is not enabled. The load should be rejected.
+TEST_F(ListTest, reloadNotEnabled) {
+    list_->configure(config_elem_zones_, false);
+    Name name("example.org");
+    // We put the cache in even when not enabled. This won't confuse the thing.
+    prepareCache(0, name);
+    // Not there yet. It would be NXDOMAIN, but it is in apex and
+    // it returns NXRRSET instead.
+    EXPECT_EQ(ZoneFinder::NXRRSET,
+              list_->find(name).finder_->find(name, RRType::SOA())->code);
+    // Now reload. It should reject it.
+    EXPECT_EQ(ConfigurableClientList::CACHE_DISABLED, list_->reload(name));
+    // Nothing changed here
+    EXPECT_EQ(ZoneFinder::NXRRSET,
+              list_->find(name).finder_->find(name, RRType::SOA())->code);
+}
+
+// Test several cases when the zone does not exist
+TEST_F(ListTest, reloadNoSuchZone) {
+    list_->configure(config_elem_zones_, true);
+    Name name("example.org");
+    // We put the cache in even when not enabled. This won't confuse the thing.
+    prepareCache(0, Name("example.com"));
+    // Not in the data sources
+    EXPECT_EQ(ConfigurableClientList::ZONE_NOT_FOUND,
+              list_->reload(Name("example.cz")));
+    // Not cached
+    EXPECT_EQ(ConfigurableClientList::ZONE_NOT_FOUND, list_->reload(name));
+    // Partial match
+    EXPECT_EQ(ConfigurableClientList::ZONE_NOT_FOUND,
+              list_->reload(Name("sub.example.com")));
+    // Nothing changed here - these zones don't exist
+    EXPECT_EQ(NULL, list_->find(name).dsrc_client_);
+    EXPECT_EQ(NULL, list_->find(Name("example.cz")).dsrc_client_);
+    EXPECT_EQ(NULL, list_->find(Name("sub.example.com"), true).dsrc_client_);
+    // Not reloaded
+    EXPECT_EQ(ZoneFinder::NXRRSET,
+              list_->find(Name("example.com")).finder_->
+              find(Name("example.com"), RRType::SOA())->code);
+}
+
+// Check we gracefuly throw an exception when a zone disappeared in
+// the underlying data source when we want to reload it
+TEST_F(ListTest, reloadZoneGone) {
+    list_->configure(config_elem_, true);
+    Name name("example.org");
+    // We put in a cache for non-existant zone. This emulates being loaded
+    // and then the zone disappearing. We prefill the cache, so we can check
+    // it.
+    prepareCache(0, name, true);
+    // The zone contains something
+    EXPECT_EQ(ZoneFinder::SUCCESS,
+              list_->find(name).finder_->find(name, RRType::SOA())->code);
+    // The zone is not there, so abort the reload.
+    EXPECT_THROW(list_->reload(name), DataSourceError);
+    // The zone is not hurt.
+    EXPECT_EQ(ZoneFinder::SUCCESS,
+              list_->find(name).finder_->find(name, RRType::SOA())->code);
+}
+
+// The underlying data source throws. Check we don't modify the state.
+TEST_F(ListTest, reloadZoneThrow) {
+    list_->configure(config_elem_zones_, true);
+    Name name("noiter.org");
+    prepareCache(0, name, true);
+    // The zone contains stuff now
+    EXPECT_EQ(ZoneFinder::SUCCESS,
+              list_->find(name).finder_->find(name, RRType::SOA())->code);
+    // The iterator throws, so abort the reload.
+    EXPECT_THROW(list_->reload(name), isc::NotImplemented);
+    // The zone is not hurt.
+    EXPECT_EQ(ZoneFinder::SUCCESS,
+              list_->find(name).finder_->find(name, RRType::SOA())->code);
+}
+
+TEST_F(ListTest, reloadNullIterator) {
+    list_->configure(config_elem_zones_, true);
+    Name name("null.org");
+    prepareCache(0, name, true);
+    // The zone contains stuff now
+    EXPECT_EQ(ZoneFinder::SUCCESS,
+              list_->find(name).finder_->find(name, RRType::SOA())->code);
+    // The iterator throws, so abort the reload.
+    EXPECT_THROW(list_->reload(name), isc::Unexpected);
+    // The zone is not hurt.
+    EXPECT_EQ(ZoneFinder::SUCCESS,
+              list_->find(name).finder_->find(name, RRType::SOA())->code);
+}
+
+// Test we can reload the master files too (special-cased)
+TEST_F(ListTest, reloadMasterFile) {
+    const ConstElementPtr elem(Element::fromJSON("["
+        "{"
+        "   \"type\": \"MasterFiles\","
+        "   \"cache-enable\": true,"
+        "   \"params\": {"
+        "       \".\": \"" TEST_DATA_DIR "/root.zone\""
+        "   }"
+        "}]"));
+    list_->configure(elem, true);
+    // Add an element there so it differs from the one in file.
+    EXPECT_EQ(ZoneFinder::NXDOMAIN,
+              list_->find(Name(".")).finder_->find(Name("nosuchdomain"),
+                                                   RRType::TXT())->code);
+    RRsetPtr txt(new RRset(Name("nosuchdomain"), RRClass::IN(), RRType::TXT(),
+                                RRTTL(3600)));
+    txt->addRdata(rdata::generic::TXT("test"));
+    dynamic_pointer_cast<InMemoryZoneFinder>(list_->find(Name(".")).finder_)->
+        add(txt);
+    // It is here now.
+    EXPECT_EQ(ZoneFinder::SUCCESS,
+              list_->find(Name(".")).finder_->find(Name("nosuchdomain"),
+                                                   RRType::TXT())->code);
+    // Do the reload.
+    EXPECT_EQ(ConfigurableClientList::ZONE_RELOADED, list_->reload(Name(".")));
+    // And our TXT record disappeared again, as it is not in the file.
+    EXPECT_EQ(ZoneFinder::NXDOMAIN,
+              list_->find(Name(".")).finder_->find(Name("nosuchdomain"),
+                                                   RRType::TXT())->code);
+}
+
 }