Commit 686594e3 authored by Jelte Jansen's avatar Jelte Jansen
Browse files

[master] Merge branch 'trac2213'

Conflicts:
	src/bin/auth/command.cc
parents b1b6e109 42298cee
......@@ -178,11 +178,6 @@ has requested the keyring holding TSIG keys from the configuration
database. It is issued during server startup is an indication that the
initialization is proceeding normally.
% AUTH_LOAD_ZONE loaded zone %1/%2
This debug message is issued during the processing of the 'loadzone' command
when the authoritative server has successfully loaded the named zone of the
named class.
% AUTH_MEM_DATASRC_DISABLED memory data source is disabled for class %1
This is a debug message reporting that the authoritative server has
discovered that the memory data source is disabled for the given class.
......
......@@ -176,51 +176,7 @@ public:
virtual ConstElementPtr exec(AuthSrv& server,
isc::data::ConstElementPtr args)
{
if (args == NULL) {
isc_throw(AuthCommandError, "Null argument");
}
ConstElementPtr class_elem = args->get("class");
RRClass 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(origin_elem->stringValue());
DataSrcClientsMgr::Holder holder(server.getDataSrcClientsMgr());
boost::shared_ptr<ConfigurableClientList> list =
holder.findClientList(zone_class);
if (!list) {
isc_throw(AuthCommandError, "There's no client list for "
"class " << zone_class);
}
switch (list->reload(origin)) {
case ConfigurableClientList::ZONE_SUCCESS:
// Everything worked fine.
LOG_DEBUG(auth_logger, DBG_AUTH_OPS, AUTH_LOAD_ZONE)
.arg(zone_class).arg(origin);
return (createAnswer());
case ConfigurableClientList::ZONE_NOT_FOUND:
isc_throw(AuthCommandError, "Zone " << origin << "/" <<
zone_class << " was not found in any configured "
"data source. Configure it first.");
case ConfigurableClientList::ZONE_NOT_CACHED:
isc_throw(AuthCommandError, "Zone " << origin << "/" <<
zone_class << " is not served from memory, but "
"directly from the data source. It is not possible "
"to reload it 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);
}
server.getDataSrcClientsMgr().loadZone(args);
return (createAnswer());
}
};
......
......@@ -45,6 +45,19 @@
namespace isc {
namespace auth {
/// \brief An exception that is thrown if initial checks for a command fail
///
/// This is raised *before* the command to the thread is constructed and
/// sent, so the application can still handle them (and therefore it is
/// public, as opposed to InternalCommandError).
///
/// And example of its use is currently in loadZone().
class CommandError : public isc::Exception {
public:
CommandError(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {}
};
namespace datasrc_clientmgr_internal {
// This namespace is essentially private for DataSrcClientsMgr(Base) and
// DataSrcClientsBuilder(Base). This is exposed in the public header
......@@ -239,6 +252,60 @@ public:
clients_map_ = new_lists;
}
/// \brief Instruct internal thread to (re)load a zone
///
/// \param args Element argument that should be a map of the form
/// { "class": "IN", "origin": "example.com" }
/// (but class is optional and will default to IN)
///
/// \exception CommandError if the args value is null, or not in
/// the expected format, or contains
/// a bad origin or class string
void
loadZone(data::ConstElementPtr args) {
if (!args) {
isc_throw(CommandError, "loadZone argument empty");
}
if (args->getType() != isc::data::Element::map) {
isc_throw(CommandError, "loadZone argument not a map");
}
if (!args->contains("origin")) {
isc_throw(CommandError,
"loadZone argument has no 'origin' value");
}
// Also check if it really is a valid name
try {
dns::Name(args->get("origin")->stringValue());
} catch (const isc::Exception& exc) {
isc_throw(CommandError, "bad origin: " << exc.what());
}
if (args->get("origin")->getType() != data::Element::string) {
isc_throw(CommandError,
"loadZone argument 'origin' value not a string");
}
if (args->contains("class")) {
if (args->get("class")->getType() != data::Element::string) {
isc_throw(CommandError,
"loadZone argument 'class' value not a string");
}
// Also check if it is a valid class
try {
dns::RRClass(args->get("class")->stringValue());
} catch (const isc::Exception& exc) {
isc_throw(CommandError, "bad class: " << exc.what());
}
}
// Note: we could do some more advanced checks here,
// e.g. check if the zone is known at all in the configuration.
// For now these are skipped, but one obvious way to
// implement it would be to factor out the code from
// the start of doLoadZone(), and call it here too
sendCommand(datasrc_clientmgr_internal::LOADZONE, args);
}
private:
// This is expected to be called at the end of the destructor. It
// actually does nothing, but provides a customization point for
......@@ -316,9 +383,6 @@ public:
///
/// It simply sets up a local copy of shared data with the manager.
///
/// Note: this will take actual set (map) of data source clients and
/// a mutex object for it in #2210 or #2212.
///
/// \throw None
DataSrcClientsBuilderBase(std::list<Command>* command_queue,
CondVarType* cond, MutexType* queue_mutex,
......@@ -489,10 +553,20 @@ DataSrcClientsBuilderBase<MutexType, CondVarType>::doLoadZone(
// called via the manager in practice. manager is expected to do the
// minimal validation.
assert(arg);
assert(arg->get("class"));
assert(arg->get("origin"));
const dns::RRClass rrclass(arg->get("class")->stringValue());
// TODO: currently, we hardcode IN as the default for the optional
// 'class' argument. We should really derive this from the specification,
// but at the moment the config/command API does not allow that to be
// done easily. Once that is in place (tickets have yet to be created,
// as we need to do a tiny bit of design work for that), this
// code can be replaced with the original part:
// assert(arg->get("class"));
// const dns::RRClass(arg->get("class")->stringValue());
isc::data::ConstElementPtr class_elem = arg->get("class");
const dns::RRClass rrclass(class_elem ?
dns::RRClass(class_elem->stringValue()) :
dns::RRClass::IN());
const dns::Name origin(arg->get("origin")->stringValue());
ClientListsMap::iterator found = (*clients_map_)->find(rrclass);
if (found == (*clients_map_)->end()) {
......
......@@ -1839,6 +1839,15 @@ namespace {
isc::config::parseAnswer(command_result, response);
EXPECT_EQ(0, command_result);
}
void sendCommand(AuthSrv& server, const std::string& command,
ConstElementPtr args, int expected_result) {
ConstElementPtr response = execAuthServerCommand(server, command,
args);
int command_result = -1;
isc::config::parseAnswer(command_result, response);
EXPECT_EQ(expected_result, command_result);
}
} // end anonymous namespace
TEST_F(AuthSrvTest, DDNSForwardCreateDestroy) {
......@@ -1910,4 +1919,18 @@ TEST_F(AuthSrvTest, DDNSForwardCreateDestroy) {
Opcode::UPDATE().getCode(), QR_FLAG, 0, 0, 0, 0);
}
TEST_F(AuthSrvTest, loadZoneCommand) {
// Just some very basic tests, to check the command is accepted, and that
// it raises on bad arguments, but not on correct ones (full testing
// is handled in the unit tests for the corresponding classes)
// Empty map should fail
ElementPtr args(Element::createMap());
sendCommand(server, "loadzone", args, 1);
// Setting an origin should be enough (even if it isn't actually loaded,
// it should be initially accepted)
args->set("origin", Element::create("example.com"));
sendCommand(server, "loadzone", args, 0);
}
}
......@@ -169,281 +169,6 @@ TEST_F(AuthCommandTest, shutdownIncorrectPID) {
EXPECT_EQ(0, rcode_);
}
// 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) {
const RRClass rrclass(RRClass::IN());
DataSrcClientsMgr::Holder holder(server.getDataSrcClientsMgr());
EXPECT_EQ(ZoneFinder::SUCCESS,
holder.findClientList(rrclass)->find(Name("ns.test1.example"))
.finder_->find(Name("ns.test1.example"), RRType::A())->code);
EXPECT_EQ(ZoneFinder::NXRRSET,
holder.findClientList(rrclass)->find(Name("ns.test1.example")).
finder_->find(Name("ns.test1.example"), RRType::AAAA())->code);
EXPECT_EQ(ZoneFinder::SUCCESS,
holder.findClientList(rrclass)->find(Name("ns.test2.example")).
finder_->find(Name("ns.test2.example"), RRType::A())->code);
EXPECT_EQ(ZoneFinder::NXRRSET,
holder.findClientList(rrclass)->find(Name("ns.test2.example")).
finder_->find(Name("ns.test2.example"), RRType::AAAA())->code);
}
void
installDataSrcClientLists(AuthSrv& server, ClientListMapPtr lists) {
server.getDataSrcClientsMgr().setDataSrcClientLists(lists);
}
void
configureZones(AuthSrv& server) {
ASSERT_EQ(0, system(INSTALL_PROG " -c " TEST_DATA_DIR "/test1.zone.in "
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"));
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"
"}]}"));
installDataSrcClientLists(server, configureDataSource(config));
zoneChecks(server);
}
void
newZoneChecks(AuthSrv& server) {
const RRClass rrclass(RRClass::IN());
DataSrcClientsMgr::Holder holder(server.getDataSrcClientsMgr());
EXPECT_EQ(ZoneFinder::SUCCESS, holder.findClientList(rrclass)->
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, holder.findClientList(rrclass)->
find(Name("ns.test1.example")).finder_->
find(Name("ns.test1.example"), RRType::AAAA())->code);
// test2.example shouldn't change
EXPECT_EQ(ZoneFinder::SUCCESS, holder.findClientList(rrclass)->
find(Name("ns.test2.example")).finder_->
find(Name("ns.test2.example"), RRType::A())->code);
EXPECT_EQ(ZoneFinder::NXRRSET,
holder.findClientList(rrclass)->
find(Name("ns.test2.example")).finder_->
find(Name("ns.test2.example"), RRType::AAAA())->code);
}
TEST_F(AuthCommandTest, loadZone) {
configureZones(server_);
ASSERT_EQ(0, system(INSTALL_PROG " -c " TEST_DATA_DIR
"/test1-new.zone.in "
TEST_DATA_BUILDDIR "/test1.zone.copied"));
ASSERT_EQ(0, system(INSTALL_PROG " -c " TEST_DATA_DIR
"/test2-new.zone.in "
TEST_DATA_BUILDDIR "/test2.zone.copied"));
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON(
"{\"origin\": \"test1.example\"}"));
checkAnswer(0);
newZoneChecks(server_);
}
TEST_F(AuthCommandTest,
#ifdef USE_STATIC_LINK
DISABLED_loadZoneSQLite3
#else
loadZoneSQLite3
#endif
)
{
// 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);
// This describes the data source in the configuration
const ConstElementPtr config(Element::fromJSON("{"
"\"IN\": [{"
" \"type\": \"sqlite3\","
" \"params\": {\"database_file\": \"" + test_db + "\"},"
" \"cache-enable\": true,"
" \"cache-zones\": [\"example.org\"]"
"}]}"));
installDataSrcClientLists(server_, configureDataSource(config));
{
DataSrcClientsMgr::Holder holder(server_.getDataSrcClientsMgr());
// Check that the A record at www.example.org does not exist
EXPECT_EQ(ZoneFinder::NXDOMAIN,
holder.findClientList(RRClass::IN())->
find(Name("example.org")).finder_->
find(Name("www.example.org"), RRType::A())->code);
// Add the record to the underlying sqlite database, by loading
// it as a separate datasource, and updating it
ConstElementPtr sql_cfg = Element::fromJSON("{ \"type\": \"sqlite3\","
"\"database_file\": \""
+ test_db + "\"}");
DataSourceClientContainer sql_ds("sqlite3", sql_cfg);
ZoneUpdaterPtr sql_updater =
sql_ds.getInstance().getUpdater(Name("example.org"), false);
RRsetPtr rrset(new RRset(Name("www.example.org."), RRClass::IN(),
RRType::A(), RRTTL(60)));
rrset->addRdata(rdata::createRdata(rrset->getType(),
rrset->getClass(),
"192.0.2.1"));
sql_updater->addRRset(*rrset);
sql_updater->commit();
EXPECT_EQ(ZoneFinder::NXDOMAIN,
holder.findClientList(RRClass::IN())->
find(Name("example.org")).finder_->
find(Name("www.example.org"), RRType::A())->code);
}
// Now send the command to reload it
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON(
"{\"origin\": \"example.org\"}"));
checkAnswer(0, "Successful load");
{
DataSrcClientsMgr::Holder holder(server_.getDataSrcClientsMgr());
// And now it should be present too.
EXPECT_EQ(ZoneFinder::SUCCESS,
holder.findClientList(RRClass::IN())->
find(Name("example.org")).finder_->
find(Name("www.example.org"), RRType::A())->code);
}
// Some error cases. First, the zone has no configuration. (note .com here)
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON("{\"origin\": \"example.com\"}"));
checkAnswer(1, "example.com");
{
DataSrcClientsMgr::Holder holder(server_.getDataSrcClientsMgr());
// The previous zone is not hurt in any way
EXPECT_EQ(ZoneFinder::SUCCESS,
holder.findClientList(RRClass::IN())->
find(Name("example.org")).finder_->
find(Name("example.org"), RRType::SOA())->code);
}
const ConstElementPtr config2(Element::fromJSON("{"
"\"IN\": [{"
" \"type\": \"sqlite3\","
" \"params\": {\"database_file\": \"" + bad_db + "\"},"
" \"cache-enable\": true,"
" \"cache-zones\": [\"example.com\"]"
"}]}"));
EXPECT_THROW(configureDataSource(config2),
ConfigurableClientList::ConfigurationError);
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON("{\"origin\": \"example.com\"}"));
checkAnswer(1, "Unreadable");
DataSrcClientsMgr::Holder holder(server_.getDataSrcClientsMgr());
// The previous zone is not hurt in any way
EXPECT_EQ(ZoneFinder::SUCCESS,
holder.findClientList(RRClass::IN())->
find(Name("example.org")).finder_->
find(Name("example.org"), RRType::SOA())->code);
}
TEST_F(AuthCommandTest, loadBrokenZone) {
configureZones(server_);
ASSERT_EQ(0, system(INSTALL_PROG " -c " TEST_DATA_DIR
"/test1-broken.zone.in "
TEST_DATA_BUILDDIR "/test1.zone.copied"));
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON(
"{\"origin\": \"test1.example\"}"));
checkAnswer(1);
zoneChecks(server_); // zone shouldn't be replaced
}
TEST_F(AuthCommandTest, loadUnreadableZone) {
configureZones(server_);
// install the zone file as unreadable
ASSERT_EQ(0, system(INSTALL_PROG " -c -m 000 " TEST_DATA_DIR
"/test1.zone.in "
TEST_DATA_BUILDDIR "/test1.zone.copied"));
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON(
"{\"origin\": \"test1.example\"}"));
checkAnswer(1);
zoneChecks(server_); // zone shouldn't be replaced
}
TEST_F(AuthCommandTest, loadZoneWithoutDataSrc) {
// try to execute load command without configuring the zone beforehand.
// it should fail.
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON(
"{\"origin\": \"test1.example\"}"));
checkAnswer(1);
}
TEST_F(AuthCommandTest, loadZoneInvalidParams) {
configureZones(server_);
// null arg
result_ = execAuthServerCommand(server_, "loadzone", ElementPtr());
checkAnswer(1, "Null arg");
// zone class is bogus
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON(
"{\"origin\": \"test1.example\","
" \"class\": \"no_such_class\"}"));
checkAnswer(1, "No such class");
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON(
"{\"origin\": \"test1.example\","
" \"class\": 1}"));
checkAnswer(1, "Integral class");
// origin is missing
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON("{}"));
checkAnswer(1, "Missing origin");
// zone doesn't exist in the data source
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON("{\"origin\": \"xx\"}"));
checkAnswer(1, "No such zone");
// origin is bogus
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON(
"{\"origin\": \"...\"}"));
checkAnswer(1, "Wrong name");
result_ = execAuthServerCommand(server_, "loadzone",
Element::fromJSON("{\"origin\": 10}"));
checkAnswer(1, "Integral name");
}
TEST_F(AuthCommandTest, getStats) {
result_ = execAuthServerCommand(server_, "getstats", ConstElementPtr());
parseAnswer(rcode_, result_);
......
......@@ -486,12 +486,6 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneInvalidParams) {
// class or origin is missing: result in assertion failure
if (!isc::util::unittests::runningOnValgrind()) {
EXPECT_DEATH_IF_SUPPORTED({
builder.handleCommand(
Command(LOADZONE,
Element::fromJSON(
"{\"origin\": \"test1.example\"}")));
}, "");
EXPECT_DEATH_IF_SUPPORTED({
builder.handleCommand(Command(LOADZONE,
Element::fromJSON(
......
......@@ -196,6 +196,55 @@ TEST(DataSrcClientsMgrTest, holder) {
EXPECT_THROW(TestDataSrcClientsMgr::Holder holder2(mgr), isc::Unexpected);
}
TEST(DataSrcClientsMgrTest, reload) {
TestDataSrcClientsMgr mgr;
EXPECT_TRUE(FakeDataSrcClientsBuilder::started);
EXPECT_TRUE(FakeDataSrcClientsBuilder::command_queue->empty());
isc::data::ElementPtr args =
isc::data::Element::fromJSON("{ \"class\": \"IN\","
" \"origin\": \"example.com\" }");
mgr.loadZone(args);
EXPECT_EQ(1, FakeDataSrcClientsBuilder::command_queue->size());
mgr.loadZone(args);
EXPECT_EQ(2, FakeDataSrcClientsBuilder::command_queue->size());
// Should fail with non-string 'class' value
args->set("class", Element::create(1));
EXPECT_THROW(mgr.loadZone(args), CommandError);
EXPECT_EQ(2, FakeDataSrcClientsBuilder::command_queue->size());
// And with badclass
args->set("class", Element::create("BADCLASS"));
EXPECT_THROW(mgr.loadZone(args), CommandError);
EXPECT_EQ(2, FakeDataSrcClientsBuilder::command_queue->size());
// Should succeed without 'class'
args->remove("class");
mgr.loadZone(args);
EXPECT_EQ(3, FakeDataSrcClientsBuilder::command_queue->size());
// but fail without origin, without sending new commands
args->remove("origin");
EXPECT_THROW(mgr.loadZone(args), CommandError);
EXPECT_EQ(3, FakeDataSrcClientsBuilder::command_queue->size());
// And for 'origin' that is not a string
args->set("origin", Element::create(1));
EXPECT_THROW(mgr.loadZone(args), CommandError);
EXPECT_EQ(3, FakeDataSrcClientsBuilder::command_queue->size());
// And origin that is not a correct name
args->set("origin", Element::create(".."));
EXPECT_THROW(mgr.loadZone(args), CommandError);
EXPECT_EQ(3, FakeDataSrcClientsBuilder::command_queue->size());
// same for empty data and data that is not a map
EXPECT_THROW(mgr.loadZone(isc::data::ConstElementPtr()), CommandError);
EXPECT_THROW(mgr.loadZone(isc::data::Element::createList()), CommandError);
EXPECT_EQ(3, FakeDataSrcClientsBuilder::command_queue->size());
}
TEST(DataSrcClientsMgrTest, realThread) {
// Using the non-test definition with a real thread. Just checking
// no disruption happens.
......
......@@ -39,7 +39,7 @@ Feature: DDNS System
# Test 5
When I use DDNS to set the SOA serial to 1237
# also check if Auth server reloaded
And wait for new bind10 stderr message AUTH_LOAD_ZONE
And wait for new bind10 stderr message AUTH_DATASRC_CLIENTS_BUILDER_LOAD_ZONE
The DDNS response should be SUCCESS
And the SOA serial for example.org should be 1237
......@@ -53,12 +53,12 @@ Feature: DDNS System
# Test 8
When I use DDNS to set the SOA serial to 1238
And wait for new bind10 stderr message AUTH_LOAD_ZONE
And wait for new bind10 stderr message AUTH_DATASRC_CLIENTS_BUILDER_LOAD_ZONE
The DDNS response should be SUCCESS
And the SOA serial for example.org should be 1238
When I use DDNS to set the SOA serial to 1239
And wait for new bind10 stderr message AUTH_LOAD_ZONE
And wait for new bind10 stderr message AUTH_DATASRC_CLIENTS_BUILDER_LOAD_ZONE
The DDNS response should be SUCCESS
And the SOA serial for example.org should be 1239
......@@ -70,7 +70,7 @@ Feature: DDNS System
# Test 10
When I use DDNS to set the SOA serial to 1240
And wait for new bind10 stderr message AUTH_LOAD_ZONE
And wait for new bind10 stderr message AUTH_DATASRC_CLIENTS_BUILDER_LOAD_ZONE