Commit 2bf854c0 authored by Francis Dupont's avatar Francis Dupont

[219-allow-an-option-value-to-be-set-from-an-expression] Addressed comments

parent 3a1cec32
...@@ -1170,31 +1170,23 @@ In the DHCPv6 case, the corresponding query will look similar to this: ...@@ -1170,31 +1170,23 @@ In the DHCPv6 case, the corresponding query will look similar to this:
flex_option Flexible Option for Option value settings flex_option Flexible Option for Option value settings
===================================================== =====================================================
This sectiom describes a hook application dedicated to generate flexible This library allows you to define an action to take, for a given option,
option values in response packets. The Kea software provides a way to based upon on the result of an expression. These actions are carried
configure option values of response packets based on global configuration, out during the final stages of constructing a query response packet,
client classes, shared networks, subnets, pools and host reservations but just before it is sent to the client. The three actions currently
in all cases values are static. However, there are sometimes scenarios supported are ``add``, ``supersede``, and ``remove``.
where the value should be computed from elements from the query packet.
These scenarios are addressed by the Flexible Option hook application. The syntax used for the action expressions is the same syntax used
for client classification and the Flex Identifier hook library
This hook is available since Kea 1.7.1. (See either :ref:`classification-using-expressions` or :ref:`flex-id`
for detailed description of the syntax).
.. note::
The ``add`` and ``supersede`` actions use an expression returning a
This library may only be loaded by the ``kea-dhp4`` or ``kea-dhp6`` string, doing nothing when it evaluates to the empty string. The
process. ``remove`` application uses an expression returning true or false,
doing nothing on false. When it is necessary to set an option to the
The library allows the definition of an action per option using an empty value this mechanism does not work but a client class can be
expression on the query packer as for the Flex Indentifier hook library used instead.
(See :ref:`flex-id`) or for client classification (See
:ref:`classification-using-expressions` for a detailed description of
the syntax available.) The ``add`` and ``supersede`` actions use an
expression returning a string, doing nothing when it evaluates to
the empty string. The ``remove`` application uses an expression returning
true or false, doing nothing on false. When it is necessary to set
an option to the empty value this mechanism does not work but a client
class can be used instead.
The ``add`` action adds an option only when the option does not already The ``add`` action adds an option only when the option does not already
exist and the expression does not evaluate to the empty string. exist and the expression does not evaluate to the empty string.
...@@ -1203,16 +1195,18 @@ if it already exists. The ``remove`` action removes the option from ...@@ -1203,16 +1195,18 @@ if it already exists. The ``remove`` action removes the option from
the response packet if it already exists and the expression evaluates to the response packet if it already exists and the expression evaluates to
true. true.
The option where an action applies is specified by its code point or The option to which an action applies may be specified by either its
by its name. At least the code or the name must be specified. The option numeric code or its name.. At least the code or the name must be
space is the DHCPv4 or DHCPv6 spaces depending of the server where the specified. The option space is the DHCPv4 or DHCPv6 spaces depending
hook library is loaded. Other spaces as vendor spaces could be supported of the server where the hook library is loaded. Other spaces as vendor
in a further version. spaces could be supported in a further version.
The library can be loaded in a similar way as other hook libraries. It takes The library is available since Kea 1.7.1 and can be loaded in a
a mandatory ``options`` parameter holding a list of per option parameter similar way as other hook libraries by the ``kea-dhp4`` or `kea-dhp6``
maps with code, name, add, supersede and remove actions. Action entries process.. It takes a mandatory ``options`` parameter holding a list of
take a string value representing an expression. per option parameter maps with code, name, add, supersede and remove
actions. Action entries take a string value representing an
expression.
:: ::
......
...@@ -20,9 +20,55 @@ using namespace isc; ...@@ -20,9 +20,55 @@ using namespace isc;
using namespace isc::data; using namespace isc::data;
using namespace isc::dhcp; using namespace isc::dhcp;
using namespace isc::eval; using namespace isc::eval;
using namespace isc::flex_option;
using namespace isc::log; using namespace isc::log;
using namespace std; using namespace std;
namespace {
/// @brief Parse an action.
///
/// @param option The option element.
/// @param opt_cfg The option configuration.
/// @param name The action name.
/// @param action The action.
/// @param parser_type The type of the parser of the expression.
void
parseAction(ConstElementPtr option,
FlexOptionImpl::OptionConfigPtr opt_cfg,
Option::Universe universe,
const string& name,
FlexOptionImpl::Action action,
EvalContext::ParserType parser_type) {
ConstElementPtr elem = option->get(name);
if (elem) {
if (elem->getType() != Element::string) {
isc_throw(BadValue, "'" << name << "' must be a string: "
<< elem->str());
}
string expr_text = elem->stringValue();
if (expr_text.empty()) {
isc_throw(BadValue, "'" << name << "' must not be empty");
}
if (opt_cfg->getAction() != FlexOptionImpl::NONE) {
isc_throw(BadValue, "multiple actions: " << option->str());
}
opt_cfg->setAction(action);
opt_cfg->setText(expr_text);
try {
EvalContext eval_ctx(universe);
eval_ctx.parseString(expr_text, parser_type);
ExpressionPtr expr(new Expression(eval_ctx.expression));
opt_cfg->setExpr(expr);
} catch (const std::exception& ex) {
isc_throw(BadValue, "can't parse " << name << " expression ["
<< expr_text << "] error: " << ex.what());
}
}
}
} // end of anonymous namespace
namespace isc { namespace isc {
namespace flex_option { namespace flex_option {
...@@ -130,8 +176,9 @@ FlexOptionImpl::parseOptionConfig(ConstElementPtr option) { ...@@ -130,8 +176,9 @@ FlexOptionImpl::parseOptionConfig(ConstElementPtr option) {
<< space << "' space"); << space << "' space");
} }
if (code_elem && (def->getCode() != code)) { if (code_elem && (def->getCode() != code)) {
isc_throw(BadValue, "option '" << name << "' has code " isc_throw(BadValue, "option '" << name << "' is defined as code: "
<< def->getCode() << " but 'code' is " << code); << def->getCode() << ", not the specified code: "
<< code);
} }
code = def->getCode(); code = def->getCode();
} }
...@@ -148,78 +195,13 @@ FlexOptionImpl::parseOptionConfig(ConstElementPtr option) { ...@@ -148,78 +195,13 @@ FlexOptionImpl::parseOptionConfig(ConstElementPtr option) {
} }
OptionConfigPtr opt_cfg(new OptionConfig(code)); OptionConfigPtr opt_cfg(new OptionConfig(code));
ConstElementPtr add_elem = option->get("add"); // opt_cfg initial action is NONE.
if (add_elem) { parseAction(option, opt_cfg, universe,
if (add_elem->getType() != Element::string) { "add", ADD, EvalContext::PARSER_STRING);
isc_throw(BadValue, "'add' must be a string: " parseAction(option, opt_cfg, universe,
<< add_elem->str()); "supersede", SUPERSEDE, EvalContext::PARSER_STRING);
} parseAction(option, opt_cfg, universe,
string add = add_elem->stringValue(); "remove", REMOVE, EvalContext::PARSER_BOOL);
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());
}
}
if (opt_cfg->getAction() == NONE) { if (opt_cfg->getAction() == NONE) {
isc_throw(BadValue, "no action: " << option->str()); isc_throw(BadValue, "no action: " << option->str());
......
...@@ -75,7 +75,7 @@ To configure it for kea-dhcp6, the commands are simply as shown below: ...@@ -75,7 +75,7 @@ To configure it for kea-dhcp6, the commands are simply as shown below:
} }
@endcode @endcode
The parameter is rge options list of option configuration maps with: The sole parameter is a options list of options with:
- @b code - Specifies the option code. - @b code - Specifies the option code.
- @b name - Speficies the option name, at least the option code or name - @b name - Speficies the option name, at least the option code or name
must be given. must be given.
...@@ -93,7 +93,8 @@ The parameter is rge options list of option configuration maps with: ...@@ -93,7 +93,8 @@ The parameter is rge options list of option configuration maps with:
response. Only one action can be specified. response. Only one action can be specified.
Note for the rare options which can be empty this mechanism does not work. Note for the rare options which can be empty this mechanism does not work.
The proposed solution in this case is to use a client class. The proposed solution in this case is to use a client class to set the
empty value to the option in a option-data clause.
## Internal operation ## Internal operation
......
...@@ -17,6 +17,11 @@ namespace isc { ...@@ -17,6 +17,11 @@ namespace isc {
namespace flex_option { namespace flex_option {
/// @brief Flex Option implementation. /// @brief Flex Option implementation.
///
/// The implementation can be divided into two parts:
/// - the configuration parsed and stored by load()
/// - the response packet processing performed by the process method
///
class FlexOptionImpl { class FlexOptionImpl {
public: public:
...@@ -152,13 +157,16 @@ public: ...@@ -152,13 +157,16 @@ public:
case NONE: case NONE:
break; break;
case ADD: case ADD:
// Don't add if option is already there.
if (opt) { if (opt) {
break; break;
} }
value = isc::dhcp::evaluateString(*opt_cfg->getExpr(), *query); value = isc::dhcp::evaluateString(*opt_cfg->getExpr(), *query);
// Do nothing is the expression evaluates to empty.
if (value.empty()) { if (value.empty()) {
break; break;
} }
// Add the option.
buffer.assign(value.begin(), value.end()); buffer.assign(value.begin(), value.end());
opt.reset(new isc::dhcp::Option(universe, opt_cfg->getCode(), opt.reset(new isc::dhcp::Option(universe, opt_cfg->getCode(),
buffer)); buffer));
...@@ -166,14 +174,17 @@ public: ...@@ -166,14 +174,17 @@ public:
logAction(ADD, opt_cfg->getCode(), value); logAction(ADD, opt_cfg->getCode(), value);
break; break;
case SUPERSEDE: case SUPERSEDE:
// Do nothing is the expression evaluates to empty.
value = isc::dhcp::evaluateString(*opt_cfg->getExpr(), *query); value = isc::dhcp::evaluateString(*opt_cfg->getExpr(), *query);
if (value.empty()) { if (value.empty()) {
break; break;
} }
// Remove the option if already there.
while (opt) { while (opt) {
response->delOption(opt_cfg->getCode()); response->delOption(opt_cfg->getCode());
opt = response->getOption(opt_cfg->getCode()); opt = response->getOption(opt_cfg->getCode());
} }
// Add the option.
buffer.assign(value.begin(), value.end()); buffer.assign(value.begin(), value.end());
opt.reset(new isc::dhcp::Option(universe, opt_cfg->getCode(), opt.reset(new isc::dhcp::Option(universe, opt_cfg->getCode(),
buffer)); buffer));
...@@ -181,12 +192,15 @@ public: ...@@ -181,12 +192,15 @@ public:
logAction(SUPERSEDE, opt_cfg->getCode(), value); logAction(SUPERSEDE, opt_cfg->getCode(), value);
break; break;
case REMOVE: case REMOVE:
// Nothing to remove if option is not present.
if (!opt) { if (!opt) {
break; break;
} }
// Do nothing is the expression evaluates to false.
if (!isc::dhcp::evaluateBool(*opt_cfg->getExpr(), *query)) { if (!isc::dhcp::evaluateBool(*opt_cfg->getExpr(), *query)) {
break; break;
} }
// Remove the option.
while (opt) { while (opt) {
response->delOption(opt_cfg->getCode()); response->delOption(opt_cfg->getCode());
opt = response->getOption(opt_cfg->getCode()); opt = response->getOption(opt_cfg->getCode());
......
...@@ -305,7 +305,9 @@ TEST_F(FlexOptionTest, optionConfigCodeNameMismatch) { ...@@ -305,7 +305,9 @@ TEST_F(FlexOptionTest, optionConfigCodeNameMismatch) {
ElementPtr name = Element::create(string("host-name")); ElementPtr name = Element::create(string("host-name"));
option->set("name", name); option->set("name", name);
EXPECT_THROW(impl_->testConfigure(options), BadValue); EXPECT_THROW(impl_->testConfigure(options), BadValue);
EXPECT_EQ("option 'host-name' has code 12 but 'code' is 13", impl_->getErrMsg()); string expected = "option 'host-name' is defined as code: 12, ";
expected += "not the specified code: 13";
EXPECT_EQ(expected, impl_->getErrMsg());
} }
// Verify that an option can be configured only once. // Verify that an option can be configured only once.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment