Browse Source

[master] Merge branch 'trac3682_rebase' (MySQL Host Data Source)

Tomek Mrugalski 9 years ago
parent
commit
275b40fb94

+ 3 - 0
src/lib/dhcpsrv/Makefile.am

@@ -103,9 +103,11 @@ libkea_dhcpsrv_la_SOURCES += d2_client_cfg.cc d2_client_cfg.h
 libkea_dhcpsrv_la_SOURCES += d2_client_mgr.cc d2_client_mgr.h
 libkea_dhcpsrv_la_SOURCES += daemon.cc daemon.h
 libkea_dhcpsrv_la_SOURCES += database_connection.cc database_connection.h
+libkea_dhcpsrv_la_SOURCES += db_exceptions.h
 libkea_dhcpsrv_la_SOURCES += dhcpsrv_log.cc dhcpsrv_log.h
 libkea_dhcpsrv_la_SOURCES += host.cc host.h
 libkea_dhcpsrv_la_SOURCES += host_container.h
+libkea_dhcpsrv_la_SOURCES += host_data_source_factory.cc host_data_source_factory.h
 libkea_dhcpsrv_la_SOURCES += host_mgr.cc host_mgr.h
 libkea_dhcpsrv_la_SOURCES += hosts_log.cc hosts_log.h
 libkea_dhcpsrv_la_SOURCES += key_from_key.h
@@ -122,6 +124,7 @@ libkea_dhcpsrv_la_SOURCES += memfile_lease_storage.h
 if HAVE_MYSQL
 libkea_dhcpsrv_la_SOURCES += mysql_lease_mgr.cc mysql_lease_mgr.h
 libkea_dhcpsrv_la_SOURCES += mysql_connection.cc mysql_connection.h
+libkea_dhcpsrv_la_SOURCES += mysql_host_data_source.cc mysql_host_data_source.h
 endif
 
 libkea_dhcpsrv_la_SOURCES += ncr_generator.cc ncr_generator.h

+ 32 - 0
src/lib/dhcpsrv/base_host_data_source.h

@@ -64,6 +64,20 @@ public:
 class BaseHostDataSource {
 public:
 
+    /// @brief Specifies the type of an identifier.
+    ///
+    /// This is currently used only by MySQL host data source for now, but
+    /// it is envisagad that it will be used by other host data sources
+    /// in the future. Also, this list will grow over time. It is likely
+    /// that we'll implement other identifiers in the future, e.g. remote-id.
+    ///
+    /// Those value correspond direclty to dhcp_identifier_type in hosts
+    /// table in MySQL schema.
+    enum IdType {
+        ID_HWADDR = 0, ///< Hardware address
+        ID_DUID = 1    ///< DUID/client-id
+    };
+
     /// @brief Default destructor implementation.
     virtual ~BaseHostDataSource() { }
 
@@ -180,6 +194,24 @@ public:
     /// @param host Pointer to the new @c Host object being added.
     virtual void add(const HostPtr& host) = 0;
 
+    /// @brief Return backend type
+    ///
+    /// Returns the type of the backend (e.g. "mysql", "memfile" etc.)
+    ///
+    /// @return Type of the backend.
+    virtual std::string getType() const = 0;
+
+    /// @brief Commit Transactions
+    ///
+    /// Commits all pending database operations.  On databases that don't
+    /// support transactions, this is a no-op.
+    virtual void commit() {};
+
+    /// @brief Rollback Transactions
+    ///
+    /// Rolls back all pending database operations.  On databases that don't
+    /// support transactions, this is a no-op.
+    virtual void rollback() {};
 };
 
 }

+ 9 - 0
src/lib/dhcpsrv/cfg_hosts.h

@@ -237,6 +237,15 @@ public:
     /// has already been added to the IPv4 or IPv6 subnet.
     virtual void add(const HostPtr& host);
 
+    /// @brief Return backend type
+    ///
+    /// Returns the type of the backend (e.g. "mysql", "memfile" etc.)
+    ///
+    /// @return Type of the backend.
+    virtual std::string getType() const {
+        return (std::string("configuration file"));
+    }
+
 private:
 
     /// @brief Returns @c Host objects for the specific identifier and type.

+ 2 - 0
src/lib/dhcpsrv/database_connection.cc

@@ -25,6 +25,8 @@ using namespace std;
 namespace isc {
 namespace dhcp {
 
+const time_t DatabaseConnection::MAX_DB_TIME = 2147483647;
+
 std::string
 DatabaseConnection::getParameter(const std::string& name) const {
     ParameterMap::const_iterator param = parameters_.find(name);

+ 9 - 0
src/lib/dhcpsrv/database_connection.h

@@ -55,6 +55,15 @@ public:
 /// @ref BaseHostDataSource derived classes.
 class DatabaseConnection : public boost::noncopyable {
 public:
+
+    /// @brief Defines maximum value for time that can be reliably stored.
+    ///
+    /// @todo: Is this common for MySQL and Postgres? Maybe we should have
+    /// specific values for each backend?
+    ///
+    /// If I'm still alive I'll be too old to care. You fix it.
+    static const time_t MAX_DB_TIME;
+
     /// @brief Database configuration parameter map
     typedef std::map<std::string, std::string> ParameterMap;
 

+ 54 - 0
src/lib/dhcpsrv/db_exceptions.h

@@ -0,0 +1,54 @@
+// Copyright (C) 2015 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 DB_EXCEPTIONS_H
+#define DB_EXCEPTIONS_H
+
+#include <exceptions/exceptions.h>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Multiple lease records found where one expected
+class MultipleRecords : public Exception {
+public:
+    MultipleRecords(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) {}
+};
+
+/// @brief Attempt to update lease that was not there
+class NoSuchLease : public Exception {
+public:
+    NoSuchLease(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) {}
+};
+
+/// @brief Data is truncated
+class DataTruncated : public Exception {
+public:
+    DataTruncated(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) {}
+};
+
+/// @brief Database duplicate entry error
+class DuplicateEntry : public Exception {
+public:
+    DuplicateEntry(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) {}
+};
+
+};
+};
+
+#endif

+ 113 - 0
src/lib/dhcpsrv/host_data_source_factory.cc

@@ -0,0 +1,113 @@
+// Copyright (C) 2015 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 "config.h"
+
+#include <dhcpsrv/dhcpsrv_log.h>
+#include <dhcpsrv/host_data_source_factory.h>
+#include <dhcpsrv/hosts_log.h>
+
+#ifdef HAVE_MYSQL
+#include <dhcpsrv/mysql_host_data_source.h>
+#endif
+
+#include <boost/algorithm/string.hpp>
+#include <boost/foreach.hpp>
+#include <boost/scoped_ptr.hpp>
+
+#include <algorithm>
+#include <iostream>
+#include <iterator>
+#include <map>
+#include <sstream>
+#include <utility>
+
+using namespace std;
+
+namespace isc {
+namespace dhcp {
+
+
+boost::scoped_ptr<BaseHostDataSource>&
+HostDataSourceFactory::getHostDataSourcePtr() {
+    static boost::scoped_ptr<BaseHostDataSource> hostDataSourcePtr;
+    return (hostDataSourcePtr);
+}
+
+void
+HostDataSourceFactory::create(const std::string& dbaccess) {
+    const std::string type = "type";
+
+    // Parse the access string and create a redacted string for logging.
+    DatabaseConnection::ParameterMap parameters =
+            DatabaseConnection::parse(dbaccess);
+    std::string redacted =
+            DatabaseConnection::redactedAccessString(parameters);
+
+    // Is "type" present?
+    if (parameters.find(type) == parameters.end()) {
+        LOG_ERROR(dhcpsrv_logger, DHCPSRV_NOTYPE_DB).arg(dbaccess);
+        isc_throw(InvalidParameter, "Database configuration parameters do not "
+                  "contain the 'type' keyword");
+    }
+
+
+    // Yes, check what it is.
+#ifdef HAVE_MYSQL
+    if (parameters[type] == string("mysql")) {
+        LOG_INFO(dhcpsrv_logger, DHCPSRV_MYSQL_DB).arg(redacted);
+        getHostDataSourcePtr().reset(new MySqlHostDataSource(parameters));
+        return;
+    }
+#endif
+
+#ifdef HAVE_PGSQL
+    if (parameters[type] == string("postgresql")) {
+        LOG_INFO(dhcpsrv_logger, DHCPSRV_PGSQL_DB).arg(redacted);
+        isc_throw(NotImplemented, "Sorry, Postgres backend for host reservations "
+                  "is not implemented yet.");
+        // Set pgsql data source here, when it will be implemented.
+        return;
+    }
+#endif
+
+    // Get here on no match.
+    LOG_ERROR(dhcpsrv_logger, DHCPSRV_UNKNOWN_DB).arg(parameters[type]);
+    isc_throw(InvalidType, "Database access parameter 'type' does "
+              "not specify a supported database backend");
+}
+
+void
+HostDataSourceFactory::destroy() {
+    // Destroy current host data source instance.  This is a no-op if no host
+    // data source is available.
+    if (getHostDataSourcePtr()) {
+        LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, HOSTS_CFG_CLOSE_HOST_DATA_SOURCE)
+            .arg(getHostDataSourcePtr()->getType());
+    }
+    getHostDataSourcePtr().reset();
+}
+
+BaseHostDataSource&
+HostDataSourceFactory::instance() {
+    BaseHostDataSource* hdsptr = getHostDataSourcePtr().get();
+    if (hdsptr == NULL) {
+        isc_throw(NoHostDataSourceManager,
+                "no current host data source instance is available");
+    }
+    return (*hdsptr);
+}
+
+}; // namespace dhcp
+}; // namespace isc

+ 114 - 0
src/lib/dhcpsrv/host_data_source_factory.h

@@ -0,0 +1,114 @@
+// Copyright (C) 2015 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 HOST_DATA_SOURCE_FACTORY_H
+#define HOST_DATA_SOURCE_FACTORY_H
+
+#include <dhcpsrv/base_host_data_source.h>
+#include <dhcpsrv/database_connection.h>
+#include <exceptions/exceptions.h>
+#include <boost/scoped_ptr.hpp>
+
+#include <string>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Invalid type exception
+///
+/// Thrown when the factory doesn't recognise the type of the backend.
+class InvalidType : public Exception {
+public:
+    InvalidType(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) {}
+};
+
+/// @brief No host data source instance exception
+///
+/// Thrown if an attempt is made to get a reference to the current
+/// host data source instance and none is currently available.
+class NoHostDataSourceManager : public Exception {
+public:
+    NoHostDataSourceManager(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) {}
+};
+
+/// @brief Host Data Source Factory
+///
+/// This class comprises nothing but static methods used to create a host data source object.
+/// It analyzes the database information passed to the creation function and instantiates
+/// an appropriate host data source object based on the type requested.
+///
+/// Strictly speaking these functions could be stand-alone functions.  However,
+/// it is convenient to encapsulate them in a class for naming purposes.
+
+class HostDataSourceFactory {
+public:
+    /// @brief Create an instance of a host data source.
+    ///
+    /// Each database backend has its own host data source type. This static
+    /// method sets the "current" host data source to be an object of the
+    /// appropriate type.  The actual host data source is returned by the
+    /// "instance" method.
+    ///
+    /// @note When called, the current host data source is <b>always</b> destroyed
+    ///       and a new one created - even if the parameters are the same.
+    ///
+    /// dbaccess is a generic way of passing parameters. Parameters are passed
+    /// in the "name=value" format, separated by spaces.  The data MUST include
+    /// a keyword/value pair of the form "type=dbtype" giving the database
+    /// type, e.q. "mysql" or "sqlite3".
+    ///
+    /// @param dbaccess Database access parameters.  These are in the form of
+    ///        "keyword=value" pairs, separated by spaces. They are backend-
+    ///        -end specific, although must include the "type" keyword which
+    ///        gives the backend in use.
+    ///
+    /// @throw isc::InvalidParameter dbaccess string does not contain the "type"
+    ///        keyword.
+    /// @throw isc::dhcp::InvalidType The "type" keyword in dbaccess does not
+    ///        identify a supported backend.
+    static void create(const std::string& dbaccess);
+
+    /// @brief Destroy host data source
+    ///
+    /// Destroys the current host data source object. This should have the effect
+    /// of closing the database connection.  The method is a no-op if no
+    /// host data source is available.
+    static void destroy();
+
+    /// @brief Return current host data source
+    ///
+    /// @returns An instance of the "current" host data source.  An exception
+    /// will be thrown if none is available.
+    ///
+    /// @throw NoHostDataSourceManager No host data source is available: use
+    ///        create() to create one before calling this method.
+    static BaseHostDataSource& instance();
+
+private:
+    /// @brief Hold pointer to host data source instance
+    ///
+    /// Holds a pointer to the singleton host data source.  The singleton
+    /// is encapsulated in this method to avoid a "static initialization
+    /// fiasco" if defined in an external static variable.
+    static boost::scoped_ptr<BaseHostDataSource>& getHostDataSourcePtr();
+
+};
+
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
+
+#endif

+ 10 - 4
src/lib/dhcpsrv/host_mgr.cc

@@ -17,6 +17,7 @@
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/host_mgr.h>
 #include <dhcpsrv/hosts_log.h>
+#include <dhcpsrv/host_data_source_factory.h>
 
 namespace {
 
@@ -37,6 +38,8 @@ namespace dhcp {
 
 using namespace isc::asiolink;
 
+boost::shared_ptr<BaseHostDataSource> HostMgr::alternate_source;
+
 boost::scoped_ptr<HostMgr>&
 HostMgr::getHostMgrPtr() {
     static boost::scoped_ptr<HostMgr> host_mgr_ptr;
@@ -44,11 +47,15 @@ HostMgr::getHostMgrPtr() {
 }
 
 void
-HostMgr::create(const std::string&) {
+HostMgr::create(const std::string& access) {
     getHostMgrPtr().reset(new HostMgr());
 
-    /// @todo Initialize alternate_source here, using the parameter.
-    /// For example: alternate_source.reset(new MysqlHostDataSource(access)).
+    if (!access.empty()) {
+        HostDataSourceFactory::create(access);
+
+        /// @todo Initialize alternate_source here.
+        //alternate_source = HostDataSourceFactory::getHostDataSourcePtr();
+    }
 }
 
 HostMgr&
@@ -152,7 +159,6 @@ HostMgr::get6(const SubnetID& subnet_id,
     return (host);
 }
 
-
 void
 HostMgr::add(const HostPtr&) {
     isc_throw(isc::NotImplemented, "HostMgr::add is not implemented");

+ 10 - 1
src/lib/dhcpsrv/host_mgr.h

@@ -198,6 +198,15 @@ public:
     /// @param host Pointer to the new @c Host object being added.
     virtual void add(const HostPtr& host);
 
+    /// @brief Return backend type
+    ///
+    /// Returns the type of the backend (e.g. "mysql", "memfile" etc.)
+    ///
+    /// @return Type of the backend.
+    virtual std::string getType() const {
+        return (std::string("host_mgr"));
+    }
+
 private:
 
     /// @brief Private default constructor.
@@ -206,7 +215,7 @@ private:
     /// @brief Pointer to an alternate host data source.
     ///
     /// If this pointer is NULL, the source is not in use.
-    boost::scoped_ptr<BaseHostDataSource> alternate_source;
+    static boost::shared_ptr<BaseHostDataSource> alternate_source;
 
     /// @brief Returns a pointer to the currently used instance of the
     /// @c HostMgr.

+ 4 - 0
src/lib/dhcpsrv/hosts_messages.mes

@@ -19,6 +19,10 @@ This debug message is issued when new host (with reservations) is added to
 the server's configuration. The argument describes the host and its
 reservations in detail.
 
+% HOSTS_CFG_CLOSE_HOST_DATA_SOURCE Closing host data source: %1
+This is a normal message being printed when the server closes host data
+source connection.
+
 % HOSTS_CFG_GET_ALL_ADDRESS4 get all hosts with reservations for IPv4 address %1
 This debug message is issued when starting to retrieve all hosts, holding the
 reservation for the specific IPv4 address, from the configuration. The

+ 0 - 2
src/lib/dhcpsrv/lease_mgr.cc

@@ -35,8 +35,6 @@ using namespace std;
 namespace isc {
 namespace dhcp {
 
-const time_t LeaseMgr::MAX_DB_TIME = 2147483647;
-
 Lease6Ptr
 LeaseMgr::getLease6(Lease::Type type, const DUID& duid,
                     uint32_t iaid, SubnetID subnet_id) const {

+ 1 - 27
src/lib/dhcpsrv/lease_mgr.h

@@ -22,7 +22,7 @@
 #include <dhcp/hwaddr.h>
 #include <dhcpsrv/lease.h>
 #include <dhcpsrv/subnet.h>
-#include <exceptions/exceptions.h>
+#include <dhcpsrv/db_exceptions.h>
 
 #include <boost/noncopyable.hpp>
 #include <boost/shared_ptr.hpp>
@@ -68,28 +68,6 @@
 namespace isc {
 namespace dhcp {
 
-/// @brief Multiple lease records found where one expected
-class MultipleRecords : public Exception {
-public:
-    MultipleRecords(const char* file, size_t line, const char* what) :
-        isc::Exception(file, line, what) {}
-};
-
-/// @brief Attempt to update lease that was not there
-class NoSuchLease : public Exception {
-public:
-    NoSuchLease(const char* file, size_t line, const char* what) :
-        isc::Exception(file, line, what) {}
-};
-
-/// @brief Data is truncated
-class DataTruncated : public Exception {
-public:
-    DataTruncated(const char* file, size_t line, const char* what) :
-        isc::Exception(file, line, what) {}
-};
-
-
 /// @brief Abstract Lease Manager
 ///
 /// This is an abstract API for lease database backends. It provides unified
@@ -102,10 +80,6 @@ public:
 /// of those classes for details.
 class LeaseMgr {
 public:
-    /// @brief Defines maximum value for time that can be reliably stored.
-    // If I'm still alive I'll be too old to care. You fix it.
-    static const time_t MAX_DB_TIME;
-
     /// @brief Constructor
     ///
     LeaseMgr()

+ 98 - 0
src/lib/dhcpsrv/mysql_connection.cc

@@ -192,8 +192,106 @@ MySqlConnection::~MySqlConnection() {
     }
     statements_.clear();
     text_statements_.clear();
+}
+
+// Time conversion methods.
+//
+// Note that the MySQL TIMESTAMP data type (used for "expire") converts data
+// from the current timezone to UTC for storage, and from UTC to the current
+// timezone for retrieval.
+//
+// This causes no problems providing that:
+// a) cltt is given in local time
+// b) We let the system take care of timezone conversion when converting
+//    from a time read from the database into a local time.
+void
+MySqlConnection::convertToDatabaseTime(const time_t input_time,
+                                       MYSQL_TIME& output_time) {
+
+    // Convert to broken-out time
+    struct tm time_tm;
+    (void) localtime_r(&input_time, &time_tm);
+
+    // Place in output expire structure.
+    output_time.year = time_tm.tm_year + 1900;
+    output_time.month = time_tm.tm_mon + 1;     // Note different base
+    output_time.day = time_tm.tm_mday;
+    output_time.hour = time_tm.tm_hour;
+    output_time.minute = time_tm.tm_min;
+    output_time.second = time_tm.tm_sec;
+    output_time.second_part = 0;                // No fractional seconds
+    output_time.neg = my_bool(0);               // Not negative
+}
+
+void
+MySqlConnection::convertToDatabaseTime(const time_t cltt,
+                                     const uint32_t valid_lifetime,
+                                     MYSQL_TIME& expire) {
+
+    // Calculate expiry time. Store it in the 64-bit value so as we can detect
+    // overflows.
+    int64_t expire_time_64 = static_cast<int64_t>(cltt) +
+        static_cast<int64_t>(valid_lifetime);
+
+    // Even on 64-bit systems MySQL doesn't seem to accept the timestamps
+    // beyond the max value of int32_t.
+    if (expire_time_64 > DatabaseConnection::MAX_DB_TIME) {
+        isc_throw(BadValue, "Time value is too large: " << expire_time_64);
+    }
+
+    const time_t expire_time = static_cast<const time_t>(expire_time_64);
 
+    // Convert to broken-out time
+    struct tm expire_tm;
+    (void) localtime_r(&expire_time, &expire_tm);
+
+    // Place in output expire structure.
+    expire.year = expire_tm.tm_year + 1900;
+    expire.month = expire_tm.tm_mon + 1;     // Note different base
+    expire.day = expire_tm.tm_mday;
+    expire.hour = expire_tm.tm_hour;
+    expire.minute = expire_tm.tm_min;
+    expire.second = expire_tm.tm_sec;
+    expire.second_part = 0;                  // No fractional seconds
+    expire.neg = my_bool(0);                 // Not negative
+}
+
+void
+MySqlConnection::convertFromDatabaseTime(const MYSQL_TIME& expire,
+                                       uint32_t valid_lifetime, time_t& cltt) {
+
+    // Copy across fields from MYSQL_TIME structure.
+    struct tm expire_tm;
+    memset(&expire_tm, 0, sizeof(expire_tm));
+
+    expire_tm.tm_year = expire.year - 1900;
+    expire_tm.tm_mon = expire.month - 1;
+    expire_tm.tm_mday = expire.day;
+    expire_tm.tm_hour = expire.hour;
+    expire_tm.tm_min = expire.minute;
+    expire_tm.tm_sec = expire.second;
+    expire_tm.tm_isdst = -1;    // Let the system work out about DST
+
+    // Convert to local time
+    cltt = mktime(&expire_tm) - valid_lifetime;
 }
 
+void MySqlConnection::commit() {
+        LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_MYSQL_COMMIT);
+        if (mysql_commit(mysql_) != 0) {
+                isc_throw(DbOperationError, "commit failed: "
+                        << mysql_error(mysql_));
+        }
+}
+
+void MySqlConnection::rollback() {
+        LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_MYSQL_ROLLBACK);
+        if (mysql_rollback(mysql_) != 0) {
+                isc_throw(DbOperationError, "rollback failed: "
+                        << mysql_error(mysql_));
+        }
+}
+
+
 } // namespace isc::dhcp
 } // namespace isc

+ 83 - 1
src/lib/dhcpsrv/mysql_connection.h

@@ -17,7 +17,7 @@
 
 #include <dhcpsrv/database_connection.h>
 #include <boost/scoped_ptr.hpp>
-#include <mysql.h>
+#include <mysql/mysql.h>
 #include <vector>
 
 namespace isc {
@@ -32,6 +32,11 @@ namespace dhcp {
 extern const my_bool MLM_FALSE;
 extern const my_bool MLM_TRUE;
 
+// Define the current database schema values
+
+const uint32_t CURRENT_VERSION_VERSION = 3;
+const uint32_t CURRENT_VERSION_MINOR = 0;
+
 /// @brief Fetch and Release MySQL Results
 ///
 /// When a MySQL statement is expected, to fetch the results the function
@@ -189,6 +194,83 @@ public:
     /// @throw DbOpenError Error opening the database
     void openDatabase();
 
+    ///@{
+    /// The following methods are used to convert between times and time
+    /// intervals stored in the Lease object, and the times stored in the
+    /// database.  The reason for the difference is because in the DHCP server,
+    /// the cltt (Client Time Since Last Transmission) is the natural data; in
+    /// the lease file - which may be read by the user - it is the expiry time
+    /// of the lease.
+
+    /// @brief Convert time_t value to database time.
+    ///
+    /// @param input_time A time_t value representing time.
+    /// @param output_time Reference to MYSQL_TIME object where converted time
+    ///        will be put.
+    static
+    void convertToDatabaseTime(const time_t input_time, MYSQL_TIME& output_time);
+
+    /// @brief Convert Lease Time to Database Times
+    ///
+    /// Within the DHCP servers, times are stored as client last transmit time
+    /// and valid lifetime.  In the database, the information is stored as
+    /// valid lifetime and "expire" (time of expiry of the lease).  They are
+    /// related by the equation:
+    ///
+    /// - expire = client last transmit time + valid lifetime
+    ///
+    /// This method converts from the times in the lease object into times
+    /// able to be added to the database.
+    ///
+    /// @param cltt Client last transmit time
+    /// @param valid_lifetime Valid lifetime
+    /// @param expire Reference to MYSQL_TIME object where the expiry time of
+    ///        the lease will be put.
+    ///
+    /// @throw isc::BadValue if the sum of the calculated expiration time is
+    /// greater than the value of @c LeaseMgr::MAX_DB_TIME.
+    static
+    void convertToDatabaseTime(const time_t cltt, const uint32_t valid_lifetime,
+            MYSQL_TIME& expire);
+
+    /// @brief Convert Database Time to Lease Times
+    ///
+    /// Within the database, time is stored as "expire" (time of expiry of the
+    /// lease) and valid lifetime.  In the DHCP server, the information is
+    /// stored client last transmit time and valid lifetime.  These are related
+    /// by the equation:
+    ///
+    /// - client last transmit time = expire - valid_lifetime
+    ///
+    /// This method converts from the times in the database into times
+    /// able to be inserted into the lease object.
+    ///
+    /// @param expire Reference to MYSQL_TIME object from where the expiry
+    ///        time of the lease is taken.
+    /// @param valid_lifetime lifetime of the lease.
+    /// @param cltt Reference to location where client last transmit time
+    ///        is put.
+    static
+    void convertFromDatabaseTime(const MYSQL_TIME& expire,
+            uint32_t valid_lifetime, time_t& cltt);
+    ///@}
+
+    /// @brief Commit Transactions
+    ///
+    /// Commits all pending database operations. On databases that don't
+    /// support transactions, this is a no-op.
+    ///
+    /// @throw DbOperationError If the commit failed.
+    void commit();
+
+    /// @brief Rollback Transactions
+    ///
+    /// Rolls back all pending database operations. On databases that don't
+    /// support transactions, this is a no-op.
+    ///
+    /// @throw DbOperationError If the rollback failed.
+    void rollback();
+
     /// @brief Prepared statements
     ///
     /// This field is public, because it is used heavily from MySqlConnection

File diff suppressed because it is too large
+ 1037 - 0
src/lib/dhcpsrv/mysql_host_data_source.cc


+ 324 - 0
src/lib/dhcpsrv/mysql_host_data_source.h

@@ -0,0 +1,324 @@
+// Copyright (C) 2015 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 MYSQL_HOST_DATA_SOURCE_H
+#define MYSQL_HOST_DATA_SOURCE_H
+
+#include <dhcp/hwaddr.h>
+#include <dhcpsrv/base_host_data_source.h>
+#include <dhcpsrv/mysql_connection.h>
+
+#include <boost/scoped_ptr.hpp>
+#include <boost/utility.hpp>
+#include <mysql/mysql.h>
+
+namespace isc {
+namespace dhcp {
+
+// Forward declaration of the Host exchange objects.  These classes are defined
+// in the .cc file.
+class MySqlHostReservationExchange;
+
+/// @brief MySQL Host Data Source
+///
+/// This class provides the @ref isc::dhcp::BaseHostDataSource interface to the MySQL
+/// database.  Use of this backend presupposes that a MySQL database is
+/// available and that the Kea schema has been created within it.
+class MySqlHostDataSource: public BaseHostDataSource {
+public:
+
+    /// @brief Constructor
+    ///
+    /// Uses the following keywords in the parameters passed to it to
+    /// connect to the database:
+    /// - name - Name of the database to which to connect (mandatory)
+    /// - host - Host to which to connect (optional, defaults to "localhost")
+    /// - user - Username under which to connect (optional)
+    /// - password - Password for "user" on the database (optional)
+    ///
+    /// If the database is successfully opened, the version number in the
+    /// schema_version table will be checked against hard-coded value in
+    /// the implementation file.
+    ///
+    /// Finally, all the SQL commands are pre-compiled.
+    ///
+    /// @param parameters A data structure relating keywords and values
+    ///        concerned with the database.
+    ///
+    /// @throw isc::dhcp::NoDatabaseName Mandatory database name not given
+    /// @throw isc::dhcp::DbOpenError Error opening the database
+    /// @throw isc::dhcp::DbOperationError An operation on the open database has
+    ///        failed.
+    MySqlHostDataSource(const DatabaseConnection::ParameterMap& parameters);
+
+    /// @brief Destructor (closes database)
+    virtual ~MySqlHostDataSource();
+
+    /// @brief Return all hosts for the specified HW address or DUID.
+    ///
+    /// This method returns all @c Host objects which represent reservations
+    /// for the specified HW address or DUID. Note, that this method may
+    /// return multiple reservations because a particular client may have
+    /// reservations in multiple subnets and the same client may be identified
+    /// by HW address or DUID. The server is unable to verify that the specific
+    /// DUID and HW address belong to the same client, until the client sends
+    /// a DHCP message.
+    ///
+    /// Specifying both hardware address and DUID is allowed for this method
+    /// and results in returning all objects that are associated with hardware
+    /// address OR duid. For example: if one host is associated with the
+    /// specified hardware address and another host is associated with the
+    /// specified DUID, two hosts will be returned.
+    ///
+    /// @param hwaddr HW address of the client or NULL if no HW address
+    /// available.
+    /// @param duid client id or NULL if not available, e.g. DHCPv4 client case.
+    ///
+    /// @return Collection of const @c Host objects.
+    virtual ConstHostCollection
+    getAll(const HWAddrPtr& hwaddr, const DuidPtr& duid = DuidPtr()) const;
+
+    /// @brief Returns a collection of hosts using the specified IPv4 address.
+    ///
+    /// This method may return multiple @c Host objects if they are connected
+    /// to different subnets.
+    ///
+    /// @param address IPv4 address for which the @c Host object is searched.
+    ///
+    /// @return Collection of const @c Host objects.
+    virtual ConstHostCollection
+    getAll4(const asiolink::IOAddress& address) const;
+
+    /// @brief Returns a host connected to the IPv4 subnet.
+    ///
+    /// Implementations of this method should guard against the case when
+    /// mutliple instances of the @c Host are present, e.g. when two
+    /// @c Host objects are found, one for the DUID, another one for the
+    /// HW address. In such case, an implementation of this method
+    /// should throw an MultipleRecords exception.
+    ///
+    /// @param subnet_id Subnet identifier.
+    /// @param hwaddr HW address of the client or NULL if no HW address
+    /// available.
+    /// @param duid client id or NULL if not available.
+    ///
+    /// @return Const @c Host object using a specified HW address or DUID.
+    virtual ConstHostPtr
+    get4(const SubnetID& subnet_id, const HWAddrPtr& hwaddr,
+            const DuidPtr& duid = DuidPtr()) const;
+
+    /// @brief Returns a host connected to the IPv4 subnet and having
+    /// a reservation for a specified IPv4 address.
+    ///
+    /// One of the use cases for this method is to detect collisions between
+    /// dynamically allocated addresses and reserved addresses. When the new
+    /// address is assigned to a client, the allocation mechanism should check
+    /// if this address is not reserved for some other host and do not allocate
+    /// this address if reservation is present.
+    ///
+    /// Implementations of this method should guard against invalid addresses,
+    /// such as IPv6 address.
+    ///
+    /// @param subnet_id Subnet identifier.
+    /// @param address reserved IPv4 address.
+    ///
+    /// @return Const @c Host object using a specified IPv4 address.
+    virtual ConstHostPtr
+    get4(const SubnetID& subnet_id, const asiolink::IOAddress& address) const;
+
+    /// @brief Returns a host connected to the IPv6 subnet.
+    ///
+    /// Implementations of this method should guard against the case when
+    /// mutliple instances of the @c Host are present, e.g. when two
+    /// @c Host objects are found, one for the DUID, another one for the
+    /// HW address. In such case, an implementation of this method
+    /// should throw an MultipleRecords exception.
+    ///
+    /// @param subnet_id Subnet identifier.
+    /// @param hwaddr HW address of the client or NULL if no HW address
+    /// available.
+    /// @param duid DUID or NULL if not available.
+    ///
+    /// @return Const @c Host object using a specified HW address or DUID.
+    virtual ConstHostPtr
+    get6(const SubnetID& subnet_id, const DuidPtr& duid,
+            const HWAddrPtr& hwaddr = HWAddrPtr()) const;
+
+    /// @brief Returns a host using the specified IPv6 prefix.
+    ///
+    /// @param prefix IPv6 prefix for which the @c Host object is searched.
+    /// @param prefix_len IPv6 prefix length.
+    ///
+    /// @return Const @c Host object using a specified HW address or DUID.
+    virtual ConstHostPtr
+    get6(const asiolink::IOAddress& prefix, const uint8_t prefix_len) const;
+
+    /// @brief Adds a new host to the collection.
+    ///
+    /// The implementations of this method should guard against duplicate
+    /// reservations for the same host, where possible. For example, when the
+    /// reservation for the same HW address and subnet id is added twice, the
+    /// addHost method should throw an DuplicateEntry exception. Note, that
+    /// usually it is impossible to guard against adding duplicated host, where
+    /// one instance is identified by HW address, another one by DUID.
+    ///
+    /// @param host Pointer to the new @c Host object being added.
+    virtual void add(const HostPtr& host);
+
+    /// @brief Return backend type
+    ///
+    /// Returns the type of the backend (e.g. "mysql", "memfile" etc.)
+    ///
+    /// @return Type of the backend.
+    virtual std::string getType() const {
+        return (std::string("mysql"));
+    }
+
+    /// @brief Returns backend name.
+    ///
+    /// Each backend have specific name, e.g. "mysql" or "sqlite".
+    ///
+    /// @return Name of the backend.
+    virtual std::string getName() const;
+
+    /// @brief Returns description of the backend.
+    ///
+    /// This description may be multiline text that describes the backend.
+    ///
+    /// @return Description of the backend.
+    virtual std::string getDescription() const;
+
+    /// @brief Returns backend version.
+    ///
+    /// @return Version number stored in the database, as a pair of unsigned
+    ///         integers. "first" is the major version number, "second" the
+    ///         minor number.
+    ///
+    /// @throw isc::dhcp::DbOperationError An operation on the open database
+    ///        has failed.
+    virtual std::pair<uint32_t, uint32_t> getVersion() const;
+
+    /// @brief Commit Transactions
+    ///
+    /// Commits all pending database operations.  On databases that don't
+    /// support transactions, this is a no-op.
+    virtual void commit();
+
+    /// @brief Rollback Transactions
+    ///
+    /// Rolls back all pending database operations.  On databases that don't
+    /// support transactions, this is a no-op.
+    virtual void rollback();
+
+    MySqlConnection* getDatabaseConnection() {
+        return &conn_;
+    }
+
+    /// @brief Statement Tags
+    ///
+    /// The contents of the enum are indexes into the list of SQL statements
+    enum StatementIndex {
+        INSERT_HOST,		// Insert new host to collection
+        GET_HOST_HWADDR_DUID,   // Gets hosts by DUID and/or HW address
+        GET_HOST_ADDR,		// Gets hosts by IPv4 address
+        GET_HOST_SUBID4_DHCPID,	// Gets host by IPv4 SubnetID, HW address/DUID
+        GET_HOST_SUBID6_DHCPID,	// Gets host by IPv6 SubnetID, HW address/DUID
+        GET_HOST_SUBID_ADDR,    // Gets host by IPv4 SubnetID and IPv4 address
+        GET_HOST_PREFIX,	// Gets host by IPv6 prefix
+        GET_VERSION,            // Obtain version number
+        NUM_STATEMENTS          // Number of statements
+    };
+
+private:
+    /// @brief Add Host Code
+    ///
+    /// This method performs adding a host operation.
+    ///	It binds the contents of the host object to
+    /// the prepared statement and adds it to the database.
+    ///
+    /// @param stindex Index of statemnent being executed
+    /// @param bind MYSQL_BIND array that has been created for the host
+    ///
+    /// @htrow isc::dhcp::DuplicateEntry Database throws duplicate entry error
+    void addHost(StatementIndex stindex, std::vector<MYSQL_BIND>& bind);
+
+    /// @brief Get Host Collection Code
+    ///
+    /// This method obtains multiple hosts from the database.
+    ///
+    /// @param stindex Index of statement being executed
+    /// @param bind MYSQL_BIND array for input parameters
+    /// @param exchange Exchange object to use
+    /// @param result ConstHostCollection object returned.  Note that any hosts
+    ///        in the collection when this method is called are not erased: the
+    ///        new data is appended to the end.
+    /// @param single If true, only a single data item is to be retrieved.
+    ///        If more than one is present, a MultipleRecords exception will
+    ///        be thrown.
+    ///
+    /// @throw isc::dhcp::BadValue Data retrieved from the database was invalid.
+    /// @throw isc::dhcp::DbOperationError An operation on the open database has
+    ///        failed.
+    /// @throw isc::dhcp::MultipleRecords Multiple records were retrieved
+    ///        from the database where only one was expected.
+    void getHostCollection(StatementIndex stindex, MYSQL_BIND* bind,
+            boost::shared_ptr<MySqlHostReservationExchange> exchange,
+            ConstHostCollection& result, bool single = false) const;
+
+    /// @brief Check Error and Throw Exception
+    ///
+    /// Virtually all MySQL functions return a status which, if non-zero,
+    /// indicates an error.  This inline function conceals a lot of error
+    /// checking/exception-throwing code.
+    ///
+    /// @param status Status code: non-zero implies an error
+    /// @param index Index of statement that caused the error
+    /// @param what High-level description of the error
+    ///
+    /// @throw isc::dhcp::DbOperationError An operation on the open database
+    ///        has failed.
+    inline void checkError(int status, StatementIndex index,
+            const char* what) const {
+        if (status != 0) {
+            isc_throw(DbOperationError, what << " for <"
+                    << conn_.text_statements_[index] << ">, reason: "
+                    << mysql_error(conn_.mysql_) << " (error code "
+                    << mysql_errno(conn_.mysql_) << ")");
+        }
+    }
+
+    /// @brief Checks if Host with same parameters already been added.
+    ///
+    /// @param host Pointer to the new @c Host object being added.
+    bool checkIfExists(const HostPtr& host);
+
+    // Members
+
+    /// The exchange objects are used for transfer of data to/from the database.
+    /// They are pointed-to objects as the contents may change in "const" calls,
+    /// while the rest of this object does not.  (At alternative would be to
+    /// declare them as "mutable".)
+
+    /// @brief MySQL Host Reservation Exchange object
+    boost::shared_ptr<MySqlHostReservationExchange> hostExchange_;
+
+    /// @brief MySQL connection
+    MySqlConnection conn_;
+
+};
+
+}
+}
+
+#endif // MYSQL_HOST_DATA_SOURCE_H

+ 8 - 79
src/lib/dhcpsrv/mysql_lease_mgr.cc

@@ -415,8 +415,8 @@ public:
             // expiry time (expire).  The relationship is given by:
             //
             // expire = cltt_ + valid_lft_
-            MySqlLeaseMgr::convertToDatabaseTime(lease_->cltt_, lease_->valid_lft_,
-                                                 expire_);
+            MySqlConnection::convertToDatabaseTime(lease_->cltt_, lease_->valid_lft_,
+                                                   expire_);
             bind_[4].buffer_type = MYSQL_TYPE_TIMESTAMP;
             bind_[4].buffer = reinterpret_cast<char*>(&expire_);
             bind_[4].buffer_length = sizeof(expire_);
@@ -590,7 +590,7 @@ public:
         // Convert times received from the database to times for the lease
         // structure
         time_t cltt = 0;
-        MySqlLeaseMgr::convertFromDatabaseTime(expire_, valid_lifetime_, cltt);
+        MySqlConnection::convertFromDatabaseTime(expire_, valid_lifetime_, cltt);
 
         if (client_id_null_==MLM_TRUE) {
             // There's no client-id, so we pass client-id_length_ set to 0
@@ -795,8 +795,8 @@ public:
             //
             // expire = cltt_ + valid_lft_
             //
-            MySqlLeaseMgr::convertToDatabaseTime(lease_->cltt_, lease_->valid_lft_,
-                                                 expire_);
+            MySqlConnection::convertToDatabaseTime(lease_->cltt_, lease_->valid_lft_,
+                                                   expire_);
             bind_[3].buffer_type = MYSQL_TYPE_TIMESTAMP;
             bind_[3].buffer = reinterpret_cast<char*>(&expire_);
             bind_[3].buffer_length = sizeof(expire_);
@@ -1161,7 +1161,7 @@ public:
                                     subnet_id_, fqdn_fwd_, fqdn_rev_,
                                     hostname, hwaddr, prefixlen_));
         time_t cltt = 0;
-        MySqlLeaseMgr::convertFromDatabaseTime(expire_, valid_lifetime_, cltt);
+        MySqlConnection::convertFromDatabaseTime(expire_, valid_lifetime_, cltt);
         result->cltt_ = cltt;
 
         // Set state.
@@ -1268,77 +1268,6 @@ MySqlLeaseMgr::getDBVersion() {
     return (tmp.str());
 }
 
-
-// Time conversion methods.
-//
-// Note that the MySQL TIMESTAMP data type (used for "expire") converts data
-// from the current timezone to UTC for storage, and from UTC to the current
-// timezone for retrieval.
-//
-// This causes no problems providing that:
-// a) cltt is given in local time
-// b) We let the system take care of timezone conversion when converting
-//    from a time read from the database into a local time.
-
-void
-MySqlLeaseMgr::convertToDatabaseTime(const time_t input_time,
-                                     MYSQL_TIME& output_time) {
-
-    // Convert to broken-out time
-    struct tm time_tm;
-    (void) localtime_r(&input_time, &time_tm);
-
-    // Place in output expire structure.
-    output_time.year = time_tm.tm_year + 1900;
-    output_time.month = time_tm.tm_mon + 1;     // Note different base
-    output_time.day = time_tm.tm_mday;
-    output_time.hour = time_tm.tm_hour;
-    output_time.minute = time_tm.tm_min;
-    output_time.second = time_tm.tm_sec;
-    output_time.second_part = 0;                // No fractional seconds
-    output_time.neg = my_bool(0);               // Not negative
-}
-
-void
-MySqlLeaseMgr::convertToDatabaseTime(const time_t cltt,
-                                     const uint32_t valid_lifetime,
-                                     MYSQL_TIME& expire) {
-
-    // Calculate expiry time. Store it in the 64-bit value so as we can detect
-    // overflows.
-    const int64_t expire_time_64 = static_cast<int64_t>(cltt) +
-        static_cast<int64_t>(valid_lifetime);
-
-    // Even on 64-bit systems MySQL doesn't seem to accept the timestamps
-    // beyond the max value of int32_t.
-    if (expire_time_64 > LeaseMgr::MAX_DB_TIME) {
-        isc_throw(BadValue, "Time value is too large: " << expire_time_64);
-    }
-
-    convertToDatabaseTime(static_cast<time_t>(expire_time_64), expire);
-}
-
-void
-MySqlLeaseMgr::convertFromDatabaseTime(const MYSQL_TIME& expire,
-                                       uint32_t valid_lifetime, time_t& cltt) {
-
-    // Copy across fields from MYSQL_TIME structure.
-    struct tm expire_tm;
-    memset(&expire_tm, 0, sizeof(expire_tm));
-
-    expire_tm.tm_year = expire.year - 1900;
-    expire_tm.tm_mon = expire.month - 1;
-    expire_tm.tm_mday = expire.day;
-    expire_tm.tm_hour = expire.hour;
-    expire_tm.tm_min = expire.minute;
-    expire_tm.tm_sec = expire.second;
-    expire_tm.tm_isdst = -1;    // Let the system work out about DST
-
-    // Convert to local time
-    cltt = mktime(&expire_tm) - valid_lifetime;
-}
-
-
 // Add leases to the database.  The two public methods accept a lease object
 // (either V4 of V6), bind the contents to the appropriate prepared
 // statement, then call common code to execute the statement.
@@ -1828,7 +1757,7 @@ MySqlLeaseMgr::getExpiredLeasesCommon(LeaseCollection& expired_leases,
 
     // Expiration timestamp.
     MYSQL_TIME expire_time;
-    convertToDatabaseTime(time(NULL), expire_time);
+    conn_.convertToDatabaseTime(time(NULL), expire_time);
     inbind[1].buffer_type = MYSQL_TYPE_TIMESTAMP;
     inbind[1].buffer = reinterpret_cast<char*>(&expire_time);
     inbind[1].buffer_length = sizeof(expire_time);
@@ -2019,7 +1948,7 @@ MySqlLeaseMgr::deleteExpiredReclaimedLeasesCommon(const uint32_t secs,
 
     // Expiration timestamp.
     MYSQL_TIME expire_time;
-    convertToDatabaseTime(time(NULL) - static_cast<time_t>(secs), expire_time);
+    conn_.convertToDatabaseTime(time(NULL) - static_cast<time_t>(secs), expire_time);
     inbind[1].buffer_type = MYSQL_TYPE_TIMESTAMP;
     inbind[1].buffer = reinterpret_cast<char*>(&expire_time);
     inbind[1].buffer_length = sizeof(expire_time);

+ 1 - 68
src/lib/dhcpsrv/mysql_lease_mgr.h

@@ -21,19 +21,13 @@
 
 #include <boost/scoped_ptr.hpp>
 #include <boost/utility.hpp>
-#include <mysql.h>
+#include <mysql/mysql.h>
 
 #include <time.h>
 
 namespace isc {
 namespace dhcp {
 
-// Define the current database schema values
-
-const uint32_t CURRENT_VERSION_VERSION = 3;
-const uint32_t CURRENT_VERSION_MINOR = 0;
-
-
 // Forward declaration of the Lease exchange objects.  These classes are defined
 // in the .cc file.
 class MySqlLease4Exchange;
@@ -401,67 +395,6 @@ public:
     /// @throw DbOperationError If the rollback failed.
     virtual void rollback();
 
-    ///@{
-    /// The following methods are used to convert between times and time
-    /// intervals stored in the Lease object, and the times stored in the
-    /// database.  The reason for the difference is because in the DHCP server,
-    /// the cltt (Client Time Since Last Transmission) is the natural data; in
-    /// the lease file - which may be read by the user - it is the expiry time
-    /// of the lease.
-
-    /// @brief Convert time_t value to database time.
-    ///
-    /// @param input_time A time_t value representing time.
-    /// @param output_time Reference to MYSQL_TIME object where converted time
-    ///        will be put.
-    static
-    void convertToDatabaseTime(const time_t input_time, MYSQL_TIME& output_time);
-
-    /// @brief Convert Lease Time to Database Times
-    ///
-    /// Within the DHCP servers, times are stored as client last transmit time
-    /// and valid lifetime.  In the database, the information is stored as
-    /// valid lifetime and "expire" (time of expiry of the lease).  They are
-    /// related by the equation:
-    ///
-    /// - expire = client last transmit time + valid lifetime
-    ///
-    /// This method converts from the times in the lease object into times
-    /// able to be added to the database.
-    ///
-    /// @param cltt Client last transmit time
-    /// @param valid_lifetime Valid lifetime
-    /// @param expire Reference to MYSQL_TIME object where the expiry time of
-    ///        the lease will be put.
-    ///
-    /// @throw isc::BadValue if the sum of the calculated expiration time is
-    /// greater than the value of @c LeaseMgr::MAX_DB_TIME.
-    static
-    void convertToDatabaseTime(const time_t cltt, const uint32_t valid_lifetime,
-                               MYSQL_TIME& expire);
-
-    /// @brief Convert Database Time to Lease Times
-    ///
-    /// Within the database, time is stored as "expire" (time of expiry of the
-    /// lease) and valid lifetime.  In the DHCP server, the information is
-    /// stored client last transmit time and valid lifetime.  These are related
-    /// by the equation:
-    ///
-    /// - client last transmit time = expire - valid_lifetime
-    ///
-    /// This method converts from the times in the database into times
-    /// able to be inserted into the lease object.
-    ///
-    /// @param expire Reference to MYSQL_TIME object from where the expiry
-    ///        time of the lease is taken.
-    /// @param valid_lifetime lifetime of the lease.
-    /// @param cltt Reference to location where client last transmit time
-    ///        is put.
-    static
-    void convertFromDatabaseTime(const MYSQL_TIME& expire,
-                                 uint32_t valid_lifetime, time_t& cltt);
-    ///@}
-
     /// @brief Statement Tags
     ///
     /// The contents of the enum are indexes into the list of SQL statements

+ 3 - 3
src/lib/dhcpsrv/pgsql_lease_mgr.cc

@@ -364,7 +364,7 @@ public:
     ///
     /// @return std::string containing the stringified time
     /// @throw isc::BadValue if the sum of the calculated expiration time is
-    /// greater than the value of @c LeaseMgr::MAX_DB_TIME.
+    /// greater than the value of @c DataSource::MAX_DB_TIME.
     static std::string
     convertToDatabaseTime(const time_t cltt, const uint32_t valid_lifetime) {
         // Calculate expiry time. Store it in the 64-bit value so as we can detect
@@ -373,11 +373,11 @@ public:
             static_cast<int64_t>(valid_lifetime);
 
         // It has been observed that the PostgreSQL doesn't deal well with the
-        // timestamp values beyond the LeaseMgr::MAX_DB_TIME seconds since the
+        // timestamp values beyond the DataSource::MAX_DB_TIME seconds since the
         // beginning of the epoch (around year 2038). The value is often
         // stored in the database but it is invalid when read back (overflow?).
         // Hence, the maximum timestamp value is restricted here.
-        if (expire_time_64 > LeaseMgr::MAX_DB_TIME) {
+        if (expire_time_64 > DatabaseConnection::MAX_DB_TIME) {
             isc_throw(isc::BadValue, "Time value is too large: " << expire_time_64);
         }
 

+ 2 - 0
src/lib/dhcpsrv/tests/Makefile.am

@@ -100,10 +100,12 @@ libdhcpsrv_unittests_SOURCES += lease_mgr_unittest.cc
 libdhcpsrv_unittests_SOURCES += logging_unittest.cc
 libdhcpsrv_unittests_SOURCES += logging_info_unittest.cc
 libdhcpsrv_unittests_SOURCES += generic_lease_mgr_unittest.cc generic_lease_mgr_unittest.h
+libdhcpsrv_unittests_SOURCES += generic_host_data_source_unittest.cc generic_host_data_source_unittest.h
 libdhcpsrv_unittests_SOURCES += memfile_lease_mgr_unittest.cc
 libdhcpsrv_unittests_SOURCES += dhcp_parsers_unittest.cc
 if HAVE_MYSQL
 libdhcpsrv_unittests_SOURCES += mysql_lease_mgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += mysql_host_data_source_unittest.cc
 endif
 
 libdhcpsrv_unittests_SOURCES += ncr_generator_unittest.cc

+ 770 - 0
src/lib/dhcpsrv/tests/generic_host_data_source_unittest.cc

@@ -0,0 +1,770 @@
+// Copyright (C) 2015 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 <dhcpsrv/tests/generic_host_data_source_unittest.h>
+#include <dhcpsrv/tests/test_utils.h>
+#include <dhcpsrv/database_connection.h>
+#include <asiolink/io_address.h>
+#include <gtest/gtest.h>
+#include <sstream>
+
+using namespace std;
+using namespace isc::asiolink;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+GenericHostDataSourceTest::GenericHostDataSourceTest()
+    :hdsptr_(NULL) {
+
+}
+
+GenericHostDataSourceTest::~GenericHostDataSourceTest() {
+}
+
+std::string
+GenericHostDataSourceTest::generateHWAddr() {
+    /// @todo: Consider moving this somewhere to lib/testutils.
+
+    // Let's use something that is easily printable. That's convenient
+    // if you need to enter MySQL queries by hand.
+    static uint8_t hwaddr[] = {65, 66, 67, 68, 69, 70};
+
+    stringstream tmp;
+    for (int i = 0; i < sizeof(hwaddr); ++i) {
+        if (i) {
+            tmp << ":";
+        }
+        tmp << std::setw(2) << std::hex << std::setfill('0')
+            << static_cast<unsigned int>(hwaddr[i]);
+    }
+
+    // Increase the address for the next time we use it.
+    // This is primitive, but will work for 65k unique
+    // addresses.
+    hwaddr[sizeof(hwaddr) - 1]++;
+    if (hwaddr[sizeof(hwaddr) - 1] == 0) {
+        hwaddr[sizeof(hwaddr) - 2]++;
+    }
+    return (tmp.str());
+}
+
+std::string
+GenericHostDataSourceTest::generateDuid() {
+    /// @todo: Consider moving this somewhere to lib/testutils.
+
+    // Let's use something that is easily printable. That's convenient
+    // if you need to enter MySQL queries by hand.
+    static uint8_t duid[] = { 65, 66, 67, 68, 69, 70, 71, 72, 73, 74 };
+
+    stringstream tmp;
+    for (int i = 0; i < sizeof(duid); ++i) {
+        tmp << std::setw(2) << std::hex << std::setfill('0')
+            << static_cast<unsigned int>(duid[i]);
+    }
+
+    // Increase the DUID for the next time we use it.
+    // This is primitive, but will work for 65k unique
+    // DUIDs.
+    duid[sizeof(duid) - 1]++;
+    if (duid[sizeof(duid) - 1] == 0) {
+        duid[sizeof(duid) - 2]++;
+    }
+    return (tmp.str());
+}
+
+HostPtr GenericHostDataSourceTest::initializeHost4(std::string address,
+                                                   bool hwaddr) {
+    string ident;
+    string ident_type;
+
+    if (hwaddr) {
+        ident = generateHWAddr();
+        ident_type = "hw-address";
+    } else {
+        ident = generateDuid();
+        ident_type = "duid";
+    }
+
+    // Let's create ever increasing subnet-ids. Let's keep those different,
+    // so subnet4 != subnet6. Useful for catching cases if the code confuses
+    // subnet4 with subnet6.
+    static SubnetID subnet4 = 0;
+    static SubnetID subnet6 = 100;
+    subnet4++;
+    subnet6++;
+
+    IOAddress addr(address);
+    HostPtr host(new Host(ident, ident_type, subnet4, subnet6, addr));
+
+    return (host);
+}
+
+HostPtr GenericHostDataSourceTest::initializeHost6(std::string address,
+                                                   BaseHostDataSource::IdType identifier,
+                                                   bool prefix) {
+    string ident;
+    string ident_type;
+
+    switch (identifier) {
+    case BaseHostDataSource::ID_HWADDR:
+        ident = generateHWAddr();
+        ident_type = "hw-address";
+        break;
+    case BaseHostDataSource::ID_DUID:
+        ident = generateDuid();
+        ident_type = "duid";
+        break;
+    default:
+        ADD_FAILURE() << "Unknown IdType: " << identifier;
+        return HostPtr();
+    }
+
+    // Let's create ever increasing subnet-ids. Let's keep those different,
+    // so subnet4 != subnet6. Useful for catching cases if the code confuses
+    // subnet4 with subnet6.
+    static SubnetID subnet4 = 0;
+    static SubnetID subnet6 = 100;
+    subnet4++;
+    subnet6++;
+
+    HostPtr host(new Host(ident, ident_type, subnet4, subnet6, IOAddress("0.0.0.0")));
+
+    if (!prefix) {
+        // Create IPv6 reservation (for an address)
+        IPv6Resrv resv(IPv6Resrv::TYPE_NA, IOAddress(address), 128);
+        host->addReservation(resv);
+    } else {
+        // Create IPv6 reservation for a /64 prefix
+        IPv6Resrv resv(IPv6Resrv::TYPE_PD, IOAddress(address), 64);
+        host->addReservation(resv);
+    }
+    return (host);
+}
+
+void
+GenericHostDataSourceTest::compareHwaddrs(const ConstHostPtr& host1,
+                                          const ConstHostPtr& host2,
+                                          bool expect_match) {
+    ASSERT_TRUE(host1);
+    ASSERT_TRUE(host2);
+
+    // Compare if both have or have not HWaddress set.
+    if ((host1->getHWAddress() && !host2->getHWAddress()) ||
+        (!host1->getHWAddress() && host2->getHWAddress())) {
+
+        // One host has hardware address set while the other has not.
+        // Let's see if it's a problem.
+        if (expect_match) {
+            ADD_FAILURE() << "Host comparison failed: host1 hwaddress="
+                          << host1->getHWAddress() << ", host2 hwaddress="
+                          << host2->getHWAddress();
+        }
+        return;
+    }
+
+    // Now we know that either both or neither have hw address set.
+    // If host1 has it, we can proceed to value comparison.
+    if (host1->getHWAddress()) {
+
+        if (expect_match) {
+            // Compare the actual address if they match.
+            EXPECT_TRUE(*host1->getHWAddress() == *host2->getHWAddress());
+        } else {
+            EXPECT_FALSE(*host1->getHWAddress() == *host2->getHWAddress());
+        }
+        if (*host1->getHWAddress() != *host2->getHWAddress()) {
+            cout << host1->getHWAddress()->toText(true) << endl;
+            cout << host2->getHWAddress()->toText(true) << endl;
+        }
+    }
+}
+
+void
+GenericHostDataSourceTest::compareDuids(const ConstHostPtr& host1,
+                                        const ConstHostPtr& host2,
+                                        bool expect_match) {
+    ASSERT_TRUE(host1);
+    ASSERT_TRUE(host2);
+
+    // compare if both have or have not DUID set
+    if ((host1->getDuid() && !host2->getDuid()) ||
+        (!host1->getDuid() && host2->getDuid())) {
+
+        // One host has a DUID and the other doesn't.
+        // Let's see if it's a problem.
+        if (expect_match) {
+            ADD_FAILURE() << "DUID comparison failed: host1 duid="
+                          << host1->getDuid() << ", host2 duid="
+                          << host2->getDuid();
+        }
+        return;
+    }
+
+    // Now we know that either both or neither have DUID set.
+    // If host1 has it, we can proceed to value comparison.
+    if (host1->getDuid()) {
+
+        if (expect_match) {
+            EXPECT_TRUE(*host1->getDuid() == *host2->getDuid());
+        } else {
+            EXPECT_FALSE(*host1->getDuid() == *host2->getDuid());
+        }
+        if (*host1->getDuid() != *host2->getDuid()) {
+            cout << host1->getDuid()->toText() << endl;
+            cout << host2->getDuid()->toText() << endl;
+        }
+    }
+}
+
+void GenericHostDataSourceTest::compareHosts(const ConstHostPtr& host1,
+                                             const ConstHostPtr& host2) {
+
+    // Let's compare HW addresses and expect match.
+    compareHwaddrs(host1, host2, true);
+
+    // Now compare DUIDs
+    compareDuids(host1, host2, true);
+
+    // Now check that the identifiers returned as vectors are the same
+    EXPECT_EQ(host1->getIdentifierType(), host2->getIdentifierType());
+    EXPECT_EQ(host1->getIdentifier(), host2->getIdentifier());
+
+    // Check host parameters
+    EXPECT_EQ(host1->getIPv4SubnetID(), host2->getIPv4SubnetID());
+    EXPECT_EQ(host1->getIPv6SubnetID(), host2->getIPv6SubnetID());
+    EXPECT_EQ(host1->getIPv4Reservation(), host2->getIPv4Reservation());
+    EXPECT_EQ(host1->getHostname(), host2->getHostname());
+
+    // Compare IPv6 reservations
+    compareReservations6(host1->getIPv6Reservations(),
+                         host2->getIPv6Reservations(),
+                         true);
+
+    // And compare client classification details
+    compareClientClasses(host1->getClientClasses4(),
+                         host2->getClientClasses4());
+
+    compareClientClasses(host1->getClientClasses6(),
+                         host2->getClientClasses6());
+}
+
+DuidPtr
+GenericHostDataSourceTest::HWAddrToDuid(const HWAddrPtr& hwaddr) {
+    if (!hwaddr) {
+        return (DuidPtr());
+    }
+
+    return (DuidPtr(new DUID(hwaddr->hwaddr_)));
+}
+
+HWAddrPtr
+GenericHostDataSourceTest::DuidToHWAddr(const DuidPtr& duid) {
+    if (!duid) {
+        return (HWAddrPtr());
+    }
+
+    return (HWAddrPtr(new HWAddr(duid->getDuid(), HTYPE_ETHER)));
+}
+
+
+void
+GenericHostDataSourceTest::compareReservations6(IPv6ResrvRange resrv1,
+                                                IPv6ResrvRange resrv2,
+                                                bool expect_match) {
+
+    // Compare number of reservations for both hosts
+    if (std::distance(resrv1.first, resrv1.second) !=
+            std::distance(resrv2.first, resrv2.second)){
+        // Number of reservations is not equal.
+        // Let's see if it's a problem.
+        if (expect_match) {
+            ADD_FAILURE()<< "Reservation comparison failed, "
+                    "hosts got different number of reservations.";
+        }
+        return;
+    }
+
+    if (std::distance(resrv1.first, resrv1.second) > 0) {
+        if (expect_match){
+            /// @todo Compare every reservation from both hosts
+            ///       This is part of the work for #4212.
+        }
+    }
+}
+
+void
+GenericHostDataSourceTest::compareClientClasses(const ClientClasses& /*classes1*/,
+                                                const ClientClasses& /*classes2*/) {
+    /// @todo: Implement client classes comparison.
+    ///        This is part of the work for #4213.
+}
+
+void GenericHostDataSourceTest::testBasic4(bool hwaddr) {
+    // Make sure we have the pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    // Create a host reservation.
+    HostPtr host = initializeHost4("192.0.2.1", hwaddr);
+    ASSERT_TRUE(host); // Make sure the host is generate properly.
+    SubnetID subnet = host->getIPv4SubnetID();
+
+    // Try to add it to the host data source.
+    ASSERT_NO_THROW(hdsptr_->add(host));
+
+    // This should not return anything
+    ConstHostPtr from_hds = hdsptr_->get4(subnet, IOAddress("10.10.10.10"));
+    ASSERT_FALSE(from_hds);
+
+    // This time it should return a host
+    from_hds = hdsptr_->get4(subnet, IOAddress("192.0.2.1"));
+    ASSERT_TRUE(from_hds);
+
+    // Finally, let's check if what we got makes any sense.
+    compareHosts(host, from_hds);
+}
+
+
+void GenericHostDataSourceTest::testGetByIPv4(bool hwaddr) {
+    // Make sure we have a pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    // Let's create a couple of hosts...
+    HostPtr host1 = initializeHost4("192.0.2.1", hwaddr);
+    HostPtr host2 = initializeHost4("192.0.2.2", hwaddr);
+    HostPtr host3 = initializeHost4("192.0.2.3", hwaddr);
+    HostPtr host4 = initializeHost4("192.0.2.4", hwaddr);
+
+    // ... and add them to the data source.
+    ASSERT_NO_THROW(hdsptr_->add(host1));
+    ASSERT_NO_THROW(hdsptr_->add(host2));
+    ASSERT_NO_THROW(hdsptr_->add(host3));
+    ASSERT_NO_THROW(hdsptr_->add(host4));
+
+    SubnetID subnet1 = host1->getIPv4SubnetID();
+    SubnetID subnet2 = host2->getIPv4SubnetID();
+    SubnetID subnet3 = host3->getIPv4SubnetID();
+    SubnetID subnet4 = host4->getIPv4SubnetID();
+
+    // And then try to retrieve them back.
+    ConstHostPtr from_hds1 = hdsptr_->get4(subnet1, IOAddress("192.0.2.1"));
+    ConstHostPtr from_hds2 = hdsptr_->get4(subnet2, IOAddress("192.0.2.2"));
+    ConstHostPtr from_hds3 = hdsptr_->get4(subnet3, IOAddress("192.0.2.3"));
+    ConstHostPtr from_hds4 = hdsptr_->get4(subnet4, IOAddress("192.0.2.4"));
+
+    // Make sure we got something back.
+    ASSERT_TRUE(from_hds1);
+    ASSERT_TRUE(from_hds2);
+    ASSERT_TRUE(from_hds3);
+    ASSERT_TRUE(from_hds4);
+
+    // Then let's check that what we got seems correct.
+    compareHosts(host1, from_hds1);
+    compareHosts(host2, from_hds2);
+    compareHosts(host3, from_hds3);
+    compareHosts(host4, from_hds4);
+
+    // Ok, finally let's check that getting by a different address
+    // will not work.
+    EXPECT_FALSE(hdsptr_->get4(subnet1, IOAddress("192.0.1.5")));
+}
+
+void GenericHostDataSourceTest::testGet4ByHWAddr() {
+    // Make sure we have a pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    HostPtr host1 = initializeHost4("192.0.2.1", true);
+    HostPtr host2 = initializeHost4("192.0.2.2", true);
+
+    // Sanity check: make sure the hosts have different HW addresses.
+    ASSERT_TRUE(host1->getHWAddress());
+    ASSERT_TRUE(host2->getHWAddress());
+    compareHwaddrs(host1, host2, false);
+
+    // Try to add both of them to the host data source.
+    ASSERT_NO_THROW(hdsptr_->add(host1));
+    ASSERT_NO_THROW(hdsptr_->add(host2));
+
+    SubnetID subnet1 = host1->getIPv4SubnetID();
+    SubnetID subnet2 = host2->getIPv4SubnetID();
+
+    ConstHostPtr from_hds1 = hdsptr_->get4(subnet1, host1->getHWAddress());
+    ConstHostPtr from_hds2 = hdsptr_->get4(subnet2, host2->getHWAddress());
+
+    // Now let's check if we got what we expected.
+    ASSERT_TRUE(from_hds1);
+    ASSERT_TRUE(from_hds2);
+    compareHosts(host1, from_hds1);
+    compareHosts(host2, from_hds2);
+}
+
+void GenericHostDataSourceTest::testGet4ByClientId() {
+    // Make sure we have a pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    HostPtr host1 = initializeHost4("192.0.2.1", false);
+    HostPtr host2 = initializeHost4("192.0.2.2", false);
+
+    // Sanity check: make sure the hosts have different client-ids.
+    ASSERT_TRUE(host1->getDuid());
+    ASSERT_TRUE(host2->getDuid());
+    compareDuids(host1, host2, false);
+
+    // Try to add both of them to the host data source.
+    ASSERT_NO_THROW(hdsptr_->add(host1));
+    ASSERT_NO_THROW(hdsptr_->add(host2));
+
+    SubnetID subnet1 = host1->getIPv4SubnetID();
+    SubnetID subnet2 = host2->getIPv4SubnetID();
+
+    ConstHostPtr from_hds1 = hdsptr_->get4(subnet1, HWAddrPtr(), host1->getDuid());
+    ConstHostPtr from_hds2 = hdsptr_->get4(subnet2, HWAddrPtr(), host2->getDuid());
+
+    // Now let's check if we got what we expected.
+    ASSERT_TRUE(from_hds1);
+    ASSERT_TRUE(from_hds2);
+    compareHosts(host1, from_hds1);
+    compareHosts(host2, from_hds2);
+}
+
+void GenericHostDataSourceTest::testHWAddrNotClientId() {
+    // Make sure we have a pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    // Create a host with HW address
+    HostPtr host = initializeHost4("192.0.2.1", true);
+    ASSERT_TRUE(host->getHWAddress());
+    ASSERT_FALSE(host->getDuid());
+
+    // Try to add it to the host data source.
+    ASSERT_NO_THROW(hdsptr_->add(host));
+
+    SubnetID subnet = host->getIPv4SubnetID();
+
+    DuidPtr duid = HWAddrToDuid(host->getHWAddress());
+
+    // Get the host by HW address (should succeed)
+    ConstHostPtr by_hwaddr = hdsptr_->get4(subnet, host->getHWAddress(), DuidPtr());
+
+    // Get the host by DUID (should fail)
+    ConstHostPtr by_duid   = hdsptr_->get4(subnet, HWAddrPtr(), duid);
+
+    // Now let's check if we got what we expected.
+    EXPECT_TRUE(by_hwaddr);
+    EXPECT_FALSE(by_duid);
+}
+
+void GenericHostDataSourceTest::testClientIdNotHWAddr() {
+    // Make sure we have a pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    // Create a host with client-id
+    HostPtr host = initializeHost4("192.0.2.1", false);
+    ASSERT_FALSE(host->getHWAddress());
+    ASSERT_TRUE(host->getDuid());
+
+    // Try to add it to the host data source.
+    ASSERT_NO_THROW(hdsptr_->add(host));
+
+    SubnetID subnet = host->getIPv4SubnetID();
+
+    HWAddrPtr hwaddr = DuidToHWAddr(host->getDuid());
+
+    // Get the host by DUID (should succeed)
+    ConstHostPtr by_duid   = hdsptr_->get4(subnet, HWAddrPtr(), host->getDuid());
+
+    // Get the host by HW address (should fail)
+    ConstHostPtr by_hwaddr = hdsptr_->get4(subnet, hwaddr, DuidPtr());
+
+    // Now let's check if we got what we expected.
+    EXPECT_TRUE(by_duid);
+    EXPECT_FALSE(by_hwaddr);
+}
+
+void
+GenericHostDataSourceTest::testHostname(std::string name, int num) {
+
+    // Make sure we have a pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    // Initialize the address to 192.0.2.0 (this will be bumped
+    // up to 192.0.2.1 in the first iteration)
+    IOAddress addr("192.0.2.0");
+
+    vector<HostPtr> hosts;
+
+    // Prepare a vector of hosts with unique hostnames
+    for (int i = 0; i < num; ++i) {
+
+        addr = IOAddress::increase(addr);
+
+        HostPtr host = initializeHost4(addr.toText(), false);
+
+        stringstream hostname;
+        hostname.str("");
+        if (num > 1) {
+            hostname << i;
+        }
+        hostname << name;
+        host->setHostname(hostname.str());
+
+        hosts.push_back(host);
+    }
+
+    // Now add them all to the host data source.
+    for (vector<HostPtr>::const_iterator it = hosts.begin();
+         it != hosts.end(); ++it) {
+        // Try to add both of the to the host data source.
+        ASSERT_NO_THROW(hdsptr_->add(*it));
+    }
+
+    // And finally retrieve them one by one and check
+    // if the hostname was preserved.
+    for (vector<HostPtr>::const_iterator it = hosts.begin();
+         it != hosts.end(); ++it) {
+
+        ConstHostPtr from_hds;
+        ASSERT_NO_THROW(from_hds = hdsptr_->get4(
+                            (*it)->getIPv4SubnetID(),
+                            (*it)->getIPv4Reservation()));
+        ASSERT_TRUE(from_hds);
+
+        EXPECT_EQ((*it)->getHostname(), from_hds->getHostname());
+    }
+}
+
+void
+GenericHostDataSourceTest::testMultipleSubnets(int subnets, bool hwaddr) {
+
+    // Make sure we have a pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    HostPtr host = initializeHost4("192.0.2.1", hwaddr);
+
+    for (int i = 0; i < subnets; ++i) {
+        host->setIPv4SubnetID(i + 1000);
+        host->setIPv6SubnetID(i + 1000);
+
+        // Check that the same host can have reservations in multiple subnets.
+        EXPECT_NO_THROW(hdsptr_->add(host));
+    }
+
+    // Now check that the reservations can be retrieved by IPv4 address from
+    // each subnet separately.
+    for (int i = 0; i < subnets; ++i) {
+
+        // Try to retrieve the host by IPv4 address.
+        ConstHostPtr from_hds = hdsptr_->get4(i + 1000, host->getIPv4Reservation());
+
+        ASSERT_TRUE(from_hds);
+        EXPECT_EQ(i + 1000, from_hds->getIPv4SubnetID());
+
+        // Try to retrieve the host by either HW address of client-id
+        from_hds = hdsptr_->get4(i + 1000, host->getHWAddress(), host->getDuid());
+        ASSERT_TRUE(from_hds);
+        EXPECT_EQ(i + 1000, from_hds->getIPv4SubnetID());
+    }
+
+    // Now check that they can be retrieved all at once, by IPv4 address.
+    ConstHostCollection all_by_addr = hdsptr_->getAll4(IOAddress("192.0.2.1"));
+    ASSERT_EQ(subnets, all_by_addr.size());
+
+    // Verify that the values returned are proper.
+    int i = 0;
+    for (ConstHostCollection::const_iterator it = all_by_addr.begin();
+         it != all_by_addr.end(); ++it) {
+        EXPECT_EQ(IOAddress("192.0.2.1"), (*it)->getIPv4Reservation());
+        EXPECT_EQ(1000 + i++, (*it)->getIPv4SubnetID());
+    }
+
+    // Finally, check that the hosts can be retrived by HW address or DUID
+    ConstHostCollection all_by_id = hdsptr_->getAll(host->getHWAddress(),
+                                                    host->getDuid());
+    ASSERT_EQ(subnets, all_by_id.size());
+
+    // Check that the returned values are as expected.
+    i = 0;
+    for (ConstHostCollection::const_iterator it = all_by_id.begin();
+         it != all_by_id.end(); ++it) {
+        EXPECT_EQ(IOAddress("192.0.2.1"), (*it)->getIPv4Reservation());
+        EXPECT_EQ(1000 + i++, (*it)->getIPv4SubnetID());
+    }
+}
+
+void GenericHostDataSourceTest::testGet6ByHWAddr() {
+    // Make sure we have the pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    // Create a host reservations.
+    HostPtr host1 = initializeHost6("2001:db8::0", BaseHostDataSource::ID_HWADDR, true);
+    HostPtr host2 = initializeHost6("2001:db8::1", BaseHostDataSource::ID_HWADDR, true);
+
+    // Sanity check: make sure the hosts have different HW addresses.
+    ASSERT_TRUE(host1->getHWAddress());
+    ASSERT_TRUE(host2->getHWAddress());
+
+    compareHwaddrs(host1, host2, false);
+
+    // Try to add both of them to the host data source.
+    ASSERT_NO_THROW(hdsptr_->add(host1));
+    ASSERT_NO_THROW(hdsptr_->add(host2));
+
+    SubnetID subnet1 = host1->getIPv6SubnetID();
+    SubnetID subnet2 = host2->getIPv6SubnetID();
+
+    ConstHostPtr from_hds1 = hdsptr_->get6(subnet1, DuidPtr(), host1->getHWAddress());
+    ConstHostPtr from_hds2 = hdsptr_->get6(subnet2, DuidPtr(), host2->getHWAddress());
+
+    // Now let's check if we got what we expected.
+    ASSERT_TRUE(from_hds1);
+    ASSERT_TRUE(from_hds2);
+    compareHosts(host1, from_hds1);
+    compareHosts(host2, from_hds2);
+}
+
+void GenericHostDataSourceTest::testGet6ByClientId() {
+    // Make sure we have the pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    // Create a host reservations.
+    HostPtr host1 = initializeHost6("2001:db8::0", BaseHostDataSource::ID_DUID, true);
+    HostPtr host2 = initializeHost6("2001:db8::1", BaseHostDataSource::ID_DUID, true);
+
+    // Sanity check: make sure the hosts have different HW addresses.
+    ASSERT_TRUE(host1->getDuid());
+    ASSERT_TRUE(host2->getDuid());
+
+    compareDuids(host1, host2, false);
+
+    // Try to add both of them to the host data source.
+    ASSERT_NO_THROW(hdsptr_->add(host1));
+    ASSERT_NO_THROW(hdsptr_->add(host2));
+
+    SubnetID subnet1 = host1->getIPv6SubnetID();
+    SubnetID subnet2 = host2->getIPv6SubnetID();
+
+    ConstHostPtr from_hds1 = hdsptr_->get6(subnet1, host1->getDuid(), HWAddrPtr());
+    ConstHostPtr from_hds2 = hdsptr_->get6(subnet2, host2->getDuid(), HWAddrPtr());
+
+    // Now let's check if we got what we expected.
+    ASSERT_TRUE(from_hds1);
+    ASSERT_TRUE(from_hds2);
+    compareHosts(host1, from_hds1);
+    compareHosts(host2, from_hds2);
+}
+
+void
+GenericHostDataSourceTest::testSubnetId6(int subnets, BaseHostDataSource::IdType id) {
+
+    // Make sure we have a pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    HostPtr host = initializeHost6("2001:db8::0", id, true);
+
+    for (int i = 0; i < subnets; ++i) {
+        host->setIPv4SubnetID(i + 1000);
+        host->setIPv6SubnetID(i + 1000);
+
+        // Check that the same host can have reservations in multiple subnets.
+        EXPECT_NO_THROW(hdsptr_->add(host));
+    }
+
+    // Check that the reservations can be retrieved from each subnet separately.
+    for (int i = 0; i < subnets; ++i) {
+
+        // Try to retrieve the host
+        ConstHostPtr from_hds = hdsptr_->get6(i + 1000, host->getDuid(),
+                                              host->getHWAddress());
+
+        ASSERT_TRUE(from_hds);
+        EXPECT_EQ(i + 1000, from_hds->getIPv6SubnetID());
+    }
+
+    // Check that the hosts can all be retrived by HW address or DUID
+    ConstHostCollection all_by_id = hdsptr_->getAll(host->getHWAddress(),
+                                                    host->getDuid());
+    ASSERT_EQ(subnets, all_by_id.size());
+
+    // Check that the returned values are as expected.
+    int i = 0;
+    for (ConstHostCollection::const_iterator it = all_by_id.begin();
+         it != all_by_id.end(); ++it) {
+        EXPECT_EQ(IOAddress("0.0.0.0"), (*it)->getIPv4Reservation());
+        EXPECT_EQ(1000 + i++, (*it)->getIPv6SubnetID());
+    }
+}
+
+void GenericHostDataSourceTest::testGetByIPv6(BaseHostDataSource::IdType id,
+                                              bool prefix) {
+    // Make sure we have a pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    // Let's create a couple of hosts...
+    HostPtr host1 = initializeHost6("2001:db8::1", id, prefix);
+    HostPtr host2 = initializeHost6("2001:db8::2", id, prefix);
+    HostPtr host3 = initializeHost6("2001:db8::3", id, prefix);
+    HostPtr host4 = initializeHost6("2001:db8::4", id, prefix);
+
+    // ... and add them to the data source.
+    ASSERT_NO_THROW(hdsptr_->add(host1));
+    ASSERT_NO_THROW(hdsptr_->add(host2));
+    ASSERT_NO_THROW(hdsptr_->add(host3));
+    ASSERT_NO_THROW(hdsptr_->add(host4));
+
+    // Are we talking about addresses or prefixes?
+    uint8_t len = prefix ? 64 : 128;
+
+    // And then try to retrieve them back.
+    ConstHostPtr from_hds1 = hdsptr_->get6(IOAddress("2001:db8::1"), len);
+    ConstHostPtr from_hds2 = hdsptr_->get6(IOAddress("2001:db8::2"), len);
+    ConstHostPtr from_hds3 = hdsptr_->get6(IOAddress("2001:db8::3"), len);
+    ConstHostPtr from_hds4 = hdsptr_->get6(IOAddress("2001:db8::4"), len);
+
+    // Make sure we got something back.
+    ASSERT_TRUE(from_hds1);
+    ASSERT_TRUE(from_hds2);
+    ASSERT_TRUE(from_hds3);
+    ASSERT_TRUE(from_hds4);
+
+    // Then let's check that what we got seems correct.
+    compareHosts(host1, from_hds1);
+    compareHosts(host2, from_hds2);
+    compareHosts(host3, from_hds3);
+    compareHosts(host4, from_hds4);
+
+    // Ok, finally let's check that getting by a different address
+    // will not work.
+    EXPECT_FALSE(hdsptr_->get6(IOAddress("2001:db8::5"), len));
+}
+
+void GenericHostDataSourceTest::testAddDuplicate() {
+    // Make sure we have the pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    // Create a host reservations.
+    HostPtr host = initializeHost6("2001:db8::1", BaseHostDataSource::ID_DUID,
+                                   true);
+
+    // Add this reservation once.
+    ASSERT_NO_THROW(hdsptr_->add(host));
+
+    // Then try to add it again, it should throw an exception.
+    ASSERT_THROW(hdsptr_->add(host), DuplicateEntry);
+
+}
+
+}; // namespace test
+}; // namespace dhcp
+}; // namespace isc

+ 248 - 0
src/lib/dhcpsrv/tests/generic_host_data_source_unittest.h

@@ -0,0 +1,248 @@
+// Copyright (C) 2015 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 GENERIC_HOST_DATA_SOURCE_UNITTEST_H
+#define GENERIC_HOST_DATA_SOURCE_UNITTEST_H
+
+#include <dhcpsrv/base_host_data_source.h>
+#include <dhcp/classify.h>
+#include <gtest/gtest.h>
+#include <vector>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Test Fixture class with utility functions for HostDataSource backends
+///
+/// It contains utility functions for test purposes.
+/// All concrete HostDataSource test classes should be derived from it.
+class GenericHostDataSourceTest : public ::testing::Test {
+public:
+
+    /// @brief Universe (V4 or V6).
+    enum Universe {
+        V4,
+        V6
+    };
+
+    /// @brief Default constructor.
+    GenericHostDataSourceTest();
+
+    /// @brief Virtual destructor.
+    virtual ~GenericHostDataSourceTest();
+
+    /// @brief Creates a host reservation for specified IPv4 address.
+    ///
+    /// @param address IPv4 address to be set
+    /// @param hwaddr type of identifier (true = hwaddr, false = client-id)
+    ///
+    /// @return generated Host object
+    HostPtr initializeHost4(std::string address, bool hwaddr);
+
+    /// @brief Creates a host reservation for specified IPv6 address.
+    ///
+    /// @param address IPv6 address to be reserved
+    /// @param id type of identifier (ID_DUID or ID_HWADDR are supported)
+    /// @param prefix reservation type (true = prefix, false = address)
+    ///
+    /// @return generated Host object
+    HostPtr initializeHost6(std::string address, BaseHostDataSource::IdType id,
+                            bool prefix);
+
+    /// @brief Generates a hardware address in text version.
+    ///
+    /// @return HW address in textual form acceptable by Host constructor
+    std::string generateHWAddr();
+
+    /// @brief Generates a hardware address in text version.
+    /// @return DUID in textual form acceptable by Host constructor
+    std::string generateDuid();
+
+    /// @brief Compares hardware addresses of the two hosts.
+    ///
+    /// This method compares two hwardware address and uses gtest
+    /// macros to signal unexpected (mismatch if expect_match is true;
+    /// match if expect_match is false) values.
+    ///
+    /// @param host1 first host to be compared
+    /// @param host2 second host to be compared
+    /// @param expect_match true = HW addresses expected to be the same,
+    ///                     false = HW addresses expected to be different
+    void
+    compareHwaddrs(const ConstHostPtr& host1, const ConstHostPtr& host2,
+                   bool expect_match);
+
+    /// @brief Compares DUIDs of the two hosts.
+    ///
+    /// This method compares two DUIDs (client-ids) and uses gtest
+    /// macros to signal unexpected (mismatch if expect_match is true;
+    /// match if expect_match is false) values.
+    ///
+    /// @param host1 first host to be compared
+    /// @param host2 second host to be compared
+    /// @param expect_match true = DUIDs expected to be the same,
+    ///                     false = DUIDs expected to be different
+    void
+    compareDuids(const ConstHostPtr& host1, const ConstHostPtr& host2,
+                 bool expect_match);
+
+    /// @brief Compares two hosts
+    ///
+    /// This method uses gtest macros to signal errors.
+    ///
+    /// @param host1 first host to compare
+    /// @param host2 second host to compare
+    void compareHosts(const ConstHostPtr& host1, const ConstHostPtr& host2);
+
+    /// @brief Compares two IPv6 reservation lists.
+    ///
+    /// This method uses gtest macros to signal errors.
+    ///
+    /// @param resv1 first IPv6 reservations list
+    /// @param resv2 second IPv6 reservations list
+    void compareReservations6(IPv6ResrvRange resv1, IPv6ResrvRange resv2,
+                              bool expect_match);
+
+    /// @brief Compares two client classes
+    ///
+    /// This method uses gtest macros to signal errors.
+    ///
+    /// @param classes1 first list of client classes
+    /// @param classes2 second list of client classes
+    void compareClientClasses(const ClientClasses& classes1,
+                              const ClientClasses& classes2);
+
+    /// @brief Pointer to the host data source
+    BaseHostDataSource* hdsptr_;
+
+    /// @brief Test that checks that simple host with IPv4 reservation
+    ///        can be inserted and later retrieved.
+    ///
+    /// Uses gtest macros to report failures.
+    /// @param hwaddr true = use HW address as identifier, false = use client-id(DUID)
+    void testBasic4(bool hwaddr);
+
+    /// @brief Test inserts several hosts with unique IPv4 address and
+    ///        checks that they can be retrieved properly.
+    ///
+    /// Uses gtest macros to report failures.
+    /// @param hwaddr true = use HW address as identifier, false = use client-id(DUID)
+    void testGetByIPv4(bool hwaddr);
+
+    /// @brief Test that hosts can be retrieved by hardware address.
+    ///
+    /// Uses gtest macros to report failures.
+    void testGet4ByHWAddr();
+
+    /// @brief Test that hosts can be retrieved by client-id
+    ///
+    /// Uses gtest macros to report failures.
+    void testGet4ByClientId();
+
+    /// @brief Test that clients with stored HW address can't be retrieved
+    ///        by DUID with the same value.
+    ///
+    /// Test procedure: add host reservation with hardware address X, try to retrieve
+    /// host by client-identifier X, verify that the reservation is not returned.
+    ///
+    /// Uses gtest macros to report failures.
+    void testHWAddrNotClientId();
+
+    /// @brief Test that clients with stored DUID can't be retrieved
+    ///        by HW address of the same value.
+    ///
+    /// Test procedure: add host reservation with client identifier X, try to
+    /// retrieve host by hardware address X, verify that the reservation is not
+    /// returned.
+    ///
+    /// Uses gtest macros to report failures.
+    void testClientIdNotHWAddr();
+
+    /// @brief Test adds specified number of hosts with unique hostnames, then
+    /// retrives them and checks that the hostnames are set properly.
+    ///
+    /// Uses gtest macros to report failures.
+    ///
+    /// @param name hostname to be used (if n>1, numbers will be appended)
+    /// @param num number of hostnames to be added.
+    void testHostname(std::string name, int num);
+
+    /// @brief Test inserts multiple reservations for the same host for different
+    /// subnets and check that they can be retrieved properly.
+    ///
+    /// Uses gtest macros to report failures.
+    ///
+    /// @param subnets number of subnets to test
+    /// @param hwaddr true = use HW address, false = use client-id
+    void testMultipleSubnets(int subnets, bool hwaddr);
+
+    /// @brief Test inserts several hosts with unique IPv6 addresses and
+    ///        checks that they can be retrieved properly.
+    ///
+    /// Uses gtest macros to report failures.
+    /// @param id type of the identifier to be used (HWAddr or DUID)
+    /// @param prefix true - reserve IPv6 prefix, false - reserve IPv6 address
+    void testGetByIPv6(BaseHostDataSource::IdType id, bool prefix);
+
+    /// @brief Test that hosts can be retrieved by hardware address.
+    ///
+    /// Uses gtest macros to report failures.
+    void testGet6ByHWAddr();
+
+    /// @brief Test that hosts can be retrieved by client-id
+    ///
+    /// Uses gtest macros to report failures.
+    void testGet6ByClientId();
+
+    /// @brief Test if host reservations made for different IPv6 subnets
+    ///        are handled correctly.
+    ///
+    /// Uses gtest macros to report failures.
+    ///
+    /// @param subnets number of subnets to test
+    /// @param id identifier type (ID_HWADDR or ID_DUID)
+    void testSubnetId6(int subnets, BaseHostDataSource::IdType id);
+
+    /// @brief Test if the duplicate host instances can't be inserted.
+    ///
+    /// Uses gtest macros to report failures.
+    void testAddDuplicate();
+
+    /// @brief Returns DUID with identical content as specified HW address
+    ///
+    /// This method does not have any sense in real life and is only useful
+    /// in testing corner cases in the database backends (e.g. whether the DB
+    /// is able to tell the difference between hwaddr and duid)
+    ///
+    /// @param hwaddr hardware address to be copied
+    /// @return duid with the same value as specified HW address
+    DuidPtr HWAddrToDuid(const HWAddrPtr& hwaddr);
+
+    /// @brief Returns HW address with identical content as specified DUID
+    ///
+    /// This method does not have any sense in real life and is only useful
+    /// in testing corner cases in the database backends (e.g. whether the DB
+    /// is able to tell the difference between hwaddr and duid)
+    ///
+    /// @param duid DUID to be copied
+    /// @return HW address with the same value as specified DUID
+    HWAddrPtr DuidToHWAddr(const DuidPtr& duid);
+};
+
+}; // namespace test
+}; // namespace dhcp
+}; // namespace isc
+
+#endif

+ 2 - 2
src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc

@@ -688,7 +688,7 @@ GenericLeaseMgrTest::testMaxDate4() {
     // Set valid_lft_ to 1 day, cllt_ to max time. This should make expire
     // time too large to store.
     lease->valid_lft_ = 24*60*60;
-    lease->cltt_ = LeaseMgr::MAX_DB_TIME;
+    lease->cltt_ = DatabaseConnection::MAX_DB_TIME;
 
     // Insert should throw.
     ASSERT_THROW(lmptr_->addLease(leases[1]), DbOperationError);
@@ -857,7 +857,7 @@ GenericLeaseMgrTest::testMaxDate6() {
     // Set valid_lft_ to 1 day, cllt_ to max time. This should make expire
     // time too large to store.
     lease->valid_lft_ = 24*60*60;
-    lease->cltt_ = LeaseMgr::MAX_DB_TIME;
+    lease->cltt_ = DatabaseConnection::MAX_DB_TIME;
 
     // Insert should throw.
     ASSERT_THROW(lmptr_->addLease(leases[1]), DbOperationError);

+ 496 - 0
src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc

@@ -0,0 +1,496 @@
+// Copyright (C) 2015 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 <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcpsrv/tests/test_utils.h>
+#include <exceptions/exceptions.h>
+#include <dhcpsrv/mysql_connection.h>
+#include <dhcpsrv/mysql_host_data_source.h>
+#include <dhcpsrv/tests/generic_host_data_source_unittest.h>
+#include <dhcpsrv/host_data_source_factory.h>
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <utility>
+
+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_mysql_copy.h"
+
+// Connection strings.
+// Database: keatest
+// Host: localhost
+// Username: keatest
+// Password: keatest
+const char* VALID_TYPE = "type=mysql";
+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() {
+    MySqlHolder mysql;
+
+    // Open database
+    (void) mysql_real_connect(mysql, "localhost", "keatest",
+                              "keatest", "keatest", 0, NULL, 0);
+
+    // Get rid of everything in it.
+    for (int i = 0; destroy_statement[i] != NULL; ++i) {
+        (void) mysql_query(mysql, destroy_statement[i]);
+    }
+}
+
+// @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() {
+    MySqlHolder mysql;
+
+    // Open database
+    (void) mysql_real_connect(mysql, "localhost", "keatest",
+                              "keatest", "keatest", 0, NULL, 0);
+
+    // Execute creation statements.
+    for (int i = 0; create_statement[i] != NULL; ++i) {
+        ASSERT_EQ(0, mysql_query(mysql, create_statement[i]))
+            << "Failed on statement " << i << ": " << create_statement[i];
+    }
+}
+
+
+
+class MySqlHostDataSourceTest : public GenericHostDataSourceTest {
+public:
+    /// @brief Constructor
+    ///
+    /// Deletes everything from the database and opens it.
+    MySqlHostDataSourceTest() {
+
+        // Ensure schema is the correct one.
+        destroySchema();
+        createSchema();
+
+        // Connect to the database
+        try {
+            HostDataSourceFactory::create(validConnectionString());
+        } catch (...) {
+            std::cerr << "*** ERROR: unable to open database. The test\n"
+                         "*** environment is broken and must be fixed before\n"
+                         "*** the MySQL tests will run correctly.\n"
+                         "*** The reason for the problem is described in the\n"
+                         "*** accompanying exception output.\n";
+            throw;
+        }
+
+        hdsptr_ = &(HostDataSourceFactory::instance());
+    }
+
+    /// @brief Destructor
+    ///
+    /// Rolls back all pending transactions.  The deletion of myhdsptr_ will close
+    /// the database.  Then reopen it and delete everything created by the test.
+    virtual ~MySqlHostDataSourceTest() {
+        hdsptr_->rollback();
+        HostDataSourceFactory::destroy();
+        destroySchema();
+    }
+
+    /// @brief Reopen the database
+    ///
+    /// Closes the database and re-open it.  Anything committed should be
+    /// visible.
+    ///
+    /// Parameter is ignored for MySQL backend as the v4 and v6 leases share
+    /// the same database.
+    void reopen(Universe) {
+        HostDataSourceFactory::destroy();
+        HostDataSourceFactory::create(validConnectionString());
+        hdsptr_ = &(HostDataSourceFactory::instance());
+    }
+
+};
+
+/// @brief Check that database can be opened
+///
+/// This test checks if the MySqlHostDataSource can be instantiated.  This happens
+/// only if the database can be opened.  Note that this is not part of the
+/// MySqlLeaseMgr test fixure set.  This test checks that the database can be
+/// opened: the fixtures assume that and check basic operations.
+
+TEST(MySqlHostDataSource, 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 {
+        HostDataSourceFactory::create(validConnectionString());
+        EXPECT_NO_THROW((void) HostDataSourceFactory::instance());
+        HostDataSourceFactory::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 MySQL 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(HostDataSourceFactory::instance(), NoHostDataSourceManager);
+
+    // Check that wrong specification of backend throws an exception.
+    // (This is really a check on LeaseMgrFactory, but is convenient to
+    // perform here.)
+    EXPECT_THROW(HostDataSourceFactory::create(connectionString(
+        NULL, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+        InvalidParameter);
+    EXPECT_THROW(HostDataSourceFactory::create(connectionString(
+        INVALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+        InvalidType);
+
+    // Check that invalid login data causes an exception.
+    EXPECT_THROW(HostDataSourceFactory::create(connectionString(
+        VALID_TYPE, INVALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD)),
+        DbOpenError);
+    EXPECT_THROW(HostDataSourceFactory::create(connectionString(
+        VALID_TYPE, VALID_NAME, INVALID_HOST, VALID_USER, VALID_PASSWORD)),
+        DbOpenError);
+    EXPECT_THROW(HostDataSourceFactory::create(connectionString(
+        VALID_TYPE, VALID_NAME, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+        DbOpenError);
+    EXPECT_THROW(HostDataSourceFactory::create(connectionString(
+        VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, INVALID_PASSWORD)),
+        DbOpenError);
+
+    // Check for missing parameters
+    EXPECT_THROW(HostDataSourceFactory::create(connectionString(
+        VALID_TYPE, NULL, VALID_HOST, INVALID_USER, VALID_PASSWORD)),
+        NoDatabaseName);
+
+    // Tidy up after the test
+    destroySchema();
+}
+
+/// @brief Check conversion functions
+///
+/// The server works using cltt and valid_filetime.  In the database, the
+/// information is stored as expire_time and valid-lifetime, which are
+/// related by
+///
+/// expire_time = cltt + valid_lifetime
+///
+/// This test checks that the conversion is correct.  It does not check that the
+/// data is entered into the database correctly, only that the MYSQL_TIME
+/// structure used for the entry is correctly set up.
+TEST(MySqlConnection, checkTimeConversion) {
+    const time_t cltt = time(NULL);
+    const uint32_t valid_lft = 86400;       // 1 day
+    struct tm tm_expire;
+    MYSQL_TIME mysql_expire;
+
+    // Work out what the broken-down time will be for one day
+    // after the current time.
+    time_t expire_time = cltt + valid_lft;
+    (void) localtime_r(&expire_time, &tm_expire);
+
+    // Convert to the database time
+    MySqlConnection::convertToDatabaseTime(cltt, valid_lft, mysql_expire);
+
+    // Are the times the same?
+    EXPECT_EQ(tm_expire.tm_year + 1900, mysql_expire.year);
+    EXPECT_EQ(tm_expire.tm_mon + 1,  mysql_expire.month);
+    EXPECT_EQ(tm_expire.tm_mday, mysql_expire.day);
+    EXPECT_EQ(tm_expire.tm_hour, mysql_expire.hour);
+    EXPECT_EQ(tm_expire.tm_min, mysql_expire.minute);
+    EXPECT_EQ(tm_expire.tm_sec, mysql_expire.second);
+    EXPECT_EQ(0, mysql_expire.second_part);
+    EXPECT_EQ(0, mysql_expire.neg);
+
+    // Convert back
+    time_t converted_cltt = 0;
+    MySqlConnection::convertFromDatabaseTime(mysql_expire, valid_lft, converted_cltt);
+    EXPECT_EQ(cltt, converted_cltt);
+}
+
+// Test verifies if a host reservation can be added and later retrieved by IPv4
+// address. Host uses hw address as identifier.
+TEST_F(MySqlHostDataSourceTest, basic4HWAddr) {
+    testBasic4(true);
+}
+
+// Test verifies if a host reservation can be added and later retrieved by IPv4
+// address. Host uses client-id (DUID) as identifier.
+TEST_F(MySqlHostDataSourceTest, basic4ClientId) {
+    testBasic4(false);
+}
+
+// Test verifies that multiple hosts can be added and later retrieved by their
+// reserved IPv4 address. This test uses HW addresses as identifiers.
+TEST_F(MySqlHostDataSourceTest, getByIPv4HWaddr) {
+    testGetByIPv4(true);
+}
+
+// Test verifies that multiple hosts can be added and later retrieved by their
+// reserved IPv4 address. This test uses client-id (DUID) as identifiers.
+TEST_F(MySqlHostDataSourceTest, getByIPv4ClientId) {
+    testGetByIPv4(false);
+}
+
+// Test verifies if a host reservation can be added and later retrieved by
+// hardware address.
+TEST_F(MySqlHostDataSourceTest, get4ByHWaddr) {
+    testGet4ByHWAddr();
+}
+
+// Test verifies if a host reservation can be added and later retrieved by
+// client identifier.
+TEST_F(MySqlHostDataSourceTest, get4ByClientId) {
+    testGet4ByClientId();
+}
+
+// Test verifies if hardware address and client identifier are not confused.
+TEST_F(MySqlHostDataSourceTest, hwaddrNotClientId1) {
+    testHWAddrNotClientId();
+}
+
+// Test verifies if hardware address and client identifier are not confused.
+TEST_F(MySqlHostDataSourceTest, hwaddrNotClientId2) {
+    testClientIdNotHWAddr();
+}
+
+// Test verifies if a host with FQDN hostname can be stored and later retrieved.
+TEST_F(MySqlHostDataSourceTest, hostnameFQDN) {
+    testHostname("foo.example.org", 1);
+}
+
+// Test verifies if 100 hosts with unique FQDN hostnames can be stored and later
+// retrieved.
+TEST_F(MySqlHostDataSourceTest, hostnameFQDN100) {
+    testHostname("foo.example.org", 100);
+}
+
+// Test verifies if a host without any hostname specified can be stored and later
+// retrieved.
+TEST_F(MySqlHostDataSourceTest, noHostname) {
+    testHostname("", 1);
+}
+
+// Test verifies if the hardware or client-id query can match hardware address.
+TEST_F(MySqlHostDataSourceTest, DISABLED_hwaddrOrClientId1) {
+    /// @todo: The logic behind ::get4(subnet_id, hwaddr, duid) call needs to
+    /// be discussed.
+    ///
+    /// @todo: Add host reservation with hardware address X, try to retrieve
+    /// host for hardware address X or client identifier Y, verify that the
+    /// reservation is returned.
+}
+
+// Test verifies if the hardware or client-id query can match client-id.
+TEST_F(MySqlHostDataSourceTest, DISABLED_hwaddrOrClientId2) {
+    /// @todo: The logic behind ::get4(subnet_id, hwaddr, duid) call needs to
+    /// be discussed.
+    ///
+    /// @todo: Add host reservation with client identifier Y, try to retrieve
+    /// host for hardware address X or client identifier Y, verify that the
+    /// reservation is returned.
+}
+
+// Test verifies that host with IPv6 address and DUID can be added and
+// later retrieved by IPv6 address.
+TEST_F(MySqlHostDataSourceTest, DISABLED_get6AddrWithDuid) {
+    /// @todo: Uncomment when IPv6 support (4212) is implemented.
+    testGetByIPv6(BaseHostDataSource::ID_DUID, false);
+}
+
+// Test verifies that host with IPv6 address and HWAddr can be added and
+// later retrieved by IPv6 address.
+TEST_F(MySqlHostDataSourceTest, DISABLED_get6AddrWithHWAddr) {
+    /// @todo: Uncomment when IPv6 support (4212) is implemented.
+    testGetByIPv6(BaseHostDataSource::ID_HWADDR, false);
+}
+
+// Test verifies that host with IPv6 prefix and DUID can be added and
+// later retrieved by IPv6 prefix.
+TEST_F(MySqlHostDataSourceTest, DISABLED_get6PrefixWithDuid) {
+    /// @todo: Uncomment when IPv6 support (4212) is implemented.
+    testGetByIPv6(BaseHostDataSource::ID_DUID, true);
+}
+
+// Test verifies that host with IPv6 prefix and HWAddr can be added and
+// later retrieved by IPv6 prefix.
+TEST_F(MySqlHostDataSourceTest, DISABLED_get6PrefixWithHWaddr) {
+    /// @todo: Uncomment when IPv6 support (4212) is implemented.
+    testGetByIPv6(BaseHostDataSource::ID_HWADDR, true);
+}
+
+// Test verifies if a host reservation can be added and later retrieved by
+// hardware address.
+TEST_F(MySqlHostDataSourceTest, DISABLED_get6ByHWaddr) {
+    /// @todo: Uncomment when IPv6 support (4212) is implemented.
+    testGet6ByHWAddr();
+}
+
+// Test verifies if a host reservation can be added and later retrieved by
+// client identifier.
+TEST_F(MySqlHostDataSourceTest, DISABLED_get6ByClientId) {
+    /// @todo: Uncomment when IPv6 support (4212) is implemented.
+    testGet6ByClientId();
+}
+
+// Test verifies if a host reservation can be stored with both IPv6 address and
+// prefix.
+TEST_F(MySqlHostDataSourceTest, DISABLED_addr6AndPrefix) {
+    /// @todo: Implement this test as part of #4212.
+
+    /// @todo: Add host reservation with an IPv6 address and IPv6 prefix,
+    /// retrieve it and verify that both v6 address and prefix are retrieved
+    /// correctly.
+}
+
+// Test verifies if multiple client classes for IPv4 can be stored.
+TEST_F(MySqlHostDataSourceTest, DISABLED_multipleClientClasses4) {
+    /// @todo: Implement this test as part of #4213.
+
+    /// Add host reservation with a multiple v4 client-classes, retrieve it and
+    /// make sure that all client classes are retrieved properly.
+}
+
+// Test verifies if multiple client classes for IPv6 can be stored.
+TEST_F(MySqlHostDataSourceTest, DISABLED_multipleClientClasses6) {
+    /// @todo: Implement this test as part of #4213.
+
+    /// Add host reservation with a multiple v6 client-classes, retrieve it and
+    /// make sure that all client classes are retrieved properly.
+}
+
+// Test verifies if multiple client classes for both IPv4 and IPv6 can be stored.
+TEST_F(MySqlHostDataSourceTest, DISABLED_multipleClientClassesBoth) {
+    /// @todo: Implement this test as part of #4213..
+
+    /// Add host reservation with a multiple v4 and v6 client-classes, retrieve
+    /// it and make sure that all client classes are retrieved properly. Also,
+    /// check that the classes are not confused.
+}
+
+// Test if the same host can have reservations in different subnets (with the
+// same hardware address). The test logic is as follows:
+// Insert 10 host reservations for a given physical host (the same
+// hardware address), but for different subnets (different subnet-ids).
+// Make sure that getAll() returns them all correctly.
+TEST_F(MySqlHostDataSourceTest, multipleSubnetsHWAddr) {
+    testMultipleSubnets(10, true);
+}
+
+// Test if the same host can have reservations in different subnets (with the
+// same client identifier). The test logic is as follows:
+//
+// Insert 10 host reservations for a given physical host (the same
+// client-identifier), but for different subnets (different subnet-ids).
+// Make sure that getAll() returns them correctly.
+TEST_F(MySqlHostDataSourceTest, multipleSubnetsClientId) {
+    testMultipleSubnets(10, false);
+}
+
+// Test if host reservations made for different IPv6 subnets are handled correctly.
+// The test logic is as follows:
+//
+// Insert 10 host reservations for different subnets. Make sure that
+// get6(subnet-id, ...) calls return correct reservation.
+TEST_F(MySqlHostDataSourceTest, subnetId6) {
+    testSubnetId6(10, BaseHostDataSource::ID_HWADDR);
+}
+
+// Test if the duplicate host instances can't be inserted. The test logic is as
+// follows: try to add multiple instances of the same host reservation and
+// verify that the second and following attempts will throw exceptions.
+TEST_F(MySqlHostDataSourceTest, addDuplicate) {
+    testAddDuplicate();
+}
+
+}; // Of anonymous namespace

+ 3 - 2
src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc

@@ -16,6 +16,7 @@
 
 #include <asiolink/io_address.h>
 #include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/mysql_connection.h>
 #include <dhcpsrv/mysql_lease_mgr.h>
 #include <dhcpsrv/tests/test_utils.h>
 #include <dhcpsrv/tests/generic_lease_mgr_unittest.h>
@@ -289,7 +290,7 @@ TEST_F(MySqlLeaseMgrTest, checkTimeConversion) {
     (void) localtime_r(&expire_time, &tm_expire);
 
     // Convert to the database time
-    MySqlLeaseMgr::convertToDatabaseTime(cltt, valid_lft, mysql_expire);
+    MySqlConnection::convertToDatabaseTime(cltt, valid_lft, mysql_expire);
 
     // Are the times the same?
     EXPECT_EQ(tm_expire.tm_year + 1900, mysql_expire.year);
@@ -303,7 +304,7 @@ TEST_F(MySqlLeaseMgrTest, checkTimeConversion) {
 
     // Convert back
     time_t converted_cltt = 0;
-    MySqlLeaseMgr::convertFromDatabaseTime(mysql_expire, valid_lft, converted_cltt);
+    MySqlConnection::convertFromDatabaseTime(mysql_expire, valid_lft, converted_cltt);
     EXPECT_EQ(cltt, converted_cltt);
 }
 

+ 2 - 0
src/lib/dhcpsrv/tests/schema_mysql_copy.h

@@ -45,6 +45,8 @@ const char* destroy_statement[] = {
     "DROP TABLE hosts",
     "DROP TABLE dhcp4_options",
     "DROP TABLE dhcp6_options",
+
+    "DROP TRIGGER host_BDEL",
     NULL
 };