#include <dhcp/dhcp6.h>
#include <dhcp/option_space.h>
#include <util/encode/hex.h>
+#include <boost/algorithm/string/split.hpp>
+#include <boost/algorithm/string/classification.hpp>
#include <string>
#include <sstream>
#include <vector>
}
void
-CfgOption::merge(CfgOption& other) {
+CfgOption::merge(CfgOptionDefPtr cfg_def, CfgOption& other) {
// First we merge our options into other.
// This adds my opitions that are not
// in other, to other (i.e we skip over
- // duplicates).
+ // duplicates).
mergeTo(other);
+ // Iterate over all the options in all the spaces and
+ // validate them against the definitions.
+ for (auto space : other.getOptionSpaceNames()) {
+ for (auto opt_desc : *(other.getAll(space))) {
+ createDescriptorOption(cfg_def, space, opt_desc);
+ }
+ }
+
// Next we copy "other" on top of ourself.
other.copyTo(*this);
}
+void
+CfgOption::createDescriptorOption(CfgOptionDefPtr cfg_def, const std::string& space,
+ OptionDescriptor& opt_desc) {
+ if (!opt_desc.option_) {
+ isc_throw(BadValue,
+ "validateCreateOption: descriptor has no option instance");
+ }
+
+ Option::Universe universe = opt_desc.option_->getUniverse();
+ uint16_t code = opt_desc.option_->getType();
+
+ // Find the option's defintion, if it has one.
+ // First, check for a standard definition.
+ OptionDefinitionPtr def = LibDHCP::getOptionDef(space, code);
+
+ // If there is no standard definition but the option is vendor specific,
+ // we should search the definition within the vendor option space.
+ if (!def && (space != DHCP4_OPTION_SPACE) && (space != DHCP6_OPTION_SPACE)) {
+ uint32_t vendor_id = LibDHCP::optionSpaceToVendorId(space);
+ if (vendor_id > 0) {
+ def = LibDHCP::getVendorOptionDef(universe, vendor_id, code);
+ }
+ }
+
+ // Still haven't found the definition, so look for custom
+ // definition in the given set of configured definitions
+ if (!def) {
+ def = cfg_def->get(space, code);
+ }
+
+ std::string& formatted_value = opt_desc.formatted_value_;
+ if (!def) {
+ if (!formatted_value.empty()) {
+ isc_throw(InvalidOperation, "option: " << space << "." << code
+ << " has a formatted value: '" << formatted_value
+ << "' but no option definition");
+ }
+
+ // If there's no definition and no formatted string, we'll
+ // settle for the generic option already in the descriptor.
+ return;
+ }
+
+ try {
+
+ // Definition found. Let's replace the generic option in
+ // the descriptor with one created based on definition's factory.
+ if (formatted_value.empty()) {
+ // No formatted value, use data stored in the generic option.
+ opt_desc.option_ = def->optionFactory(universe, code, opt_desc.option_->getData());
+ } else {
+ // Spit the value specified in comma separated values format.
+ std::vector<std::string> split_vec;
+ boost::split(split_vec, formatted_value, boost::is_any_of(","));
+ opt_desc.option_ = def->optionFactory(universe, code, split_vec);
+ }
+ } catch (const std::exception& ex) {
+ isc_throw(InvalidOperation, "could not create option: " << space << "." << code
+ << " from data specified, reason: " << ex.what());
+ }
+}
+
void
CfgOption::mergeTo(CfgOption& other) const {
// Merge non-vendor options.
#include <cc/cfg_to_element.h>
#include <cc/stamped_element.h>
#include <cc/user_context.h>
+#include <dhcpsrv/cfg_option_def.h>
#include <dhcpsrv/key_from_key.h>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/hashed_index.hpp>
/// @brief Merges another option configuration into this one.
///
/// This method calls @c mergeTo() to add this configuration's
- /// options into @c other (skipping any duplicates). It then calls
- /// @c copyTo() to overwrite this configurations' options with
- /// the merged set in @c other.
+ /// options into @c other (skipping any duplicates). Next it calls
+ /// @c createDescriptorOption() for each option descriptor in the
+ /// merged set. This (re)-creates each descriptor's option based on
+ /// the merged set of opt definitioins. Finally, it calls
+ /// @c copyTo() to overwrite this configuration's options with
+ /// the merged set in @c other.
///
/// @warning The merge operation will affect the @c other configuration.
/// Therefore, the caller must not rely on the data held in the @c other
/// merged configuration.
///
/// @param option configurations to merge with.
- void merge(CfgOption& other);
+ void merge(CfgOptionDefPtr cfg_def, CfgOption& other);
+
+ /// @brief Creates an option descriptor's option based on a set of option defs
+ ///
+ /// This function's primary use is to create definition specific options for
+ /// option descriptors fetched from a configuration backend, as part of a
+ /// configuration merge.
+ ///
+ /// Given an OptionDescriptor whose option_ member contains a generic option
+ /// (i.e has a code and/or data), this function will attempt to find a matching
+ /// definition and then use that definition's factory to create an option
+ /// instance specific to that definition. It will then replace the descriptor's
+ /// generic option with the specific option.
+ ///
+ /// Three sources of definitions are searched, in the following order:
+ ///
+ /// 1. Standard option definitions (@c LIBDHCP::getOptionDef))
+ /// 2. Vendor option definitions (@c LIBDHCP::getVendorOptionDef))
+ /// 3. User specified definitions passed in via cfg_def parameter.
+ ///
+ /// The code will use the first matching definition found. It then applies
+ /// the following rules:
+ ///
+ /// -# If no definition is found but the descriptor conveys a non-empty
+ /// formatted value, throw an error.
+ /// -# If not definition is found and there is no formatted value, return
+ /// This leaves intact the generic option in the descriptor.
+ /// -# If a definition is found and there is no formatted value, pass the
+ /// descriptor's generic option's data into the definition's factory. Replace
+ /// the descriptor's option with the newly created option.
+ /// -# If a definition is found and there is a formatted value, split
+ /// the value into vector of values and pass that into the definition's
+ /// factory. Replace the descriptor's option with the newly created option.
+ ///
+ /// @param cfg_def the user specified definitions to use
+ /// @param space the option space name of the option
+ /// @param opt_desc OptionDescriptor describing the option.
+ ///
+ /// @throw InvalidOperation if the descriptor conveys a formatted value and
+ /// there is no definition matching the option code in the given space, or
+ /// if the definition factory invocation fails.
+ static void createDescriptorOption(CfgOptionDefPtr cfg_def, const std::string& space,
+ OptionDescriptor& opt_desc);
/// @brief Merges this configuration to another configuration.
///
// can be merged into an existing configuration, with any
// duplicates in other overwriting those in the existing
// configuration.
-TEST_F(CfgOptionTest, merge) {
+TEST_F(CfgOptionTest, validMerge) {
CfgOption this_cfg;
CfgOption other_cfg;
- // Create collection of options in option space dhcp6, with option codes
- // from the range of 100 to 109 and holding one byte of data equal to 0xFF.
- for (uint16_t code = 100; code < 110; ++code) {
- OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF)));
- ASSERT_NO_THROW(this_cfg.add(option, false, DHCP6_OPTION_SPACE));
- }
+ // We need to create a dictionary of defintions pass into option merge.
+ CfgOptionDefPtr defs(new CfgOptionDef());
+ defs->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "uint8"))), "isc");
+ defs->add((OptionDefinitionPtr(new OptionDefinition("two", 2, "uint8"))), "isc");
+ defs->add((OptionDefinitionPtr(new OptionDefinition("four", 4, "uint8"))), "isc");
+ defs->add((OptionDefinitionPtr(new OptionDefinition("three", 3, "uint8"))), "fluff");
+ defs->add((OptionDefinitionPtr(new OptionDefinition("four", 4, "uint8"))), "fluff");
- // Create collection of options in vendor space 123, with option codes
- // from the range of 100 to 109 and holding one byte of data equal to 0xFF.
- for (uint16_t code = 100; code < 110; code += 2) {
- OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0xFF)));
- ASSERT_NO_THROW(this_cfg.add(option, false, "vendor-123"));
- }
+ // Create our existing config, that gets merged into.
+ OptionPtr option(new Option(Option::V4, 1, OptionBuffer(1, 0x01)));
+ ASSERT_NO_THROW(this_cfg.add(option, false, "isc"));
- // Create destination configuration (configuration that we merge the
- // other configuration to).
+ // Add option 3 to "fluff"
+ option.reset(new Option(Option::V4, 3, OptionBuffer(1, 0x03)));
+ ASSERT_NO_THROW(this_cfg.add(option, false, "fluff"));
- // Create collection of options having even option codes in the range of
- // 100 to 108.
- for (uint16_t code = 100; code < 110; code += 2) {
- OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0x01)));
- ASSERT_NO_THROW(other_cfg.add(option, false, DHCP6_OPTION_SPACE));
- }
+ // Add option 4 to "fluff".
+ option.reset(new Option(Option::V4, 4, OptionBuffer(1, 0x04)));
+ ASSERT_NO_THROW(this_cfg.add(option, false, "fluff"));
- // Create collection of options having odd option codes in the range of
- // 101 to 109.
- for (uint16_t code = 101; code < 110; code += 2) {
- OptionPtr option(new Option(Option::V6, code, OptionBuffer(1, 0x01)));
- ASSERT_NO_THROW(other_cfg.add(option, false, "vendor-123"));
- }
+ // Create our other config that will be merged from.
+ // Add Option 1 to "isc", this should "overwrite" the original.
+ option.reset(new Option(Option::V4, 1, OptionBuffer(1, 0x10)));
+ ASSERT_NO_THROW(other_cfg.add(option, false, "isc"));
+
+ // Add option 2 to "isc".
+ option.reset(new Option(Option::V4, 2, OptionBuffer(1, 0x20)));
+ ASSERT_NO_THROW(other_cfg.add(option, false, "isc"));
+
+ // Add option 4 to "isc".
+ option.reset(new Option(Option::V4, 4, OptionBuffer(1, 0x40)));
+ ASSERT_NO_THROW(other_cfg.add(option, false, "isc"));
// Merge source configuration to the destination configuration. The options
// in the destination should be preserved. The options from the source
// configuration should be added.
- ASSERT_NO_THROW(this_cfg.merge(other_cfg));
-
+ try {
+ this_cfg.merge(defs, other_cfg);
+ } catch(const std::exception& ex) {
+ GTEST_FAIL () << "Unexpected exception:" << ex.what();
+ }
+
+// ASSERT_NO_THROW(this_cfg.merge(defs, other_cfg));
+
+ // isc:1 should come from "other" config.
+ OptionDescriptor desc = this_cfg.get("isc", 1);
+ ASSERT_TRUE(desc.option_);
+ ASSERT_EQ(1, desc.option_->getData().size());
+ EXPECT_EQ(0x10, desc.option_->getData()[0]);
+
+ // isc:2 should come from "other" config.
+ desc = this_cfg.get("isc", 2);
+ ASSERT_TRUE(desc.option_);
+ ASSERT_EQ(1, desc.option_->getData().size());
+ EXPECT_EQ(0x20, desc.option_->getData()[0]);
+
+ // isc:4 should come from "other" config.
+ desc = this_cfg.get("isc", 4);
+ ASSERT_TRUE(desc.option_);
+ ASSERT_EQ(1, desc.option_->getData().size());
+ EXPECT_EQ(0x40, desc.option_->getData()[0]);
+
+ // fluff:3 should come from "this" config.
+ desc = this_cfg.get("fluff", 3);
+ ASSERT_TRUE(desc.option_);
+ ASSERT_EQ(1, desc.option_->getData().size());
+ EXPECT_EQ(0x03, desc.option_->getData()[0]);
+
+ // fluff:4 should come from "this" config.
+ desc = this_cfg.get("fluff", 4);
+ ASSERT_TRUE(desc.option_);
+ ASSERT_EQ(1, desc.option_->getData().size());
+ EXPECT_EQ(0x4, desc.option_->getData()[0]);
+}
- // Validate the options in the dhcp6 option space in the destination.
- for (uint16_t code = 100; code < 110; ++code) {
- OptionDescriptor desc = this_cfg.get(DHCP6_OPTION_SPACE, code);
- ASSERT_TRUE(desc.option_);
- ASSERT_EQ(1, desc.option_->getData().size());
- // The options with even option codes should hold one byte of data
- // equal to 0x1. These are the ones that we have initially added to
- // the destination configuration. The other options should hold the
- // values of 0xFF which indicates that they have been merged from the
- // source configuration.
- if ((code % 2) == 0) {
- EXPECT_EQ(0x01, desc.option_->getData()[0]);
- } else {
- EXPECT_EQ(0xFF, desc.option_->getData()[0]);
- }
- }
+TEST_F(CfgOptionTest, invalidMerge) {
+ CfgOption this_cfg;
+ CfgOption other_cfg;
- // Validate the options in the vendor space.
- for (uint16_t code = 100; code < 110; ++code) {
- OptionDescriptor desc = this_cfg.get(123, code);
- ASSERT_TRUE(desc.option_);
- ASSERT_EQ(1, desc.option_->getData().size());
- // This time, the options with even option codes should hold a byte
- // of data equal to 0xFF. The other options should hold the byte of
- // data equal to 0x01.
- if ((code % 2) == 0) {
- EXPECT_EQ(0xFF, desc.option_->getData()[0]);
- } else {
- EXPECT_EQ(0x01, desc.option_->getData()[0]);
- }
+ // Create an empty dictionary of defintions pass into option merge.
+ CfgOptionDefPtr defs(new CfgOptionDef());
+
+ // Create our other config that will be merged from.
+ // Add an option without a formatted value.
+ OptionPtr option(new Option(Option::V4, 1, OptionBuffer(1, 0x01)));
+ ASSERT_NO_THROW(other_cfg.add(option, false, "isc"));
+
+ // Add an option with a formatted value.
+ option.reset(new Option(Option::V4, 2));
+ OptionDescriptor desc(option, false, "one,two,three");
+ ASSERT_NO_THROW(other_cfg.add(desc, "isc"));
+
+ // When we attempt to merge, it should fail, recognizing that
+ // option two has no definition.
+ try {
+ this_cfg.merge(defs, other_cfg);
+ GTEST_FAIL() << "merge should have thrown";
+ } catch (const InvalidOperation& ex) {
+ std::string exp_msg = "option: isc.2 has a formatted value: "
+ "'one,two,three' but no option definition";
+ EXPECT_EQ(std::string(exp_msg), std::string(ex.what()));
+ } catch (const std::exception& ex) {
+ GTEST_FAIL() << "wrong exception thrown:" << ex.what();
+ }
+
+ // Now let's add an option definition that will force data truncated
+ // error for option one.
+ defs->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "uint16"))), "isc");
+
+ // When we attempt to merge, it should fail because option one's data
+ // is not valid per its definition.
+ try {
+ this_cfg.merge(defs, other_cfg);
+ GTEST_FAIL() << "merge should have thrown";
+ } catch (const InvalidOperation& ex) {
+ std::string exp_msg = "could not create option: isc.1"
+ " from data specified, reason:"
+ " Option 1 truncated";
+ EXPECT_EQ(std::string(exp_msg), std::string(ex.what()));
+ } catch (const std::exception& ex) {
+ GTEST_FAIL() << "wrong exception thrown:" << ex.what();
}
}
-
// This test verifies that encapsulated options are added as sub-options
// to the top level options on request.
TEST_F(CfgOptionTest, encapsulate) {