Commit ac1eaa10 authored by Thomas Markwalder's avatar Thomas Markwalder
Browse files

[master] Addes support for Host Reservations to PostgreSQL backend

    Merge branch 'trac4277'
parents 0c63a297 018e92a0
......@@ -886,7 +886,7 @@ EXAMPLE_RECURSIVE = NO
# directories that contain image that are included in the documentation (see
# the \image command).
IMAGE_PATH = ../doc/images ../src/lib/hooks/images ../src/bin/d2/images
IMAGE_PATH = ../doc/images ../src/lib/hooks/images ../src/bin/d2/images ../src/lib/dhcpsrv/images
# The INPUT_FILTER tag can be used to specify a program that doxygen should
# invoke to filter for each input file. Doxygen will invoke the filter program
......
......@@ -44,6 +44,9 @@ EXTRA_DIST += parsers/host_reservations_list_parser.h
EXTRA_DIST += parsers/ifaces_config_parser.cc
EXTRA_DIST += parsers/ifaces_config_parser.h
# Devel guide diagrams
EXTRA_DIST += images/pgsql_host_data_source.svg
# Define rule to build logging source files from message file
alloc_engine_messages.h alloc_engine_messages.cc dhcpsrv_messages.h \
dhcpsrv_messages.cc hosts_messages.h hosts_messages.cc: s-messages
......@@ -133,6 +136,7 @@ libkea_dhcpsrv_la_SOURCES += ncr_generator.cc ncr_generator.h
if HAVE_PGSQL
libkea_dhcpsrv_la_SOURCES += pgsql_connection.cc pgsql_connection.h
libkea_dhcpsrv_la_SOURCES += pgsql_exchange.cc pgsql_exchange.h
libkea_dhcpsrv_la_SOURCES += pgsql_host_data_source.cc pgsql_host_data_source.h
libkea_dhcpsrv_la_SOURCES += pgsql_lease_mgr.cc pgsql_lease_mgr.h
endif
if HAVE_CQL
......
......@@ -634,6 +634,10 @@ An error message indicating that communication with the MySQL database server
has been lost. When this occurs the server exits immediately with a non-zero
exit code. This is most likely due to a network issue.
% DHCPSRV_PGSQL_HOST_DB_GET_VERSION obtaining schema version information for the PostgreSQL hosts database
A debug message issued when the server is about to obtain schema version
information from the PostgreSQL hosts database.
% DHCPSRV_PGSQL_GET_ADDR4 obtaining IPv4 lease for address %1
A debug message issued when the server is attempting to obtain an IPv4
lease from the PostgreSQL database for the specified address.
......@@ -696,6 +700,15 @@ connection including database name and username needed to access it
The code has issued a rollback call. All outstanding transaction will
be rolled back and not committed to the database.
% DHCPSRV_PGSQL_START_TRANSACTION starting a new PostgreSQL transaction
A debug message issued when a new PostgreSQL transaction is being started.
This message is typically not issued when inserting data into a
single table because the server doesn't explicitly start
transactions in this case. This message is issued when data is
inserted into multiple tables with multiple INSERT statements
and there may be a need to rollback the whole transaction if
any of these INSERT statements fail.
% DHCPSRV_PGSQL_UPDATE_ADDR4 updating IPv4 lease for address %1
A debug message issued when the server is attempting to update IPv4
lease from the PostgreSQL database for the specified address.
......
// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2015-2016 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
......@@ -14,6 +14,10 @@
#include <dhcpsrv/mysql_host_data_source.h>
#endif
#ifdef HAVE_PGSQL
#include <dhcpsrv/pgsql_host_data_source.h>
#endif
#include <boost/algorithm/string.hpp>
#include <boost/foreach.hpp>
#include <boost/scoped_ptr.hpp>
......@@ -63,8 +67,10 @@ HostDataSourceFactory::create(const std::string& dbaccess) {
#ifdef HAVE_PGSQL
if (db_type == "postgresql") {
isc_throw(NotImplemented, "Sorry, PostgreSQL backend for host reservations "
"is not implemented yet.");
LOG_INFO(dhcpsrv_logger, DHCPSRV_PGSQL_HOST_DB)
.arg(DatabaseConnection::redactedAccessString(parameters));
getHostDataSourcePtr().reset(new PgSqlHostDataSource(parameters));
return;
}
#endif
......
This diff is collapsed.
......@@ -112,6 +112,15 @@ data source (if present) and concatenate results.
For more information about the \ref isc::dhcp::HostMgr please refer to its
documentation.
@subsection postgreSQLHostMgr PostgreSQL Host Reservation Management
Storing and retrieving host reservations within a PostgreSQL schema is
provided by the class, \ref isc::dhcp::PgSqlHostDataSource, a derivation of
\ref isc::dhcp::BaseHostDataSource and is depicted in the following
class diagram:
@image html pgsql_host_data_source.svg "PgSqlHostDataSource Class Diagram"
@section optionsConfig Options Configuration Information
The \ref isc::dhcp::CfgOption object holds a collection of options being
......
......@@ -1779,6 +1779,7 @@ public:
};
namespace {
/// @brief Prepared MySQL statements used by the backend to insert and
/// retrieve hosts from the database.
TaggedStatement tagged_statements[] = {
......@@ -1934,6 +1935,8 @@ TaggedStatement tagged_statements[] = {
{MySqlHostDataSourceImpl::NUM_STATEMENTS, NULL}
};
}; // end anonymouse namespace
MySqlHostDataSourceImpl::
MySqlHostDataSourceImpl(const MySqlConnection::ParameterMap& parameters)
: host_exchange_(new MySqlHostWithOptionsExchange(MySqlHostWithOptionsExchange::DHCP4_ONLY)),
......@@ -2318,6 +2321,7 @@ MySqlHostDataSource::get4(const SubnetID& subnet_id,
ConstHostPtr
MySqlHostDataSource::get4(const SubnetID& subnet_id,
const asiolink::IOAddress& address) const {
/// @todo: check that address is really v4, not v6.
// Set up the WHERE clause value
MYSQL_BIND inbind[2];
......@@ -2384,6 +2388,7 @@ MySqlHostDataSource::get6(const SubnetID& subnet_id,
ConstHostPtr
MySqlHostDataSource::get6(const asiolink::IOAddress& prefix,
const uint8_t prefix_len) const {
/// @todo: Check that prefix is v6 address, not v4.
// Set up the WHERE clause value
MYSQL_BIND inbind[2];
......
......@@ -35,6 +35,77 @@ const int PGSQL_DEFAULT_CONNECTION_TIMEOUT = 5; // seconds
const char PgSqlConnection::DUPLICATE_KEY[] = ERRCODE_UNIQUE_VIOLATION;
PgSqlResult::PgSqlResult(PGresult *result)
: result_(result), rows_(0), cols_(0) {
if (!result) {
isc_throw (BadValue, "PgSqlResult result pointer cannot be null");
}
rows_ = PQntuples(result);
cols_ = PQnfields(result);
}
void
PgSqlResult::rowCheck(int row) const {
if (row < 0 || row >= rows_) {
isc_throw (DbOperationError, "row: " << row
<< ", out of range: 0.." << rows_);
}
}
PgSqlResult::~PgSqlResult() {
if (result_) {
PQclear(result_);
}
}
void
PgSqlResult::colCheck(int col) const {
if (col < 0 || col >= cols_) {
isc_throw (DbOperationError, "col: " << col
<< ", out of range: 0.." << cols_);
}
}
void
PgSqlResult::rowColCheck(int row, int col) const {
rowCheck(row);
colCheck(col);
}
std::string
PgSqlResult::getColumnLabel(const int col) const {
const char* label = NULL;
try {
colCheck(col);
label = PQfname(result_, col);
} catch (...) {
std::ostringstream os;
os << "Unknown column:" << col;
return (os.str());
}
return (label);
}
PgSqlTransaction::PgSqlTransaction(PgSqlConnection& conn)
: conn_(conn), committed_(false) {
conn_.startTransaction();
}
PgSqlTransaction::~PgSqlTransaction() {
// If commit() wasn't explicitly called, rollback.
if (!committed_) {
conn_.rollback();
}
}
void
PgSqlTransaction::commit() {
conn_.commit();
committed_ = true;
}
PgSqlConnection::~PgSqlConnection() {
if (conn_) {
// Deallocate the prepared queries.
......@@ -192,6 +263,18 @@ PgSqlConnection::checkStatementError(const PgSqlResult& r,
}
}
void
PgSqlConnection::startTransaction() {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_PGSQL_START_TRANSACTION);
PgSqlResult r(PQexec(conn_, "START TRANSACTION"));
if (PQresultStatus(r) != PGRES_COMMAND_OK) {
const char* error_message = PQerrorMessage(conn_);
isc_throw(DbOperationError, "unable to start transaction"
<< error_message);
}
}
void
PgSqlConnection::commit() {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_COMMIT);
......
......@@ -28,7 +28,6 @@ const size_t PGSQL_MAX_PARAMETERS_IN_QUERY = 32;
/// Each statement is associated with an index, which is used to reference the
/// associated prepared statement.
struct PgSqlTaggedStatement {
/// Number of parameters for a given query
int nbparams;
......@@ -48,14 +47,16 @@ struct PgSqlTaggedStatement {
/// @brief Constants for PostgreSQL data types
/// This are defined by PostreSQL in <catalog/pg_type.h>, but including
/// this file is extrordinarily convoluted, so we'll use these to fill-in.
/// this file is extraordinarily convoluted, so we'll use these to fill-in.
const size_t OID_NONE = 0; // PostgreSQL infers proper type
const size_t OID_BOOL = 16;
const size_t OID_BYTEA = 17;
const size_t OID_INT8 = 20; // 8 byte int
const size_t OID_INT2 = 21; // 2 byte int
const size_t OID_TIMESTAMP = 1114;
const size_t OID_INT4 = 23; // 4 byte int
const size_t OID_TEXT = 25;
const size_t OID_VARCHAR = 1043;
const size_t OID_TIMESTAMP = 1114;
//@}
......@@ -80,20 +81,60 @@ class PgSqlResult : public boost::noncopyable {
public:
/// @brief Constructor
///
/// Store the pointer to the result set to being fetched.
/// Store the pointer to the result set to being fetched. Set row
/// and column counts for convenience.
///
PgSqlResult(PGresult *result) : result_(result)
{}
PgSqlResult(PGresult *result);
/// @brief Destructor
///
/// Frees the result set
~PgSqlResult() {
if (result_) {
PQclear(result_);
}
~PgSqlResult();
/// @brief Returns the number of rows in the result set.
int getRows() const {
return (rows_);
}
/// @brief Returns the number of columns in the result set.
int getCols() const {
return (cols_);
}
/// @brief Determines if a row index is valid
///
/// @param row index to range check
///
/// @throw DbOperationError if the row index is out of range
void rowCheck(int row) const;
/// @brief Determines if a column index is valid
///
/// @param col index to range check
///
/// @throw DbOperationError if the column index is out of range
void colCheck(int col) const;
/// @brief Determines if both a row and column index are valid
///
/// @param row index to range check
/// @param col index to range check
///
/// @throw DbOperationError if either the row or column index
/// is out of range
void rowColCheck(int row, int col) const;
/// @brief Fetches the name of the column in a result set
///
/// Returns the column name of the column from the result set.
/// If the column index is out of range it will return the
/// string "Unknown column:<index>"
///
/// @param col index of the column name to fetch
/// @return string containing the name of the column
/// This method is exception safe.
std::string getColumnLabel(const int col) const;
/// @brief Conversion Operator
///
/// Allows the PgSqlResult object to be passed as the result set argument to
......@@ -111,6 +152,8 @@ public:
private:
PGresult* result_; ///< Result set to be freed
int rows_; ///< Number of rows in the result set
int cols_; ///< Number of columns in the result set
};
......@@ -176,6 +219,67 @@ private:
PGconn* pgconn_; ///< Postgresql connection
};
/// @brief Forward declaration to @ref PgSqlConnection.
class PgSqlConnection;
/// @brief RAII object representing a PostgreSQL transaction.
///
/// An instance of this class should be created in a scope where multiple
/// INSERT statements should be executed within a single transaction. The
/// transaction is started when the constructor of this class is invoked.
/// The transaction is ended when the @ref PgSqlTransaction::commit is
/// explicitly called or when the instance of this class is destroyed.
/// The @ref PgSqlTransaction::commit commits changes to the database.
/// If the class instance is destroyed before @ref PgSqlTransaction::commit
/// has been called, the transaction is rolled back. The rollback on
/// destruction guarantees that partial data is not stored in the database
/// when an error occurs during any of the operations within a transaction.
///
/// By default PostgreSQL performs a commit following each statement which
/// alters the database (i.e. "autocommit"). Starting a transaction
/// stops autocommit for the connection until the transaction is ended by
/// either commit or rollback. Other connections are unaffected.
class PgSqlTransaction : public boost::noncopyable {
public:
/// @brief Constructor.
///
/// Starts transaction by executing the SQL statement: "START TRANSACTION"
///
/// @param conn PostgreSQL connection to use for the transaction. This
/// connection will be later used to commit or rollback changes.
///
/// @throw DbOperationError if statement execution fails
PgSqlTransaction(PgSqlConnection& conn);
/// @brief Destructor.
///
/// If the transaction has not been committed, it is rolled back
/// by executing the SQL statement: "ROLLBACK"
///
/// @throw DbOperationError if statement execution fails
~PgSqlTransaction();
/// @brief Commits transaction.
///
/// Commits all changes made during the transaction by executing the
/// SQL statement: "COMMIT"
///
/// @throw DbOperationError if statement execution fails
void commit();
private:
/// @brief Holds reference to the PostgreSQL database connection.
PgSqlConnection& conn_;
/// @brief Boolean flag indicating if the transaction has been committed.
///
/// This flag is used in the class destructor to assess if the
/// transaction should be rolled back.
bool committed_;
};
/// @brief Common PgSql Connector Pool
///
/// This class provides common operations for PgSql database connection
......@@ -218,18 +322,23 @@ public:
/// @throw DbOpenError Error opening the database
void openDatabase();
/// @brief Start a transaction
///
/// Starts a transaction.
///
/// @throw DbOperationError If the transaction start failed.
void startTransaction();
/// @brief Commit Transactions
///
/// Commits all pending database operations. On databases that don't
/// support transactions, this is a no-op.
/// Commits all pending database operations.
///
/// @throw DbOperationError If the commit failed.
void commit();
/// @brief Rollback Transactions
///
/// Rolls back all pending database operations. On databases that don't
/// support transactions, this is a no-op.
/// Rolls back all pending database operations.
///
/// @throw DbOperationError If the rollback failed.
void rollback();
......@@ -289,8 +398,6 @@ public:
};
}; // end of isc::dhcp namespace
}; // end of isc namespace
......
......@@ -21,6 +21,10 @@ const char* PsqlBindArray::TRUE_STR = "TRUE";
const char* PsqlBindArray::FALSE_STR = "FALSE";
void PsqlBindArray::add(const char* value) {
if (!value) {
isc_throw(BadValue, "PsqlBindArray::add - char* value cannot be NULL");
}
values_.push_back(value);
lengths_.push_back(strlen(value));
formats_.push_back(TEXT_FMT);
......@@ -38,10 +42,52 @@ void PsqlBindArray::add(const std::vector<uint8_t>& data) {
formats_.push_back(BINARY_FMT);
}
void PsqlBindArray::add(const uint8_t* data, const size_t len) {
if (!data) {
isc_throw(BadValue, "PsqlBindArray::add - uint8_t data cannot be NULL");
}
values_.push_back(reinterpret_cast<const char*>(&(data[0])));
lengths_.push_back(len);
formats_.push_back(BINARY_FMT);
}
void PsqlBindArray::add(const bool& value) {
add(value ? TRUE_STR : FALSE_STR);
}
void PsqlBindArray::add(const uint8_t& byte) {
// We static_cast to an unsigned int, otherwise lexcial_cast may to
// treat byte as a character, which yields "" for unprintable values
addTempString(boost::lexical_cast<std::string>
(static_cast<unsigned int>(byte)));
}
void PsqlBindArray::add(const isc::asiolink::IOAddress& addr) {
if (addr.isV4()) {
addTempString(boost::lexical_cast<std::string>
(static_cast<uint32_t>(addr)));
} else {
addTempString(addr.toText());
}
}
void PsqlBindArray::addNull(const int format) {
values_.push_back(NULL);
lengths_.push_back(0);
formats_.push_back(format);
}
/// @todo Eventually this could replace add(std::string&)? This would mean
/// all bound strings would be internally copies rather than perhaps belonging
/// to the originating object such as Host::hostname_. One the one hand it
/// would make all strings handled one-way only, on the other hand it would
/// mean duplicating strings where it isn't strictly necessary.
void PsqlBindArray::addTempString(const std::string& str) {
bound_strs_.push_back(ConstStringPtr(new std::string(str)));
PsqlBindArray::add((bound_strs_.back())->c_str());
}
std::string PsqlBindArray::toText() const {
std::ostringstream stream;
for (int i = 0; i < values_.size(); ++i) {
......@@ -54,12 +100,13 @@ std::string PsqlBindArray::toText() const {
stream << "empty" << std::endl;
} else {
stream << "0x";
for (int i = 0; i < lengths_[i]; ++i) {
for (int x = 0; x < lengths_[i]; ++x) {
stream << std::setfill('0') << std::setw(2)
<< std::setbase(16)
<< static_cast<unsigned int>(data[i]);
<< static_cast<unsigned int>(data[x]);
}
stream << std::endl;
stream << std::setbase(10);
}
}
}
......@@ -111,18 +158,32 @@ PgSqlExchange::convertFromDatabaseTime(const std::string& db_time_val) {
const char*
PgSqlExchange::getRawColumnValue(const PgSqlResult& r, const int row,
const size_t col) const {
const size_t col) {
r.rowColCheck(row,col);
const char* value = PQgetvalue(r, row, col);
if (!value) {
isc_throw(DbOperationError, "getRawColumnValue no data for :"
<< getColumnLabel(col) << " row:" << row);
<< getColumnLabel(r, col) << " row:" << row);
}
return (value);
}
bool
PgSqlExchange::isColumnNull(const PgSqlResult& r, const int row,
const size_t col) {
r.rowColCheck(row,col);
return (PQgetisnull(r, row, col));
}
void
PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
const size_t col, bool &value) const {
const size_t col, std::string& value) {
value = getRawColumnValue(r, row, col);
}
void
PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
const size_t col, bool &value) {
const char* data = getRawColumnValue(r, row, col);
if (!strlen(data) || *data == 'f') {
value = false;
......@@ -130,48 +191,35 @@ PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
value = true;
} else {
isc_throw(DbOperationError, "Invalid boolean data: " << data
<< " for: " << getColumnLabel(col) << " row:" << row
<< " for: " << getColumnLabel(r, col) << " row:" << row
<< " : must be 't' or 'f'");
}
}
void
PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
const size_t col, uint32_t &value) const {
const size_t col, uint8_t &value) {
const char* data = getRawColumnValue(r, row, col);
try {
value = boost::lexical_cast<uint32_t>(data);
} catch (const std::exception& ex) {
isc_throw(DbOperationError, "Invalid uint32_t data: " << data
<< " for: " << getColumnLabel(col) << " row:" << row
<< " : " << ex.what());
}
}
void
PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
const size_t col, int32_t &value) const {
const char* data = getRawColumnValue(r, row, col);
try {
value = boost::lexical_cast<int32_t>(data);
// lexically casting as uint8_t doesn't convert from char
// so we use uint16_t and implicitly convert.
value = boost::lexical_cast<uint16_t>(data);
} catch (const std::exception& ex) {
isc_throw(DbOperationError, "Invalid int32_t data: " << data
<< " for: " << getColumnLabel(col) << " row:" << row
isc_throw(DbOperationError, "Invalid uint8_t data: " << data
<< " for: " << getColumnLabel(r, col) << " row:" << row
<< " : " << ex.what());
}
}
void
PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
const size_t col, uint8_t &value) const {
isc::asiolink::IOAddress
PgSqlExchange::getIPv6Value(const PgSqlResult& r, const int row,
const size_t col) {
const char* data = getRawColumnValue(r, row, col);
try {
// lexically casting as uint8_t doesn't convert from char
// so we use uint16_t and implicitly convert.
value = boost::lexical_cast<uint16_t>(data);
return (isc::asiolink::IOAddress(data));
} catch (const std::exception& ex) {
isc_throw(DbOperationError, "Invalid uint8_t data: " << data
<< " for: " << getColumnLabel(col) << " row:" << row
isc_throw(DbOperationError, "Cannot convert data: " << data
<< " for: " << getColumnLabel(r, col) << " row:" << row
<< " : " << ex.what());
}
}
......@@ -180,7 +228,7 @@ void
PgSqlExchange::convertFromBytea(const PgSqlResult& r, const int row,