summaryrefslogtreecommitdiffstats
path: root/src/hooks/dhcp/high_availability/ha_impl.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/hooks/dhcp/high_availability/ha_impl.cc')
-rw-r--r--src/hooks/dhcp/high_availability/ha_impl.cc500
1 files changed, 500 insertions, 0 deletions
diff --git a/src/hooks/dhcp/high_availability/ha_impl.cc b/src/hooks/dhcp/high_availability/ha_impl.cc
new file mode 100644
index 0000000..1c8d811
--- /dev/null
+++ b/src/hooks/dhcp/high_availability/ha_impl.cc
@@ -0,0 +1,500 @@
+// Copyright (C) 2018-2021 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_parser.h>
+#include <ha_impl.h>
+#include <ha_log.h>
+#include <asiolink/io_service.h>
+#include <cc/data.h>
+#include <cc/command_interpreter.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt6.h>
+#include <dhcpsrv/lease.h>
+#include <stats/stats_mgr.h>
+
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::hooks;
+using namespace isc::log;
+
+namespace isc {
+namespace ha {
+
+HAImpl::HAImpl()
+ : config_(new HAConfig()) {
+}
+
+void
+HAImpl::configure(const ConstElementPtr& input_config) {
+ HAConfigParser parser;
+ parser.parse(config_, input_config);
+}
+
+void
+HAImpl::startService(const IOServicePtr& io_service,
+ const NetworkStatePtr& network_state,
+ const HAServerType& server_type) {
+ // Create the HA service and crank up the state machine.
+ service_ = boost::make_shared<HAService>(io_service, network_state,
+ config_, server_type);
+ // Schedule a start of the services. This ensures we begin after
+ // the dust has settled and Kea MT mode has been firmly established.
+ io_service->post([&]() { service_->startClientAndListener(); } );
+}
+
+HAImpl::~HAImpl() {
+ if (service_) {
+ // Shut down the services explicitly, we need finer control
+ // than relying on destruction order.
+ service_->stopClientAndListener();
+ }
+}
+
+void
+HAImpl::buffer4Receive(hooks::CalloutHandle& callout_handle) {
+ Pkt4Ptr query4;
+ callout_handle.getArgument("query4", query4);
+
+ /// @todo Add unit tests to verify the behavior for different
+ /// malformed packets.
+ try {
+ // We have to unpack the query to get access into HW address which is
+ // used to load balance the packet.
+ if (callout_handle.getStatus() != CalloutHandle::NEXT_STEP_SKIP) {
+ query4->unpack();
+ }
+
+ } catch (const SkipRemainingOptionsError& ex) {
+ // An option failed to unpack but we are to attempt to process it
+ // anyway. Log it and let's hope for the best.
+ LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC,
+ HA_BUFFER4_RECEIVE_PACKET_OPTIONS_SKIPPED)
+ .arg(ex.what());
+
+ } catch (const std::exception& ex) {
+ // Packet parsing failed. Drop the packet.
+ LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_BUFFER4_RECEIVE_UNPACK_FAILED)
+ .arg(query4->getRemoteAddr().toText())
+ .arg(query4->getLocalAddr().toText())
+ .arg(query4->getIface())
+ .arg(ex.what());
+
+ // Increase the statistics of parse failures and dropped packets.
+ isc::stats::StatsMgr::instance().addValue("pkt4-parse-failed",
+ static_cast<int64_t>(1));
+ isc::stats::StatsMgr::instance().addValue("pkt4-receive-drop",
+ static_cast<int64_t>(1));
+
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+ return;
+ }
+
+ // Check if we should process this query. If not, drop it.
+ if (!service_->inScope(query4)) {
+ LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_BUFFER4_RECEIVE_NOT_FOR_US)
+ .arg(query4->getLabel());
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+
+ } else {
+ // We have successfully parsed the query so we have to signal
+ // to the server that it must not parse it.
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+ }
+}
+
+void
+HAImpl::leases4Committed(CalloutHandle& callout_handle) {
+ // If the hook library is configured to not send lease updates to the
+ // partner, there is nothing to do because this whole callout is
+ // currently about sending lease updates.
+ if (!config_->amSendingLeaseUpdates()) {
+ // No need to log it, because it was already logged when configuration
+ // was applied.
+ return;
+ }
+
+ Pkt4Ptr query4;
+ Lease4CollectionPtr leases4;
+ Lease4CollectionPtr deleted_leases4;
+
+ // Get all arguments available for the leases4_committed hook point.
+ // If any of these arguments is not available this is a programmatic
+ // error. An exception will be thrown which will be caught by the
+ // caller and logged.
+ callout_handle.getArgument("query4", query4);
+
+ callout_handle.getArgument("leases4", leases4);
+ callout_handle.getArgument("deleted_leases4", deleted_leases4);
+
+ // In some cases we may have no leases, e.g. DHCPNAK.
+ if (leases4->empty() && deleted_leases4->empty()) {
+ LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_LEASES4_COMMITTED_NOTHING_TO_UPDATE)
+ .arg(query4->getLabel());
+ return;
+ }
+
+ // Get the parking lot for this hook point. We're going to remember this
+ // pointer until we unpark the packet.
+ ParkingLotHandlePtr parking_lot = callout_handle.getParkingLotHandlePtr();
+
+ // Create a reference to the parked packet. This signals that we have a
+ // stake in unparking it.
+ parking_lot->reference(query4);
+
+ // Asynchronously send lease updates. In some cases no updates will be sent,
+ // e.g. when this server is in the partner-down state and there are no backup
+ // servers. In those cases we simply return without parking the DHCP query.
+ // The response will be sent to the client immediately.
+ try {
+ if (service_->asyncSendLeaseUpdates(query4, leases4, deleted_leases4, parking_lot) == 0) {
+ // Dereference the parked packet. This releases our stake in it.
+ parking_lot->dereference(query4);
+ return;
+ }
+ } catch (...) {
+ // Make sure we dereference.
+ parking_lot->dereference(query4);
+ throw;
+ }
+
+ // The callout returns this status code to indicate to the server that it
+ // should leave the packet parked. It will be parked until each hook
+ // library with a reference, unparks the packet.
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_PARK);
+}
+
+void
+HAImpl::buffer6Receive(hooks::CalloutHandle& callout_handle) {
+ Pkt6Ptr query6;
+ callout_handle.getArgument("query6", query6);
+
+ /// @todo Add unit tests to verify the behavior for different
+ /// malformed packets.
+ try {
+ // We have to unpack the query to get access into DUID which is
+ // used to load balance the packet.
+ if (callout_handle.getStatus() != CalloutHandle::NEXT_STEP_SKIP) {
+ query6->unpack();
+ }
+
+ } catch (const SkipRemainingOptionsError& ex) {
+ // An option failed to unpack but we are to attempt to process it
+ // anyway. Log it and let's hope for the best.
+ LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC,
+ HA_BUFFER6_RECEIVE_PACKET_OPTIONS_SKIPPED)
+ .arg(ex.what());
+
+ } catch (const std::exception& ex) {
+ // Packet parsing failed. Drop the packet.
+ LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_BUFFER6_RECEIVE_UNPACK_FAILED)
+ .arg(query6->getRemoteAddr().toText())
+ .arg(query6->getLocalAddr().toText())
+ .arg(query6->getIface())
+ .arg(ex.what());
+
+ // Increase the statistics of parse failures and dropped packets.
+ isc::stats::StatsMgr::instance().addValue("pkt6-parse-failed",
+ static_cast<int64_t>(1));
+ isc::stats::StatsMgr::instance().addValue("pkt6-receive-drop",
+ static_cast<int64_t>(1));
+
+
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+ return;
+ }
+
+ // Check if we should process this query. If not, drop it.
+ if (!service_->inScope(query6)) {
+ LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_BUFFER6_RECEIVE_NOT_FOR_US)
+ .arg(query6->getLabel());
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_DROP);
+
+ } else {
+ // We have successfully parsed the query so we have to signal
+ // to the server that it must not parse it.
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
+ }
+}
+
+void
+HAImpl::leases6Committed(CalloutHandle& callout_handle) {
+ // If the hook library is configured to not send lease updates to the
+ // partner, there is nothing to do because this whole callout is
+ // currently about sending lease updates.
+ if (!config_->amSendingLeaseUpdates()) {
+ // No need to log it, because it was already logged when configuration
+ // was applied.
+ return;
+ }
+
+ Pkt6Ptr query6;
+ Lease6CollectionPtr leases6;
+ Lease6CollectionPtr deleted_leases6;
+
+ // Get all arguments available for the leases6_committed hook point.
+ // If any of these arguments is not available this is a programmatic
+ // error. An exception will be thrown which will be caught by the
+ // caller and logged.
+ callout_handle.getArgument("query6", query6);
+
+ callout_handle.getArgument("leases6", leases6);
+ callout_handle.getArgument("deleted_leases6", deleted_leases6);
+
+ // In some cases we may have no leases.
+ if (leases6->empty() && deleted_leases6->empty()) {
+ LOG_DEBUG(ha_logger, DBGLVL_TRACE_BASIC, HA_LEASES6_COMMITTED_NOTHING_TO_UPDATE)
+ .arg(query6->getLabel());
+ return;
+ }
+
+ // Get the parking lot for this hook point. We're going to remember this
+ // pointer until we unpark the packet.
+ ParkingLotHandlePtr parking_lot = callout_handle.getParkingLotHandlePtr();
+
+ // Create a reference to the parked packet. This signals that we have a
+ // stake in unparking it.
+ parking_lot->reference(query6);
+
+ // Asynchronously send lease updates. In some cases no updates will be sent,
+ // e.g. when this server is in the partner-down state and there are no backup
+ // servers. In those cases we simply return without parking the DHCP query.
+ // The response will be sent to the client immediately.
+ try {
+ if (service_->asyncSendLeaseUpdates(query6, leases6, deleted_leases6, parking_lot) == 0) {
+ // Dereference the parked packet. This releases our stake in it.
+ parking_lot->dereference(query6);
+ return;
+ }
+ } catch (...) {
+ // Make sure we dereference.
+ parking_lot->dereference(query6);
+ throw;
+ }
+
+ // The callout returns this status code to indicate to the server that it
+ // should leave the packet parked. It will be unparked until each hook
+ // library with a reference, unparks the packet.
+ callout_handle.setStatus(CalloutHandle::NEXT_STEP_PARK);
+}
+
+void
+HAImpl::commandProcessed(hooks::CalloutHandle& callout_handle) {
+ std::string command_name;
+ callout_handle.getArgument("name", command_name);
+ if (command_name == "status-get") {
+ // Get the response.
+ ConstElementPtr response;
+ callout_handle.getArgument("response", response);
+ if (!response || (response->getType() != Element::map)) {
+ return;
+ }
+ // Get the arguments item from the response.
+ ConstElementPtr resp_args = response->get("arguments");
+ if (!resp_args || (resp_args->getType() != Element::map)) {
+ return;
+ }
+ // Add the ha servers info to arguments.
+ ElementPtr mutable_resp_args =
+ boost::const_pointer_cast<Element>(resp_args);
+
+ /// @todo Today we support only one HA relationship per Kea server.
+ /// In the future there will be more of them. Therefore we enclose
+ /// our sole relationship in a list.
+ auto ha_relationships = Element::createList();
+ auto ha_relationship = Element::createMap();
+ ConstElementPtr ha_servers = service_->processStatusGet();
+ ha_relationship->set("ha-servers", ha_servers);
+ ha_relationship->set("ha-mode", Element::create(HAConfig::HAModeToString(config_->getHAMode())));
+ ha_relationships->add(ha_relationship);
+ mutable_resp_args->set("high-availability", ha_relationships);
+ }
+}
+
+void
+HAImpl::heartbeatHandler(CalloutHandle& callout_handle) {
+ ConstElementPtr response = service_->processHeartbeat();
+ callout_handle.setArgument("response", response);
+}
+
+void
+HAImpl::synchronizeHandler(hooks::CalloutHandle& callout_handle) {
+ // Command must always be provided.
+ ConstElementPtr command;
+ callout_handle.getArgument("command", command);
+
+ // Retrieve arguments.
+ ConstElementPtr args;
+ static_cast<void>(parseCommand(args, command));
+
+ ConstElementPtr server_name;
+ unsigned int max_period_value = 0;
+
+ try {
+ // Arguments are required for the ha-sync command.
+ if (!args) {
+ isc_throw(BadValue, "arguments not found in the 'ha-sync' command");
+ }
+
+ // Arguments must be a map.
+ if (args->getType() != Element::map) {
+ isc_throw(BadValue, "arguments in the 'ha-sync' command are not a map");
+ }
+
+ // server-name is mandatory. Otherwise how can we know the server to
+ // communicate with.
+ server_name = args->get("server-name");
+ if (!server_name) {
+ isc_throw(BadValue, "'server-name' is mandatory for the 'ha-sync' command");
+ }
+
+ // server-name must obviously be a string.
+ if (server_name->getType() != Element::string) {
+ isc_throw(BadValue, "'server-name' must be a string in the 'ha-sync' command");
+ }
+
+ // max-period is optional. In fact it is optional for dhcp-disable command too.
+ ConstElementPtr max_period = args->get("max-period");
+ if (max_period) {
+ // If it is specified, it must be a positive integer.
+ if ((max_period->getType() != Element::integer) ||
+ (max_period->intValue() <= 0)) {
+ isc_throw(BadValue, "'max-period' must be a positive integer in the 'ha-sync' command");
+ }
+
+ max_period_value = static_cast<unsigned int>(max_period->intValue());
+ }
+
+ } catch (const std::exception& ex) {
+ // There was an error while parsing command arguments. Return an error status
+ // code to notify the user.
+ ConstElementPtr response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
+ callout_handle.setArgument("response", response);
+ return;
+ }
+
+ // Command parsing was successful, so let's process the command.
+ ConstElementPtr response = service_->processSynchronize(server_name->stringValue(),
+ max_period_value);
+ callout_handle.setArgument("response", response);
+}
+
+void
+HAImpl::scopesHandler(hooks::CalloutHandle& callout_handle) {
+ // Command must always be provided.
+ ConstElementPtr command;
+ callout_handle.getArgument("command", command);
+
+ // Retrieve arguments.
+ ConstElementPtr args;
+ static_cast<void>(parseCommand(args, command));
+
+ std::vector<std::string> scopes_vector;
+
+ try {
+ // Arguments must be present.
+ if (!args) {
+ isc_throw(BadValue, "arguments not found in the 'ha-scopes' command");
+ }
+
+ // Arguments must be a map.
+ if (args->getType() != Element::map) {
+ isc_throw(BadValue, "arguments in the 'ha-scopes' command are not a map");
+ }
+
+ // scopes argument is mandatory.
+ ConstElementPtr scopes = args->get("scopes");
+ if (!scopes) {
+ isc_throw(BadValue, "'scopes' is mandatory for the 'ha-scopes' command");
+ }
+
+ // It contains a list of scope names.
+ if (scopes->getType() != Element::list) {
+ isc_throw(BadValue, "'scopes' must be a list in the 'ha-scopes' command");
+ }
+
+ // Retrieve scope names from this list. The list may be empty to clear the
+ // scopes.
+ for (size_t i = 0; i < scopes->size(); ++i) {
+ ConstElementPtr scope = scopes->get(i);
+ if (!scope || scope->getType() != Element::string) {
+ isc_throw(BadValue, "scope name must be a string in the 'scopes' argument");
+ }
+ scopes_vector.push_back(scope->stringValue());
+ }
+
+ } catch (const std::exception& ex) {
+ // There was an error while parsing command arguments. Return an error status
+ // code to notify the user.
+ ConstElementPtr response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
+ callout_handle.setArgument("response", response);
+ return;
+ }
+
+ // Command parsing was successful, so let's process the command.
+ ConstElementPtr response = service_->processScopes(scopes_vector);
+ callout_handle.setArgument("response", response);
+}
+
+void
+HAImpl::continueHandler(hooks::CalloutHandle& callout_handle) {
+ ConstElementPtr response = service_->processContinue();
+ callout_handle.setArgument("response", response);
+}
+
+void
+HAImpl::maintenanceNotifyHandler(hooks::CalloutHandle& callout_handle) {
+ // Command must always be provided.
+ ConstElementPtr command;
+ callout_handle.getArgument("command", command);
+
+ // Retrieve arguments.
+ ConstElementPtr args;
+ static_cast<void>(parseCommandWithArgs(args, command));
+
+ ConstElementPtr cancel_op = args->get("cancel");
+ if (!cancel_op) {
+ isc_throw(BadValue, "'cancel' is mandatory for the 'ha-maintenance-notify' command");
+ }
+
+ if (cancel_op->getType() != Element::boolean) {
+ isc_throw(BadValue, "'cancel' must be a boolean in the 'ha-maintenance-notify' command");
+ }
+
+ ConstElementPtr response = service_->processMaintenanceNotify(cancel_op->boolValue());
+ callout_handle.setArgument("response", response);
+}
+
+void
+HAImpl::maintenanceStartHandler(hooks::CalloutHandle& callout_handle) {
+ ConstElementPtr response = service_->processMaintenanceStart();
+ callout_handle.setArgument("response", response);
+}
+
+void
+HAImpl::maintenanceCancelHandler(hooks::CalloutHandle& callout_handle) {
+ ConstElementPtr response = service_->processMaintenanceCancel();
+ callout_handle.setArgument("response", response);
+}
+
+void
+HAImpl::haResetHandler(hooks::CalloutHandle& callout_handle) {
+ ConstElementPtr response = service_->processHAReset();
+ callout_handle.setArgument("response", response);
+}
+
+void
+HAImpl::syncCompleteNotifyHandler(hooks::CalloutHandle& callout_handle) {
+ ConstElementPtr response = service_->processSyncCompleteNotify();
+ callout_handle.setArgument("response", response);
+}
+
+} // end of namespace isc::ha
+} // end of namespace isc