From: Marcin Siodelski Date: Tue, 10 Oct 2023 09:39:41 +0000 (+0200) Subject: [#3106] Allow multiple HA configs X-Git-Tag: Kea-2.5.5~141 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6085d691b8d3a6fea64b7dadbf7a7c2dbf5bc339;p=thirdparty%2Fkea.git [#3106] Allow multiple HA configs --- diff --git a/src/hooks/dhcp/high_availability/Makefile.am b/src/hooks/dhcp/high_availability/Makefile.am index 5d7a2689e9..cd6e1ef1b6 100644 --- a/src/hooks/dhcp/high_availability/Makefile.am +++ b/src/hooks/dhcp/high_availability/Makefile.am @@ -22,6 +22,7 @@ libha_la_SOURCES += ha_config_parser.cc ha_config_parser.h libha_la_SOURCES += ha_impl.cc ha_impl.h libha_la_SOURCES += ha_log.cc ha_log.h libha_la_SOURCES += ha_messages.cc ha_messages.h +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 diff --git a/src/hooks/dhcp/high_availability/ha_config.cc b/src/hooks/dhcp/high_availability/ha_config.cc index e96bb52c11..dfe94b38fd 100644 --- a/src/hooks/dhcp/high_availability/ha_config.cc +++ b/src/hooks/dhcp/high_availability/ha_config.cc @@ -171,6 +171,11 @@ HAConfig::HAConfig() state_machine_(new StateMachineConfig()) { } +HAConfigPtr +HAConfig::create() { + return (boost::make_shared()); +} + HAConfig::PeerConfigPtr HAConfig::selectNextPeerConfig(const std::string& name) { // Check if there is a configuration for this server name already. We can't diff --git a/src/hooks/dhcp/high_availability/ha_config.h b/src/hooks/dhcp/high_availability/ha_config.h index 13f9f300da..0e25fd00e3 100644 --- a/src/hooks/dhcp/high_availability/ha_config.h +++ b/src/hooks/dhcp/high_availability/ha_config.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 @@ -7,6 +7,7 @@ #ifndef HA_CONFIG_H #define HA_CONFIG_H +#include #include #include #include @@ -29,6 +30,14 @@ public: isc::Exception(file, line, what) { }; }; +class HAConfig; + +/// @brief Pointer to the High Availability configuration structure. +typedef boost::shared_ptr HAConfigPtr; + +/// @brief Pointer to an object mapping HAConfig to relationships. +typedef boost::shared_ptr> HAConfigMapperPtr; + /// @brief Storage for High Availability configuration. class HAConfig { public: @@ -314,6 +323,9 @@ public: /// @brief Constructor. HAConfig(); + /// @brief Instantiates a HAConfig. + static HAConfigPtr create(); + /// @brief Creates and returns pointer to the new peer's configuration. /// /// This method is called during peers configuration parsing, when the @@ -816,9 +828,6 @@ public: StateMachineConfigPtr state_machine_; ///< State machine configuration. }; -/// @brief Pointer to the High Availability configuration structure. -typedef boost::shared_ptr HAConfigPtr; - } // end of namespace isc::ha } // end of namespace isc diff --git a/src/hooks/dhcp/high_availability/ha_config_parser.cc b/src/hooks/dhcp/high_availability/ha_config_parser.cc index 0dcad36e6c..25ae7681ad 100644 --- a/src/hooks/dhcp/high_availability/ha_config_parser.cc +++ b/src/hooks/dhcp/high_availability/ha_config_parser.cc @@ -95,13 +95,8 @@ HAConfigParser::parseInternal(const HAConfigPtr& config_storage, isc_throw(ConfigError, "HA configuration must be a list"); } - const auto& config_vec = config->listValue(); - if (config_vec.size() != 1) { - isc_throw(ConfigError, "invalid number of configurations in the HA configuration" - " list. Expected exactly one configuration"); - } - // Get the HA configuration. + const auto& config_vec = config->listValue(); ElementPtr c = config_vec[0]; // Get 'mode'. That's the first thing to gather because the defaults we diff --git a/src/hooks/dhcp/high_availability/ha_relationship_mapper.h b/src/hooks/dhcp/high_availability/ha_relationship_mapper.h new file mode 100644 index 0000000000..2f9c7cf83f --- /dev/null +++ b/src/hooks/dhcp/high_availability/ha_relationship_mapper.h @@ -0,0 +1,104 @@ +// 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_RELATIONSHIP_MAPPER_H +#define HA_RELATIONSHIP_MAPPER_H + +#include + +#include +#include +#include +#include + +namespace isc { +namespace ha { + +/// @brief Holds associations between objects and HA relationships. +/// +/// There are at least two classes that require associations with the +/// HA relationships: @c HAService and @c HAConfig. The @c HAImpl class +/// may hold one or more instances of these classes. The library must be +/// able to select appropriate instances depending on the partner name. +/// This class associates partners with the relationships. Each partner +/// may be associated with only one relationship. One relationship may +/// be associated with many partners (e.g., primary and standby). +/// +/// @tparam MappedType type of a mapped object (i.e., @c HAService or +/// @c HAConfig). +template +class HARelationshipMapper { +public: + + /// @brief A pointer to the held object type. + typedef boost::shared_ptr MappedTypePtr; + + /// @brief Associates a key with the object. + /// + /// @param key typically a name of a partner belonging to a relationship. + /// @param obj mapped object. + void map(const std::string& key, MappedTypePtr obj) { + if (mapping_.count(key) > 0) { + isc_throw(InvalidOperation, "a relationship '" << key << "' already exists"); + } + mapping_[key] = obj; + + auto found = false; + for (auto o : vector_) { + if (o == obj) { + found = true; + break; + } + } + if (!found) { + vector_.push_back(obj); + } + } + + /// @brief Retrieves mapped object by a key (e.g., partner name). + /// + /// @param key typically a name of the partner belonging to a relationship. + /// @return Mapped object or null pointer if the object was not found. + MappedTypePtr get(const std::string& key) const { + auto obj = mapping_.find(key); + if (obj == mapping_.end()) { + return (MappedTypePtr()); + } + return (obj->second); + } + + /// @brief Returns the sole mapped object. + /// + /// @return Mapped object. + /// @throw InvalidOperation when there is no mapped object or if there + /// are multiple mapped objects. + MappedTypePtr get() const { + if (vector_.empty() || vector_.size() > 1) { + isc_throw(InvalidOperation, "expected one relationship to be configured"); + } + return (vector_[0]); + } + + /// @brief Returns all mapped objects. + /// + /// @return A reference to a vector of mapped objects. + const std::vector& getAll() const { + return (vector_); + } + +private: + + /// Key-to-object mappings. + std::unordered_map mapping_; + + /// A vector of unique objects in the order in which they were mapped. + std::vector vector_; +}; + +} // end of namespace isc::ha +} // end of namespace isc + +#endif // HA_RELATIONSHIP_MAPPER_H diff --git a/src/hooks/dhcp/high_availability/ha_service.cc b/src/hooks/dhcp/high_availability/ha_service.cc index 2521197d9d..e6abe0dc19 100644 --- a/src/hooks/dhcp/high_availability/ha_service.cc +++ b/src/hooks/dhcp/high_availability/ha_service.cc @@ -1037,7 +1037,7 @@ HAService::inScopeInternal(QueryPtrType& query) { query->addClass(dhcp::ClientClass(scope_class)); // The following is the part of the server failure detection algorithm. // If the query should be processed by the partner we need to check if - // the partner responds. If the number of unanswered queries exceeds a + // the partner responds. If the number of unansweered queries exceeds a // configured threshold, we will consider the partner to be offline. if (!in_scope && communication_state_->isCommunicationInterrupted()) { communication_state_->analyzeMessage(query); diff --git a/src/hooks/dhcp/high_availability/tests/Makefile.am b/src/hooks/dhcp/high_availability/tests/Makefile.am index f99f8ebebb..35997f559c 100644 --- a/src/hooks/dhcp/high_availability/tests/Makefile.am +++ b/src/hooks/dhcp/high_availability/tests/Makefile.am @@ -34,6 +34,7 @@ ha_unittests_SOURCES += ha_impl_unittest.cc 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_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 f8e6940ec9..d1f6ad0182 100644 --- a/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc +++ b/src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc @@ -402,6 +402,111 @@ TEST_F(HAConfigTest, configurePassiveBackup) { EXPECT_EQ(hardware_threads_, impl->getConfig()->getHttpClientThreads()); } +// Verifies that hot standby configuration is parsed correctly. +TEST_F(HAConfigTest, configureHub) { + const std::string ha_config = + "[" + " {" + " \"this-server-name\": \"server2\"," + " \"mode\": \"hot-standby\"," + " \"peers\": [" + " {" + " \"name\": \"server1\"," + " \"url\": \"http://127.0.0.1:8080/\"," + " \"role\": \"primary\"," + " \"auto-failover\": false" + " }," + " {" + " \"name\": \"server2\"," + " \"url\": \"http://127.0.0.1:8081/\"," + " \"role\": \"standby\"," + " \"auto-failover\": true" + " }" + " ]" + " }," + " {" + " \"this-server-name\": \"server4\"," + " \"mode\": \"hot-standby\"," + " \"peers\": [" + " {" + " \"name\": \"server3\"," + " \"url\": \"http://127.0.0.1:8080/\"," + " \"role\": \"primary\"," + " \"auto-failover\": false" + " }," + " {" + " \"name\": \"server4\"," + " \"url\": \"http://127.0.0.1:8081/\"," + " \"role\": \"standby\"," + " \"auto-failover\": true" + " }" + " ]" + " }" + "]"; + + HAImplPtr impl(new HAImpl()); + ASSERT_NO_THROW(impl->configure(Element::fromJSON(ha_config))); + EXPECT_EQ("server2", impl->getConfig()->getThisServerName()); + EXPECT_EQ(HAConfig::HOT_STANDBY, impl->getConfig()->getHAMode()); + + HAConfig::PeerConfigPtr cfg = impl->getConfig()->getThisServerConfig(); + ASSERT_TRUE(cfg); + EXPECT_EQ("server2", cfg->getName()); + EXPECT_EQ("http://127.0.0.1:8081/", cfg->getUrl().toText()); + EXPECT_EQ(HAConfig::PeerConfig::STANDBY, cfg->getRole()); + EXPECT_TRUE(cfg->isAutoFailover()); + + cfg = impl->getConfig()->getPeerConfig("server1"); + ASSERT_TRUE(cfg); + EXPECT_EQ("server1", cfg->getName()); + EXPECT_EQ("http://127.0.0.1:8080/", cfg->getUrl().toText()); + EXPECT_EQ(HAConfig::PeerConfig::PRIMARY, cfg->getRole()); + EXPECT_FALSE(cfg->isAutoFailover()); + + HAConfig::StateConfigPtr state_cfg; + ASSERT_NO_THROW(state_cfg = impl->getConfig()->getStateMachineConfig()-> + getStateConfig(HA_BACKUP_ST)); + ASSERT_TRUE(state_cfg); + EXPECT_EQ(STATE_PAUSE_NEVER, state_cfg->getPausing()); + + ASSERT_NO_THROW(state_cfg = impl->getConfig()->getStateMachineConfig()-> + getStateConfig(HA_HOT_STANDBY_ST)); + ASSERT_TRUE(state_cfg); + EXPECT_EQ(STATE_PAUSE_NEVER, state_cfg->getPausing()); + + ASSERT_NO_THROW(state_cfg = impl->getConfig()->getStateMachineConfig()-> + getStateConfig(HA_PARTNER_DOWN_ST)); + ASSERT_TRUE(state_cfg); + EXPECT_EQ(STATE_PAUSE_NEVER, state_cfg->getPausing()); + + ASSERT_NO_THROW(state_cfg = impl->getConfig()->getStateMachineConfig()-> + getStateConfig(HA_READY_ST)); + ASSERT_TRUE(state_cfg); + EXPECT_EQ(STATE_PAUSE_NEVER, state_cfg->getPausing()); + + ASSERT_NO_THROW(state_cfg = impl->getConfig()->getStateMachineConfig()-> + getStateConfig(HA_SYNCING_ST)); + ASSERT_TRUE(state_cfg); + EXPECT_EQ(STATE_PAUSE_NEVER, state_cfg->getPausing()); + + ASSERT_NO_THROW(state_cfg = impl->getConfig()->getStateMachineConfig()-> + getStateConfig(HA_TERMINATED_ST)); + ASSERT_TRUE(state_cfg); + EXPECT_EQ(STATE_PAUSE_NEVER, state_cfg->getPausing()); + + ASSERT_NO_THROW(state_cfg = impl->getConfig()->getStateMachineConfig()-> + getStateConfig(HA_WAITING_ST)); + ASSERT_TRUE(state_cfg); + EXPECT_EQ(STATE_PAUSE_NEVER, state_cfg->getPausing()); + + // Verify multi-threading default values. Default is 0 for the listener and client threads, but + // after MT is applied, HAImpl resolves them to the auto-detected values. + EXPECT_TRUE(impl->getConfig()->getEnableMultiThreading()); + EXPECT_TRUE(impl->getConfig()->getHttpDedicatedListener()); + EXPECT_EQ(hardware_threads_, impl->getConfig()->getHttpListenerThreads()); + EXPECT_EQ(hardware_threads_, impl->getConfig()->getHttpClientThreads()); +} + // This server name must not be empty. TEST_F(HAConfigTest, emptyServerName) { testInvalidConfig( diff --git a/src/hooks/dhcp/high_availability/tests/ha_relationship_mapper_unittest.cc b/src/hooks/dhcp/high_availability/tests/ha_relationship_mapper_unittest.cc new file mode 100644 index 0000000000..3ca3b06f20 --- /dev/null +++ b/src/hooks/dhcp/high_availability/tests/ha_relationship_mapper_unittest.cc @@ -0,0 +1,72 @@ +// 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 + +using namespace isc; +using namespace isc::ha; + +namespace { + +/// Tests associating objects with relationships and fetching them by the +/// partner names. +TEST(HARelationshipMapper, mapGet) { + HARelationshipMapper mapper; + + auto rel1 = HAConfig::create(); + auto rel2 = HAConfig::create(); + EXPECT_NO_THROW(mapper.map("server1", rel1)); + EXPECT_NO_THROW(mapper.map("server2", rel1)); + EXPECT_NO_THROW(mapper.map("server3", rel2)); + EXPECT_NO_THROW(mapper.map("server4", rel2)); + + EXPECT_EQ(rel1, mapper.get("server1")); + EXPECT_EQ(rel1, mapper.get("server2")); + EXPECT_EQ(rel2, mapper.get("server3")); + EXPECT_EQ(rel2, mapper.get("server4")); + + EXPECT_FALSE(mapper.get("server5")); +} + +/// Tests getting a sole mapped object. +TEST(HARelationshipMapper, mapGetSole) { + HARelationshipMapper mapper; + + auto rel1 = HAConfig::create(); + EXPECT_NO_THROW(mapper.map("server1", rel1)); + EXPECT_NO_THROW(mapper.map("server2", rel1)); + + EXPECT_EQ(rel1, mapper.get()); +} + +/// Tests that getting a sole mapped object fails when there are multiple. +TEST(HARelationshipMapper, multipleMappingsGetError) { + HARelationshipMapper mapper; + + auto rel1 = HAConfig::create(); + auto rel2 = HAConfig::create(); + EXPECT_NO_THROW(mapper.map("server1", rel1)); + EXPECT_NO_THROW(mapper.map("server2", rel2)); + + EXPECT_THROW(mapper.get(), InvalidOperation); +} + +/// Tests that the same server can't be associated with many relationships. +TEST(HARelationshipMapper, existingMappingError) { + HARelationshipMapper mapper; + + auto rel1 = HAConfig::create(); + auto rel2 = HAConfig::create(); + EXPECT_NO_THROW(mapper.map("server1", rel1)); + EXPECT_THROW(mapper.map("server1", rel2), InvalidOperation); +} + +} // end of anonymous 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 f0c286ea83..34b79c4420 100644 --- a/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc +++ b/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc @@ -5025,7 +5025,9 @@ TEST_F(HAServiceTest, processSyncCompleteNotify) { EXPECT_NO_THROW(service.transition(HA_PARTNER_DOWN_ST, HAService::NOP_EVT)); // Simulate disabling the DHCP service for synchronization. - EXPECT_NO_THROW(service.network_state_->disableService(NetworkState::Origin::HA_COMMAND)); + if (service.network_state_->isServiceEnabled()) { + EXPECT_NO_THROW(service.network_state_->disableService(NetworkState::Origin::HA_COMMAND)); + } ConstElementPtr rsp; EXPECT_NO_THROW(rsp = service.processSyncCompleteNotify()); @@ -5535,10 +5537,14 @@ public: // Also, let's preset the DHCP server state to the opposite of the expected // state. if (dhcp_enabled) { - service_->network_state_->disableService(NetworkState::Origin::HA_COMMAND); + if (service_->network_state_->isServiceEnabled()) { + service_->network_state_->disableService(NetworkState::Origin::HA_COMMAND); + } } else { - service_->network_state_->enableService(NetworkState::Origin::HA_COMMAND); + if (!service_->network_state_->isServiceEnabled()) { + service_->network_state_->enableService(NetworkState::Origin::HA_COMMAND); + } } // Transition to the desired state.