From: Tomek Mrugalski Date: Mon, 30 Nov 2015 20:26:25 +0000 (+0100) Subject: [3682_rebase] Merge branch 'trac3682' into trac3682_rebase X-Git-Tag: trac4231_base~25^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=65efc0da7f5d3bbc9cd2e698a92574f47fd20c96;p=thirdparty%2Fkea.git [3682_rebase] Merge branch 'trac3682' into trac3682_rebase # Conflicts: # src/lib/dhcpsrv/mysql_lease_mgr.cc # src/lib/dhcpsrv/mysql_lease_mgr.h --- diff --git a/src/lib/dhcpsrv/Makefile.am b/src/lib/dhcpsrv/Makefile.am old mode 100644 new mode 100755 index c2f057dd8c..0b7985cfe7 --- a/src/lib/dhcpsrv/Makefile.am +++ b/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 diff --git a/src/lib/dhcpsrv/base_host_data_source.h b/src/lib/dhcpsrv/base_host_data_source.h index aae1676e3c..ce458b15f4 100644 --- a/src/lib/dhcpsrv/base_host_data_source.h +++ b/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() {}; }; } diff --git a/src/lib/dhcpsrv/cfg_hosts.h b/src/lib/dhcpsrv/cfg_hosts.h index e8b08b0213..c0e4151669 100644 --- a/src/lib/dhcpsrv/cfg_hosts.h +++ b/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. diff --git a/src/lib/dhcpsrv/database_connection.cc b/src/lib/dhcpsrv/database_connection.cc index 440a4d21cd..88072d8b22 100755 --- a/src/lib/dhcpsrv/database_connection.cc +++ b/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); diff --git a/src/lib/dhcpsrv/database_connection.h b/src/lib/dhcpsrv/database_connection.h index 7e75debaeb..0479434917 100755 --- a/src/lib/dhcpsrv/database_connection.h +++ b/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 ParameterMap; diff --git a/src/lib/dhcpsrv/db_exceptions.h b/src/lib/dhcpsrv/db_exceptions.h new file mode 100644 index 0000000000..3924e117c9 --- /dev/null +++ b/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 + +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 diff --git a/src/lib/dhcpsrv/host_data_source_factory.cc b/src/lib/dhcpsrv/host_data_source_factory.cc new file mode 100644 index 0000000000..245bf83fc1 --- /dev/null +++ b/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 +#include +#include + +#ifdef HAVE_MYSQL +#include +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace isc { +namespace dhcp { + + +boost::scoped_ptr& +HostDataSourceFactory::getHostDataSourcePtr() { + static boost::scoped_ptr 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 diff --git a/src/lib/dhcpsrv/host_data_source_factory.h b/src/lib/dhcpsrv/host_data_source_factory.h new file mode 100644 index 0000000000..c0b38c643b --- /dev/null +++ b/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 +#include +#include +#include + +#include + +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 always 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& getHostDataSourcePtr(); + +}; + + +}; // end of isc::dhcp namespace +}; // end of isc namespace + +#endif diff --git a/src/lib/dhcpsrv/host_mgr.cc b/src/lib/dhcpsrv/host_mgr.cc index a0a3e455cf..6b54804f89 100644 --- a/src/lib/dhcpsrv/host_mgr.cc +++ b/src/lib/dhcpsrv/host_mgr.cc @@ -17,6 +17,7 @@ #include #include #include +#include namespace { @@ -37,6 +38,8 @@ namespace dhcp { using namespace isc::asiolink; +boost::shared_ptr HostMgr::alternate_source; + boost::scoped_ptr& HostMgr::getHostMgrPtr() { static boost::scoped_ptr 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"); diff --git a/src/lib/dhcpsrv/host_mgr.h b/src/lib/dhcpsrv/host_mgr.h index 55a4f4c05e..051a0fb66b 100644 --- a/src/lib/dhcpsrv/host_mgr.h +++ b/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 alternate_source; + static boost::shared_ptr alternate_source; /// @brief Returns a pointer to the currently used instance of the /// @c HostMgr. diff --git a/src/lib/dhcpsrv/hosts_messages.mes b/src/lib/dhcpsrv/hosts_messages.mes index cc3b8545a2..0644b4594d 100644 --- a/src/lib/dhcpsrv/hosts_messages.mes +++ b/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 diff --git a/src/lib/dhcpsrv/lease_mgr.cc b/src/lib/dhcpsrv/lease_mgr.cc index 4153771131..2067e7a74e 100755 --- a/src/lib/dhcpsrv/lease_mgr.cc +++ b/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 { diff --git a/src/lib/dhcpsrv/lease_mgr.h b/src/lib/dhcpsrv/lease_mgr.h index 7e1024a103..78d6504528 100755 --- a/src/lib/dhcpsrv/lease_mgr.h +++ b/src/lib/dhcpsrv/lease_mgr.h @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include #include @@ -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() diff --git a/src/lib/dhcpsrv/mysql_connection.cc b/src/lib/dhcpsrv/mysql_connection.cc index b9d3e05e68..10f841905d 100755 --- a/src/lib/dhcpsrv/mysql_connection.cc +++ b/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(cltt) + + static_cast(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(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 diff --git a/src/lib/dhcpsrv/mysql_connection.h b/src/lib/dhcpsrv/mysql_connection.h index 0824d4955a..2ebc6be7ed 100755 --- a/src/lib/dhcpsrv/mysql_connection.h +++ b/src/lib/dhcpsrv/mysql_connection.h @@ -17,7 +17,7 @@ #include #include -#include +#include #include 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 diff --git a/src/lib/dhcpsrv/mysql_host_data_source.cc b/src/lib/dhcpsrv/mysql_host_data_source.cc new file mode 100644 index 0000000000..e5aeb71413 --- /dev/null +++ b/src/lib/dhcpsrv/mysql_host_data_source.cc @@ -0,0 +1,1037 @@ +// 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 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +using namespace isc; +using namespace isc::dhcp; +using namespace std; + +namespace { +/// @brief Maximum length of classes stored in a dhcp4/6_client_classes field +/// +const size_t CLIENT_CLASSES_MAX_LEN = 255; + +/// @brief Maximum length of the hostname stored in DNS. +/// +/// This length is restricted by the length of the domain-name carried +/// in the Client FQDN %Option (see RFC4702 and RFC4704). +const size_t HOSTNAME_MAX_LEN = 255; + + +TaggedStatement tagged_statements[] = { + {MySqlHostDataSource::INSERT_HOST, + "INSERT INTO hosts(host_id, dhcp_identifier, dhcp_identifier_type, " + "dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, " + "dhcp4_client_classes, dhcp6_client_classes) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"}, + {MySqlHostDataSource::GET_HOST_HWADDR_DUID, + "SELECT host_id, dhcp_identifier, dhcp_identifier_type, " + "dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, " + "dhcp4_client_classes, dhcp6_client_classes " + "FROM hosts " + "WHERE dhcp_identifier = ? AND dhcp_identifier_type = ?"}, + {MySqlHostDataSource::GET_HOST_ADDR, + "SELECT host_id, dhcp_identifier, dhcp_identifier_type, " + "dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, " + "dhcp4_client_classes, dhcp6_client_classes " + "FROM hosts " + "WHERE ipv4_address = ?"}, + {MySqlHostDataSource::GET_HOST_SUBID4_DHCPID, + "SELECT host_id, dhcp_identifier, dhcp_identifier_type, " + "dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, " + "dhcp4_client_classes, dhcp6_client_classes " + "FROM hosts " + "WHERE dhcp4_subnet_id = ? AND dhcp_identifier_type = ? " + " AND dhcp_identifier = ?"}, + {MySqlHostDataSource::GET_HOST_SUBID6_DHCPID, + "SELECT host_id, dhcp_identifier, dhcp_identifier_type, " + "dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, " + "dhcp4_client_classes, dhcp6_client_classes " + "FROM hosts " + "WHERE dhcp6_subnet_id = ? AND dhcp_identifier_type = ? " + " AND dhcp_identifier = ?"}, + {MySqlHostDataSource::GET_HOST_SUBID_ADDR, + "SELECT host_id, dhcp_identifier, dhcp_identifier_type, " + "dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, " + "dhcp4_client_classes, dhcp6_client_classes " + "FROM hosts " + "WHERE dhcp4_subnet_id = ? AND ipv4_address = ?"}, + {MySqlHostDataSource::GET_HOST_PREFIX, + "SELECT h.host_id, dhcp_identifier, dhcp_identifier_type, " + "dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, " + "dhcp4_client_classes, dhcp6_client_classes " + "FROM hosts h, ipv6_reservations r " + "WHERE h.host_id = r.host_id AND r.prefix_len = ? " + " AND r.address = ?"}, + {MySqlHostDataSource::GET_VERSION, + "SELECT version, minor FROM schema_version"}, + {MySqlHostDataSource::NUM_STATEMENTS, NULL} +}; + +}; + +namespace isc { +namespace dhcp { + +class MySqlHostReservationExchange { + /// @brief Set number of database columns for this host structure + static const size_t HOST_COLUMNS = 9; + +public: + + /// @brief Constructor + /// + /// The initialization of the variables here is only to satisfy cppcheck - + /// all variables are initialized/set in the methods before they are used. + MySqlHostReservationExchange() : host_id_(0), dhcp_identifier_length_(0), + dhcp_identifier_type_(0), dhcp4_subnet_id_(0), dhcp6_subnet_id_(0), + ipv4_address_(0), hostname_length_(0), dhcp4_client_classes_length_(0), + dhcp6_client_classes_length_(0), dhcp4_subnet_id_null_(MLM_FALSE), + dhcp6_subnet_id_null_(MLM_FALSE), ipv4_address_null_(MLM_FALSE), + hostname_null_(MLM_FALSE), dhcp4_client_classes_null_(MLM_FALSE), + dhcp6_client_classes_null_(MLM_FALSE){ + + memset(dhcp_identifier_buffer_, 0, sizeof(dhcp_identifier_buffer_)); + memset(hostname_, 0, sizeof(hostname_)); + memset(dhcp4_client_classes_, 0, sizeof(dhcp4_client_classes_)); + memset(dhcp6_client_classes_, 0, sizeof(dhcp6_client_classes_)); + std::fill(&error_[0], &error_[HOST_COLUMNS], MLM_FALSE); + + // Set the column names (for error messages) + columns_[0] = "host_id"; + columns_[1] = "dhcp_identifier"; + columns_[2] = "dhcp_identifier_type"; + columns_[3] = "dhcp4_subnet_id"; + columns_[4] = "dhcp6_subnet_id"; + columns_[5] = "ipv4_address"; + columns_[6] = "hostname"; + columns_[7] = "dhcp4_client_classes"; + columns_[8] = "dhcp6_client_classes"; + BOOST_STATIC_ASSERT(8 < HOST_COLUMNS); + } + + /// @brief Set error indicators + /// + /// Sets the error indicator for each of the MYSQL_BIND elements. It points + /// the "error" field within an element of the input array to the + /// corresponding element of the passed error array. + /// + /// @param bind Array of BIND elements + /// @param error Array of error elements. If there is an error in getting + /// data associated with one of the "bind" elements, the + /// corresponding element in the error array is set to MLM_TRUE. + /// @param count Size of each of the arrays. + static void setErrorIndicators(MYSQL_BIND* bind, my_bool* error, + size_t count) { + for (size_t i = 0; i < count; ++i) { + error[i] = MLM_FALSE; + bind[i].error = reinterpret_cast(&error[i]); + } + } + + /// @brief Return columns in error + /// + /// If an error is returned from a fetch (in particular, a truncated + /// status), this method can be called to get the names of the fields in + /// error. It returns a string comprising the names of the fields + /// separated by commas. In the case of there being no error indicators + /// set, it returns the string "(None)". + /// + /// @param error Array of error elements. An element is set to MLM_TRUE + /// if the corresponding column in the database is the source of + /// the error. + /// @param names Array of column names, the same size as the error array. + /// @param count Size of each of the arrays. + static std::string getColumnsInError(my_bool* error, std::string* names, + size_t count) { + std::string result = ""; + + // Accumulate list of column names + for (size_t i = 0; i < count; ++i) { + if (error[i] == MLM_TRUE) { + if (!result.empty()) { + result += ", "; + } + result += names[i]; + } + } + + if (result.empty()) { + result = "(None)"; + } + + return (result); + } + + /// @brief Create MYSQL_BIND objects for Host Pointer + /// + /// Fills in the MYSQL_BIND array for sending data in the Host object to + /// the database. + /// + /// @param host Host object to be added to the database. + /// None of the fields in the host reservation are modified - + /// the host data is only read. + /// + /// @return Vector of MySQL BIND objects representing the data to be added. + std::vector createBindForSend(const HostPtr& host) { + + // Store host object to ensure it remains valid. + host_ = host; + + // Initialize prior to constructing the array of MYSQL_BIND structures. + // It sets all fields, including is_null, to zero, so we need to set + // is_null only if it should be true. This gives up minor performance + // benefit while being safe approach. For improved readability, the + // code that explicitly sets is_null is there, but is commented out. + // This also takes care of seeeting bind_[X].is_null to MLM_FALSE. + memset(bind_, 0, sizeof(bind_)); + + // Set up the structures for the various components of the host structure. + + try { + // host_id : INT UNSIGNED NOT NULL + // The host_id is auto_incremented by MySQL database, + // so we need to pass the NULL value + host_id_ = static_cast(NULL); + bind_[0].buffer_type = MYSQL_TYPE_LONG; + bind_[0].buffer = reinterpret_cast(&host_id_); + bind_[0].is_unsigned = MLM_TRUE; + + // dhcp_identifier : VARBINARY(128) NOT NULL + // Check which of the identifiers is used and set values accordingly + if (host_->getDuid()) { + dhcp_identifier_length_ = host_->getDuid()->getDuid().size(); + bind_[1].buffer_type = MYSQL_TYPE_BLOB; + bind_[1].buffer = reinterpret_cast + (const_cast(&(host_->getDuid()->getDuid()[0]))); + bind_[1].buffer_length = dhcp_identifier_length_; + bind_[1].length = &dhcp_identifier_length_; + } else if (host_->getHWAddress()){ + dhcp_identifier_length_ = host_->getHWAddress()->hwaddr_.size(); + bind_[1].buffer_type = MYSQL_TYPE_BLOB; + bind_[1].buffer = reinterpret_cast + (&(host_->getHWAddress()->hwaddr_[0])); + bind_[1].buffer_length = dhcp_identifier_length_; + bind_[1].length = &dhcp_identifier_length_; + } + + // dhcp_identifier_type : TINYINT NOT NULL + // Check which of the identifier types is used and set values accordingly + if (host_->getHWAddress()) { + dhcp_identifier_type_ = BaseHostDataSource::ID_HWADDR; // 0 + bind_[2].buffer_type = MYSQL_TYPE_TINY; + bind_[2].buffer = reinterpret_cast(&dhcp_identifier_type_); + bind_[2].is_unsigned = MLM_TRUE; + } else if (host_->getDuid()) { + dhcp_identifier_type_ = BaseHostDataSource::ID_DUID; // 1 + bind_[2].buffer_type = MYSQL_TYPE_TINY; + bind_[2].buffer = reinterpret_cast(&dhcp_identifier_type_); + bind_[2].is_unsigned = MLM_TRUE; + } + + // dhcp4_subnet_id : INT UNSIGNED NULL + // Can't take an address of intermediate object, so let's store it + // in dhcp4_subnet_id_ + dhcp4_subnet_id_ = host_->getIPv4SubnetID(); + bind_[3].buffer_type = MYSQL_TYPE_LONG; + bind_[3].buffer = reinterpret_cast(&dhcp4_subnet_id_); + bind_[3].is_unsigned = MLM_TRUE; + + // dhcp6_subnet_id : INT UNSIGNED NULL + // Can't take an address of intermediate object, so let's store it + // in dhcp6_subnet_id_ + dhcp6_subnet_id_ = host_->getIPv6SubnetID(); + bind_[4].buffer_type = MYSQL_TYPE_LONG; + bind_[4].buffer = reinterpret_cast(&dhcp6_subnet_id_); + bind_[4].is_unsigned = MLM_TRUE; + + // ipv4_address : INT UNSIGNED NULL + // The address in the Host structure is an IOAddress object. Convert + // this to an integer for storage. + ipv4_address_ = static_cast(host_->getIPv4Reservation()); + bind_[5].buffer_type = MYSQL_TYPE_LONG; + bind_[5].buffer = reinterpret_cast(&ipv4_address_); + bind_[5].is_unsigned = MLM_TRUE; + // bind_[5].is_null = &MLM_FALSE; // commented out for performance + // reasons, see memset() above + + // hostname : VARCHAR(255) NULL + strncpy(hostname_, host_->getHostname().c_str(), HOSTNAME_MAX_LEN - 1); + hostname_length_ = host_->getHostname().length(); + bind_[6].buffer_type = MYSQL_TYPE_STRING; + bind_[6].buffer = reinterpret_cast(hostname_); + bind_[6].buffer_length = hostname_length_; + + // dhcp4_client_classes : VARCHAR(255) NULL + bind_[7].buffer_type = MYSQL_TYPE_STRING; + string classes4_txt = classesToString(host_->getClientClasses4()); + strncpy(dhcp4_client_classes_, classes4_txt.c_str(), CLIENT_CLASSES_MAX_LEN - 1); + bind_[7].buffer = dhcp4_client_classes_; + bind_[7].buffer_length = classes4_txt.length(); + + // dhcp6_client_classes : VARCHAR(255) NULL + bind_[8].buffer_type = MYSQL_TYPE_STRING; + string classes6_txt = classesToString(host->getClientClasses6()); + strncpy(dhcp6_client_classes_, classes6_txt.c_str(), CLIENT_CLASSES_MAX_LEN - 1); + bind_[8].buffer = dhcp6_client_classes_; + bind_[8].buffer_length = classes6_txt.length(); + bind_[8].buffer_length = sizeof(host_->getClientClasses6()); + + } catch (const std::exception& ex) { + isc_throw(DbOperationError, + "Could not create bind array from Host: " + << host_->getHostname() << ", reason: " << ex.what()); + } + + // Add the data to the vector. Note the end element is one after the + // end of the array. + return (std::vector(&bind_[0], &bind_[HOST_COLUMNS])); + } + + /// @brief Create BIND array to receive data + /// + /// Creates a MYSQL_BIND array to receive Lease4 data from the database. + /// After data is successfully received, getLeaseData() can be used to copy + /// it to a Lease6 object. + /// + std::vector createBindForReceive() { + // Initialize MYSQL_BIND array. + // It sets all fields, including is_null, to zero, so we need to set + // is_null only if it should be true. This gives up minor performance + // benefit while being safe approach. For improved readability, the + // code that explicitly sets is_null is there, but is commented out. + // This also takes care of seeeting bind_[X].is_null to MLM_FALSE. + memset(bind_, 0, sizeof(bind_)); + + // host_id : INT UNSIGNED NOT NULL + bind_[0].buffer_type = MYSQL_TYPE_LONG; + bind_[0].buffer = reinterpret_cast(&host_id_); + bind_[0].is_unsigned = MLM_TRUE; + + // dhcp_identifier : VARBINARY(128) NOT NULL + dhcp_identifier_length_ = sizeof(dhcp_identifier_buffer_); + bind_[1].buffer_type = MYSQL_TYPE_BLOB; + bind_[1].buffer = reinterpret_cast(dhcp_identifier_buffer_); + bind_[1].buffer_length = dhcp_identifier_length_; + bind_[1].length = &dhcp_identifier_length_; + + // dhcp_identifier_type : TINYINT NOT NULL + bind_[2].buffer_type = MYSQL_TYPE_TINY; + bind_[2].buffer = reinterpret_cast(&dhcp_identifier_type_); + bind_[2].is_unsigned = MLM_TRUE; + + // dhcp4_subnet_id : INT UNSIGNED NULL + dhcp4_subnet_id_null_ = MLM_FALSE; + bind_[3].buffer_type = MYSQL_TYPE_LONG; + bind_[3].buffer = reinterpret_cast(&dhcp4_subnet_id_); + bind_[3].is_unsigned = MLM_TRUE; + bind_[3].is_null = &dhcp4_subnet_id_null_; + + // dhcp6_subnet_id : INT UNSIGNED NULL + dhcp6_subnet_id_null_ = MLM_FALSE; + bind_[4].buffer_type = MYSQL_TYPE_LONG; + bind_[4].buffer = reinterpret_cast(&dhcp6_subnet_id_); + bind_[4].is_unsigned = MLM_TRUE; + bind_[4].is_null = &dhcp6_subnet_id_null_; + + // ipv4_address : INT UNSIGNED NULL + ipv4_address_null_ = MLM_FALSE; + bind_[5].buffer_type = MYSQL_TYPE_LONG; + bind_[5].buffer = reinterpret_cast(&ipv4_address_); + bind_[5].is_unsigned = MLM_TRUE; + bind_[5].is_null = &ipv4_address_null_; + + // hostname : VARCHAR(255) NULL + hostname_null_ = MLM_FALSE; + hostname_length_ = sizeof(hostname_); + bind_[6].buffer_type = MYSQL_TYPE_STRING; + bind_[6].buffer = reinterpret_cast(hostname_); + bind_[6].buffer_length = hostname_length_; + bind_[6].length = &hostname_length_; + bind_[6].is_null = &hostname_null_; + + // dhcp4_client_classes : VARCHAR(255) NULL + dhcp4_client_classes_null_ = MLM_FALSE; + dhcp4_client_classes_length_ = sizeof(dhcp4_client_classes_); + bind_[7].buffer_type = MYSQL_TYPE_STRING; + bind_[7].buffer = reinterpret_cast(dhcp4_client_classes_); + bind_[7].buffer_length = dhcp4_client_classes_length_; + bind_[7].length = &dhcp4_client_classes_length_; + bind_[7].is_null = &dhcp4_client_classes_null_; + + // dhcp6_client_classes : VARCHAR(255) NULL + dhcp6_client_classes_null_ = MLM_FALSE; + dhcp6_client_classes_length_ = sizeof(dhcp6_client_classes_); + bind_[8].buffer_type = MYSQL_TYPE_STRING; + bind_[8].buffer = reinterpret_cast(dhcp6_client_classes_); + bind_[8].buffer_length = dhcp6_client_classes_length_; + bind_[8].length = &dhcp6_client_classes_length_; + bind_[8].is_null = &dhcp6_client_classes_null_; + + // Add the error flags + setErrorIndicators(bind_, error_, HOST_COLUMNS); + + // .. and check that we have the numbers correct at compile time. + BOOST_STATIC_ASSERT(8 < HOST_COLUMNS); + + // Add the data to the vector. Note the end element is one after the + // end of the array. + return(std::vector(&bind_[0], &bind_[HOST_COLUMNS])); + + } + + /// @brief Copy Received Data into Host Object + /// + /// Called after the MYSQL_BIND array created by createBindForReceive() + /// has been used, this copies data from the internal member variables + /// into a Host objec. + /// + /// @return Host Pointer to a Lease6 object holding the relevant data. + HostPtr getHostData(){ + + // Set the dhcp identifier type in a variable of the appropriate data type, + // which has been initialized with an arbitrary (but valid) value. + Host::IdentifierType type = Host::IDENT_HWADDR; + + switch (dhcp_identifier_type_) { + case 0: + type = Host::IDENT_HWADDR; + break; + + case 1: + type = Host::IDENT_DUID; + break; + + default: + isc_throw(BadValue, "invalid dhcp identifier type returned: " + << static_cast(dhcp_identifier_type_) + << ". Only 0 or 1 are allowed."); + } + + // Set subnets ID's if they are given, if not, leave an empty object + SubnetID ipv4_subnet_id(0); + if (dhcp4_subnet_id_null_ == MLM_FALSE) { + ipv4_subnet_id = static_cast(dhcp4_subnet_id_); + } + + SubnetID ipv6_subnet_id(0); + if (dhcp6_subnet_id_null_ == MLM_FALSE) { + ipv6_subnet_id = static_cast(dhcp6_subnet_id_); + } + + // Set IPv4 address reservation if it was given, if not, set IPv4 zero address + asiolink::IOAddress ipv4_reservation = asiolink::IOAddress::IPV4_ZERO_ADDRESS(); + if (ipv4_address_null_ == MLM_FALSE) { + ipv4_reservation = asiolink::IOAddress(ipv4_address_); + } + + // Set hostname if it was given, if not lease empty string + std::string hostname; + if (hostname_null_ == MLM_FALSE) { + hostname = std::string (hostname_, hostname_ + hostname_length_); + } + + // Set client classes if they were given, if not, set empty client classes + ClientClasses dhcp4_client_classes; + if (dhcp4_client_classes_null_ == MLM_FALSE) { + dhcp4_client_classes.insert(dhcp4_client_classes_); + } + + ClientClasses dhcp6_client_classes; + if (dhcp6_client_classes_null_ == MLM_FALSE) { + dhcp6_client_classes.insert(dhcp6_client_classes_); + } + + // Returning Host object with set fields + return (HostPtr(new Host(dhcp_identifier_buffer_, dhcp_identifier_length_, + type, ipv4_subnet_id, ipv6_subnet_id, ipv4_reservation, + hostname, dhcp4_client_classes_, dhcp6_client_classes_))); + } + + /// @brief Return columns in error + /// + /// If an error is returned from a fetch (in particular, a truncated + /// status), this method can be called to get the names of the fields in + /// error. It returns a string comprising the names of the fields + /// separated by commas. In the case of there being no error indicators + /// set, it returns the string "(None)". + /// + /// @return Comma-separated list of columns in error, or the string + /// "(None)". + std::string getErrorColumns() { + return (getColumnsInError(error_, columns_, HOST_COLUMNS)); + } + + /// @brief Converts ClientClasses to a signgle string with coma separated values + /// + /// @param classes classes structure that contains zero or more classes + /// @return a single string with coma separated values + std::string classesToString(const ClientClasses& classes) { + string txt; + bool first = true; + for (ClientClasses::const_iterator it = classes.begin(); + it != classes.end(); ++it) { + if (!first) { + txt += ","; + } + txt += (*it); + + first = false; + } + + return (txt); + } + +private: + uint32_t host_id_; /// Host unique identifier + std::vector dhcp_identifier_; /// HW address (0) / DUID (1) + uint8_t dhcp_identifier_buffer_[DUID::MAX_DUID_LEN]; + /// Buffer for dhcp identifier + size_t dhcp_identifier_length_; /// Length of dhcp identifier + uint8_t dhcp_identifier_type_; /// Type of dhcp_identifier + uint32_t dhcp4_subnet_id_; /// Subnet DHCPv4 identifier + uint32_t dhcp6_subnet_id_; /// Subnet DHCPv6 identifier + uint32_t ipv4_address_; /// Reserved IPv4 address. + IPv6ResrvCollection ipv6_reservations_; /// IPv6 reservations collection + char hostname_[HOSTNAME_MAX_LEN]; /// Name reserved for the host + unsigned long hostname_length_; /// hostname length + char dhcp4_client_classes_[CLIENT_CLASSES_MAX_LEN]; + /// DHCPv4 client classes + unsigned long dhcp4_client_classes_length_; /// dhcp4_client_classes length + char dhcp6_client_classes_[CLIENT_CLASSES_MAX_LEN]; + /// DHCPv6 client classes + unsigned long dhcp6_client_classes_length_; /// dhcp6_client_classes length + HWAddrPtr hw_address_; /// Pointer to hardware address + DuidPtr duid_; /// Pointer to DUID + + // NULL flags for subnets id, ipv4 address, hostname and client classes + my_bool dhcp4_subnet_id_null_; + my_bool dhcp6_subnet_id_null_; + my_bool ipv4_address_null_; + my_bool hostname_null_; + my_bool dhcp4_client_classes_null_; + my_bool dhcp6_client_classes_null_; + + MYSQL_BIND bind_[HOST_COLUMNS]; + std::string columns_[HOST_COLUMNS]; /// Column names + my_bool error_[HOST_COLUMNS]; /// Error array + HostPtr host_; // Pointer to Host object +}; + +// MySqlHostDataSource Constructor and Destructor + +MySqlHostDataSource::MySqlHostDataSource( + const MySqlConnection::ParameterMap& parameters) : conn_(parameters) { + + // Open the database. + conn_.openDatabase(); + + // Enable autocommit. To avoid a flush to disk on every commit, the global + // parameter innodb_flush_log_at_trx_commit should be set to 2. This will + // cause the changes to be written to the log, but flushed to disk in the + // background every second. Setting the parameter to that value will speed + // up the system, but at the risk of losing data if the system crashes. + my_bool result = mysql_autocommit(conn_.mysql_, 1); + if (result != 0) { + isc_throw(DbOperationError, mysql_error(conn_.mysql_)); + } + + // Prepare all statements likely to be used. + conn_.prepareStatements(tagged_statements, + MySqlHostDataSource::NUM_STATEMENTS); + + // Create the exchange objects for use in exchanging data between the + // program and the database. + hostExchange_.reset(new MySqlHostReservationExchange()); +} + +MySqlHostDataSource::~MySqlHostDataSource() { + // Free up the prepared statements, ignoring errors. (What would we do + // about them? We're destroying this object and are not really concerned + // with errors on a database connection that is about to go away.) + for (int i = 0; i < conn_.statements_.size(); ++i) { + if (conn_.statements_[i] != NULL) { + (void) mysql_stmt_close(conn_.statements_[i]); + conn_.statements_[i] = NULL; + } + } + + // There is no need to close the database in this destructor: it is + // closed in the destructor of the mysql_ member variable. +} + +bool +MySqlHostDataSource::checkIfExists(const HostPtr& host){ + /// @todo: Implement this as a single query get(identifier_type, identifier) + return (get4(host->getIPv4SubnetID(), host->getHWAddress(), host->getDuid()) || + get6(host->getIPv6SubnetID(), host->getDuid(), host->getHWAddress())); +} + +void +MySqlHostDataSource::add(const HostPtr& host) { + // Check if the host is not a duplicate + if (checkIfExists(host)){ + isc_throw(DuplicateEntry, "Host with same parameters already exists."); + } else { + // Create the MYSQL_BIND array for the host + std::vector bind = hostExchange_->createBindForSend(host); + + // ... and call addHost() code. + addHost(INSERT_HOST, bind); + + /// @todo: Insert IPv6 reservations, if present (ticket #4212) + } +} + +void +MySqlHostDataSource::addHost(StatementIndex stindex, + std::vector& bind) { + + // Bind the parameters to the statement + int status = mysql_stmt_bind_param(conn_.statements_[stindex], + &bind[0]); + checkError(status, stindex, "unable to bind parameters"); + + // Execute the statement + status = mysql_stmt_execute(conn_.statements_[stindex]); + if (status != 0) { + // Failure: check for the special case of duplicate entry. + if (mysql_errno(conn_.mysql_) == ER_DUP_ENTRY) { + isc_throw(DuplicateEntry, "Database duplicate entry error"); + } + checkError(status, stindex, "unable to execute"); + } +} + +void +MySqlHostDataSource::getHostCollection(StatementIndex stindex, MYSQL_BIND* bind, + boost::shared_ptr exchange, + ConstHostCollection& result, bool single) const { + + // Bind the selection parameters to the statement + int status = mysql_stmt_bind_param(conn_.statements_[stindex], bind); + checkError(status, stindex, "unable to bind WHERE clause parameter"); + + // Set up the MYSQL_BIND array for the data being returned and bind it to + // the statement. + std::vector outbind = exchange->createBindForReceive(); + status = mysql_stmt_bind_result(conn_.statements_[stindex], &outbind[0]); + checkError(status, stindex, "unable to bind SELECT clause parameters"); + + // Execute the statement + status = mysql_stmt_execute(conn_.statements_[stindex]); + checkError(status, stindex, "unable to execute"); + + // Ensure that all the lease information is retrieved in one go to avoid + // overhead of going back and forth between client and server. + status = mysql_stmt_store_result(conn_.statements_[stindex]); + checkError(status, stindex, "unable to set up for storing all results"); + + // Set up the fetch "release" object to release resources associated + // with the call to mysql_stmt_fetch when this method exits, then + // retrieve the data. + MySqlFreeResult fetch_release(conn_.statements_[stindex]); + int count = 0; + while ((status = mysql_stmt_fetch(conn_.statements_[stindex])) == 0) { + try { + result.push_back(exchange->getHostData()); + + } catch (const isc::BadValue& ex) { + // Rethrow the exception with a bit more data. + isc_throw(BadValue, ex.what() << ". Statement is <" << + conn_.text_statements_[stindex] << ">"); + } + + if (single && (++count > 1)) { + isc_throw(MultipleRecords, "multiple records were found in the " + "database where only one was expected for query " + << conn_.text_statements_[stindex]); + } + } + + // How did the fetch end? + if (status == 1) { + // Error - unable to fetch results + checkError(status, stindex, "unable to fetch results"); + } else if (status == MYSQL_DATA_TRUNCATED) { + // Data truncated - throw an exception indicating what was at fault + isc_throw(DataTruncated, conn_.text_statements_[stindex] + << " returned truncated data: columns affected are " + << exchange->getErrorColumns()); + } +} + +ConstHostCollection +MySqlHostDataSource::getAll(const HWAddrPtr& hwaddr, const DuidPtr& duid) const { + + // Set up the WHERE clause value + MYSQL_BIND inbind[2]; + memset(inbind, 0, sizeof(inbind)); + + uint8_t dhcp_identifier_type = 0; + long unsigned int length = 0; + if (duid){ + // DUID + // set proper dhcp_identifier_type + dhcp_identifier_type = BaseHostDataSource::ID_DUID; // 1 + inbind[1].buffer = reinterpret_cast(&dhcp_identifier_type); + + const vector& duid_vector = duid->getDuid(); + length = duid_vector.size(); + inbind[0].buffer_type = MYSQL_TYPE_BLOB; + inbind[0].buffer = reinterpret_cast + (const_cast(&duid_vector[0])); + inbind[0].buffer_length = length; + inbind[0].length = &length; + } else if (hwaddr) { + // HW Address + dhcp_identifier_type = BaseHostDataSource::ID_HWADDR; // 0 + inbind[1].buffer = reinterpret_cast(&dhcp_identifier_type); + + const vector& hwaddr_vector = hwaddr->hwaddr_; + length = hwaddr_vector.size(); + inbind[0].buffer_type = MYSQL_TYPE_BLOB; + inbind[0].buffer = reinterpret_cast + (const_cast(&hwaddr_vector[0])); + inbind[0].buffer_length = length; + inbind[0].length = &length; + } + + // dhcp identifier type + inbind[1].buffer_type = MYSQL_TYPE_TINY; + inbind[1].buffer = reinterpret_cast(&dhcp_identifier_type); + inbind[1].is_unsigned = MLM_TRUE; + + ConstHostCollection result; + getHostCollection(GET_HOST_HWADDR_DUID, inbind, hostExchange_, result, false); + + return (result); +} + +ConstHostCollection +MySqlHostDataSource::getAll4(const asiolink::IOAddress& address) const { + + // Set up the WHERE clause value + MYSQL_BIND inbind[1]; + memset(inbind, 0, sizeof(inbind)); + + uint32_t addr4 = static_cast(address); + inbind[0].buffer_type = MYSQL_TYPE_LONG; + inbind[0].buffer = reinterpret_cast(&addr4); + inbind[0].is_unsigned = MLM_TRUE; + + ConstHostCollection result; + getHostCollection(GET_HOST_ADDR, inbind, hostExchange_, result, false); + + return (result); +} + +ConstHostPtr +MySqlHostDataSource::get4(const SubnetID& subnet_id, const HWAddrPtr& hwaddr, + const DuidPtr& duid) const { + + // Set up the WHERE clause value + MYSQL_BIND inbind[3]; + memset(inbind, 0, sizeof(inbind)); + + uint32_t subnet_buffer = static_cast(subnet_id); + inbind[0].buffer_type = MYSQL_TYPE_LONG; + inbind[0].buffer = reinterpret_cast(&subnet_buffer); + inbind[0].is_unsigned = MLM_TRUE; + + /// @todo: Rethink the logic in BaseHostDataSource::get4(subnet, hwaddr, duid) + if (hwaddr && duid) { + isc_throw(BadValue, "MySQL host data source get4() called with both" + " hwaddr and duid, only one of them is allowed"); + } + if (!hwaddr && !duid) { + isc_throw(BadValue, "MySQL host data source get4() called with " + "neither hwaddr or duid specified, one of them is required"); + } + + unsigned long length = 0; + uint8_t dhcp_identifier_type_ = 0; + + // Choosing one of the identifiers + if (hwaddr) { + // set identifier type + dhcp_identifier_type_ = BaseHostDataSource::ID_HWADDR; // 0 + + // set identifier value + const vector& hwaddr_vector = hwaddr->hwaddr_; + length = hwaddr_vector.size(); + inbind[2].buffer_type = MYSQL_TYPE_BLOB; + inbind[2].buffer = reinterpret_cast + (const_cast(&hwaddr_vector[0])); + inbind[2].buffer_length = length; + inbind[2].length = &length; + + } else if (duid) { + // set identifier type + dhcp_identifier_type_ = BaseHostDataSource::ID_DUID; // 1 + + // set identifier value + const vector& duid_vector = duid->getDuid(); + length = duid_vector.size(); + inbind[2].buffer_type = MYSQL_TYPE_BLOB; + inbind[2].buffer = reinterpret_cast + (const_cast(&duid_vector[0])); + inbind[2].buffer_length = length; + inbind[2].length = &length; + } + + // dhcp identifier type + inbind[1].buffer_type = MYSQL_TYPE_TINY; + inbind[1].buffer = reinterpret_cast(&dhcp_identifier_type_); + inbind[1].is_unsigned = MLM_TRUE; + + ConstHostCollection collection; + getHostCollection(GET_HOST_SUBID4_DHCPID, inbind, hostExchange_, + collection, true); + + // Return single record if present, else clear the host. + ConstHostPtr result; + if (!collection.empty()) + result = *collection.begin(); + + return (result); +} + +ConstHostPtr +MySqlHostDataSource::get4(const SubnetID& subnet_id, + const asiolink::IOAddress& address) const { + + // Set up the WHERE clause value + MYSQL_BIND inbind[2]; + uint32_t subnet = subnet_id; + memset(inbind, 0, sizeof(inbind)); + inbind[0].buffer_type = MYSQL_TYPE_LONG; + inbind[0].buffer = reinterpret_cast(&subnet); + inbind[0].is_unsigned = MLM_TRUE; + + uint32_t addr4 = static_cast(address); + inbind[1].buffer_type = MYSQL_TYPE_LONG; + inbind[1].buffer = reinterpret_cast(&addr4); + inbind[1].is_unsigned = MLM_TRUE; + + ConstHostCollection collection; + getHostCollection(GET_HOST_SUBID_ADDR, inbind, hostExchange_, + collection, true); + + // Return single record if present, else clear the host. + ConstHostPtr result; + if (!collection.empty()) + result = *collection.begin(); + + return (result); +} + +ConstHostPtr +MySqlHostDataSource::get6(const SubnetID& subnet_id, const DuidPtr& duid, + const HWAddrPtr& hwaddr) const { + + // Set up the WHERE clause value + MYSQL_BIND inbind[3]; + memset(inbind, 0, sizeof(inbind)); + + uint32_t subnet_buffer = static_cast(subnet_id); + inbind[0].buffer_type = MYSQL_TYPE_LONG; + inbind[0].buffer = reinterpret_cast(&subnet_buffer); + inbind[0].is_unsigned = MLM_TRUE; + + /// @todo: Rethink the logic in BaseHostDataSource::get4(subnet, hwaddr, duid) + if (hwaddr && duid) { + isc_throw(BadValue, "MySQL host data source get6() called with both" + " hwaddr and duid, only one of them is allowed"); + } + if (!hwaddr && !duid) { + isc_throw(BadValue, "MySQL host data source get6() called with " + "neither hwaddr or duid specified, one of them is required"); + } + + unsigned long length = 0; + uint8_t dhcp_identifier_type_ = 0; + + // Choosing one of the identifiers + if (hwaddr) { + // set identifier type + dhcp_identifier_type_ = BaseHostDataSource::ID_HWADDR; // 0 + + // set identifier value + const vector& hwaddr_vector = hwaddr->hwaddr_; + length = hwaddr_vector.size(); + inbind[2].buffer_type = MYSQL_TYPE_BLOB; + inbind[2].buffer = reinterpret_cast + (const_cast(&hwaddr_vector[0])); + inbind[2].buffer_length = length; + inbind[2].length = &length; + } else if (duid) { + // set identifier type + dhcp_identifier_type_ = BaseHostDataSource::ID_DUID; // 1 + + // set identifier value + const vector& duid_vector = duid->getDuid(); + length = duid_vector.size(); + inbind[2].buffer_type = MYSQL_TYPE_BLOB; + inbind[2].buffer = reinterpret_cast + (const_cast(&duid_vector[0])); + inbind[2].buffer_length = length; + inbind[2].length = &length; + } + + // dhcp identifier type + inbind[1].buffer_type = MYSQL_TYPE_TINY; + inbind[1].buffer = reinterpret_cast(&dhcp_identifier_type_); + inbind[1].is_unsigned = MLM_TRUE; + + ConstHostCollection collection; + getHostCollection(GET_HOST_SUBID6_DHCPID, inbind, hostExchange_, + collection, true); + + // Return single record if present, else clear the host. + ConstHostPtr result; + if (!collection.empty()) { + result = *collection.begin(); + } + + return (result); +} + +ConstHostPtr +MySqlHostDataSource::get6(const asiolink::IOAddress& prefix, + const uint8_t prefix_len) const { + + // Set up the WHERE clause value + MYSQL_BIND inbind[2]; + memset(inbind, 0, sizeof(inbind)); + + inbind[0].buffer_type = MYSQL_TYPE_LONG; + inbind[0].buffer = reinterpret_cast(&prefix.toBytes()[0]); + inbind[0].is_unsigned = MLM_TRUE; + + uint8_t tmp = prefix_len; + inbind[1].buffer_type = MYSQL_TYPE_LONG; + inbind[1].buffer = reinterpret_cast(&tmp); + inbind[1].is_unsigned = MLM_TRUE; + + ConstHostCollection collection; + getHostCollection(GET_HOST_PREFIX, inbind, hostExchange_, + collection, true); + + // Return single record if present, else clear the host. + ConstHostPtr result; + if (!collection.empty()) { + result = *collection.begin(); + } + + return (result); +} + +// Miscellaneous database methods. + +std::string MySqlHostDataSource::getName() const { + std::string name = ""; + try { + name = conn_.getParameter("name"); + } catch (...) { + // Return an empty name + } + return (name); +} + +std::string MySqlHostDataSource::getDescription() const { + return (std::string("Host data source that stores host information" + "in MySQL database")); +} + +std::pair MySqlHostDataSource::getVersion() const { + const StatementIndex stindex = GET_VERSION; + + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, + DHCPSRV_MYSQL_GET_VERSION); + + uint32_t major; // Major version number + uint32_t minor; // Minor version number + + // Execute the prepared statement + int status = mysql_stmt_execute(conn_.statements_[stindex]); + if (status != 0) { + isc_throw(DbOperationError, "unable to execute <" + << conn_.text_statements_[stindex] + << "> - reason: " << mysql_error(conn_.mysql_)); + } + + // Bind the output of the statement to the appropriate variables. + MYSQL_BIND bind[2]; + memset(bind, 0, sizeof(bind)); + + bind[0].buffer_type = MYSQL_TYPE_LONG; + bind[0].is_unsigned = 1; + bind[0].buffer = &major; + bind[0].buffer_length = sizeof(major); + + bind[1].buffer_type = MYSQL_TYPE_LONG; + bind[1].is_unsigned = 1; + bind[1].buffer = &minor; + bind[1].buffer_length = sizeof(minor); + + status = mysql_stmt_bind_result(conn_.statements_[stindex], bind); + if (status != 0) { + isc_throw(DbOperationError, "unable to bind result set: " + << mysql_error(conn_.mysql_)); + } + + // Fetch the data and set up the "release" object to release associated + // resources when this method exits then retrieve the data. + MySqlFreeResult fetch_release(conn_.statements_[stindex]); + status = mysql_stmt_fetch(conn_.statements_[stindex]); + if (status != 0) { + isc_throw(DbOperationError, "unable to obtain result set: " + << mysql_error(conn_.mysql_)); + } + + return (std::make_pair(major, minor)); +} + +void +MySqlHostDataSource::commit() { + conn_.commit(); +} + + +void +MySqlHostDataSource::rollback() { + conn_.rollback(); +} + + +}; // end of isc::dhcp namespace +}; // end of isc namespace diff --git a/src/lib/dhcpsrv/mysql_host_data_source.h b/src/lib/dhcpsrv/mysql_host_data_source.h new file mode 100644 index 0000000000..ba4344d119 --- /dev/null +++ b/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 +#include +#include + +#include +#include +#include + +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 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& 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 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 hostExchange_; + + /// @brief MySQL connection + MySqlConnection conn_; + +}; + +} +} + +#endif // MYSQL_HOST_DATA_SOURCE_H diff --git a/src/lib/dhcpsrv/mysql_lease_mgr.cc b/src/lib/dhcpsrv/mysql_lease_mgr.cc index 19170ff9ff..0855671cc0 100755 --- a/src/lib/dhcpsrv/mysql_lease_mgr.cc +++ b/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(&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(&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(cltt) + - static_cast(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(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(&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(secs), expire_time); + conn_.convertToDatabaseTime(time(NULL) - static_cast(secs), expire_time); inbind[1].buffer_type = MYSQL_TYPE_TIMESTAMP; inbind[1].buffer = reinterpret_cast(&expire_time); inbind[1].buffer_length = sizeof(expire_time); diff --git a/src/lib/dhcpsrv/mysql_lease_mgr.h b/src/lib/dhcpsrv/mysql_lease_mgr.h index 12567acbd9..f8eef77031 100755 --- a/src/lib/dhcpsrv/mysql_lease_mgr.h +++ b/src/lib/dhcpsrv/mysql_lease_mgr.h @@ -21,19 +21,13 @@ #include #include -#include +#include #include 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 diff --git a/src/lib/dhcpsrv/pgsql_lease_mgr.cc b/src/lib/dhcpsrv/pgsql_lease_mgr.cc index d67134e290..b83f140751 100644 --- a/src/lib/dhcpsrv/pgsql_lease_mgr.cc +++ b/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(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); } diff --git a/src/lib/dhcpsrv/tests/Makefile.am b/src/lib/dhcpsrv/tests/Makefile.am old mode 100644 new mode 100755 index a591e5466e..91677fcc01 --- a/src/lib/dhcpsrv/tests/Makefile.am +++ b/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 diff --git a/src/lib/dhcpsrv/tests/generic_host_data_source_unittest.cc b/src/lib/dhcpsrv/tests/generic_host_data_source_unittest.cc new file mode 100644 index 0000000000..40bb94ace0 --- /dev/null +++ b/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 +#include +#include +#include +#include +#include + +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(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(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 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::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::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 diff --git a/src/lib/dhcpsrv/tests/generic_host_data_source_unittest.h b/src/lib/dhcpsrv/tests/generic_host_data_source_unittest.h new file mode 100644 index 0000000000..d73d8ec6c5 --- /dev/null +++ b/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 +#include +#include +#include + +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 diff --git a/src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc b/src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc index ddedd2d1d2..36705744ca 100644 --- a/src/lib/dhcpsrv/tests/generic_lease_mgr_unittest.cc +++ b/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); diff --git a/src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc b/src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc new file mode 100644 index 0000000000..5a44365c60 --- /dev/null +++ b/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 + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::dhcp; +using namespace isc::dhcp::test; +using namespace std; + +namespace { + +// This holds statements to create and destroy the schema. +#include "schema_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 diff --git a/src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc b/src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc index 75d4238b82..ec5d558099 100755 --- a/src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc +++ b/src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -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); } diff --git a/src/lib/dhcpsrv/tests/schema_mysql_copy.h b/src/lib/dhcpsrv/tests/schema_mysql_copy.h index f21ccee7ff..a62f248948 100755 --- a/src/lib/dhcpsrv/tests/schema_mysql_copy.h +++ b/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 };