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
-// 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
#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>
#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;
}
}
+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
#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>
/// @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?
// 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());
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());
}
// 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())
// 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
// 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());
}
// 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())
// 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
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) {
dhcp_disable_timeout = 1;
}
+ lease_sync_filter_.apply();
asyncSyncLeases(*client_, config_->getFailoverPeerConfig()->getName(),
dhcp_disable_timeout, LeasePtr(), null_action);
}
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) {
.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_,
.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) {
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);
-// 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
#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>
/// @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:
--- /dev/null
+// 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
--- /dev/null
+// 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
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
#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;
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 =
"["
" {"
"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
user3_(""),
password3_("") {
MultiThreadingMgr::instance().setMode(false);
+ CfgMgr::instance().clear();
}
/// @brief Destructor.
io_service_->restart();
io_service_->poll();
MultiThreadingMgr::instance().setMode(false);
+ CfgMgr::instance().clear();
}
/// @brief Callback function invoke upon test timeout.
// 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);
// 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);
}
// 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);
// 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);
}
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) {
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) {
" {"
" \"this-server-name\": \"server2\","
" \"mode\": \"hot-standby\","
+ " \"sync-page-limit\": 3,"
" \"multi-threading\": {"
" \"enable-multi-threading\": false"
" },"
+ " \"wait-backup-ack\": false,"
" \"peers\": ["
" {"
" \"name\": \"server1\","
" },"
" {"
" \"name\": \"server5\","
- " \"url\": \"http://127.0.0.1:18124/\","
+ " \"url\": \"http://127.0.0.1:18125/\","
" \"role\": \"backup\""
" }"
" ]"
" \"multi-threading\": {"
" \"enable-multi-threading\": false"
" },"
+ " \"wait-backup-ack\": false,"
" \"peers\": ["
" {"
" \"name\": \"server3\","
" },"
" {"
" \"name\": \"server6\","
- " \"url\": \"http://127.0.0.1:18124/\","
+ " \"url\": \"http://127.0.0.1:18125/\","
" \"role\": \"backup\""
" }"
" ]"
--- /dev/null
+// 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