summaryrefslogtreecommitdiffstats
path: root/src/bin/dhcp4/tests/kea_controller_unittest.cc
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:15:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:15:43 +0000
commitf5f56e1a1c4d9e9496fcb9d81131066a964ccd23 (patch)
tree49e44c6f87febed37efb953ab5485aa49f6481a7 /src/bin/dhcp4/tests/kea_controller_unittest.cc
parentInitial commit. (diff)
downloadisc-kea-upstream.tar.xz
isc-kea-upstream.zip
Adding upstream version 2.4.1.upstream/2.4.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/bin/dhcp4/tests/kea_controller_unittest.cc')
-rw-r--r--src/bin/dhcp4/tests/kea_controller_unittest.cc1102
1 files changed, 1102 insertions, 0 deletions
diff --git a/src/bin/dhcp4/tests/kea_controller_unittest.cc b/src/bin/dhcp4/tests/kea_controller_unittest.cc
new file mode 100644
index 0000000..ea5149f
--- /dev/null
+++ b/src/bin/dhcp4/tests/kea_controller_unittest.cc
@@ -0,0 +1,1102 @@
+// Copyright (C) 2014-2023 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 <asiolink/interval_timer.h>
+#include <asiolink/io_address.h>
+#include <asiolink/io_service.h>
+#include <cc/command_interpreter.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/hwaddr.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <dhcp4/parser_context.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcpsrv/cb_ctl_dhcp4.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <process/config_base.h>
+
+#ifdef HAVE_MYSQL
+#include <mysql/testutils/mysql_schema.h>
+#endif
+
+#include <log/logger_support.h>
+#include <util/stopwatch.h>
+
+#include <boost/pointer_cast.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+#include <functional>
+#include <fstream>
+#include <iostream>
+#include <signal.h>
+#include <sstream>
+
+#include <arpa/inet.h>
+#include <unistd.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+
+#ifdef HAVE_MYSQL
+using namespace isc::db::test;
+#endif
+
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::hooks;
+
+namespace {
+
+/// @brief Test implementation of the @c CBControlDHCPv4.
+///
+/// This implementation is installed on the test server instance. It
+/// overrides the implementation of the @c databaseConfigFetch function
+/// to verify arguments passed to this function and throw an exception
+/// when desired in the negative test scenarios. It doesn't do the
+/// actual configuration fetch as this is tested elswhere and would
+/// require setting up a database configuration backend.
+class TestCBControlDHCPv4 : public CBControlDHCPv4 {
+public:
+
+ /// @brief Constructor.
+ TestCBControlDHCPv4()
+ : CBControlDHCPv4(), db_total_config_fetch_calls_(0),
+ db_current_config_fetch_calls_(0), db_staging_config_fetch_calls_(0),
+ enable_check_fetch_mode_(false), enable_throw_(false) {
+ }
+
+ /// @brief Stub implementation of the "fetch" function.
+ ///
+ /// If this is not the first invocation of this function, it
+ /// verifies that the @c fetch_mode has been correctly set to
+ /// @c FetchMode::FETCH_UPDATE.
+ ///
+ /// It also throws an exception when desired by a test, to
+ /// verify that the server gracefully handles such exception.
+ ///
+ /// @param config either the staging or the current configuration.
+ /// @param fetch_mode value indicating if the method is called upon the
+ /// server start up or it is called to fetch configuration updates.
+ ///
+ /// @throw Unexpected when configured to do so.
+ virtual void databaseConfigFetch(const process::ConfigPtr& config,
+ const FetchMode& fetch_mode) {
+ ++db_total_config_fetch_calls_;
+
+ if (config == CfgMgr::instance().getCurrentCfg()) {
+ ++db_current_config_fetch_calls_;
+ } else if (config == CfgMgr::instance().getStagingCfg()) {
+ ++db_staging_config_fetch_calls_;
+ }
+
+ if (enable_check_fetch_mode_) {
+ if ((db_total_config_fetch_calls_ <= 1) &&
+ (fetch_mode == FetchMode::FETCH_UPDATE)) {
+ ADD_FAILURE() << "databaseConfigFetch was called with the value "
+ "of fetch_mode=FetchMode::FETCH_UPDATE upon the server configuration";
+
+ } else if ((db_total_config_fetch_calls_ > 1) &&
+ (fetch_mode == FetchMode::FETCH_ALL)) {
+ ADD_FAILURE() << "databaseConfigFetch was called with the value "
+ "of fetch_mode=FetchMode::FETCH_ALL during fetching the updates";
+ }
+ }
+
+ if (enable_throw_) {
+ isc_throw(Unexpected, "testing if exceptions are correctly handled");
+ }
+ }
+
+ /// @brief Returns number of invocations of the @c databaseConfigFetch
+ /// (total).
+ size_t getDatabaseTotalConfigFetchCalls() const {
+ return (db_total_config_fetch_calls_);
+ }
+
+ /// @brief Returns number of invocations of the @c databaseConfigFetch
+ /// (current configuration).
+ size_t getDatabaseCurrentConfigFetchCalls() const {
+ return (db_current_config_fetch_calls_);
+ }
+
+ /// @brief Returns number of invocations of the @c databaseConfigFetch
+ /// (staging configuration).
+ size_t getDatabaseStagingConfigFetchCalls() const {
+ return (db_staging_config_fetch_calls_);
+ }
+
+ /// @brief Enables checking of the @c fetch_mode value.
+ void enableCheckFetchMode() {
+ enable_check_fetch_mode_ = true;
+ }
+
+ /// @brief Enables the object to throw from @c databaseConfigFetch.
+ void enableThrow() {
+ enable_throw_ = true;
+ }
+
+private:
+
+ /// @brief Counter holding number of invocations of the
+ /// @c databaseConfigFetch (total).
+ size_t db_total_config_fetch_calls_;
+
+ /// @brief Counter holding number of invocations of the
+ /// @c databaseConfigFetch (current configuration).
+ size_t db_current_config_fetch_calls_;
+
+ /// @brief Counter holding number of invocations of the
+ /// @c databaseConfigFetch (staging configuration).
+ size_t db_staging_config_fetch_calls_;
+
+ /// @brief Boolean flag indicated if the value of the @c fetch_mode
+ /// should be verified.
+ bool enable_check_fetch_mode_;
+
+ /// @brief Boolean flag indicating if the @c databaseConfigFetch should
+ /// throw.
+ bool enable_throw_;
+};
+
+/// @brief Shared pointer to the @c TestCBControlDHCPv4.
+typedef boost::shared_ptr<TestCBControlDHCPv4> TestCBControlDHCPv4Ptr;
+
+/// @brief "Naked" DHCPv4 server.
+///
+/// Exposes internal fields and installs stub implementation of the
+/// @c CBControlDHCPv4 object.
+class NakedControlledDhcpv4Srv: public ControlledDhcpv4Srv {
+public:
+
+ /// @brief Constructor.
+ NakedControlledDhcpv4Srv()
+ : ControlledDhcpv4Srv(0) {
+ // We're replacing the @c CBControlDHCPv4 instance with our
+ // stub implementation used in tests.
+ cb_control_.reset(new TestCBControlDHCPv4());
+ }
+};
+
+/// @brief test class for Kea configuration backend
+///
+/// This class is used for testing Kea configuration backend.
+/// It is very simple and currently focuses on reading
+/// config file from disk. It is expected to be expanded in the
+/// near future.
+class JSONFileBackendTest : public isc::dhcp::test::BaseServerTest {
+public:
+ JSONFileBackendTest() {
+ }
+
+ ~JSONFileBackendTest() {
+ LeaseMgrFactory::destroy();
+ static_cast<void>(remove(TEST_FILE));
+ static_cast<void>(remove(TEST_INCLUDE));
+ };
+
+ /// @brief writes specified content to a well known file
+ ///
+ /// Writes specified content to TEST_FILE. Tests will
+ /// attempt to read that file.
+ ///
+ /// @param file_name name of file to be written
+ /// @param content content to be written to file
+ void writeFile(const std::string& file_name, const std::string& content) {
+ static_cast<void>(remove(file_name.c_str()));
+
+ ofstream out(file_name.c_str(), ios::trunc);
+ EXPECT_TRUE(out.is_open());
+ out << content;
+ out.close();
+ }
+
+ /// @brief Runs timers for specified time.
+ ///
+ /// @param io_service Pointer to the IO service to be ran.
+ /// @param timeout_ms Amount of time after which the method returns.
+ /// @param cond Pointer to the function which if returns true it
+ /// stops the IO service and causes the function to return.
+ void runTimersWithTimeout(const IOServicePtr& io_service, const long timeout_ms,
+ std::function<bool()> cond = std::function<bool()>()) {
+ IntervalTimer timer(*io_service);
+ std::atomic<bool> stopped(false);
+ timer.setup([&io_service, &stopped]() {
+ stopped = true;
+ io_service->stop();
+ }, timeout_ms, IntervalTimer::ONE_SHOT);
+
+ // Run as long as the timeout hasn't occurred and the interrupting
+ // condition is not specified or not met.
+ while (!stopped && (!cond || !cond())) {
+ io_service->run_one();
+ }
+ io_service->get_io_service().reset();
+ }
+
+ /// @brief This test verifies that the timer used to fetch the configuration
+ /// updates from the database works as expected.
+ void testConfigBackendTimer(const int config_wait_fetch_time,
+ const bool throw_during_fetch = false,
+ const bool call_command = false) {
+ std::ostringstream config;
+ config <<
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ ]"
+ "},"
+ "\"lease-database\": {"
+ " \"type\": \"memfile\","
+ " \"persist\": false"
+ "},"
+ "\"config-control\": {"
+ " \"config-fetch-wait-time\": " << config_wait_fetch_time <<
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+ writeFile(TEST_FILE, config.str());
+
+ // Create an instance of the server and initialize it.
+ boost::scoped_ptr<NakedControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedControlledDhcpv4Srv()));
+ ASSERT_NO_THROW(srv->init(TEST_FILE));
+
+ // Get the CBControlDHCPv4 object belonging to this server.
+ auto cb_control = boost::dynamic_pointer_cast<TestCBControlDHCPv4>(srv->getCBControl());
+
+ // Verify that the parameter passed to the databaseConfigFetch has an
+ // expected value.
+ cb_control->enableCheckFetchMode();
+
+ // Instruct our stub implementation of the CBControlDHCPv4 to throw as a
+ // result of fetch if desired.
+ if (throw_during_fetch) {
+ cb_control->enableThrow();
+ }
+
+ // So far there should be exactly one attempt to fetch the configuration
+ // from the backend. That's the attempt made upon startup on
+ // the staging configuration.
+ // All other fetches will be on the current configuration:
+ // - the timer makes a closure with the staging one but it is
+ // committed so becomes the current one.
+ // - the command is called outside configuration so it must
+ // be the current configuration. The test explicitly checks this.
+ EXPECT_EQ(1, cb_control->getDatabaseTotalConfigFetchCalls());
+ EXPECT_EQ(0, cb_control->getDatabaseCurrentConfigFetchCalls());
+ EXPECT_EQ(1, cb_control->getDatabaseStagingConfigFetchCalls());
+
+
+ if (call_command) {
+ // The case where there is no backend is tested in the
+ // controlled server tests so we have only to verify
+ // that the command calls the database config fetch.
+
+ // Count the startup.
+ EXPECT_EQ(cb_control->getDatabaseTotalConfigFetchCalls(), 1);
+ EXPECT_EQ(cb_control->getDatabaseCurrentConfigFetchCalls(), 0);
+ EXPECT_EQ(cb_control->getDatabaseStagingConfigFetchCalls(), 1);
+
+ ConstElementPtr result =
+ ControlledDhcpv4Srv::processCommand("config-backend-pull",
+ ConstElementPtr());
+ EXPECT_EQ(cb_control->getDatabaseTotalConfigFetchCalls(), 2);
+ std::string expected;
+
+ if (throw_during_fetch) {
+ expected = "{ \"result\": 1, \"text\": ";
+ expected += "\"On demand configuration update failed: ";
+ expected += "testing if exceptions are correctly handled\" }";
+ } else {
+ expected = "{ \"result\": 0, \"text\": ";
+ expected += "\"On demand configuration update successful.\" }";
+ }
+ EXPECT_EQ(expected, result->str());
+
+ // No good way to check the rescheduling...
+ ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 20));
+
+ if (config_wait_fetch_time > 0) {
+ EXPECT_GE(cb_control->getDatabaseTotalConfigFetchCalls(), 5);
+ EXPECT_GE(cb_control->getDatabaseCurrentConfigFetchCalls(), 4);
+ EXPECT_EQ(cb_control->getDatabaseStagingConfigFetchCalls(), 1);
+ } else {
+ EXPECT_EQ(cb_control->getDatabaseTotalConfigFetchCalls(), 2);
+ EXPECT_EQ(cb_control->getDatabaseCurrentConfigFetchCalls(), 1);
+ EXPECT_EQ(cb_control->getDatabaseStagingConfigFetchCalls(), 1);
+ }
+
+ } else if ((config_wait_fetch_time > 0) && (!throw_during_fetch)) {
+ // If we're configured to run the timer, we expect that it was
+ // invoked at least 3 times. This is sufficient to verify that
+ // the timer was scheduled and that the timer continued to run
+ // even when an exception occurred during fetch (that's why it
+ // is 3 not 2).
+ ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 500,
+ [cb_control]() {
+ // Interrupt the timers poll if we have recorded at
+ // least 3 attempts to fetch the updates.
+ return (cb_control->getDatabaseTotalConfigFetchCalls() >= 3);
+ }));
+ EXPECT_GE(cb_control->getDatabaseTotalConfigFetchCalls(), 3);
+ EXPECT_GE(cb_control->getDatabaseCurrentConfigFetchCalls(), 2);
+ EXPECT_EQ(cb_control->getDatabaseStagingConfigFetchCalls(), 1);
+
+ } else {
+ ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 500));
+
+ if (throw_during_fetch) {
+ // If we're simulating the failure condition the number
+ // of consecutive failures should not exceed 10. Therefore
+ // the number of recorded fetches should be 12. One at
+ // startup, 10 failures and one that causes the timer
+ // to stop.
+ EXPECT_EQ(12, cb_control->getDatabaseTotalConfigFetchCalls());
+ EXPECT_EQ(11, cb_control->getDatabaseCurrentConfigFetchCalls());
+ EXPECT_EQ(1, cb_control->getDatabaseStagingConfigFetchCalls());
+
+ } else {
+ // If the server is not configured to schedule the timer,
+ // we should still have one fetch attempt recorded.
+ EXPECT_EQ(1, cb_control->getDatabaseTotalConfigFetchCalls());
+ EXPECT_EQ(0, cb_control->getDatabaseCurrentConfigFetchCalls());
+ EXPECT_EQ(1, cb_control->getDatabaseStagingConfigFetchCalls());
+ }
+ }
+ }
+
+ /// Name of a config file used during tests
+ static const char* TEST_FILE;
+ static const char* TEST_INCLUDE;
+};
+
+const char* JSONFileBackendTest::TEST_FILE = "test-config.json";
+const char* JSONFileBackendTest::TEST_INCLUDE = "test-include.json";
+
+// This test checks if configuration can be read from a JSON file.
+TEST_F(JSONFileBackendTest, jsonFile) {
+
+ // Prepare configuration file.
+ string config = "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/24\" "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.3.101 - 192.0.3.150\" } ],"
+ " \"subnet\": \"192.0.3.0/24\", "
+ " \"id\": 2 "
+ " },"
+ " {"
+ " \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+ " \"id\": 3, "
+ " \"subnet\": \"192.0.4.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+
+ writeFile(TEST_FILE, config);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using the config file.
+ EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+ // Now check if the configuration has been applied correctly.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(3, subnets->size()); // We expect 3 subnets.
+
+ // Check subnet 1.
+ auto subnet = subnets->begin();
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.2.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(24, (*subnet)->get().second);
+
+ // Check pools in the first subnet.
+ const PoolCollection& pools1 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools1.size());
+ EXPECT_EQ("192.0.2.1", pools1.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.2.100", pools1.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+
+ // Check subnet 2.
+ ++subnet;
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.3.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(24, (*subnet)->get().second);
+
+ // Check pools in the second subnet.
+ const PoolCollection& pools2 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools2.size());
+ EXPECT_EQ("192.0.3.101", pools2.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.3.150", pools2.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools2.at(0)->getType());
+
+ // And finally check subnet 3.
+ ++subnet;
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.4.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(24, (*subnet)->get().second);
+
+ // ... and its only pool.
+ const PoolCollection& pools3 = (*subnet)->getPools(Lease::TYPE_V4);
+ EXPECT_EQ("192.0.4.101", pools3.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.4.150", pools3.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools3.at(0)->getType());
+}
+
+// This test checks if configuration can be read from a JSON file
+// using hash (#) line comments
+TEST_F(JSONFileBackendTest, hashComments) {
+
+ string config_hash_comments = "# This is a comment. It should be \n"
+ "#ignored. Real config starts in line below\n"
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "# comments in the middle should be ignored, too\n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/22\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+
+ writeFile(TEST_FILE, config_hash_comments);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using config with comments.
+ EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+ // Now check if the configuration has been applied correctly.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(1, subnets->size());
+
+ // Check subnet 1.
+ auto subnet = subnets->begin();
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.2.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(22, (*subnet)->get().second);
+
+ // Check pools in the first subnet.
+ const PoolCollection& pools1 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools1.size());
+ EXPECT_EQ("192.0.2.0", pools1.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.2.255", pools1.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+}
+
+// This test checks if configuration can be read from a JSON file
+// using C++ line (//) comments.
+TEST_F(JSONFileBackendTest, cppLineComments) {
+
+ string config_cpp_line_comments = "// This is a comment. It should be \n"
+ "//ignored. Real config starts in line below\n"
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "// comments in the middle should be ignored, too\n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/22\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+
+ writeFile(TEST_FILE, config_cpp_line_comments);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using config with comments.
+ EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+ // Now check if the configuration has been applied correctly.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(1, subnets->size());
+
+ // Check subnet 1.
+ auto subnet = subnets->begin();
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.2.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(22, (*subnet)->get().second);
+
+ // Check pools in the first subnet.
+ const PoolCollection& pools1 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools1.size());
+ EXPECT_EQ("192.0.2.0", pools1.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.2.255", pools1.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+}
+
+// This test checks if configuration can be read from a JSON file
+// using C block (/* */) comments
+TEST_F(JSONFileBackendTest, cBlockComments) {
+
+ string config_c_block_comments = "/* This is a comment. It should be \n"
+ "ignored. Real config starts in line below*/\n"
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "/* comments in the middle should be ignored, too*/\n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/22\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+
+ writeFile(TEST_FILE, config_c_block_comments);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using config with comments.
+ EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+ // Now check if the configuration has been applied correctly.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(1, subnets->size());
+
+ // Check subnet 1.
+ auto subnet = subnets->begin();
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.2.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(22, (*subnet)->get().second);
+
+ // Check pools in the first subnet.
+ const PoolCollection& pools1 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools1.size());
+ EXPECT_EQ("192.0.2.0", pools1.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.2.255", pools1.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+}
+
+// This test checks if configuration can be read from a JSON file
+// using an include file.
+TEST_F(JSONFileBackendTest, include) {
+
+ string config_hash_comments = "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ \"*\" ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "<?include \"" + string(TEST_INCLUDE) + "\"?>,"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+ string include = "\n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/22\" "
+ " } ]\n";
+
+ writeFile(TEST_FILE, config_hash_comments);
+ writeFile(TEST_INCLUDE, include);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using config with comments.
+ EXPECT_NO_THROW(srv->init(TEST_FILE));
+
+ // Now check if the configuration has been applied correctly.
+ const Subnet4Collection* subnets =
+ CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(1, subnets->size());
+
+ // Check subnet 1.
+ auto subnet = subnets->begin();
+ ASSERT_TRUE(subnet != subnets->end());
+ EXPECT_EQ("192.0.2.0", (*subnet)->get().first.toText());
+ EXPECT_EQ(22, (*subnet)->get().second);
+
+ // Check pools in the first subnet.
+ const PoolCollection& pools1 = (*subnet)->getPools(Lease::TYPE_V4);
+ ASSERT_EQ(1, pools1.size());
+ EXPECT_EQ("192.0.2.0", pools1.at(0)->getFirstAddress().toText());
+ EXPECT_EQ("192.0.2.255", pools1.at(0)->getLastAddress().toText());
+ EXPECT_EQ(Lease::TYPE_V4, pools1.at(0)->getType());
+}
+
+// This test checks if recursive include of a file is detected
+TEST_F(JSONFileBackendTest, recursiveInclude) {
+
+ string config_recursive_include = "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ <?include \"" + string(TEST_INCLUDE) + "\"?> ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ { "
+ " \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
+ " \"id\": 1, "
+ " \"subnet\": \"192.0.2.0/22\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+ string include = "\"eth\", <?include \"" + string(TEST_INCLUDE) + "\"?>";
+ string msg = "configuration error using file '" + string(TEST_FILE) +
+ "': Too many nested include.";
+
+ writeFile(TEST_FILE, config_recursive_include);
+ writeFile(TEST_INCLUDE, include);
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // And configure it using config with comments.
+ try {
+ srv->init(TEST_FILE);
+ FAIL() << "Expected Dhcp4ParseError but nothing was raised";
+ }
+ catch (const Exception& ex) {
+ EXPECT_EQ(msg, ex.what());
+ }
+}
+
+// This test checks if configuration detects failure when trying:
+// - empty file
+// - empty filename
+// - no Dhcp4 element
+// - Config file that contains Dhcp4 but has a content error
+TEST_F(JSONFileBackendTest, configBroken) {
+
+ // Empty config is not allowed, because Dhcp4 element is missing
+ string config_empty = "";
+
+ // This config does not have mandatory Dhcp4 element
+ string config_v6 = "{ \"Dhcp6\": { \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"2001:db8::/80\" ],"
+ " \"id\": 1, "
+ " \"subnet\": \"2001:db8::/64\" "
+ " } ]}";
+
+ // This has Dhcp4 element, but it's utter nonsense
+ string config_nonsense = "{ \"Dhcp4\": { \"reviews\": \"are so much fun\" } }";
+
+ // Now initialize the server
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ // Try to configure without filename. Should fail.
+ EXPECT_THROW(srv->init(""), BadValue);
+
+ // Try to configure it using empty file. Should fail.
+ writeFile(TEST_FILE, config_empty);
+ EXPECT_THROW(srv->init(TEST_FILE), BadValue);
+
+ // Now try to load a config that does not have Dhcp4 component.
+ writeFile(TEST_FILE, config_v6);
+ EXPECT_THROW(srv->init(TEST_FILE), BadValue);
+
+ // Now try to load a config with Dhcp4 full of nonsense.
+ writeFile(TEST_FILE, config_nonsense);
+ EXPECT_THROW(srv->init(TEST_FILE), BadValue);
+}
+
+/// This unit-test reads all files enumerated in configs-test.txt file, loads
+/// each of them and verify that they can be loaded.
+///
+/// @todo: Unfortunately, we have this test disabled, because all loaded
+/// configs use memfile, which attempts to create lease file in
+/// /usr/local/var/lib/kea/kea-leases4.csv. We have couple options here:
+/// a) disable persistence in example configs - a very bad thing to do
+/// as users will forget to reenable it and then will be surprised when their
+/// leases disappear
+/// b) change configs to store lease file in /tmp. It's almost as bad as the
+/// previous one. Users will then be displeased when all their leases are
+/// wiped. (most systems wipe /tmp during boot)
+/// c) read each config and rewrite it on the fly, so persistence is disabled.
+/// This is probably the way to go, but this is a work for a dedicated ticket.
+///
+/// Hence I'm leaving the test in, but it is disabled.
+TEST_F(JSONFileBackendTest, DISABLED_loadAllConfigs) {
+
+ // Create server first
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(
+ srv.reset(new ControlledDhcpv4Srv(0))
+ );
+
+ const char* configs_list = "configs-list.txt";
+ fstream configs(configs_list, ios::in);
+ ASSERT_TRUE(configs.is_open());
+ std::string config_name;
+ while (std::getline(configs, config_name)) {
+
+ // Ignore empty and commented lines
+ if (config_name.empty() || config_name[0] == '#') {
+ continue;
+ }
+
+ // Unit-tests usually do not print out anything, but in this case I
+ // think printing out tests configs is warranted.
+ std::cout << "Loading config file " << config_name << std::endl;
+
+ try {
+ srv->init(config_name);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "Exception thrown" << ex.what() << endl;
+ }
+ }
+}
+
+// This test verifies that the DHCP server installs the timers for reclaiming
+// and flushing expired leases.
+TEST_F(JSONFileBackendTest, timers) {
+ // This is a basic configuration which enables timers for reclaiming
+ // expired leases and flushing them after 500 seconds since they expire.
+ // Both timers run at 1 second intervals.
+ string config =
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ ]"
+ "},"
+ "\"lease-database\": {"
+ " \"type\": \"memfile\","
+ " \"persist\": false"
+ "},"
+ "\"expired-leases-processing\": {"
+ " \"reclaim-timer-wait-time\": 1,"
+ " \"hold-reclaimed-time\": 500,"
+ " \"flush-reclaimed-timer-wait-time\": 1"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+ writeFile(TEST_FILE, config);
+
+ // Create an instance of the server and initialize it.
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new ControlledDhcpv4Srv(0)));
+ ASSERT_NO_THROW(srv->init(TEST_FILE));
+
+ // Create an expired lease. The lease is expired by 40 seconds ago
+ // (valid lifetime = 60, cltt = now - 100). The lease will be reclaimed
+ // but shouldn't be flushed in the database because the reclaimed are
+ // held in the database 500 seconds after reclamation, according to the
+ // current configuration.
+ HWAddrPtr hwaddr_expired(new HWAddr(HWAddr::fromText("00:01:02:03:04:05")));
+ Lease4Ptr lease_expired(new Lease4(IOAddress("10.0.0.1"), hwaddr_expired,
+ ClientIdPtr(), 60,
+ time(NULL) - 100, SubnetID(1)));
+
+ // Create expired-reclaimed lease. The lease has expired 1000 - 60 seconds
+ // ago. It should be removed from the lease database when the "flush" timer
+ // goes off.
+ HWAddrPtr hwaddr_reclaimed(new HWAddr(HWAddr::fromText("01:02:03:04:05:06")));
+ Lease4Ptr lease_reclaimed(new Lease4(IOAddress("10.0.0.2"), hwaddr_reclaimed,
+ ClientIdPtr(), 60,
+ time(NULL) - 1000, SubnetID(1)));
+ lease_reclaimed->state_ = Lease4::STATE_EXPIRED_RECLAIMED;
+
+ // Add leases to the database.
+ LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
+ ASSERT_NO_THROW(lease_mgr.addLease(lease_expired));
+ ASSERT_NO_THROW(lease_mgr.addLease(lease_reclaimed));
+
+ // Make sure they have been added.
+ ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.1")));
+ ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.2")));
+
+ // Poll the timers for a while to make sure that each of them is executed
+ // at least once.
+ ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 5000));
+
+ // Verify that the leases in the database have been processed as expected.
+
+ // First lease should be reclaimed, but not removed.
+ ASSERT_NO_THROW(lease_expired = lease_mgr.getLease4(IOAddress("10.0.0.1")));
+ ASSERT_TRUE(lease_expired);
+ EXPECT_TRUE(lease_expired->stateExpiredReclaimed());
+
+ // Second lease should have been removed.
+ ASSERT_NO_THROW(
+ lease_reclaimed = lease_mgr.getLease4(IOAddress("10.0.0.2"));
+ );
+ EXPECT_FALSE(lease_reclaimed);
+}
+
+// This test verifies that the server uses default (Memfile) lease database
+// backend when no backend is explicitly specified in the configuration.
+TEST_F(JSONFileBackendTest, defaultLeaseDbBackend) {
+ // This is basic server configuration which excludes lease database
+ // backend specification. The default Memfile backend should be
+ // initialized in this case.
+ string config =
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ ]"
+ "},"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+ writeFile(TEST_FILE, config);
+
+ // Create an instance of the server and initialize it.
+ boost::scoped_ptr<ControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new ControlledDhcpv4Srv(0)));
+ ASSERT_NO_THROW(srv->init(TEST_FILE));
+
+ // The backend should have been created.
+ EXPECT_NO_THROW(static_cast<void>(LeaseMgrFactory::instance()));
+}
+
+// This test verifies that the timer triggering configuration updates
+// is invoked according to the configured value of the
+// config-fetch-wait-time.
+TEST_F(JSONFileBackendTest, configBackendTimer) {
+ testConfigBackendTimer(1);
+}
+
+// This test verifies that the timer for triggering configuration updates
+// is not invoked when the value of the config-fetch-wait-time is set
+// to 0.
+TEST_F(JSONFileBackendTest, configBackendTimerDisabled) {
+ testConfigBackendTimer(0);
+}
+
+// This test verifies that the server will gracefully handle exceptions
+// thrown from the CBControlDHCPv4::databaseConfigFetch, i.e. will
+// reschedule the timer.
+TEST_F(JSONFileBackendTest, configBackendTimerWithThrow) {
+ // The true value instructs the test to throw during the fetch.
+ testConfigBackendTimer(1, true);
+}
+
+// This test verifies that the server will be updated by the
+// config-backend-pull command.
+TEST_F(JSONFileBackendTest, configBackendPullCommand) {
+ testConfigBackendTimer(0, false, true);
+}
+
+// This test verifies that the server will be updated by the
+// config-backend-pull command even when updates fail.
+TEST_F(JSONFileBackendTest, configBackendPullCommandWithThrow) {
+ testConfigBackendTimer(0, true, true);
+}
+
+// This test verifies that the server will be updated by the
+// config-backend-pull command and the timer rescheduled.
+TEST_F(JSONFileBackendTest, configBackendPullCommandWithTimer) {
+ testConfigBackendTimer(1, false, true);
+}
+
+// Starting tests which require MySQL backend availability. Those tests
+// will not be executed if Kea has been compiled without the
+// --with-mysql.
+#ifdef HAVE_MYSQL
+
+/// @brief Test fixture class for the tests utilizing MySQL database
+/// backend.
+class JSONFileBackendMySQLTest : public JSONFileBackendTest {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Recreates MySQL schema for a test.
+ JSONFileBackendMySQLTest() : JSONFileBackendTest() {
+ // Ensure we have the proper schema with no transient data.
+ createMySQLSchema();
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Destroys MySQL schema.
+ virtual ~JSONFileBackendMySQLTest() {
+ // If data wipe enabled, delete transient data otherwise destroy the schema.
+ destroyMySQLSchema();
+ }
+
+ /// @brief Creates server configuration with specified backend type.
+ ///
+ /// @param backend Backend type or empty string to indicate that the
+ /// backend configuration should not be placed in the resulting
+ /// JSON configuration.
+ ///
+ /// @return Server configuration.
+ std::string createConfiguration(const std::string& backend) const;
+
+ /// @brief Test reconfiguration with a backend change.
+ ///
+ /// If any of the parameters is an empty string it indicates that the
+ /// created configuration should exclude backend configuration.
+ ///
+ /// @param backend_first Type of a backend to be used initially.
+ /// @param backend_second Type of a backend to be used after
+ /// reconfiguration.
+ void testBackendReconfiguration(const std::string& backend_first,
+ const std::string& backend_second);
+};
+
+std::string
+JSONFileBackendMySQLTest::createConfiguration(const std::string& backend) const {
+ // This is basic server configuration which excludes lease database
+ // backend specification. The default Memfile backend should be
+ // initialized in this case.
+ std::ostringstream config;
+ config <<
+ "{ \"Dhcp4\": {"
+ "\"interfaces-config\": {"
+ " \"interfaces\": [ ]"
+ "},";
+
+ // For non-empty lease backend type we have to add a backend configuration
+ // section.
+ if (!backend.empty()) {
+ config <<
+ "\"lease-database\": {"
+ " \"type\": \"" << backend << "\"";
+
+ // SQL backends require database credentials.
+ if (backend != "memfile") {
+ config <<
+ ","
+ " \"name\": \"keatest\","
+ " \"user\": \"keatest\","
+ " \"password\": \"keatest\"";
+ }
+ config << "},";
+ }
+
+ // Append the rest of the configuration.
+ config <<
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, \n"
+ "\"subnet4\": [ ],"
+ "\"valid-lifetime\": 4000 }"
+ "}";
+
+ return (config.str());
+}
+
+void
+JSONFileBackendMySQLTest::
+testBackendReconfiguration(const std::string& backend_first,
+ const std::string& backend_second) {
+ writeFile(TEST_FILE, createConfiguration(backend_first));
+
+ // Create an instance of the server and initialize it.
+ boost::scoped_ptr<NakedControlledDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedControlledDhcpv4Srv()));
+ srv->setConfigFile(TEST_FILE);
+ ASSERT_NO_THROW(srv->init(TEST_FILE));
+
+ // The backend should have been created and its type should be
+ // correct.
+ ASSERT_NO_THROW(static_cast<void>(LeaseMgrFactory::instance()));
+ EXPECT_EQ(backend_first.empty() ? "memfile" : backend_first,
+ LeaseMgrFactory::instance().getType());
+
+ // New configuration modifies the lease database backend type.
+ writeFile(TEST_FILE, createConfiguration(backend_second));
+
+ // Explicitly calling signal handler for SIGHUP to trigger server
+ // reconfiguration.
+ raise(SIGHUP);
+
+ // Polling once to be sure that the signal handle has been called.
+ srv->getIOService()->poll();
+
+ // The backend should have been created and its type should be
+ // correct.
+ ASSERT_NO_THROW(static_cast<void>(LeaseMgrFactory::instance()));
+ EXPECT_EQ(backend_second.empty() ? "memfile" : backend_second,
+ LeaseMgrFactory::instance().getType());
+}
+
+
+// This test verifies that backend specification can be added on
+// server reconfiguration.
+TEST_F(JSONFileBackendMySQLTest, reconfigureBackendUndefinedToMySQL) {
+ testBackendReconfiguration("", "mysql");
+}
+
+// This test verifies that when backend specification is removed the
+// default backend is used.
+TEST_F(JSONFileBackendMySQLTest, reconfigureBackendMySQLToUndefined) {
+ testBackendReconfiguration("mysql", "");
+}
+
+// This test verifies that backend type can be changed from Memfile
+// to MySQL.
+TEST_F(JSONFileBackendMySQLTest, reconfigureBackendMemfileToMySQL) {
+ testBackendReconfiguration("memfile", "mysql");
+}
+
+#endif
+
+} // End of anonymous namespace