diff --git a/configure.ac b/configure.ac index 83707ee50e..e70d5bb72a 100644 --- a/configure.ac +++ b/configure.ac @@ -969,6 +969,58 @@ AC_CHECK_HEADER(sys/filio.h) # ... and at the shell level, so Makefile.am can take action depending on this. AM_CONDITIONAL(HAVE_MYSQL, test "$MYSQL_CONFIG" != "") +pg_config="no" +AC_ARG_WITH([dhcp-pgsql], + AC_HELP_STRING([--with-dhcp-pgsql=PATH], + [path to the PostgreSQL 'pg_config' script]), + [pg_config="$withval"]) + +if test "${pg_config}" = "yes" ; then + PG_CONFIG="/usr/bin/pg_config" +elif test "${pg_config}" != "no" ; then + PG_CONFIG="${withval}" +fi + +if test "$PG_CONFIG" != "" ; then + if test -d "$PG_CONFIG" -o ! -x "$PG_CONFIG" ; then + AC_MSG_ERROR([--with-dhcp-pgsql should point to a pg_config program]) + fi + + PGSQL_CPPFLAGS=`$PG_CONFIG --cppflags` + PGSQL_INCLUDEDIR=`$PG_CONFIG --includedir` + PGSQL_CPPFLAGS="$PGSQL_CPPFLAGS -I$PGSQL_INCLUDEDIR" + PGSQL_LIBS=`$PG_CONFIG --ldflags` + PGSQL_LIBS="$PGSQL_LIBS -lpq" + PGSQL_VERSION=`$PG_CONFIG --version` + + AC_SUBST(PGSQL_CPPFLAGS) + AC_SUBST(PGSQL_LIBS) + + # Check that a simple program using PostgreSQL functions can compile and link. + CPPFLAGS_SAVED="$CPPFLAGS" + LIBS_SAVED="$LIBS" + + CPPFLAGS="$PGSQL_CPPFLAGS $CPPFLAGS" + LIBS="$PGSQL_LIBS $LIBS" + + AC_LINK_IFELSE( + [AC_LANG_PROGRAM([#include ], + [PGconn * c = PQconnectdb("dbname = 'postgres'"); + PQfinish(c);])], + [AC_MSG_RESULT([checking for PostgreSQL headers and library... yes])], + [AC_MSG_RESULT([checking for PostgreSQL headers and library... no]) + AC_MSG_ERROR([Needs PostgreSQL library])] + ) + + CPPFLAGS=$CPPFLAGS_SAVED + LIBS=$LIBS_SAVED + + # Note that PostgreSQL is present in the config.h file + AC_DEFINE([HAVE_PGSQL], [1], [PostgreSQL is present]) +fi + +# ... and at the shell level, so Makefile.am can take action depending on this. +AM_CONDITIONAL(HAVE_PGSQL, test "$PG_CONFIG" != "") # Check for log4cplus log4cplus_path="yes" @@ -1743,6 +1795,26 @@ MySQL: MYSQL_CPPFLAGS: ${MYSQL_CPPFLAGS} MYSQL_LIBS: ${MYSQL_LIBS} END +else +cat >> config.report << END + +MySQL: no +END +fi + +if test "$PGSQL_CPPFLAGS" != "" ; then +cat >> config.report << END + +PostgreSQL: + PGSQL_VERSION: ${PGSQL_VERSION} + PGSQL_CPPFLAGS: ${PGSQL_CPPFLAGS} + PGSQL_LIBS: ${PGSQL_LIBS} +END +else +cat >> config.report << END + +PostgreSQL: no +END fi if test "$enable_gtest" != "no"; then diff --git a/src/lib/dhcpsrv/Makefile.am b/src/lib/dhcpsrv/Makefile.am index 1c8a18ae10..dffdcf466c 100644 --- a/src/lib/dhcpsrv/Makefile.am +++ b/src/lib/dhcpsrv/Makefile.am @@ -7,6 +7,9 @@ AM_CPPFLAGS += $(BOOST_INCLUDES) if HAVE_MYSQL AM_CPPFLAGS += $(MYSQL_CPPFLAGS) endif +if HAVE_PGSQL +AM_CPPFLAGS += $(PGSQL_CPPFLAGS) +endif AM_CXXFLAGS = $(B10_CXXFLAGS) @@ -54,6 +57,9 @@ libb10_dhcpsrv_la_SOURCES += memfile_lease_mgr.cc memfile_lease_mgr.h if HAVE_MYSQL libb10_dhcpsrv_la_SOURCES += mysql_lease_mgr.cc mysql_lease_mgr.h endif +if HAVE_PGSQL +libb10_dhcpsrv_la_SOURCES += pgsql_lease_mgr.cc pgsql_lease_mgr.h +endif libb10_dhcpsrv_la_SOURCES += option_space_container.h libb10_dhcpsrv_la_SOURCES += pool.cc pool.h libb10_dhcpsrv_la_SOURCES += subnet.cc subnet.h @@ -77,6 +83,9 @@ libb10_dhcpsrv_la_LDFLAGS = -no-undefined -version-info 3:0:0 if HAVE_MYSQL libb10_dhcpsrv_la_LDFLAGS += $(MYSQL_LIBS) endif +if HAVE_PGSQL +libb10_dhcpsrv_la_LDFLAGS += $(PGSQL_LIBS) +endif if USE_CLANGPP # Disable unused parameter warning caused by some of the @@ -88,8 +97,8 @@ endif EXTRA_DIST = dhcpsrv_messages.mes # Distribute MySQL schema creation script and backend documentation -EXTRA_DIST += dhcpdb_create.mysql database_backends.dox libdhcpsrv.dox -dist_pkgdata_DATA = dhcpdb_create.mysql +EXTRA_DIST += dhcpdb_create.mysql dhcpdb_create.pgsql database_backends.dox libdhcpsrv.dox +dist_pkgdata_DATA = dhcpdb_create.mysql dhcpdb_create.pgsql install-data-local: $(mkinstalldirs) $(DESTDIR)$(dhcp_data_dir) diff --git a/src/lib/dhcpsrv/database_backends.dox b/src/lib/dhcpsrv/database_backends.dox index 174b5e22fc..94beadd35b 100644 --- a/src/lib/dhcpsrv/database_backends.dox +++ b/src/lib/dhcpsrv/database_backends.dox @@ -121,4 +121,78 @@ The unit tests are run automatically when "make check" is executed (providing that BIND 10 has been build with the \--with-dhcp-mysql switch (see the installation section in the BIND 10 Guide). + + @subsection dhcp-pgsql-unittest PostgreSQL unit-tests + + Conceptually, the steps required to run PostgreSQL unit-tests are the same as + in MySQL. First, a database called keatest must be created. A database + user, also called keatest (that will be allowed to log in using password + keatest) must be created and given full privileges in that database. The + unit tests create the schema in the database before each test and delete it + afterwards. + + PostgreSQL set up differs from system to system. Please consult your OS-specific + PostgreSQL documentation. The remainder of that section uses Ubuntu 13.10 x64 as + example. On Ubuntu, after installing PostgreSQL (with sudo apt-get install + postgresql), it is installed as user postgres. To create new databases + or add new users, initial commands must be issued as user postgres: + +@verbatim +$ sudo -u postgres psql postgres +[sudo] password for thomson: +psql (9.1.12) +Type "help" for help. +postgres=# CREATE USER keatest WITH PASSWORD 'keatest'; +CREATE ROLE +postgres=# CREATE DATABASE keatest; +CREATE DATABASE +postgres=# GRANT ALL PRIVILEGES ON DATABASE keatest TO keatest; +GRANT +postgres=# \q +@endverbatim + + Now we are back to our regular, unprivileged user. Try to log into the newly + created database using keatest credentials: +@verbatim +$ psql -d keatest -U keatest +$ psql keatest -U keatest -W +Password for user keatest: +psql (9.1.12) +Type "help" for help. + +keatest=> +@endverbatim + + If instead of seeing keatest=> prompt, your login will be refused with error + code about failed peer or indent authentication, it means that PostgreSQL is + configured to check unix username and reject login attepts if PostgreSQL names + are different. To alter that, PostgreSQL configuration must be changed. + Alternatively, you may set up your environment, so the tests would be run from + unix account keatest. /etc/postgresql/9.1/main/pg_hba.conf config file + had to betweaked. It may be in a different location in your system. The following + lines: + +@verbatim +local all all peer +host all all 127.0.0.1/32 md5 +host all all ::1/128 md5 +@endverbatim + + were replaced with: + +@verbatim +local all all password +host all all 127.0.0.1/32 password +host all all ::1/128 password +@endverbatim + + Please consult your PostgreSQL user manual before applying those changes as + those changes may expose your other databases that you run on the same system. + In general case, it is a poor idea to run anything of value on a system + that runs tests. Use caution! + + The unit tests are run automatically when "make check" is executed (providing + that BIND 10 has been build with the \--with-dhcp-pgsql switch (see the installation + section in the BIND10 Guide). + */ diff --git a/src/lib/dhcpsrv/dbaccess_parser.cc b/src/lib/dhcpsrv/dbaccess_parser.cc index d29cf54293..08722dfad4 100644 --- a/src/lib/dhcpsrv/dbaccess_parser.cc +++ b/src/lib/dhcpsrv/dbaccess_parser.cc @@ -67,7 +67,7 @@ DbAccessParser::build(isc::data::ConstElementPtr config_value) { // b. Check if the 'type; keyword known and throw an exception if not. string dbtype = type_ptr->second; - if ((dbtype != "memfile") && (dbtype != "mysql")) { + if ((dbtype != "memfile") && (dbtype != "mysql") && (dbtype != "postgresql")) { isc_throw(BadValue, "unknown backend database type: " << dbtype); } diff --git a/src/lib/dhcpsrv/dhcpdb_create.pgsql b/src/lib/dhcpsrv/dhcpdb_create.pgsql new file mode 100644 index 0000000000..2bf9a47ff6 --- /dev/null +++ b/src/lib/dhcpsrv/dhcpdb_create.pgsql @@ -0,0 +1,129 @@ +-- Copyright (C) 2012-2013 Internet Systems Consortium. + +-- Permission to use, copy, modify, and 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 INTERNET SYSTEMS CONSORTIUM +-- DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +-- INTERNET SYSTEMS CONSORTIUM 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. + +-- This is the BIND 10 DHCP schema specification for PostgreSQL. + +-- The schema is reasonably portable (with the exception of the engine +-- specification, which is MySQL-specific). Minor changes might be needed for +-- other databases. + +-- To create the schema, either type the command: + +-- psql -U -W < dhcpdb_create.pgsql + +-- ... at the command prompt, or log in to the PostgreSQL database and at the "postgres=#" +-- prompt, issue the command: + +-- @dhcpdb_create.pgsql + + +-- Holds the IPv4 leases. +CREATE TABLE lease4 ( + address BIGINT PRIMARY KEY NOT NULL, -- IPv4 address + hwaddr BYTEA, -- Hardware address + client_id BYTEA, -- Client ID + valid_lifetime BIGINT, -- Length of the lease (seconds) + expire TIMESTAMP, -- Expiration time of the lease + subnet_id BIGINT -- Subnet identification + ); + + +-- Create search indexes for lease4 table +-- index by hwaddr and subnet_id +CREATE INDEX lease4_by_hwaddr_subnet_id ON lease4 (hwaddr, subnet_id); + +-- index by client_id and subnet_id +CREATE INDEX lease4_by_client_id_subnet_id ON lease4 (client_id, subnet_id); + +-- Holds the IPv6 leases. +-- N.B. The use of a VARCHAR for the address is temporary for development: +-- it will eventually be replaced by BINARY(16). +CREATE TABLE lease6 ( + address VARCHAR(39) PRIMARY KEY NOT NULL, -- IPv6 address + duid BYTEA, -- DUID + valid_lifetime BIGINT, -- Length of the lease (seconds) + expire TIMESTAMP, -- Expiration time of the lease + subnet_id BIGINT, -- Subnet identification + pref_lifetime BIGINT, -- Preferred lifetime + lease_type SMALLINT, -- Lease type (see lease6_types + -- table for possible values) + iaid INT, -- See Section 10 of RFC 3315 + prefix_len SMALLINT -- For IA_PD only + ); + +-- Create search indexes for lease4 table +-- index by iaid, subnet_id, and duid +CREATE INDEX lease6_by_iaid_subnet_id_duid ON lease6 (iaid, subnet_id, duid); + +-- ... and a definition of lease6 types. This table is a convenience for +-- users of the database - if they want to view the lease table and use the +-- type names, they can join this table with the lease6 table +CREATE TABLE lease6_types ( + lease_type SMALLINT PRIMARY KEY NOT NULL, -- Lease type code. + name VARCHAR(5) -- Name of the lease type + ); +START TRANSACTION; +INSERT INTO lease6_types VALUES (0, 'IA_NA'); -- Non-temporary v6 addresses +INSERT INTO lease6_types VALUES (1, 'IA_TA'); -- Temporary v6 addresses +INSERT INTO lease6_types VALUES (2, 'IA_PD'); -- Prefix delegations +COMMIT; + +-- Finally, the version of the schema. We start at 0.1 during development. +-- This table is only modified during schema upgrades. For historical reasons +-- (related to the names of the columns in the BIND 10 DNS database file), the +-- first column is called "version" and not "major". + +-- NOTE: this MUST be kept in step with src/lib/dhcpsrv/tests/schema_copy.h, +-- which defines the schema for the unit tests. If you are updating +-- the version number, the schema has changed: please ensure that +-- schema_copy.h has been updated as well. +CREATE TABLE schema_version ( + version INT PRIMARY KEY NOT NULL, -- Major version number + minor INT -- Minor version number + ); +START TRANSACTION; +INSERT INTO schema_version VALUES (1, 0); +COMMIT; + +-- Notes: + +-- Indexes +-- ======= +-- It is likely that additional indexes will be needed. However, the +-- increase in lookup performance from these will come at the expense +-- of a decrease in performance during insert operations due to the need +-- to update the indexes. For this reason, the need for additional indexes +-- will be determined by experiment during performance tests. + +-- The most likely additional indexes will cover the following columns: + +-- expire +-- To speed up the deletion of expired leases from the database. + +-- hwaddr and client_id +-- For lease stability: if a client requests a new lease, try to find an +-- existing or recently expired lease for it so that it can keep using the +-- same IP address. + +-- Field Sizes +-- =========== +-- If any of the VARxxx field sizes are altered, the lengths in the MySQL +-- backend source file (mysql_lease_mgr.cc) must be correspondingly changed. + +-- Portability +-- =========== +-- Some columns contain binary data so are stored as BYTEA instead of +-- VARCHAR. This may be non-portable between databases: in this case, the +-- definition should be changed to VARCHAR. diff --git a/src/lib/dhcpsrv/dhcpsrv_messages.mes b/src/lib/dhcpsrv/dhcpsrv_messages.mes index f06ebe6c89..8e4fded694 100644 --- a/src/lib/dhcpsrv/dhcpsrv_messages.mes +++ b/src/lib/dhcpsrv/dhcpsrv_messages.mes @@ -375,6 +375,83 @@ lease from the MySQL database for the specified address. A debug message issued when the server is attempting to update IPv6 lease from the MySQL database for the specified address. +% DHCPSRV_PGSQL_ADD_ADDR4 adding IPv4 lease with address %1 +A debug message issued when the server is about to add an IPv4 lease +with the specified address to the PostgreSQL backend database. + +% DHCPSRV_PGSQL_ADD_ADDR6 adding IPv6 lease with address %1 +A debug message issued when the server is about to add an IPv6 lease +with the specified address to the PostgreSQL backend database. + +% DHCPSRV_PGSQL_COMMIT committing to MySQL database +The code has issued a commit call. All outstanding transactions will be +committed to the database. Note that depending on the PostgreSQL settings, +the committal may not include a write to disk. + +% DHCPSRV_PGSQL_DB opening PostgreSQL lease database: %1 +This informational message is logged when a DHCP server (either V4 or +V6) is about to open a PostgreSQL lease database. The parameters of the +connection including database name and username needed to access it +(but not the password if any) are logged. + +% DHCPSRV_PGSQL_DELETE_ADDR deleting lease for address %1 +A debug message issued when the server is attempting to delete a lease for +the specified address from the PostgreSQL database for the specified address. + +% 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. + +% DHCPSRV_PGSQL_GET_ADDR6 obtaining IPv6 lease for address %1 (lease type %2) +A debug message issued when the server is attempting to obtain an IPv6 +lease from the PostgreSQL database for the specified address. + +% DHCPSRV_PGSQL_GET_CLIENTID obtaining IPv4 leases for client ID %1 +A debug message issued when the server is attempting to obtain a set +of IPv4 leases from the PostgreSQL database for a client with the specified +client identification. + +% DHCPSRV_PGSQL_GET_HWADDR obtaining IPv4 leases for hardware address %1 +A debug message issued when the server is attempting to obtain a set +of IPv4 leases from the PostgreSQL database for a client with the specified +hardware address. + +% DHCPSRV_PGSQL_GET_IAID_DUID obtaining IPv4 leases for IAID %1 and DUID %2, lease type %3 +A debug message issued when the server is attempting to obtain a set of +IPv6 lease from the PostgreSQL database for a client with the specified IAID +(Identity Association ID) and DUID (DHCP Unique Identifier). + +% DHCPSRV_PGSQL_GET_IAID_SUBID_DUID obtaining IPv4 leases for IAID %1, Subnet ID %2 and DUID %3 +A debug message issued when the server is attempting to obtain an IPv6 +lease from the PostgreSQL database for a client with the specified IAID +(Identity Association ID), Subnet ID and DUID (DHCP Unique Identifier). + +% DHCPSRV_PGSQL_GET_SUBID_CLIENTID obtaining IPv4 lease for subnet ID %1 and client ID %2 +A debug message issued when the server is attempting to obtain an IPv4 +lease from the PostgreSQL database for a client with the specified subnet ID +and client ID. + +% DHCPSRV_PGSQL_GET_SUBID_HWADDR obtaining IPv4 lease for subnet ID %1 and hardware address %2 +A debug message issued when the server is attempting to obtain an IPv4 +lease from the PostgreSQL database for a client with the specified subnet ID +and hardware address. + +% DHCPSRV_PGSQL_GET_VERSION obtaining schema version information +A debug message issued when the server is about to obtain schema version +information from the PostgreSQL database. + +% DHCPSRV_PGSQL_ROLLBACK rolling back PostgreSQL database +The code has issued a rollback call. All outstanding transaction will +be rolled back and not committed to the database. + +% 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. + +% DHCPSRV_PGSQL_UPDATE_ADDR6 updating IPv6 lease for address %1 +A debug message issued when the server is attempting to update IPv6 +lease from the PostgreSQL database for the specified address. + % DHCPSRV_NOTYPE_DB no 'type' keyword to determine database backend: %1 This is an error message, logged when an attempt has been made to access a database backend, but where no 'type' keyword has been included in diff --git a/src/lib/dhcpsrv/lease_mgr_factory.cc b/src/lib/dhcpsrv/lease_mgr_factory.cc index ede0dba9dc..9fd6789382 100644 --- a/src/lib/dhcpsrv/lease_mgr_factory.cc +++ b/src/lib/dhcpsrv/lease_mgr_factory.cc @@ -20,6 +20,9 @@ #ifdef HAVE_MYSQL #include #endif +#ifdef HAVE_PGSQL +#include +#endif #include #include @@ -124,6 +127,13 @@ LeaseMgrFactory::create(const std::string& dbaccess) { getLeaseMgrPtr().reset(new MySqlLeaseMgr(parameters)); return; } +#endif +#ifdef HAVE_PGSQL + if (parameters[type] == string("postgresql")) { + LOG_INFO(dhcpsrv_logger, DHCPSRV_PGSQL_DB).arg(redacted); + getLeaseMgrPtr().reset(new PgSqlLeaseMgr(parameters)); + return; + } #endif if (parameters[type] == string("memfile")) { LOG_INFO(dhcpsrv_logger, DHCPSRV_MEMFILE_DB).arg(redacted); diff --git a/src/lib/dhcpsrv/pgsql_lease_mgr.cc b/src/lib/dhcpsrv/pgsql_lease_mgr.cc new file mode 100644 index 0000000000..4ec5ca450f --- /dev/null +++ b/src/lib/dhcpsrv/pgsql_lease_mgr.cc @@ -0,0 +1,1185 @@ +// Copyright (C) 2012 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 + +using namespace isc; +using namespace isc::dhcp; +using namespace std; + +namespace { +const size_t ADDRESS6_TEXT_MAX_LEN = 39; + +struct TaggedStatement { + PgSqlLeaseMgr::StatementIndex index; + int nbparams; + const Oid types[10]; + const char * name; + const char * text; +}; + +TaggedStatement tagged_statements[] = { + {PgSqlLeaseMgr::DELETE_LEASE4, 1, + { 20 }, + "delete_lease4", + "DELETE FROM lease4 WHERE address = $1"}, + {PgSqlLeaseMgr::DELETE_LEASE6, 1, + { 1043 }, + "delete_lease6", + "DELETE FROM lease6 WHERE address = $1"}, + {PgSqlLeaseMgr::GET_LEASE4_ADDR, 1, + { 20 }, + "get_lease4_addr", + "DECLARE get_lease4_addr CURSOR FOR " + "SELECT address, hwaddr, client_id, " + "valid_lifetime, extract(epoch from expire), subnet_id " + "FROM lease4 " + "WHERE address = $1"}, + {PgSqlLeaseMgr::GET_LEASE4_CLIENTID, 1, + { 17 }, + "get_lease4_clientid", + "DECLARE get_lease4_clientid CURSOR FOR " + "SELECT address, hwaddr, client_id, " + "valid_lifetime, extract(epoch from expire), subnet_id " + "FROM lease4 " + "WHERE client_id = $1"}, + {PgSqlLeaseMgr::GET_LEASE4_CLIENTID_SUBID, 2, + { 17, 20 }, + "get_lease4_clientid_subid", + "DECLARE get_lease4_clientid_subid CURSOR FOR " + "SELECT address, hwaddr, client_id, " + "valid_lifetime, extract(epoch from expire)::bigint, subnet_id " + "FROM lease4 " + "WHERE client_id = $1 AND subnet_id = $2"}, + {PgSqlLeaseMgr::GET_LEASE4_HWADDR, 1, + { 17 }, + "get_lease4_hwaddr", + "DECLARE get_lease4_hwaddr CURSOR FOR " + "SELECT address, hwaddr, client_id, " + "valid_lifetime, extract(epoch from expire)::bigint, subnet_id " + "FROM lease4 " + "WHERE hwaddr = $1"}, + {PgSqlLeaseMgr::GET_LEASE4_HWADDR_SUBID, 2, + { 17, 20 }, + "get_lease4_hwaddr_subid", + "DECLARE get_lease4_hwaddr_subid CURSOR FOR " + "SELECT address, hwaddr, client_id, " + "valid_lifetime, extract(epoch from expire)::bigint, subnet_id " + "FROM lease4 " + "WHERE hwaddr = $1 AND subnet_id = $2"}, + {PgSqlLeaseMgr::GET_LEASE6_ADDR, 1, + { 1043 }, + "get_lease6_addr", + "DECLARE get_lease6_addr CURSOR FOR " + "SELECT address, duid, valid_lifetime, " + "extract(epoch from expire)::bigint, subnet_id, pref_lifetime, " + "lease_type, iaid, prefix_len " + "FROM lease6 " + "WHERE address = $1"}, + {PgSqlLeaseMgr::GET_LEASE6_DUID_IAID, 2, + { 17, 20 }, + "get_lease6_duid_iaid", + "DECLARE get_lease6_duid_iaid CURSOR FOR " + "SELECT address, duid, valid_lifetime, " + "extract(epoch from expire)::bigint, subnet_id, pref_lifetime, " + "lease_type, iaid, prefix_len " + "FROM lease6 " + "WHERE duid = $1 AND iaid = $2"}, + {PgSqlLeaseMgr::GET_LEASE6_DUID_IAID_SUBID, 3, + { 17, 20, 20 }, + "get_lease6_duid_iaid_subid", + "DECLARE get_lease6_duid_iaid_subid CURSOR FOR " + "SELECT address, duid, valid_lifetime, " + "extract(epoch from expire)::bigint, subnet_id, pref_lifetime, " + "lease_type, iaid, prefix_len " + "FROM lease6 " + "WHERE duid = $1 AND iaid = $2 AND subnet_id = $3"}, + {PgSqlLeaseMgr::GET_VERSION, 0, + { 0 }, + "get_version", + "DECLARE get_version CURSOR FOR " + "SELECT version, minor FROM schema_version"}, + {PgSqlLeaseMgr::INSERT_LEASE4, 6, + { 20, 17, 17, 20, 1114, 20 }, + "insert_lease4", + "INSERT INTO lease4(address, hwaddr, client_id, " + "valid_lifetime, expire, subnet_id) " + "VALUES ($1, $2, $3, $4, $5, $6)"}, + {PgSqlLeaseMgr::INSERT_LEASE6, 9, + { 1043, 17, 20, 1114, 20, + 20, 21, 20, 21 }, + "insert_lease6", + "INSERT INTO lease6(address, duid, valid_lifetime, " + "expire, subnet_id, pref_lifetime, " + "lease_type, iaid, prefix_len) " + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"}, + {PgSqlLeaseMgr::UPDATE_LEASE4, 7, + { 20, 17, 17, 20, 1114, 20, 20 }, + "update_lease4", + "UPDATE lease4 SET address = $1, hwaddr = $2, " + "client_id = $3, valid_lifetime = $4, expire = $5, " + "subnet_id = $6 " + "WHERE address = $7"}, + {PgSqlLeaseMgr::UPDATE_LEASE6, 10, + { 1043, 17, 20, 1114, 20, + 20, 21, 20, 21, 1043 }, + "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 " + "WHERE address = $10"}, + // End of list sentinel + {PgSqlLeaseMgr::NUM_STATEMENTS, 0, { 0 }, NULL, NULL} +}; + +}; + +namespace isc { +namespace dhcp { + +class PgSqlLeaseExchange { +protected: + bindparams params; + void + convertToTimestamp(const time_t & expire_, + char buffer_[20]) { + struct tm tinfo; + localtime_r(&expire_, &tinfo); + strftime(buffer_, 20, "%Y-%m-%d %H:%M:%S", &tinfo); + } +}; + +class PgSqlLease4Exchange : public PgSqlLeaseExchange { + static const size_t LEASE_COLUMNS = 6; +public: + PgSqlLease4Exchange() : addr4_(0) { + memset(hwaddr_buffer_, 0, sizeof(hwaddr_buffer_)); + memset(client_id_buffer_, 0, sizeof(client_id_buffer_)); + + // Set the column names (for error messages) + columns_[0] = "address"; + columns_[1] = "hwaddr"; + columns_[2] = "client_id"; + columns_[3] = "valid_lifetime"; + columns_[4] = "expire"; + columns_[5] = "subnet_id"; + + BOOST_STATIC_ASSERT(5 < LEASE_COLUMNS); + + params.reserve(LEASE_COLUMNS); + } + + bindparams + createBindForSend(const Lease4Ptr& lease) { + lease_ = lease; + params.clear(); + ostringstream tmp; + + tmp << lease_->addr_; + PgSqlParam paddr4 = { .value = tmp.str() }; + + params.push_back(paddr4); + tmp.str(""); + tmp.clear(); + + uint8_t* data = const_cast(&(lease_->hwaddr_[0])); + PgSqlParam pdest = { .value = reinterpret_cast(data), .isbinary = 1, .binarylen = static_cast(lease_->hwaddr_.size()) }; + params.push_back(pdest); + + if(lease_->client_id_) { + vector client_data = lease_->client_id_->getClientId(); + PgSqlParam pclient_dest = { .value = reinterpret_cast(&client_data[0]), .isbinary = 1, .binarylen = static_cast(lease_->client_id_->getClientId().size()) }; + params.push_back(pclient_dest); + } else { + params.push_back(PgSqlParam()); + } + + string valid_lft_str; + tmp << static_cast(lease_->valid_lft_); + PgSqlParam pvalid_lft = { .value = tmp.str() }; + params.push_back(pvalid_lft); + tmp.str(""); + tmp.clear(); + time_t expire_ = lease_->valid_lft_ + lease_->cltt_; + char buffer_[20] = { 0 }; + convertToTimestamp(expire_, buffer_); + PgSqlParam pbuffer = { .value = buffer_ }; + params.push_back(pbuffer); + string subnet_id_str; + tmp << static_cast(lease_->subnet_id_); + PgSqlParam psubnet_id = { .value = tmp.str() }; + params.push_back(psubnet_id); + + BOOST_STATIC_ASSERT(5 < LEASE_COLUMNS); + + return (params); + } + + Lease4Ptr + convertFromDatabase(PGresult *& r, int line) { + const char * addr4_str = PQgetvalue(r, line, 0); + unsigned char * hwaddr_str = PQunescapeBytea( + (const unsigned char *) + PQgetvalue(r, line, 1), + & hwaddr_length_); + unsigned char * client_id_str = PQunescapeBytea( + (const unsigned char *) + PQgetvalue(r, line, 2), + & client_id_length_); + const char * valid_lifetime_str = PQgetvalue(r, line, 3); + const char * expire_str = PQgetvalue(r, line, 4); + const char * subnet_id_str = PQgetvalue(r, line, 5); + unsigned long valid_lifetime, expire, subnet_id; + + istringstream tmp; + tmp.str(addr4_str); + tmp >> addr4_; + tmp.str(""); + tmp.clear(); + + memcpy(hwaddr_buffer_, hwaddr_str, hwaddr_length_); + memcpy(client_id_buffer_, client_id_str, client_id_length_); + + PQfreemem(client_id_str); + PQfreemem(hwaddr_str); + + tmp.str(valid_lifetime_str); + tmp >> valid_lifetime; + tmp.str(""); + tmp.clear(); + valid_lifetime_ = static_cast(valid_lifetime); + + tmp.str(expire_str); + tmp >> expire; + tmp.str(""); + tmp.clear(); + expire_ = static_cast(expire); + + tmp.str(subnet_id_str); + tmp >> subnet_id; + subnet_id_ = static_cast(subnet_id); + + time_t cltt = expire_ - valid_lifetime_; + + return (Lease4Ptr(new Lease4(addr4_, hwaddr_buffer_, hwaddr_length_, + client_id_buffer_, client_id_length_, + valid_lifetime_, 0, 0, cltt, subnet_id_))); + } + +private: + uint32_t addr4_; + string columns_[LEASE_COLUMNS]; + vector hwaddr_; + uint8_t hwaddr_buffer_[HWAddr::MAX_HWADDR_LEN]; + uint8_t client_id_buffer_[ClientId::MAX_CLIENT_ID_LEN]; + Lease4Ptr lease_; + time_t expire_; + uint32_t subnet_id_; + uint32_t valid_lifetime_; + unsigned long hwaddr_length_; + unsigned long client_id_length_; +}; + +class PgSqlLease6Exchange : public PgSqlLeaseExchange { + static const size_t LEASE_COLUMNS = 9; +public: + PgSqlLease6Exchange() { + memset(duid_buffer_, 0, sizeof(duid_buffer_)); + // Set the column names (for error messages) + columns_[0] = "address"; + columns_[1] = "duid"; + columns_[2] = "valid_lifetime"; + columns_[3] = "expire"; + columns_[4] = "subnet_id"; + columns_[5] = "pref_lifetime"; + columns_[6] = "lease_type"; + columns_[7] = "iaid"; + columns_[8] = "prefix_len"; + BOOST_STATIC_ASSERT(8 < LEASE_COLUMNS); + + params.reserve(LEASE_COLUMNS); + } + + bindparams + createBindForSend(const Lease6Ptr& lease) { + lease_ = lease; + params.clear(); + ostringstream tmp; + + PgSqlParam paddr6 = { .value = lease_->addr_.toText() }; + + params.push_back(paddr6); + vector duid_data = lease_->duid_->getDuid(); + PgSqlParam pdest = { .value = reinterpret_cast(&duid_data[0]) + , .isbinary = 1, .binarylen = static_cast(lease_->duid_->getDuid().size()) }; + params.push_back(pdest); + + string valid_lft_str; + tmp << static_cast(lease_->valid_lft_); + PgSqlParam pvalid_lft = { .value = tmp.str() }; + params.push_back(pvalid_lft); + tmp.str(""); + tmp.clear(); + + time_t expire_ = lease_->valid_lft_ + lease_->cltt_; + char buffer_[20] = { 0 }; + convertToTimestamp(expire_, buffer_); + PgSqlParam pbuffer = { .value = buffer_ }; + params.push_back(pbuffer); + + tmp << static_cast(lease_->subnet_id_); + PgSqlParam psubnet_id = { .value = tmp.str() }; + params.push_back(psubnet_id); + tmp.str(""); + tmp.clear(); + + tmp << static_cast(lease_->preferred_lft_); + PgSqlParam preferred_lft = { .value = tmp.str() }; + params.push_back(preferred_lft); + tmp.str(""); + tmp.clear(); + + tmp << static_cast(lease_->type_); + PgSqlParam type = { .value = tmp.str() }; + params.push_back(type); + tmp.str(""); + tmp.clear(); + + tmp << static_cast(lease_->iaid_); + PgSqlParam iaid = { .value = tmp.str() }; + params.push_back(iaid); + tmp.str(""); + tmp.clear(); + + tmp << static_cast(lease_->prefixlen_); + PgSqlParam prefixlen = { .value = tmp.str() }; + params.push_back(prefixlen); + + BOOST_STATIC_ASSERT(8 < LEASE_COLUMNS); + + return (params); + } + + Lease6Ptr + convertFromDatabase(PGresult * r, int line) { + const char * addr6_str = PQgetvalue(r, line, 0); + unsigned char * duid_str = PQunescapeBytea( + (const unsigned char *) + PQgetvalue(r, line, 1), + & duid_length_); + const char * valid_lifetime_str = PQgetvalue(r, line, 2); + const char * expire_str = PQgetvalue(r, line, 3); + const char * subnet_id_str = PQgetvalue(r, line, 4); + const char * pref_lifetime_str = PQgetvalue(r, line, 5); + const char * lease_type_str = PQgetvalue(r, line, 6); + const char * iaid_str = PQgetvalue(r, line, 7); + const char * prefixlen_str = PQgetvalue(r, line, 8); + unsigned int lease_type, prefixlen; + unsigned long valid_lifetime, expire, subnet_id, pref_lifetime, iaid; + + istringstream tmp; + + addr6_ = addr6_str; + isc::asiolink::IOAddress addr(addr6_); + + memcpy(duid_buffer_, duid_str, duid_length_); + + PQfreemem(duid_str); + + tmp.str(valid_lifetime_str); + tmp >> valid_lifetime; + tmp.str(""); + tmp.clear(); + valid_lifetime_ = static_cast(valid_lifetime); + + tmp.str(expire_str); + tmp >> expire; + tmp.str(""); + tmp.clear(); + expire_ = static_cast(expire); + + tmp.str(subnet_id_str); + tmp >> subnet_id; + tmp.str(""); + tmp.clear(); + subnet_id_ = static_cast(subnet_id); + + tmp.str(pref_lifetime_str); + tmp >> pref_lifetime; + tmp.str(""); + tmp.clear(); + pref_lifetime_ = static_cast(pref_lifetime); + + tmp.str(lease_type_str); + tmp >> lease_type; + tmp.str(""); + tmp.clear(); + + tmp.str(iaid_str); + tmp >> iaid; + tmp.str(""); + tmp.clear(); + iaid_ = static_cast(iaid); + + tmp.str(prefixlen_str); + tmp >> prefixlen; + prefixlen_ = static_cast(prefixlen); + + Lease6::Type type = Lease6::TYPE_NA; + switch (lease_type) { + case Lease6::TYPE_NA: + type = Lease6::TYPE_NA; + break; + + case Lease6::TYPE_TA: + type = Lease6::TYPE_TA; + break; + + case Lease6::TYPE_PD: + type = Lease6::TYPE_PD; + break; + + default: + isc_throw(BadValue, "invalid lease type returned (" << + lease_type_ << ") for lease with address " << + addr6_ << ". Only 0, 1, or 2 are allowed."); + } + + // Set up DUID, + DuidPtr duid_ptr(new DUID(duid_buffer_, duid_length_)); + + Lease6Ptr result(new Lease6(type, addr, duid_ptr, iaid_, + pref_lifetime_, valid_lifetime_, 0, 0, + subnet_id_, prefixlen_)); + + time_t cltt = expire_ - valid_lifetime_; + result->cltt_ = cltt; + + return (result); + } + +private: + string addr6_; + // char addr6_buffer_[ADDRESS6_TEXT_MAX_LEN + 1]; + string columns_[LEASE_COLUMNS]; + time_t expire_; + vector duid_; + uint8_t duid_buffer_[DUID::MAX_DUID_LEN]; + uint32_t iaid_; + Lease6Ptr lease_; + uint8_t lease_type_; + uint8_t prefixlen_; + uint32_t pref_lifetime_; + uint32_t subnet_id_; + uint32_t valid_lifetime_; + unsigned long duid_length_; +}; + +PgSqlLeaseMgr::PgSqlLeaseMgr(const LeaseMgr::ParameterMap& parameters) + : LeaseMgr(parameters){ + status = NULL; + openDatabase(); + prepareStatements(); + + exchange4_.reset(new PgSqlLease4Exchange()); + exchange6_.reset(new PgSqlLease6Exchange()); +} + +PgSqlLeaseMgr::~PgSqlLeaseMgr() { + if(status) { + // Attempt to deallocate prepared queries set previously with DEALLOCATE query + // No internal libpq function for that, no errors checking as well + PGresult * r; + for(int i = 0; tagged_statements[i].text != NULL; ++ i) { + string deallocate = "DEALLOCATE \""; + deallocate += tagged_statements[i].name; + deallocate += "\""; + r = PQexec(status, deallocate.c_str()); + PQclear(r); + } + + PQfinish(status); + } +} + +void PgSqlLeaseMgr::prepareStatements() { + statements_.clear(); + statements_.resize(NUM_STATEMENTS, PgSqlStatementBind()); + + PGresult * r = PQexec(status, "SET AUTOCOMMIT TO OFF"); + PQclear(r); + + for(int i = 0; tagged_statements[i].text != NULL; ++ i) { + string checkstatement = "SELECT * FROM pg_prepared_statements " + "WHERE name = '"; + checkstatement += tagged_statements[i].name; + checkstatement += "'"; + + r = PQexec(status, checkstatement.c_str()); + + if(!PQntuples(r)) { + PQclear(r); + r = PQexec(status, "BEGIN"); + PQclear(r); + // Prepare all statements queries with all known fields datatype + // No need to remind them when called + r = PQprepare(status, 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(status)); + } + + PQclear(r); + + r = PQexec(status, "END"); + PQclear(r); + + statements_[i].stmt_name = tagged_statements[i].name; + statements_[i].stmt_nbparams = tagged_statements[i].nbparams; + } else { + PQclear(r); + } + } + + r = PQexec(status, "SET AUTOCOMMIT TO ON"); + 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 "NoName" exception + isc_throw(NoDatabaseName, "must specified a name for the database"); + } + + status = PQconnectdb(dbconnparameters.c_str()); + if(status == NULL || PQstatus(status) != CONNECTION_OK) { + isc_throw(DbOpenError, PQerrorMessage(status)); + } +} + +bool +PgSqlLeaseMgr::addLeaseCommon(StatementIndex stindex, + bindparams & params) { + + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, + DHCPSRV_PGSQL_ADD_ADDR4).arg(statements_[stindex].stmt_name); + + vector params_; + vector lengths_; + vector formats_; + convertToQuery(params, params_, lengths_, formats_); + + PGresult * r = PQexecPrepared(status, statements_[stindex].stmt_name, + statements_[stindex].stmt_nbparams, + ¶ms_[0], &lengths_[0], &formats_[0], 0); + + int s = PQresultStatus(r); + if (s != PGRES_COMMAND_OK) { + const char * errorMsg = PQerrorMessage(status); + PQclear(r); + + if(!strncmp(errorMsg, "ERROR: duplicate key", + sizeof("ERROR: duplicate key") - 1)) { + return (false); + } + + isc_throw(DbOperationError, "unable to INSERT for <" << + statements_[stindex].stmt_name << ">, " << + errorMsg); + } + + PQclear(r); + + return (true); +} + +bool +PgSqlLeaseMgr::addLease(const Lease4Ptr& lease) { + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, + DHCPSRV_PGSQL_ADD_ADDR4).arg(lease->addr_.toText()); + bindparams params = exchange4_->createBindForSend(lease); + + return (addLeaseCommon(INSERT_LEASE4, params)); +} + +bool +PgSqlLeaseMgr::addLease(const Lease6Ptr& lease) { + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, + DHCPSRV_PGSQL_ADD_ADDR6).arg(lease->addr_.toText()); + bindparams params = exchange6_->createBindForSend(lease); + + return (addLeaseCommon(INSERT_LEASE6, params)); +} + +template +void PgSqlLeaseMgr::getLeaseCollection(StatementIndex stindex, + bindparams & params, + Exchange& exchange, + LeaseCollection& result, + bool single) const { + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, + DHCPSRV_PGSQL_GET_ADDR4).arg(statements_[stindex].stmt_name); + + vector params_; + vector lengths_; + vector formats_; + convertToQuery(params, params_, lengths_, formats_); + + PGresult * r = PQexec(status, "SET AUTOCOMMIT TO OFF"); + PQclear(r); + + r = PQexec(status, "BEGIN"); + PQclear(r); + + r = PQexecPrepared(status, statements_[stindex].stmt_name, + statements_[stindex].stmt_nbparams, + ¶ms_[0], &lengths_[0], &formats_[0], 0); + + checkError(r, stindex, "unable to bind WHERE clause parameter"); + PQclear(r); + + string fetchall = "FETCH ALL in "; + fetchall += statements_[stindex].stmt_name; + + r = PQexec(status, fetchall.c_str()); + checkError(r, stindex, "unable to FETCH ALL"); + + int lines = PQntuples(r); + + if(single && lines > 1) { + PQclear(r); + r = PQexec(status, "END"); + PQclear(r); + isc_throw(MultipleRecords, "multiple records were found in the " + "database where only one was expected for query " + << statements_[stindex].stmt_name); + } + + for(int i = 0; i < lines; ++ i) { + result.push_back(exchange->convertFromDatabase(r, i)); + } + + PQclear(r); + + string closecursor = "CLOSE "; + closecursor += statements_[stindex].stmt_name; + r = PQexec(status, closecursor.c_str()); + PQclear(r); + + r = PQexec(status, "END"); + PQclear(r); + + r = PQexec(status, "SET AUTOCOMMIT TO ON"); + PQclear(r); +} + +void +PgSqlLeaseMgr::getLease(StatementIndex stindex, bindparams & params, + Lease4Ptr& result) const { + // Create appropriate collection object and get all leases matching + // the selection criteria. The "single" paraeter 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, params, 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, bindparams & params, + Lease6Ptr& result) const { + // Create appropriate collection object and get all leases matching + // the selection criteria. The "single" paraeter 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, params, 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 + bindparams inparams; + ostringstream tmp; + string baddr; + + tmp << addr; + PgSqlParam addr4 = { .value = tmp.str() }; + + inparams.push_back(addr4); + + // Get the data + Lease4Ptr result; + getLease(GET_LEASE4_ADDR, inparams, 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 + bindparams inparams; + + uint8_t* data = const_cast(&hwaddr.hwaddr_[0]); + PgSqlParam pdest = { .value = reinterpret_cast(data) + , .isbinary = 1, .binarylen = static_cast(hwaddr.hwaddr_.size()) }; + inparams.push_back(pdest); + + // Get the data + Lease4Collection result; + getLeaseCollection(GET_LEASE4_HWADDR, inparams, 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 + bindparams inparams; + ostringstream tmp; + + uint8_t* data = const_cast(&hwaddr.hwaddr_[0]); + PgSqlParam pdest = { .value = reinterpret_cast(data) + , .isbinary = 1, .binarylen = static_cast(hwaddr.hwaddr_.size()) }; + inparams.push_back(pdest); + + tmp << static_cast(subnet_id); + PgSqlParam psubnet_id = { .value = tmp.str() }; + + inparams.push_back(psubnet_id); + + // Get the data + Lease4Ptr result; + getLease(GET_LEASE4_HWADDR_SUBID, inparams, 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 + bindparams inparams; + + vector client_data = clientid.getClientId(); + PgSqlParam pdest = { .value = reinterpret_cast(&client_data[0]) + , .isbinary = 1, .binarylen = static_cast(clientid.getClientId().size()) }; + inparams.push_back(pdest); + + // Get the data + Lease4Collection result; + getLeaseCollection(GET_LEASE4_CLIENTID, inparams, 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 + bindparams inparams; + ostringstream tmp; + + vector client_data = clientid.getClientId(); + PgSqlParam pdest = { .value = reinterpret_cast(&client_data[0]) + , .isbinary = 1, .binarylen = static_cast(clientid.getClientId().size()) }; + inparams.push_back(pdest); + + tmp << static_cast(subnet_id); + PgSqlParam psubnet_id = { .value = tmp.str() }; + inparams.push_back(psubnet_id); + + // Get the data + Lease4Ptr result; + getLease(GET_LEASE4_CLIENTID_SUBID, inparams, result); + + return (result); +} + +Lease4Ptr +PgSqlLeaseMgr::getLease4(const ClientId& /*client_id*/, const HWAddr& /*hwaddr*/, + SubnetID /*subnet_id*/) const { + /// @todo + + Lease4Ptr result; + return (result); +} + +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 + bindparams inparams; + + PgSqlParam addr6 = { .value = addr.toText() }; + + /// @todo: use type param + + inparams.push_back(addr6); + + Lease6Ptr result; + getLease(GET_LEASE6_ADDR, inparams, result); + + return (result); +} + +Lease6Collection +PgSqlLeaseMgr::getLeases6(Lease::Type 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(type); + + // Set up the WHERE clause value + bindparams inparams; + ostringstream tmp; + vector duid_data = duid.getDuid(); + PgSqlParam pdest = { .value = reinterpret_cast(&duid_data[0]) + , .isbinary = 1, .binarylen = static_cast(duid.getDuid().size()) }; + inparams.push_back(pdest); + + /// @todo: use type + + // IAID + tmp << static_cast(iaid); + PgSqlParam piaid = { .value = tmp.str() }; + inparams.push_back(piaid); + + // ... and get the data + Lease6Collection result; + getLeaseCollection(GET_LEASE6_DUID_IAID, inparams, 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_MYSQL_GET_IAID_SUBID_DUID) + .arg(iaid).arg(subnet_id).arg(duid.toText()).arg(lease_type); + + // Set up the WHERE clause value + bindparams inparams; + ostringstream tmp; + + /// @todo: use type + + // See the earlier description of the use of "const_cast" when accessing + // the DUID for an explanation of the reason. + vector duid_data = duid.getDuid(); + PgSqlParam pdest = { .value = reinterpret_cast(&duid_data[0]) + , .isbinary = 1, .binarylen = static_cast(duid.getDuid().size()) }; + inparams.push_back(pdest); + + // IAID + tmp << static_cast(iaid); + PgSqlParam piaid = { .value = tmp.str() }; + inparams.push_back(piaid); + tmp.str(""); + tmp.clear(); + + // Subnet ID + tmp << static_cast(subnet_id); + PgSqlParam psubnet_id = { .value = tmp.str() }; + inparams.push_back(psubnet_id); + tmp.str(""); + tmp.clear(); + + Lease6Collection result; + getLeaseCollection(GET_LEASE6_DUID_IAID_SUBID, inparams, result); + + return (result); +} + +template +void +PgSqlLeaseMgr::updateLeaseCommon(StatementIndex stindex, bindparams & params, + const LeasePtr& lease) { + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, + DHCPSRV_PGSQL_ADD_ADDR4).arg(statements_[stindex].stmt_name); + + vector params_; + vector lengths_; + vector formats_; + convertToQuery(params, params_, lengths_, formats_); + + PGresult * r = PQexecPrepared(status, statements_[stindex].stmt_name, + statements_[stindex].stmt_nbparams, + ¶ms_[0], &lengths_[0], &formats_[0], 0); + checkError(r, stindex, "unable to bind UPDATE parameters"); + int affected_rows = boost::lexical_cast(PQcmdTuples(r)); + + PQclear(r); + + if (!affected_rows) { + isc_throw(NoSuchLease, "unable to update lease for address " << + lease->addr_.toText() << " as it does not exist"); + } else if (affected_rows > 1) { + // 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 MYSQL_BIND array for the data being updated + ostringstream tmp; + bindparams params = exchange4_->createBindForSend(lease); + + // Set up the WHERE clause and append it to the MYSQL_BIND array + tmp << lease->addr_; + PgSqlParam addr4 = { .value = tmp.str() }; + params.push_back(addr4); + + // Drop to common update code + updateLeaseCommon(stindex, params, 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 MYSQL_BIND array for the data being updated + bindparams params = exchange6_->createBindForSend(lease); + + // Set up the WHERE clause and append it to the MYSQL_BIND array + PgSqlParam addr6 = { .value = lease->addr_.toText() }; + params.push_back(addr6); + + // Drop to common update code + updateLeaseCommon(stindex, params, lease); +} + +bool +PgSqlLeaseMgr::deleteLeaseCommon(StatementIndex stindex, bindparams & params) { + vector params_; + vector lengths_; + vector formats_; + convertToQuery(params, params_, lengths_, formats_); + + PGresult * r = PQexecPrepared(status, statements_[stindex].stmt_name, + statements_[stindex].stmt_nbparams, + ¶ms_[0], &lengths_[0], &formats_[0], 0); + checkError(r, stindex, "unable to bind WHERE clause parameter"); + 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 + bindparams inparams; + + if (addr.isV4()) { + ostringstream tmp; + tmp << addr; + PgSqlParam addr4 = { .value = tmp.str() }; + + inparams.push_back(addr4); + + return (deleteLeaseCommon(DELETE_LEASE4, inparams)); + + } else { + PgSqlParam addr6 = { .value = addr.toText() }; + + inparams.push_back(addr6); + + return (deleteLeaseCommon(DELETE_LEASE6, inparams)); + } +} + +string +PgSqlLeaseMgr::getName() const { + string name = ""; + try { + name = getParameter("name"); + } catch (...) { + // Return an empty name + } + return (name); +} + +string +PgSqlLeaseMgr::getDescription() const { + return (string("PostgreSQL Database")); +} + +pair +PgSqlLeaseMgr::getVersion() const { + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, + DHCPSRV_PGSQL_GET_VERSION).arg("get_version"); + + PGresult * r = PQexec(status, "SET AUTOCOMMIT TO OFF"); + PQclear(r); + + r = PQexec(status, "BEGIN"); + PQclear(r); + + r = PQexecPrepared(status, "get_version", + 0, + NULL, NULL, NULL, 0); + + PQclear(r); + + r = PQexec(status, "FETCH ALL in get_version"); + + const char * version_str = PQgetvalue(r, 0, 0); + const char * minor_str = PQgetvalue(r, 0, 1); + + istringstream tmp; + + uint32_t version; + tmp.str(version_str); + tmp >> version; + tmp.str(""); + tmp.clear(); + + uint32_t minor; + tmp.str(minor_str); + tmp >> minor; + + PQclear(r); + r = PQexec(status, "CLOSE get_version"); + PQclear(r); + + r = PQexec(status, "END"); + PQclear(r); + + r = PQexec(status, "SET AUTOCOMMIT TO ON"); + PQclear(r); + + return make_pair(version, minor); +} + +void +PgSqlLeaseMgr::commit() { + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_MYSQL_COMMIT); + PGresult * r = PQexec(status, "COMMIT"); + if (PQresultStatus(r) != PGRES_COMMAND_OK) { + isc_throw(DbOperationError, "commit failed: " << PQerrorMessage(status)); + } + + PQclear(r); +} + +void +PgSqlLeaseMgr::rollback() { + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_MYSQL_ROLLBACK); + PGresult * r = PQexec(status, "ROLLBACK"); + if (PQresultStatus(r) != PGRES_COMMAND_OK) { + isc_throw(DbOperationError, "rollback failed: " << PQerrorMessage(status)); + } + + PQclear(r); +} + +}; // end of isc::dhcp namespace +}; // end of isc namespace diff --git a/src/lib/dhcpsrv/pgsql_lease_mgr.h b/src/lib/dhcpsrv/pgsql_lease_mgr.h new file mode 100644 index 0000000000..2bda355683 --- /dev/null +++ b/src/lib/dhcpsrv/pgsql_lease_mgr.h @@ -0,0 +1,160 @@ +// Copyright (C) 2013-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. + +#ifndef PGSQL_LEASE_MGR_H +#define PGSQL_LEASE_MGR_H + +#include +#include + +#include +#include +#include + +namespace isc { +namespace dhcp { + +struct PgSqlParam { + std::string value; + int isbinary; + int binarylen; +}; + +typedef std::vector bindparams; + +struct PgSqlStatementBind { + const char * stmt_name; + int stmt_nbparams; +}; + +class PgSqlLease4Exchange; +class PgSqlLease6Exchange; + +const uint32_t PG_CURRENT_VERSION = 1; +const uint32_t PG_CURRENT_MINOR = 0; + +class PgSqlLeaseMgr : public LeaseMgr { +public: + PgSqlLeaseMgr(const ParameterMap& parameters); + virtual ~PgSqlLeaseMgr(); + virtual bool addLease(const Lease4Ptr& lease); + virtual bool addLease(const Lease6Ptr& lease); + + virtual Lease4Ptr getLease4(const isc::asiolink::IOAddress& addr) const; + virtual Lease4Collection getLease4(const isc::dhcp::HWAddr& hwaddr) const; + virtual Lease4Ptr getLease4(const isc::dhcp::HWAddr& hwaddr, + SubnetID subnet_id) const; + virtual Lease4Collection getLease4(const ClientId& clientid) const; + virtual Lease4Ptr getLease4(const ClientId& client_id, const HWAddr& hwaddr, + SubnetID subnet_id) const; + virtual Lease4Ptr getLease4(const ClientId& clientid, + SubnetID subnet_id) const; + + virtual Lease6Ptr getLease6(Lease::Type type, + const isc::asiolink::IOAddress& addr) const; + virtual Lease6Collection getLeases6(Lease::Type type, const DUID& duid, + uint32_t iaid) const; + virtual Lease6Collection getLeases6(Lease::Type type, const DUID& duid, + uint32_t iaid, SubnetID subnet_id) const; + + virtual void updateLease4(const Lease4Ptr& lease4); + virtual void updateLease6(const Lease6Ptr& lease6); + virtual bool deleteLease(const isc::asiolink::IOAddress& addr); + virtual std::string getType() const { + return (std::string("postgresql")); + } + virtual std::string getName() const; + virtual std::string getDescription() const; + virtual std::pair getVersion() const; + virtual void commit(); + virtual void rollback(); + enum StatementIndex { + DELETE_LEASE4, // Delete from lease4 by address + DELETE_LEASE6, // Delete from lease6 by address + GET_LEASE4_ADDR, // Get lease4 by address + GET_LEASE4_CLIENTID, // Get lease4 by client ID + GET_LEASE4_CLIENTID_SUBID, // Get lease4 by client ID & subnet ID + GET_LEASE4_HWADDR, // Get lease4 by HW address + GET_LEASE4_HWADDR_SUBID, // Get lease4 by HW address & subnet ID + GET_LEASE6_ADDR, // Get lease6 by address + GET_LEASE6_DUID_IAID, // Get lease6 by DUID and IAID + GET_LEASE6_DUID_IAID_SUBID, // Get lease6 by DUID, IAID and subnet ID + GET_VERSION, // Obtain version number + INSERT_LEASE4, // Add entry to lease4 table + INSERT_LEASE6, // Add entry to lease6 table + UPDATE_LEASE4, // Update a Lease4 entry + UPDATE_LEASE6, // Update a Lease6 entry + NUM_STATEMENTS // Number of statements + }; +private: + void prepareStatements(); + void openDatabase(); + bool addLeaseCommon(StatementIndex stindex, bindparams & params); + template + void getLeaseCollection(StatementIndex stindex, bindparams & params, + Exchange& exchange, LeaseCollection& result, + bool single = false) const; + void getLeaseCollection(StatementIndex stindex, bindparams & params, + Lease4Collection& result) const { + getLeaseCollection(stindex, params, exchange4_, result); + } + void getLeaseCollection(StatementIndex stindex, bindparams & params, + Lease6Collection& result) const { + getLeaseCollection(stindex, params, exchange6_, result); + } + inline void checkError(PGresult * r, StatementIndex index, + const char* what) const { + int s = PQresultStatus(r); + if (s != PGRES_COMMAND_OK && s != PGRES_TUPLES_OK) { + PQclear(r); + + isc_throw(DbOperationError, what << " for <" << + statements_[index].stmt_name << ">, " << + PQerrorMessage(status)); + } + } + inline void convertToQuery(bindparams & params, + std::vector& params_, + std::vector& lengths_, + std::vector& formats_) const { + params_.reserve(params.size()); + lengths_.reserve(params.size()); + formats_.reserve(params.size()); + + for(bindparams::const_iterator it = params.begin(); it != params.end(); + ++ it) { + params_.push_back((* it).value.c_str()); + lengths_.push_back((* it).binarylen); + formats_.push_back((* it).isbinary); + } + } + void getLease(StatementIndex stindex, bindparams & params, + Lease4Ptr& result) const; + void getLease(StatementIndex stindex, bindparams & params, + Lease6Ptr& result) const; + template + void updateLeaseCommon(StatementIndex stindex, bindparams & params, + const LeasePtr& lease); + bool deleteLeaseCommon(StatementIndex stindex, bindparams & params); + + boost::scoped_ptr exchange4_; + boost::scoped_ptr exchange6_; + std::vector statements_; + PGconn * status; +}; + +}; // end of isc::dhcp namespace +}; // end of isc namespace + +#endif // PGSQL_LEASE_MGR_H diff --git a/src/lib/dhcpsrv/tests/Makefile.am b/src/lib/dhcpsrv/tests/Makefile.am index 9e135920ae..4c030a00df 100644 --- a/src/lib/dhcpsrv/tests/Makefile.am +++ b/src/lib/dhcpsrv/tests/Makefile.am @@ -67,8 +67,12 @@ libdhcpsrv_unittests_SOURCES += dhcp_parsers_unittest.cc if HAVE_MYSQL libdhcpsrv_unittests_SOURCES += mysql_lease_mgr_unittest.cc endif +if HAVE_PGSQL +libdhcpsrv_unittests_SOURCES += pgsql_lease_mgr_unittest.cc +endif libdhcpsrv_unittests_SOURCES += pool_unittest.cc -libdhcpsrv_unittests_SOURCES += schema_copy.h +libdhcpsrv_unittests_SOURCES += schema_mysql_copy.h +libdhcpsrv_unittests_SOURCES += schema_pgsql_copy.h libdhcpsrv_unittests_SOURCES += subnet_unittest.cc libdhcpsrv_unittests_SOURCES += test_get_callout_handle.cc test_get_callout_handle.h libdhcpsrv_unittests_SOURCES += triplet_unittest.cc @@ -78,11 +82,17 @@ libdhcpsrv_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INC if HAVE_MYSQL libdhcpsrv_unittests_CPPFLAGS += $(MYSQL_CPPFLAGS) endif +if HAVE_PGSQL +libdhcpsrv_unittests_CPPFLAGS += $(PGSQL_CPPFLAGS) +endif libdhcpsrv_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) if HAVE_MYSQL libdhcpsrv_unittests_LDFLAGS += $(MYSQL_LIBS) endif +if HAVE_PGSQL +libdhcpsrv_unittests_LDFLAGS += $(PGSQL_LIBS) +endif libdhcpsrv_unittests_CXXFLAGS = $(AM_CXXFLAGS) if USE_CLANGPP diff --git a/src/lib/dhcpsrv/tests/pgsql_lease_mgr_unittest.cc b/src/lib/dhcpsrv/tests/pgsql_lease_mgr_unittest.cc new file mode 100644 index 0000000000..1e4e529af0 --- /dev/null +++ b/src/lib/dhcpsrv/tests/pgsql_lease_mgr_unittest.cc @@ -0,0 +1,453 @@ +// 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 +#include + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::dhcp; +using namespace isc::dhcp::test; +using namespace std; + +namespace { + +// This holds statements to create and destroy the schema. +#include "schema_pgsql_copy.h" + +// Connection strings. +// Database: keatest +// Host: localhost +// Username: keatest +// Password: keatest +const char* VALID_TYPE = "type=postgresql"; +const char* INVALID_TYPE = "type=unknown"; +const char* VALID_NAME = "name=keatest"; +const char* INVALID_NAME = "name=invalidname"; +const char* VALID_HOST = "host=localhost"; +const char* INVALID_HOST = "host=invalidhost"; +const char* VALID_USER = "user=keatest"; +const char* INVALID_USER = "user=invaliduser"; +const char* VALID_PASSWORD = "password=keatest"; +const char* INVALID_PASSWORD = "password=invalid"; + +// Given a combination of strings above, produce a connection string. +string connectionString(const char* type, const char* name, const char* host, + const char* user, const char* password) { + const string space = " "; + string result = ""; + + if (type != NULL) { + result += string(type); + } + if (name != NULL) { + if (! result.empty()) { + result += space; + } + result += string(name); + } + + if (host != NULL) { + if (! result.empty()) { + result += space; + } + result += string(host); + } + + if (user != NULL) { + if (! result.empty()) { + result += space; + } + result += string(user); + } + + if (password != NULL) { + if (! result.empty()) { + result += space; + } + result += string(password); + } + + return (result); +} + +// Return valid connection string +string +validConnectionString() { + return (connectionString(VALID_TYPE, VALID_NAME, VALID_HOST, + VALID_USER, VALID_PASSWORD)); +} + +// @brief Clear everything from the database +// +// There is no error checking in this code: if something fails, one of the +// tests will (should) fall over. +void destroySchema() { + // Open database + PGconn * conn = 0; + conn = PQconnectdb("host = 'localhost' user = 'keatest'" + " password = 'keatest' dbname = 'keatest'"); + + PGresult * r; + // Get rid of everything in it. + for (int i = 0; destroy_statement[i] != NULL; ++i) { + r = PQexec(conn, destroy_statement[i]); + PQclear(r); + } + + PQfinish(conn); +} + +// @brief Create the Schema +// +// Creates all the tables in what is assumed to be an empty database. +// +// There is no error checking in this code: if it fails, one of the tests +// will fall over. +void createSchema() { + // Open database + PGconn * conn = 0; + conn = PQconnectdb("host = 'localhost' user = 'keatest'" + " password = 'keatest' dbname = 'keatest'"); + + PGresult * r; + // Get rid of everything in it. + for (int i = 0; create_statement[i] != NULL; ++i) { + r = PQexec(conn, create_statement[i]); + PQclear(r); + } +} + +/// @brief Test fixture class for testing PostgreSQL Lease Manager +/// +/// Opens the database prior to each test and closes it afterwards. +/// All pending transactions are deleted prior to closure. + +class PgSqlLeaseMgrTest : public GenericLeaseMgrTest { +public: + /// @brief Constructor + /// + /// Deletes everything from the database and opens it. + PgSqlLeaseMgrTest() { + + // Ensure schema is the correct one. + destroySchema(); + createSchema(); + + // Connect to the database + try { + LeaseMgrFactory::create(validConnectionString()); + } catch (...) { + std::cerr << "*** ERROR: unable to open database. The test\n" + "*** environment is broken and must be fixed before\n" + "*** the PostgreSQL tests will run correctly.\n" + "*** The reason for the problem is described in the\n" + "*** accompanying exception output.\n"; + throw; + } + lmptr_ = &(LeaseMgrFactory::instance()); + } + + /// @brief Destructor + /// + /// Rolls back all pending transactions. The deletion of lmptr_ will close + /// the database. Then reopen it and delete everything created by the test. + virtual ~PgSqlLeaseMgrTest() { + lmptr_->rollback(); + LeaseMgrFactory::destroy(); + destroySchema(); + } + + /// @brief Reopen the database + /// + /// Closes the database and re-open it. Anything committed should be + /// visible. + void reopen() { + LeaseMgrFactory::destroy(); + LeaseMgrFactory::create(validConnectionString()); + lmptr_ = &(LeaseMgrFactory::instance()); + } + +}; + +/// @brief Check that database can be opened +/// +/// This test checks if the PgSqlLeaseMgr can be instantiated. This happens +/// only if the database can be opened. Note that this is not part of the +/// PgSqlLeaseMgr test fixure set. This test checks that the database can be +/// opened: the fixtures assume that and check basic operations. + +TEST(PgSqlOpenTest, OpenDatabase) { + + // Schema needs to be created for the test to work. + destroySchema(); + createSchema(); + + // Check that lease manager open the database opens correctly and tidy up. + // If it fails, print the error message. + try { + LeaseMgrFactory::create(validConnectionString()); + EXPECT_NO_THROW((void) LeaseMgrFactory::instance()); + LeaseMgrFactory::destroy(); + } catch (const isc::Exception& ex) { + FAIL() << "*** ERROR: unable to open database, reason:\n" + << " " << ex.what() << "\n" + << "*** The test environment is broken and must be fixed\n" + << "*** before the PostgreSQL tests will run correctly.\n"; + } + + // Check that attempting to get an instance of the lease manager when + // none is set throws an exception. + EXPECT_THROW(LeaseMgrFactory::instance(), NoLeaseManager); + + // Check that wrong specification of backend throws an exception. + // (This is really a check on LeaseMgrFactory, but is convenient to + // perform here.) + EXPECT_THROW(LeaseMgrFactory::create(connectionString( + NULL, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)), + InvalidParameter); + EXPECT_THROW(LeaseMgrFactory::create(connectionString( + INVALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)), + InvalidType); + + // Check that invalid login data causes an exception. + EXPECT_THROW(LeaseMgrFactory::create(connectionString( + VALID_TYPE, INVALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)), + DbOpenError); + EXPECT_THROW(LeaseMgrFactory::create(connectionString( + VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)), + DbOpenError); + EXPECT_THROW(LeaseMgrFactory::create(connectionString( + VALID_TYPE, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)), + DbOpenError); + EXPECT_THROW(LeaseMgrFactory::create(connectionString( + VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, INVALID_PASSWORD)), + DbOpenError); + + // Check for missing parameters + EXPECT_THROW(LeaseMgrFactory::create(connectionString( + VALID_TYPE, NULL, VALID_HOST, INVALID_USER, VALID_PASSWORD)), + NoDatabaseName); + + // Tidy up after the test + destroySchema(); +} + +/// @brief Check the getType() method +/// +/// getType() returns a string giving the type of the backend, which should +/// always be "postgresql". +TEST_F(PgSqlLeaseMgrTest, getType) { + EXPECT_EQ(std::string("postgresql"), lmptr_->getType()); +} + +/// @brief Check getName() returns correct database name +TEST_F(PgSqlLeaseMgrTest, getName) { + EXPECT_EQ(std::string("keatest"), lmptr_->getName()); +} + +/// @brief Check that getVersion() returns the expected version +TEST_F(PgSqlLeaseMgrTest, checkVersion) { + // Check version + pair version; + ASSERT_NO_THROW(version = lmptr_->getVersion()); + EXPECT_EQ(PG_CURRENT_VERSION, version.first); + EXPECT_EQ(PG_CURRENT_MINOR, version.second); +} + +/// @brief Basic Lease4 Checks +/// +/// Checks that the addLease, getLease4 (by address) and deleteLease (with an +/// IPv4 address) works. +TEST_F(PgSqlLeaseMgrTest, basicLease4) { + // Get the leases to be used for the test. + vector leases = createLeases4(); + + // Start the tests. Add three leases to the database, read them back and + // check they are what we think they are. + EXPECT_TRUE(lmptr_->addLease(leases[1])); + EXPECT_TRUE(lmptr_->addLease(leases[2])); + EXPECT_TRUE(lmptr_->addLease(leases[3])); + lmptr_->commit(); + + // Reopen the database to ensure that they actually got stored. + reopen(); + + Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[1]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[1], l_returned); + + l_returned = lmptr_->getLease4(ioaddress4_[2]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[2], l_returned); + + l_returned = lmptr_->getLease4(ioaddress4_[3]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[3], l_returned); + + // Check that we can't add a second lease with the same address + EXPECT_FALSE(lmptr_->addLease(leases[1])); + + // Delete a lease, check that it's gone, and that we can't delete it + // a second time. + EXPECT_TRUE(lmptr_->deleteLease(ioaddress4_[1])); + l_returned = lmptr_->getLease4(ioaddress4_[1]); + EXPECT_FALSE(l_returned); + EXPECT_FALSE(lmptr_->deleteLease(ioaddress4_[1])); + + // Check that the second address is still there. + l_returned = lmptr_->getLease4(ioaddress4_[2]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[2], l_returned); +} + +/// @brief Basic Lease4 Checks +/// +/// Checks that the addLease, getLease4(by address), getLease4(hwaddr,subnet_id), +/// updateLease4() and deleteLease (IPv4 address) can handle NULL client-id. +/// (client-id is optional and may not be present) +TEST_F(PgSqlLeaseMgrTest, lease4NullClientId) { + // Get the leases to be used for the test. + vector leases = createLeases4(); + + // Let's clear client-id pointers + leases[1]->client_id_ = ClientIdPtr(); + leases[2]->client_id_ = ClientIdPtr(); + leases[3]->client_id_ = ClientIdPtr(); + + // Start the tests. Add three leases to the database, read them back and + // check they are what we think they are. + EXPECT_TRUE(lmptr_->addLease(leases[1])); + EXPECT_TRUE(lmptr_->addLease(leases[2])); + EXPECT_TRUE(lmptr_->addLease(leases[3])); + lmptr_->commit(); + + // Reopen the database to ensure that they actually got stored. + reopen(); + + Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[1]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[1], l_returned); + + l_returned = lmptr_->getLease4(ioaddress4_[2]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[2], l_returned); + + l_returned = lmptr_->getLease4(ioaddress4_[3]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[3], l_returned); + + // Check that we can't add a second lease with the same address + EXPECT_FALSE(lmptr_->addLease(leases[1])); + + // Check that we can get the lease by HWAddr + HWAddr tmp(leases[2]->hwaddr_, HTYPE_ETHER); + Lease4Collection returned = lmptr_->getLease4(tmp); + ASSERT_EQ(1, returned.size()); + detailCompareLease(leases[2], *returned.begin()); + + l_returned = lmptr_->getLease4(tmp, leases[2]->subnet_id_); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[2], l_returned); + + + // Check that we can update the lease + // Modify some fields in lease 1 (not the address) and update it. + ++leases[1]->subnet_id_; + leases[1]->valid_lft_ *= 2; + lmptr_->updateLease4(leases[1]); + + // ... and check that the lease is indeed updated + l_returned = lmptr_->getLease4(ioaddress4_[1]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[1], l_returned); + + + + // Delete a lease, check that it's gone, and that we can't delete it + // a second time. + EXPECT_TRUE(lmptr_->deleteLease(ioaddress4_[1])); + l_returned = lmptr_->getLease4(ioaddress4_[1]); + EXPECT_FALSE(l_returned); + EXPECT_FALSE(lmptr_->deleteLease(ioaddress4_[1])); + + // Check that the second address is still there. + l_returned = lmptr_->getLease4(ioaddress4_[2]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[2], l_returned); + +} + +/// @brief Basic Lease6 Checks +/// +/// Checks that the addLease, getLease6 (by address) and deleteLease (with an +/// IPv6 address) works. +TEST_F(PgSqlLeaseMgrTest, basicLease6) { + // Get the leases to be used for the test. + vector leases = createLeases6(); + + // Start the tests. Add three leases to the database, read them back and + // check they are what we think they are. + EXPECT_TRUE(lmptr_->addLease(leases[1])); + EXPECT_TRUE(lmptr_->addLease(leases[2])); + EXPECT_TRUE(lmptr_->addLease(leases[3])); + lmptr_->commit(); + + // Reopen the database to ensure that they actually got stored. + reopen(); + + Lease6Ptr l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[1], l_returned); + + l_returned = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[2], l_returned); + + l_returned = lmptr_->getLease6(leasetype6_[3], ioaddress6_[3]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[3], l_returned); + + // Check that we can't add a second lease with the same address + EXPECT_FALSE(lmptr_->addLease(leases[1])); + + // Delete a lease, check that it's gone, and that we can't delete it + // a second time. + EXPECT_TRUE(lmptr_->deleteLease(ioaddress6_[1])); + l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]); + EXPECT_FALSE(l_returned); + EXPECT_FALSE(lmptr_->deleteLease(ioaddress6_[1])); + + // Check that the second address is still there. + l_returned = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]); + ASSERT_TRUE(l_returned); + detailCompareLease(leases[2], l_returned); +} + +}; diff --git a/src/lib/dhcpsrv/tests/schema_copy.h b/src/lib/dhcpsrv/tests/schema_mysql_copy.h similarity index 100% rename from src/lib/dhcpsrv/tests/schema_copy.h rename to src/lib/dhcpsrv/tests/schema_mysql_copy.h diff --git a/src/lib/dhcpsrv/tests/schema_pgsql_copy.h b/src/lib/dhcpsrv/tests/schema_pgsql_copy.h new file mode 100644 index 0000000000..6290e6bd5a --- /dev/null +++ b/src/lib/dhcpsrv/tests/schema_pgsql_copy.h @@ -0,0 +1,89 @@ +// 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. + +#ifndef SCHEMA_COPY_H +#define SCHEMA_COPY_H + +namespace { + +// What follows is a set of statements that creates a copy of the schema +// in the test database. It is used by the PostgreSQL unit test prior to each +// test. +// +// Each SQL statement is a single string. The statements are not terminated +// by semicolons, and the strings must end with a comma. The final line +// statement must be NULL (not in quotes) + +// NOTE: This file mirrors the schema in src/lib/dhcpsrv/dhcpdb_create.pgsql. +// If this file is altered, please ensure that any change is compatible +// with the schema in dhcpdb_create.pgsql. + +// Deletion of existing tables. + +const char* destroy_statement[] = { + "DROP TABLE lease4", + "DROP TABLE lease6", + "DROP TABLE lease6_types", + "DROP TABLE schema_version", + NULL +}; + +// Creation of the new tables. + +const char* create_statement[] = { + "START TRANSACTION", + "CREATE TABLE lease4 (" + "address BIGINT PRIMARY KEY NOT NULL," + "hwaddr BYTEA," + "client_id BYTEA," + "valid_lifetime BIGINT," + "expire TIMESTAMP," + "subnet_id BIGINT" + ")", + + "CREATE TABLE lease6 (" + "address VARCHAR(39) PRIMARY KEY NOT NULL," + "duid BYTEA," + "valid_lifetime BIGINT," + "expire TIMESTAMP," + "subnet_id BIGINT," + "pref_lifetime BIGINT," + "lease_type SMALLINT," + "iaid BIGINT," + "prefix_len SMALLINT" + ")", + + "CREATE TABLE lease6_types (" + "lease_type SMALLINT PRIMARY KEY NOT NULL," + "name VARCHAR(5)" + ")", + + "INSERT INTO lease6_types VALUES (0, 'IA_NA')", + "INSERT INTO lease6_types VALUES (1, 'IA_TA')", + "INSERT INTO lease6_types VALUES (2, 'IA_PD')", + + "CREATE TABLE schema_version (" + "version INT PRIMARY KEY NOT NULL," + "minor INT" + ")", + + "INSERT INTO schema_version VALUES (1, 0)", + "COMMIT", + + NULL +}; + +}; // Anonymous namespace + +#endif // SCHEMA_COPY_H