]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3106] Allow multiple HA configs
authorMarcin Siodelski <marcin@isc.org>
Tue, 10 Oct 2023 09:39:41 +0000 (11:39 +0200)
committerMarcin Siodelski <marcin@isc.org>
Wed, 29 Nov 2023 19:58:55 +0000 (20:58 +0100)
src/hooks/dhcp/high_availability/Makefile.am
src/hooks/dhcp/high_availability/ha_config.cc
src/hooks/dhcp/high_availability/ha_config.h
src/hooks/dhcp/high_availability/ha_config_parser.cc
src/hooks/dhcp/high_availability/ha_relationship_mapper.h [new file with mode: 0644]
src/hooks/dhcp/high_availability/ha_service.cc
src/hooks/dhcp/high_availability/tests/Makefile.am
src/hooks/dhcp/high_availability/tests/ha_config_unittest.cc
src/hooks/dhcp/high_availability/tests/ha_relationship_mapper_unittest.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc

index 5d7a2689e904554d2bfc8eb9600e19482cafd8b1..cd6e1ef1b6d5aa6f61ac68864847811656dd5ca3 100644 (file)
@@ -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
index e96bb52c1116afcdc9d6b1ceef96e3e585ca3782..dfe94b38fd9dd5c0514b675d8d010592c2f2a4ee 100644 (file)
@@ -171,6 +171,11 @@ HAConfig::HAConfig()
       state_machine_(new StateMachineConfig()) {
 }
 
+HAConfigPtr
+HAConfig::create() {
+    return (boost::make_shared<HAConfig>());
+}
+
 HAConfig::PeerConfigPtr
 HAConfig::selectNextPeerConfig(const std::string& name) {
     // Check if there is a configuration for this server name already. We can't
index 13f9f300da2de744d73db239294625b6687323de..0e25fd00e30529a02804880139aeb0c44558e9df 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2018-2022 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2018-2023 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -7,6 +7,7 @@
 #ifndef HA_CONFIG_H
 #define HA_CONFIG_H
 
+#include <ha_relationship_mapper.h>
 #include <asiolink/crypto_tls.h>
 #include <exceptions/exceptions.h>
 #include <http/basic_auth.h>
@@ -29,6 +30,14 @@ public:
         isc::Exception(file, line, what) { };
 };
 
+class HAConfig;
+
+/// @brief Pointer to the High Availability configuration structure.
+typedef boost::shared_ptr<HAConfig> HAConfigPtr;
+
+/// @brief Pointer to an object mapping HAConfig to relationships.
+typedef boost::shared_ptr<HARelationshipMapper<HAConfig>> 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<HAConfig> HAConfigPtr;
-
 } // end of namespace isc::ha
 } // end of namespace isc
 
index 0dcad36e6c4ec3cd7ee079349a18503eb7759cc7..25ae7681ad9e6cdebb4fead5bd9627c22bdfddd5 100644 (file)
@@ -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 (file)
index 0000000..2f9c7cf
--- /dev/null
@@ -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 <config.h>
+
+#include <exceptions/exceptions.h>
+#include <boost/shared_ptr.hpp>
+#include <unordered_map>
+#include <vector>
+
+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<typename MappedType>
+class HARelationshipMapper {
+public:
+
+    /// @brief A pointer to the held object type.
+    typedef boost::shared_ptr<MappedType> 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<MappedTypePtr>& getAll() const {
+        return (vector_);
+    }
+
+private:
+
+    /// Key-to-object mappings.
+    std::unordered_map<std::string, MappedTypePtr> mapping_;
+
+    /// A vector of unique objects in the order in which they were mapped.
+    std::vector<MappedTypePtr> vector_;
+};
+
+} // end of namespace isc::ha
+} // end of namespace isc
+
+#endif // HA_RELATIONSHIP_MAPPER_H
index 2521197d9deb9ec43341f296843d55859d6ab60d..e6abe0dc19f615ea693e7a633ddc447fc89bc1b7 100644 (file)
@@ -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);
index f99f8ebebbeeca5bd3d905b49cc3c63a3654f6cb..35997f559c50af0b437c6212b4d81b74893dc688 100644 (file)
@@ -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
index f8e6940ec9bf5ed2bf953675cd6b84bcc350aeaa..d1f6ad0182c8bde53ca713dbb628c483f46084bc 100644 (file)
@@ -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 (file)
index 0000000..3ca3b06
--- /dev/null
@@ -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 <config.h>
+
+#include <ha_config.h>
+#include <ha_relationship_mapper.h>
+#include <exceptions/exceptions.h>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::ha;
+
+namespace {
+
+/// Tests associating objects with relationships and fetching them by the
+/// partner names.
+TEST(HARelationshipMapper, mapGet) {
+    HARelationshipMapper<HAConfig> 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<HAConfig> 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<HAConfig> 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<HAConfig> 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
index f0c286ea83745045cf22942207cf4f8e53afc854..34b79c442096ec9bb5012440a587ac6689c4bcb4 100644 (file)
@@ -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.