From: Francis Dupont Date: Wed, 22 Jul 2026 23:31:35 +0000 (+0200) Subject: [#4143] Checkpoint: doc todo X-Git-Tag: Kea-3.3.0~16 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ffd683df5d4c9bc606c14467b3e13d4b73cfbabd;p=thirdparty%2Fkea.git [#4143] Checkpoint: doc todo --- diff --git a/src/hooks/dhcp/flex_option/flex_option.cc b/src/hooks/dhcp/flex_option/flex_option.cc index b7cb445b28..f7d9c05a46 100644 --- a/src/hooks/dhcp/flex_option/flex_option.cc +++ b/src/hooks/dhcp/flex_option/flex_option.cc @@ -324,16 +324,6 @@ FlexOptionImpl::parseOptionConfig(ConstElementPtr option) { .arg(code) .arg(opt_cfg->getClass()); } - if (!opt_cfg->getDestination() && opt_cfg->getExpr()) { - for (auto const& tok : *opt_cfg->getExpr()) { - if (boost::dynamic_pointer_cast(tok)) { - LOG_WARN(flex_option_logger, FLEX_OPTION_CONFIG_USELESS_MEMBER) - .arg(code) - .arg(opt_cfg->getText()); - break; - } - } - } // opt_cfg initial action is NONE. if (sub_options) { @@ -368,6 +358,19 @@ FlexOptionImpl::parseOptionConfig(ConstElementPtr option) { opt_lst.push_back(opt_cfg); } + // Not working as expected: the destination is the query and + // TokenMember is used. + if (!opt_cfg->getDestination() && opt_cfg->getExpr()) { + for (auto const& tok : *opt_cfg->getExpr()) { + if (boost::dynamic_pointer_cast(tok)) { + LOG_WARN(flex_option_logger, FLEX_OPTION_CONFIG_USELESS_MEMBER) + .arg(code) + .arg(opt_cfg->getText()); + break; + } + } + } + // Check if we have to copy classes from the query to the response. if (need_copy_classes_to_response_ || opt_cfg->getSource() || @@ -523,6 +526,15 @@ FlexOptionImpl::parseSubOption(ConstElementPtr sub_option, sub_cfg->setSource(opt_cfg->getSource()); sub_cfg->setDestination(opt_cfg->getDestination()); + // Not working as expected: the destination is the query and classes + // are used. + if (!opt_cfg->getDestination() && !sub_cfg->getClass().empty()) { + LOG_WARN(flex_option_logger, FLEX_OPTION_CONFIG_SUB_USELESS_CLASS) + .arg(code) + .arg(opt_cfg->getCode()) + .arg(sub_cfg->getClass()); + } + // sub_cfg initial action is NONE. parseAction(sub_option, sub_cfg, universe, "add", ADD, EvalContext::PARSER_STRING); @@ -535,6 +547,21 @@ FlexOptionImpl::parseSubOption(ConstElementPtr sub_option, isc_throw(BadValue, "no action: " << sub_option->str()); } + // Not working as expected: the destination is the query and + // TokenMember is used. + if (!opt_cfg->getDestination() && sub_cfg->getExpr()) { + for (auto const& tok : *sub_cfg->getExpr()) { + if (boost::dynamic_pointer_cast(tok)) { + LOG_WARN(flex_option_logger, + FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER) + .arg(code) + .arg(opt_cfg->getCode()) + .arg(sub_cfg->getText()); + break; + } + } + } + ConstElementPtr container_add = sub_option->get("container-add"); ConstElementPtr container_remove = sub_option->get("container-remove"); if ((sub_cfg->getAction() == ADD) || (sub_cfg->getAction() == SUPERSEDE)) { @@ -558,6 +585,20 @@ FlexOptionImpl::parseSubOption(ConstElementPtr sub_option, << " was already specified"); } sub_map[code] = sub_cfg; + + // Check if we have to copy classes from the query to the response. + if (need_copy_classes_to_response_ || + opt_cfg->getSource() || + !sub_cfg->getExpr()) { + return; + } + for (auto const& tok : *sub_cfg->getExpr()) { + if (boost::dynamic_pointer_cast(tok)) { + need_copy_classes_to_response_ = true; + break; + } + } + } void diff --git a/src/hooks/dhcp/flex_option/flex_option.dox b/src/hooks/dhcp/flex_option/flex_option.dox index 0436b791a5..d458f499bd 100644 --- a/src/hooks/dhcp/flex_option/flex_option.dox +++ b/src/hooks/dhcp/flex_option/flex_option.dox @@ -99,6 +99,16 @@ The sole parameter is a options list of options with: definition. - @b client-class - Specifies the guard i.e. the client class the query must belong to. + - @b source - Specifies what is the source for expression evaluation. + The default is the query but this can be changed to the response. + Note that if an expression includes a member clause the query classes + are copied to the response to make this to work. + - @b destination - Specifies what is the destination of option changes. + The default is the response but this can be changed to the query. + Note the change is performed before the classification so + @ client-class or a member clause in expression is very likely + to not work as expected. A warning is emitted when this situation + is detected when the configuration is parsed. 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 to set the @@ -121,7 +131,9 @@ decodes the option configurations. @ref unload() free the configuration. Kea engine checks if the library has functions that match known hook point names. This library has two such functions: @ref pkt4_send and @ref pkt6_send, -all located in flex_option_callouts.cc. +all located in flex_option_callouts.cc. The @b destination feature added +@ref pkt4_receive and @ref pkt6_receive, of course only the query is passed +when they are called. kea-dhcp4 server calls @ref pkt4_send (and kea-dhcp6 @ref pkt6_send) with the query and response packets. For each configured option and sub-option diff --git a/src/hooks/dhcp/flex_option/flex_option_messages.cc b/src/hooks/dhcp/flex_option/flex_option_messages.cc index 0f5cc6a69b..c1d5fc88d6 100644 --- a/src/hooks/dhcp/flex_option/flex_option_messages.cc +++ b/src/hooks/dhcp/flex_option/flex_option_messages.cc @@ -4,6 +4,8 @@ #include #include +extern const isc::log::MessageID FLEX_OPTION_CONFIG_SUB_USELESS_CLASS = "FLEX_OPTION_CONFIG_SUB_USELESS_CLASS"; +extern const isc::log::MessageID FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER = "FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER"; extern const isc::log::MessageID FLEX_OPTION_CONFIG_USELESS_CLASS = "FLEX_OPTION_CONFIG_USELESS_CLASS"; extern const isc::log::MessageID FLEX_OPTION_CONFIG_USELESS_MEMBER = "FLEX_OPTION_CONFIG_USELESS_MEMBER"; extern const isc::log::MessageID FLEX_OPTION_LOAD_ERROR = "FLEX_OPTION_LOAD_ERROR"; @@ -22,8 +24,10 @@ extern const isc::log::MessageID FLEX_OPTION_UNLOAD = "FLEX_OPTION_UNLOAD"; namespace { const char* values[] = { - "FLEX_OPTION_CONFIG_USELESS_CLASS", "For the option '%1' the client class '%2' is required before classification for a 'query' destination", - "FLEX_OPTION_CONFIG_USELESS_MEMBER", "For the option '%1' the member expression '%2' is evaluated before classification for a 'query' destination", + "FLEX_OPTION_CONFIG_SUB_USELESS_CLASS", "For the sub-option code %1 in option code %2 the client class %3 is required before classification for a query destination", + "FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER", "For the sub-option code %1 in option code %2 the member expression %3 is evaluated before classification for a query destination", + "FLEX_OPTION_CONFIG_USELESS_CLASS", "For the option code %1 the client class %2 is required before classification for a query destination", + "FLEX_OPTION_CONFIG_USELESS_MEMBER", "For the option code %1 the member expression %2 is evaluated before classification for a query destination", "FLEX_OPTION_LOAD_ERROR", "loading Flex Option hooks library failed: %1", "FLEX_OPTION_PROCESS_ADD", "Added the option code %1 with value %2", "FLEX_OPTION_PROCESS_CLIENT_CLASS", "Skip processing of the option code %1 for class '%2'", diff --git a/src/hooks/dhcp/flex_option/flex_option_messages.h b/src/hooks/dhcp/flex_option/flex_option_messages.h index 67950a2cb3..599d59c30d 100644 --- a/src/hooks/dhcp/flex_option/flex_option_messages.h +++ b/src/hooks/dhcp/flex_option/flex_option_messages.h @@ -5,6 +5,8 @@ #include +extern const isc::log::MessageID FLEX_OPTION_CONFIG_SUB_USELESS_CLASS; +extern const isc::log::MessageID FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER; extern const isc::log::MessageID FLEX_OPTION_CONFIG_USELESS_CLASS; extern const isc::log::MessageID FLEX_OPTION_CONFIG_USELESS_MEMBER; extern const isc::log::MessageID FLEX_OPTION_LOAD_ERROR; diff --git a/src/hooks/dhcp/flex_option/flex_option_messages.mes b/src/hooks/dhcp/flex_option/flex_option_messages.mes index 18da2940d4..7402f50c0e 100644 --- a/src/hooks/dhcp/flex_option/flex_option_messages.mes +++ b/src/hooks/dhcp/flex_option/flex_option_messages.mes @@ -9,13 +9,27 @@ This error message indicates an error during loading the Flex Option hooks library. The details of the error are provided as argument of the log message. -% FLEX_OPTION_CONFIG_USELESS_CLASS For the option '%1' the client class '%2' is required before classification for a 'query' destination +% FLEX_OPTION_CONFIG_SUB_USELESS_CLASS For the sub-option code %1 in option code %2 the client class %3 is required before classification for a query destination This warning message indicates the config specifies a class requirement when the destination is the query but the callout point for patching queries is before the classification so it will very likely not work -as expected. The code of the option and the client class are displayed. +as expected. The codes of the sub-option and option, and the client +class name are displayed. -% FLEX_OPTION_CONFIG_USELESS_MEMBER For the option '%1' the member expression '%2' is evaluated before classification for a 'query' destination +% FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER For the sub-option code %1 in option code %2 the member expression %3 is evaluated before classification for a query destination +This warning message indicates the config specifies an expression +with a member clause when the destination is the query but the callout point +for patching queries is before the classification so it will very likely +not work as expected. The code of the sub-option and option, and the +expression are displayed. + +% FLEX_OPTION_CONFIG_USELESS_CLASS For the option code %1 the client class %2 is required before classification for a query destination +This warning message indicates the config specifies a class requirement +when the destination is the query but the callout point for patching +queries is before the classification so it will very likely not work +as expected. The code of the option and the client class name are displayed. + +% FLEX_OPTION_CONFIG_USELESS_MEMBER For the option code %1 the member expression %2 is evaluated before classification for a query destination This warning message indicates the config specifies an expression with a member clause when the destination is the query but the callout point for patching queries is before the classification so it will very likely 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 92d56f0f2e..b180181f0e 100644 --- a/src/hooks/dhcp/flex_option/tests/flex_option_unittests.cc +++ b/src/hooks/dhcp/flex_option/tests/flex_option_unittests.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -25,6 +26,7 @@ using namespace std; using namespace isc; using namespace isc::data; using namespace isc::dhcp; +using namespace isc::dhcp::test; using namespace isc::eval; using namespace isc::hooks; using namespace isc::flex_option; @@ -1881,4 +1883,151 @@ TEST_F(FlexOptionTest, processQuery) { EXPECT_EQ(0U, buffer_fqdn[12]); } +// Verify that the source is followed: derived from processFullTest: +// - the source is set to response +// - the trigger option is added to the response +TEST_F(FlexOptionTest, processSource) { + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr code = Element::create(DHO_BOOT_FILE_NAME); + option->set("code", code); + string expr = "ifelse(option[host-name].exists,"; + expr += "concat(option[host-name].text,'.boot'),'')"; + ElementPtr add = Element::create(expr); + option->set("add", add); + ElementPtr source = Element::create(string("response")); + option->set("source", source); + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 12345)); + Pkt4Ptr response(new Pkt4(DHCPOFFER, 12345)); + OptionStringPtr str(new OptionString(Option::V4, DHO_HOST_NAME, "foo")); + response->addOption(str); + EXPECT_FALSE(query->getOption(DHO_HOST_NAME)); + EXPECT_FALSE(response->getOption(DHO_BOOT_FILE_NAME)); + + EXPECT_NO_THROW(impl_->process(Option::V4, query, response)); + + OptionPtr opt = response->getOption(DHO_BOOT_FILE_NAME); + ASSERT_TRUE(opt); + EXPECT_EQ(DHO_BOOT_FILE_NAME, opt->getType()); + const OptionBuffer& buffer = opt->getData(); + ASSERT_EQ(8U, buffer.size()); + EXPECT_EQ(0, memcmp(&buffer[0], "foo.boot", 8)); +} + +// Verify that a member in the expression works with the response source. +TEST_F(FlexOptionTest, processMemberSource) { + CfgMgr::instance().setFamily(AF_INET6); + + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr code = Element::create(D6O_BOOTFILE_URL); + option->set("code", code); + ElementPtr remove = Element::create(string("member('foobar')")); + option->set("remove", remove); + ElementPtr source = Element::create(string("response")); + option->set("source", source); + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 12345)); + query->addClass("foobar"); + Pkt6Ptr response(new Pkt6(DHCPV6_ADVERTISE, 12345)); + string response_txt = response->toText(); + OptionStringPtr str(new OptionString(Option::V6, D6O_BOOTFILE_URL, "http")); + response->addOption(str); + + EXPECT_NO_THROW(impl_->process(Option::V6, query, response)); + + EXPECT_EQ(response_txt, response->toText()); + EXPECT_FALSE(response->getOption(D6O_BOOTFILE_URL)); + // No magic: we simply copied classes from query to response... + EXPECT_TRUE(response->inClass("foobar")); +} + +/// @brief Test fixture for testing warnings from the Flex Option library. +class FlexOptionLogTest : public LogContentTest { +public: + /// @brief Constructor. + FlexOptionLogTest() { + impl_.reset(new TestFlexOptionImpl()); + CfgMgr::instance().setFamily(AF_INET); + } + + /// @brief Destructor. + virtual ~FlexOptionLogTest() { + LibDHCP::clearRuntimeOptionDefs(); + CfgMgr::instance().setFamily(AF_INET); + impl_.reset(); + } + + /// @brief Flex Option implementation. + TestFlexOptionImplPtr impl_; +}; + +// Verify that client-classes does not trigger a warning by default. +TEST_F(FlexOptionLogTest, noWarning) { + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr add = Element::create(string("'abc'")); + option->set("add", add); + ElementPtr code = Element::create(DHO_HOST_NAME); + option->set("code", code); + option->set("client-class", Element::create(string("foobar"))); + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_CLASS")); + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_MEMBER")); +} + +// Verify that client-classes triggers a warning with query destination. +TEST_F(FlexOptionLogTest, classWarning) { + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr add = Element::create(string("'abc'")); + option->set("add", add); + ElementPtr code = Element::create(DHO_HOST_NAME); + option->set("code", code); + option->set("client-class", Element::create(string("foobar"))); + ElementPtr dest = Element::create(string("query")); + option->set("destination", dest); + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + string expected = "FLEX_OPTION_CONFIG_USELESS_CLASS "; + expected += "For the option code 12 the client class foobar is "; + expected += "required before classification for a query destination"; + EXPECT_EQ(1U, countFile(expected)); + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_MEMBER")); +} + +// Verify that client-classes does not trigger a warning by default. +TEST_F(FlexOptionLogTest, memberWarning) { + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr remove = Element::create(string("member('foobar')")); + option->set("remove", remove); + ElementPtr code = Element::create(DHO_HOST_NAME); + option->set("code", code); + ElementPtr dest = Element::create(string("query")); + option->set("destination", dest); + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_CLASS")); + string expected = "FLEX_OPTION_CONFIG_USELESS_MEMBER "; + expected += "For the option code 12 the member expression "; + expected += "member('foobar') is "; + expected += "evaluated before classification for a query destination"; + EXPECT_EQ(1U, countFile(expected)); +} + } // end of anonymous namespace diff --git a/src/hooks/dhcp/flex_option/tests/meson.build b/src/hooks/dhcp/flex_option/tests/meson.build index da0c58c43e..f1715be074 100644 --- a/src/hooks/dhcp/flex_option/tests/meson.build +++ b/src/hooks/dhcp/flex_option/tests/meson.build @@ -12,6 +12,6 @@ dhcp_flex_option_lib_tests = executable( ], dependencies: [GTEST_DEP, CRYPTO_DEP], include_directories: [include_directories('.'), include_directories('..')] + INCLUDES, - link_with: [dhcp_flex_option_archive] + LIBS_BUILT_SO_FAR, + link_with: [dhcp_flex_option_archive, kea_testutils_lib] + LIBS_BUILT_SO_FAR, ) test('dhcp-flex-option-lib-tests', dhcp_flex_option_lib_tests, protocol: 'gtest') diff --git a/src/hooks/dhcp/flex_option/tests/sub_option_unittests.cc b/src/hooks/dhcp/flex_option/tests/sub_option_unittests.cc index 85b4c58a41..57de021a00 100644 --- a/src/hooks/dhcp/flex_option/tests/sub_option_unittests.cc +++ b/src/hooks/dhcp/flex_option/tests/sub_option_unittests.cc @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -29,6 +30,7 @@ using namespace isc; using namespace isc::asiolink; using namespace isc::data; using namespace isc::dhcp; +using namespace isc::dhcp::test; using namespace isc::eval; using namespace isc::hooks; using namespace isc::flex_option; @@ -1080,6 +1082,43 @@ TEST_F(FlexSubOptionTest, subOptionConfigComplex) { // Empty sub-option config list doing nothing is the same as empty option list. +// Verify that response processing does nothing with no response. +TEST_F(FlexSubOptionTest, subProcessNoResponse) { + OptionDefSpaceContainer defs; + OptionDefinitionPtr def(new OptionDefinition("my-container", 222, + DHCP4_OPTION_SPACE, "empty", + "my-space")); + defs.addItem(def); + OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space", + "fqdn", true)); + defs.addItem(sdef); + EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs)); + + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr code = Element::create(222); + option->set("code", code); + ElementPtr sub_options = Element::createList(); + option->set("sub-options", sub_options); + ElementPtr sub_option = Element::createMap(); + sub_options->add(sub_option); + ElementPtr space = Element::create(string("my-space")); + sub_option->set("space", space); + ElementPtr add = Element::create(string("'example.com'")); + sub_option->set("add", add); + ElementPtr name = Element::create(string("my-option")); + sub_option->set("name", name); + sub_option->set("csv-format", Element::create(true)); + + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 12345)); + + EXPECT_NO_THROW(impl_->process(Option::V4, query, Pkt4Ptr())); +} + // Verify that NONE action really does nothing. TEST_F(FlexSubOptionTest, subProcessNone) { auto rai_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE, @@ -3170,4 +3209,327 @@ TEST_F(FlexSubOptionTest, subOptionConfigGuardSubOptiondMatch) { EXPECT_EQ(0, memcmp(&buffer[0], "abc", 3)); } +// Verify that ADD action adds the specified sub-option in an already +// existing container option with a query destination. +TEST_F(FlexSubOptionTest, subProcessQuery) { + OptionDefSpaceContainer defs; + OptionDefinitionPtr def(new OptionDefinition("my-container", 222, + DHCP4_OPTION_SPACE, "empty", + "my-space")); + defs.addItem(def); + OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space", + "string")); + defs.addItem(sdef); + EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs)); + + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr code = Element::create(222); + option->set("code", code); + ElementPtr sub_options = Element::createList(); + option->set("sub-options", sub_options); + ElementPtr dest = Element::create(string("query")); + option->set("destination", dest); + ElementPtr sub_option = Element::createMap(); + sub_options->add(sub_option); + ElementPtr space = Element::create(string("my-space")); + sub_option->set("space", space); + ElementPtr add = Element::create(string("'abc'")); + sub_option->set("add", add); + ElementPtr name = Element::create(string("my-option")); + sub_option->set("name", name); + + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 12345)); + OptionPtr container(new Option(Option::V4, 222)); + query->addOption(container); + EXPECT_TRUE(query->getOption(222)); + + EXPECT_NO_THROW(impl_->process(Option::V4, query, Pkt4Ptr())); + + // Only one option with code 222. + EXPECT_EQ(1U, query->options_.count(222)); + + OptionPtr opt = query->getOption(222); + ASSERT_TRUE(opt); + EXPECT_EQ(222U, opt->getType()); + OptionPtr sub = opt->getOption(1); + ASSERT_TRUE(sub); + EXPECT_EQ(1U, sub->getType()); + const OptionBuffer& buffer = sub->getData(); + ASSERT_EQ(3U, buffer.size()); + EXPECT_EQ(0, memcmp(&buffer[0], "abc", 3)); + + // Only one sub-option. + auto const& opts = opt->getOptions(); + EXPECT_EQ(1U, opts.size()); +} + +// Verify that ADD action adds the specified sub-option in an already +// existing container option with a query destination. +TEST_F(FlexSubOptionTest, subProcessSource) { + OptionDefSpaceContainer defs; + OptionDefinitionPtr def(new OptionDefinition("my-container", 222, + DHCP4_OPTION_SPACE, "empty", + "my-space")); + defs.addItem(def); + OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space", + "string")); + defs.addItem(sdef); + EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs)); + + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr code = Element::create(222); + option->set("code", code); + ElementPtr sub_options = Element::createList(); + option->set("sub-options", sub_options); + ElementPtr sub_option = Element::createMap(); + sub_options->add(sub_option); + ElementPtr space = Element::create(string("my-space")); + sub_option->set("space", space); + string expr = "ifelse(option[host-name].exists,"; + expr += "concat(option[host-name].text,'.boot'),'')"; + ElementPtr add = Element::create(expr); + sub_option->set("add", add); + ElementPtr name = Element::create(string("my-option")); + sub_option->set("name", name); + ElementPtr source = Element::create(string("response")); + option->set("source", source); + + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 12345)); + Pkt4Ptr response(new Pkt4(DHCPOFFER, 12345)); + OptionStringPtr str(new OptionString(Option::V4, DHO_HOST_NAME, "foo")); + response->addOption(str); + OptionPtr container(new Option(Option::V4, 222)); + response->addOption(container); + EXPECT_TRUE(response->getOption(222)); + + EXPECT_NO_THROW(impl_->process(Option::V4, query, response)); + + // Only one option with code 222. + EXPECT_EQ(1U, response->options_.count(222)); + + OptionPtr opt = response->getOption(222); + ASSERT_TRUE(opt); + EXPECT_EQ(222U, opt->getType()); + OptionPtr sub = opt->getOption(1); + ASSERT_TRUE(sub); + EXPECT_EQ(1U, sub->getType()); + const OptionBuffer& buffer = sub->getData(); + ASSERT_EQ(8U, buffer.size()); + EXPECT_EQ(0, memcmp(&buffer[0], "foo.boot", 3)); + + // Only one sub-option. + auto const& opts = opt->getOptions(); + EXPECT_EQ(1U, opts.size()); +} + +// Verify that a member in the expression works with the response source. +TEST_F(FlexSubOptionTest, subProcessMemberSource) { + OptionDefSpaceContainer defs; + OptionDefinitionPtr def(new OptionDefinition("my-container", 222, + DHCP4_OPTION_SPACE, "empty", + "my-space")); + defs.addItem(def); + OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space", + "string")); + defs.addItem(sdef); + EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs)); + + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr code = Element::create(222); + option->set("code", code); + ElementPtr sub_options = Element::createList(); + option->set("sub-options", sub_options); + ElementPtr source = Element::create(string("response")); + option->set("source", source); + + ElementPtr sub_option = Element::createMap(); + sub_options->add(sub_option); + ElementPtr space = Element::create(string("my-space")); + sub_option->set("space", space); + ElementPtr remove = Element::create(string("member('foobar')")); + sub_option->set("remove", remove); + ElementPtr name = Element::create(string("my-option")); + sub_option->set("name", name); + + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 12345)); + query->addClass("foobar"); + Pkt4Ptr response(new Pkt4(DHCPOFFER, 12345)); + string response_txt = response->toText(); + OptionPtr container(new Option(Option::V4, 222)); + response->addOption(container); + EXPECT_TRUE(response->getOption(222)); + OptionStringPtr str(new OptionString(Option::V4, 1, "xyzt")); + container->addOption(str); + + EXPECT_NO_THROW(impl_->process(Option::V4, query, response)); + + EXPECT_EQ(response_txt, response->toText()); + EXPECT_FALSE(response->getOption(222)); + // No magic: we simply copied classes from query to response... + EXPECT_TRUE(response->inClass("foobar")); +} + +/// @brief Test fixture for testing warnings from the Flex Option library. +class FlexSubOptionLogTest : public LogContentTest { +public: + /// @brief Constructor. + FlexSubOptionLogTest() { + impl_.reset(new TestFlexOptionImpl()); + CfgMgr::instance().setFamily(AF_INET); + } + + /// @brief Destructor. + virtual ~FlexSubOptionLogTest() { + LibDHCP::clearRuntimeOptionDefs(); + CfgMgr::instance().setFamily(AF_INET); + impl_.reset(); + } + + /// @brief Flex Option implementation. + TestFlexOptionImplPtr impl_; +}; + +// Verify that client-classes does not trigger a warning by default. +TEST_F(FlexSubOptionLogTest, noWarning) { + OptionDefSpaceContainer defs; + OptionDefinitionPtr def(new OptionDefinition("my-container", 222, + DHCP4_OPTION_SPACE, "empty", + "my-space")); + defs.addItem(def); + OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space", + "string")); + defs.addItem(sdef); + EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs)); + + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr code = Element::create(222); + option->set("code", code); + + ElementPtr sub_options = Element::createList(); + option->set("sub-options", sub_options); + ElementPtr sub_option = Element::createMap(); + sub_options->add(sub_option); + ElementPtr space = Element::create(string("my-space")); + sub_option->set("space", space); + ElementPtr add = Element::create(string("'abc'")); + sub_option->set("add", add); + ElementPtr name = Element::create(string("my-option")); + sub_option->set("name", name); + sub_option->set("client-class", Element::create(string("foobar"))); + + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_CLASS")); + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_MEMBER")); + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_SUB_USELESS_CLASS")); + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER")); +} + +// Verify that client-classes triggers a warning with query destination. +TEST_F(FlexSubOptionLogTest, classWarning) { + OptionDefSpaceContainer defs; + OptionDefinitionPtr def(new OptionDefinition("my-container", 222, + DHCP4_OPTION_SPACE, "empty", + "my-space")); + defs.addItem(def); + OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space", + "string")); + defs.addItem(sdef); + EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs)); + + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr code = Element::create(222); + option->set("code", code); + ElementPtr sub_options = Element::createList(); + option->set("sub-options", sub_options); + ElementPtr dest = Element::create(string("query")); + option->set("destination", dest); + + ElementPtr sub_option = Element::createMap(); + sub_options->add(sub_option); + ElementPtr space = Element::create(string("my-space")); + sub_option->set("space", space); + ElementPtr add = Element::create(string("'abc'")); + sub_option->set("add", add); + ElementPtr name = Element::create(string("my-option")); + sub_option->set("name", name); + sub_option->set("client-class", Element::create(string("foobar"))); + + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_CLASS")); + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_MEMBER")); + string expected = "FLEX_OPTION_CONFIG_SUB_USELESS_CLASS "; + expected += "For the sub-option code 1 in option code 222 "; + expected += "the client class foobar is "; + expected += "required before classification for a query destination"; + EXPECT_EQ(1U, countFile(expected)); + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER")); +} + +// Verify that client-classes does not trigger a warning by default. +TEST_F(FlexSubOptionLogTest, memberWarning) { + OptionDefSpaceContainer defs; + OptionDefinitionPtr def(new OptionDefinition("my-container", 222, + DHCP4_OPTION_SPACE, "empty", + "my-space")); + defs.addItem(def); + OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space", + "string")); + defs.addItem(sdef); + EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs)); + + ElementPtr options = Element::createList(); + ElementPtr option = Element::createMap(); + options->add(option); + ElementPtr code = Element::create(222); + option->set("code", code); + ElementPtr sub_options = Element::createList(); + option->set("sub-options", sub_options); + ElementPtr dest = Element::create(string("query")); + option->set("destination", dest); + + ElementPtr sub_option = Element::createMap(); + sub_options->add(sub_option); + ElementPtr space = Element::create(string("my-space")); + sub_option->set("space", space); + ElementPtr remove = Element::create(string("member('foobar')")); + sub_option->set("remove", remove); + ElementPtr name = Element::create(string("my-option")); + sub_option->set("name", name); + + EXPECT_NO_THROW(impl_->testConfigure(options)); + EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg(); + + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_CLASS")); + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_MEMBER")); + EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_SUB_USELESS_CLASS")); + string expected = "FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER "; + expected += "For the sub-option code 1 in option code 222 "; + expected += "the member expression member('foobar') is "; + expected += "evaluated before classification for a query destination"; + EXPECT_EQ(1U, countFile(expected)); +} + } // end of anonymous namespace