2
0
mirror of https://gitlab.isc.org/isc-projects/kea synced 2025-08-30 05:27:55 +00:00

Merge branch 'master' of ssh://bind10.isc.org/var/bind10/git/bind10

This commit is contained in:
chenzhengzhang 2011-04-19 22:21:15 +08:00
commit 712d1db4df
14 changed files with 510 additions and 96 deletions

View File

@ -612,6 +612,7 @@ AC_CONFIG_FILES([Makefile
src/bin/bindctl/Makefile
src/bin/bindctl/tests/Makefile
src/bin/cfgmgr/Makefile
src/bin/cfgmgr/plugins/Makefile
src/bin/cfgmgr/tests/Makefile
src/bin/host/Makefile
src/bin/loadzone/Makefile

View File

@ -1,4 +1,4 @@
SUBDIRS = . tests
SUBDIRS = . plugins tests
pkglibexecdir = $(libexecdir)/@PACKAGE@

View File

@ -23,6 +23,8 @@ import isc.util.process
import signal
import os
from optparse import OptionParser
import glob
import os.path
isc.util.process.rename()
@ -39,9 +41,11 @@ if "B10_FROM_SOURCE" in os.environ:
DATA_PATH = os.environ["B10_FROM_SOURCE_LOCALSTATEDIR"]
else:
DATA_PATH = os.environ["B10_FROM_SOURCE"]
PLUGIN_PATH = [DATA_PATH + '/src/bin/cfgmgr/plugins']
else:
PREFIX = "@prefix@"
DATA_PATH = "@localstatedir@/@PACKAGE@".replace("${prefix}", PREFIX)
PLUGIN_PATHS = ["@prefix@/share/@PACKAGE@/config_plugins"]
DEFAULT_CONFIG_FILE = "b10-config.db"
cm = None
@ -65,6 +69,28 @@ def signal_handler(signal, frame):
if cm:
cm.running = False
def load_plugins(path, cm):
"""Load all python files in the given path and treat them as plugins."""
# Find the python files
plugins = glob.glob(path + os.sep + '*.py')
# Search this directory first, but leave the others there for the imports
# of the modules
sys.path.insert(0, path)
try:
for plugin in plugins:
# Generate the name of the plugin
filename = os.path.basename(plugin)
name = filename[:-3]
# Load it
module = __import__(name)
# Ask it to provide the spec and checking function
(spec, check_func) = module.load()
# And insert it into the manager
cm.set_virtual_module(spec, check_func)
finally:
# Restore the search path
sys.path = sys.path[1:]
def main():
options = parse_options()
global cm
@ -73,6 +99,8 @@ def main():
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
cm.read_config()
for ppath in PLUGIN_PATHS:
load_plugins(ppath, cm)
cm.notify_boss()
cm.run()
except SessionError as se:

View File

@ -0,0 +1 @@
EXTRA_DIST = README

View File

@ -0,0 +1,34 @@
How to write a configuration plugin
===================================
The plugins are used to describe configuration modules that have no hosting
process. Therefore there's no process to provide their specification and to
check them for correctness. So the plugin takes this responsibility.
Each plugin is a python file installed into the
`@prefix@/share/@PACKAGE@/config_plugins` directory (usually
`/usr/share/bind10/config_plugins`). It is loaded automatically at startup.
The entrypoint of a plugin is function called `load()`. It should return a
tuple, first value should be the module specification (usually instance of
`isc.config.module_spec.ModuleSpec`, loaded by `module_spec_from_file()`).
The second value is a callable object. It will be called whenever the
configuration of module needs to be checked. The only parameter will be the new
config (as python dictionary). To accept the new configuration, return None. If
you return a string, it is taken as an error message. If you raise an
exception, the config is rejected as well, however it is not considered a
graceful rejection, but a failure of the module.
So, this is how a plugin could look like:
from isc.config.module_spec import module_spec_from_file
def check(config):
if config['bogosity'] > 1:
return "Too bogus to live with"
else:
return None
def load():
return (module_spec_from_file('module_spec.spec'), check)

View File

@ -1,7 +1,7 @@
PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
PYTESTS = b10-cfgmgr_test.py
EXTRA_DIST = $(PYTESTS)
EXTRA_DIST = $(PYTESTS) testdata/plugins/testplugin.py
# test using command-line arguments, so use check-local target instead of TESTS
check-local:
@ -12,6 +12,7 @@ if ENABLE_PYTHON_COVERAGE
endif
for pytest in $(PYTESTS) ; do \
echo Running test: $$pytest ; \
env TESTDATA_PATH=$(abs_srcdir)/testdata \
env PYTHONPATH=$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/bin/cfgmgr \
$(PYCOVERAGE_RUN) $(abs_builddir)/$$pytest || exit ; \
done

View File

@ -30,6 +30,7 @@ class MyConfigManager:
self.run_called = False
self.write_config_called = False
self.running = True
self.virtual_modules = []
def read_config(self):
self.read_config_called = True
@ -43,6 +44,24 @@ class MyConfigManager:
def write_config(self):
self.write_config_called = True
def set_virtual_module(self, spec, function):
self.virtual_modules.append((spec, function))
class TestPlugins(unittest.TestCase):
def test_load_plugins(self):
"""Test we can successfully find and load the mock plugin."""
# Let it load the plugin
b = __import__("b10-cfgmgr")
# The parameters aren't important for this test
cm = MyConfigManager(None, None)
b.load_plugins(os.environ['TESTDATA_PATH'] + os.sep + 'plugins', cm)
# Check exactly one plugin was loaded and the right data were fed into
# the cm
self.assertEqual(len(cm.virtual_modules), 1)
(spec, check) = cm.virtual_modules[0]
self.assertEqual(spec.get_module_name(), "mock_config_plugin")
self.assertEqual(check(None), "Mock config plugin")
class TestConfigManagerStartup(unittest.TestCase):
def test_cfgmgr(self):
# some creative module use;
@ -50,13 +69,24 @@ class TestConfigManagerStartup(unittest.TestCase):
# this also gives us the chance to override the imported
# module ConfigManager in it.
b = __import__("b10-cfgmgr")
orig_load = b.load_plugins
b.PLUGIN_PATHS = ["/plugin/path"]
self.loaded_plugins = False
def load_plugins(path, cm):
# Check it's called with proper arguments
self.assertEqual(path, "/plugin/path")
self.assertTrue(isinstance(cm, MyConfigManager))
self.loaded_plugins = True
b.load_plugins = load_plugins
b.ConfigManager = MyConfigManager
b.main()
b.load_plugins = orig_load
self.assertTrue(b.cm.read_config_called)
self.assertTrue(b.cm.notify_boss_called)
self.assertTrue(b.cm.run_called)
self.assertTrue(self.loaded_plugins)
# if there are no changes, config is not written
self.assertFalse(b.cm.write_config_called)
@ -81,6 +111,7 @@ class TestConfigManagerStartup(unittest.TestCase):
os.environ["B10_FROM_SOURCE"] = tmp_env_var
b = __import__("b10-cfgmgr", globals(), locals())
b.PLUGIN_PATH = [] # It's enough to test plugins in one test
b.ConfigManager = MyConfigManager
self.assertEqual(tmp_env_var, b.DATA_PATH)

View File

@ -0,0 +1,34 @@
# Copyright (C) 2011 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# A test plugin. It does mostly nothing, just provides a function we can
# recognize from the test. However, it looks like a real plugin, with the
# (almost) correct interface, even when it's not used.
class MockSpec:
"""Mock spec, contains no data, it can only provide it's name.
It'll not really be used for anything."""
def get_module_name(self):
return "mock_config_plugin"
def mock_check_config(config):
"""Mock function to check config. Does nothing, only returns
an "error" string to indicate it's this one."""
return "Mock config plugin"
def load():
"""When a plugin is loaded, this is called to provide the spec and
checking function."""
return (MockSpec(), mock_check_config)

View File

@ -12,31 +12,32 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
/// \brief Nameserver Address Store Tests
///
/// This file contains tests for the nameserver address store as a whole.
#include <algorithm>
#include <cassert>
#include <string.h>
#include <vector>
#include <boost/foreach.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/shared_ptr.hpp>
#include <gtest/gtest.h>
#include <config.h>
/// \brief Test Deleter Objects
///
/// This file contains tests for the "deleter" classes within the nameserver
/// address store. These act to delete zones from the zone hash table when
/// the element reaches the top of the LRU list.
#include <dns/rrttl.h>
#include <dns/rdataclass.h>
#include <dns/rrclass.h>
#include <dns/rrset.h>
#include <dns/rrttl.h>
#include <gtest/gtest.h>
#include <boost/shared_ptr.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/foreach.hpp>
#include <string.h>
#include <cassert>
#include "../nameserver_address_store.h"
#include "../nsas_entry_compare.h"
#include "../nameserver_entry.h"
#include "../zone_entry.h"
#include "../address_request_callback.h"
#include "../nameserver_address_store.h"
#include "../nameserver_entry.h"
#include "../nsas_entry_compare.h"
#include "../zone_entry.h"
#include "nsas_test.h"
using namespace isc::dns;
@ -67,42 +68,61 @@ public:
virtual ~DerivedNsas()
{}
/// \brief Add Nameserver Entry to Hash and LRU Tables
/// \brief Add Nameserver Entry to hash and LRU tables
///
/// \param entry Nameserver Entry to add.
void AddNameserverEntry(boost::shared_ptr<NameserverEntry>& entry) {
HashKey h = entry->hashKey();
nameserver_hash_->add(entry, h);
nameserver_lru_->add(entry);
}
/// \brief Add Zone Entry to Hash and LRU Tables
/// \brief Add Zone Entry to hash and LRU tables
///
/// \param entry Zone Entry to add.
void AddZoneEntry(boost::shared_ptr<ZoneEntry>& entry) {
HashKey h = entry->hashKey();
zone_hash_->add(entry, h);
zone_lru_->add(entry);
}
/**
* \short Just wraps the common lookup
*
* It calls the lookup and provides the authority section
* if it is asked for by the resolver.
*/
/// \brief Wrap the common lookup
///
/// Calls the lookup and provides the authority section if it is asked
/// for by the resolver.
///
/// \param name Name of zone for which an address is required
/// \param class_code Class of the zone
/// \param authority Pointer to authority section RRset to which NS
/// records will be added.
/// \param callback Callback object used to pass result back to caller
void lookupAndAnswer(const string& name, const RRClass& class_code,
RRsetPtr authority,
boost::shared_ptr<AddressRequestCallback> callback)
{
// Note how many requests are in the resolver's queue
size_t size(resolver_->requests.size());
// Lookup the name. This should generate a request for NS records.
NameserverAddressStore::lookup(name, class_code, callback, ANY_OK);
// It asked something, the only thing it can ask is the NS list
if (size < resolver_->requests.size()) {
// It asked something, the only thing it can ask is the NS list.
// Once answered, drop the request so no-one else sees it
resolver_->provideNS(size, authority);
// Once answered, drop the request so noone else sees it
resolver_->requests.erase(resolver_->requests.begin() + size);
} else {
ADD_FAILURE() << "Not asked for NS";
// The test resolver's requests queue has not increased in size,
// so the lookup did not generate a request.
ADD_FAILURE() << "Lookup did not generate a request for NS records";
}
}
private:
boost::shared_ptr<TestResolver> resolver_;
///< Resolver used to answer generated requests
};
@ -111,6 +131,7 @@ private:
class NameserverAddressStoreTest : public TestWithRdata {
protected:
/// \brief Constructor
NameserverAddressStoreTest() :
authority_(new RRset(Name("example.net."), RRClass::IN(), RRType::NS(),
RRTTL(128))),
@ -118,8 +139,8 @@ protected:
RRType::NS(), RRTTL(128))),
resolver_(new TestResolver)
{
// Constructor - initialize a set of nameserver and zone objects. For
// convenience, these are stored in vectors.
// Initialize a set of nameserver and zone objects. For convenience,
// these are stored in vectors.
for (int i = 1; i <= 9; ++i) {
std::string name = "nameserver" + boost::lexical_cast<std::string>(
i);
@ -145,18 +166,16 @@ protected:
NSASCallback::results.clear();
}
// Vector of pointers to nameserver and zone entries.
std::vector<boost::shared_ptr<NameserverEntry> > nameservers_;
std::vector<boost::shared_ptr<ZoneEntry> > zones_;
RRsetPtr authority_, empty_authority_;
boost::shared_ptr<TestResolver> resolver_;
class NSASCallback : public AddressRequestCallback {
public:
/// \brief Internal callback object
///
/// Callback object. It just records whether the success() or
/// unreachable() methods were called and if success, a copy of the
/// Nameserver object. (The data is held in a static object that will
/// outlive the lifetime of the callback object.)
struct NSASCallback : public AddressRequestCallback {
typedef pair<bool, NameserverAddress> Result;
static vector<Result> results;
virtual void success(const NameserverAddress& address) {
results.push_back(Result(true, address));
}
@ -165,37 +184,52 @@ protected:
}
};
/// \brief Return pointer to callback object
boost::shared_ptr<AddressRequestCallback> getCallback() {
return (boost::shared_ptr<AddressRequestCallback>(new NSASCallback));
}
// Member variables
// Vector of pointers to nameserver and zone entries.
std::vector<boost::shared_ptr<NameserverEntry> > nameservers_;
std::vector<boost::shared_ptr<ZoneEntry> > zones_;
// Authority sections used in the tests
RRsetPtr authority_;
RRsetPtr empty_authority_;
// ... and the resolver
boost::shared_ptr<TestResolver> resolver_;
};
/// Definition of the static results object
vector<NameserverAddressStoreTest::NSASCallback::Result>
NameserverAddressStoreTest::NSASCallback::results;
/// \brief Remove Zone Entry from Hash Table
///
/// Check that when an entry reaches the top of the zone LRU list, it is removed from the
/// hash table as well.
/// Check that when an entry reaches the top of the zone LRU list, it is removed
/// from the hash table as well.
TEST_F(NameserverAddressStoreTest, ZoneDeletionCheck) {
// Create a NSAS with a hash size of three and a LRU size of 9 (both zone and
// nameserver tables).
// Create a NSAS with a hash size of three and a LRU size of 9 (both zone
// and nameserver tables).
DerivedNsas nsas(resolver_, 2, 2);
// Add six entries to the tables. After addition the reference count of each element
// should be 3 - one for the entry in the zones_ vector, and one each for the entries
// in the LRU list and hash table.
// Add six entries to the tables. After addition the reference count of
// each element should be 3 - one for the entry in the zones_ vector, and
// one each for the entries in the LRU list and hash table.
for (int i = 1; i <= 6; ++i) {
EXPECT_EQ(1, zones_[i].use_count());
nsas.AddZoneEntry(zones_[i]);
EXPECT_EQ(3, zones_[i].use_count());
}
// Adding another entry should cause the first one to drop off the LRU list, which
// should also trigger the deletion of the entry from the hash table. This should
// reduce its use count to 1.
// Adding another entry should cause the first one to drop off the LRU list,
// which should also trigger the deletion of the entry from the hash table.
// This should reduce its use count to 1.
EXPECT_EQ(1, zones_[7].use_count());
nsas.AddZoneEntry(zones_[7]);
EXPECT_EQ(3, zones_[7].use_count());
@ -206,26 +240,26 @@ TEST_F(NameserverAddressStoreTest, ZoneDeletionCheck) {
/// \brief Remove Entry from Hash Table
///
/// Check that when an entry reaches the top of the LRU list, it is removed from the
/// hash table as well.
/// Check that when an entry reaches the top of the LRU list, it is removed from
/// the hash table as well.
TEST_F(NameserverAddressStoreTest, NameserverDeletionCheck) {
// Create a NSAS with a hash size of three and a LRU size of 9 (both zone and
// nameserver tables).
// Create a NSAS with a hash size of three and a LRU size of 9 (both zone
// and nameserver tables).
DerivedNsas nsas(resolver_, 2, 2);
// Add six entries to the tables. After addition the reference count of each element
// should be 3 - one for the entry in the nameservers_ vector, and one each for the entries
// in the LRU list and hash table.
// Add six entries to the tables. After addition the reference count of
// each element should be 3 - one for the entry in the nameservers_ vector,
// and one each for the entries in the LRU list and hash table.
for (int i = 1; i <= 6; ++i) {
EXPECT_EQ(1, nameservers_[i].use_count());
nsas.AddNameserverEntry(nameservers_[i]);
EXPECT_EQ(3, nameservers_[i].use_count());
}
// Adding another entry should cause the first one to drop off the LRU list, which
// should also trigger the deletion of the entry from the hash table. This should
// reduce its use count to 1.
// Adding another entry should cause the first one to drop off the LRU list,
// which should also trigger the deletion of the entry from the hash table.
// This should reduce its use count to 1.
EXPECT_EQ(1, nameservers_[7].use_count());
nsas.AddNameserverEntry(nameservers_[7]);
EXPECT_EQ(3, nameservers_[7].use_count());
@ -238,14 +272,17 @@ TEST_F(NameserverAddressStoreTest, NameserverDeletionCheck) {
/// Check if it asks correct questions and it keeps correct internal state.
TEST_F(NameserverAddressStoreTest, emptyLookup) {
DerivedNsas nsas(resolver_, 10, 10);
// Ask it a question
nsas.lookupAndAnswer("example.net.", RRClass::IN(), authority_,
getCallback());
// It should ask for IP addresses for ns.example.com.
EXPECT_NO_THROW(resolver_->asksIPs(Name("ns.example.com."), 0, 1));
// Ask another question for the same zone
nsas.lookup("example.net.", RRClass::IN(), getCallback());
// It should ask no more questions now
EXPECT_EQ(2, resolver_->requests.size());
@ -253,6 +290,7 @@ TEST_F(NameserverAddressStoreTest, emptyLookup) {
authority_->setName(Name("example.com."));
nsas.lookupAndAnswer("example.com.", RRClass::IN(), authority_,
getCallback());
// It still should ask nothing
EXPECT_EQ(2, resolver_->requests.size());
@ -272,11 +310,14 @@ TEST_F(NameserverAddressStoreTest, emptyLookup) {
/// It should not ask anything and say it is unreachable right away.
TEST_F(NameserverAddressStoreTest, zoneWithoutNameservers) {
DerivedNsas nsas(resolver_, 10, 10);
// Ask it a question
nsas.lookupAndAnswer("example.net.", RRClass::IN(), empty_authority_,
getCallback());
// There should be no questions, because there's nothing to ask
EXPECT_EQ(0, resolver_->requests.size());
// And there should be one "unreachable" answer for the query
ASSERT_EQ(1, NSASCallback::results.size());
EXPECT_FALSE(NSASCallback::results[0].first);
@ -289,9 +330,11 @@ TEST_F(NameserverAddressStoreTest, zoneWithoutNameservers) {
/// without further asking.
TEST_F(NameserverAddressStoreTest, unreachableNS) {
DerivedNsas nsas(resolver_, 10, 10);
// Ask it a question
nsas.lookupAndAnswer("example.net.", RRClass::IN(), authority_,
getCallback());
// It should ask for IP addresses for example.com.
EXPECT_NO_THROW(resolver_->asksIPs(Name("ns.example.com."), 0, 1));
@ -299,6 +342,7 @@ TEST_F(NameserverAddressStoreTest, unreachableNS) {
authority_->setName(Name("example.com."));
nsas.lookupAndAnswer("example.com.", RRClass::IN(), authority_,
getCallback());
// It should ask nothing more now
EXPECT_EQ(2, resolver_->requests.size());
@ -308,12 +352,14 @@ TEST_F(NameserverAddressStoreTest, unreachableNS) {
// We should have 2 answers now
EXPECT_EQ(2, NSASCallback::results.size());
// When we ask one same and one other zone with the same nameserver,
// it should generate no questions and answer right away
nsas.lookup("example.net.", RRClass::IN(), getCallback());
authority_->setName(Name("example.org."));
nsas.lookupAndAnswer("example.org.", RRClass::IN(), authority_,
getCallback());
// There should be 4 negative answers now
EXPECT_EQ(4, NSASCallback::results.size());
BOOST_FOREACH(const NSASCallback::Result& result, NSASCallback::results) {
@ -326,18 +372,23 @@ TEST_F(NameserverAddressStoreTest, unreachableNS) {
/// Does some asking, on a set of zones that share some nameservers, with
/// slower answering, evicting data, etc.
TEST_F(NameserverAddressStoreTest, CombinedTest) {
// Create small caches, so we get some evictions
DerivedNsas nsas(resolver_, 1, 1);
// Ask for example.net. It has single nameserver out of the zone
nsas.lookupAndAnswer("example.net.", RRClass::IN(), authority_,
getCallback());
// It should ask for the nameserver IP addresses
EXPECT_NO_THROW(resolver_->asksIPs(Name("ns.example.com."), 0, 1));
EXPECT_EQ(0, NSASCallback::results.size());
// But we do not answer it right away. We create a new zone and
// let this nameserver entry get out.
rrns_->addRdata(rdata::generic::NS("example.cz"));
nsas.lookupAndAnswer(EXAMPLE_CO_UK, RRClass::IN(), rrns_, getCallback());
// It really should ask something, one of the nameservers
// (or both)
ASSERT_GT(resolver_->requests.size(), 2);
@ -347,8 +398,8 @@ TEST_F(NameserverAddressStoreTest, CombinedTest) {
EXPECT_NO_THROW(resolver_->asksIPs(name, 2, 3));
EXPECT_EQ(0, NSASCallback::results.size());
size_t request_count(resolver_->requests.size());
// This should still be in the hash table, so try it asks no more questions
size_t request_count(resolver_->requests.size());
nsas.lookup("example.net.", RRClass::IN(), getCallback());
EXPECT_EQ(request_count, resolver_->requests.size());
EXPECT_EQ(0, NSASCallback::results.size());
@ -356,6 +407,7 @@ TEST_F(NameserverAddressStoreTest, CombinedTest) {
// We respond to one of the 3 nameservers
EXPECT_NO_THROW(resolver_->answer(2, name, RRType::A(),
rdata::in::A("192.0.2.1")));
// That should trigger one answer
EXPECT_EQ(1, NSASCallback::results.size());
EXPECT_TRUE(NSASCallback::results[0].first);
@ -363,6 +415,7 @@ TEST_F(NameserverAddressStoreTest, CombinedTest) {
NSASCallback::results[0].second.getAddress().toText());
EXPECT_NO_THROW(resolver_->answer(3, name, RRType::AAAA(),
rdata::in::AAAA("2001:bd8::1")));
// And there should be yet another query
ASSERT_GT(resolver_->requests.size(), 4);
EXPECT_NE(name, resolver_->requests[4].first->getName());
@ -380,9 +433,9 @@ TEST_F(NameserverAddressStoreTest, CombinedTest) {
EXPECT_EQ(request_count + 2, resolver_->requests.size());
EXPECT_NO_THROW(resolver_->asksIPs(Name("ns.example.com."), request_count,
request_count + 1));
// Now, we answer both queries for the same address
// and three (one for the original, one for this one) more answers should
// arrive
// Now, we answer both queries for the same address and three (one for the
// original, one for this one) more answers should arrive
NSASCallback::results.clear();
EXPECT_NO_THROW(resolver_->answer(0, Name("ns.example.com."), RRType::A(),
rdata::in::A("192.0.2.2")));
@ -395,5 +448,113 @@ TEST_F(NameserverAddressStoreTest, CombinedTest) {
}
}
// Check that we can update the RTT associated with nameservers successfully.
// Also checks that we can't set the RTT to zero (which would cause problems
// with selection algorithm).
TEST_F(NameserverAddressStoreTest, updateRTT) {
// Initialization.
string zone_name = "example.net.";
string ns_name = "ns.example.com.";
vector<string> address;
address.push_back("192.0.2.1");
address.push_back("192.0.2.2");
uint32_t HIGH_RTT = 10000; // 1E4; When squared, the result fits in 32 bits
DerivedNsas nsas(resolver_, 103, 107); // Arbitrary cache sizes
// Ensure that location holding the addresses returned is empty. We'll
// be using this throughout the tests. As the full name is a bit of a
// mouthful, set up an alias.
vector<NameserverAddressStoreTest::NSASCallback::Result>& results =
NameserverAddressStoreTest::NSASCallback::results;
results.clear();
// Initialize the test resolver with the answer for the A record query
// for ns.example.com (the nameserver set for example.net in the class
// initialization). We'll set two addresses.
Name ns_example_com(ns_name);
RRsetPtr ns_address = boost::shared_ptr<RRset>(new RRset(
ns_example_com, RRClass::IN(), RRType::A(), RRTTL(300)));
BOOST_FOREACH(string addr, address) {
ns_address->addRdata(rdata::in::A(addr));
}
// All set. Ask for example.net.
boost::shared_ptr<AddressRequestCallback> callback = getCallback();
nsas.lookupAndAnswer(zone_name, RRClass::IN(), authority_, getCallback());
// This should generate two requests - one for A and one for AAAA.
EXPECT_EQ(2, resolver_->requests.size());
// Provide an answer that has two A records. This should generate one
// result.
EXPECT_NO_THROW(resolver_->answer(0, ns_address));
EXPECT_EQ(1, results.size());
// We expect the lookup to be successful. Check that the address is one of
// the two we've set and that the RTT associated with this nameserver is
// non-zero.
EXPECT_EQ(true, results[0].first);
vector<string>::iterator addr1 = find(address.begin(), address.end(),
results[0].second.getAddress().toText());
EXPECT_TRUE(addr1 != address.end());
// The RTT we got should be non-zero and less than the high value we are
// using for the test.
uint32_t rtt1 = results[0].second.getAddressEntry().getRTT();
EXPECT_NE(0, rtt1);
EXPECT_LT(rtt1, HIGH_RTT);
// Update the address with a very high RTT. Owning to the way the NSAS is
// written, we can update the RTT but cannot read the new value back from
// the new object.
results[0].second.updateRTT(HIGH_RTT);
// Get another nameserver. As the probability of returning a particular
// address is proporational to 1/t^2, we should get the second address
// since the first now has a larger RTT. However, this is not guaranteed
// - this is a probability (albeit small) of getting the first again. We'll
// allow three chances of getting the "wrong" address before we declare
// an error.
int attempt = 0;
vector<string>::iterator addr2 = addr1;
for (attempt = 0; (attempt < 3) && (*addr1 == *addr2); ++attempt) {
results.clear();
nsas.lookup(zone_name, RRClass::IN(),
getCallback(), ANY_OK);
addr2 = find(address.begin(), address.end(),
results[0].second.getAddress().toText());
}
EXPECT_LT(attempt, 3);
// Ensure that the RTT is non-zero.
// obtained earlier.
uint32_t rtt2 = results[0].second.getAddressEntry().getRTT();
EXPECT_NE(0, rtt2);
// The test has shown that the code can return the two nameservers. Now
// try to set the RTT for the last one returned to zero. As there is a
// smoothing effect in the calculations which damps out an abrupt change
// in the RTT, the underlying RTT will not be set to zero immediately. So
// loop a large number of times, each time setting it to zero.
//
// Between each setting of the RTT, we have to retrieve the nameserver from
// the NSAS again. This means that we _could_ occasionally get the address
// of the one whose RTT we have raised to HIGH_RTT. We overcome this by
// looping a _very_ large number of times. Ultimately the RTT of both
// addresses should decay to a small value.
for (int i = 0; i < 2000; ++i) { // 1000 times for each nameserver
results.clear();
nsas.lookup(zone_name, RRClass::IN(), getCallback(), ANY_OK);
EXPECT_EQ(1, results.size());
uint32_t rtt3 = results[0].second.getAddressEntry().getRTT();
EXPECT_NE(0, rtt3);
results[0].second.updateRTT(0);
}
}
} // namespace nsas
} // namespace isc

View File

@ -513,6 +513,11 @@ TEST_F(NameserverEntryTest, UpdateRTT) {
// The rtt should be close to stable rtt value
EXPECT_TRUE((stable_rtt - new_rtt) < (new_rtt - init_rtt));
// Finally, try updating the RTT to a very large value (large enough for
// RTT^2 - used in the internal calculation - to exceed a 32-bit value).
EXPECT_NO_THROW(vec[0].updateRTT(1000000000)); // 10^9
}
} // namespace

View File

@ -321,20 +321,26 @@ class TestResolver : public isc::resolve::ResolverInterface {
/*
* Sends a simple answer to a query.
* Provide index of a query and the address to pass.
* 1) Provide index of a query and the address(es) to pass.
* 2) Provide index of query and components of address to pass.
*/
void answer(size_t index, const Name& name, const RRType& type,
const rdata::Rdata& rdata, size_t TTL = 100)
{
void answer(size_t index, RRsetPtr& set) {
if (index >= requests.size()) {
throw NoSuchRequest();
}
requests[index].second->success(createResponseMessage(set));
}
void answer(size_t index, const Name& name, const RRType& type,
const rdata::Rdata& rdata, size_t TTL = 100)
{
RRsetPtr set(new RRset(name, RRClass::IN(),
type, RRTTL(TTL)));
set->addRdata(rdata);
requests[index].second->success(createResponseMessage(set));
answer(index, set);
}
void provideNS(size_t index,
RRsetPtr nameservers)
{

View File

@ -170,6 +170,10 @@ class ConfigManager:
self.data_path = data_path
self.database_filename = database_filename
self.module_specs = {}
# Virtual modules are the ones which have no process running. The
# checking of validity is done by functions presented here instead
# of some other process
self.virtual_modules = {}
self.config = ConfigManagerData(data_path, database_filename)
if session:
self.cc = session
@ -187,11 +191,20 @@ class ConfigManager:
"""Adds a ModuleSpec"""
self.module_specs[spec.get_module_name()] = spec
def set_virtual_module(self, spec, check_func):
"""Adds a virtual module with its spec and checking function."""
self.module_specs[spec.get_module_name()] = spec
self.virtual_modules[spec.get_module_name()] = check_func
def remove_module_spec(self, module_name):
"""Removes the full ModuleSpec for the given module_name.
Also removes the virtual module check function if it
was present.
Does nothing if the module was not present."""
if module_name in self.module_specs:
del self.module_specs[module_name]
if module_name in self.virtual_modules:
del self.virtual_modules[module_name]
def get_module_spec(self, module_name = None):
"""Returns the full ModuleSpec for the module with the given
@ -299,24 +312,48 @@ class ConfigManager:
# todo: use api (and check the data against the definition?)
old_data = copy.deepcopy(self.config.data)
conf_part = data.find_no_exc(self.config.data, module_name)
update_cmd = None
use_part = None
if conf_part:
data.merge(conf_part, cmd)
update_cmd = ccsession.create_command(ccsession.COMMAND_CONFIG_UPDATE,
conf_part)
seq = self.cc.group_sendmsg(update_cmd, module_name)
try:
answer, env = self.cc.group_recvmsg(False, seq)
except isc.cc.SessionTimeout:
answer = ccsession.create_answer(1, "Timeout waiting for answer from " + module_name)
use_part = conf_part
else:
conf_part = data.set(self.config.data, module_name, {})
data.merge(conf_part[module_name], cmd)
# send out changed info
use_part = conf_part[module_name]
# The command to send
update_cmd = ccsession.create_command(ccsession.COMMAND_CONFIG_UPDATE,
conf_part[module_name])
seq = self.cc.group_sendmsg(update_cmd, module_name)
# replace 'our' answer with that of the module
use_part)
# TODO: This design might need some revisiting. We might want some
# polymorphism instead of branching. But it just might turn out it
# will get solved by itself when we move everything to virtual modules
# (which is possible solution to the offline configuration problem)
# or when we solve the incorect behaviour here when a config is
# rejected (spying modules don't know it was rejected and some modules
# might have been commited already).
if module_name in self.virtual_modules:
# The module is virtual, so call it to get the answer
try:
error = self.virtual_modules[module_name](use_part)
if error is None:
answer = ccsession.create_answer(0)
# OK, it is successful, send the notify, but don't wait
# for answer
seq = self.cc.group_sendmsg(update_cmd, module_name)
else:
answer = ccsession.create_answer(1, error)
# Make sure just a validating plugin don't kill the whole manager
except Exception as excp:
# Provide answer
answer = ccsession.create_answer(1, "Exception: " + str(excp))
else:
# Real module, send it over the wire to it
# send out changed info and wait for answer
seq = self.cc.group_sendmsg(update_cmd, module_name)
try:
# replace 'our' answer with that of the module
answer, env = self.cc.group_recvmsg(False, seq)
except isc.cc.SessionTimeout:
answer = ccsession.create_answer(1, "Timeout waiting for answer from " + module_name)

View File

@ -135,6 +135,8 @@ class TestConfigManager(unittest.TestCase):
self.assert_(module_spec.get_module_name() not in self.cm.module_specs)
self.cm.set_module_spec(module_spec)
self.assert_(module_spec.get_module_name() in self.cm.module_specs)
self.assert_(module_spec.get_module_name() not in
self.cm.virtual_modules)
def test_remove_module_spec(self):
module_spec = isc.config.module_spec.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
@ -143,6 +145,30 @@ class TestConfigManager(unittest.TestCase):
self.assert_(module_spec.get_module_name() in self.cm.module_specs)
self.cm.remove_module_spec(module_spec.get_module_name())
self.assert_(module_spec.get_module_name() not in self.cm.module_specs)
self.assert_(module_spec.get_module_name() not in
self.cm.virtual_modules)
def test_add_remove_virtual_module(self):
module_spec = isc.config.module_spec.module_spec_from_file(
self.data_path + os.sep + "spec1.spec")
check_func = lambda: True
# Make sure it's not in there before
self.assert_(module_spec.get_module_name() not in self.cm.module_specs)
self.assert_(module_spec.get_module_name() not in
self.cm.virtual_modules)
# Add it there
self.cm.set_virtual_module(module_spec, check_func)
# Check it's in there
self.assert_(module_spec.get_module_name() in self.cm.module_specs)
self.assertEqual(self.cm.module_specs[module_spec.get_module_name()],
module_spec)
self.assertEqual(self.cm.virtual_modules[module_spec.get_module_name()],
check_func)
# Remove it again
self.cm.remove_module_spec(module_spec.get_module_name())
self.assert_(module_spec.get_module_name() not in self.cm.module_specs)
self.assert_(module_spec.get_module_name() not in
self.cm.virtual_modules)
def test_get_module_spec(self):
module_spec = isc.config.module_spec.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
@ -312,6 +338,51 @@ class TestConfigManager(unittest.TestCase):
},
{'result': [0]})
def test_set_config_virtual(self):
"""Test that if the module is virtual, we don't send it over the
message bus, but call the checking function.
"""
# We run the same three times, with different return values
def single_test(value, returnFunc, expectedResult):
# Because closures can't assign to closed-in variables, we pass
# it trough self
self.called_with = None
def check_test(new_data):
self.called_with = new_data
return returnFunc()
# Register our virtual module
self.cm.set_virtual_module(self.spec, check_test)
# The fake session will throw now if it tries to read a response.
# Handy, we don't need to find a complicated way to check for it.
result = self.cm._handle_set_config_module(self.spec.
get_module_name(),
{'item1': value})
# Check the correct result is passed and our function was called
# With correct data
self.assertEqual(self.called_with['item1'], value)
self.assertEqual(result, {'result': expectedResult})
if expectedResult[0] == 0:
# Check it provided the correct notification
self.assertEqual(len(self.fake_session.message_queue), 1)
self.assertEqual({'command': [ 'config_update',
{'item1': value}]},
self.fake_session.get_message('Spec2', None))
# and the queue should now be empty again
self.assertEqual(len(self.fake_session.message_queue), 0)
else:
# It shouldn't send anything on error
self.assertEqual(len(self.fake_session.message_queue), 0)
# Success
single_test(5, lambda: None, [0])
# Graceful error
single_test(6, lambda: "Just error", [1, "Just error"])
# Exception from the checker
def raiser():
raise Exception("Just exception")
single_test(7, raiser, [1, "Exception: Just exception"])
def test_set_config_all(self):
my_ok_answer = { 'result': [ 0 ] }

View File

@ -21,6 +21,10 @@
#include <config.h>
// on sunstudio, asio.hpp needs unistd.h for pipe() to be defined
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <asio.hpp>
#include <asiolink/io_address.h>