diff options
author | Marcin Siodelski <marcin@isc.org> | 2018-09-25 10:50:13 +0200 |
---|---|---|
committer | Marcin Siodelski <marcin@isc.org> | 2018-10-08 16:39:22 +0200 |
commit | 3ef5938626712225e3302b8e663211462953b7f1 (patch) | |
tree | e13ae39fe277838997dd9e54ae32a5190b7fd9c3 | |
parent | [#93,!35] Extended MySqlConnection with generic query functions. (diff) | |
download | kea-3ef5938626712225e3302b8e663211462953b7f1.tar.xz kea-3ef5938626712225e3302b8e663211462953b7f1.zip |
[#93,!35] Implemented subnet fetching in MySQL Config Backend.
-rw-r--r-- | src/hooks/dhcp/mysql_cb/Makefile.am | 1 | ||||
-rw-r--r-- | src/hooks/dhcp/mysql_cb/mysql_cb_dhcp4.cc | 508 | ||||
-rw-r--r-- | src/hooks/dhcp/mysql_cb/tests/Makefile.am | 1 | ||||
-rw-r--r-- | src/hooks/dhcp/mysql_cb/tests/mysql_cb_dhcp4_unittest.cc | 172 | ||||
-rw-r--r-- | src/lib/cc/Makefile.am | 2 | ||||
-rw-r--r-- | src/lib/cc/stamped_element.cc | 22 | ||||
-rw-r--r-- | src/lib/cc/stamped_element.h | 57 | ||||
-rw-r--r-- | src/lib/cc/tests/Makefile.am | 1 | ||||
-rw-r--r-- | src/lib/cc/tests/stamped_element_unittest.cc | 60 | ||||
-rw-r--r-- | src/lib/dhcpsrv/network.h | 5 | ||||
-rw-r--r-- | src/lib/dhcpsrv/subnet.cc | 41 | ||||
-rw-r--r-- | src/lib/dhcpsrv/subnet.h | 27 | ||||
-rw-r--r-- | src/lib/dhcpsrv/tests/subnet_unittest.cc | 60 | ||||
-rw-r--r-- | src/lib/mysql/mysql_binding.cc | 40 | ||||
-rw-r--r-- | src/lib/mysql/mysql_binding.h | 91 | ||||
-rw-r--r-- | src/lib/mysql/mysql_connection.h | 10 | ||||
-rw-r--r-- | src/lib/mysql/tests/Makefile.am | 3 | ||||
-rw-r--r-- | src/lib/mysql/tests/mysql_binding_unittest.cc | 97 | ||||
-rw-r--r-- | src/lib/mysql/tests/mysql_connection_unittest.cc | 26 |
19 files changed, 1178 insertions, 46 deletions
diff --git a/src/hooks/dhcp/mysql_cb/Makefile.am b/src/hooks/dhcp/mysql_cb/Makefile.am index ae7aef442f..28da17d4f1 100644 --- a/src/hooks/dhcp/mysql_cb/Makefile.am +++ b/src/hooks/dhcp/mysql_cb/Makefile.am @@ -43,6 +43,7 @@ libdhcp_mysql_cb_la_LDFLAGS = $(AM_LDFLAGS) $(MYSQL_LIBS)o libdhcp_mysql_cb_la_LDFLAGS += -avoid-version -export-dynamic -module libdhcp_mysql_cb_la_LIBADD = libmysqlcb.la +libdhcp_mysql_cb_la_LIBADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la libdhcp_mysql_cb_la_LIBADD += $(top_builddir)/src/lib/eval/libkea-eval.la libdhcp_mysql_cb_la_LIBADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la libdhcp_mysql_cb_la_LIBADD += $(top_builddir)/src/lib/stats/libkea-stats.la diff --git a/src/hooks/dhcp/mysql_cb/mysql_cb_dhcp4.cc b/src/hooks/dhcp/mysql_cb/mysql_cb_dhcp4.cc index da0acce5cf..86a2053745 100644 --- a/src/hooks/dhcp/mysql_cb/mysql_cb_dhcp4.cc +++ b/src/hooks/dhcp/mysql_cb/mysql_cb_dhcp4.cc @@ -4,20 +4,24 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +#include <cc/data.h> #include <database/db_exceptions.h> +#include <dhcp/classify.h> +#include <dhcp/dhcp6.h> #include <mysql_cb_dhcp4.h> #include <mysql/mysql_connection.h> +#include <boost/date_time/posix_time/posix_time.hpp> +#include <boost/lexical_cast.hpp> #include <mysql.h> #include <mysqld_error.h> #include <array> +#include <sstream> #include <utility> #include <vector> using namespace isc::db; - -namespace { - -} +using namespace isc::data; +using namespace isc::asiolink; namespace isc { namespace dhcp { @@ -34,6 +38,11 @@ public: /// database. enum StatementIndex { GET_SUBNET4_ID, + GET_SUBNET4_PREFIX, + GET_ALL_SUBNETS4, + GET_MODIFIED_SUBNETS4, + INSERT_SUBNET4, + UPDATE_SUBNET4, NUM_STATEMENTS }; @@ -46,6 +55,292 @@ public: /// @brief Destructor. ~MySqlConfigBackendDHCPv4Impl(); + /// @brief Sends query to the database to retrieve multiple subnets. + /// + /// Query should order subnets by subnet_id. + /// + /// @param index Index of the query to be used. + /// @param in_bindings Input bindings specifying selection criteria. The + /// size of the bindings collection must match the number of placeholders + /// in the prepared statement. The input bindings collection must be empty + /// if the query contains no WHERE clause. + /// @param [out] subnets Reference to the container where fetched subnets + /// will be inserted. + void getSubnets4(const StatementIndex& index, + const MySqlBindingCollection& in_bindings, + Subnet4Collection& subnets) { + // Create output bindings. The order must match that in the prepared + // statement. + MySqlBindingCollection out_bindings = { + MySqlBinding::createInteger<uint32_t>(), // subnet_id + MySqlBinding::createString(32), // subnet_prefix + MySqlBinding::createString(128), // 4o6_interface + MySqlBinding::createString(128), // 4o6_interface_id + MySqlBinding::createString(64), // 4o6_subnet + MySqlBinding::createString(512), // boot_file_name + MySqlBinding::createString(128), // client_class + MySqlBinding::createString(128), // interface + MySqlBinding::createInteger<uint8_t>(), // match_client_id + MySqlBinding::createTimestamp(), // modification_ts + MySqlBinding::createInteger<uint32_t>(), // next_server + MySqlBinding::createInteger<uint32_t>(), // rebind_timer + MySqlBinding::createString(65536), // relay + MySqlBinding::createInteger<uint32_t>(), // renew_timer + MySqlBinding::createString(65536), // require_client_classes + MySqlBinding::createInteger<uint8_t>(), // reservation_mode + MySqlBinding::createString(512), // server_hostname + MySqlBinding::createString(128), // shared_network_name + MySqlBinding::createString(65536), // user_context + MySqlBinding::createInteger<uint32_t>() // valid_lifetime + }; + + // Execute actual query. + conn_.selectQuery(index, in_bindings, out_bindings, + [&subnets](MySqlBindingCollection& out_bindings) { + // Get pointer to the last subnet in the collection. + Subnet4Ptr last_subnet; + if (!subnets.empty()) { + last_subnet = *subnets.rbegin(); + } + + // Subnet has been returned. Assuming that subnets are ordered by + // subnet identifier, if the subnet identifier of the current row + // is different than the subnet identifier of the previously returned + // row, it means that we have to construct new subnet object. + if (!last_subnet || (last_subnet->getID() != out_bindings[0]->getInteger<uint32_t>())) { + // subnet_id + SubnetID subnet_id(out_bindings[0]->getInteger<uint32_t>()); + // subnet_prefix + std::string subnet_prefix = out_bindings[1]->getString(); + auto prefix_pair = Subnet4::parsePrefix(subnet_prefix); + // renew_timer + uint32_t renew_timer = out_bindings[13]->getIntegerOrDefault<uint32_t>(0); + // rebind_timer + uint32_t rebind_timer = out_bindings[11]->getIntegerOrDefault<uint32_t>(0); + // valid_lifetime + uint32_t valid_lifetime = out_bindings[19]->getIntegerOrDefault<uint32_t>(0); + + // Create subnet with basic settings. + last_subnet.reset(new Subnet4(prefix_pair.first, prefix_pair.second, + renew_timer, rebind_timer, + valid_lifetime, subnet_id)); + + // 4o6_interface + if (!out_bindings[2]->amNull()) { + last_subnet->get4o6().setIface4o6(out_bindings[2]->getString()); + } + // 4o6_interface_id + if (!out_bindings[3]->amNull()) { + std::string dhcp4o6_interface_id = out_bindings[3]->getString(); + OptionBuffer dhcp4o6_interface_id_buf(dhcp4o6_interface_id.begin(), + dhcp4o6_interface_id.end()); + OptionPtr option_dhcp4o6_interface_id(new Option(Option::V6, D6O_INTERFACE_ID, + dhcp4o6_interface_id_buf)); + last_subnet->get4o6().setInterfaceId(option_dhcp4o6_interface_id); + } + // 4o6_subnet + if (!out_bindings[4]->amNull()) { + std::pair<IOAddress, uint8_t> dhcp4o6_subnet_prefix_pair = + Subnet6::parsePrefix(out_bindings[4]->getString()); + last_subnet->get4o6().setSubnet4o6(dhcp4o6_subnet_prefix_pair.first, + dhcp4o6_subnet_prefix_pair.second); + } + // boot_file_name + last_subnet->setFilename(out_bindings[5]->getStringOrDefault("")); + // client_class + if (!out_bindings[6]->amNull()) { + last_subnet->allowClientClass(out_bindings[6]->getString()); + } + // interface + last_subnet->setIface(out_bindings[7]->getStringOrDefault("")); + // match_client_id + last_subnet->setMatchClientId(static_cast<bool> + (out_bindings[8]->getIntegerOrDefault<uint8_t>(1))); + // next_server + last_subnet->setSiaddr(IOAddress(out_bindings[10]->getIntegerOrDefault<uint32_t>(0))); + // relay + ElementPtr relay_element = out_bindings[12]->getJSON(); + if (relay_element) { + if (relay_element->getType() != Element::list) { + isc_throw(BadValue, "invalid relay value " + << out_bindings[12]->getString()); + } + for (auto i = 0; i < relay_element->size(); ++i) { + auto relay_address_element = relay_element->get(i); + if (relay_address_element->getType() != Element::string) { + isc_throw(BadValue, "relay address must be a string"); + } + last_subnet->addRelayAddress(IOAddress(relay_element->get(i)->stringValue())); + } + } + // require_client_classes + ElementPtr require_element = out_bindings[14]->getJSON(); + if (require_element) { + if (require_element->getType() != Element::list) { + isc_throw(BadValue, "invalid require_client_classes value " + << out_bindings[14]->getString()); + } + for (auto i = 0; i < require_element->size(); ++i) { + auto require_item = require_element->get(i); + if (require_item->getType() != Element::string) { + isc_throw(BadValue, "elements of require_client_classes list must" + "be valid strings"); + } + last_subnet->requireClientClass(require_item->stringValue()); + } + } + // reservation_mode + last_subnet->setHostReservationMode(static_cast<Subnet4::HRMode> + (out_bindings[15]->getIntegerOrDefault<uint8_t>(Subnet4::HR_ALL))); + // server_hostname + last_subnet->setSname(out_bindings[16]->getStringOrDefault("")); + // user_context + ElementPtr user_context = out_bindings[18]->getJSON(); + if (user_context) { + last_subnet->setContext(user_context); + } + + // Subnet ready. Add it to the list. + subnets.push_back(last_subnet); + } + }); + } + + /// @brief Sends query to retrieve single subnet by id. + /// + /// @param selector Server selector. + /// @param subnet_id Subnet identifier. + /// + /// @return Pointer to the returned subnet or NULL if such subnet + /// doesn't exist. + Subnet4Ptr getSubnet4(const ServerSelector& selector, + const SubnetID& subnet_id) { + MySqlBindingCollection in_bindings; + in_bindings.push_back(MySqlBinding::createInteger<uint32_t>(subnet_id)); + + Subnet4Collection subnets; + getSubnets4(GET_SUBNET4_ID, in_bindings, subnets); + + return (subnets.empty() ? Subnet4Ptr() : *subnets.begin()); + } + + /// @brief Sends query to retrieve single subnet by prefix. + /// + /// The prefix should be in the following format: "192.0.2.0/24". + /// + /// @param selector Server selector. + /// @param subnet_id Subnet identifier. + /// + /// @return Pointer to the returned subnet or NULL if such subnet + /// doesn't exist. + Subnet4Ptr getSubnet4(const ServerSelector& selector, + const std::string& subnet_prefix) { + MySqlBindingCollection in_bindings; + in_bindings.push_back(MySqlBinding::createString(subnet_prefix)); + + Subnet4Collection subnets; + getSubnets4(GET_SUBNET4_PREFIX, in_bindings, subnets); + + return (subnets.empty() ? Subnet4Ptr() : *subnets.begin()); + } + + /// @brief Sends query to insert or update subnet. + /// + /// @param selector Server selector. + /// @param subnet Pointer to the subnet to be inserted or updated. + void createUpdateSubnet4(const ServerSelector& selector, + const Subnet4Ptr& subnet) { + // Convert DHCPv4o6 interface id to text. + OptionPtr dhcp4o6_interface_id = subnet->get4o6().getInterfaceId(); + std::string dhcp4o6_interface_id_text; + if (dhcp4o6_interface_id) { + dhcp4o6_interface_id_text.assign(dhcp4o6_interface_id->getData().begin(), + dhcp4o6_interface_id->getData().end()); + } + + // Convert DHCPv4o6 subnet to text. + std::string dhcp4o6_subnet; + if (!subnet->get4o6().getSubnet4o6().first.isV6Zero() || + (subnet->get4o6().getSubnet4o6().second != 128u)) { + std::ostringstream s; + s << subnet->get4o6().getSubnet4o6().first << "/" + << static_cast<int>(subnet->get4o6().getSubnet4o6().second); + dhcp4o6_subnet = s.str(); + } + + // Create JSON list of relay addresses. + ElementPtr relay_element = Element::createList(); + const auto& addresses = subnet->getRelayAddresses(); + if (!addresses.empty()) { + for (const auto& address : addresses) { + relay_element->add(Element::create(address.toText())); + } + } + + // Create JSON list of required classes. + ElementPtr required_classes_element = Element::createList(); + const auto& required_classes = subnet->getRequiredClasses(); + for (auto required_class = required_classes.cbegin(); + required_class != required_classes.cend(); + ++required_class) { + required_classes_element->add(Element::create(*required_class)); + } + + // Create binding with shared network name if the subnet belongs to a + // shared network. + SharedNetwork4Ptr shared_network; + subnet->getSharedNetwork(shared_network); + MySqlBindingPtr shared_network_binding = + (shared_network ? MySqlBinding::createString(shared_network->getName()) : + MySqlBinding::createNull()); + + // Create user context binding if user context exists. + auto context_element = subnet->getContext(); + MySqlBindingPtr context_binding = + (context_element ? MySqlBinding::createString(context_element->str()) : + MySqlBinding::createNull()); + + // Create input bindings. + MySqlBindingCollection in_bindings = { + MySqlBinding::createInteger<uint32_t>(subnet->getID()), + MySqlBinding::createString(subnet->toText()), + MySqlBinding::condCreateString(subnet->get4o6().getIface4o6()), + MySqlBinding::condCreateString(dhcp4o6_interface_id_text), + MySqlBinding::condCreateString(dhcp4o6_subnet), + MySqlBinding::condCreateString(subnet->getFilename()), + MySqlBinding::condCreateString(subnet->getClientClass()), + MySqlBinding::condCreateString(subnet->getIface()), + MySqlBinding::createInteger<uint8_t>(static_cast<uint8_t>(subnet->getMatchClientId())), + MySqlBinding::createTimestamp(subnet->getModificationTime()), + MySqlBinding::condCreateInteger<uint32_t>(subnet->getSiaddr().toUint32()), + MySqlBinding::createInteger<uint32_t>(subnet->getT2()), + MySqlBinding::condCreateString(relay_element->str()), + MySqlBinding::createInteger<uint32_t>(subnet->getT1()), + MySqlBinding::condCreateString(required_classes_element->str()), + MySqlBinding::createInteger<uint8_t>(static_cast<uint8_t>(subnet->getHostReservationMode())), + MySqlBinding::condCreateString(subnet->getSname()), + shared_network_binding, + context_binding, + MySqlBinding::createInteger<uint32_t>(subnet->getValid()) + }; + + // Check if the subnet already exists. + Subnet4Ptr existing_subnet = getSubnet4(selector, subnet->getID()); + + // If the subnet exists we are going to update this subnet. + if (existing_subnet) { + // Need to add one more binding for WHERE clause. + in_bindings.push_back(MySqlBinding::createInteger<uint32_t>(existing_subnet->getID())); + conn_.updateDeleteQuery(MySqlConfigBackendDHCPv4Impl::UPDATE_SUBNET4, + in_bindings); + + } else { + // If the subnet doesn't exist, let's insert it. + conn_.insertQuery(MySqlConfigBackendDHCPv4Impl::INSERT_SUBNET4, + in_bindings); + } + } + /// @brief Represents connection to the MySQL database. MySqlConnection conn_; }; @@ -57,8 +352,161 @@ TaggedStatementArray; /// @brief Prepared MySQL statements used by the backend to insert and /// retrieve data from the database. TaggedStatementArray tagged_statements = { { + // Select subnet by id. { MySqlConfigBackendDHCPv4Impl::GET_SUBNET4_ID, - "SELECT hostname FROM hosts WHERE dhcp4_subnet_id = ?" } + "SELECT " + " subnet_id," + " subnet_prefix," + " 4o6_interface," + " 4o6_interface_id," + " 4o6_subnet," + " boot_file_name," + " client_class," + " interface," + " match_client_id," + " modification_ts," + " next_server," + " rebind_timer," + " relay," + " renew_timer," + " require_client_classes," + " reservation_mode," + " server_hostname," + " shared_network_name," + " user_context," + " valid_lifetime " + "FROM dhcp4_subnet WHERE subnet_id = ? " + "ORDER BY subnet_id" }, + + // Select subnet by prefix. + { MySqlConfigBackendDHCPv4Impl::GET_SUBNET4_PREFIX, + "SELECT " + " subnet_id," + " subnet_prefix," + " 4o6_interface," + " 4o6_interface_id," + " 4o6_subnet," + " boot_file_name," + " client_class," + " interface," + " match_client_id," + " modification_ts," + " next_server," + " rebind_timer," + " relay," + " renew_timer," + " require_client_classes," + " reservation_mode," + " server_hostname," + " shared_network_name," + " user_context," + " valid_lifetime " + "FROM dhcp4_subnet WHERE subnet_prefix = ? " + "ORDER BY subnet_id" }, + + // Select all subnets. + { MySqlConfigBackendDHCPv4Impl::GET_ALL_SUBNETS4, + "SELECT " + " subnet_id," + " subnet_prefix," + " 4o6_interface," + " 4o6_interface_id," + " 4o6_subnet," + " boot_file_name," + " client_class," + " interface," + " match_client_id," + " modification_ts," + " next_server," + " rebind_timer," + " relay," + " renew_timer," + " require_client_classes," + " reservation_mode," + " server_hostname," + " shared_network_name," + " user_context," + " valid_lifetime " + "FROM dhcp4_subnet " + "ORDER BY subnet_id" }, + + // Select subnets having modification time later than X. + { MySqlConfigBackendDHCPv4Impl::GET_MODIFIED_SUBNETS4, + "SELECT " + " subnet_id," + " subnet_prefix," + " 4o6_interface," + " 4o6_interface_id," + " 4o6_subnet," + " boot_file_name," + " client_class," + " interface," + " match_client_id," + " modification_ts," + " next_server," + " rebind_timer," + " relay," + " renew_timer," + " require_client_classes," + " reservation_mode," + " server_hostname," + " shared_network_name," + " user_context," + " valid_lifetime " + "FROM dhcp4_subnet " + "WHERE modification_ts > ? " + "ORDER BY subnet_id" }, + + // Insert a subnet. + { MySqlConfigBackendDHCPv4Impl::INSERT_SUBNET4, + "INSERT INTO dhcp4_subnet(" + " subnet_id," + " subnet_prefix," + " 4o6_interface," + " 4o6_interface_id," + " 4o6_subnet," + " boot_file_name," + " client_class," + " interface," + " match_client_id," + " modification_ts," + " next_server," + " rebind_timer," + " relay," + " renew_timer," + " require_client_classes," + " reservation_mode," + " server_hostname," + " shared_network_name," + " user_context," + " valid_lifetime" + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," + "?, ?, ?, ?, ?, ?, ?, ?)" }, + + // Update existing subnet. + { MySqlConfigBackendDHCPv4Impl::UPDATE_SUBNET4, + "UPDATE dhcp4_subnet SET " + " subnet_id = ?," + " subnet_prefix = ?," + " 4o6_interface = ?," + " 4o6_interface_id = ?," + " 4o6_subnet = ?," + " boot_file_name = ?," + " client_class = ?," + " interface = ?," + " match_client_id = ?," + " modification_ts = ?," + " next_server = ?," + " rebind_timer = ?," + " relay = ?," + " renew_timer = ?," + " require_client_classes = ?," + " reservation_mode = ?," + " server_hostname = ?," + " shared_network_name = ?," + " user_context = ?," + " valid_lifetime = ? " + "WHERE subnet_id = ?" } } }; @@ -119,80 +567,94 @@ MySqlConfigBackendDHCPv4(const DatabaseConnection::ParameterMap& parameters) Subnet4Ptr MySqlConfigBackendDHCPv4::getSubnet4(const ServerSelector& selector, const std::string& subnet_prefix) const { + return (impl_->getSubnet4(selector, subnet_prefix)); } Subnet4Ptr MySqlConfigBackendDHCPv4::getSubnet4(const ServerSelector& selector, const SubnetID& subnet_id) const { - BindingCollection in_bindings; - in_bindings.push_back(Binding::createString("1024")); - - BindingCollection out_bindings; - out_bindings.push_back(Binding::createString()); - - impl_->conn_.selectQuery(MySqlConfigBackendDHCPv4Impl::GET_SUBNET4_ID, - in_bindings, out_bindings, - [&out_bindings]() { - uint32_t hostname = out_bindings[0]->getValue<uint32_t>(); - }); - - return (Subnet4Ptr()); + return (impl_->getSubnet4(selector, subnet_id)); } Subnet4Collection MySqlConfigBackendDHCPv4::getAllSubnets4(const ServerSelector& selector) const { + Subnet4Collection subnets; + MySqlBindingCollection in_bindings; + impl_->getSubnets4(MySqlConfigBackendDHCPv4Impl::GET_ALL_SUBNETS4, + in_bindings, subnets); + return (subnets); } Subnet4Collection MySqlConfigBackendDHCPv4::getModifiedSubnets4(const ServerSelector& selector, const boost::posix_time::ptime& modification_time) const { + Subnet4Collection subnets; + MySqlBindingCollection in_bindings = { + MySqlBinding::createTimestamp(modification_time) + }; + impl_->getSubnets4(MySqlConfigBackendDHCPv4Impl::GET_MODIFIED_SUBNETS4, + in_bindings, subnets); + return (subnets); } SharedNetwork4Ptr MySqlConfigBackendDHCPv4::getSharedNetwork4(const ServerSelector& selector, const std::string& name) const { + isc_throw(NotImplemented, "not implemented"); } SharedNetwork4Collection MySqlConfigBackendDHCPv4::getAllSharedNetworks4(const ServerSelector& selector) const { + isc_throw(NotImplemented, "not implemented"); } SharedNetwork4Collection -MySqlConfigBackendDHCPv4::getModifiedSharedNetworks4(const ServerSelector& selector, - const boost::posix_time::ptime& modification_time) const { +MySqlConfigBackendDHCPv4:: +getModifiedSharedNetworks4(const ServerSelector& selector, + const boost::posix_time::ptime& modification_time) const { + isc_throw(NotImplemented, "not implemented"); } OptionDefinitionPtr MySqlConfigBackendDHCPv4::getOptionDef4(const ServerSelector& selector, const uint16_t code, const std::string& space) const { + isc_throw(NotImplemented, "not implemented"); } OptionDefContainer MySqlConfigBackendDHCPv4::getAllOptionDefs4(const ServerSelector& selector) const { + isc_throw(NotImplemented, "not implemented"); } OptionDefContainer -MySqlConfigBackendDHCPv4::getModifiedOptionDefs4(const ServerSelector& selector, - const boost::posix_time::ptime& modification_time) const { +MySqlConfigBackendDHCPv4:: +getModifiedOptionDefs4(const ServerSelector& selector, + const boost::posix_time::ptime& modification_time) const { + isc_throw(NotImplemented, "not implemented"); } util::OptionalValue<std::string> MySqlConfigBackendDHCPv4::getGlobalStringParameter4(const ServerSelector& selector, const std::string& name) const { + isc_throw(NotImplemented, "not implemented"); } + util::OptionalValue<int64_t> MySqlConfigBackendDHCPv4::getGlobalNumberParameter4(const ServerSelector& selector, const std::string& name) const { + isc_throw(NotImplemented, "not implemented"); } std::map<std::string, std::string> MySqlConfigBackendDHCPv4::getAllGlobalParameters4(const ServerSelector& selector) const { + isc_throw(NotImplemented, "not implemented"); } void MySqlConfigBackendDHCPv4::createUpdateSubnet4(const ServerSelector& selector, const Subnet4Ptr& subnet) { + impl_->createUpdateSubnet4(selector, subnet); } void @@ -305,10 +767,12 @@ MySqlConfigBackendDHCPv4::getType() const { std::string MySqlConfigBackendDHCPv4::getHost() const { + return (""); } uint16_t MySqlConfigBackendDHCPv4::getPort() const { + return (0); } } // end of namespace isc::dhcp diff --git a/src/hooks/dhcp/mysql_cb/tests/Makefile.am b/src/hooks/dhcp/mysql_cb/tests/Makefile.am index 5546b734dd..c8663e2d00 100644 --- a/src/hooks/dhcp/mysql_cb/tests/Makefile.am +++ b/src/hooks/dhcp/mysql_cb/tests/Makefile.am @@ -33,6 +33,7 @@ mysql_cb_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS) mysql_cb_unittests_CXXFLAGS = $(AM_CXXFLAGS) mysql_cb_unittests_LDADD = $(top_builddir)/src/hooks/dhcp/mysql_cb/libmysqlcb.la +mysql_cb_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la mysql_cb_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la mysql_cb_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la mysql_cb_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la diff --git a/src/hooks/dhcp/mysql_cb/tests/mysql_cb_dhcp4_unittest.cc b/src/hooks/dhcp/mysql_cb/tests/mysql_cb_dhcp4_unittest.cc index feb191bfab..4d7a532e92 100644 --- a/src/hooks/dhcp/mysql_cb/tests/mysql_cb_dhcp4_unittest.cc +++ b/src/hooks/dhcp/mysql_cb/tests/mysql_cb_dhcp4_unittest.cc @@ -5,24 +5,34 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. #include <config.h> +#include <dhcp/dhcp6.h> #include <mysql_cb_dhcp4.h> #include <mysql/testutils/mysql_schema.h> +#include <boost/shared_ptr.hpp> #include <gtest/gtest.h> +#include <map> +using namespace isc::asiolink; using namespace isc::db; using namespace isc::db::test; +using namespace isc::data; using namespace isc::dhcp; namespace { +/// @brief Test fixture class for @c MySqlConfigBackendDHCPv4. class MySqlConfigBackendDHCPv4Test : public ::testing::Test { public: - MySqlConfigBackendDHCPv4Test() { + /// @brief Constructor. + MySqlConfigBackendDHCPv4Test() + : test_subnets_(), timestamps_() { + // Recreate database schema. destroyMySQLSchema(); createMySQLSchema(); try { + // Create MySQL connection and use it to start the backend. DatabaseConnection::ParameterMap params = DatabaseConnection::parse(validMySQLConnectionString()); cbptr_.reset(new MySqlConfigBackendDHCPv4(params)); @@ -35,19 +45,175 @@ public: "*** accompanying exception output.\n"; throw; } + + // Create test data. + initTestSubnets(); + initTimestamps(); } + /// @brief Destructor. virtual ~MySqlConfigBackendDHCPv4Test() { cbptr_.reset(); destroyMySQLSchema(); } - MySqlConfigBackendDHCPv4Ptr cbptr_; + /// @brief Creates several subnets used in tests. + void initTestSubnets() { + // First subnet includes all parameters. + std::string interface_id_text = "whale"; + OptionBuffer interface_id(interface_id_text.begin(), interface_id_text.end()); + ElementPtr user_context = Element::createMap(); + user_context->set("foo", Element::create("bar")); + + Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 24, 30, 40, 60, 1024)); + subnet->get4o6().setIface4o6("eth0"); + subnet->get4o6().setInterfaceId(OptionPtr(new Option(Option::V6, D6O_INTERFACE_ID, + interface_id))); + subnet->get4o6().setSubnet4o6(IOAddress("2001:db8:1::"), 64); + subnet->setFilename("/tmp/filename"); + subnet->allowClientClass("home"); + subnet->setIface("eth1"); + subnet->setMatchClientId(false); + subnet->setSiaddr(IOAddress("10.1.2.3")); + subnet->setT2(323212); + subnet->addRelayAddress(IOAddress("10.2.3.4")); + subnet->addRelayAddress(IOAddress("10.5.6.7")); + subnet->setT1(1234); + subnet->requireClientClass("required-class1"); + subnet->requireClientClass("required-class2"); + subnet->setHostReservationMode(Subnet4::HR_DISABLED); + subnet->setSname("server-hostname"); + subnet->setContext(user_context); + // shared-network? + subnet->setValid(555555); + + test_subnets_.push_back(subnet); + + // Other subnets include mostly null values except for mandatory parameters. + subnet.reset(new Subnet4(IOAddress("10.0.0.0"), 8, 20, 30, 40, 1024)); + test_subnets_.push_back(subnet); + + subnet.reset(new Subnet4(IOAddress("192.0.3.0"), 24, 20, 30, 40, 2048)); + test_subnets_.push_back(subnet); + + subnet.reset(new Subnet4(IOAddress("192.0.4.0"), 24, 30, 40, 60, 4096)); + test_subnets_.push_back(subnet); + } + + /// @brief Initialize posix time values used in tests. + void initTimestamps() { + // Current time minus 1 hour to make sure it is in the past. + timestamps_["today"] = boost::posix_time::second_clock::universal_time() + - boost::posix_time::hours(1); + // Yesterday. + timestamps_["yesterday"] = timestamps_["today"] - boost::posix_time::hours(24); + // Tomorrow. + timestamps_["tomorrow"] = timestamps_["today"] + boost::posix_time::hours(24); + } + + /// @brief Holds pointers to subnets used in tests. + std::vector<Subnet4Ptr> test_subnets_; + + /// @brief Holds timestamp values used in tests. + std::map<std::string, boost::posix_time::ptime> timestamps_; + + /// @brief Holds pointer to the backend. + boost::shared_ptr<ConfigBackendDHCPv4> cbptr_; }; +// Test that subnet can be inserted, fetched, updated and then fetched again. TEST_F(MySqlConfigBackendDHCPv4Test, getSubnet4) { - cbptr_->getSubnet4(ServerSelector::UNASSIGNED(), SubnetID(1)); + // Insert new subnet. + Subnet4Ptr subnet = test_subnets_[0]; + cbptr_->createUpdateSubnet4(ServerSelector::UNASSIGNED(), subnet); + + // Fetch this subnet by subnet identifier. + Subnet4Ptr returned_subnet = cbptr_->getSubnet4(ServerSelector::UNASSIGNED(), + test_subnets_[0]->getID()); + ASSERT_TRUE(returned_subnet); + + // The easiest way to verify whether the returned subnet matches the inserted + // subnet is to convert both to text. + EXPECT_EQ(subnet->toElement()->str(), returned_subnet->toElement()->str()); + + // Update the subnet in the database (both use the same ID). + Subnet4Ptr subnet2 = test_subnets_[1]; + cbptr_->createUpdateSubnet4(ServerSelector::UNASSIGNED(), subnet2); + + // Fetch updated subnet and see if it matches. + returned_subnet = cbptr_->getSubnet4(ServerSelector::UNASSIGNED(), + SubnetID(1024)); + EXPECT_EQ(subnet2->toElement()->str(), returned_subnet->toElement()->str()); } +// Test that subnet can be fetched by prefix. +TEST_F(MySqlConfigBackendDHCPv4Test, getSubnet4ByPrefix) { + // Insert subnet to the database. + Subnet4Ptr subnet = test_subnets_[0]; + cbptr_->createUpdateSubnet4(ServerSelector::UNASSIGNED(), subnet); + + // Fetch the subnet by prefix. + Subnet4Ptr returned_subnet = cbptr_->getSubnet4(ServerSelector::UNASSIGNED(), + "192.0.2.0/24"); + ASSERT_TRUE(returned_subnet); + + // Verify subnet contents. + EXPECT_EQ(subnet->toElement()->str(), returned_subnet->toElement()->str()); +} + +// Test that all subnets can be fetched. +TEST_F(MySqlConfigBackendDHCPv4Test, getAllSubnets4) { + // Insert test subnets into the database. Note that the second subnet will + // overwrite the first subnet as they use the same ID. + for (auto subnet : test_subnets_) { + cbptr_->createUpdateSubnet4(ServerSelector::UNASSIGNED(), subnet); + } + + // Fetch all subnets. + Subnet4Collection subnets = cbptr_->getAllSubnets4(ServerSelector::UNASSIGNED()); + ASSERT_EQ(test_subnets_.size() - 1, subnets.size()); + + // See if the subnets are returned ok. + for (auto i = 0; i < subnets.size(); ++i) { + EXPECT_EQ(test_subnets_[i + 1]->toElement()->str(), + subnets[i]->toElement()->str()); + } +} + +// Test that subnets modified after given time can be fetched. +TEST_F(MySqlConfigBackendDHCPv4Test, getModifiedSubnets4) { + // Explicitly set timestamps of subnets. First subnet has a timestamp + // pointing to the future. Second subnet has timestamp pointing to the + // past (yesterday). Third subnet has a timestamp pointing to the + // past (an hour ago). + test_subnets_[1]->setModificationTime(timestamps_["tomorrow"]); + test_subnets_[2]->setModificationTime(timestamps_["yesterday"]); + test_subnets_[3]->setModificationTime(timestamps_["today"]); + + // Insert subnets into the database. + for (int i = 1; i < test_subnets_.size(); ++i) { + cbptr_->createUpdateSubnet4(ServerSelector::UNASSIGNED(), + test_subnets_[i]); + } + + // Fetch subnets with timestamp later than today. Only one subnet + // should be returned. + Subnet4Collection + subnets = cbptr_->getModifiedSubnets4(ServerSelector::UNASSIGNED(), + timestamps_["today"]); + ASSERT_EQ(1, subnets.size()); + + // Fetch subnets with timestamp later than yesterday. We should get + // two subnets. + subnets = cbptr_->getModifiedSubnets4(ServerSelector::UNASSIGNED(), + timestamps_["yesterday"]); + ASSERT_EQ(2, subnets.size()); + + // Fetch subnets with timestamp later than tomorrow. Nothing should + // be returned. + subnets = cbptr_->getModifiedSubnets4(ServerSelector::UNASSIGNED(), + timestamps_["tomorrow"]); + ASSERT_TRUE(subnets.empty()); +} } diff --git a/src/lib/cc/Makefile.am b/src/lib/cc/Makefile.am index 18362f650b..f191e6c819 100644 --- a/src/lib/cc/Makefile.am +++ b/src/lib/cc/Makefile.am @@ -10,6 +10,7 @@ libkea_cc_la_SOURCES += cfg_to_element.h dhcp_config_error.h libkea_cc_la_SOURCES += command_interpreter.cc command_interpreter.h libkea_cc_la_SOURCES += json_feed.cc json_feed.h libkea_cc_la_SOURCES += simple_parser.cc simple_parser.h +libkea_cc_la_SOURCES += stamped_element.cc stamped_element.h libkea_cc_la_SOURCES += user_context.cc user_context.h libkea_cc_la_LIBADD = $(top_builddir)/src/lib/util/libkea-util.la @@ -28,6 +29,7 @@ libkea_cc_include_HEADERS = \ dhcp_config_error.h \ json_feed.h \ simple_parser.h \ + stamped_element.h \ user_context.h EXTRA_DIST = cc.dox diff --git a/src/lib/cc/stamped_element.cc b/src/lib/cc/stamped_element.cc new file mode 100644 index 0000000000..0761978844 --- /dev/null +++ b/src/lib/cc/stamped_element.cc @@ -0,0 +1,22 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <cc/stamped_element.h> + +namespace isc { +namespace data { + +StampedElement::StampedElement() + : timestamp_(boost::posix_time::second_clock::universal_time()) { +} + +void +StampedElement::updateModificationTime() { + setModificationTime(boost::posix_time::second_clock::universal_time()); +} + +} // end of namespace isc::data +} // end of namespace isc diff --git a/src/lib/cc/stamped_element.h b/src/lib/cc/stamped_element.h new file mode 100644 index 0000000000..9d15bb952c --- /dev/null +++ b/src/lib/cc/stamped_element.h @@ -0,0 +1,57 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef STAMPED_ELEMENT_H +#define STAMPED_ELEMENT_H + +#include <boost/date_time/posix_time/posix_time.hpp> + +namespace isc { +namespace data { + +/// @brief This class represents configuration element which is +/// associated with the modification timestamp. +/// +/// Classes storing Kea configuration should derive from this object +/// to track modification times of the configuration objects. This +/// is specifically required by the Kea Configuration Backend feature +/// which stores configuration in the database and must be able +/// to recognize recently modified objects to fetch incremental +/// changes. +class StampedElement { +public: + + /// @brief Constructor. + /// + /// Sets timestamp to the current time. + StampedElement(); + + /// @brief Sets timestamp to the explicitly provided value. + /// + /// @param timestamp New timestamp value. + void setModificationTime(const boost::posix_time::ptime& timestamp) { + timestamp_ = timestamp; + } + + /// @brief Sets timestmp to the current time. + void updateModificationTime(); + + /// @brief Returns timestamp. + boost::posix_time::ptime getModificationTime() const { + return (timestamp_); + } + +private: + + /// @brief Holds timestamp value. + boost::posix_time::ptime timestamp_; + +}; + +} // end of namespace isc::data +} // end of namespace isc + +#endif diff --git a/src/lib/cc/tests/Makefile.am b/src/lib/cc/tests/Makefile.am index 452926e36c..e33c049b74 100644 --- a/src/lib/cc/tests/Makefile.am +++ b/src/lib/cc/tests/Makefile.am @@ -19,6 +19,7 @@ run_unittests_SOURCES += data_unittests.cc run_unittests_SOURCES += data_file_unittests.cc run_unittests_SOURCES += json_feed_unittests.cc run_unittests_SOURCES += simple_parser_unittest.cc +run_unittests_SOURCES += stamped_element_unittest.cc run_unittests_SOURCES += user_context_unittests.cc run_unittests_SOURCES += run_unittests.cc run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) diff --git a/src/lib/cc/tests/stamped_element_unittest.cc b/src/lib/cc/tests/stamped_element_unittest.cc new file mode 100644 index 0000000000..6b832ebd31 --- /dev/null +++ b/src/lib/cc/tests/stamped_element_unittest.cc @@ -0,0 +1,60 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <cc/stamped_element.h> +#include <boost/date_time/gregorian/gregorian.hpp> +#include <boost/date_time/posix_time/posix_time.hpp> +#include <gtest/gtest.h> + +using namespace isc::data; + +namespace { + +// Tests that the modification timestamp is by default set to current +// time. +TEST(StampedElementTest, create) { + StampedElement element; + + // Checking that the delta between now and the timestamp is within + // 5s range should be sufficient. + boost::posix_time::time_duration delta = + boost::posix_time::second_clock::universal_time() - + element.getModificationTime(); + EXPECT_LT(delta.seconds(), 5); +} + +// Tests that the modification timestamp can be set to an arbitrary +// value. +TEST(StampedElementTest, setModificationTime) { + boost::posix_time::ptime + modification_time(boost::gregorian::date(2002, boost::date_time::Jan, 10), + boost::posix_time::time_duration(1,2,3)); + StampedElement element; + element.setModificationTime(modification_time); + EXPECT_TRUE(element.getModificationTime() == modification_time); +} + +// Tests that updating modification timestamp sets it to the current +// time. +TEST(StampedElementTest, update) { + boost::posix_time::ptime + modification_time(boost::gregorian::date(2002, boost::date_time::Jan, 10), + boost::posix_time::time_duration(1,2,3)); + StampedElement element; + element.setModificationTime(modification_time); + element.updateModificationTime(); + + // Checking that the delta between now and the timestamp is within + // 5s range should be sufficient. + boost::posix_time::time_duration delta = + boost::posix_time::second_clock::universal_time() - + element.getModificationTime(); + EXPECT_LT(delta.seconds(), 5); +} + +} diff --git a/src/lib/dhcpsrv/network.h b/src/lib/dhcpsrv/network.h index 433e80102c..4f1d0fa736 100644 --- a/src/lib/dhcpsrv/network.h +++ b/src/lib/dhcpsrv/network.h @@ -10,6 +10,7 @@ #include <asiolink/io_address.h> #include <cc/cfg_to_element.h> #include <cc/data.h> +#include <cc/stamped_element.h> #include <cc/user_context.h> #include <dhcp/classify.h> #include <dhcp/option.h> @@ -45,7 +46,9 @@ typedef std::vector<isc::asiolink::IOAddress> IOAddressList; /// class provides an abstract interface that must be implemented by derived /// classes and, where appropriate, implements common methods used by the /// derived classes. -class Network : public virtual isc::data::UserContext, public isc::data::CfgToElement { +class Network : public virtual isc::data::StampedElement, + public virtual isc::data::UserContext, + public isc::data::CfgToElement { public: /// @brief Holds optional information about relay. /// diff --git a/src/lib/dhcpsrv/subnet.cc b/src/lib/dhcpsrv/subnet.cc index 993d5495c8..eb41681dae 100644 --- a/src/lib/dhcpsrv/subnet.cc +++ b/src/lib/dhcpsrv/subnet.cc @@ -11,6 +11,7 @@ #include <dhcp/option_space.h> #include <dhcpsrv/shared_network.h> #include <dhcpsrv/subnet.h> +#include <boost/lexical_cast.hpp> #include <algorithm> #include <sstream> @@ -214,6 +215,26 @@ Subnet::sumPoolCapacity(const PoolCollection& pools, return (sum); } +std::pair<IOAddress, uint8_t> +Subnet::parsePrefixCommon(const std::string& prefix) { + auto pos = prefix.find('/'); + if ((pos == std::string::npos) || + (pos == prefix.size() - 1) || + (pos == 0)) { + isc_throw(BadValue, "unable to parse invalid prefix " << prefix); + } + + try { + IOAddress address(prefix.substr(0, pos)); + int length = boost::lexical_cast<int>(prefix.substr(pos + 1)); + return (std::make_pair(address, static_cast<int>(length))); + + } catch (...) { + isc_throw(BadValue, "unable to parse invalid prefix " << prefix); + } +} + + void Subnet4::checkType(Lease::Type type) const { if (type != Lease::TYPE_V4) { isc_throw(BadValue, "Only TYPE_V4 is allowed for Subnet4"); @@ -717,6 +738,16 @@ Subnet4::toElement() const { return (map); } +std::pair<IOAddress, uint8_t> +Subnet4::parsePrefix(const std::string& prefix) { + std::pair<IOAddress, uint8_t> parsed = Subnet::parsePrefixCommon(prefix); + if (!parsed.first.isV4() || parsed.first.isV4Zero() || + (parsed.second > 32) || (parsed.second == 0)) { + isc_throw(BadValue, "unable to parse invalid IPv4 prefix " << prefix); + } + return (parsed); +} + data::ElementPtr Subnet6::toElement() const { // Prepare the map @@ -748,6 +779,16 @@ Subnet6::toElement() const { return (map); } +std::pair<IOAddress, uint8_t> +Subnet6::parsePrefix(const std::string& prefix) { + std::pair<IOAddress, uint8_t> parsed = Subnet::parsePrefixCommon(prefix); + if (!parsed.first.isV6() || parsed.first.isV6Zero() || + (parsed.second > 128) || (parsed.second == 0)) { + isc_throw(BadValue, "unable to parse invalid IPv6 prefix " << prefix); + } + return (parsed); +} + } // end of isc::dhcp namespace } // end of isc namespace diff --git a/src/lib/dhcpsrv/subnet.h b/src/lib/dhcpsrv/subnet.h index e995283e35..25af5ddeed 100644 --- a/src/lib/dhcpsrv/subnet.h +++ b/src/lib/dhcpsrv/subnet.h @@ -25,7 +25,9 @@ #include <boost/date_time/posix_time/posix_time.hpp> #include <boost/pointer_cast.hpp> #include <boost/shared_ptr.hpp> +#include <cstdint> #include <map> +#include <utility> namespace isc { namespace dhcp { @@ -359,6 +361,17 @@ protected: /// @return A pointer to unparsed subnet configuration. virtual data::ElementPtr toElement() const; + /// @brief Converts subnet prefix to a pair of prefix/length pair. + /// + /// IPv4 and IPv6 specific conversion functions should apply extra checks + /// on the returned values, i.e. whether length is in range and the IP + /// address has a valid type. + /// + /// @param prefix Prefix to be parsed. + /// @throw BadValue if provided prefix is not valid. + static std::pair<asiolink::IOAddress, uint8_t> + parsePrefixCommon(const std::string& prefix); + /// @brief subnet-id /// /// Subnet-id is a unique value that can be used to find or identify @@ -545,6 +558,13 @@ public: /// @return A pointer to unparsed subnet configuration. virtual data::ElementPtr toElement() const; + /// @brief Converts subnet prefix to a pair of prefix/length pair. + /// + /// @param prefix Prefix to be parsed. + /// @throw BadValue if provided invalid IPv4 prefix. + static std::pair<asiolink::IOAddress, uint8_t> + parsePrefix(const std::string& prefix); + private: /// @brief Returns default address for pool selection @@ -657,6 +677,13 @@ public: /// @return A pointer to unparsed subnet configuration. virtual data::ElementPtr toElement() const; + /// @brief Converts subnet prefix to a pair of prefix/length pair. + /// + /// @param prefix Prefix to be parsed. + /// @throw BadValue if provided invalid IPv4 prefix. + static std::pair<asiolink::IOAddress, uint8_t> + parsePrefix(const std::string& prefix); + private: /// @brief Returns default address for pool selection diff --git a/src/lib/dhcpsrv/tests/subnet_unittest.cc b/src/lib/dhcpsrv/tests/subnet_unittest.cc index 9d444be237..e69c6fc961 100644 --- a/src/lib/dhcpsrv/tests/subnet_unittest.cc +++ b/src/lib/dhcpsrv/tests/subnet_unittest.cc @@ -548,6 +548,36 @@ TEST(Subnet4Test, toText) { EXPECT_EQ("192.0.2.0/24", subnet->toText()); } +// This test verifies that the IPv4 prefix can be parsed into prefix/length pair. +TEST(Subnet4Test, parsePrefix) { + std::pair<IOAddress, uint8_t> parsed = + std::make_pair(IOAddress::IPV4_ZERO_ADDRESS(), 0); + + // Valid prefix. + EXPECT_NO_THROW(parsed = Subnet4::parsePrefix("192.0.5.0/24")); + EXPECT_EQ("192.0.5.0", parsed.first.toText()); + EXPECT_EQ(24, static_cast<int>(parsed.second)); + + // Invalid IPv4 address. + EXPECT_THROW(Subnet4::parsePrefix("192.0.2.322/24"), BadValue); + + // Invalid prefix length. + EXPECT_THROW(Subnet4::parsePrefix("192.0.2.0/64"), BadValue); + EXPECT_THROW(Subnet4::parsePrefix("192.0.2.0/0"), BadValue); + + // No IP address. + EXPECT_THROW(Subnet4::parsePrefix(" /24"), BadValue); + + // No prefix length but slash present. + EXPECT_THROW(Subnet4::parsePrefix("10.0.0.0/ "), BadValue); + + // No slash sign. + EXPECT_THROW(Subnet4::parsePrefix("10.0.0.1"), BadValue); + + // IPv6 is not allowed here. + EXPECT_THROW(Subnet4::parsePrefix("3000::/24"), BadValue); +} + // This test checks if the get() method returns proper parameters TEST(Subnet4Test, get) { Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 28, 1, 2, 3)); @@ -1405,6 +1435,36 @@ TEST(Subnet6Test, toText) { EXPECT_EQ("2001:db8::/32", subnet.toText()); } +// This test verifies that the IPv6 prefix can be parsed into prefix/length pair. +TEST(Subnet6Test, parsePrefix) { + std::pair<IOAddress, uint8_t> parsed = + std::make_pair(IOAddress::IPV6_ZERO_ADDRESS(), 0); + + // Valid prefix. + EXPECT_NO_THROW(parsed = Subnet6::parsePrefix("2001:db8:1::/64")); + EXPECT_EQ("2001:db8:1::", parsed.first.toText()); + EXPECT_EQ(64, static_cast<int>(parsed.second)); + + // Invalid IPv6 address. + EXPECT_THROW(Subnet6::parsePrefix("2001:db8::1::/64"), BadValue); + + // Invalid prefix length. + EXPECT_THROW(Subnet6::parsePrefix("2001:db8:1::/164"), BadValue); + EXPECT_THROW(Subnet6::parsePrefix("2001:db8:1::/0"), BadValue); + + // No IP address. + EXPECT_THROW(Subnet6::parsePrefix(" /64"), BadValue); + + // No prefix length but slash present. + EXPECT_THROW(Subnet6::parsePrefix("3000::/ "), BadValue); + + // No slash sign. + EXPECT_THROW(Subnet6::parsePrefix("3000::"), BadValue); + + // IPv4 is not allowed here. + EXPECT_THROW(Subnet6::parsePrefix("192.0.2.0/24"), BadValue); +} + // This test checks if the get() method returns proper parameters TEST(Subnet6Test, get) { Subnet6 subnet(IOAddress("2001:db8::"), 32, 1, 2, 3, 4); diff --git a/src/lib/mysql/mysql_binding.cc b/src/lib/mysql/mysql_binding.cc index ad07c4a97a..974bb3fa82 100644 --- a/src/lib/mysql/mysql_binding.cc +++ b/src/lib/mysql/mysql_binding.cc @@ -8,6 +8,8 @@ #include <mysql/mysql_binding.h> +using namespace isc::data; + namespace isc { namespace db { @@ -21,6 +23,23 @@ MySqlBinding::getString() const { return (std::string(buffer_.begin(), buffer_.begin() + length_)); } +std::string +MySqlBinding::getStringOrDefault(const std::string& default_value) const { + if (amNull()) { + return (default_value); + } + return (getString()); +} + +ElementPtr +MySqlBinding::getJSON() const { + if (amNull()) { + return (ElementPtr()); + } + std::string s = getString(); + return (Element::fromJSON(s)); +} + std::vector<uint8_t> MySqlBinding::getBlob() const { // Make sure the binding type is blob. @@ -31,6 +50,14 @@ MySqlBinding::getBlob() const { return (std::vector<uint8_t>(buffer_.begin(), buffer_.begin() + length_)); } +std::vector<uint8_t> +MySqlBinding::getBlobOrDefault(const std::vector<uint8_t>& default_value) const { + if (amNull()) { + return (default_value); + } + return (getBlob()); +} + boost::posix_time::ptime MySqlBinding::getTimestamp() const { // Make sure the binding type is timestamp. @@ -41,6 +68,14 @@ MySqlBinding::getTimestamp() const { return (convertFromDatabaseTime(*database_time)); } +boost::posix_time::ptime +MySqlBinding::getTimestampOrDefault(const boost::posix_time::ptime& default_value) const { + if (amNull()) { + return (default_value); + } + return (getTimestamp()); +} + MySqlBindingPtr MySqlBinding::createString(const unsigned long length) { MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits<std::string>::column_type, @@ -57,6 +92,11 @@ MySqlBinding::createString(const std::string& value) { } MySqlBindingPtr +MySqlBinding::condCreateString(const std::string& value) { + return (value.empty() ? MySqlBinding::createNull() : createString(value)); +} + +MySqlBindingPtr MySqlBinding::createBlob(const unsigned long length) { MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits<std::vector<uint8_t> >::column_type, length)); diff --git a/src/lib/mysql/mysql_binding.h b/src/lib/mysql/mysql_binding.h index 450d63ddd3..05cb8bd035 100644 --- a/src/lib/mysql/mysql_binding.h +++ b/src/lib/mysql/mysql_binding.h @@ -7,6 +7,7 @@ #ifndef MYSQL_BINDING_H #define MYSQL_BINDING_H +#include <cc/data.h> #include <database/database_connection.h> #include <exceptions/exceptions.h> #include <boost/date_time/posix_time/conversion.hpp> @@ -182,6 +183,28 @@ public: /// @return String value. std::string getString() const; + /// @brief Returns value held in the binding as string. + /// + /// If the value to be returned is null, a default value is returned. + /// + /// @param default_value Default value. + /// + /// @throw InvalidOperation if the binding type is not @c MYSQL_TYPE_STRING. + /// + /// @return String value. + std::string getStringOrDefault(const std::string& default_value) const; + + /// @brief Returns value held in the binding as JSON. + /// + /// Call @c MySqlBinding::amNull to verify that the value is not + /// null prior to calling this method. + /// + /// @throw InvalidOperation if the binding is not @c MYSQL_TYPE_STRING. + /// @throw data::JSONError if the string value is not a valid JSON. + /// + /// @return JSON structure or NULL if the string is null. + data::ElementPtr getJSON() const; + /// @brief Returns value held in the binding as blob. /// /// Call @c MySqlBinding::amNull to verify that the value is not @@ -193,6 +216,18 @@ public: /// @return Blob in a vactor. std::vector<uint8_t> getBlob() const; + /// @brief Returns value held in the binding as blob. + /// + /// If the value to be returned is null, a default value is returned. + /// + /// @param default_value Default value. + /// + /// @throw InvalidOperation if the binding type is not @c MYSQL_TYPE_BLOB. + /// + /// @return Blob in a vactor. + std::vector<uint8_t> + getBlobOrDefault(const std::vector<uint8_t>& default_value) const; + /// @brief Returns numeric value held in the binding. /// /// Call @c MySqlBinding::amNull to verify that the value is not @@ -215,6 +250,26 @@ public: return (*value); } + /// @brief Returns numeric value held in the binding. + /// + /// If the value to be returned is null, a default value is returned. + /// + /// @tparam Numeric type corresponding to the binding type, e.g. + /// @c uint8_t, @c uint16_t etc. + /// @param default_value Default value. + /// + /// @throw InvalidOperation if the binding type does not match the + /// template parameter. + /// + /// @return Numeric value of a specified type. + template<typename T> + T getIntegerOrDefault(T default_value) const { + if (amNull()) { + return (default_value); + } + return (getInteger<T>()); + } + /// @brief Returns timestamp value held in the binding. /// /// Call @c MySqlBinding::amNull to verify that the value is not @@ -226,6 +281,18 @@ public: /// @return Timestamp converted to posix time. boost::posix_time::ptime getTimestamp() const; + /// @brief Returns timestamp value held in the binding. + /// + /// If the value to be returned is null, a default value is returned. + /// + /// @param default_value Default value. + /// + /// @throw InvalidOperation if the binding type is not @c MYSQL_TYPE_TIMESTAMP. + /// + /// @return Timestamp converted to posix time. + boost::posix_time::ptime + getTimestampOrDefault(const boost::posix_time::ptime& default_value) const; + /// @brief Checks if the bound value is NULL. /// /// @return true if the value in the binding is NULL, false otherwise. @@ -248,6 +315,14 @@ public: /// @return Pointer to the created binding. static MySqlBindingPtr createString(const std::string& value); + /// @brief Conditionally creates binding of text type for sending + /// data if provided value is not empty. + /// + /// @param value String value to be sent to the database. + /// + /// @return Pointer to the created binding. + static MySqlBindingPtr condCreateString(const std::string& value); + /// @brief Creates binding of blob type for receiving data. /// /// @param length Length of the buffer into which received data will @@ -304,6 +379,20 @@ public: return (binding); } + /// @brief Conditionally creates binding of numeric type for sending + /// data if provided value is not 0. + /// + /// @tparam Numeric type corresponding to the binding type, e.g. + /// @c uint8_t, @c uint16_t etc. + /// + /// @param value Numeric value to be sent to the database. + /// + /// @return Pointer to the created binding. + template<typename T> + static MySqlBindingPtr condCreateInteger(T value) { + return (value == 0 ? createNull() : createInteger(value)); + } + /// @brief Creates binding of timestamp type for receiving data. /// /// @return Pointer to the created binding. @@ -470,7 +559,7 @@ private: }; /// @brief Collection of bindings. -typedef std::vector<MySqlBindingPtr> BindingCollection; +typedef std::vector<MySqlBindingPtr> MySqlBindingCollection; } // end of namespace isc::db diff --git a/src/lib/mysql/mysql_connection.h b/src/lib/mysql/mysql_connection.h index 16f0458123..28b6facc08 100644 --- a/src/lib/mysql/mysql_connection.h +++ b/src/lib/mysql/mysql_connection.h @@ -192,7 +192,7 @@ class MySqlConnection : public db::DatabaseConnection { public: /// @brief Function invoked to process fetched row. - typedef std::function<void(BindingCollection&)> ConsumeResultFun; + typedef std::function<void(MySqlBindingCollection&)> ConsumeResultFun; /// @brief Constructor /// @@ -341,8 +341,8 @@ public: /// output bindings. template<typename StatementIndex> void selectQuery(const StatementIndex& index, - const BindingCollection& in_bindings, - BindingCollection& out_bindings, + const MySqlBindingCollection& in_bindings, + MySqlBindingCollection& out_bindings, ConsumeResultFun process_result) { // Extract native input bindings. std::vector<MYSQL_BIND> in_bind_vec; @@ -417,7 +417,7 @@ public: /// in the query. template<typename StatementIndex> void insertQuery(const StatementIndex& index, - const BindingCollection& in_bindings) { + const MySqlBindingCollection& in_bindings) { std::vector<MYSQL_BIND> in_bind_vec; for (MySqlBindingPtr in_binding : in_bindings) { in_bind_vec.push_back(in_binding->getMySqlBinding()); @@ -455,7 +455,7 @@ public: /// @return Number of affected rows. template<typename StatementIndex> uint64_t updateDeleteQuery(const StatementIndex& index, - const BindingCollection& in_bindings) { + const MySqlBindingCollection& in_bindings) { std::vector<MYSQL_BIND> in_bind_vec; for (MySqlBindingPtr in_binding : in_bindings) { in_bind_vec.push_back(in_binding->getMySqlBinding()); diff --git a/src/lib/mysql/tests/Makefile.am b/src/lib/mysql/tests/Makefile.am index 14d5f16f5c..f2d6ed2142 100644 --- a/src/lib/mysql/tests/Makefile.am +++ b/src/lib/mysql/tests/Makefile.am @@ -18,7 +18,8 @@ TESTS = if HAVE_GTEST TESTS += libmysql_unittests -libmysql_unittests_SOURCES = mysql_connection_unittest.cc +libmysql_unittests_SOURCES = mysql_binding_unittest.cc +libmysql_unittests_SOURCES += mysql_connection_unittest.cc libmysql_unittests_SOURCES += run_unittests.cc libmysql_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) diff --git a/src/lib/mysql/tests/mysql_binding_unittest.cc b/src/lib/mysql/tests/mysql_binding_unittest.cc new file mode 100644 index 0000000000..48d96671ff --- /dev/null +++ b/src/lib/mysql/tests/mysql_binding_unittest.cc @@ -0,0 +1,97 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <mysql/mysql_binding.h> +#include <boost/date_time/posix_time/posix_time.hpp> +#include <gtest/gtest.h> + +using namespace isc::data; +using namespace isc::db; + +namespace { + +// This test verifies that default string is returned if binding is null. +TEST(MySqlBindingTest, defaultString) { + auto binding = MySqlBinding::createNull(); + EXPECT_EQ("foo", binding->getStringOrDefault("foo")); + + binding = MySqlBinding::createString("bar"); + EXPECT_EQ("bar", binding->getStringOrDefault("foo")); +} + +// This test verifies that null binding is created for empty string. +TEST(MySqlBindingTest, conditionalString) { + auto binding = MySqlBinding::condCreateString(""); + EXPECT_TRUE(binding->amNull()); + + binding = MySqlBinding::condCreateString("foo"); + ASSERT_FALSE(binding->amNull()); + EXPECT_EQ("foo", binding->getString()); +} + +// This test verifies that null JSON is returned if the string binding +// is null, JSON value is returned when string value is valid JSON and +// that exception is thrown if the string is not a valid JSON. +TEST(MySqlBindingTest, getJSON) { + auto binding = MySqlBinding::createNull(); + EXPECT_FALSE(binding->getJSON()); + + binding = MySqlBinding::createString("{ \"foo\": \"bar\" }"); + auto json = binding->getJSON(); + ASSERT_TRUE(json); + ASSERT_EQ(Element::map, json->getType()); + auto foo = json->get("foo"); + ASSERT_TRUE(foo); + ASSERT_EQ(Element::string, foo->getType()); + EXPECT_EQ("bar", foo->stringValue()); +} + +// This test verifies that default blob is returned if binding is null. +TEST(MySqlBindingTest, defaultBlob) { + std::vector<uint8_t> blob(10, 1); + std::vector<uint8_t> default_blob(10, 5); + auto binding = MySqlBinding::createNull(); + EXPECT_EQ(default_blob, binding->getBlobOrDefault(default_blob)); + + binding = MySqlBinding::createBlob(blob.begin(), blob.end()); + EXPECT_EQ(blob, binding->getBlobOrDefault(default_blob)); +} + +// This test verifies that default number is returned if binding is null. +TEST(MySqlBindingTest, defaultInteger) { + auto binding = MySqlBinding::createNull(); + EXPECT_EQ(123, binding->getIntegerOrDefault<uint32_t>(123)); + + binding = MySqlBinding::createInteger<uint32_t>(1024); + EXPECT_EQ(1024, binding->getIntegerOrDefault<uint32_t>(123)); +} + +// This test verifies that null binding is created for 0 number. +TEST(MySqlBindingTest, conditionalInteger) { + auto binding = MySqlBinding::condCreateInteger<uint16_t>(0); + EXPECT_TRUE(binding->amNull()); + + binding = MySqlBinding::condCreateInteger<uint16_t>(1); + ASSERT_FALSE(binding->amNull()); + EXPECT_EQ(1, binding->getInteger<uint16_t>()); +} + +// This test verifies that default timestamp is returned if binding is null. +TEST(MySqlBindingTest, defaultTimestamp) { + boost::posix_time::ptime current_time = boost::posix_time::second_clock::universal_time(); + boost::posix_time::ptime past_time = current_time - boost::posix_time::hours(1); + + auto binding = MySqlBinding::createNull(); + EXPECT_TRUE(past_time == binding->getTimestampOrDefault(past_time)); + + binding = MySqlBinding::createTimestamp(current_time); + EXPECT_TRUE(current_time == binding->getTimestampOrDefault(past_time)); +} + +} + diff --git a/src/lib/mysql/tests/mysql_connection_unittest.cc b/src/lib/mysql/tests/mysql_connection_unittest.cc index f8455923a5..6d305df613 100644 --- a/src/lib/mysql/tests/mysql_connection_unittest.cc +++ b/src/lib/mysql/tests/mysql_connection_unittest.cc @@ -149,7 +149,7 @@ public: /// /// @param in_bindings Collection of bindings encapsulating the data to /// be inserted into the database and then retrieved. - void testInsertSelect(const BindingCollection& in_bindings) { + void testInsertSelect(const MySqlBindingCollection& in_bindings) { // Expecting 6 bindings because we have 6 columns in our table. ASSERT_EQ(6, in_bindings.size()); // We are going to select by int_value so this value must not be null. @@ -160,11 +160,11 @@ public: in_bindings)); // Create input binding for select query. - BindingCollection bindings = + MySqlBindingCollection bindings = { MySqlBinding::createInteger<uint32_t>(in_bindings[1]->getInteger<uint32_t>()) }; // Also, create output (placeholder) bindings for receiving data. - BindingCollection out_bindings = { + MySqlBindingCollection out_bindings = { MySqlBinding::createInteger<uint8_t>(), MySqlBinding::createInteger<uint32_t>(), MySqlBinding::createInteger<int64_t>(), @@ -177,7 +177,7 @@ public: // returned row the lambda provided as 4th argument should be executed. ASSERT_NO_THROW(conn_.selectQuery(MySqlConnectionTest::GET_BY_INT_VALUE, bindings, out_bindings, - [&](BindingCollection& out_bindings) { + [&](MySqlBindingCollection& out_bindings) { // Compare received data with input data assuming they are both non-null. @@ -230,7 +230,7 @@ public: // from the dataabse. TEST_F(MySqlConnectionTest, select) { std::string blob = "myblob"; - BindingCollection in_bindings = { + MySqlBindingCollection in_bindings = { MySqlBinding::createInteger<uint8_t>(123), MySqlBinding::createInteger<uint32_t>(1024), MySqlBinding::createInteger<int64_t>(-4096), @@ -246,7 +246,7 @@ TEST_F(MySqlConnectionTest, select) { // retrieved. TEST_F(MySqlConnectionTest, selectNullInteger) { std::string blob = "myblob"; - BindingCollection in_bindings = { + MySqlBindingCollection in_bindings = { MySqlBinding::createNull(), MySqlBinding::createInteger<uint32_t>(1024), MySqlBinding::createInteger<int64_t>(-4096), @@ -263,7 +263,7 @@ TEST_F(MySqlConnectionTest, selectNullInteger) { TEST_F(MySqlConnectionTest, selectNullString) { std::string blob = "myblob"; - BindingCollection in_bindings = { + MySqlBindingCollection in_bindings = { MySqlBinding::createInteger<uint8_t>(123), MySqlBinding::createInteger<uint32_t>(1024), MySqlBinding::createInteger<int64_t>(-4096), @@ -278,7 +278,7 @@ TEST_F(MySqlConnectionTest, selectNullString) { // Test that null value can be inserted to a column having blob type and // retrieved. TEST_F(MySqlConnectionTest, selectNullBlob) { - BindingCollection in_bindings = { + MySqlBindingCollection in_bindings = { MySqlBinding::createInteger<uint8_t>(123), MySqlBinding::createInteger<uint32_t>(1024), MySqlBinding::createInteger<int64_t>(-4096), @@ -294,7 +294,7 @@ TEST_F(MySqlConnectionTest, selectNullBlob) { // retrieved. TEST_F(MySqlConnectionTest, selectNullTimestamp) { std::string blob = "myblob"; - BindingCollection in_bindings = { + MySqlBindingCollection in_bindings = { MySqlBinding::createInteger<uint8_t>(123), MySqlBinding::createInteger<uint32_t>(1024), MySqlBinding::createInteger<int64_t>(-4096), @@ -309,7 +309,7 @@ TEST_F(MySqlConnectionTest, selectNullTimestamp) { // Test that empty string and empty blob can be inserted to a database. TEST_F(MySqlConnectionTest, selectEmptyStringBlob) { std::string blob = ""; - BindingCollection in_bindings = { + MySqlBindingCollection in_bindings = { MySqlBinding::createInteger<uint8_t>(123), MySqlBinding::createInteger<uint32_t>(1024), MySqlBinding::createInteger<int64_t>(-4096), @@ -324,7 +324,7 @@ TEST_F(MySqlConnectionTest, selectEmptyStringBlob) { // Test that a row can be deleted from the database. TEST_F(MySqlConnectionTest, deleteByValue) { // Insert a row with numeric values. - BindingCollection in_bindings = { + MySqlBindingCollection in_bindings = { MySqlBinding::createInteger<uint8_t>(123), MySqlBinding::createInteger<uint32_t>(1024), MySqlBinding::createInteger<int64_t>(-4096), @@ -354,7 +354,7 @@ TEST_F(MySqlConnectionTest, deleteByValue) { ASSERT_TRUE(deleted); // Let's confirm that it has been deleted by issuing a select query. - BindingCollection out_bindings = { + MySqlBindingCollection out_bindings = { MySqlBinding::createInteger<uint8_t>(), MySqlBinding::createInteger<uint32_t>(), MySqlBinding::createInteger<int64_t>(), @@ -365,7 +365,7 @@ TEST_F(MySqlConnectionTest, deleteByValue) { ASSERT_NO_THROW(conn_.selectQuery(MySqlConnectionTest::GET_BY_INT_VALUE, in_bindings, out_bindings, - [&deleted](BindingCollection& out_bindings) { + [&deleted](MySqlBindingCollection& out_bindings) { // This will be executed if the row is returned as a result of // select query. We expect that this is not executed. deleted = false; |