Commit b1c67df1 authored by Thomas Markwalder's avatar Thomas Markwalder

[4276] Initial impl of PgSqlConnection class

    Initial refactoring of Postgresql connection logic out of
    PgSqlLeaseMgr into new PgSqlConnection.
parent 8745f651
......@@ -133,6 +133,7 @@ endif
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_lease_mgr.cc pgsql_lease_mgr.h
endif
libkea_dhcpsrv_la_SOURCES += pool.cc pool.h
......
// 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 <dhcpsrv/dhcpsrv_log.h>
#include <dhcpsrv/pgsql_connection.h>
#include <boost/static_assert.hpp>
#include <iostream>
#include <iomanip>
#include <limits>
#include <sstream>
#include <string>
#include <time.h>
// PostgreSQL errors should be tested based on the SQL state code. Each state
// code is 5 decimal, ASCII, digits, the first two define the category of
// error, the last three are the specific error. PostgreSQL makes the state
// code as a char[5]. Macros for each code are defined in PostgreSQL's
// server/utils/errcodes.h, although they require a second macro,
// MAKE_SQLSTATE for completion. For example, duplicate key error as:
//
// #define ERRCODE_UNIQUE_VIOLATION MAKE_SQLSTATE('2','3','5','0','5')
//
// PostgreSQL deliberately omits the MAKE_SQLSTATE macro so callers can/must
// supply their own. We'll define it as an initlizer_list:
#define MAKE_SQLSTATE(ch1,ch2,ch3,ch4,ch5) {ch1,ch2,ch3,ch4,ch5}
// So we can use it like this: const char some_error[] = ERRCODE_xxxx;
#define PGSQL_STATECODE_LEN 5
#include <utils/errcodes.h>
using namespace std;
namespace isc {
namespace dhcp {
const char PgSqlConnection::DUPLICATE_KEY[] = ERRCODE_UNIQUE_VIOLATION;
PgSqlConnection::~PgSqlConnection() {
if (conn_) {
// Deallocate the prepared queries.
PgSqlResult r(PQexec(conn_, "DEALLOCATE all"));
if(PQresultStatus(r) != PGRES_COMMAND_OK) {
// Highly unlikely but we'll log it and go on.
LOG_ERROR(dhcpsrv_logger, DHCPSRV_PGSQL_DEALLOC_ERROR)
.arg(PQerrorMessage(conn_));
}
}
}
void
PgSqlConnection::prepareStatement(const PgSqlTaggedStatement& statement) {
// Prepare all statements queries with all known fields datatype
PgSqlResult r(PQprepare(conn_, statement.name, statement.text,
statement.nbparams, statement.types));
if(PQresultStatus(r) != PGRES_COMMAND_OK) {
isc_throw(DbOperationError, "unable to prepare PostgreSQL statement: "
<< statement.text << ", reason: " << PQerrorMessage(conn_));
}
}
void
PgSqlConnection::openDatabase() {
string dbconnparameters;
string shost = "localhost";
try {
shost = getParameter("host");
} catch(...) {
// No host. Fine, we'll use "localhost"
}
dbconnparameters += "host = '" + shost + "'" ;
string suser;
try {
suser = getParameter("user");
dbconnparameters += " user = '" + suser + "'";
} catch(...) {
// No user. Fine, we'll use NULL
}
string spassword;
try {
spassword = getParameter("password");
dbconnparameters += " password = '" + spassword + "'";
} catch(...) {
// No password. Fine, we'll use NULL
}
string sname;
try {
sname = getParameter("name");
dbconnparameters += " dbname = '" + sname + "'";
} catch(...) {
// No database name. Throw a "NoDatabaseName" exception
isc_throw(NoDatabaseName, "must specify a name for the database");
}
// Connect to Postgres, saving the low level connection pointer
// in the holder object
PGconn* new_conn = PQconnectdb(dbconnparameters.c_str());
if (!new_conn) {
isc_throw(DbOpenError, "could not allocate connection object");
}
if (PQstatus(new_conn) != CONNECTION_OK) {
// If we have a connection object, we have to call finish
// to release it, but grab the error message first.
std::string error_message = PQerrorMessage(new_conn);
PQfinish(new_conn);
isc_throw(DbOpenError, error_message);
}
// We have a valid connection, so let's save it to our holder
conn_.setConnection(new_conn);
}
bool
PgSqlConnection::compareError(PGresult*& r, const char* error_state) {
const char* sqlstate = PQresultErrorField(r, PG_DIAG_SQLSTATE);
// PostgreSQL garuantees it will always be 5 characters long
return ((sqlstate != NULL) &&
(memcmp(sqlstate, error_state, PGSQL_STATECODE_LEN) == 0));
}
void
PgSqlConnection::checkStatementError(PGresult*& r,
PgSqlTaggedStatement& statement) const {
int s = PQresultStatus(r);
if (s != PGRES_COMMAND_OK && s != PGRES_TUPLES_OK) {
// We're testing the first two chars of SQLSTATE, as this is the
// error class. Note, there is a severity field, but it can be
// misleadingly returned as fatal.
const char* sqlstate = PQresultErrorField(r, PG_DIAG_SQLSTATE);
if ((sqlstate != NULL) &&
((memcmp(sqlstate, "08", 2) == 0) || // Connection Exception
(memcmp(sqlstate, "53", 2) == 0) || // Insufficient resources
(memcmp(sqlstate, "54", 2) == 0) || // Program Limit exceeded
(memcmp(sqlstate, "57", 2) == 0) || // Operator intervention
(memcmp(sqlstate, "58", 2) == 0))) { // System error
LOG_ERROR(dhcpsrv_logger, DHCPSRV_PGSQL_FATAL_ERROR)
.arg(statement.name)
.arg(PQerrorMessage(conn_))
.arg(sqlstate);
exit (-1);
}
const char* error_message = PQerrorMessage(conn_);
isc_throw(DbOperationError, "Statement exec failed:" << " for: "
<< statement.name << ", reason: "
<< error_message);
}
}
void
PgSqlConnection::commit() {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_COMMIT);
PgSqlResult r(PQexec(conn_, "COMMIT"));
if (PQresultStatus(r) != PGRES_COMMAND_OK) {
const char* error_message = PQerrorMessage(conn_);
isc_throw(DbOperationError, "commit failed: " << error_message);
}
}
void
PgSqlConnection::rollback() {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_ROLLBACK);
PgSqlResult r(PQexec(conn_, "ROLLBACK"));
if (PQresultStatus(r) != PGRES_COMMAND_OK) {
const char* error_message = PQerrorMessage(conn_);
isc_throw(DbOperationError, "rollback failed: " << error_message);
}
}
}; // end of isc::dhcp namespace
}; // end of isc namespace
// 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_CONNECTION_H
#define PGSQL_CONNECTION_H
#include <dhcpsrv/database_connection.h>
#include <libpq-fe.h>
#include <boost/scoped_ptr.hpp>
#include <vector>
namespace isc {
namespace dhcp {
// Maximum number of parameters that can be used a statement
// @todo This allows us to use an initializer list (since we don't
// require C++11). It's unlikely we'd go past in a single statement.
const size_t PGSQL_MAX_PARAMETERS_IN_QUERY = 32;
/// @brief Defines a Postgresql SQL statement
///
/// 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;
/// @brief OID types
///
/// Specify parameter types. See /usr/include/postgresql/catalog/pg_type.h.
/// For some reason that header does not export those parameters.
/// Those OIDs must match both input and output parameters.
const Oid types[PGSQL_MAX_PARAMETERS_IN_QUERY];
/// Short name of the query.
const char* name;
/// Text representation of the actual query.
const char* text;
};
/// @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.
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_VARCHAR = 1043;
//@}
/// @brief RAII wrapper for Posgtresql Result sets
///
/// When a Postgresql statement is executed, the results are returned
/// in pointer allocated structure, PGresult*. Data and status information
/// are accessed via calls to functions such as PQgetvalue() which require
/// the results pointer. In order to ensure this structure is freed, any
/// invocation of Psql function which returns a PGresult* (e.g. PQexec and
/// class. Examples:
/// {{{
/// PgSqlResult r(PQexec(conn_, "ROLLBACK"));
/// }}}
///
/// This eliminates the need for an explicit release via, PQclear() and
/// guarantees that the resources are released even if the an exception is
/// thrown.
class PgSqlResult {
public:
/// @brief Constructor
///
/// Store the pointer to the result set to being fetched.
///
PgSqlResult(PGresult *result) : result_(result)
{}
/// @brief Destructor
///
/// Frees the result set
~PgSqlResult() {
if (result_) {
PQclear(result_);
}
}
/// @brief Conversion Operator
///
/// Allows the PgSqlResult object to be passed as the context argument to
/// PQxxxx functions.
operator PGresult*() const {
return (result_);
}
/// @brief Boolean Operator
///
/// Allows testing the PgSqlResult object for emptiness: "if (result)"
operator bool() const {
return (result_);
}
private:
PGresult* result_; ///< Result set to be freed
};
/// @brief PgSql Handle Holder
///
/// Small RAII object for safer initialization, will close the database
/// connection upon destruction. This means that if an exception is thrown
/// during database initialization, resources allocated to the database are
/// guaranteed to be freed.
///
/// It makes no sense to copy an object of this class. After the copy, both
/// objects would contain pointers to the same PgSql context object. The
/// destruction of one would invalid the context in the remaining object.
/// For this reason, the class is declared noncopyable.
class PgSqlHolder : public boost::noncopyable {
public:
/// @brief Constructor
///
/// Initialize PgSql
///
PgSqlHolder() : pgconn_(NULL) {
}
/// @brief Destructor
///
/// Frees up resources allocated by the connection.
~PgSqlHolder() {
if (pgconn_ != NULL) {
PQfinish(pgconn_);
}
}
void setConnection(PGconn* connection) {
if (pgconn_ != NULL) {
// Already set? Release the current connection first.
// Maybe this should be an error instead?
PQfinish(pgconn_);
}
pgconn_ = connection;
}
/// @brief Conversion Operator
///
/// Allows the PgSqlHolder object to be passed as the context argument to
/// PQxxxx functions.
operator PGconn*() const {
return (pgconn_);
}
/// @brief Boolean Operator
///
/// Allows testing the connection for emptiness: "if (holder)"
operator bool() const {
return (pgconn_);
}
private:
PGconn* pgconn_; ///< Postgresql connection
};
/// @brief Common PgSql Connector Pool
///
/// This class provides common operations for PgSql database connection
/// used by both PgSqlLeaseMgr and PgSqlHostDataSource. It manages connecting
/// to the database and preparing compiled statements. Its fields are
/// public, because they are used (both set and retrieved) in classes
/// that use instances of PgSqlConnection.
class PgSqlConnection : public DatabaseConnection {
public:
/// @brief Defines the PgSql error state for a duplicate key error
static const char DUPLICATE_KEY[];
/// @brief Constructor
///
/// Initialize PgSqlConnection object with parameters needed for connection.
PgSqlConnection(const ParameterMap& parameters)
: DatabaseConnection(parameters) {
}
/// @brief Destructor
virtual ~PgSqlConnection();
/// @brief Prepare Single Statement
///
/// Creates a prepared statement from the text given and adds it to the
/// statements_ vector at the given index.
///
/// @param index Index into the statements_ vector into which the text
/// should be placed. The vector must be big enough for the index
/// to be valid, else an exception will be thrown.
/// @param text Text of the SQL statement to be prepared.
///
/// @throw isc::dhcp::DbOperationError An operation on the open database has
/// failed.
void prepareStatement(const PgSqlTaggedStatement& statement);
/// @brief Open Database
///
/// Opens the database using the information supplied in the parameters
/// passed to the constructor.
///
/// @throw NoDatabaseName Mandatory database name not given
/// @throw DbOpenError Error opening the database
void openDatabase();
/// @brief Commit Transactions
///
/// Commits all pending database operations. On databases that don't
/// support transactions, this is a no-op.
///
/// @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.
///
/// @throw DbOperationError If the rollback failed.
void rollback();
/// @brief Checks a result set's SQL state against an error state.
///
/// @param r result set to check
/// @param error_state error state to compare against
///
/// @return True if the result set's SQL state equals the error_state,
/// false otherwise.
bool compareError(PGresult*& r, const char* error_state);
/// @brief Checks result of the r object
///
/// This function is used to determine whether or not the SQL statement
/// execution succeeded, and in the event of failures, decide whether or
/// not the failures are recoverable.
///
/// If the error is recoverable, the method will throw a DbOperationError.
/// In the error is deemed unrecoverable, such as a loss of connectivity
/// with the server, this method will log the error and call exit(-1);
///
/// @todo Calling exit() is viewed as a short term solution for Kea 1.0.
/// Two tickets are likely to alter this behavior, first is #3639, which
/// calls for the ability to attempt to reconnect to the database. The
/// second ticket, #4087 which calls for the implementation of a generic,
/// FatalException class which will propagate outward.
///
/// @param r result of the last PostgreSQL operation
/// @param statement - tagged statement that was executed
///
/// @throw isc::dhcp::DbOperationError Detailed PostgreSQL failure
void checkStatementError(PGresult*& r, PgSqlTaggedStatement& statement) const;
/// @brief PgSql connection handle
///
/// This field is public, because it is used heavily from PgSqlLeaseMgr
/// and from PgSqlHostDataSource.
PgSqlHolder conn_;
/// @brief Conversion Operator
///
/// Allows the PgConnection object to be passed as the context argument to
/// PQxxxx functions.
operator PGconn*() const {
return (conn_);
}
/// @brief Boolean Operator
///
/// Allows testing the PgConnection for initialized connection
operator bool() const {
return (conn_);
}
};
}; // end of isc::dhcp namespace
}; // end of isc namespace
#endif // PGSQL_CONNECTION_H
......@@ -21,64 +21,16 @@
#include <string>
#include <time.h>
// PostgreSQL errors should be tested based on the SQL state code. Each state
// code is 5 decimal, ASCII, digits, the first two define the category of
// error, the last three are the specific error. PostgreSQL makes the state
// code as a char[5]. Macros for each code are defined in PostgreSQL's
// errorcodes.h, although they require a second macro, MAKE_SQLSTATE for
// completion. PostgreSQL deliberately omits this macro from errocodes.h
// so callers can supply their own.
#define MAKE_SQLSTATE(ch1,ch2,ch3,ch4,ch5) {ch1,ch2,ch3,ch4,ch5}
#include <utils/errcodes.h>
const size_t STATECODE_LEN = 5;
// Currently the only one we care to look for is duplicate key.
const char DUPLICATE_KEY[] = ERRCODE_UNIQUE_VIOLATION;
using namespace isc;
using namespace isc::dhcp;
using namespace std;
namespace {
// Maximum number of parameters used in any single query
const size_t MAX_PARAMETERS_IN_QUERY = 14;
/// @brief Defines a single query
struct TaggedStatement {
/// Number of parameters for a given query
int nbparams;
/// @brief OID types
///
/// Specify parameter types. See /usr/include/postgresql/catalog/pg_type.h.
/// For some reason that header does not export those parameters.
/// Those OIDs must match both input and output parameters.
const Oid types[MAX_PARAMETERS_IN_QUERY];
/// Short name of the query.
const char* name;
/// Text representation of the actual query.
const char* text;
};
/// @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.
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_VARCHAR = 1043;
/// @brief Catalog of all the SQL statements currently supported. Note
/// that the order columns appear in statement body must match the order they
/// that the occur in the table. This does not apply to the where clause.
TaggedStatement tagged_statements[] = {
PgSqlTaggedStatement tagged_statements[] = {
// DELETE_LEASE4
{ 1, { OID_INT8 },
"delete_lease4",
......@@ -1039,25 +991,12 @@ private:
PgSqlLeaseMgr::PgSqlLeaseMgr(const DatabaseConnection::ParameterMap& parameters)
: LeaseMgr(), exchange4_(new PgSqlLease4Exchange()),
exchange6_(new PgSqlLease6Exchange()), dbconn_(parameters), conn_(NULL) {
openDatabase();
exchange6_(new PgSqlLease6Exchange()), conn_(parameters) {
conn_.openDatabase();
prepareStatements();
}
PgSqlLeaseMgr::~PgSqlLeaseMgr() {
if (conn_) {
// Deallocate the prepared queries.
PGresult* r = PQexec(conn_, "DEALLOCATE all");
if(PQresultStatus(r) != PGRES_COMMAND_OK) {
// Highly unlikely but we'll log it and go on.
LOG_ERROR(dhcpsrv_logger, DHCPSRV_PGSQL_DEALLOC_ERROR)
.arg(PQerrorMessage(conn_));
}
PQclear(r);
PQfinish(conn_);
conn_ = NULL;
}
}
std::string
......@@ -1072,73 +1011,7 @@ PgSqlLeaseMgr::getDBVersion() {
void
PgSqlLeaseMgr::prepareStatements() {
for(int i = 0; tagged_statements[i].text != NULL; ++ i) {
// Prepare all statements queries with all known fields datatype
PGresult* r = PQprepare(conn_, tagged_statements[i].name,
tagged_statements[i].text,
tagged_statements[i].nbparams,
tagged_statements[i].types);
if(PQresultStatus(r) != PGRES_COMMAND_OK) {
PQclear(r);
isc_throw(DbOperationError,
"unable to prepare PostgreSQL statement: "
<< tagged_statements[i].text << ", reason: "
<< PQerrorMessage(conn_));
}
PQclear(r);
}
}
void
PgSqlLeaseMgr::openDatabase() {
string dbconnparameters;
string shost = "localhost";
try {
shost = dbconn_.getParameter("host");
} catch(...) {
// No host. Fine, we'll use "localhost"
}
dbconnparameters += "host = '" + shost + "'" ;
string suser;
try {
suser = dbconn_.getParameter("user");
dbconnparameters += " user = '" + suser + "'";
} catch(...) {
// No user. Fine, we'll use NULL
}
string spassword;
try {
spassword = dbconn_.getParameter("password");
dbconnparameters += " password = '" + spassword + "'";
} catch(...) {
// No password. Fine, we'll use NULL
}
string sname;
try {
sname= dbconn_.getParameter("name");
dbconnparameters += " dbname = '" + sname + "'";
} catch(...) {
// No database name. Throw a "NoDatabaseName" exception
isc_throw(NoDatabaseName, "must specify a name for the database");
}
conn_ = PQconnectdb(dbconnparameters.c_str());
if (conn_ == NULL) {
isc_throw(DbOpenError, "could not allocate connection object");
}
if (PQstatus(conn_) != CONNECTION_OK) {
// If we have a connection object, we have to call finish
// to release it, but grab the error message first.
std::string error_message = PQerrorMessage(conn_);
PQfinish(conn_);