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.
///
/// @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
/// @brief Fetches a text column as the given value type
///
/// @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
///
......
This diff is collapsed.
// 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/.
#ifndef PGSQL_HOST_DATA_SOURCE_H
#define PGSQL_HOST_DATA_SOURCE_H
#include <dhcpsrv/base_host_data_source.h>
#include <dhcpsrv/pgsql_connection.h>
#include <dhcpsrv/pgsql_exchange.h>
namespace isc {
namespace dhcp {
/// Forward declaration to the implementation of the @ref PgSqlHostDataSource.
class PgSqlHostDataSourceImpl;
/// @brief PostgreSQL Host Data Source
///
/// This class implements the @ref isc::dhcp::BaseHostDataSource interface to
/// the PostgreSQL database. Use of this backend presupposes that a PostgreSQL
/// database is available and that the Kea schema has been created within it.
class PgSqlHostDataSource: public BaseHostDataSource {
public:
/// @brief Constructor
///
/// Uses the following keywords in the parameters passed to it to
/// connect to the database:
/// - name - Name of the database to which to connect (mandatory)
/// - host - Host to which to connect (optional, defaults to "localhost")
/// - user - Username under which to connect (optional)
/// - password - Password for "user" on the database (optional)
///
/// If the database is successfully opened, the version number in the
/// schema_version table will be checked against hard-coded value in
/// the implementation file.
///
/// Finally, all the SQL commands are pre-compiled.
///
/// @param parameters A data structure relating keywords and values
/// concerned with the database.
///
/// @throw isc::dhcp::NoDatabaseName Mandatory database name not given
/// @throw isc::dhcp::DbOpenError Error opening the database
/// @throw isc::dhcp::DbOperationError An operation on the open database has
/// failed.
PgSqlHostDataSource(const DatabaseConnection::ParameterMap& parameters);
/// @brief Virtual destructor.
///
/// Releases prepared MySQL statements used by the backend.
virtual ~PgSqlHostDataSource();
/// @brief Return all hosts for the specified HW address or DUID.
///
/// This method returns all @c Host objects which represent reservations
/// for the specified HW address or DUID. Note, that this method may
/// return multiple reservations because a particular client may have
/// reservations in multiple subnets and the same client may be identified
/// by HW address or DUID. The server is unable to verify that the specific
/// DUID and HW address belong to the same client, until the client sends
/// a DHCP message.
///
/// Specifying both hardware address and DUID is allowed for this method
/// and results in returning all objects that are associated with hardware
/// address OR duid. For example: if one host is associated with the
/// specified hardware address and another host is associated with the
/// specified DUID, two hosts will be returned.
///
/// @param hwaddr HW address of the client or NULL if no HW address
/// available.
/// @param duid client id or NULL if not available, e.g. DHCPv4 client case.
///
/// @return Collection of const @c Host objects.
virtual ConstHostCollection
getAll(const HWAddrPtr& hwaddr, const DuidPtr& duid = DuidPtr()) const;
/// @brief Return all hosts connected to any subnet for which reservations
/// have been made using a specified identifier.
///
/// This method returns all @c Host objects which represent reservations
/// for a specified identifier. This method may return multiple hosts
/// because a particular client may have reservations in multiple subnets.
///
/// @param identifier_type Identifier type.
/// @param identifier_begin Pointer to a begining of a buffer containing
/// an identifier.
/// @param identifier_len Identifier length.
///
/// @return Collection of const @c Host objects.
virtual ConstHostCollection
getAll(const Host::IdentifierType& identifier_type,
const uint8_t* identifier_begin, const size_t identifier_len) const;
/// @brief Returns a collection of hosts using the specified IPv4 address.
///
/// This method may return multiple @c Host objects if they are connected
/// to different subnets.
///
/// @param address IPv4 address for which the @c Host object is searched.
///
/// @return Collection of const @c Host objects.
virtual ConstHostCollection
getAll4(const asiolink::IOAddress& address) const;
/// @brief Returns a host connected to the IPv4 subnet.
///
/// Implementations of this method should guard against the case when
/// mutliple instances of the @c Host are present, e.g. when two
/// @c Host objects are found, one for the DUID, another one for the
/// HW address. In such case, an implementation of this method
/// should throw an MultipleRecords exception.
///
/// @param subnet_id Subnet identifier.
/// @param hwaddr HW address of the client or NULL if no HW address
/// available.
/// @param duid client id or NULL if not available.
///
/// @return Const @c Host object using a specified HW address or DUID.
virtual ConstHostPtr
get4(const SubnetID& subnet_id, const HWAddrPtr& hwaddr,
const DuidPtr& duid = DuidPtr()) const;
/// @brief Returns a host connected to the IPv4 subnet.
///
/// @param subnet_id Subnet identifier.
/// @param identifier_type Identifier type.
/// @param identifier_begin Pointer to a begining of a buffer containing
/// an identifier.
/// @param identifier_len Identifier length.
///
/// @return Const @c Host object for which reservation has been made using
/// the specified identifier.
virtual ConstHostPtr
get4(const SubnetID& subnet_id, const Host::IdentifierType& identifier_type,
const uint8_t* identifier_begin, const size_t identifier_len) const;
/// @brief Returns a host connected to the IPv4 subnet and having
/// a reservation for a specified IPv4 address.
///
/// One of the use cases for this method is to detect collisions between
/// dynamically allocated addresses and reserved addresses. When the new
/// address is assigned to a client, the allocation mechanism should check
/// if this address is not reserved for some other host and do not allocate
/// this address if reservation is present.
///
/// Implementations of this method should guard against invalid addresses,
/// such as IPv6 address.
///
/// @param subnet_id Subnet identifier.
/// @param address reserved IPv4 address.
///
/// @return Const @c Host object using a specified IPv4 address.
virtual ConstHostPtr
get4(const SubnetID& subnet_id, const asiolink::IOAddress& address) const;
/// @brief Returns a host connected to the IPv6 subnet.
///
/// Implementations of this method should guard against the case when
/// mutliple instances of the @c Host are present, e.g. when two
/// @c Host objects are found, one for the DUID, another one for the
/// HW address. In such case, an implementation of this method
/// should throw an MultipleRecords exception.
///
/// @param subnet_id Subnet identifier.
/// @param hwaddr HW address of the client or NULL if no HW address
/// available.
/// @param duid DUID or NULL if not available.
///
/// @return Const @c Host object using a specified HW address or DUID.
virtual ConstHostPtr
get6(const SubnetID& subnet_id, const DuidPtr& duid,
const HWAddrPtr& hwaddr = HWAddrPtr()) const;
/// @brief Returns a host connected to the IPv6 subnet.
///
/// @param subnet_id Subnet identifier.
/// @param identifier_type Identifier type.
/// @param identifier_begin Pointer to a begining of a buffer containing
/// an identifier.
/// @param identifier_len Identifier length.
///
/// @return Const @c Host object for which reservation has been made using
/// the specified identifier.
virtual ConstHostPtr
get6(const SubnetID& subnet_id, const Host::IdentifierType& identifier_type,
const uint8_t* identifier_begin, const size_t identifier_len) const;
/// @brief Returns a host using the specified IPv6 prefix.
///
/// @param prefix IPv6 prefix for which the @c Host object is searched.
/// @param prefix_len IPv6 prefix length.
///
/// @return Const @c Host object using a specified HW address or DUID.
virtual ConstHostPtr
get6(const asiolink::IOAddress& prefix, const uint8_t prefix_len) const;
/// @brief Adds a new host to the collection.
///
/// The implementations of this method should guard against duplicate
/// reservations for the same host, where possible. For example, when the
/// reservation for the same HW address and subnet id is added twice, the
/// addHost method should throw an DuplicateEntry exception. Note, that
/// usually it is impossible to guard against adding duplicated host, where
/// one instance is identified by HW address, another one by DUID.
///
/// @param host Pointer to the new @c Host object being added.
virtual void add(const HostPtr& host);
/// @brief Return backend type
///
/// Returns the type of the backend (e.g. "mysql", "memfile" etc.)
///
/// @return Type of the backend.
virtual std::string getType() const {
return (std::string("mysql"));
}