diff --git a/doc/sphinx/arm/hooks.rst b/doc/sphinx/arm/hooks.rst index cedcb341c051a9dea68af15544d62019ef438a8e..449bbf330af64d6598acfdfe3af367b764698fb8 100644 --- a/doc/sphinx/arm/hooks.rst +++ b/doc/sphinx/arm/hooks.rst @@ -1170,31 +1170,23 @@ In the DHCPv6 case, the corresponding query will look similar to this: flex_option Flexible Option for Option value settings ===================================================== -This sectiom describes a hook application dedicated to generate flexible -option values in response packets. The Kea software provides a way to -configure option values of response packets based on global configuration, -client classes, shared networks, subnets, pools and host reservations but -in all cases values are static. However, there are sometimes scenarios -where the value should be computed from elements from the query packet. -These scenarios are addressed by the Flexible Option hook application. - -This hook is available since Kea 1.7.1. - -.. note:: - - This library may only be loaded by the ``kea-dhp4`` or ``kea-dhp6`` - process. - -The library allows the definition of an action per option using an -expression on the query packer as for the Flex Indentifier hook library -(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. +This library allows you to define an action to take, for a given option, +based upon on the result of an expression. These actions are carried +out during the final stages of constructing a query response packet, +just before it is sent to the client. The three actions currently +supported are ``add``, ``supersede``, and ``remove``. + +The syntax used for the action expressions is the same syntax used +for client classification and the Flex Identifier hook library +(See either :ref:`classification-using-expressions` or :ref:`flex-id` +for detailed description of the syntax). + +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 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 the response packet if it already exists and the expression evaluates to true. -The option where an action applies is specified by its code point or -by its name. At least the code or the name must be specified. The option -space is the DHCPv4 or DHCPv6 spaces depending of the server where the -hook library is loaded. Other spaces as vendor spaces could be supported -in a further version. - -The library can be loaded in a similar way as other hook libraries. It takes -a mandatory ``options`` parameter holding a list of per option parameter -maps with code, name, add, supersede and remove actions. Action entries -take a string value representing an expression. +The option to which an action applies may be specified by either its +numeric code or its name.. At least the code or the name must be +specified. The option space is the DHCPv4 or DHCPv6 spaces depending +of the server where the hook library is loaded. Other spaces as vendor +spaces could be supported in a further version. + +The library is available since Kea 1.7.1 and can be loaded in a +similar way as other hook libraries by the ``kea-dhp4`` or `kea-dhp6`` +process.. It takes a mandatory ``options`` parameter holding a list of +per option parameter maps with code, name, add, supersede and remove +actions. Action entries take a string value representing an +expression. :: diff --git a/src/hooks/dhcp/flex_option/flex_option.cc b/src/hooks/dhcp/flex_option/flex_option.cc index 1d8f1e53bc4a78e15c4d1ecabc50fb486210609f..5cea66d9dfda8e71e38c5f8e69490d6df769e099 100644 --- a/src/hooks/dhcp/flex_option/flex_option.cc +++ b/src/hooks/dhcp/flex_option/flex_option.cc @@ -20,9 +20,55 @@ using namespace isc; using namespace isc::data; using namespace isc::dhcp; using namespace isc::eval; +using namespace isc::flex_option; using namespace isc::log; 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 flex_option { @@ -130,8 +176,9 @@ FlexOptionImpl::parseOptionConfig(ConstElementPtr option) { << space << "' space"); } if (code_elem && (def->getCode() != code)) { - isc_throw(BadValue, "option '" << name << "' has code " - << def->getCode() << " but 'code' is " << code); + isc_throw(BadValue, "option '" << name << "' is defined as code: " + << def->getCode() << ", not the specified code: " + << code); } code = def->getCode(); } @@ -148,78 +195,13 @@ FlexOptionImpl::parseOptionConfig(ConstElementPtr option) { } 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()); - } - } + // opt_cfg initial action is NONE. + parseAction(option, opt_cfg, universe, + "add", ADD, EvalContext::PARSER_STRING); + parseAction(option, opt_cfg, universe, + "supersede", SUPERSEDE, EvalContext::PARSER_STRING); + parseAction(option, opt_cfg, universe, + "remove", REMOVE, EvalContext::PARSER_BOOL); if (opt_cfg->getAction() == NONE) { isc_throw(BadValue, "no action: " << option->str()); diff --git a/src/hooks/dhcp/flex_option/flex_option.dox b/src/hooks/dhcp/flex_option/flex_option.dox index d168dcf7d82b8ceeb6cb8e7a6d6ec4ed7dccba23..59e8beccc7e535e184beef3f55127dcb89684bc4 100644 --- a/src/hooks/dhcp/flex_option/flex_option.dox +++ b/src/hooks/dhcp/flex_option/flex_option.dox @@ -75,7 +75,7 @@ To configure it for kea-dhcp6, the commands are simply as shown below: } @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 name - Speficies the option name, at least the option code or name must be given. @@ -93,7 +93,8 @@ The parameter is rge options list of option configuration maps with: response. Only one action can be specified. 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 diff --git a/src/hooks/dhcp/flex_option/flex_option.h b/src/hooks/dhcp/flex_option/flex_option.h index 681f40fcd1be25adc2027119ba38fea5d3360cf4..a093f54b828749de67950ee80c3ec990495e3a85 100644 --- a/src/hooks/dhcp/flex_option/flex_option.h +++ b/src/hooks/dhcp/flex_option/flex_option.h @@ -17,6 +17,11 @@ namespace isc { namespace flex_option { /// @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 { public: @@ -152,13 +157,16 @@ public: case NONE: break; case ADD: + // Don't add if option is already there. if (opt) { break; } value = isc::dhcp::evaluateString(*opt_cfg->getExpr(), *query); + // Do nothing is the expression evaluates to empty. if (value.empty()) { break; } + // Add the option. buffer.assign(value.begin(), value.end()); opt.reset(new isc::dhcp::Option(universe, opt_cfg->getCode(), buffer)); @@ -166,14 +174,17 @@ public: logAction(ADD, opt_cfg->getCode(), value); break; case SUPERSEDE: + // Do nothing is the expression evaluates to empty. value = isc::dhcp::evaluateString(*opt_cfg->getExpr(), *query); if (value.empty()) { break; } + // Remove the option if already there. while (opt) { response->delOption(opt_cfg->getCode()); opt = response->getOption(opt_cfg->getCode()); } + // Add the option. buffer.assign(value.begin(), value.end()); opt.reset(new isc::dhcp::Option(universe, opt_cfg->getCode(), buffer)); @@ -181,12 +192,15 @@ public: logAction(SUPERSEDE, opt_cfg->getCode(), value); break; case REMOVE: + // Nothing to remove if option is not present. if (!opt) { break; } + // Do nothing is the expression evaluates to false. if (!isc::dhcp::evaluateBool(*opt_cfg->getExpr(), *query)) { break; } + // Remove the option. while (opt) { response->delOption(opt_cfg->getCode()); opt = response->getOption(opt_cfg->getCode()); diff --git a/src/hooks/dhcp/flex_option/tests/flex_option_unittests.cc b/src/hooks/dhcp/flex_option/tests/flex_option_unittests.cc index 5126b1fdd0b130854ea496a1c42b82f6c1d4e093..20886b776887e4f3b83d13e859e9084ae53ee7fb 100644 --- a/src/hooks/dhcp/flex_option/tests/flex_option_unittests.cc +++ b/src/hooks/dhcp/flex_option/tests/flex_option_unittests.cc @@ -305,7 +305,9 @@ TEST_F(FlexOptionTest, optionConfigCodeNameMismatch) { ElementPtr name = Element::create(string("host-name")); option->set("name", name); 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.