Commit 7b2555aa authored by Marcin Siodelski's avatar Marcin Siodelski

[#92,!13] New libkea-database library created.

parent 12411724
......@@ -1534,6 +1534,8 @@ AC_CONFIG_FILES([Makefile
src/lib/config/tests/testdata/Makefile
src/lib/cryptolink/Makefile
src/lib/cryptolink/tests/Makefile
src/lib/database/Makefile
src/lib/database/tests/Makefile
src/lib/dhcp/Makefile
src/lib/dhcp/tests/Makefile
src/lib/dhcp_ddns/Makefile
......
# The following build order must be maintained.
SUBDIRS = exceptions util log cryptolink dns asiolink cc testutils hooks dhcp \
config stats asiodns dhcp_ddns eval dhcpsrv cfgrpt process http
SUBDIRS = exceptions util log cryptolink dns asiolink cc database testutils \
hooks dhcp config stats asiodns dhcp_ddns eval dhcpsrv cfgrpt \
process http
SUBDIRS = . tests
AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CXXFLAGS = $(KEA_CXXFLAGS)
lib_LTLIBRARIES = libkea-database.la
libkea_database_la_SOURCES = database_connection.cc database_connection.h
libkea_database_la_SOURCES += db_exceptions.h
libkea_database_la_SOURCES += db_log.cc db_log.h
libkea_database_la_LIBADD = $(top_builddir)/src/lib/log/libkea-log.la
libkea_database_la_LIBADD += $(top_builddir)/src/lib/log/interprocess/libkea-log_interprocess.la
libkea_database_la_LIBADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
libkea_database_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la
libkea_database_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
libkea_database_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
libkea_database_la_LIBADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS)
libkea_database_la_LDFLAGS = -no-undefined -version-info 0:0:0
# The message file should be in the distribution.
#EXTRA_DIST = config_backend.dox
CLEANFILES = *.gcno *.gcda
# Specify the headers for copying into the installation directory tree.
#libkea_cb_includedir = $(pkgincludedir)/config
#libkea_cb_include_HEADERS =
// Copyright (C) 2015-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#include <config.h>
#include <database/database_connection.h>
#include <database/db_exceptions.h>
#include <database/db_log.h>
#include <exceptions/exceptions.h>
#include <boost/algorithm/string.hpp>
#include <boost/foreach.hpp>
#include <vector>
using namespace std;
namespace isc {
namespace db {
const time_t DatabaseConnection::MAX_DB_TIME = 2147483647;
std::string
DatabaseConnection::getParameter(const std::string& name) const {
ParameterMap::const_iterator param = parameters_.find(name);
if (param == parameters_.end()) {
isc_throw(BadValue, "Parameter " << name << " not found");
}
return (param->second);
}
DatabaseConnection::ParameterMap
DatabaseConnection::parse(const std::string& dbaccess) {
DatabaseConnection::ParameterMap mapped_tokens;
if (!dbaccess.empty()) {
vector<string> tokens;
// We need to pass a string to is_any_of, not just char*. Otherwise
// there are cryptic warnings on Debian6 running g++ 4.4 in
// /usr/include/c++/4.4/bits/stl_algo.h:2178 "array subscript is above
// array bounds"
boost::split(tokens, dbaccess, boost::is_any_of(string("\t ")));
BOOST_FOREACH(std::string token, tokens) {
size_t pos = token.find("=");
if (pos != string::npos) {
string name = token.substr(0, pos);
string value = token.substr(pos + 1);
mapped_tokens.insert(make_pair(name, value));
} else {
DB_LOG_ERROR(DB_INVALID_ACCESS).arg(dbaccess);
isc_throw(InvalidParameter, "Cannot parse " << token
<< ", expected format is name=value");
}
}
}
return (mapped_tokens);
}
std::string
DatabaseConnection::redactedAccessString(const ParameterMap& parameters) {
// Reconstruct the access string: start of with an empty string, then
// work through all the parameters in the original string and add them.
std::string access;
for (DatabaseConnection::ParameterMap::const_iterator i = parameters.begin();
i != parameters.end(); ++i) {
// Separate second and subsequent tokens are preceded by a space.
if (!access.empty()) {
access += " ";
}
// Append name of parameter...
access += i->first;
access += "=";
// ... and the value, except in the case of the password, where a
// redacted value is appended.
if (i->first == std::string("password")) {
access += "*****";
} else {
access += i->second;
}
}
return (access);
}
bool
DatabaseConnection::configuredReadOnly() const {
std::string readonly_value = "false";
try {
readonly_value = getParameter("readonly");
boost::algorithm::to_lower(readonly_value);
} catch (...) {
// Parameter "readonly" hasn't been specified so we simply use
// the default value of "false".
}
if ((readonly_value != "false") && (readonly_value != "true")) {
isc_throw(DbInvalidReadOnly, "invalid value '" << readonly_value
<< "' specified for boolean parameter 'readonly'");
}
return (readonly_value == "true");
}
ReconnectCtlPtr
DatabaseConnection::makeReconnectCtl() const {
ReconnectCtlPtr retry;
string type = "unknown";
unsigned int retries = 0;
unsigned int interval = 0;
// Assumes that parsing ensurse only valid values are present
try {
type = getParameter("type");
} catch (...) {
// Wasn't specified so we'll use default of "unknown".
}
std::string parm_str;
try {
parm_str = getParameter("max-reconnect-tries");
retries = boost::lexical_cast<unsigned int>(parm_str);
} catch (...) {
// Wasn't specified so we'll use default of 0;
}
try {
parm_str = getParameter("reconnect-wait-time");
interval = boost::lexical_cast<unsigned int>(parm_str);
} catch (...) {
// Wasn't specified so we'll use default of 0;
}
retry.reset(new ReconnectCtl(type, retries, interval));
return (retry);
}
bool
DatabaseConnection::invokeDbLostCallback() const {
if (DatabaseConnection::db_lost_callback) {
// Invoke the callback, passing in a new instance of ReconnectCtl
return (DatabaseConnection::db_lost_callback)(makeReconnectCtl());
}
return (false);
}
DatabaseConnection::DbLostCallback
DatabaseConnection::db_lost_callback = 0;
};
};
// Copyright (C) 2015-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#ifndef DATABASE_CONNECTION_H
#define DATABASE_CONNECTION_H
#include <boost/noncopyable.hpp>
#include <boost/function.hpp>
#include <boost/shared_ptr.hpp>
#include <exceptions/exceptions.h>
#include <map>
#include <string>
namespace isc {
namespace db {
/// @brief Exception thrown if name of database is not specified
class NoDatabaseName : public Exception {
public:
NoDatabaseName(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {}
};
/// @brief Exception thrown on failure to open database
class DbOpenError : public Exception {
public:
DbOpenError(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {}
};
/// @brief Exception thrown on failure to execute a database function
class DbOperationError : public Exception {
public:
DbOperationError(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {}
};
/// @brief Invalid type exception
///
/// Thrown when the factory doesn't recognize the type of the backend.
class InvalidType : public Exception {
public:
InvalidType(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {}
};
/// @brief Invalid Timeout
///
/// Thrown when the timeout specified for the database connection is invalid.
class DbInvalidTimeout : public Exception {
public:
DbInvalidTimeout(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {}
};
/// @brief Invalid 'readonly' value specification.
///
/// Thrown when the value of the 'readonly' boolean parameter is invalid.
class DbInvalidReadOnly : public Exception {
public:
DbInvalidReadOnly(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {}
};
/// @brief Warehouses DB reconnect control values
///
/// When a DatabaseConnection loses connectivity to its backend, it
/// creates an instance of this class based on its configuration parameters and
/// passes the instance into connection's DB lost callback. This allows
/// the layer(s) above the connection to know how to proceed.
///
class ReconnectCtl {
public:
/// @brief Constructor
/// @param backend_type type of the caller backend.
/// @param max_retries maximum number of reconnect attempts to make
/// @param retry_interval amount of time to between reconnect attempts
ReconnectCtl(const std::string& backend_type, unsigned int max_retries,
unsigned int retry_interval)
: backend_type_(backend_type), max_retries_(max_retries),
retries_left_(max_retries), retry_interval_(retry_interval) {}
/// @brief Returns the type of the caller backend.
std::string backendType() const {
return (backend_type_);
}
/// @brief Decrements the number of retries remaining
///
/// Each call decrements the number of retries by one until zero is reached.
/// @return true the number of retries remaining is greater than zero.
bool checkRetries() {
return (retries_left_ ? --retries_left_ : false);
}
/// @brief Returns the maximum number for retries allowed
unsigned int maxRetries() {
return (max_retries_);
}
/// @brief Returns the number for retries remaining
unsigned int retriesLeft() {
return (retries_left_);
}
/// @brief Returns the amount of time to wait between reconnect attempts
unsigned int retryInterval() {
return (retry_interval_);
}
private:
/// @brief Caller backend type.
const std::string backend_type_;
/// @brief Maximum number of retry attempts to make
unsigned int max_retries_;
/// @brief Number of attempts remaining
unsigned int retries_left_;
/// @brief The amount of time to wait between reconnect attempts
unsigned int retry_interval_;
};
/// @brief Pointer to an instance of ReconnectCtl
typedef boost::shared_ptr<ReconnectCtl> ReconnectCtlPtr;
/// @brief Common database connection class.
///
/// This class provides functions that are common for establishing
/// connection with different types of databases; enables operations
/// on access parameters strings. In particular, it provides a way
/// to parse parameters in key=value format. This class is expected
/// to be a base class for all @ref LeaseMgr and possibly
/// @ref BaseHostDataSource derived classes.
class DatabaseConnection : public boost::noncopyable {
public:
/// @brief Defines maximum value for time that can be reliably stored.
///
/// @todo: Is this common for MySQL and Postgres? Maybe we should have
/// specific values for each backend?
///
/// If I'm still alive I'll be too old to care. You fix it.
static const time_t MAX_DB_TIME;
/// @brief Database configuration parameter map
typedef std::map<std::string, std::string> ParameterMap;
/// @brief Constructor
///
/// @param parameters A data structure relating keywords and values
/// concerned with the database.
DatabaseConnection(const ParameterMap& parameters)
:parameters_(parameters) {
}
/// @brief Destructor
virtual ~DatabaseConnection(){};
/// @brief Instantiates a ReconnectCtl based on the connection's
/// reconnect parameters
/// @return pointer to the new ReconnectCtl object
virtual ReconnectCtlPtr makeReconnectCtl() const;
/// @brief Returns value of a connection parameter.
///
/// @param name Name of the parameter which value should be returned.
/// @return Value of one of the connection parameters.
/// @throw BadValue if parameter is not found
std::string getParameter(const std::string& name) const;
/// @brief Parse database access string
///
/// Parses the string of "keyword=value" pairs and separates them
/// out into the map.
///
/// @param dbaccess Database access string.
///
/// @return @ref ParameterMap of keyword/value pairs.
static ParameterMap parse(const std::string& dbaccess);
/// @brief Redact database access string
///
/// Takes the database parameters and returns a database access string
/// passwords replaced by asterisks. This string is used in log messages.
///
/// @param parameters Database access parameters (output of "parse").
///
/// @return Redacted database access string.
static std::string redactedAccessString(const ParameterMap& parameters);
/// @brief Convenience method checking if database should be opened with
/// read only access.
///
/// @return true if "readonly" parameter is specified and set to true;
/// false if "readonly" parameter is not specified or it is specified
/// and set to false.
bool configuredReadOnly() const;
/// @brief Defines a callback prototype for propogating events upward
typedef boost::function<bool (ReconnectCtlPtr db_retry)> DbLostCallback;
/// @brief Invokes the connection's lost connectivity callback
///
/// This function may be called by derivations when the connectivity
/// to their data server is lost. If connectivity callback was specified,
/// this function will instantiate a ReconnectCtl and pass it to the
/// callback.
///
/// @return Returns the result of the callback or false if there is no
/// callback.
bool invokeDbLostCallback() const;
/// @brief Optional call back function to invoke if a successfully
/// open connection subsequently fails
static DbLostCallback db_lost_callback;
private:
/// @brief List of parameters passed in dbconfig
///
/// That will be mostly used for storing database name, username,
/// password and other parameters required for DB access. It is not
/// intended to keep any DHCP-related parameters.
ParameterMap parameters_;
};
}; // end of isc::db namespace
}; // end of isc namespace
#endif // DATABASE_CONNECTION_H
......@@ -10,7 +10,7 @@
#include <exceptions/exceptions.h>
namespace isc {
namespace dhcp {
namespace db {
/// @brief Database statement not applied
///
......@@ -75,6 +75,6 @@ public:
};
} // namespace isc
} // namespace dhcp
} // namespace db
#endif
......@@ -9,13 +9,14 @@
#include <config.h>
#include <exceptions/exceptions.h>
#include <dhcpsrv/db_log.h>
#include <dhcpsrv/dhcpsrv_db_log.h>
#include <database/db_log.h>
using namespace isc::log;
namespace isc {
namespace dhcp {
namespace db {
DbLoggerStack db_logger_stack;
const MessageID&
DbLogger::translateMessage(const DbMessageID& id) const {
......@@ -32,5 +33,5 @@ void checkDbLoggerStack() {
}
}
} // namespace dhcp
} // namespace db
} // namespace isc
......@@ -24,7 +24,7 @@
/// logger with mapped messages.
namespace isc {
namespace dhcp {
namespace db {
///@{
/// @brief Database logging levels
......@@ -146,7 +146,7 @@ void checkDbLoggerStack();
///@}
} // namespace dhcp
} // namespace db
} // namespace isc
#endif // DB_LOG_H
SUBDIRS = .
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/lib/config/tests\"
AM_CXXFLAGS = $(KEA_CXXFLAGS)
if USE_STATIC_LINK
AM_LDFLAGS = -static
endif
CLEANFILES = *.gcno *.gcda
TESTS_ENVIRONMENT = \
$(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
TESTS =
if HAVE_GTEST
TESTS += libdatabase_unittests
libdatabase_unittests_SOURCES = database_connection_unittest.cc
libdatabase_unittests_SOURCES += run_unittests.cc
libdatabase_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
libdatabase_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
libdatabase_unittests_LDADD = $(top_builddir)/src/lib/database/libkea-database.la
libdatabase_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
libdatabase_unittests_LDADD += $(top_builddir)/src/lib/log/interprocess/libkea-log_interprocess.la
libdatabase_unittests_LDADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
libdatabase_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
libdatabase_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
libdatabase_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
libdatabase_unittests_LDADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS) $(GTEST_LDADD)
endif
noinst_PROGRAMS = $(TESTS)
// Copyright (C) 2015-2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#include <config.h>
#include <exceptions/exceptions.h>
#include <database/database_connection.h>
#include <gtest/gtest.h>
#include <boost/bind.hpp>
using namespace isc::db;
/// @brief Test fixture for exercising DbLostCallback invocation
class DatabaseConnectionCallbackTest : public ::testing::Test {
public:
/// Constructor
DatabaseConnectionCallbackTest()
: db_reconnect_ctl_(0) {
}
/// @brief Callback to register with a DatabaseConnection
///
/// @param db_reconnect_ctl ReconnectCtl containing reconnect
/// parameters
bool dbLostCallback(ReconnectCtlPtr db_reconnect_ctl) {
if (!db_reconnect_ctl) {
isc_throw(isc::BadValue, "db_reconnect_ctl should not be null");
}
db_reconnect_ctl_ = db_reconnect_ctl;
return (true);
}
/// @brief Retainer for the control passed into the callback
ReconnectCtlPtr db_reconnect_ctl_;
};
/// @brief getParameter test
///
/// This test checks if the LeaseMgr can be instantiated and that it
/// parses parameters string properly.
TEST(DatabaseConnectionTest, getParameter) {
DatabaseConnection::ParameterMap pmap;
pmap[std::string("param1")] = std::string("value1");
pmap[std::string("param2")] = std::string("value2");
DatabaseConnection datasrc(pmap);
EXPECT_EQ("value1", datasrc.getParameter("param1"));
EXPECT_EQ("value2", datasrc.getParameter("param2"));
EXPECT_THROW(datasrc.getParameter("param3"), isc::BadValue);
}
/// @brief NoDbLostCallback
///
/// This test verifies that DatabaseConnection::invokeDbLostCallback
/// returns a false if there is connection has no registered
/// DbLostCallback.
TEST_F(DatabaseConnectionCallbackTest, NoDbLostCallback) {
DatabaseConnection::ParameterMap pmap;
pmap[std::string("type")] = std::string("test");
pmap[std::string("max-reconnect-tries")] = std::string("3");
pmap[std::string("reconnect-wait-time")] = std::string("60");
DatabaseConnection datasrc(pmap);
bool ret = false;
ASSERT_NO_THROW(ret = datasrc.invokeDbLostCallback());
EXPECT_FALSE(ret);
EXPECT_FALSE(db_reconnect_ctl_);
}
/// @brief dbLostCallback
///
/// This test verifies that DatabaseConnection::invokeDbLostCallback
/// safely invokes the registered DbLostCallback. It also tests
/// operation of DbReconnectCtl retry accounting methods.
TEST_F(DatabaseConnectionCallbackTest, dbLostCallback) {
/// Create a Database configuration that includes the reconnect
/// control parameters.
DatabaseConnection::ParameterMap pmap;
pmap[std::string("type")] = std::string("test");
pmap[std::string("max-reconnect-tries")] = std::string("3");
pmap[std::string("reconnect-wait-time")] = std::string("60");
/// Install the callback.
DatabaseConnection::db_lost_callback =
boost::bind(&DatabaseConnectionCallbackTest::dbLostCallback, this, _1);
/// Create the connection..
DatabaseConnection datasrc(pmap);
/// We should be able to invoke the callback and glean
/// the correct reconnect contorl parameters from it.
bool ret = false;
ASSERT_NO_THROW(ret = datasrc.invokeDbLostCallback());
EXPECT_TRUE(ret);
ASSERT_TRUE(db_reconnect_ctl_);
ASSERT_EQ("test", db_reconnect_ctl_->backendType());
ASSERT_EQ(3, db_reconnect_ctl_->maxRetries());
ASSERT_EQ(3, db_reconnect_ctl_->retriesLeft());
EXPECT_EQ(60, db_reconnect_ctl_->retryInterval());
/// Verify that checkRetries() correctly decrements
/// down to zero, and that retriesLeft() returns
/// the correct value.
for (int i = 3; i > 1 ; --i) {
ASSERT_EQ(i, db_reconnect_ctl_->retriesLeft());
ASSERT_TRUE(db_reconnect_ctl_->checkRetries());
}
/// Retries are exhausted, verify that's reflected.
EXPECT_FALSE(db_reconnect_ctl_->checkRetries());
EXPECT_EQ(0, db_reconnect_ctl_->retriesLeft());
EXPECT_EQ(3, db_reconnect_ctl_->maxRetries());
}
// This test checks that a database access string can be parsed correctly.
TEST(DatabaseConnectionTest, parse) {
DatabaseConnection::ParameterMap parameters = DatabaseConnection::parse(
"user=me password=forbidden name=kea somethingelse= type=mysql");
EXPECT_EQ(5, parameters.size());
EXPECT_EQ("me", parameters["user"]);
EXPECT_EQ("forbidden", parameters["password"]);
EXPECT_EQ("kea", parameters["name"]);
EXPECT_EQ("mysql", parameters["type"]);
EXPECT_EQ("", parameters["somethingelse"]);
}
// This test checks that an invalid database access string behaves as expected.
TEST(DatabaseConnectionTest, parseInvalid) {
// No tokens in the string, so we expect no parameters
std::string invalid = "";
DatabaseConnection::ParameterMap parameters = DatabaseConnection::parse(invalid);
EXPECT_EQ(0, parameters.size());
// With spaces, there are some tokens so we expect invalid parameter
// as there are no equals signs.
invalid = " \t ";
EXPECT_THROW(DatabaseConnection::parse(invalid), isc::InvalidParameter);
invalid = " noequalshere ";
EXPECT_THROW(DatabaseConnection::parse(invalid), isc::InvalidParameter);
// A single "=" is valid string, but is placed here as the result is
// expected to be nothing.
invalid = "=";
parameters = DatabaseConnection::parse(invalid);
EXPECT_EQ(1, parameters.size());
EXPECT_EQ("", parameters[""]);
}
/// @brief redactConfigString test
///
/// Checks that the redacted configuration string includes the password only
/// as a set of asterisks.
TEST(DatabaseConnectionTest, redactAccessString) {
DatabaseConnection::ParameterMap parameters =
DatabaseConnection::parse("user=me password=forbidden name=kea type=mysql");
EXPECT_EQ(4, parameters.size());
EXPECT_EQ("me", parameters["user"]);
EXPECT_EQ("forbidden", parameters["password"]);