From: Marcin Siodelski Date: Wed, 20 Dec 2023 11:59:33 +0000 (+0100) Subject: [#3178] Partition synchronized leases X-Git-Tag: Kea-2.5.5~81 X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=a71d8b5f5d513f5d534ae091138ba0cd7531a844;p=thirdparty%2Fkea.git [#3178] Partition synchronized leases --- diff --git a/src/hooks/dhcp/high_availability/Makefile.am b/src/hooks/dhcp/high_availability/Makefile.am index cd6e1ef1b6..d1ded7acfe 100644 --- a/src/hooks/dhcp/high_availability/Makefile.am +++ b/src/hooks/dhcp/high_availability/Makefile.am @@ -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 diff --git a/src/hooks/dhcp/high_availability/ha_config.cc b/src/hooks/dhcp/high_availability/ha_config.cc index c50b6c3e14..bc31fde230 100644 --- a/src/hooks/dhcp/high_availability/ha_config.cc +++ b/src/hooks/dhcp/high_availability/ha_config.cc @@ -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 #include #include +#include #include #include #include @@ -20,6 +21,8 @@ #include 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 diff --git a/src/hooks/dhcp/high_availability/ha_config.h b/src/hooks/dhcp/high_availability/ha_config.h index 18e85200df..e818b5c69a 100644 --- a/src/hooks/dhcp/high_availability/ha_config.h +++ b/src/hooks/dhcp/high_availability/ha_config.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -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? diff --git a/src/hooks/dhcp/high_availability/ha_impl.cc b/src/hooks/dhcp/high_availability/ha_impl.cc index a54d772cea..c00a393b95 100644 --- a/src/hooks/dhcp/high_availability/ha_impl.cc +++ b/src/hooks/dhcp/high_availability/ha_impl.cc @@ -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 diff --git a/src/hooks/dhcp/high_availability/ha_service.cc b/src/hooks/dhcp/high_availability/ha_service.cc index 6455c19258..1a538f06f7 100644 --- a/src/hooks/dhcp/high_availability/ha_service.cc +++ b/src/hooks/dhcp/high_availability/ha_service.cc @@ -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); + } + + 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); } - } 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); - } } } 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); diff --git a/src/hooks/dhcp/high_availability/ha_service.h b/src/hooks/dhcp/high_availability/ha_service.h index 35f208ad9a..33f9e6b758 100644 --- a/src/hooks/dhcp/high_availability/ha_service.h +++ b/src/hooks/dhcp/high_availability/ha_service.h @@ -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 #include #include +#include #include #include #include @@ -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 index 0000000000..7cdee015a7 --- /dev/null +++ b/src/hooks/dhcp/high_availability/lease_sync_filter.cc @@ -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 + +#include +#include +#include + +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 index 0000000000..77eb2793fb --- /dev/null +++ b/src/hooks/dhcp/high_availability/lease_sync_filter.h @@ -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 +#include +#include +#include +#include +#include + +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 subnet_ids_; +}; + + +} // end of namespace isc::ha +} // end of namespace isc + +#endif // HA_LEASE_SYNC_FILTER_H diff --git a/src/hooks/dhcp/high_availability/tests/Makefile.am b/src/hooks/dhcp/high_availability/tests/Makefile.am index 35997f559c..22d9da3802 100644 --- a/src/hooks/dhcp/high_availability/tests/Makefile.am +++ b/src/hooks/dhcp/high_availability/tests/Makefile.am @@ -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 diff --git a/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc b/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc index f00e4c9fc8..ef76d0b369 100644 --- a/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc +++ b/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc @@ -9,18 +9,22 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include #include 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 diff --git a/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc b/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc index 3b43bd6076..921ca2de1d 100644 --- a/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc +++ b/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc @@ -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(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) { diff --git a/src/hooks/dhcp/high_availability/tests/ha_test.cc b/src/hooks/dhcp/high_availability/tests/ha_test.cc index ddfadee3eb..f662f8719a 100644 --- a/src/hooks/dhcp/high_availability/tests/ha_test.cc +++ b/src/hooks/dhcp/high_availability/tests/ha_test.cc @@ -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 index 0000000000..f0873d575e --- /dev/null +++ b/src/hooks/dhcp/high_availability/tests/lease_sync_filter_unittest.cc @@ -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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(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(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(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(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(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(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(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(IOAddress("192.0.2.1"), HWAddrPtr(), ClientIdPtr(), + 100, 0, SubnetID(i)); + EXPECT_TRUE(filter.shouldSync(lease)); + } +} + + +} // end of anonymous namespace