]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3178] Partition synchronized leases
authorMarcin Siodelski <marcin@isc.org>
Wed, 20 Dec 2023 11:59:33 +0000 (12:59 +0100)
committerMarcin Siodelski <marcin@isc.org>
Fri, 5 Jan 2024 18:04:19 +0000 (19:04 +0100)
13 files changed:
src/hooks/dhcp/high_availability/Makefile.am
src/hooks/dhcp/high_availability/ha_config.cc
src/hooks/dhcp/high_availability/ha_config.h
src/hooks/dhcp/high_availability/ha_impl.cc
src/hooks/dhcp/high_availability/ha_service.cc
src/hooks/dhcp/high_availability/ha_service.h
src/hooks/dhcp/high_availability/lease_sync_filter.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/lease_sync_filter.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/Makefile.am
src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc
src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc
src/hooks/dhcp/high_availability/tests/ha_test.cc
src/hooks/dhcp/high_availability/tests/lease_sync_filter_unittest.cc [new file with mode: 0644]

index cd6e1ef1b6d5aa6f61ac68864847811656dd5ca3..d1ded7acfea7152157fd614e86cea726f69a54cc 100644 (file)
@@ -26,6 +26,7 @@ libha_la_SOURCES += ha_relationship_mapper.h
 libha_la_SOURCES += ha_server_type.h
 libha_la_SOURCES += ha_service.cc ha_service.h
 libha_la_SOURCES += ha_service_states.cc ha_service_states.h
+libha_la_SOURCES += lease_sync_filter.cc lease_sync_filter.h
 libha_la_SOURCES += lease_update_backlog.cc lease_update_backlog.h
 libha_la_SOURCES += query_filter.cc query_filter.h
 libha_la_SOURCES += version.cc
index c50b6c3e14ed1f211a20e58753dbbd750794d609..bc31fde230f825d2b251a3c5738c7164be765876 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2018-2022 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2018-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
@@ -11,6 +11,7 @@
 #include <asiolink/crypto_tls.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/cfg_multi_threading.h>
+#include <dhcpsrv/network.h>
 #include <exceptions/exceptions.h>
 #include <util/multi_threading_mgr.h>
 #include <util/strutil.h>
@@ -20,6 +21,8 @@
 #include <sstream>
 
 using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
 using namespace isc::http;
 using namespace isc::util;
 using namespace isc::dhcp;
@@ -517,5 +520,26 @@ HAConfig::validate() {
     }
 }
 
+std::string
+HAConfig::getSubnetServerName(const SubnetPtr& subnet) {
+    const std::string parameter_name = "ha-server-name";
+    auto context = subnet->getContext();
+    if (!context || (context->getType() != Element::map) || !context->contains(parameter_name)) {
+        NetworkPtr shared_network;
+        subnet->getSharedNetwork(shared_network);
+        if (shared_network) {
+            context = shared_network->getContext();
+        }
+    }
+    if (context && (context->getType() == Element::map) && context->contains(parameter_name)) {
+        auto server_name_element = context->get(parameter_name);
+        if ((server_name_element->getType() != Element::string) || server_name_element->stringValue().empty()) {
+            isc_throw(BadValue, "'" << parameter_name << "'  must be a non-empty string");
+        }
+        return (server_name_element->stringValue());
+    }
+    return ("");
+}
+
 } // end of namespace isc::ha
 } // end of namespace isc
index 18e85200df9f8e5895daf0a5000188c54a26d86a..e818b5c69a4a1605642ffe61b2750d9d41b30eef 100644 (file)
@@ -9,6 +9,7 @@
 
 #include <ha_relationship_mapper.h>
 #include <asiolink/crypto_tls.h>
+#include <dhcpsrv/subnet.h>
 #include <exceptions/exceptions.h>
 #include <http/basic_auth.h>
 #include <http/post_request_json.h>
@@ -803,6 +804,18 @@ public:
     /// @throw HAConfigValidationError if configuration is invalid.
     void validate();
 
+    /// @brief Convenience function extracting a value of the ha-server-name
+    /// parameter from a subnet context.
+    ///
+    /// If the subnet does not contain this parameter it tries to find this
+    /// parameter in the shared network.
+    ///
+    /// @param subnet pointer to the subnet.
+    /// @return ha-server-name parameter value or an empty string if it was
+    /// not found.
+    /// @throw BadValue if the parameter is not a string or is empty.
+    static std::string getSubnetServerName(const dhcp::SubnetPtr& subnet);
+
     std::string this_server_name_;            ///< This server name.
     HAMode ha_mode_;                          ///< Mode of operation.
     bool send_lease_updates_;                 ///< Send lease updates to partner?
index a54d772ceae6c733727497287df02e198cb61d4c..c00a393b9511526b8e44159b6ef3e22f42a984ae 100644 (file)
@@ -148,15 +148,10 @@ HAImpl::subnet4Select(hooks::CalloutHandle& callout_handle) {
     // and this context should contain a mapping of the subnet to a
     // relationship. If the context doesn't exist there is no way
     // to determine which relationship the packet belongs to.
-    auto context = subnet4->getContext();
-    if (!context || (context->getType() != Element::map) || !context->contains("ha-server-name")) {
-        // The server name can be also specified at the shared network level.
-        SharedNetwork4Ptr shared_network;
-        subnet4->getSharedNetwork(shared_network);
-        if (shared_network) {
-            context = shared_network->getContext();
-        }
-        if (!context || (context->getType() != Element::map) || !context->contains("ha-server-name")) {
+    std::string server_name;
+    try {
+        server_name = HAConfig::getSubnetServerName(subnet4);
+        if (server_name.empty()) {
             LOG_ERROR(ha_logger, HA_SUBNET4_SELECT_NO_RELATIONSHIP_SELECTOR_FOR_SUBNET)
                 .arg(query4->getLabel())
                 .arg(subnet4->toText());
@@ -164,11 +159,7 @@ HAImpl::subnet4Select(hooks::CalloutHandle& callout_handle) {
             return;
         }
 
-    }
-
-    // The context has been found and it contains the ha-server-name.
-    auto server_name = context->get("ha-server-name");
-    if ((server_name->getType() != Element::string) || server_name->stringValue().empty()) {
+    } catch (...) {
         LOG_ERROR(ha_logger, HA_SUBNET4_SELECT_INVALID_HA_SERVER_NAME)
             .arg(query4->getLabel())
             .arg(subnet4->toText());
@@ -177,7 +168,7 @@ HAImpl::subnet4Select(hooks::CalloutHandle& callout_handle) {
     }
 
     // Try to find a relationship matching this server name.
-    auto service = services_->get(server_name->stringValue());
+    auto service = services_->get(server_name);
     if (!service) {
         LOG_ERROR(ha_logger, HA_SUBNET4_SELECT_NO_RELATIONSHIP_FOR_SUBNET)
             .arg(query4->getLabel())
@@ -200,7 +191,7 @@ HAImpl::subnet4Select(hooks::CalloutHandle& callout_handle) {
     // Remember the server name we retrieved from the subnet. We will
     // need it in a leases4_committed callout that doesn't have access
     // to the subnet object.
-    callout_handle.setContext("ha-server-name", server_name->stringValue());
+    callout_handle.setContext("ha-server-name", server_name);
 }
 
 void
@@ -406,27 +397,18 @@ HAImpl::subnet6Select(hooks::CalloutHandle& callout_handle) {
     // and this context should contain a mapping of the subnet to a
     // relationship. If the context doesn't exist there is no way
     // to determine which relationship the packet belongs to.
-    auto context = subnet6->getContext();
-    if (!context || (context->getType() != Element::map) || !context->contains("ha-server-name")) {
-        // The server name can be also specified at the shared network level.
-        SharedNetwork6Ptr shared_network;
-        subnet6->getSharedNetwork(shared_network);
-        if (shared_network) {
-            context = shared_network->getContext();
-        }
-        if (!context || (context->getType() != Element::map) || !context->contains("ha-server-name")) {
-            LOG_ERROR(ha_logger, HA_SUBNET6_SELECT_NO_RELATIONSHIP_SELECTOR_FOR_SUBNET)
+    std::string server_name;
+    try {
+        server_name = HAConfig::getSubnetServerName(subnet6);
+        if (server_name.empty()) {
+            LOG_ERROR(ha_logger, HA_SUBNET4_SELECT_NO_RELATIONSHIP_SELECTOR_FOR_SUBNET)
                 .arg(query6->getLabel())
                 .arg(subnet6->toText());
             callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
             return;
         }
 
-    }
-
-    // The context has been found and it contains the ha-server-name.
-    auto server_name = context->get("ha-server-name");
-    if ((server_name->getType() != Element::string) || server_name->stringValue().empty()) {
+    } catch (...) {
         LOG_ERROR(ha_logger, HA_SUBNET6_SELECT_INVALID_HA_SERVER_NAME)
             .arg(query6->getLabel())
             .arg(subnet6->toText());
@@ -435,7 +417,7 @@ HAImpl::subnet6Select(hooks::CalloutHandle& callout_handle) {
     }
 
     // Try to find a relationship matching this server name.
-    auto service = services_->get(server_name->stringValue());
+    auto service = services_->get(server_name);
     if (!service) {
         LOG_ERROR(ha_logger, HA_SUBNET6_SELECT_NO_RELATIONSHIP_FOR_SUBNET)
             .arg(query6->getLabel())
@@ -458,7 +440,7 @@ HAImpl::subnet6Select(hooks::CalloutHandle& callout_handle) {
     // Remember the server name we retrieved from the subnet. We will
     // need it in a leases4_committed callout that doesn't have access
     // to the subnet object.
-    callout_handle.setContext("ha-server-name", server_name->stringValue());
+    callout_handle.setContext("ha-server-name", server_name);
 }
 
 void
index 6455c192583362e40a40f52d0b90f54e85392857..1a538f06f718866d93666332fdd2700097aa5729 100644 (file)
@@ -76,8 +76,8 @@ HAService::HAService(const unsigned int id, const IOServicePtr& io_service,
                      const HAServerType& server_type)
     : id_(id), io_service_(io_service), network_state_(network_state), config_(config),
       server_type_(server_type), client_(), listener_(), communication_state_(),
-      query_filter_(config), mutex_(), pending_requests_(),
-      lease_update_backlog_(config->getDelayedUpdatesLimit()),
+      query_filter_(config), lease_sync_filter_(server_type, config), mutex_(),
+      pending_requests_(), lease_update_backlog_(config->getDelayedUpdatesLimit()),
       sync_complete_notified_(false) {
 
     if (server_type == HAServerType::DHCPv4) {
@@ -2078,6 +2078,7 @@ HAService::asyncSyncLeases() {
         dhcp_disable_timeout = 1;
     }
 
+    lease_sync_filter_.apply();
     asyncSyncLeases(*client_, config_->getFailoverPeerConfig()->getName(),
                     dhcp_disable_timeout, LeasePtr(), null_action);
 }
@@ -2205,6 +2206,18 @@ HAService::asyncSyncLeasesInternal(http::HttpClient& http_client,
                             if (server_type_ == HAServerType::DHCPv4) {
                                 Lease4Ptr lease = Lease4::fromElement(*l);
 
+                                // If we're not on the last page and we're processing final lease on
+                                // this page, let's record the lease as input to the next
+                                // lease4-get-page command.
+                                if ((leases_element.size() >= config_->getSyncPageLimit()) &&
+                                    (l + 1 == leases_element.end())) {
+                                    last_lease = boost::dynamic_pointer_cast<Lease>(lease);
+                                }
+
+                                if (!lease_sync_filter_.shouldSync(lease)) {
+                                    continue;
+                                }
+
                                 // Check if there is such lease in the database already.
                                 Lease4Ptr existing_lease = LeaseMgrFactory::instance().getLease4(lease->addr_);
                                 if (!existing_lease) {
@@ -2227,16 +2240,20 @@ HAService::asyncSyncLeasesInternal(http::HttpClient& http_client,
                                         .arg(lease->subnet_id_);
                                 }
 
+                            } else {
+                                Lease6Ptr lease = Lease6::fromElement(*l);
+
                                 // If we're not on the last page and we're processing final lease on
                                 // this page, let's record the lease as input to the next
-                                // lease4-get-page command.
+                                // lease6-get-page command.
                                 if ((leases_element.size() >= config_->getSyncPageLimit()) &&
                                     (l + 1 == leases_element.end())) {
                                     last_lease = boost::dynamic_pointer_cast<Lease>(lease);
                                 }
 
-                            } else {
-                                Lease6Ptr lease = Lease6::fromElement(*l);
+                                if (!lease_sync_filter_.shouldSync(lease)) {
+                                    continue;
+                                }
 
                                 // Check if there is such lease in the database already.
                                 Lease6Ptr existing_lease = LeaseMgrFactory::instance().getLease6(lease->type_,
@@ -2260,14 +2277,6 @@ HAService::asyncSyncLeasesInternal(http::HttpClient& http_client,
                                         .arg(lease->addr_.toText())
                                         .arg(lease->subnet_id_);
                                 }
-
-                                // If we're not on the last page and we're processing final lease on
-                                // this page, let's record the lease as input to the next
-                                // lease6-get-page command.
-                                if ((leases_element.size() >= config_->getSyncPageLimit()) &&
-                                    (l + 1 == leases_element.end())) {
-                                    last_lease = boost::dynamic_pointer_cast<Lease>(lease);
-                                }
                             }
 
                         } catch (const std::exception& ex) {
@@ -2326,6 +2335,9 @@ HAService::processSynchronize(const std::string& server_name,
 int
 HAService::synchronize(std::string& status_message, const std::string& server_name,
                        const unsigned int max_period) {
+
+    lease_sync_filter_.apply();
+
     IOService io_service;
     HttpClient client(io_service, false);
 
index 35f208ad9ad83030cd0fbd570b4c703deb4c9468..33f9e6b7586cc74a7713fac8d1ba5add9d8637df 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2018-2022 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2018-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
@@ -10,6 +10,7 @@
 #include <communication_state.h>
 #include <ha_config.h>
 #include <ha_server_type.h>
+#include <lease_sync_filter.h>
 #include <lease_update_backlog.h>
 #include <query_filter.h>
 #include <asiolink/asio_wrapper.h>
@@ -1247,6 +1248,9 @@ protected:
     /// @brief Selects queries to be processed/dropped.
     QueryFilter query_filter_;
 
+    /// @brief Lease synchronization filter used in hub-and-spoke model.
+    LeaseSyncFilter lease_sync_filter_;
+
     /// @brief Handle last pending request for this query.
     ///
     /// Search if there are pending requests for this query:
diff --git a/src/hooks/dhcp/high_availability/lease_sync_filter.cc b/src/hooks/dhcp/high_availability/lease_sync_filter.cc
new file mode 100644 (file)
index 0000000..7cdee01
--- /dev/null
@@ -0,0 +1,61 @@
+// Copyright (C) 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 <dhcpsrv/cfgmgr.h>
+#include <ha_config.h>
+#include <lease_sync_filter.h>
+
+using namespace isc::data;
+using namespace isc::dhcp;
+
+namespace isc {
+namespace ha {
+
+LeaseSyncFilter::LeaseSyncFilter(const HAServerType& server_type, const HAConfigPtr& config)
+    : server_type_(server_type), config_(config), subnet_ids_() {
+}
+
+void
+LeaseSyncFilter::apply() {
+    subnet_ids_.clear();
+    if (server_type_ == HAServerType::DHCPv4) {
+        for (auto subnet : *CfgMgr::instance().getCurrentCfg()->getCfgSubnets4()->getAll()) {
+            conditionallyApplySubnetFilter(subnet);
+        }
+    } else {
+        for (auto subnet : *CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll()) {
+            conditionallyApplySubnetFilter(subnet);
+        }
+    }
+}
+
+bool
+LeaseSyncFilter::shouldSync(const LeasePtr& lease) const {
+    return (subnet_ids_.empty() || subnet_ids_.count(lease->subnet_id_));
+}
+
+void
+LeaseSyncFilter::conditionallyApplySubnetFilter(const SubnetPtr& subnet) {
+    try {
+        auto server_name = HAConfig::getSubnetServerName(subnet);
+        if (!server_name.empty()) {
+            for (auto peer : config_->getAllServersConfig()) {
+                if (peer.first == server_name) {
+                    subnet_ids_.insert(subnet->getID());
+                    return;
+                }
+            }
+        }
+    } catch (...) {
+        // Don't add ID when there was no way to fetch
+        // the server name.
+    }
+}
+
+} // end of namespace isc::ha
+} // end of namespace isc
diff --git a/src/hooks/dhcp/high_availability/lease_sync_filter.h b/src/hooks/dhcp/high_availability/lease_sync_filter.h
new file mode 100644 (file)
index 0000000..77eb279
--- /dev/null
@@ -0,0 +1,103 @@
+// Copyright (C) 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/.
+
+#ifndef HA_LEASE_SYNC_FILTER_H
+#define HA_LEASE_SYNC_FILTER_H
+
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/subnet.h>
+#include <ha_config.h>
+#include <ha_server_type.h>
+#include <string>
+#include <unordered_set>
+
+namespace isc {
+namespace ha {
+
+/// @brief Checks if a lease fetched from the other server should be
+/// synchronized into the local lease database.
+///
+/// In a simple case when a pair of the servers have only one relationship,
+/// a server recovering from a crash fetches all leases from the partner
+/// during the database synchronization. If a server participates in more
+/// than one relationship or the server is connected to a server having
+/// more relationshins a decision whether a received lease should be stored
+/// in a database has to be made based on the filtering rules implemented
+/// in this class.
+///
+/// Suppose there are two relationships configured for the server and
+/// each relationship runs the DHCP service for a subset of subnets. When
+/// the server is synchronizing the lease database for one of the relationships
+/// it receives all leases from its partner. Some of them belong to this
+/// relationship and some belong to the other relationship. The server must
+/// only accept the leases belonging to the former relationship and discard
+/// the remaining ones. Accepting all leases could affect lease database
+/// integrity for the second relationship.
+///
+/// This class implements a simple mechanism to determine whether a lease
+/// should be accepted or discarded during the synchronization. It iterates
+/// over the configured subnets and checks which of them belong to the
+/// current relationship. It next records their IDs. For each received lease
+/// it compares the subnet identifier with the recorded list. If the lease
+/// matches the list it is accepted, otherwise it is discarded by the
+/// @c HAService.
+///
+/// If none of the configured subnets contain associations with the HA
+/// relationships the class accepts all leases. It helps to simplify
+/// HA configurations with only one relationship. Such configurations
+/// typically lack subnet/HA associations in the subnet specification.
+class LeaseSyncFilter {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param server_type specified whether it is a DHCPv4 or DHCPv6 server.
+    /// @param config HA service configuration.
+    LeaseSyncFilter(const HAServerType& server_type, const HAConfigPtr& config);
+
+    /// @brief Applies filtering rules based on the current server configuration.
+    ///
+    /// This function should be called before each synchronization in case
+    /// any subnets have been added or removed. It ensures that the filter
+    /// is using the most up-to-date configuration.
+    void apply();
+
+    /// @brief Checks if the lease should be accepted or discarded.
+    ///
+    /// The lease is accepted when the lease subnet belongs to this relationship
+    /// or when none of the subnets have explicit associations with the
+    /// configured HA relationships.
+    ///
+    /// @param lease lease instance.
+    /// @return true if the lease should be accepted, false otherwise.
+    bool shouldSync(const dhcp::LeasePtr& lease) const;
+
+private:
+
+    /// @brief Conditionally applies filtering for a subnet.
+    ///
+    /// This function is called internally by the @c apply() function. It
+    /// checks if the subnet is associated with the relationship and adds
+    /// its ID to the list, if so.
+    ///
+    /// @param subnet subnet instance.
+    void conditionallyApplySubnetFilter(const dhcp::SubnetPtr& subnet);
+
+    /// Server type (i.e., DHCPv4 or DHCPv6).
+    HAServerType server_type_;
+
+    /// Relationship configuration.
+    HAConfigPtr config_;
+
+    /// IDs of the subnets belonging to this relationship.
+    std::unordered_set<uint32_t> subnet_ids_;
+};
+
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
+#endif // HA_LEASE_SYNC_FILTER_H
index 35997f559c50af0b437c6212b4d81b74893dc688..22d9da38024569c8befe09fddee95b9fd111171e 100644 (file)
@@ -35,6 +35,7 @@ ha_unittests_SOURCES += ha_service_unittest.cc
 ha_unittests_SOURCES += ha_test.cc ha_test.h
 ha_unittests_SOURCES += ha_mt_unittest.cc
 ha_unittests_SOURCES += ha_relationship_mapper_unittest.cc
+ha_unittests_SOURCES += lease_sync_filter_unittest.cc
 ha_unittests_SOURCES += lease_update_backlog_unittest.cc
 ha_unittests_SOURCES += query_filter_unittest.cc
 ha_unittests_SOURCES += run_unittests.cc
index f00e4c9fc8a4a67dbf85121cd6df9d1f9f693ada..ef76d0b369df2b118a5f59f648344606570c5368 100644 (file)
@@ -9,18 +9,22 @@
 #include <ha_impl.h>
 #include <ha_service_states.h>
 #include <ha_test.h>
+#include <asiolink/io_address.h>
 #include <cc/command_interpreter.h>
 #include <cc/data.h>
 #include <cc/dhcp_config_error.h>
 #include <config/command_mgr.h>
+#include <dhcpsrv/shared_network.h>
 #include <util/state_model.h>
 #include <util/multi_threading_mgr.h>
 #include <testutils/gtest_utils.h>
 #include <string>
 
 using namespace isc;
+using namespace isc::asiolink;
 using namespace isc::config;
 using namespace isc::data;
+using namespace isc::dhcp;
 using namespace isc::ha;
 using namespace isc::hooks;
 using namespace isc::ha::test;
@@ -402,9 +406,9 @@ TEST_F(HAConfigTest, configurePassiveBackup) {
     EXPECT_EQ(hardware_threads_, impl->getConfig()->getHttpClientThreads());
 }
 
-// Verifies that the correct hub configuration in the hub-and-spoke model is parsed correctly
+// Verifies that multiple relationships in hot-standby mode are parsed correctly
 // and accepted.
-TEST_F(HAConfigTest, configureHub) {
+TEST_F(HAConfigTest, configureMultipleHotStandby) {
     const std::string ha_config =
         "["
         "    {"
@@ -2043,5 +2047,66 @@ TEST_F(HAConfigTest, hubAndSpokeRepeatingThisServerName) {
         "server names must be unique for different relationships: a relationship 'server1' already exists");
 }
 
+// Test that server name can be fetched for a subnet at shared network level.
+TEST_F(HAConfigTest, getSubnetServerNameSharedNetworkLevel) {
+    auto context = Element::createMap();
+    context->set("ha-server-name", Element::create("server1"));
+    auto shared_network = SharedNetwork6::create("foo");
+    auto subnet6 = Subnet6::create(IOAddress("2001:db8:1::"), 64, 30, 40, 50, 60, SubnetID(1));
+    shared_network->setContext(context);
+    shared_network->add(subnet6);
+    auto server_name = HAConfig::getSubnetServerName(subnet6);
+    EXPECT_EQ("server1", server_name);
+}
+
+// Test that server name can be fetched for a subnet at subnet level.
+TEST_F(HAConfigTest, getSubnetServerNameSubnetLevel) {
+    auto context = Element::createMap();
+    context->set("ha-server-name", Element::create("server2"));
+    auto shared_network = SharedNetwork6::create("foo");
+    auto subnet6 = Subnet6::create(IOAddress("2001:db8:1::"), 64, 30, 40, 50, 60, SubnetID(1));
+    subnet6->setContext(context);
+    shared_network->add(subnet6);
+    auto server_name = HAConfig::getSubnetServerName(subnet6);
+    EXPECT_EQ("server2", server_name);
+}
+
+// Test that server name can be fetched for a subnet when there is no
+// shared network.
+TEST_F(HAConfigTest, getSubnetServerName) {
+    auto context = Element::createMap();
+    context->set("ha-server-name", Element::create("server3"));
+    auto subnet6 = Subnet6::create(IOAddress("2001:db8:1::"), 64, 30, 40, 50, 60, SubnetID(1));
+    subnet6->setContext(context);
+    auto server_name = HAConfig::getSubnetServerName(subnet6);
+    EXPECT_EQ("server3", server_name);
+}
+
+// Test that empty server name is returned when it is not specified.
+TEST_F(HAConfigTest, getSubnetServerNameUnspecified) {
+    auto subnet6 = Subnet6::create(IOAddress("2001:db8:1::"), 64, 30, 40, 50, 60, SubnetID(1));
+    auto server_name = HAConfig::getSubnetServerName(subnet6);
+    EXPECT_TRUE(server_name.empty());
+}
+
+// Test that an exception is thrown when server name is empty.
+TEST_F(HAConfigTest, getSubnetServerNameEmpty) {
+    auto context = Element::createMap();
+    context->set("ha-server-name", Element::create(""));
+    auto subnet6 = Subnet6::create(IOAddress("2001:db8:1::"), 64, 30, 40, 50, 60, SubnetID(1));
+    subnet6->setContext(context);
+    EXPECT_THROW(HAConfig::getSubnetServerName(subnet6), BadValue);
+}
+
+// Test that an exception is thrown when server name is not a
+// valid string.
+TEST_F(HAConfigTest, getSubnetServerNameInvalid) {
+    auto context = Element::createMap();
+    context->set("ha-server-name", Element::createMap());
+    auto subnet6 = Subnet6::create(IOAddress("2001:db8:1::"), 64, 30, 40, 50, 60, SubnetID(1));
+    subnet6->setContext(context);
+    EXPECT_THROW(HAConfig::getSubnetServerName(subnet6), BadValue);
+}
+
 
 }  // namespace
index 3b43bd607660edd77f47f1de23236881f1fd6cc9..921ca2de1dfdb354bbd1736688889c5590e07f72 100644 (file)
@@ -615,6 +615,7 @@ public:
           user3_(""),
           password3_("") {
         MultiThreadingMgr::instance().setMode(false);
+        CfgMgr::instance().clear();
     }
 
     /// @brief Destructor.
@@ -627,6 +628,7 @@ public:
         io_service_->restart();
         io_service_->poll();
         MultiThreadingMgr::instance().setMode(false);
+        CfgMgr::instance().clear();
     }
 
     /// @brief Callback function invoke upon test timeout.
@@ -701,16 +703,19 @@ public:
 
         // First, return leases with indexes from 0 to 2.
         response_arguments->set("leases", getTestLeases4AsJson(0, 3));
+        factory_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
         factory2_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
         factory3_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
 
         // Next, return leases with indexes from 3 to 5.
         response_arguments->set("leases", getTestLeases4AsJson(3, 6));
+        factory_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
         factory2_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
         factory3_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
 
         // Then, return leases with indexes from 6 to 8.
         response_arguments->set("leases", getTestLeases4AsJson(6, 9));
+        factory_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
         factory2_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
         factory3_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
 
@@ -719,6 +724,7 @@ public:
         // means that the last page was returned. At this point, the
         // server ends synchronization.
         response_arguments->set("leases", getTestLeases4AsJson(9, 10));
+        factory_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
         factory2_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
         factory3_->getResponseCreator()->setArguments("lease4-get-page", response_arguments);
     }
@@ -735,16 +741,19 @@ public:
 
         // First, return leases with indexes from 0 to 2.
         response_arguments->set("leases", getTestLeases6AsJson(0, 3));
+        factory_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
         factory2_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
         factory3_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
 
         // Next, return leases with indexes from 3 to 5.
         response_arguments->set("leases", getTestLeases6AsJson(3, 6));
+        factory_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
         factory2_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
         factory3_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
 
         // Then, return leases with indexes from 6 to 8.
         response_arguments->set("leases", getTestLeases6AsJson(6, 9));
+        factory_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
         factory2_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
         factory3_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
 
@@ -753,6 +762,7 @@ public:
         // means that the last page was returned. At this point, the
         // server ends synchronization.
         response_arguments->set("leases", getTestLeases6AsJson(9, 10));
+        factory_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
         factory2_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
         factory3_->getResponseCreator()->setArguments("lease6-get-page", response_arguments);
     }
@@ -3418,6 +3428,85 @@ TEST_F(HAServiceTest, asyncSyncLeases4ServerUnauthorized) {
     ASSERT_NO_THROW(runIOService(1000));
 }
 
+// This test verifies that IPv4 leases belonging to the particular service can
+// be fetched from the peer and inserted into a local lease database in the
+// hub-and-spoke configuration.
+TEST_F(HAServiceTest, asyncSyncLeases4Hub) {
+    // Create lease manager.
+    ASSERT_NO_THROW(LeaseMgrFactory::create("universe=4 type=memfile persist=false"));
+
+    // Create IPv4 leases which will be fetched from the other server.
+    ASSERT_NO_THROW(generateTestLeases4());
+
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidHubConfiguration();
+    setBasicAuth(config_storage);
+
+    // Convert leases to the JSON format, the same as used by the lease_cmds
+    // hook library. Configure our test HTTP servers to return those
+    // leases in this format.
+    ElementPtr response_arguments = Element::createMap();
+
+    // In the hub-and-spoke configuration we have to filter out the leases
+    // belonging to the subnets the service is responsible for and we drop
+    // all other leases. Let's create a new subnet for each received lease
+    // and associate the subnets with even indexes with this service. It
+    // should cause the synchronization to accept only these leases for this
+    // service.
+    for (auto i = 1; i <= 10; ++i) {
+        auto subnet = Subnet4::create(IOAddress(static_cast<uint32_t>(i << 24)), 24,
+                                                30, 40, 50, SubnetID(i));
+        if (i % 2 == 0) {
+            auto context = Element::createMap();
+            context->set("ha-server-name", Element::create("server1"));
+            subnet->setContext(context);
+        }
+        CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(subnet);
+    }
+    CfgMgr::instance().commit();
+
+    // Leases are fetched in pages, so the lease4-get-page should be
+    // sent multiple times. The server is configured to return leases
+    // in 3-element chunks.
+    createPagedSyncResponses4();
+
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+    });
+
+    TestHAService service(1, io_service_, network_state_, config_storage);
+    // Setting the heartbeat delay to 0 disables the recurring heartbeat.
+    // We just want to synchronize leases and not send the heartbeat.
+    config_storage->setHeartbeatDelay(0);
+
+    // Start fetching leases asynchronously.
+    ASSERT_NO_THROW(service.asyncSyncLeases());
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(TEST_TIMEOUT, []() {
+        // Stop running the IO service if we see a lease in the lease
+        // database which is expected to be inserted as a result of lease
+        // syncing.
+        return (!LeaseMgrFactory::instance().getLeases4(SubnetID(10)).empty());
+    }));
+
+    // Check that some leases have been added to the lease database and
+    // some not.
+    for (size_t i = 0; i < leases4_.size(); ++i) {
+        Lease4Ptr existing_lease = LeaseMgrFactory::instance().getLease4(leases4_[i]->addr_);
+        // This time we take odd indexes because we count from 0 (not from 1).
+        if (i % 2 == 1) {
+            EXPECT_TRUE(existing_lease) << "lease " << leases4_[i]->addr_.toText()
+                                        << " not in the lease database";
+        } else {
+            EXPECT_FALSE(existing_lease) << "lease " << leases4_[i]->addr_.toText()
+                                         << " is in the lease database but it should not be";
+        }
+    }
+}
+
 // This test verifies that IPv6 leases can be fetched from the peer and inserted
 // or updated in the local lease database.
 TEST_F(HAServiceTest, asyncSyncLeases6) {
@@ -3764,6 +3853,90 @@ TEST_F(HAServiceTest, asyncSyncLeases6Unauthorized) {
     ASSERT_NO_THROW(runIOService(1000));
 }
 
+// This test verifies that IPv6 leases belonging to the particular service can
+// be fetched from the peer and inserted into a local lease database in the
+// hub-and-spoke configuration.
+TEST_F(HAServiceTest, asyncSyncLeases6Hub) {
+    // Create lease manager.
+    ASSERT_NO_THROW(LeaseMgrFactory::create("universe=6 type=memfile persist=false"));
+
+    // Create IPv6 leases which will be fetched from the other server.
+    ASSERT_NO_THROW(generateTestLeases6());
+
+    // Create HA configuration.
+    HAConfigPtr config_storage = createValidHubConfiguration();
+    setBasicAuth(config_storage);
+
+    // Convert leases to the JSON format, the same as used by the lease_cmds
+    // hook library. Configure our test HTTP servers to return those
+    // leases in this format.
+    ElementPtr response_arguments = Element::createMap();
+
+    // In the hub-and-spoke configuration we have to filter out the leases
+    // belonging to the subnets the service is responsible for and we drop
+    // all other leases. Let's create a new subnet for each received lease
+    // and associate the subnets with even indexes with this service. It
+    // should cause the synchronization to accept only these leases for this
+    // service.
+    for (auto i = 1; i <= 10; ++i) {
+        std::ostringstream s;
+        s << i << "::";
+        auto subnet = Subnet6::create(IOAddress(s.str()), 64, 30, 40, 50,
+                                      60, SubnetID(i));
+        if (i % 2 == 0) {
+            auto context = Element::createMap();
+            context->set("ha-server-name", Element::create("server1"));
+            subnet->setContext(context);
+        }
+        CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->add(subnet);
+    }
+    CfgMgr::instance().commit();
+
+    // Leases are fetched in pages, so the lease4-get-page should be
+    // sent multiple times. We need to configure the server side to
+    // return leases in 3-element chunks.
+    createPagedSyncResponses6();
+
+    // Start the servers.
+    ASSERT_NO_THROW({
+        listener_->start();
+        listener2_->start();
+    });
+
+    TestHAService service(1, io_service_, network_state_, config_storage,
+                          HAServerType::DHCPv6);
+    // Setting the heartbeat delay to 0 disables the recurring heartbeat.
+    // We just want to synchronize leases and not send the heartbeat.
+    config_storage->setHeartbeatDelay(0);
+
+    // Start fetching leases asynchronously.
+    ASSERT_NO_THROW(service.asyncSyncLeases());
+
+    // Run IO service to actually perform the transaction.
+    ASSERT_NO_THROW(runIOService(TEST_TIMEOUT, []() {
+        // Stop running the IO service if we see a lease in the lease
+        // database which is expected to be inserted as a result of lease
+        // syncing.
+        return (!LeaseMgrFactory::instance().getLeases6(SubnetID(10)).empty());
+    }));
+
+    // Check that some leases have been added to the lease database and
+    // some not.
+    for (size_t i = 0; i < leases6_.size(); ++i) {
+        // Other leases should be inserted/updated.
+        Lease6Ptr existing_lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+                                                                         leases6_[i]->addr_);
+        // This time we take odd indexes because we count from 0 (not from 1).
+        if (i % 2 == 1) {
+            EXPECT_TRUE(existing_lease) << "lease " << leases6_[i]->addr_.toText()
+                                        << " not in the lease database";
+        } else {
+            EXPECT_FALSE(existing_lease) << "lease " << leases6_[i]->addr_.toText()
+                                         << " is in the lease database but it should not be";
+        }
+    }
+}
+
 // This test verifies that the ha-sync command is processed successfully for the
 // DHCPv4 server.
 TEST_F(HAServiceTest, processSynchronize4) {
index ddfadee3eb6ff90fd230131787832cb9ca561b96..f662f8719a11f87ded83e28871592ae9ad7ee5c9 100644 (file)
@@ -230,9 +230,11 @@ HATest::createValidHubJsonConfiguration() const {
         "     {"
         "         \"this-server-name\": \"server2\","
         "         \"mode\": \"hot-standby\","
+        "         \"sync-page-limit\": 3,"
         "         \"multi-threading\": {"
         "             \"enable-multi-threading\": false"
         "         },"
+        "         \"wait-backup-ack\": false,"
         "         \"peers\": ["
         "             {"
         "                 \"name\": \"server1\","
@@ -246,7 +248,7 @@ HATest::createValidHubJsonConfiguration() const {
         "             },"
         "             {"
         "                 \"name\": \"server5\","
-        "                 \"url\": \"http://127.0.0.1:18124/\","
+        "                 \"url\": \"http://127.0.0.1:18125/\","
         "                 \"role\": \"backup\""
         "             }"
         "         ]"
@@ -257,6 +259,7 @@ HATest::createValidHubJsonConfiguration() const {
         "         \"multi-threading\": {"
         "             \"enable-multi-threading\": false"
         "         },"
+        "         \"wait-backup-ack\": false,"
         "         \"peers\": ["
         "             {"
         "                 \"name\": \"server3\","
@@ -270,7 +273,7 @@ HATest::createValidHubJsonConfiguration() const {
         "             },"
         "             {"
         "                 \"name\": \"server6\","
-        "                 \"url\": \"http://127.0.0.1:18124/\","
+        "                 \"url\": \"http://127.0.0.1:18125/\","
         "                 \"role\": \"backup\""
         "             }"
         "         ]"
diff --git a/src/hooks/dhcp/high_availability/tests/lease_sync_filter_unittest.cc b/src/hooks/dhcp/high_availability/tests/lease_sync_filter_unittest.cc
new file mode 100644 (file)
index 0000000..f0873d5
--- /dev/null
@@ -0,0 +1,180 @@
+// Copyright (C) 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/io_address.h>
+#include <cc/data.h>
+#include <dhcp/hwaddr.h>
+#include <dhcp/duid.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/subnet_id.h>
+#include <ha_config.h>
+#include <ha_server_type.h>
+#include <ha_test.h>
+#include <lease_sync_filter.h>
+#include <boost/make_shared.hpp>
+#include <gtest/gtest.h>
+#include <sstream>
+
+using namespace isc::asiolink;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::ha;
+using namespace isc::ha::test;
+
+namespace {
+
+/// @brief Test fixture class for testing @c LeaseSyncFilterTest.
+class LeaseSyncFilterTest : public HATest {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Clears configuration in the configuration manager.
+    LeaseSyncFilterTest() : HATest() {
+        CfgMgr::instance().clear();
+    };
+
+    /// @brief Destructor.
+    ///
+    /// Clears configuration in the configuration manager.
+    virtual ~LeaseSyncFilterTest() {
+        CfgMgr::instance().clear();
+    }
+};
+
+// This test verifies that lease sync filter correctly identifies that
+// IPv4 leases belong to the configured subnets.
+TEST_F(LeaseSyncFilterTest, explicitSubnets4) {
+    for (auto i = 0; i < 10; ++i) {
+        auto subnet = Subnet4::create(IOAddress(static_cast<uint32_t>(i << 24)), 24,
+                                      30, 40, 50, SubnetID(i+1));
+        auto context = Element::createMap();
+        context->set("ha-server-name", i < 7 ? Element::create("server2") : Element::create("server5"));
+        subnet->setContext(context);
+        CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(subnet);
+    }
+    CfgMgr::instance().commit();
+
+    auto config = createValidConfiguration(HAConfig::HOT_STANDBY);
+    LeaseSyncFilter filter(HAServerType::DHCPv4, config);
+    filter.apply();
+
+    for (auto i = 0; i < 10; ++i) {
+        auto lease = boost::make_shared<Lease4>(IOAddress("192.0.2.1"), HWAddrPtr(), ClientIdPtr(),
+                                                100, 0, SubnetID(i+1));
+        if (i < 7) {
+            EXPECT_TRUE(filter.shouldSync(lease));
+        } else {
+            EXPECT_FALSE(filter.shouldSync(lease));
+        }
+    }
+}
+
+// This test verifies that lease sync filter correctly identifies that
+// IPv6 leases belong to the configured subnets.
+TEST_F(LeaseSyncFilterTest, explicitSubnets6) {
+    for (auto i = 0; i < 10; ++i) {
+        std::ostringstream s;
+        s << i << "::";
+        auto subnet = Subnet6::create(IOAddress(s.str()), 64,
+                                      30, 40, 50, 60, SubnetID(i+1));
+        auto context = Element::createMap();
+        context->set("ha-server-name", i < 7 ? Element::create("server2") : Element::create("server5"));
+        subnet->setContext(context);
+        CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->add(subnet);
+    }
+    CfgMgr::instance().commit();
+
+    auto config = createValidConfiguration(HAConfig::HOT_STANDBY);
+    LeaseSyncFilter filter(HAServerType::DHCPv6, config);
+    filter.apply();
+
+    for (auto i = 0; i < 10; ++i) {
+        std::ostringstream s;
+        s << i << "::";
+        auto lease = boost::make_shared<Lease4>(IOAddress(s.str()), HWAddrPtr(), ClientIdPtr(),
+                                                100, 0, SubnetID(i+1));
+        if (i < 7) {
+            EXPECT_TRUE(filter.shouldSync(lease));
+        } else {
+            EXPECT_FALSE(filter.shouldSync(lease));
+        }
+    }
+}
+
+// This test verifies that lease sync filter correctly identifies that
+// leases belong to the configured subnets when the server name is
+// specified at shared network level.
+TEST_F(LeaseSyncFilterTest, explicitSharedNetworks) {
+    for (auto i = 0; i < 10; ++i) {
+        std::ostringstream s;
+        s << "net" << i;
+        auto shared_network = SharedNetwork4::create(s.str());
+        auto subnet = Subnet4::create(IOAddress(static_cast<uint32_t>(i << 24)), 24,
+                                      30, 40, 50, SubnetID(i+1));
+        auto context = Element::createMap();
+        context->set("ha-server-name", i < 7 ? Element::create("server2") : Element::create("server5"));
+        shared_network->setContext(context);
+        shared_network->add(subnet);
+        CfgMgr::instance().getStagingCfg()->getCfgSharedNetworks4()->add(shared_network);
+        CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(subnet);
+    }
+    CfgMgr::instance().commit();
+
+    auto config = createValidConfiguration(HAConfig::HOT_STANDBY);
+    LeaseSyncFilter filter(HAServerType::DHCPv4, config);
+    filter.apply();
+
+    for (auto i = 0; i < 10; ++i) {
+        auto lease = boost::make_shared<Lease4>(IOAddress("192.0.2.1"), HWAddrPtr(), ClientIdPtr(),
+                                                100, 0, SubnetID(i+1));
+        if (i < 7) {
+            EXPECT_TRUE(filter.shouldSync(lease));
+        } else {
+            EXPECT_FALSE(filter.shouldSync(lease));
+        }
+    }
+}
+
+// This test verifies that lease sync filter accepts all leases when no
+// subnets have been specified in the configuration manager.
+TEST_F(LeaseSyncFilterTest, noSubnets4) {
+    auto config = createValidConfiguration(HAConfig::HOT_STANDBY);
+    LeaseSyncFilter filter(HAServerType::DHCPv4, config);
+    filter.apply();
+
+    for (auto i = 0; i < 10; ++i) {
+        auto lease = boost::make_shared<Lease4>(IOAddress("192.0.2.1"), HWAddrPtr(), ClientIdPtr(),
+                                                100, 0, SubnetID(i));
+        EXPECT_TRUE(filter.shouldSync(lease));
+    }
+}
+
+// This test verifies that lease sync filter accepts all leases when
+// there are no associations specified in the subnets.
+TEST_F(LeaseSyncFilterTest, noAssociationsInSubnets4) {
+    for (auto i = 0; i < 10; ++i) {
+        auto subnet = Subnet4::create(IOAddress(static_cast<uint32_t>(i << 24)), 24,
+                                      30, 40, 50, SubnetID(i+1));
+        CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->add(subnet);
+    }
+    CfgMgr::instance().commit();
+
+    auto config = createValidConfiguration(HAConfig::HOT_STANDBY);
+    LeaseSyncFilter filter(HAServerType::DHCPv4, config);
+    filter.apply();
+
+    for (auto i = 0; i < 10; ++i) {
+        auto lease = boost::make_shared<Lease4>(IOAddress("192.0.2.1"), HWAddrPtr(), ClientIdPtr(),
+                                                100, 0, SubnetID(i));
+        EXPECT_TRUE(filter.shouldSync(lease));
+    }
+}
+
+
+} // end of anonymous namespace