Commit 248b058a authored by Francis Dupont's avatar Francis Dupont

[219-allow-an-option-value-to-be-set-from-an-expression] Checkpoint: wrote...

[219-allow-an-option-value-to-be-set-from-an-expression] Checkpoint: wrote flex option code, to do tests, to finish doc
parent 6775f8a4
......@@ -1694,6 +1694,8 @@ AC_CONFIG_FILES([Makefile
src/bin/shell/tests/shell_unittest.py
src/hooks/Makefile
src/hooks/dhcp/Makefile
src/hooks/dhcp/flex_option/Makefile
src/hooks/dhcp/flex_option/tests/Makefile
src/hooks/dhcp/high_availability/Makefile
src/hooks/dhcp/high_availability/tests/Makefile
src/hooks/dhcp/lease_cmds/Makefile
......
......@@ -4,4 +4,4 @@ if HAVE_MYSQL
SUBDIRS += mysql_cb
endif
SUBDIRS += stat_cmds user_chk
SUBDIRS += stat_cmds flex_option user_chk
SUBDIRS = . tests
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CXXFLAGS = $(KEA_CXXFLAGS)
# Ensure that the message file and doxygen file is included in the distribution
EXTRA_DIST = flex_option_messages.mes
EXTRA_DIST += flex_option.dox
CLEANFILES = *.gcno *.gcda
# convenience archive
noinst_LTLIBRARIES = libflex_option.la
libflex_option_la_SOURCES = flex_option.cc flex_option.h
libflex_option_la_SOURCES += flex_option_callouts.cc
libflex_option_la_SOURCES += flex_option_log.cc flex_option_log.h
libflex_option_la_SOURCES += flex_option_messages.cc flex_option_messages.h
libflex_option_la_SOURCES += version.cc
libflex_option_la_CXXFLAGS = $(AM_CXXFLAGS)
libflex_option_la_CPPFLAGS = $(AM_CPPFLAGS)
# install the shared object into $(libdir)/kea/hooks
lib_hooksdir = $(libdir)/kea/hooks
lib_hooks_LTLIBRARIES = libdhcp_flex_option.la
libdhcp_flex_option_la_SOURCES =
libdhcp_flex_option_la_LDFLAGS = $(AM_LDFLAGS)
libdhcp_flex_option_la_LDFLAGS += -avoid-version -export-dynamic -module
libdhcp_flex_option_la_LIBADD = libflex_option.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/eval/libkea-eval.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/database/libkea-database.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/cryptolink/libkea-cryptolink.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la
libdhcp_flex_option_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
libdhcp_flex_option_la_LIBADD += $(LOG4CPLUS_LIBS)
libdhcp_flex_option_la_LIBADD += $(CRYPTO_LIBS)
libdhcp_flex_option_la_LIBADD += $(BOOST_LIBS)
# If we want to get rid of all generated messages files, we need to use
# make maintainer-clean. The proper way to introduce custom commands for
# that operation is to define maintainer-clean-local target. However,
# make maintainer-clean also removes Makefile, so running configure script
# is required. To make it easy to rebuild messages without going through
# reconfigure, a new target messages-clean has been added.
maintainer-clean-local:
rm -f flex_option_messages.h flex_option_messages.cc
# To regenerate messages files, one can do:
#
# make messages-clean
# make messages
#
# This is needed only when a .mes file is modified.
messages-clean: maintainer-clean-local
if GENERATE_MESSAGES
# Define rule to build logging source files from message file
messages: flex_option_messages.h flex_option_messages.cc
@echo Message files regenerated
flex_option_messages.h flex_option_messages.cc: flex_option_messages.mes
$(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/hooks/dhcp/flex_option/flex_option_messages.mes
else
messages flex_option_messages.h flex_option_messages.cc:
@echo Messages generation disabled. Configure with --enable-generate-messages to enable it.
endif
// Copyright (C) 2019 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 <flex_option.h>
#include <cc/simple_parser.h>
#include <dhcp/dhcp4.h>
#include <dhcp/libdhcp++.h>
#include <dhcp/option_definition.h>
#include <dhcp/option_space.h>
#include <dhcpsrv/cfgmgr.h>
#include <eval/eval_context.h>
using namespace isc;
using namespace isc::data;
using namespace isc::dhcp;
using namespace isc::eval;
using namespace std;
namespace isc {
namespace flex_option {
FlexOptionImpl::OptionConfig::OptionConfig(uint16_t code) : code_(code) {
}
FlexOptionImpl::OptionConfig::~OptionConfig() {
}
FlexOptionImpl::FlexOptionImpl() {
}
FlexOptionImpl::~FlexOptionImpl() {
option_config_map_.clear();
}
void
FlexOptionImpl::configure(ConstElementPtr options) {
if (!options) {
isc_throw(BadValue, "'options' parameter is mandatory");
}
if (options->getType() != Element::list) {
isc_throw(BadValue, "'options' parameter must be a list");
}
if (options->empty()) {
return;
}
for (auto option : options->listValue()) {
parseOptionConfig(option);
}
}
void
FlexOptionImpl::parseOptionConfig(ConstElementPtr option) {
uint16_t family = CfgMgr::instance().getFamily();
if (!option) {
isc_throw(BadValue, "null option element");
}
if (option->getType() != Element::map) {
isc_throw(BadValue, "option element is not a map");
}
ConstElementPtr code_elem = option->get("code");
ConstElementPtr name_elem = option->get("name");
if (!code_elem && !name_elem) {
isc_throw(BadValue, "'code' or 'name' must be specified: "
<< option->str());
}
uint16_t code;
if (code_elem) {
if (code_elem->getType() != Element::integer) {
isc_throw(BadValue, "'code' must be an integer: "
<< code_elem->str());
}
int64_t value = code_elem->intValue();
int64_t max_code;
if (family == AF_INET) {
max_code = numeric_limits<uint8_t>::max();
} else {
max_code = numeric_limits<uint16_t>::max();
}
if ((value < 0) || (value > max_code)) {
isc_throw(OutOfRange, "invalid 'code' value " << value
<< " not in [0.." << max_code << "]");
}
if (family == AF_INET) {
if (value == DHO_PAD) {
isc_throw(BadValue,
"invalid 'code' value 0: reserved for PAD");
} else if (value == DHO_END) {
isc_throw(BadValue,
"invalid 'code' value 255: reserved for END");
}
} else {
if (value == 0) {
isc_throw(BadValue, "invalid 'code' value 0: reserved");
}
}
code = static_cast<uint16_t>(value);
}
if (name_elem) {
if (name_elem->getType() != Element::string) {
isc_throw(BadValue, "'name' must be a string: "
<< name_elem->str());
}
string name = name_elem->stringValue();
if (name.empty()) {
isc_throw(BadValue, "'name' must not be empty");
}
string space;
if (family == AF_INET) {
space = DHCP4_OPTION_SPACE;
} else {
space = DHCP6_OPTION_SPACE;
}
OptionDefinitionPtr def = LibDHCP::getOptionDef(space, name);
if (!def) {
def = LibDHCP::getRuntimeOptionDef(space, name);
}
if (!def) {
def = LibDHCP::getLastResortOptionDef(space, name);
}
if (!def) {
isc_throw(BadValue, "no known '" << name << "' option in '"
<< space << "' space");
}
if (code_elem && (def->getCode() != code)) {
isc_throw(BadValue, "option '" << name << "' has code "
<< def->getCode() << " but 'code' is " << code);
}
code = def->getCode();
}
if (option_config_map_.count(code)) {
isc_throw(BadValue, "option " << code << " was already specified");
}
Option::Universe universe;
if (family == AF_INET) {
universe = Option::V4;
} else {
universe = Option::V6;
}
OptionConfigPtr opt_cfg(new OptionConfig(code));
ConstElementPtr add_elem = option->get("add");
if (add_elem) {
if (add_elem->getType() != Element::string) {
isc_throw(BadValue, "'add' must be a string: "
<< add_elem->str());
}
string add = add_elem->stringValue();
if (add.empty()) {
isc_throw(BadValue, "'add' must not be empty");
}
opt_cfg->setAction(ADD);
opt_cfg->setText(add);
try {
EvalContext eval_ctx(universe);
eval_ctx.parseString(add, EvalContext::PARSER_STRING);
ExpressionPtr expr(new Expression(eval_ctx.expression));
opt_cfg->setExpr(expr);
} catch (const std::exception& ex) {
isc_throw(BadValue, "can't parse add expression ["
<< add << "] error: " << ex.what());
}
}
ConstElementPtr supersede_elem = option->get("supersede");
if (supersede_elem) {
if (supersede_elem->getType() != Element::string) {
isc_throw(BadValue, "'supersede' must be a string: "
<< supersede_elem->str());
}
string supersede = supersede_elem->stringValue();
if (supersede.empty()) {
isc_throw(BadValue, "'supersede' must not be empty");
}
if (opt_cfg->getAction() != NONE) {
isc_throw(BadValue, "multiple actions: " << option->str());
}
opt_cfg->setAction(SUPERSEDE);
opt_cfg->setText(supersede);
try {
EvalContext eval_ctx(universe);
eval_ctx.parseString(supersede, EvalContext::PARSER_STRING);
ExpressionPtr expr(new Expression(eval_ctx.expression));
opt_cfg->setExpr(expr);
} catch (const std::exception& ex) {
isc_throw(BadValue, "can't parse supersede expression ["
<< supersede << "] error: " << ex.what());
}
}
ConstElementPtr remove_elem = option->get("remove");
if (remove_elem) {
if (remove_elem->getType() != Element::string) {
isc_throw(BadValue, "'remove' must be a string: "
<< remove_elem->str());
}
string remove = remove_elem->stringValue();
if (remove.empty()) {
isc_throw(BadValue, "'remove' must not be empty");
}
if (opt_cfg->getAction() != NONE) {
isc_throw(BadValue, "multiple actions: " << option->str());
}
opt_cfg->setAction(REMOVE);
opt_cfg->setText(remove);
try {
EvalContext eval_ctx(universe);
eval_ctx.parseString(remove, EvalContext::PARSER_BOOL);
ExpressionPtr expr(new Expression(eval_ctx.expression));
opt_cfg->setExpr(expr);
} catch (const std::exception& ex) {
isc_throw(BadValue, "can't parse remove expression ["
<< remove << "] error: " << ex.what());
}
}
}
} // end of namespace flex_option
} // end of namespace isc
// Copyright (C) 2019 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/.
/**
@page libdhcp_flex_option Kea Flexible Option Hooks Library
@section libdhcp_flex_optionIntro Introduction
Welcome to Kea Flexible Option Hooks Library. This documentation is
addressed to developers who are interested in the internal operation
of the Flexible Option library. This file provides information needed
to understand and perhaps extend this library.
This documentation is stand-alone: you should have read and understood
the <a href="https://jenkins.isc.org/job/Kea_doc/doxygen/">Kea
Developer's Guide</a> and in particular its section about hooks.
@section flex_option Flexible Option Overview
Flexible Option (or flex_option) is a Hook library that can be loaded by
either kea-dhcp4 and kea-dhcp6 servers to extend them with additional
option value setting mechanisms.
@section flex_optionCode Flexible Option Code Overview
Todo
*/
// Copyright (C) 2019 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 FLEX_OPTION_H
#define FLEX_OPTION_H
#include <cc/data.h>
#include <eval/evaluate.h>
#include <eval/token.h>
#include <string>
#include <map>
namespace isc {
namespace flex_option {
/// @brief Flex Option implementation.
class FlexOptionImpl {
public:
/// @brief Action.
///
/// Currently supported actions are:
/// - add (if not already existing)
/// - supersede (as add but also when already existing)
/// - remove
enum Action {
NONE,
ADD,
SUPERSEDE,
REMOVE
};
/// @brief Option configuration.
///
/// Per option configuration.
class OptionConfig {
public:
/// @brief Constructor.
///
/// @param option code.
OptionConfig(uint16_t code);
/// @brief Destructor.
virtual ~OptionConfig();
/// @brief Return option code.
///
/// @return option code.
uint16_t getCode() const {
return (code_);
}
/// @brief Set action.
///
/// @param action the action.
void setAction(Action action) {
action_ = action;
}
/// @brief Return action.
///
/// @return action.
Action getAction() const {
return (action_);
}
/// @brief Set textual expression.
///
/// @param text the textual expression.
void setText(const std::string& text) {
text_ = text;
};
/// @brief Get textual expression.
///
/// @return textual expression.
const std::string& getText() const {
return (text_);
}
/// @brief Set match expression.
///
/// @param expr the match expression.
void setExpr(const isc::dhcp::ExpressionPtr& expr) {
expr_ = expr;
}
/// @brief Get match expression.
///
/// @return the match expression.
const isc::dhcp::ExpressionPtr& getExpr() const {
return (expr_);
}
private:
/// @brief The code.
uint16_t code_;
/// @brief The action.
Action action_;
/// @brief The textual expression.
std::string text_;
/// @brief The match expression.
isc::dhcp::ExpressionPtr expr_;
};
/// @brief The type of shared pointers to option config.
typedef boost::shared_ptr<OptionConfig> OptionConfigPtr;
/// @brief The type of the option config map.
typedef std::map<uint16_t, OptionConfigPtr> OptionConfigMap;
/// @brief Constructor.
FlexOptionImpl();
/// @brief Destructor.
~FlexOptionImpl();
/// @brief Get the option config map.
///
/// @return The option config map.
const OptionConfigMap& getOptionConfigMap() const {
return (option_config_map_);
}
/// @brief Configure the Flex Option implementation.
///
/// @param options The element with option config list.
/// @throw BadValue and similar exceptions on error.
void configure(isc::data::ConstElementPtr options);
/// @brief Process a query / response pair.
///
/// @tparam PktType The type of pointers to packets: Pkt4Ptr or Pkt6Ptr.
/// @param universe The option universe: Option::V4 or Option::V6.
/// @param query The query packet.
/// @param response The response packet.
template <typename PktType>
void process(isc::dhcp::Option::Universe universe,
PktType query, PktType response) {
for (auto pair : getOptionConfigMap()) {
const OptionConfigPtr& opt_cfg = pair.second;
std::string value;
isc::dhcp::OptionBuffer buffer;
isc::dhcp::OptionPtr opt = response->getOption(opt_cfg->getCode());
switch (opt_cfg->getAction()) {
case NONE:
break;
case ADD:
if (opt) {
break;
}
value = isc::dhcp::evaluateString(*opt_cfg->getExpr(), *query);
if (value.empty()) {
break;
}
buffer.assign(value.begin(), value.end());
opt.reset(new isc::dhcp::Option(universe, opt_cfg->getCode(),
buffer));
response->addOption(opt);
break;
case SUPERSEDE:
value = isc::dhcp::evaluateString(*opt_cfg->getExpr(), *query);
if (value.empty()) {
break;
}
while (opt) {
response->delOption(opt_cfg->getCode());
opt = response->getOption(opt_cfg->getCode());
}
buffer.assign(value.begin(), value.end());
opt.reset(new isc::dhcp::Option(universe, opt_cfg->getCode(),
buffer));
response->addOption(opt);
break;
case REMOVE:
if (!opt) {
break;
}
if (!isc::dhcp::evaluateBool(*opt_cfg->getExpr(), *query)) {
break;
}
while (opt) {
response->delOption(opt_cfg->getCode());
opt = response->getOption(opt_cfg->getCode());
}
break;
}
}
}
private:
/// @brief The option config map (code and pointer to option config).
OptionConfigMap option_config_map_;
/// @brief Parse an option config.
///
/// @param option The element with option config.
/// @throw BadValue and similar exceptionson error.
void parseOptionConfig(isc::data::ConstElementPtr option);
};
/// @brief The type of shared pointers to Flex Option implementations.
typedef boost::shared_ptr<FlexOptionImpl> FlexOptionImplPtr;
} // end of namespace flex_option
} // end of namespace isc
#endif
// Copyright (C) 2019 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 <flex_option.h>
#include <flex_option_log.h>
#include <cc/command_interpreter.h>
#include <hooks/hooks.h>
#include <dhcp/pkt4.h>
#include <dhcp/pkt6.h>
namespace isc {
namespace flex_option {
FlexOptionImplPtr impl;
} // end of namespace flex_option
} // end of namespace isc
using namespace isc;
using namespace isc::data;
using namespace isc::dhcp;
using namespace isc::hooks;
using namespace isc::flex_option;
// Functions accessed by the hooks framework use C linkage to avoid the name
// mangling that accompanies use of the C++ compiler as well as to avoid
// issues related to namespaces.
extern "C" {
/// @brief This callout is called at the "pkt4_send" hook.
///
/// It retrieves v4 query and response packets, and then adds, supersedes
/// or removes option values in the response according to expressions
/// evaluated on the query.
///
/// @param handle CalloutHandle.
///
/// @return 0 upon success, non-zero otherwise
int pkt4_send(CalloutHandle& handle) {
// Get the parameters.
Pkt4Ptr query;
Pkt4Ptr response;
handle.getArgument("query4", query);
handle.getArgument("response4", response);
try {
impl->process<Pkt4Ptr>(Option::V4, query, response);
} catch (const std::exception& ex) {
LOG_ERROR(flex_option_logger, FLEX_OPTION_PROCESS_ERROR)
.arg(query->getLabel())
.arg(ex.what());
}
return (0);
}
/// @brief This callout is called at the "pkt6_send" hook.