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
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
-// 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
#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>
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:
/// @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
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
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
--- /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_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
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);
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
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(
--- /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 <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
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());
// 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.