]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#1282] Added HA test
authorFrancis Dupont <fdupont@isc.org>
Fri, 26 Jun 2020 10:16:14 +0000 (12:16 +0200)
committerFrancis Dupont <fdupont@isc.org>
Sat, 27 Jun 2020 15:59:10 +0000 (17:59 +0200)
src/hooks/dhcp/high_availability/libloadtests/Makefile.am
src/hooks/dhcp/high_availability/libloadtests/close_unittests.cc [new file with mode: 0644]

index bfa2dcdc950b4a85f73720e8926cd41a5efddd9d..a96eafc16fc02b922c2e1d9d941c065551085605 100644 (file)
@@ -27,6 +27,7 @@ TESTS += ha_unittests
 
 ha_unittests_SOURCES = run_unittests.cc
 ha_unittests_SOURCES += load_unload_unittests.cc
+ha_unittests_SOURCES += close_unittests.cc
 
 ha_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
 
diff --git a/src/hooks/dhcp/high_availability/libloadtests/close_unittests.cc b/src/hooks/dhcp/high_availability/libloadtests/close_unittests.cc
new file mode 100644 (file)
index 0000000..0c3d165
--- /dev/null
@@ -0,0 +1,555 @@
+// Copyright (C) 2020 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/.
+
+/// @file This file contains tests which exercise the load and unload
+/// functions in the high availability hook library. In order to test
+/// the load function, one must be able to pass it hook library
+/// parameters. The the only way to populate these parameters is by
+/// actually loading the library via HooksManager::loadLibraries().
+
+#include <config.h>
+
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/io_service.h>
+#include <cc/command_interpreter.h>
+#include <cc/data.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <dhcp/option.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt6.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/network_state.h>
+#include <hooks/hooks.h>
+#include <hooks/hooks_manager.h>
+#include <hooks/callout_manager.h>
+#include <process/daemon.h>
+#include <util/buffer.h>
+#include <util/watched_thread.h>
+
+#include <gtest/gtest.h>
+
+#include <boost/weak_ptr.hpp>
+#include <errno.h>
+#include <fcntl.h>
+#include <iostream>
+#include <map>
+#include <netinet/in.h>
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <unistd.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::hooks;
+using namespace isc::process;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Structure that holds registered hook indexes.
+struct TestHooks {
+    /// @brief Index of dhcp4_srv_configured callout.
+    int hook_index_dhcp4_srv_configured_;
+
+    /// @brief Index of buffer4_receive callout.
+    int hook_index_buffer4_receive_;
+
+    /// @brief Index of leases4_committed callout.
+    int hook_index_leases4_committed_;
+
+    /// @brief Index of dhcp6_srv_configured callout.
+    int hook_index_dhcp6_srv_configured_;
+
+    /// @brief Index of buffer6_receive callout.
+    int hook_index_buffer6_receive_;
+
+    /// @brief Index of leases6_committed callout.
+    int hook_index_leases6_committed_;
+
+    /// @brief Constructor
+    ///
+    /// The constructor registers hook points for callout tests.
+    TestHooks() {
+        hook_index_dhcp4_srv_configured_ =
+            HooksManager::registerHook("dhcp4_srv_configured");
+        hook_index_buffer4_receive_ =
+            HooksManager::registerHook("buffer4_receive");
+        hook_index_leases4_committed_ =
+            HooksManager::registerHook("leases4_committed");
+        hook_index_dhcp6_srv_configured_ =
+            HooksManager::registerHook("dhcp6_srv_configured");
+        hook_index_buffer6_receive_ =
+            HooksManager::registerHook("buffer6_receive");
+        hook_index_leases6_committed_ =
+            HooksManager::registerHook("leases6_committed");
+    }
+};
+
+TestHooks testHooks;
+
+/// @brief Test fixture for testing closing the HA library
+class CloseHATest : public ::testing::Test {
+public:
+    /// @brief Constructor
+    CloseHATest() {
+    }
+
+    /// @brief Destructor
+    virtual ~CloseHATest() {
+        HooksManager::prepareUnloadLibraries();
+        bool status = HooksManager::unloadLibraries();
+        if (!status) {
+            cerr << "(fixture dtor) unloadLibraries failed" << endl;
+        }
+    }
+
+    /// @brief Return HA configuration with three servers in JSON format.
+    ConstElementPtr createValidJsonConfiguration() const;
+
+    /// @brief Run parters.
+    ///
+    /// Simulate parters by accepting connections. The HA will send
+    /// lease updates and waits for answers so will own the query.
+    void runPartners();
+
+    /// @brief The watched thread.
+    WatchedThreadPtr wthread_;
+};
+
+ConstElementPtr
+CloseHATest::createValidJsonConfiguration() const {
+    std::string config_text =
+        "["
+        "     {"
+        "         \"this-server-name\": \"server1\","
+        "         \"mode\": \"passive-backup\","
+        "         \"wait-backup-ack\": true,"
+        "         \"peers\": ["
+        "             {"
+        "                 \"name\": \"server1\","
+        "                 \"url\": \"http://127.0.0.1:18123/\","
+        "                 \"role\": \"primary\""
+        "             },"
+        "             {"
+        "                 \"name\": \"server2\","
+        "                 \"url\": \"http://127.0.0.1:18124/\","
+        "                 \"role\": \"backup\""
+        "             },"
+        "             {"
+        "                 \"name\": \"server3\","
+        "                 \"url\": \"http://127.0.0.1:18125/\","
+        "                 \"role\": \"backup\""
+        "             }"
+        "         ]"
+        "     }"
+        "]";
+
+    return (Element::fromJSON(config_text));
+}
+
+void
+CloseHATest::runPartners() {
+    int accept_partner1 = -1;
+    int accept_partner2 = -1;
+    std::map<int, bool> readers;
+
+    try {
+        struct sockaddr_in partner;
+        memset(&partner, 0, sizeof(partner));
+        partner.sin_family =  AF_INET;
+#ifdef HAVE_SA_LEN
+        partner.sin_len = sizeof(partner);
+#endif
+        partner.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+        socklen_t slen = sizeof(partner);
+
+#define SA(x)   reinterpret_cast<const sockaddr*>(x)
+        accept_partner1 = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
+        if (accept_partner1 < 0) {
+            isc_throw(Unexpected, "socket1 " << strerror(errno));
+        }
+        if (fcntl(accept_partner1, F_SETFL, O_NONBLOCK) < 0) {
+            isc_throw(Unexpected, "fcntl1 " << strerror(errno));
+        }
+        partner.sin_port = htons(18124);
+        if (::bind(accept_partner1, SA(&partner), slen) < 0) {
+            isc_throw(Unexpected, "bind1 " << strerror(errno));
+        }
+        if (listen(accept_partner1, 1) < 0) {
+            isc_throw(Unexpected, "listen1 " << strerror(errno));
+        }
+
+        accept_partner2 = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
+        if (accept_partner2 < 0) {
+            isc_throw(Unexpected, "socket2 " << strerror(errno));
+        }
+        if (fcntl(accept_partner2, F_SETFL, O_NONBLOCK) < 0) {
+            isc_throw(Unexpected, "fcntl2 " << strerror(errno));
+        }
+        partner.sin_port = htons(18125);
+        if (::bind(accept_partner2, SA(&partner), slen) < 0) {
+            isc_throw(Unexpected, "bind2 " << strerror(errno));
+        }
+        if (listen(accept_partner2, 1) < 0) {
+            isc_throw(Unexpected, "listen2 " << strerror(errno));
+        }
+
+        wthread_->markReady(WatchedThread::READY);
+
+        // In fact below only the terminate matters as in fact
+        // accepts have not the time to be called.
+        for (;;) {
+            int nfd;
+            fd_set fds;
+            FD_ZERO(&fds);
+            FD_SET(wthread_->getWatchFd(WatchedThread::TERMINATE), &fds);
+            nfd = wthread_->getWatchFd(WatchedThread::TERMINATE);
+            // FD_SET(accept_partner1, &fds);
+            if (accept_partner1 > nfd) {
+                nfd = accept_partner1;
+            }
+            // FD_SET(accept_partner2, &fds);
+            if (accept_partner1 > nfd) {
+                nfd = accept_partner1;
+            }
+            for (auto reader : readers) {
+                if (!reader.second) {
+                    continue;
+                }
+                int fd = reader.first;
+                FD_SET(fd, &fds);
+                if (fd > nfd) {
+                    nfd = fd;
+                }
+            }
+            struct timeval tm;
+            tm.tv_sec = tm.tv_usec = 0;
+            int n = select(nfd + 1, &fds, 0, 0, &tm);
+            if ((n < 0) && (errno == EINTR)) {
+                cerr << "interrupted" << endl;
+                continue;
+            }
+            if (FD_ISSET(wthread_->getWatchFd(WatchedThread::TERMINATE), &fds)) {
+                break;
+            }
+            if (FD_ISSET(accept_partner1, &fds)) {
+                int fd = accept(accept_partner1, 0, 0);
+                if (fd < 0) {
+                    cerr << "accept1 failed " << strerror(errno) << endl;
+                } else {
+                    if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) {
+                        cerr << "fcntl NONBLOCK " << strerror(errno) << endl;
+                    }
+                    readers[fd] = true;
+                }
+            }
+            if (FD_ISSET(accept_partner2, &fds)) {
+                int fd = accept(accept_partner2, 0, 0);
+                if (fd < 0) {
+                    cerr << "accept2 failed " << strerror(errno) << endl;
+                } else {
+                    if (fcntl(fd, F_SETFL, O_NONBLOCK)) {
+                        cerr << "fcntl NONBLOCK " << strerror(errno) << endl;
+                    }
+                    readers[fd] = true;
+                }
+            }
+            for (auto reader : readers) {
+                if (!reader.second) {
+                    continue;
+                }
+                int     fd = reader.first;
+                if (FD_ISSET(fd, &fds)) {
+                    char buf[128];
+                    int cc = read(fd, buf, 128);
+                    if (cc < 0) {
+                        cerr << "read " << strerror(errno) << endl;
+                    }
+                    if (cc == 0) {
+                        readers[fd] = false;
+                        close(fd);
+                    }
+                }
+            }
+        }
+    } catch (const std::exception& ex) {
+        wthread_->setError(ex.what());
+    }
+
+    wthread_->clearReady(WatchedThread::TERMINATE);
+    if (accept_partner1 >= 0) {
+        close(accept_partner1);
+        accept_partner1 = -1;
+    }
+    if (accept_partner2 >= 0) {
+        close(accept_partner2);
+        accept_partner2 = -1;
+    }
+    for (auto reader : readers) {
+        if (!reader.second) {
+            continue;
+        }
+        int fd = reader.first;
+        readers[fd] = false;
+        close(fd);
+    }
+}
+
+// Test that checks the library can be close in a DHCPv4 server.
+TEST_F(CloseHATest, close4) {
+    // Start partners.
+    wthread_.reset(new WatchedThread());
+    wthread_->start([this] () { runPartners(); });
+
+    // Prepare parameters,
+    ElementPtr params = Element::createMap();
+    params->set("high-availability", createValidJsonConfiguration());
+
+    // Set family and proc name.
+    CfgMgr::instance().setFamily(AF_INET);
+    Daemon::setProcName("kea-dhcp4");
+
+    // Load the library.
+    HookLibsCollection libraries;
+    libraries.push_back(make_pair(LIBDHCP_HA_SO, params));
+    ASSERT_TRUE(HooksManager::loadLibraries(libraries));
+
+    // Prepare objects.
+    IOServicePtr io_service(new IOService());
+    NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv4));
+    Pkt4Ptr query(new Pkt4(DHCPREQUEST, 12345));
+    HWAddrPtr hwaddr(new HWAddr(std::vector<uint8_t>(6, 1), HTYPE_ETHER));
+    query->setHWAddr(hwaddr);
+    ASSERT_NO_THROW(query->pack());
+    const OutputBuffer& buffer = query->getBuffer();
+    query.reset(new Pkt4(reinterpret_cast<const uint8_t*>(buffer.getData()),
+                         buffer.getLength()));
+
+    Lease4Ptr lease(new Lease4(IOAddress("192.0.2.1"), hwaddr,
+                               static_cast<const uint8_t*>(0), 0,
+                               60, 0, 1));
+    Lease4CollectionPtr leases(new Lease4Collection());
+    leases->push_back(lease);
+    Lease4CollectionPtr deleted_leases(new Lease4Collection());
+
+    // Start HA service.
+    EXPECT_TRUE(HooksManager::calloutsPresent(testHooks.hook_index_dhcp4_srv_configured_));
+    {
+        CalloutHandlePtr handle = HooksManager::createCalloutHandle();
+        handle->setArgument("io_context", io_service);
+        handle->setArgument("network_state", network_state);
+        HooksManager::callCallouts(testHooks.hook_index_dhcp4_srv_configured_,
+                                   *handle);
+    }
+
+    // Pretend to be the parter.
+    ConstElementPtr command = createCommand("ha-heartbeat", "dhcp4");
+    ConstElementPtr response;
+    EXPECT_TRUE(HooksManager::commandHandlersPresent("ha-heartbeat"));
+    {
+        CalloutHandlePtr handle = HooksManager::createCalloutHandle();
+        handle->setArgument("command", command);
+        handle->setArgument("response", response);
+        HooksManager::callCommandHandlers("ha-heartbeat", *handle);
+        handle->getArgument("response", response);
+    }
+    int code = 0;
+    ConstElementPtr arguments = parseAnswer(code, response);
+    EXPECT_EQ(0, code);
+    ASSERT_TRUE(arguments);
+    ASSERT_EQ(Element::map, arguments->getType());
+    ConstElementPtr state = arguments->get("state");
+    ASSERT_TRUE(state);
+    ASSERT_EQ(Element::string, state->getType());
+    EXPECT_EQ("passive-backup", state->stringValue());
+
+    // Submit packet.
+    EXPECT_TRUE(HooksManager::calloutsPresent(testHooks.hook_index_buffer4_receive_));
+    {
+        // Use the query callout handle.
+        CalloutHandlePtr handle = query->getCalloutHandle();
+        ScopedCalloutHandleState handle_state(handle);
+        handle->setArgument("query4", query);
+        HooksManager::callCallouts(testHooks.hook_index_buffer4_receive_,
+                                   *handle);
+        EXPECT_EQ(CalloutHandle::NEXT_STEP_SKIP, handle->getStatus());
+    }
+
+    // Wait for the partners.
+    while (!wthread_->isReady(WatchedThread::READY) &&
+           !wthread_->isReady(WatchedThread::ERROR)) {
+        cerr << ".";
+        usleep(50000);
+    }
+    if (wthread_->isReady(WatchedThread::ERROR)) {
+        cerr << "partners simulation failed "
+             << wthread_->getLastError() << endl;
+    }
+
+    // Submit lease.
+    EXPECT_TRUE(HooksManager::calloutsPresent(testHooks.hook_index_leases4_committed_));
+    {
+        CalloutHandlePtr handle = HooksManager::createCalloutHandle();
+        handle->setArgument("query4", query);
+        handle->setArgument("leases4", leases);
+        handle->setArgument("deleted_leases4", deleted_leases);
+        HooksManager::callCallouts(testHooks.hook_index_leases4_committed_,
+                                   *handle);
+        EXPECT_EQ(CalloutHandle::NEXT_STEP_PARK, handle->getStatus());
+    }
+
+    // Done: purge I/Os.
+    io_service->poll();
+    io_service->stop();
+    io_service.reset();
+
+    // Verify the HA owns the query.
+    boost::weak_ptr<Pkt4> weak_query(query);
+    query.reset();
+    EXPECT_FALSE(weak_query.expired());
+
+    EXPECT_FALSE(HooksManager::unloadLibraries());
+
+    // Tell the HA to cleanup first.
+    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
+    EXPECT_TRUE(HooksManager::unloadLibraries());
+
+    // Stop partners.
+    wthread_->stop();
+}
+
+// Test that checks the library can be close in a DHCPv6 server.
+TEST_F(CloseHATest, close6) {
+    // Start partners.
+    wthread_.reset(new WatchedThread());
+    wthread_->start([this] () { runPartners(); });
+
+    // Prepare parameters,
+    ElementPtr params = Element::createMap();
+    params->set("high-availability", createValidJsonConfiguration());
+
+    // Set family and proc name.
+    CfgMgr::instance().setFamily(AF_INET6);
+    Daemon::setProcName("kea-dhcp6");
+
+    // Load the library.
+    HookLibsCollection libraries;
+    libraries.push_back(make_pair(LIBDHCP_HA_SO, params));
+    ASSERT_TRUE(HooksManager::loadLibraries(libraries));
+
+    // Prepare objects.
+    IOServicePtr io_service(new IOService());
+    NetworkStatePtr network_state(new NetworkState(NetworkState::DHCPv6));
+    Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 12345));
+    DuidPtr duid(new DUID(std::vector<uint8_t>(8, 2)));
+    OptionPtr opt_duid(new Option(Option::V6, D6O_CLIENTID, duid->getDuid()));
+    query->addOption(opt_duid);
+    ASSERT_NO_THROW(query->pack());
+    const OutputBuffer& buffer = query->getBuffer();
+    query.reset(new Pkt6(reinterpret_cast<const uint8_t*>(buffer.getData()),
+                         buffer.getLength()));
+
+    Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::2"),
+                               duid, 1234, 50, 60, 1));
+    Lease6CollectionPtr leases(new Lease6Collection());
+    leases->push_back(lease);
+    Lease6CollectionPtr deleted_leases(new Lease6Collection());
+
+    // Start HA service.
+    EXPECT_TRUE(HooksManager::calloutsPresent(testHooks.hook_index_dhcp6_srv_configured_));
+    {
+        CalloutHandlePtr handle = HooksManager::createCalloutHandle();
+        handle->setArgument("io_context", io_service);
+        handle->setArgument("network_state", network_state);
+        HooksManager::callCallouts(testHooks.hook_index_dhcp6_srv_configured_,
+                                   *handle);
+    }
+
+    // Pretend to be the parter.
+    ConstElementPtr command = createCommand("ha-heartbeat", "dhcp6");
+    ConstElementPtr response;
+    EXPECT_TRUE(HooksManager::commandHandlersPresent("ha-heartbeat"));
+    {
+        CalloutHandlePtr handle = HooksManager::createCalloutHandle();
+        handle->setArgument("command", command);
+        handle->setArgument("response", response);
+        HooksManager::callCommandHandlers("ha-heartbeat", *handle);
+        handle->getArgument("response", response);
+    }
+    int code = 0;
+    ConstElementPtr arguments = parseAnswer(code, response);
+    EXPECT_EQ(0, code);
+    ASSERT_TRUE(arguments);
+    ASSERT_EQ(Element::map, arguments->getType());
+    ConstElementPtr state = arguments->get("state");
+    ASSERT_TRUE(state);
+    ASSERT_EQ(Element::string, state->getType());
+    EXPECT_EQ("passive-backup", state->stringValue());
+
+    // Submit packet.
+    EXPECT_TRUE(HooksManager::calloutsPresent(testHooks.hook_index_buffer6_receive_));
+    {
+        // Use the query callout handle.
+        CalloutHandlePtr handle = query->getCalloutHandle();
+        ScopedCalloutHandleState handle_state(handle);
+        handle->setStatus(CalloutHandle::NEXT_STEP_SKIP);
+        handle->setArgument("query6", query);
+        HooksManager::callCallouts(testHooks.hook_index_buffer6_receive_,
+                                   *handle);
+        EXPECT_EQ(CalloutHandle::NEXT_STEP_SKIP, handle->getStatus());
+    }
+
+    // Wait for the partners.
+    while (!wthread_->isReady(WatchedThread::READY) &&
+           !wthread_->isReady(WatchedThread::ERROR)) {
+        cerr << ".";
+        usleep(50000);
+    }
+    if (wthread_->isReady(WatchedThread::ERROR)) {
+        cerr << "partners simulation failed "
+             << wthread_->getLastError() << endl;
+    }
+
+    // Submit lease.
+    EXPECT_TRUE(HooksManager::calloutsPresent(testHooks.hook_index_leases6_committed_));
+    {
+        CalloutHandlePtr handle = HooksManager::createCalloutHandle();
+        handle->setArgument("query6", query);
+        handle->setArgument("leases6", leases);
+        handle->setArgument("deleted_leases6", deleted_leases);
+        HooksManager::callCallouts(testHooks.hook_index_leases6_committed_,
+                                   *handle);
+        EXPECT_EQ(CalloutHandle::NEXT_STEP_PARK, handle->getStatus());
+    }
+
+    // Done: purge I/Os.
+    io_service->poll();
+    io_service->stop();
+    io_service.reset();
+
+    // Verify the HA owns the query.
+    boost::weak_ptr<Pkt6> weak_query(query);
+    query.reset();
+    EXPECT_FALSE(weak_query.expired());
+
+    EXPECT_FALSE(HooksManager::unloadLibraries());
+
+    // Tell the HA to cleanup first.
+    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
+    EXPECT_TRUE(HooksManager::unloadLibraries());
+
+    // Stop partners.
+    wthread_->stop();
+}
+
+}