Commit 997e43ad authored by Marcin Siodelski's avatar Marcin Siodelski

[5645] Moved HA hook library from premium repo to main repo.

parent 4dfd330a
......@@ -5,12 +5,12 @@
Primary developers:
- Tomek Mrugalski (lead developer: DHCPv4, DHCPv6 components, prefix
delegation, memfile, database interface, core libdhcp++,
host reservation, MAC extraction in DHCPv6, statistics manager,
kea-shell)
host reservation, MAC extraction in DHCPv6,
statistics manager, kea-shell)
- Stephen Morris (Hooks, MySQL)
- Marcin Siodelski (DHCPv4, DHCPv6 components, options handling, perfdhcp,
host reservation, lease file cleanup, lease expiration,
control agent, shared networks)
host reservation, lease file cleanup, lease expiration,
control agent, shared networks, high availability)
- Thomas Markwalder (DDNS, user_chk)
- Jeremy C. Reed (documentation, build system, testing, release engineering)
- Wlodek Wencel (testing, release engineering)
......
......@@ -1409,6 +1409,8 @@ AC_CONFIG_FILES([Makefile
src/bin/shell/tests/shell_unittest.py
src/hooks/Makefile
src/hooks/dhcp/Makefile
src/hooks/dhcp/high_availability/Makefile
src/hooks/dhcp/high_availability/tests/Makefile
src/hooks/dhcp/lease_cmds/Makefile
src/hooks/dhcp/lease_cmds/tests/Makefile
src/hooks/dhcp/user_chk/Makefile
......
SUBDIRS = user_chk lease_cmds stat_cmds
SUBDIRS = high_availability lease_cmds stat_cmds user_chk
/ha_messages.cc
/ha_messages.h
/s-messages
/html
This diff is collapsed.
# This is a doxygen configuration for generating XML output as well as HTML.
#
# Inherit everything from our default Doxyfile except GENERATE_XML, which
# will be reset to YES
@INCLUDE = Doxyfile
GENERATE_XML = YES
SUBDIRS = . tests
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CXXFLAGS = $(KEA_CXXFLAGS)
# Define rule to build logging source files from message file
ha_messages.h ha_messages.cc: s-messages
s-messages: ha_messages.mes
$(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/hooks/dhcp/high_availability/ha_messages.mes
touch $@
# Tell automake that the message files are built as part of the build process
# (so that they are built before the main library is built).
BUILT_SOURCES = ha_messages.h ha_messages.cc
# Ensure that the message file is included in the distribution
EXTRA_DIST = ha_messages.mes
# Get rid of generated message files on a clean
CLEANFILES = *.gcno *.gcda ha_messages.h ha_messages.cc s-messages
# convenience archive
noinst_LTLIBRARIES = libha.la
libha_la_SOURCES = command_creator.cc command_creator.h
libha_la_SOURCES += communication_state.cc communication_state.h
libha_la_SOURCES += ha_callouts.cc
libha_la_SOURCES += ha_config.cc ha_config.h
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_server_type.h
libha_la_SOURCES += ha_service.cc ha_service.h
libha_la_SOURCES += ha_service_states.h
libha_la_SOURCES += query_filter.cc query_filter.h
libha_la_SOURCES += version.cc
nodist_libha_la_SOURCES = ha_messages.cc ha_messages.h
libha_la_CXXFLAGS = $(AM_CXXFLAGS)
libha_la_CPPFLAGS = $(AM_CPPFLAGS)
# install the shared object into $(libdir)/hooks
lib_hooksdir = $(libdir)/hooks
lib_hooks_LTLIBRARIES = libdhcp_ha.la
libdhcp_ha_la_SOURCES =
libdhcp_ha_la_LDFLAGS = $(AM_LDFLAGS)
libdhcp_ha_la_LDFLAGS += -avoid-version -export-dynamic -module
libdhcp_ha_la_LIBADD = libha.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/eval/libkea-eval.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/stats/libkea-stats.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/http/libkea-http.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la
libdhcp_ha_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
EXTRA_DIST += ha.dox Doxyfile Doxyfile-xml
devel:
mkdir -p html
(cat Doxyfile; echo PROJECT_NUMBER=$(PACKAGE_VERSION)) | doxygen - > html/doxygen.log 2> html/doxygen-error.log
echo `grep -i ": warning:" html/doxygen-error.log | wc -l` warnings/errors detected.
clean-local:
rm -rf html
// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the End User License
// Agreement. See COPYING file in the premium/ directory.
#include <config.h>
#include <command_creator.h>
#include <cc/command_interpreter.h>
#include <exceptions/exceptions.h>
#include <boost/pointer_cast.hpp>
using namespace isc::data;
using namespace isc::dhcp;
namespace isc {
namespace ha {
ConstElementPtr
CommandCreator::createDHCPDisable(const unsigned int max_period,
const HAServerType& server_type) {
ElementPtr args;
// max-period is optional. A value of 0 means that it is not specified.
if (max_period > 0) {
args = Element::createMap();
args->set("max-period", Element::create(static_cast<long int>(max_period)));
}
ConstElementPtr command = config::createCommand("dhcp-disable", args);
insertService(command, server_type);
return (command);
}
ConstElementPtr
CommandCreator::createDHCPEnable(const HAServerType& server_type) {
ConstElementPtr command = config::createCommand("dhcp-enable");
insertService(command, server_type);
return (command);
}
ConstElementPtr
CommandCreator::createHeartbeat(const HAServerType& server_type) {
ConstElementPtr command = config::createCommand("ha-heartbeat");
insertService(command, server_type);
return (command);
}
ConstElementPtr
CommandCreator::createLease4Update(const Lease4& lease4) {
ElementPtr lease_as_json = lease4.toElement();
insertLeaseExpireTime(lease_as_json);
lease_as_json->set("force-create", Element::create(true));
ConstElementPtr command = config::createCommand("lease4-update", lease_as_json);
insertService(command, HAServerType::DHCPv4);
return (command);
}
ConstElementPtr
CommandCreator::createLease4Delete(const Lease4& lease4) {
ElementPtr lease_as_json = lease4.toElement();
insertLeaseExpireTime(lease_as_json);
ConstElementPtr command = config::createCommand("lease4-del", lease_as_json);
insertService(command, HAServerType::DHCPv4);
return (command);
}
ConstElementPtr
CommandCreator::createLease4GetAll() {
ConstElementPtr command = config::createCommand("lease4-get-all");
insertService(command, HAServerType::DHCPv4);
return (command);
}
ConstElementPtr
CommandCreator::createLease6Update(const Lease6& lease6) {
ElementPtr lease_as_json = lease6.toElement();
insertLeaseExpireTime(lease_as_json);
lease_as_json->set("force-create", Element::create(true));
ConstElementPtr command = config::createCommand("lease6-update", lease_as_json);
insertService(command, HAServerType::DHCPv6);
return (command);
}
ConstElementPtr
CommandCreator::createLease6Delete(const Lease6& lease6) {
ElementPtr lease_as_json = lease6.toElement();
insertLeaseExpireTime(lease_as_json);
ConstElementPtr command = config::createCommand("lease6-del", lease_as_json);
insertService(command, HAServerType::DHCPv6);
return (command);
}
ConstElementPtr
CommandCreator::createLease6GetAll() {
ConstElementPtr command = config::createCommand("lease6-get-all");
insertService(command, HAServerType::DHCPv6);
return (command);
}
void
CommandCreator::insertLeaseExpireTime(ElementPtr& lease) {
if ((lease->getType() != Element::map) ||
(!lease->contains("cltt") || (lease->get("cltt")->getType() != Element::integer) ||
(!lease->contains("valid-lft") ||
(lease->get("valid-lft")->getType() != Element::integer)))) {
isc_throw(Unexpected, "invalid lease format");
}
int64_t cltt = lease->get("cltt")->intValue();
int64_t valid_lifetime = lease->get("valid-lft")->intValue();
int64_t expire = cltt + valid_lifetime;
lease->set("expire", Element::create(expire));
lease->remove("cltt");
}
void
CommandCreator::insertService(ConstElementPtr& command,
const HAServerType& server_type) {
ElementPtr service = Element::createList();
const std::string service_name = (server_type == HAServerType::DHCPv4 ? "dhcp4" : "dhcp6");
service->add(Element::create(service_name));
// We have no better way of setting a new element here than
// doing const pointer cast. That's another reason why this
// functionality could be moved to the core code. We don't
// do it however, because we want to minimize concurrent
// code changes in the premium and core Kea repos.
(boost::const_pointer_cast<Element>(command))->set("service", service);
}
} // end of namespace ha
} // end of namespace isc
// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the End User License
// Agreement. See COPYING file in the premium/ directory.
#ifndef HA_COMMAND_CREATOR_H
#define HA_COMMAND_CREATOR_H
#include <ha_server_type.h>
#include <cc/data.h>
#include <dhcpsrv/lease.h>
#include <string>
namespace isc {
namespace ha {
/// @brief Holds a collection of functions which generate commands
/// used for High Availability.
class CommandCreator {
public:
/// @brief Creates dhcp-disable command for DHCP server.
///
/// @return Pointer to the JSON representation of the command.
static data::ConstElementPtr
createDHCPDisable(const unsigned int max_period,
const HAServerType& server_type);
/// @brief Creates dhcp-enable command for DHCP server.
///
/// @return Pointer to the JSON representation of the command.
static data::ConstElementPtr
createDHCPEnable(const HAServerType& server_type);
/// @brief Creates ha-heartbeat command for DHCP server.
///
/// @return Pointer to the JSON representation of the command.
static data::ConstElementPtr
createHeartbeat(const HAServerType& server_type);
/// @brief Creates lease4-update command.
///
/// It adds "force-create" parameter to the lease information to force
/// the remote server to create the lease if it doesn't exist in its
/// lease database.
///
/// @param lease4 Reference to a lease for which the command should
/// be created.
///
/// @return Pointer to the JSON representation of the command.
static data::ConstElementPtr
createLease4Update(const dhcp::Lease4& lease4);
/// @brief Creates lease4-del command.
///
/// @param lease4 Reference to a lease for which the command should
/// be created.
///
/// @return Pointer to the JSON representation of the command.
static data::ConstElementPtr
createLease4Delete(const dhcp::Lease4& lease4);
/// @brief Creates lease4-get-all command.
///
/// @return Pointer to the JSON representation of the command.
static data::ConstElementPtr
createLease4GetAll();
/// @brief Creates lease6-update command.
///
/// It adds "force-create" parameter to the lease information to force
/// the remote server to create the lease if it doesn't exist in its
/// lease database.
///
/// @param lease6 Reference to a lease for which the command should
/// be created.
///
/// @return Pointer to the JSON representation of the command.
static data::ConstElementPtr
createLease6Update(const dhcp::Lease6& lease6);
/// @brief Creates lease6-del command.
///
/// @param lease6 Reference to a lease for which the command should
/// be created.
///
/// @return Pointer to the JSON representation of the command.
static data::ConstElementPtr
createLease6Delete(const dhcp::Lease6& lease6);
/// @brief Creates lease6-get-all command.
///
/// @return Pointer to the JSON representation of the command.
static data::ConstElementPtr
createLease6GetAll();
private:
/// @brief Replaces "cltt" with "expire" value within the lease.
///
/// The "lease_cmds" hooks library expects "expire" time to be provided
/// for a lease rather than "cltt". If the "expire" is not provided
/// it will use the current time for a cltt. We want to make sure that
/// the lease is inserted into the lease database untouched.
/// Hence, this method is used to replace "cltt" with "expire" time in
/// the lease.
///
/// @param lease in the JSON format created using @c Lease::toElement
/// method.
static void insertLeaseExpireTime(data::ElementPtr& lease);
/// @brief Sets "service" parameter for the command.
///
/// Commands generated by the HA hooks library are always sent to
/// DHCPv4 or DHCPv6 server via Control Agent. The Control Agent
/// requires a "service" parameter which provides the list of servers
/// to which the command should be forwarded. In our case, we always
/// send commands to a single server so this method appends a single
/// element list to the command.
///
/// @todo We should consider moving this functionality to the main
/// Kea code.
///
/// @param [out] command command to which the service parameter must
/// be inserted.
/// @param server_type DHCP server type, i.e. DHCPv4 or DHCPv6.
///
/// @return Pointer to the command with service parameter inserted.
static void
insertService(data::ConstElementPtr& command,
const HAServerType& server_type);
};
} // end of namespace ha
} // end of namespace isc
#endif
// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the End User License
// Agreement. See COPYING file in the premium/ directory.
#include <config.h>
#include <communication_state.h>
#include <ha_service_states.h>
#include <exceptions/exceptions.h>
#include <dhcp/dhcp4.h>
#include <dhcp/dhcp6.h>
#include <dhcp/option_int.h>
#include <dhcp/pkt4.h>
#include <dhcp/pkt6.h>
#include <http/date_time.h>
#include <boost/bind.hpp>
#include <boost/pointer_cast.hpp>
#include <sstream>
#include <utility>
using namespace isc::asiolink;
using namespace isc::dhcp;
using namespace isc::http;
using namespace boost::posix_time;
namespace {
/// @brief Warning is issued if the clock skew exceeds this value.
constexpr long WARN_CLOCK_SKEW = 30;
/// @brief HA service terminates if the clock skew exceeds this value.
constexpr long TERM_CLOCK_SKEW = 60;
/// @brief Minimum time between two consecutive clock skew warnings.
constexpr long MIN_TIME_SINCE_CLOCK_SKEW_WARN = 60;
}
namespace isc {
namespace ha {
CommunicationState::CommunicationState(const IOServicePtr& io_service,
const HAConfigPtr& config)
: io_service_(io_service), config_(config), timer_(), interval_(0),
poke_time_(boost::posix_time::microsec_clock::universal_time()),
heartbeat_impl_(0), partner_state_(-1), clock_skew_(0, 0, 0, 0),
last_clock_skew_warn_() {
}
CommunicationState::~CommunicationState() {
stopHeartbeat();
}
void
CommunicationState::setPartnerState(const std::string& state) {
if (state == "hot-standby") {
partner_state_ = HA_HOT_STANDBY_ST;
} else if (state == "load-balancing") {
partner_state_ = HA_LOAD_BALANCING_ST;
} else if (state == "partner-down") {
partner_state_ = HA_PARTNER_DOWN_ST;
} else if (state == "ready") {
partner_state_ = HA_READY_ST;
} else if (state == "syncing") {
partner_state_ = HA_SYNCING_ST;
} else if (state == "terminated") {
partner_state_ = HA_TERMINATED_ST;
} else if (state == "waiting") {
partner_state_ = HA_WAITING_ST;
} else if (state == "unavailable") {
partner_state_ = HA_UNAVAILABLE_ST;
} else {
isc_throw(BadValue, "unsupported HA partner state returned "
<< state);
}
}
void
CommunicationState::startHeartbeat(const long interval,
const boost::function<void()>& heartbeat_impl) {
startHeartbeatInternal(interval, heartbeat_impl);
}
void
CommunicationState::startHeartbeatInternal(const long interval,
const boost::function<void()>& heartbeat_impl) {
bool settings_modified = false;
// If we're setting the heartbeat for the first time, it should
// be non-null.
if (heartbeat_impl) {
settings_modified = true;
heartbeat_impl_ = heartbeat_impl;
} else if (!heartbeat_impl_) {
// The heartbeat is re-scheduled but we have no historic implementation
// pointer we could re-use. This is a programmatic issue.
isc_throw(BadValue, "unable to start heartbeat when pointer"
" to the heartbeat implementation is not specified");
}
// If we're setting the heartbeat for the first time, the interval
// should be greater than 0.
if (interval != 0) {
settings_modified |= (interval_ != interval);
interval_ = interval;
} else if (interval_ <= 0) {
// The heartbeat is re-scheduled but we have no historic interval
// which we could re-use. This is a programmatic issue.
heartbeat_impl_ = 0;
isc_throw(BadValue, "unable to start heartbeat when interval"
" for the heartbeat timer is not specified");
}
if (!timer_) {
timer_.reset(new IntervalTimer(*io_service_));
}
if (settings_modified) {
timer_->setup(heartbeat_impl_, interval_, IntervalTimer::ONE_SHOT);
}
}
void
CommunicationState::stopHeartbeat() {
if (timer_) {
timer_->cancel();
timer_.reset();
interval_ = 0;
heartbeat_impl_ = 0;
}
}
void
CommunicationState::poke() {
// Remember previous poke time.
boost::posix_time::ptime prev_poke_time = poke_time_;
// Set poke time to the current time.
poke_time_ = boost::posix_time::microsec_clock::universal_time();
// If we have been tracking the unanswered DHCP messages directed to the
// partner, we need to clear any gathered information because the connection
// seems to be (re)established.
clearUnackedClients();
if (timer_) {
// Check the duration since last poke. If it is less than a second, we don't
// want to reschedule the timer. The only case when the poke time duration is
// lower than 1s is when we're performing lease updates. In order to avoid the
// overhead of re-scheduling the timer too frequently we reschedule it only if the
// duration is 1s or more. This matches the time resolution for heartbeats.
boost::posix_time::time_duration duration_since_poke = poke_time_ - prev_poke_time;
if (duration_since_poke.total_seconds() > 0) {
// A poke causes the timer to be re-scheduled to prevent it
// from triggering a heartbeat shortly after confirming the
// connection is ok, based on the lease update or another
// command.
startHeartbeatInternal();
}
}
}
int64_t
CommunicationState::getDurationInMillisecs() const {
ptime now = boost::posix_time::microsec_clock::universal_time();
time_duration duration = now - poke_time_;
return (duration.total_milliseconds());
}
bool
CommunicationState::isCommunicationInterrupted() const {
return (getDurationInMillisecs() > config_->getMaxResponseDelay());
}
bool
CommunicationState::clockSkewShouldWarn() {
// First check if the clock skew is beyond the threshold.
if (isClockSkewGreater(WARN_CLOCK_SKEW)) {
// In order to prevent to frequent warnings we provide a gating mechanism
// which doesn't allow for issuing a warning earlier than 60 seconds after
// the previous one.
// Find the current time and the duration since last warning.
ptime now = boost::posix_time::microsec_clock::universal_time();
time_duration since_warn_duration = now - last_clock_skew_warn_;
// If the last warning was issued more than 60 seconds ago or it is a
// first warning, we need to update the last warning timestamp and return
// true to indicate that new warning should be issued.
if (last_clock_skew_warn_.is_not_a_date_time() ||
(since_warn_duration.total_seconds() > MIN_TIME_SINCE_CLOCK_SKEW_WARN)) {
last_clock_skew_warn_ = now;
return (true);
}
}
// The warning should not be issued.
return (false);
}
bool
CommunicationState::clockSkewShouldTerminate() const {
// Issue a warning if the clock skew is greater than 60s.
return (isClockSkewGreater(TERM_CLOCK_SKEW));
}
bool
CommunicationState::isClockSkewGreater(const long seconds) const {
return ((clock_skew_.total_seconds() > seconds) ||
(clock_skew_.total_seconds() < -seconds));
}
void
CommunicationState::setPartnerTime(const std::string& time_text) {
HttpDateTime partner_time = HttpDateTime().fromRfc1123(time_text);
HttpDateTime current_time = HttpDateTime();
clock_skew_ = partner_time.getPtime() - current_time.getPtime();
}
std::string
CommunicationState::logFormatClockSkew() const {
std::ostringstream s;
// If negative clock skew, the partner's time is behind our time.
if (clock_skew_.is_negative()) {
s << clock_skew_.invert_sign().total_seconds() << "s behind";
} else {
// Partner's time is ahead of ours.
s << clock_skew_.total_seconds() << "s ahead";
}
return (s.str());
}
CommunicationState4::CommunicationState4(const IOServicePtr& io_service,
const HAConfigPtr& config)
: CommunicationState(io_service, config), unacked_clients_() {
}
void
CommunicationState4::analyzeMessage(const boost::shared_ptr<dhcp::Pkt>& message) {
// The DHCP message must successfully cast to a Pkt4 object.
Pkt4Ptr msg = boost::dynamic_pointer_cast<Pkt4>(message);
if (!msg) {
isc_throw(BadValue, "DHCP message to be analyzed is not a DHCPv4 message");
}
// Check value of the "secs" field by comparing it with the configured
// threshold.
uint16_t secs = msg->getSecs();
// It was observed that some Windows clients may send swapped bytes in the
// "secs" field. When the second byte is 0 and the first byte is non-zero
// we consider bytes to be swapped and so we correct them.
if ((secs > 255) && ((secs & 0xFF) == 0)) {
secs = ((secs >> 8) | (secs << 8));
}
// Check the value of the "secs" field. If it is below the threshold there
// is nothing to do. The "secs" field holds a value in seconds, hence we
// have to multiple by 1000 to get a value in milliseconds.
if (secs * 1000 <= config_->getMaxAckDelay()) {
return;
}
// The "secs" value is above the threshold so we should count it as unacked
// request, but we will first have to check if there is such request already
// recorded.
auto existing_requests = unacked_clients_.equal_range(msg->getHWAddr()->hwaddr_);
// Client identifier will be stored together with the hardware address. It
// may remain empty if the client hasn't specified it.
std::vector<uint8_t> client_id;
OptionPtr opt_client_id = msg->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
if (opt_client_id) {
client_id = opt_client_id->getData();
}
// Iterate over the requests we found so far and see if we have a match with
// the client identifier (this includes empty client identifiers).
for (auto r = existing_requests.first; r != existing_requests.second; ++r) {
if (r->second == client_id) {
// There is a match so we have already recorded this client as
// unacked.
return;
}