// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. #include #include #include #include #include #include #include #include #include #include #include #include // 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 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 = 13; /// @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 , 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[] = { // DELETE_LEASE4 { 1, { OID_INT8 }, "delete_lease4", "DELETE FROM lease4 WHERE address = $1"}, // DELETE_LEASE6 { 1, { OID_VARCHAR }, "delete_lease6", "DELETE FROM lease6 WHERE address = $1"}, // GET_LEASE4_ADDR { 1, { OID_INT8 }, "get_lease4_addr", "SELECT address, hwaddr, client_id, " "valid_lifetime, extract(epoch from expire)::bigint, subnet_id, " "fqdn_fwd, fqdn_rev, hostname " "FROM lease4 " "WHERE address = $1"}, // GET_LEASE4_CLIENTID { 1, { OID_BYTEA }, "get_lease4_clientid", "SELECT address, hwaddr, client_id, " "valid_lifetime, extract(epoch from expire)::bigint, subnet_id, " "fqdn_fwd, fqdn_rev, hostname " "FROM lease4 " "WHERE client_id = $1"}, // GET_LEASE4_CLIENTID_SUBID { 2, { OID_BYTEA, OID_INT8 }, "get_lease4_clientid_subid", "SELECT address, hwaddr, client_id, " "valid_lifetime, extract(epoch from expire)::bigint, subnet_id, " "fqdn_fwd, fqdn_rev, hostname " "FROM lease4 " "WHERE client_id = $1 AND subnet_id = $2"}, // GET_LEASE4_HWADDR { 1, { OID_BYTEA }, "get_lease4_hwaddr", "SELECT address, hwaddr, client_id, " "valid_lifetime, extract(epoch from expire)::bigint, subnet_id, " "fqdn_fwd, fqdn_rev, hostname " "FROM lease4 " "WHERE hwaddr = $1"}, // GET_LEASE4_HWADDR_SUBID { 2, { OID_BYTEA, OID_INT8 }, "get_lease4_hwaddr_subid", "SELECT address, hwaddr, client_id, " "valid_lifetime, extract(epoch from expire)::bigint, subnet_id, " "fqdn_fwd, fqdn_rev, hostname " "FROM lease4 " "WHERE hwaddr = $1 AND subnet_id = $2"}, // GET_LEASE6_ADDR { 2, { OID_VARCHAR, OID_INT2 }, "get_lease6_addr", "SELECT address, duid, valid_lifetime, " "extract(epoch from expire)::bigint, subnet_id, pref_lifetime, " "lease_type, iaid, prefix_len, fqdn_fwd, fqdn_rev, hostname " "FROM lease6 " "WHERE address = $1 AND lease_type = $2"}, // GET_LEASE6_DUID_IAID { 3, { OID_BYTEA, OID_INT8, OID_INT2 }, "get_lease6_duid_iaid", "SELECT address, duid, valid_lifetime, " "extract(epoch from expire)::bigint, subnet_id, pref_lifetime, " "lease_type, iaid, prefix_len, fqdn_fwd, fqdn_rev, hostname " "FROM lease6 " "WHERE duid = $1 AND iaid = $2 AND lease_type = $3"}, // GET_LEASE6_DUID_IAID_SUBID { 4, { OID_INT2, OID_BYTEA, OID_INT8, OID_INT8 }, "get_lease6_duid_iaid_subid", "SELECT address, duid, valid_lifetime, " "extract(epoch from expire)::bigint, subnet_id, pref_lifetime, " "lease_type, iaid, prefix_len, fqdn_fwd, fqdn_rev, hostname " "FROM lease6 " "WHERE lease_type = $1 " "AND duid = $2 AND iaid = $3 AND subnet_id = $4"}, // GET_VERSION { 0, { OID_NONE }, "get_version", "SELECT version, minor FROM schema_version"}, // INSERT_LEASE4 { 9, { OID_INT8, OID_BYTEA, OID_BYTEA, OID_INT8, OID_TIMESTAMP, OID_INT8, OID_BOOL, OID_BOOL, OID_VARCHAR }, "insert_lease4", "INSERT INTO lease4(address, hwaddr, client_id, " "valid_lifetime, expire, subnet_id, fqdn_fwd, fqdn_rev, hostname) " "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"}, // INSERT_LEASE6 { 12, { OID_VARCHAR, OID_BYTEA, OID_INT8, OID_TIMESTAMP, OID_INT8, OID_INT8, OID_INT2, OID_INT8, OID_INT2, OID_BOOL, OID_BOOL, OID_VARCHAR }, "insert_lease6", "INSERT INTO lease6(address, duid, valid_lifetime, " "expire, subnet_id, pref_lifetime, " "lease_type, iaid, prefix_len, fqdn_fwd, fqdn_rev, hostname) " "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)"}, // UPDATE_LEASE4 { 10, { OID_INT8, OID_BYTEA, OID_BYTEA, OID_INT8, OID_TIMESTAMP, OID_INT8, OID_BOOL, OID_BOOL, OID_VARCHAR, OID_INT8 }, "update_lease4", "UPDATE lease4 SET address = $1, hwaddr = $2, " "client_id = $3, valid_lifetime = $4, expire = $5, " "subnet_id = $6, fqdn_fwd = $7, fqdn_rev = $8, hostname = $9 " "WHERE address = $10"}, // UPDATE_LEASE6 { 13, { OID_VARCHAR, OID_BYTEA, OID_INT8, OID_TIMESTAMP, OID_INT8, OID_INT8, OID_INT2, OID_INT8, OID_INT2, OID_BOOL, OID_BOOL, OID_VARCHAR, OID_VARCHAR }, "update_lease6", "UPDATE lease6 SET address = $1, duid = $2, " "valid_lifetime = $3, expire = $4, subnet_id = $5, " "pref_lifetime = $6, lease_type = $7, iaid = $8, " "prefix_len = $9, fqdn_fwd = $10, fqdn_rev = $11, hostname = $12 " "WHERE address = $13"}, // End of list sentinel { 0, { 0 }, NULL, NULL} }; }; namespace isc { namespace dhcp { const int PsqlBindArray::TEXT_FMT = 0; const int PsqlBindArray::BINARY_FMT = 1; const char* PsqlBindArray::TRUE_STR = "TRUE"; const char* PsqlBindArray::FALSE_STR = "FALSE"; void PsqlBindArray::add(const char* value) { values_.push_back(value); lengths_.push_back(strlen(value)); formats_.push_back(TEXT_FMT); } void PsqlBindArray::add(const std::string& value) { values_.push_back(value.c_str()); lengths_.push_back(value.size()); formats_.push_back(TEXT_FMT); } void PsqlBindArray::add(const std::vector& data) { values_.push_back(reinterpret_cast(&(data[0]))); lengths_.push_back(data.size()); formats_.push_back(BINARY_FMT); } void PsqlBindArray::add(const bool& value) { add(value ? TRUE_STR : FALSE_STR); } std::string PsqlBindArray::toText() { std::ostringstream stream; for (int i = 0; i < values_.size(); ++i) { stream << i << " : "; if (formats_[i] == TEXT_FMT) { stream << "\"" << values_[i] << "\"" << std::endl; } else { const char *data = values_[i]; if (lengths_[i] == 0) { stream << "empty" << std::endl; } else { stream << "0x"; for (int i = 0; i < lengths_[i]; ++i) { stream << setfill('0') << setw(2) << setbase(16) << static_cast(data[i]); } stream << std::endl; } } } return (stream.str()); } /// @brief Base class for marshalling leases to and from PostgreSQL. /// /// Provides the common functionality to set up binding information between /// lease objects in the program and their database representation in the /// database. class PgSqlLeaseExchange { public: PgSqlLeaseExchange() : addr_str_(""), valid_lifetime_(0), valid_lft_str_(""), expire_(0), expire_str_(""), subnet_id_(0), subnet_id_str_(""), cltt_(0), fqdn_fwd_(false), fqdn_rev_(false), hostname_("") { } virtual ~PgSqlLeaseExchange(){} /// @brief Converts time_t structure to a text representation in local time. /// /// The format of the output string is "%Y-%m-%d %H:%M:%S". Database /// table columns using this value should be typed as TIMESTAMP WITH /// TIME ZONE. For such columns PostgreSQL assumes input strings without /// timezones should be treated as in local time and are converted to UTC /// when stored. Likewise, these columns are automatically adjusted /// upon retrieval unless fetched via "extract(epoch from ))". /// /// @param time_val timestamp to be converted /// @return std::string containing the stringified time std::string convertToDatabaseTime(const time_t& time_val) { // PostgreSQL does funny things with time if you get past Y2038. It // will accept the values (unlike MySQL which throws) but it // stops correctly adjusting to local time when reading them back // out. So lets disallow it here. if (time_val > LeaseMgr::MAX_DB_TIME) { isc_throw(BadValue, "Time value is too large: " << time_val); } struct tm tinfo; char buffer[20]; localtime_r(&time_val, &tinfo); strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tinfo); return (std::string(buffer)); } /// @brief Converts time stamp from the database to a time_t /// /// @param db_time_val timestamp to be converted. This value /// is expected to be the number of seconds since the epoch /// expressed as base-10 integer string. /// @return Converted timestamp as time_t value. time_t convertFromDatabaseTime(const std::string& db_time_val) { // Convert string time value to time_t try { return (boost::lexical_cast(db_time_val)); } catch (const std::exception& ex) { isc_throw(BadValue, "Database time value is invalid: " << db_time_val); } } /// @brief Gets a pointer to the raw column value in a result set row /// /// Given a result set, row, and column return a const char* pointer to /// the data value in the result set. The pointer is valid as long as /// the result set has not been freed. It may point to text or binary /// data depending on how query was structured. You should not attempt /// to free this pointer. /// /// @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 /// /// @return a const char* pointer to the column's raw data /// @throw DbOperationError if the value cannot be fetched. const char* getRawColumnValue(PGresult*& r, const int row, const size_t col) { const char* value = PQgetvalue(r, row, col); if (!value) { isc_throw(DbOperationError, "getRawColumnValue no data for :" << getColumnLabel(col) << " row:" << row); } return (value); } /// @brief Converts a column in a row in a result set to a boolean. /// /// @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(PGresult*& r, const int row, const size_t col, bool &value) { const char* data = getRawColumnValue(r, row, col); if (!strlen(data) || *data == 'f') { value = false; } else if (*data == 't') { value = true; } else { isc_throw(DbOperationError, "Invalid boolean data: " << data << " for: " << getColumnLabel(col) << " row:" << row << " : must be 't' or 'f'"); } } /// @brief Converts a column in a row in a result set to a uint32_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 /// /// @throw DbOperationError if the value cannot be fetched or is /// invalid. void getColumnValue(PGresult*& r, const int row, const size_t col, uint32_t &value) { const char* data = getRawColumnValue(r, row, col); try { value = boost::lexical_cast(data); } catch (const std::exception& ex) { isc_throw(DbOperationError, "Invalid uint32_t data: " << data << " for: " << getColumnLabel(col) << " row:" << row << " : " << ex.what()); } } /// @brief Converts a column in a row in a result set to a uint8_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 /// /// @throw DbOperationError if the value cannot be fetched or is /// invalid. void getColumnValue(PGresult*& r, const int row, const size_t col, uint8_t &value) { 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(data); } catch (const std::exception& ex) { isc_throw(DbOperationError, "Invalid uint8_t data: " << data << " for: " << getColumnLabel(col) << " row:" << row << " : " << ex.what()); } } /// @brief Converts a column in a row in a result set to a Lease6::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(PGresult*& r, const int row, const size_t col, Lease6::Type& value) { uint32_t raw_value = 0; getColumnValue(r, row , col, raw_value); switch (raw_value) { case Lease6::TYPE_NA: value = Lease6::TYPE_NA; break; case Lease6::TYPE_TA: value = Lease6::TYPE_TA; break; case Lease6::TYPE_PD: value = Lease6::TYPE_PD; break; default: isc_throw(DbOperationError, "Invalid lease type: " << raw_value << " for: " << getColumnLabel(col) << " row:" << row); } } /// @brief Converts a column in a row in a result set to a binary bytes /// /// Method is used to convert columns stored as BYTEA into a buffer of /// binary bytes, (uint8_t). It uses PQunescapeBytea to do the conversion. /// /// @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] buffer pre-allocated buffer to which the converted bytes /// will be stored. /// @param buffer_size size of the output buffer /// @param[out] bytes_converted number of bytes converted /// value /// /// @throw DbOperationError if the value cannot be fetched or is /// invalid. void convertFromBytea(PGresult*& r, const int row, const size_t col, uint8_t* buffer, const size_t buffer_size, size_t &bytes_converted) { // Returns converted bytes in a dynamically allocated buffer, and // sets bytes_converted. unsigned char* bytes = PQunescapeBytea((const unsigned char*) (getRawColumnValue(r, row, col)), &bytes_converted); // Unlikely it couldn't allocate it but you never know. if (!bytes) { isc_throw (DbOperationError, "PQunescapeBytea failed for:" << getColumnLabel(col) << " row:" << row); } // Make sure it's not larger than expected. if (bytes_converted > buffer_size) { // Free the allocated buffer first! PQfreemem(bytes); isc_throw (DbOperationError, "Converted data size: " << bytes_converted << " is too large for: " << getColumnLabel(col) << " row:" << row); } // Copy from the allocated buffer to caller's buffer the free up // the allocated buffer. memcpy(buffer, bytes, bytes_converted); PQfreemem(bytes); } /// @brief Returns column label given a column number std::string getColumnLabel(const size_t column) { if (column > columnLabels_.size()) { ostringstream os; os << "Unknown column:" << column; return (os.str()); } return (columnLabels_[column]); } protected: /// @brief Stores text labels for columns, currently only used for /// logging and errors. std::vectorcolumnLabels_; /// @brief Common Instance members used for binding and conversion //@{ std::string addr_str_; uint32_t valid_lifetime_; std::string valid_lft_str_; time_t expire_; std::string expire_str_; uint32_t subnet_id_; std::string subnet_id_str_; time_t cltt_; bool fqdn_fwd_; bool fqdn_rev_; std::string hostname_; //@} }; /// @brief Supports exchanging IPv4 leases with PostgreSQL. class PgSqlLease4Exchange : public PgSqlLeaseExchange { private: /// @brief Column numbers for each column in the Lease4 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 Lease4 table. static const size_t ADDRESS_COL = 0; static const size_t HWADDR_COL = 1; static const size_t CLIENT_ID_COL = 2; static const size_t VALID_LIFETIME_COL = 3; static const size_t EXPIRE_COL = 4; static const size_t SUBNET_ID_COL = 5; static const size_t FQDN_FWD_COL = 6; static const size_t FQDN_REV_COL = 7; static const size_t HOSTNAME_COL = 8; /// @brief Number of columns in the table holding DHCPv4 leases. static const size_t LEASE_COLUMNS = 9; public: /// @brief Default constructor PgSqlLease4Exchange() : lease_(), addr4_(0), hwaddr_length_(0), hwaddr_(hwaddr_length_), client_id_length_(0) { BOOST_STATIC_ASSERT(8 < LEASE_COLUMNS); memset(hwaddr_buffer_, 0, sizeof(hwaddr_buffer_)); memset(client_id_buffer_, 0, sizeof(client_id_buffer_)); // Set the column names (for error messages) columnLabels_.push_back("address"); columnLabels_.push_back("hwaddr"); columnLabels_.push_back("client_id"); columnLabels_.push_back("valid_lifetime"); columnLabels_.push_back("expire"); columnLabels_.push_back("subnet_id"); columnLabels_.push_back("fqdn_fwd"); columnLabels_.push_back("fqdn_rev"); columnLabels_.push_back("hostname"); } /// @brief Creates the bind array for sending Lease4 data to the database. /// /// Converts each Lease4 member into the appropriate form and adds it /// to the bind array. Note that the array additions must occur in the /// order the columns are specified in the SQL statement. By convention /// all columns in the table are explicitly listed in the SQL statement(s) /// in the same order as they occur in the table. /// /// @param lease Lease4 object that is to be written to the database /// @param[out] bind_array array to populate with the lease data values /// /// @throw DbOperationError if bind_array cannot be populated. void createBindForSend(const Lease4Ptr& lease, PsqlBindArray& bind_array) { if (!lease) { isc_throw(BadValue, "createBindForSend:: Lease4 object is NULL"); } // Store lease object to ensure it remains valid. lease_ = lease; try { addr_str_ = boost::lexical_cast (static_cast(lease->addr_)); bind_array.add(addr_str_); if (!lease->hwaddr_.empty()) { // PostgreSql does not provide MAX on variable length types // so we have to enforce it ourselves. if (lease->hwaddr_.size() > HWAddr::MAX_HWADDR_LEN) { isc_throw(DbOperationError, "Hardware address length : " << lease_->hwaddr_.size() << " exceeds maximum allowed of: " << HWAddr::MAX_HWADDR_LEN); } bind_array.add(lease->hwaddr_); } else { bind_array.add(""); } if (lease->client_id_) { bind_array.add(lease->client_id_->getClientId()); } else { bind_array.add(""); } valid_lft_str_ = boost::lexical_cast (lease->valid_lft_); bind_array.add(valid_lft_str_); expire_str_ = convertToDatabaseTime(lease->valid_lft_ + lease->cltt_); bind_array.add(expire_str_); subnet_id_str_ = boost::lexical_cast (lease->subnet_id_); bind_array.add(subnet_id_str_); bind_array.add(lease->fqdn_fwd_); bind_array.add(lease->fqdn_rev_); bind_array.add(lease->hostname_); } catch (const std::exception& ex) { isc_throw(DbOperationError, "Could not create bind array for Lease4 lease: " << lease_->addr_.toText() << " reason: " << ex.what()); } } /// @brief Creates a Lease4 object from a given row in a result set. /// /// @param r result set containing one or rows from the Lease4 table /// @param row row number within the result set from to create the Lease4 /// object. /// /// @return Lease4Ptr to the newly created Lease4 object /// @throw DbOperationError if the lease cannot be created. Lease4Ptr convertFromDatabase(PGresult*& r, int row) { try { getColumnValue(r, row, ADDRESS_COL, addr4_); convertFromBytea(r, row, HWADDR_COL, hwaddr_buffer_, sizeof(hwaddr_buffer_), hwaddr_length_); convertFromBytea(r, row, CLIENT_ID_COL, client_id_buffer_, sizeof(client_id_buffer_), client_id_length_); getColumnValue(r, row, VALID_LIFETIME_COL, valid_lifetime_); expire_ = convertFromDatabaseTime(getRawColumnValue(r, row, EXPIRE_COL)); getColumnValue(r, row , SUBNET_ID_COL, subnet_id_); cltt_ = expire_ - valid_lifetime_; getColumnValue(r, row, FQDN_FWD_COL, fqdn_fwd_); getColumnValue(r, row, FQDN_REV_COL, fqdn_rev_); hostname_ = getRawColumnValue(r, row, HOSTNAME_COL); return (Lease4Ptr(new Lease4(addr4_, hwaddr_buffer_, hwaddr_length_, client_id_buffer_, client_id_length_, valid_lifetime_, 0, 0, cltt_, subnet_id_, fqdn_fwd_, fqdn_rev_, hostname_))); } catch (const std::exception& ex) { isc_throw(DbOperationError, "Could not convert data to Lease4, reason: " << ex.what()); } } private: /// @brief Lease4 object currently being sent to the database. /// Storing this value ensures that it remains in scope while any bindings /// that refer to its contents are in use. Lease4Ptr lease_; /// @Brief Lease4 specific members used for binding and conversion. uint32_t addr4_; size_t hwaddr_length_; std::vector hwaddr_; uint8_t hwaddr_buffer_[HWAddr::MAX_HWADDR_LEN]; size_t client_id_length_; uint8_t client_id_buffer_[ClientId::MAX_CLIENT_ID_LEN]; }; /// @brief Supports exchanging IPv6 leases with PostgreSQL. class PgSqlLease6Exchange : public PgSqlLeaseExchange { private: /// @brief Column numbers for each column in the Lease6 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 Lease6 table. //@{ static const int ADDRESS_COL = 0; static const int DUID_COL = 1; static const int VALID_LIFETIME_COL = 2; static const int EXPIRE_COL = 3; static const int SUBNET_ID_COL = 4; static const int PREF_LIFETIME_COL = 5; static const int LEASE_TYPE_COL = 6; static const int IAID_COL = 7; static const int PREFIX_LEN_COL = 8; static const int FQDN_FWD_COL = 9; static const int FQDN_REV_COL = 10; static const int HOSTNAME_COL = 11; //@} /// @brief Number of columns in the table holding DHCPv4 leases. static const size_t LEASE_COLUMNS = 12; public: PgSqlLease6Exchange() : lease_(), duid_length_(0), duid_(), iaid_(0), iaid_str_(""), lease_type_(Lease6::TYPE_NA), lease_type_str_(""), prefix_len_(0), prefix_len_str_(""), pref_lifetime_(0), preferred_lft_str_("") { BOOST_STATIC_ASSERT(11 < LEASE_COLUMNS); memset(duid_buffer_, 0, sizeof(duid_buffer_)); // Set the column names (for error messages) columnLabels_.push_back("address"); columnLabels_.push_back("duid"); columnLabels_.push_back("valid_lifetime"); columnLabels_.push_back("expire"); columnLabels_.push_back("subnet_id"); columnLabels_.push_back("pref_lifetime"); columnLabels_.push_back("lease_type"); columnLabels_.push_back("iaid"); columnLabels_.push_back("prefix_len"); columnLabels_.push_back("fqdn_fwd"); columnLabels_.push_back("fqdn_rev"); columnLabels_.push_back("hostname"); } /// @brief Creates the bind array for sending Lease6 data to the database. /// /// Converts each Lease6 member into the appropriate form and adds it /// to the bind array. Note that the array additions must occur in the /// order the columns are specified in the SQL statement. By convention /// all columns in the table are explicitly listed in the SQL statement(s) /// in the same order as they occur in the table. /// /// @param lease Lease6 object that is to be written to the database /// @param[out] bind_array array to populate with the lease data values /// /// @throw DbOperationError if bind_array cannot be populated. void createBindForSend(const Lease6Ptr& lease, PsqlBindArray& bind_array) { if (!lease) { isc_throw(BadValue, "createBindForSend:: Lease6 object is NULL"); } // Store lease object to ensure it remains valid. lease_ = lease; try { addr_str_ = lease_->addr_.toText(); bind_array.add(addr_str_); if (lease_->duid_) { bind_array.add(lease_->duid_->getDuid()); } else { isc_throw (BadValue, "IPv6 Lease cannot have a null DUID"); } valid_lft_str_ = boost::lexical_cast (lease->valid_lft_); bind_array.add(valid_lft_str_); expire_str_ = convertToDatabaseTime(lease->valid_lft_ + lease->cltt_); bind_array.add(expire_str_); subnet_id_str_ = boost::lexical_cast (lease->subnet_id_); bind_array.add(subnet_id_str_); preferred_lft_str_ = boost::lexical_cast (lease_->preferred_lft_); bind_array.add(preferred_lft_str_); lease_type_str_ = boost::lexical_cast(lease_->type_); bind_array.add(lease_type_str_); iaid_str_ = boost::lexical_cast(lease_->iaid_); bind_array.add(iaid_str_); prefix_len_str_ = boost::lexical_cast (static_cast(lease_->prefixlen_)); bind_array.add(prefix_len_str_); bind_array.add(lease->fqdn_fwd_); bind_array.add(lease->fqdn_rev_); bind_array.add(lease->hostname_); } catch (const std::exception& ex) { isc_throw(DbOperationError, "Could not create bind array from Lease6: " << lease_->addr_.toText() << " reason: " << ex.what()); } } /// @brief Creates a Lease6 object from a given row in a result set. /// /// @param r result set containing one or rows from the Lease6 table /// @param row row number within the result set from to create the Lease6 /// object. /// /// @return Lease6Ptr to the newly created Lease4 object /// @throw DbOperationError if the lease cannot be created. Lease6Ptr convertFromDatabase(PGresult*& r, int row) { try { isc::asiolink::IOAddress addr(getIPv6Value(r, row, ADDRESS_COL)); convertFromBytea(r, row, DUID_COL, duid_buffer_, sizeof(duid_buffer_), duid_length_); DuidPtr duid_ptr(new DUID(duid_buffer_, duid_length_)); getColumnValue(r, row, VALID_LIFETIME_COL, valid_lifetime_); expire_ = convertFromDatabaseTime(getRawColumnValue(r, row, EXPIRE_COL)); cltt_ = expire_ - valid_lifetime_; getColumnValue(r, row , SUBNET_ID_COL, subnet_id_); getColumnValue(r, row , PREF_LIFETIME_COL, pref_lifetime_); getColumnValue(r, row, LEASE_TYPE_COL, lease_type_); getColumnValue(r, row , IAID_COL, iaid_); getColumnValue(r, row , PREFIX_LEN_COL, prefix_len_); getColumnValue(r, row, FQDN_FWD_COL, fqdn_fwd_); getColumnValue(r, row, FQDN_REV_COL, fqdn_rev_); hostname_ = getRawColumnValue(r, row, HOSTNAME_COL); Lease6Ptr result(new Lease6(lease_type_, addr, duid_ptr, iaid_, pref_lifetime_, valid_lifetime_, 0, 0, subnet_id_, fqdn_fwd_, fqdn_rev_, hostname_, prefix_len_)); result->cltt_ = cltt_; return (result); } catch (const std::exception& ex) { isc_throw(DbOperationError, "Could not convert data to Lease6, reason: " << ex.what()); } } /// @brief Converts a column in a row in a result set into IPv6 address. /// /// @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 /// /// @return isc::asiolink::IOAddress containing the IPv6 address. /// @throw DbOperationError if the value cannot be fetched or is /// invalid. isc::asiolink::IOAddress getIPv6Value(PGresult*& r, const int row, const size_t col) { const char* data = getRawColumnValue(r, row, col); try { return (isc::asiolink::IOAddress(data)); } catch (const std::exception& ex) { isc_throw(DbOperationError, "Cannot convert data: " << data << " for: " << getColumnLabel(col) << " row:" << row << " : " << ex.what()); } } private: /// @brief Lease6 object currently being sent to the database. /// Storing this value ensures that it remains in scope while any bindings /// that refer to its contents are in use. Lease6Ptr lease_; /// @brief Lease6 specific members for binding and conversion. //@{ size_t duid_length_; vector duid_; uint8_t duid_buffer_[DUID::MAX_DUID_LEN]; uint32_t iaid_; std::string iaid_str_; Lease6::Type lease_type_; std::string lease_type_str_; uint8_t prefix_len_; std::string prefix_len_str_; uint32_t pref_lifetime_; std::string preferred_lft_str_; //@} }; PgSqlLeaseMgr::PgSqlLeaseMgr(const LeaseMgr::ParameterMap& parameters) : LeaseMgr(parameters), exchange4_(new PgSqlLease4Exchange()), exchange6_(new PgSqlLease6Exchange()), conn_(NULL) { 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; } } 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 = 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"); } 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_); conn_ = NULL; isc_throw(DbOpenError, error_message); } } bool PgSqlLeaseMgr::addLeaseCommon(StatementIndex stindex, PsqlBindArray& bind_array) { PGresult* r = PQexecPrepared(conn_, tagged_statements[stindex].name, tagged_statements[stindex].nbparams, &bind_array.values_[0], &bind_array.lengths_[0], &bind_array.formats_[0], 0); int s = PQresultStatus(r); if (s != PGRES_COMMAND_OK) { // Failure: check for the special case of duplicate entry. If this is // the case, we return false to indicate that the row was not added. // Otherwise we throw an exception. if (compareError(r, DUPLICATE_KEY)) { PQclear(r); return (false); } const char* errorMsg = PQerrorMessage(conn_); PQclear(r); isc_throw(DbOperationError, "unable to INSERT for " << tagged_statements[stindex].name << ", reason: " << errorMsg); } PQclear(r); return (true); } bool PgSqlLeaseMgr::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, STATECODE_LEN) == 0)); } bool PgSqlLeaseMgr::addLease(const Lease4Ptr& lease) { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_ADD_ADDR4).arg(lease->addr_.toText()); PsqlBindArray bind_array; exchange4_->createBindForSend(lease, bind_array); return (addLeaseCommon(INSERT_LEASE4, bind_array)); } bool PgSqlLeaseMgr::addLease(const Lease6Ptr& lease) { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_ADD_ADDR6).arg(lease->addr_.toText()); PsqlBindArray bind_array; exchange6_->createBindForSend(lease, bind_array); return (addLeaseCommon(INSERT_LEASE6, bind_array)); } template void PgSqlLeaseMgr::getLeaseCollection(StatementIndex stindex, PsqlBindArray& bind_array, Exchange& exchange, LeaseCollection& result, bool single) const { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_GET_ADDR4).arg(tagged_statements[stindex].name); PGresult* r = PQexecPrepared(conn_, tagged_statements[stindex].name, tagged_statements[stindex].nbparams, &bind_array.values_[0], &bind_array.lengths_[0], &bind_array.formats_[0], 0); checkStatementError(r, stindex); int rows = PQntuples(r); if (single && rows > 1) { PQclear(r); isc_throw(MultipleRecords, "multiple records were found in the " "database where only one was expected for query " << tagged_statements[stindex].name); } for(int i = 0; i < rows; ++ i) { result.push_back(exchange->convertFromDatabase(r, i)); } PQclear(r); } void PgSqlLeaseMgr::getLease(StatementIndex stindex, PsqlBindArray& bind_array, Lease4Ptr& result) const { // Create appropriate collection object and get all leases matching // the selection criteria. The "single" parameter is true to indicate // that the called method should throw an exception if multiple // matching records are found: this particular method is called when only // one or zero matches is expected. Lease4Collection collection; getLeaseCollection(stindex, bind_array, exchange4_, collection, true); // Return single record if present, else clear the lease. if (collection.empty()) { result.reset(); } else { result = *collection.begin(); } } void PgSqlLeaseMgr::getLease(StatementIndex stindex, PsqlBindArray& bind_array, Lease6Ptr& result) const { // Create appropriate collection object and get all leases matching // the selection criteria. The "single" parameter is true to indicate // that the called method should throw an exception if multiple // matching records are found: this particular method is called when only // one or zero matches is expected. Lease6Collection collection; getLeaseCollection(stindex, bind_array, exchange6_, collection, true); // Return single record if present, else clear the lease. if (collection.empty()) { result.reset(); } else { result = *collection.begin(); } } Lease4Ptr PgSqlLeaseMgr::getLease4(const isc::asiolink::IOAddress& addr) const { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_GET_ADDR4).arg(addr.toText()); // Set up the WHERE clause value PsqlBindArray bind_array; // LEASE ADDRESS std::string addr_str = boost::lexical_cast (static_cast(addr)); bind_array.add(addr_str); // Get the data Lease4Ptr result; getLease(GET_LEASE4_ADDR, bind_array, result); return (result); } Lease4Collection PgSqlLeaseMgr::getLease4(const HWAddr& hwaddr) const { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_GET_HWADDR).arg(hwaddr.toText()); // Set up the WHERE clause value PsqlBindArray bind_array; // HWADDR if (!hwaddr.hwaddr_.empty()) { bind_array.add(hwaddr.hwaddr_); } else { bind_array.add(""); } // Get the data Lease4Collection result; getLeaseCollection(GET_LEASE4_HWADDR, bind_array, result); return (result); } Lease4Ptr PgSqlLeaseMgr::getLease4(const HWAddr& hwaddr, SubnetID subnet_id) const { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_GET_SUBID_HWADDR) .arg(subnet_id).arg(hwaddr.toText()); // Set up the WHERE clause value PsqlBindArray bind_array; // HWADDR if (!hwaddr.hwaddr_.empty()) { bind_array.add(hwaddr.hwaddr_); } else { bind_array.add(""); } // SUBNET_ID std::string subnet_id_str = boost::lexical_cast(subnet_id); bind_array.add(subnet_id_str); // Get the data Lease4Ptr result; getLease(GET_LEASE4_HWADDR_SUBID, bind_array, result); return (result); } Lease4Collection PgSqlLeaseMgr::getLease4(const ClientId& clientid) const { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_GET_CLIENTID).arg(clientid.toText()); // Set up the WHERE clause value PsqlBindArray bind_array; // CLIENT_ID bind_array.add(clientid.getClientId()); // Get the data Lease4Collection result; getLeaseCollection(GET_LEASE4_CLIENTID, bind_array, result); return (result); } Lease4Ptr PgSqlLeaseMgr::getLease4(const ClientId& clientid, SubnetID subnet_id) const { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_GET_SUBID_CLIENTID) .arg(subnet_id).arg(clientid.toText()); // Set up the WHERE clause value PsqlBindArray bind_array; // CLIENT_ID bind_array.add(clientid.getClientId()); // SUBNET_ID std::string subnet_id_str = boost::lexical_cast(subnet_id); bind_array.add(subnet_id_str); // Get the data Lease4Ptr result; getLease(GET_LEASE4_CLIENTID_SUBID, bind_array, result); return (result); } Lease4Ptr PgSqlLeaseMgr::getLease4(const ClientId&, const HWAddr&, SubnetID) const { /// This function is currently not implemented because allocation engine /// searches for the lease using HW address or client identifier. /// It never uses both parameters in the same time. We need to /// consider if this function is needed at all. isc_throw(NotImplemented, "The PgSqlLeaseMgr::getLease4 function was" " called, but it is not implemented"); } Lease6Ptr PgSqlLeaseMgr::getLease6(Lease::Type lease_type, const isc::asiolink::IOAddress& addr) const { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_GET_ADDR6) .arg(addr.toText()).arg(lease_type); // Set up the WHERE clause value PsqlBindArray bind_array; // LEASE ADDRESS std::string addr_str = addr.toText(); bind_array.add(addr_str); // LEASE_TYPE std::string type_str_ = boost::lexical_cast(lease_type); bind_array.add(type_str_); // ... and get the data Lease6Ptr result; getLease(GET_LEASE6_ADDR, bind_array, result); return (result); } Lease6Collection PgSqlLeaseMgr::getLeases6(Lease::Type lease_type, const DUID& duid, uint32_t iaid) const { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_GET_IAID_DUID) .arg(iaid).arg(duid.toText()).arg(lease_type); // Set up the WHERE clause value PsqlBindArray bind_array; // DUID bind_array.add(duid.getDuid()); // IAID std::string iaid_str = boost::lexical_cast(iaid); bind_array.add(iaid_str); // LEASE_TYPE std::string lease_type_str = boost::lexical_cast(lease_type); bind_array.add(lease_type_str); // ... and get the data Lease6Collection result; getLeaseCollection(GET_LEASE6_DUID_IAID, bind_array, result); return (result); } Lease6Collection PgSqlLeaseMgr::getLeases6(Lease::Type lease_type, const DUID& duid, uint32_t iaid, SubnetID subnet_id) const { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_GET_IAID_SUBID_DUID) .arg(iaid).arg(subnet_id).arg(duid.toText()).arg(lease_type); // Set up the WHERE clause value PsqlBindArray bind_array; // LEASE_TYPE std::string lease_type_str = boost::lexical_cast(lease_type); bind_array.add(lease_type_str); // DUID bind_array.add(duid.getDuid()); // IAID std::string iaid_str = boost::lexical_cast(iaid); bind_array.add(iaid_str); // SUBNET ID std::string subnet_id_str = boost::lexical_cast(subnet_id); bind_array.add(subnet_id_str); // ... and get the data Lease6Collection result; getLeaseCollection(GET_LEASE6_DUID_IAID_SUBID, bind_array, result); return (result); } template void PgSqlLeaseMgr::updateLeaseCommon(StatementIndex stindex, PsqlBindArray& bind_array, const LeasePtr& lease) { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_ADD_ADDR4).arg(tagged_statements[stindex].name); PGresult* r = PQexecPrepared(conn_, tagged_statements[stindex].name, tagged_statements[stindex].nbparams, &bind_array.values_[0], &bind_array.lengths_[0], &bind_array.formats_[0], 0); checkStatementError(r, stindex); int affected_rows = boost::lexical_cast(PQcmdTuples(r)); PQclear(r); // Check success case first as it is the most likely outcome. if (affected_rows == 1) { return; } // If no rows affected, lease doesn't exist. if (affected_rows == 0) { isc_throw(NoSuchLease, "unable to update lease for address " << lease->addr_.toText() << " as it does not exist"); } // Should not happen - primary key constraint should only have selected // one row. isc_throw(DbOperationError, "apparently updated more than one lease " "that had the address " << lease->addr_.toText()); } void PgSqlLeaseMgr::updateLease4(const Lease4Ptr& lease) { const StatementIndex stindex = UPDATE_LEASE4; LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_UPDATE_ADDR4).arg(lease->addr_.toText()); // Create the BIND array for the data being updated PsqlBindArray bind_array; exchange4_->createBindForSend(lease, bind_array); // Set up the WHERE clause and append it to the SQL_BIND array std::string addr4_ = boost::lexical_cast (static_cast(lease->addr_)); bind_array.add(addr4_); // Drop to common update code updateLeaseCommon(stindex, bind_array, lease); } void PgSqlLeaseMgr::updateLease6(const Lease6Ptr& lease) { const StatementIndex stindex = UPDATE_LEASE6; LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_UPDATE_ADDR6).arg(lease->addr_.toText()); // Create the BIND array for the data being updated PsqlBindArray bind_array; exchange6_->createBindForSend(lease, bind_array); // Set up the WHERE clause and append it to the BIND array std::string addr_str = lease->addr_.toText(); bind_array.add(addr_str); // Drop to common update code updateLeaseCommon(stindex, bind_array, lease); } bool PgSqlLeaseMgr::deleteLeaseCommon(StatementIndex stindex, PsqlBindArray& bind_array) { PGresult* r = PQexecPrepared(conn_, tagged_statements[stindex].name, tagged_statements[stindex].nbparams, &bind_array.values_[0], &bind_array.lengths_[0], &bind_array.formats_[0], 0); checkStatementError(r, stindex); int affected_rows = boost::lexical_cast(PQcmdTuples(r)); PQclear(r); return (affected_rows > 0); } bool PgSqlLeaseMgr::deleteLease(const isc::asiolink::IOAddress& addr) { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_DELETE_ADDR).arg(addr.toText()); // Set up the WHERE clause value PsqlBindArray bind_array; if (addr.isV4()) { std::string addr4_str = boost::lexical_cast (static_cast(addr)); bind_array.add(addr4_str); return (deleteLeaseCommon(DELETE_LEASE4, bind_array)); } std::string addr6_str = addr.toText(); bind_array.add(addr6_str); return (deleteLeaseCommon(DELETE_LEASE6, bind_array)); } string PgSqlLeaseMgr::getName() const { string name = ""; try { name = getParameter("name"); } catch (...) { // Return an empty name } return (name); } void PgSqlLeaseMgr::checkStatementError(PGresult*& r, StatementIndex index) const { int s = PQresultStatus(r); if (s != PGRES_COMMAND_OK && s != PGRES_TUPLES_OK) { const char* error_message = PQerrorMessage(conn_); PQclear(r); isc_throw(DbOperationError, "Statement exec faild:" << " for: " << tagged_statements[index].name << ", reason: " << error_message); } } string PgSqlLeaseMgr::getDescription() const { return (string("PostgreSQL Database")); } pair PgSqlLeaseMgr::getVersion() const { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_GET_VERSION); PGresult* r = PQexecPrepared(conn_, "get_version", 0, NULL, NULL, NULL, 0); checkStatementError(r, GET_VERSION); istringstream tmp; uint32_t version; tmp.str(PQgetvalue(r, 0, 0)); tmp >> version; tmp.str(""); tmp.clear(); uint32_t minor; tmp.str(PQgetvalue(r, 0, 1)); tmp >> minor; PQclear(r); return make_pair(version, minor); } void PgSqlLeaseMgr::commit() { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_COMMIT); PGresult* r = PQexec(conn_, "COMMIT"); if (PQresultStatus(r) != PGRES_COMMAND_OK) { const char* error_message = PQerrorMessage(conn_); PQclear(r); isc_throw(DbOperationError, "commit failed: " << error_message); } PQclear(r); } void PgSqlLeaseMgr::rollback() { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_ROLLBACK); PGresult* r = PQexec(conn_, "ROLLBACK"); if (PQresultStatus(r) != PGRES_COMMAND_OK) { const char* error_message = PQerrorMessage(conn_); PQclear(r); isc_throw(DbOperationError, "rollback failed: " << error_message); } PQclear(r); } }; // end of isc::dhcp namespace }; // end of isc namespace