Commit 33428957 authored by Thomas Markwalder's avatar Thomas Markwalder

[4277] Bare bones implementation of PgSqlHostDataSource

src/lib/dhcpsrv
    pgsql_host_data_source.c
    pgsql_host_data_source.h  - new files, preliminary implementation

src/lib/dhcpsrv/Makefile.am
    Added new files pgsql_host_data_source.cc, pgsql_host_data_source.h

src/lib/dhcpsrv/dhcpsrv_messages.mes
    Added log messages DHCPSRV_PGSQL_HOST_DB_GET_VERSION, DHCPSRV_PGSQL_START_TRANSACTION

src/lib/dhcpsrv/pgsql_connection.cc
src/lib/dhcpsrv/pgsql_connection.h
    Added PgSqlTransaction
    Added PgSqlConnection::startTransaction()

src/lib/dhcpsrv/pgsql_exchange.cc
src/lib/dhcpsrv/pgsql_exchange.h
    PsqlBindArray
    - Added storage of conversion strings used for bound values
    - Added add() variants for uint8_t, IOAddress, uint8_t buffer
    - Added templated variant for miscellaneous types

    PgSqlExchange
    - Removed getColumnValue variants for various integers, replaced
    with templated version for miscellaneous types

src/lib/dhcpsrv/pgsql_lease_mgr.cc
    Added todo comment to remember to account for hwaddr columns added to lease6

src/lib/dhcpsrv/tests/pgsql_exchange_unittest.cc
    TEST(PsqlBindArray, basicOperation) - new test to exercise bind functions
parent 7299124a
......@@ -136,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
libkea_dhcpsrv_la_SOURCES += pool.cc pool.h
......
......@@ -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.
......
......@@ -35,6 +35,25 @@ const int PGSQL_DEFAULT_CONNECTION_TIMEOUT = 5; // seconds
const char PgSqlConnection::DUPLICATE_KEY[] = ERRCODE_UNIQUE_VIOLATION;
PgSqlTransaction::PgSqlTransaction(PgSqlConnection& conn)
: conn_(conn), committed_(false) {
conn_.startTransaction();
}
PgSqlTransaction::~PgSqlTransaction() {
// Rollback if the PgSqlTransaction::commit wasn't explicitly
// called.
if (!committed_) {
conn_.rollback();
}
}
void
PgSqlTransaction::commit() {
conn_.commit();
committed_ = true;
}
PgSqlConnection::~PgSqlConnection() {
if (conn_) {
// Deallocate the prepared queries.
......@@ -192,6 +211,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);
......
......@@ -53,6 +53,7 @@ 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_INT4 = 23; // 4 byte int
const size_t OID_INT2 = 21; // 2 byte int
const size_t OID_TIMESTAMP = 1114;
const size_t OID_VARCHAR = 1043;
......@@ -176,6 +177,62 @@ private:
PGconn* pgconn_; ///< Postgresql connection
};
/// @brief Forward declaration to @ref PgSqlConnection.
class PgSqlConnection;
/// @brief RAII object representing 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
/// and the changes remain in the database when the instance of the
/// class is destroyed. If the class instance is destroyed before the
/// @ref PgSqlTransaction::commit is called, the transaction is rolled
/// back. The rollback on destruction guarantees that partial data is
/// not stored in the database when there is an error during any
/// of the operations belonging to a transaction.
///
/// The default PostgreSQL backend configuration enables 'autocommit'.
/// Starting a transaction overrides 'autocommit' setting for this
/// particular transaction only. It does not affect the global 'autocommit'
/// setting for the database connection, i.e. all modifications to the
/// database which don't use transactions will still be auto committed.
class PgSqlTransaction : public boost::noncopyable {
public:
/// @brief Constructor.
///
/// Starts transaction by making a "START TRANSACTION" query.
///
/// @param conn PostgreSQL connection to use for the transaction. This
/// connection will be later used to commit or rollback changes.
///
/// @throw DbOperationError if "START TRANSACTION" query fails.
PgSqlTransaction(PgSqlConnection& conn);
/// @brief Destructor.
///
/// Rolls back the transaction if changes haven't been committed.
~PgSqlTransaction();
/// @brief Commits transaction.
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 +275,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();
......
......@@ -38,10 +38,38 @@ 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) {
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
bindString(boost::lexical_cast<std::string>
(static_cast<unsigned int>(byte)));
}
void PsqlBindArray::add(const isc::asiolink::IOAddress& addr) {
if (addr.isV4()) {
bindString(boost::lexical_cast<std::string>
(static_cast<uint32_t>(addr)));
} else {
bindString(addr.toText());
}
}
// eventually this should replace add(std::string)
void PsqlBindArray::bindString(const std::string& str) {
bound_strs_.push_back(StringPtr(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) {
......@@ -135,32 +163,6 @@ PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
}
}
void
PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
const size_t col, uint32_t &value) const {
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);
} catch (const std::exception& ex) {
isc_throw(DbOperationError, "Invalid int32_t data: " << data
<< " for: " << getColumnLabel(col) << " row:" << row
<< " : " << ex.what());
}
}
void
PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
const size_t col, uint8_t &value) const {
......
......@@ -7,10 +7,16 @@
#ifndef PGSQL_EXCHANGE_H
#define PGSQL_EXCHANGE_H
#include <asiolink/io_address.h>
#include <dhcpsrv/pgsql_connection.h>
#include <boost/lexical_cast.hpp>
#include <boost/noncopyable.hpp>
#include <boost/shared_ptr.hpp>
#include <stdint.h>
#include <vector>
#include <iostream>
namespace isc {
namespace dhcp {
......@@ -24,6 +30,11 @@ namespace dhcp {
/// be valid for the duration of the PostgreSQL statement execution. In other
/// words populating them with pointers to values that go out of scope before
/// statement is executed is a bad idea.
/// @brief smart pointer to strings used by PsqlBindArray to ensure scope
/// of strings supplying exchange values
typedef boost::shared_ptr<std::string> StringPtr;
struct PsqlBindArray {
/// @brief Vector of pointers to the data values.
std::vector<const char *> values_;
......@@ -74,14 +85,27 @@ struct PsqlBindArray {
/// @param value std::string containing the value to add.
void add(const std::string& value);
/// @brief Adds a binary value to the bind array.
/// @brief Adds a vector of binary data to the bind array.
///
/// Adds a BINARY_FMT value to the end of the bind array using the
/// given vector as the data source.
/// given vector as the data source. NOTE this does not replicate
/// the vector, so it must remain in scope until the bind array
/// is destroyed.
///
/// @param data vector of binary bytes.
void add(const std::vector<uint8_t>& data);
/// @brief Adds a buffer of binary data to the bind array.
///
/// Adds a BINARY_FMT value to the end of the bind array using the
/// given vector as the data source. NOTE this does not replicate
/// the buffer, so it must remain in scope until the bind array
/// is destroyed.
///
/// @param data buffer of binary data.
/// @param len number of bytes of data in buffer
void add(const uint8_t* data, const size_t len);
/// @brief Adds a boolean value to the bind array.
///
/// Converts the given boolean value to its corresponding to PostgreSQL
......@@ -90,11 +114,62 @@ struct PsqlBindArray {
/// @param value bool value to add.
void add(const bool& value);
/// @brief Adds a uint8_t value to the bind array.
///
/// Converts the given uint8_t value to its corresponding numeric string
/// literal and adds it as a TEXT_FMT value to the bind array.
///
/// @param value bool value to add.
void add(const uint8_t& byte);
/// @brief Adds a the given IOAddress value to the bind array.
///
/// Converts the IOAddress, based on its protocol family, to the
/// corresponding string literal and adds it as a TEXT_FMT value to
/// the bind array.
///
/// @param value bool value to add.
void add(const isc::asiolink::IOAddress& addr);
/// @brief Adds a the given value to the bind array.
///
/// Converts the given value its corresponding string literal
/// boost::lexical_cast and adds it as a TEXT_FMT value to the bind array.
///
/// @param value bool value to add.
template<typename T>
void add(const T& numeric) {
bindString(boost::lexical_cast<std::string>(numeric));
}
/// @brief Binds a the given string to the bind array.
///
/// Prior to added the The given string the vector of exchange values,
/// it duplicated as a StringPtr and saved internally. This garauntees
/// the string remains in scope until the PsqlBindArray is destroyed,
/// without the caller maintaining the string values.
///
/// @param value bool value to add.
void bindString(const std::string& str);
//std::vector<const std::string> getBoundStrs() {
std::vector<StringPtr> getBoundStrs() {
return (bound_strs_);
}
/// @brief Dumps the contents of the array to a string.
/// @return std::string containing the dump
std::string toText() const;
private:
/// @brief vector of strings which supplied the values
std::vector<StringPtr> bound_strs_;
};
/// @brief Defines a smart pointer to PsqlBindArray
typedef boost::shared_ptr<PsqlBindArray> PsqlBindArrayPtr;
/// @brief Base class for marshalling data to and from PostgreSQL.
///
/// Provides the common functionality to set up binding information between
......@@ -179,7 +254,7 @@ public:
void getColumnValue(const PgSqlResult& r, const int row, const size_t col,
bool &value) const;
/// @brief Fetches an integer text column as a uint32_t.
/// @brief Fetches an integer text column as a uint8_t.
///
/// @param r the result set containing the query results
/// @param row the row number within the result set
......@@ -189,21 +264,12 @@ public:
/// @throw DbOperationError if the value cannot be fetched or is
/// invalid.
void getColumnValue(const PgSqlResult& r, const int row, const size_t col,
uint32_t &value) const;
uint8_t &value) const;
/// @brief Fetches an integer text column as a int32_t.
/// @brief Fetches a text column as the given value type
///
/// @param r the result set containing the query results
/// @param row the row number within the result set
/// @param col the column number within the row
/// @param[out] value parameter to receive the converted value
///
/// @throw DbOperationError if the value cannot be fetched or is
/// invalid.
void getColumnValue(const PgSqlResult& r, const int row, const size_t col,
int32_t &value) const;
/// @brief Fetches an integer text column as a uint8_t.
/// Uses boost::lexicalcast to convert the text column value into
/// a value of type T.
///
/// @param r the result set containing the query results
/// @param row the row number within the result set
......@@ -212,8 +278,18 @@ public:
///
/// @throw DbOperationError if the value cannot be fetched or is
/// invalid.
template<typename T>
void getColumnValue(const PgSqlResult& r, const int row, const size_t col,
uint8_t &value) const;
T& value) const {
const char* data = getRawColumnValue(r, row, col);
try {
value = boost::lexical_cast<T>(data);
} catch (const std::exception& ex) {
isc_throw(DbOperationError, "Invalid data: " << data
<< " for: " << getColumnLabel(col) << " row:" << row
<< " : " << ex.what());
}
}
/// @brief Converts a column in a row in a result set to a binary bytes
///
......
// Copyright (C) 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
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#include <config.h>
#include <dhcp/libdhcp++.h>
#include <dhcp/option.h>
#include <dhcp/option_definition.h>
#include <dhcp/option_space.h>
#include <dhcpsrv/cfg_option.h>
#include <dhcpsrv/dhcpsrv_log.h>
#include <dhcpsrv/pgsql_host_data_source.h>
#include <dhcpsrv/db_exceptions.h>
#include <util/buffer.h>
#include <util/optional_value.h>
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/classification.hpp>
#include <boost/pointer_cast.hpp>
#include <boost/static_assert.hpp>
#include <stdint.h>
#include <string>
using namespace isc;
using namespace isc::asiolink;
using namespace isc::dhcp;
using namespace isc::util;
using namespace std;
namespace {
#if 0
/// @brief Maximum size of an IPv6 address represented as a text string.
///
/// This is 32 hexadecimal characters written in 8 groups of four, plus seven
/// colon separators.
const size_t ADDRESS6_TEXT_MAX_LEN = 39;
/// @brief Maximum length of classes stored in a dhcp4/6_client_classes
/// columns.
const size_t CLIENT_CLASSES_MAX_LEN = 255;
/// @brief Maximum length of the hostname stored in DNS.
///
/// This length is restricted by the length of the domain-name carried
/// in the Client FQDN %Option (see RFC4702 and RFC4704).
const size_t HOSTNAME_MAX_LEN = 255;
/// @brief Maximum length of option value.
const size_t OPTION_VALUE_MAX_LEN = 4096;
/// @brief Maximum length of option value specified in textual format.
const size_t OPTION_FORMATTED_VALUE_MAX_LEN = 8192;
/// @brief Maximum length of option space name.
const size_t OPTION_SPACE_MAX_LEN = 128;
#endif
/// @brief Numeric value representing last supported identifier.
///
/// This value is used to validate whether the identifier type stored in
/// a database is within bounds. of supported identifiers.
const uint8_t MAX_IDENTIFIER_TYPE = static_cast<uint8_t>(Host::IDENT_CIRCUIT_ID);
/// @brief Maximum length of DHCP identifier value.
const size_t DHCP_IDENTIFIER_MAX_LEN = 128;
/// @brief This class provides mechanisms for sending and retrieving
/// information from the 'hosts' table.
///
/// This class is used to insert and retrieve entries from the 'hosts' table.
/// The queries used with this class do not retrieve IPv6 reservations or
/// options associated with a host to minimize impact on performance. Other
/// classes derived from @ref PgSqlHostExchange should be used to retrieve
/// information about IPv6 reservations and options.
class PgSqlHostExchange : public PgSqlExchange {
private:
/// @brief Column numbers for each column in the hosts table.
/// These are used for both retrieving data and for looking up
/// column labels for logging. Note that their numeric order
/// MUST match that of the column order in the hosts table.
static const int HOST_ID_COL = 0;
static const int DHCP_IDENTIFIER_COL = 1;
static const int DHCP_IDENTIFIER_TYPE_COL = 2;
static const int DHCP4_SUBNET_ID_COL = 3;
static const int DHCP6_SUBNET_ID_COL = 4;
static const int IPV4_ADDRESS_COL = 5;
static const int HOSTNAME_COL = 6;
static const int DHCP4_CLIENT_CLASSES_COL = 7;
static const int DHCP6_CLIENT_CLASSES_COL = 8;
/// @brief Number of columns returned for SELECT queries send by this class.
static const size_t HOST_COLUMNS = 9;
public:
/// @brief Constructor
///
/// @param additional_columns_num This value is set by the derived classes
/// to indicate how many additional columns will be returned by SELECT
/// queries performed by the derived class. This constructor will allocate
/// resources for these columns, e.g. binding table, error indicators.
PgSqlHostExchange(const size_t additional_columns_num = 0)
: columns_num_(HOST_COLUMNS + additional_columns_num),
columns_(columns_num_), host_id_(0) {
// Set the column names for use by this class. This only comprises
// names used by the PgSqlHostExchange class. Derived classes will
// need to set names for the columns they use. Currenty these are
// only used for logging purposes.
columns_[HOST_ID_COL] = "host_id";
columns_[DHCP_IDENTIFIER_COL] = "dhcp_identifier";
columns_[DHCP_IDENTIFIER_TYPE_COL] = "dhcp_identifier_type";
columns_[DHCP4_SUBNET_ID_COL] = "dhcp4_subnet_id";
columns_[DHCP6_SUBNET_ID_COL] = "dhcp6_subnet_id";
columns_[IPV4_ADDRESS_COL] = "ipv4_address";
columns_[HOSTNAME_COL] = "hostname";
columns_[DHCP4_CLIENT_CLASSES_COL] = "dhcp4_client_classes";
columns_[DHCP6_CLIENT_CLASSES_COL] = "dhcp6_client_classes";
BOOST_STATIC_ASSERT(8 < HOST_COLUMNS);
};
/// @brief Virtual destructor.
virtual ~PgSqlHostExchange() {
}
/// @brief Returns index of the first uninitialized column name.
///
/// This method is called by the derived classes to determine which
/// column indexes are available for the derived classes within a
/// binding array, error array and column names. This method
/// determines the first available index by searching the first
/// empty value within the columns_ vector. Previously we relied on
/// the fixed values set for each class, but this was hard to maintain
/// when new columns were added to the SELECT queries. It required
/// modifying indexes in all derived classes.
///
/// Derived classes must call this method in their constructors and
/// use returned value as an index for the first column used by the
/// derived class and increment this value for each subsequent column.
size_t findAvailColumn() const {
std::vector<std::string>::const_iterator empty_column =
std::find(columns_.begin(), columns_.end(), std::string());
return (std::distance(columns_.begin(), empty_column));
}
/// @todo TKM Not certain this is actually needed ....
/// @brief Returns value of host id.
///
/// This method is used by derived classes.
uint64_t getHostId() const {
return (host_id_);
};
/// @brief Populate a bind array from a host
///
/// Constructs a PsqlBindArray for sending data stored in a Host object
/// to the database.
///
/// @param host Host object to be added to the database.
/// None of the fields in the host reservation are modified -
/// the host data is only read.
///
/// @return pointer to newly constructed bind_array containing the
/// bound values extracted from host
///
/// @throw DbOperationError if bind_array cannot be populated.
PsqlBindArrayPtr
createBindForSend(const HostPtr& host) {
if (!host) {
isc_throw(BadValue, "createBindForSend:: host object is NULL");
}
// Store the host to ensure bound values remain in scope
host_ = host;
// Bind the host data to the array
PsqlBindArrayPtr bind_array(new PsqlBindArray());
try {
// host_id : is auto_incremented skip it
// dhcp_identifier : BYTEA NOT NULL
bind_array->add(host->getIdentifier());
// dhcp_identifier_type : SMALLINT NOT NULL
bind_array->add(host->getIdentifierType());
// dhcp4_subnet_id : INT NULL
bind_array->add(host->getIPv4SubnetID());
// dhcp6_subnet_id : INT NULL
bind_array->add(host->getIPv6SubnetID());
// ipv4_address : BIGINT NULL
bind_array->add(host->getIPv4Reservation());
// hostname : VARCHAR(255) NULL
bind_array->bindString(host->getHostname());
// dhcp4_client_classes : VARCHAR(255) NULL
// Override default separator to not include space after comma.
bind_array->bindString(host->getClientClasses4().toText(","));
// dhcp6_client_classes : VARCHAR(255) NULL
bind_array->bindString(host->getClientClasses6().toText(","));
} catch (const std::exception& ex) {
host_.reset();
isc_throw(DbOperationError,
"Could not create bind array from Host: "
<< host->getHostname() << ", reason: " << ex.what());
}
return (bind_array);
};
/// @brief Creates a Host object from a given row in a result set.