#include <stdint.h>
using namespace std;
+using namespace isc;
using namespace isc::data;
using namespace isc::dhcp;
using namespace isc::asiolink;
namespace {
+// Forward declarations of some of the parser classes.
+// They are used to define pointer types for these classes.
+class BooleanParser;
+class StringParser;
+class Uint32Parser;
+
+// Pointers to various parser objects.
+typedef boost::shared_ptr<BooleanParser> BooleanParserPtr;
+typedef boost::shared_ptr<StringParser> StringParserPtr;
+typedef boost::shared_ptr<Uint32Parser> Uint32ParserPtr;
+
/// @brief Auxiliary type used for storing an element name and its parser.
typedef pair<string, ConstElementPtr> ConfigPair;
/// @brief Collection of elements that store string values.
typedef std::map<string, string> StringStorage;
+/// @brief Storage for option definitions.
+typedef OptionSpaceContainer<OptionDefContainer,
+ OptionDefinitionPtr> OptionDefStorage;
+
/// @brief Collection of address pools.
///
/// This type is used as intermediate storage, when pools are parsed, but there is
ParserCollection parsers_;
};
+/// @brief Parser for a single option definition.
+///
+/// This parser creates an instance of a single option definition.
+class OptionDefParser: DhcpConfigParser {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// This constructor sets the pointer to the option definitions
+ /// storage to NULL. It must be set to point to the actual storage
+ /// before \ref build is called.
+ OptionDefParser(const std::string&)
+ : storage_(NULL) {
+ }
+
+ /// @brief Parses an entry that describes single option definition.
+ ///
+ /// @param option_def a configuration entry to be parsed.
+ ///
+ /// @throw DhcpConfigError if parsing was unsuccessful.
+ void build(ConstElementPtr option_def) {
+ if (storage_ == NULL) {
+ isc_throw(DhcpConfigError, "parser logic error: storage must be set"
+ " before parsing option definition data");
+ }
+ // Parse the elements that make up the option definition.
+ BOOST_FOREACH(ConfigPair param, option_def->mapValue()) {
+ std::string entry(param.first);
+ ParserPtr parser;
+ if (entry == "name" || entry == "type" ||
+ entry == "record-types" || entry == "space") {
+ StringParserPtr
+ str_parser(dynamic_cast<StringParser*>(StringParser::factory(entry)));
+ if (str_parser) {
+ str_parser->setStorage(&string_values_);
+ parser = str_parser;
+ }
+ } else if (entry == "code") {
+ Uint32ParserPtr
+ code_parser(dynamic_cast<Uint32Parser*>(Uint32Parser::factory(entry)));
+ if (code_parser) {
+ code_parser->setStorage(&uint32_values_);
+ parser = code_parser;
+ }
+ } else if (entry == "array") {
+ BooleanParserPtr
+ array_parser(dynamic_cast<BooleanParser*>(BooleanParser::factory(entry)));
+ if (array_parser) {
+ array_parser->setStorage(&boolean_values_);
+ parser = array_parser;
+ }
+ } else {
+ isc_throw(DhcpConfigError, "invalid parameter: " << entry);
+ }
+
+ parser->build(param.second);
+ parser->commit();
+ }
+
+ // Create an instance of option definition.
+ createOptionDef();
+
+ // Get all items we collected so far for the particular option space.
+ OptionDefContainerPtr defs = storage_->getItems(option_space_name_);
+ // Check if there are any items with option code the same as the
+ // one specified for the definition we are now creating.
+ const OptionDefContainerTypeIndex& idx = defs->get<1>();
+ const OptionDefContainerTypeRange& range =
+ idx.equal_range(option_definition_->getCode());
+ // If there are any items with this option code already we need
+ // to issue an error because we don't allow duplicates for
+ // option definitions within an option space.
+ if (std::distance(range.first, range.second) > 0) {
+ isc_throw(DhcpConfigError, "duplicated option definition for"
+ << " code '" << option_definition_->getCode() << "'");
+ }
+ }
+
+ /// @brief Stores the parsed option definition in a storage.
+ void commit() {
+ // @todo validate option space name once 2313 is merged.
+ if (storage_ && option_definition_) {
+ storage_->addItem(option_definition_, option_space_name_);
+ }
+ }
+
+ /// @brief Sets a pointer to a storage.
+ ///
+ /// The newly created instance of an option definition will be
+ /// added to a storage given by the argument.
+ ///
+ /// @param storage pointer to a storage where the option definition
+ /// will be added to.
+ void setStorage(OptionDefStorage* storage) {
+ storage_ = storage;
+ }
+
+private:
+
+ /// @brief Create option definition from the parsed parameters.
+ void createOptionDef() {
+ // Get the option space name and validate it.
+ std::string space = getParam<std::string>("space", string_values_);
+ // @todo uncomment the code below when the #2313 is merged.
+ /* if (!OptionSpace::validateName()) {
+ isc_throw(DhcpConfigError, "invalid option space name '"
+ << space << "'");
+ } */
+
+ // Get other parameters that are needed to create the
+ // option definition.
+ std::string name = getParam<std::string>("name", string_values_);
+ uint32_t code = getParam<uint32_t>("code", uint32_values_);
+ std::string type = getParam<std::string>("type", string_values_);
+ bool array_type = getParam<bool>("array", boolean_values_);
+
+ OptionDefinitionPtr def(new OptionDefinition(name, code,
+ type, array_type));
+ // The record-types field may carry a list of comma separated names
+ // of data types that form a record.
+ std::string record_types = getParam<std::string>("record-types",
+ string_values_);
+ // Split the list of record types into tokens.
+ std::vector<std::string> record_tokens =
+ isc::util::str::tokens(record_types, ",");
+ // Iterate over each token and add a record typy into
+ // option definition.
+ BOOST_FOREACH(std::string record_type, record_tokens) {
+ try {
+ boost::trim(record_type);
+ if (!record_type.empty()) {
+ def->addRecordField(record_type);
+ }
+ } catch (const Exception& ex) {
+ isc_throw(DhcpConfigError, "invalid record type values"
+ << " specified for the option definition: "
+ << ex.what());
+ }
+ }
+
+ // Check the option definition parameters are valid.
+ try {
+ def->validate();
+ } catch (const isc::Exception ex) {
+ isc_throw(DhcpConfigError, "invalid option definition"
+ << " parameters" << ex.what());
+ }
+ // Option definition has been created successfully.
+ option_space_name_ = space;
+ option_definition_ = def;
+ }
+
+ /// Instance of option definition being created by this parser.
+ OptionDefinitionPtr option_definition_;
+
+ std::string option_space_name_;
+
+ /// Pointer to a storage where the option definition will be
+ /// added when \ref commit is called.
+ OptionDefStorage* storage_;
+
+ /// Storage for boolean values.
+ BooleanStorage boolean_values_;
+ /// Storage for string values.
+ StringStorage string_values_;
+ /// Storage for uint32 values.
+ Uint32Storage uint32_values_;
+};
+
+/// @brief Parser for a list of option definitions.
+///
+/// This parser iterates over all configuration entries that define
+/// option definitions and creates instances of these definitions.
+/// If the parsing is successful, the collection of created definitions
+/// is put into the provided storage.
+class OptionDefListParser : DhcpConfigParser {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// This constructor initializes the pointer to option definitions
+ /// storage to NULL value. This pointer has to be set to point to
+ /// the actual storage before the \ref build function is called.
+ OptionDefListParser(const std::string&) {
+ }
+
+ /// @brief Parse configuration entries.
+ ///
+ /// This function parses configuration entries and creates instances
+ /// of option definitions.
+ ///
+ /// @param option_def_list pointer to an element that holds entries
+ /// that define option definitions.
+ /// @throw DhcpConfigError if configuration parsing fails.
+ void build(ConstElementPtr option_def_list) {
+ if (!option_def_list) {
+ isc_throw(DhcpConfigError, "parser error: a pointer to a list of"
+ << " option definitions is NULL");
+ }
+
+ BOOST_FOREACH(ConstElementPtr option_def, option_def_list->listValue()) {
+ boost::shared_ptr<OptionDefParser>
+ parser(new OptionDefParser("single-option-def"));
+ parser->setStorage(&option_defs_local_);
+ parser->build(option_def);
+ parser->commit();
+ }
+ }
+
+ /// @brief Stores option definitions in the provided storage.
+ void commit() {
+
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ cfg_mgr.deleteOptionDefs();
+
+ // We need to move option definitions from the temporary
+ // storage to the global storage.
+ BOOST_FOREACH(std::string space_name,
+ option_defs_local_.getOptionSpaceNames()) {
+
+ BOOST_FOREACH(OptionDefinitionPtr def,
+ *option_defs_local_.getItems(space_name)) {
+ assert(def);
+ cfg_mgr.addOptionDef(def, space_name);
+ }
+ }
+ }
+
+ /// @brief Create an OptionDefListParser object.
+ ///
+ /// @param param_name configuration entry holding option definitions.
+ ///
+ /// @return OptionDefListParser object.
+ static DhcpConfigParser* factory(const std::string& param_name) {
+ return (new OptionDefListParser(param_name));
+ }
+
+private:
+
+ /// Temporary storage for option definitions. It holds option
+ /// definitions before \ref commit is called.
+ OptionDefStorage option_defs_local_;
+};
+
/// @brief this class parses a single subnet
///
/// This class parses the whole subnet definition. It creates parsers
factories["interface"] = InterfaceListConfigParser::factory;
factories["subnet6"] = Subnets6ListConfigParser::factory;
factories["option-data"] = OptionDataListParser::factory;
+ factories["option-def"] = OptionDefListParser::factory;
factories["version"] = StringParser::factory;
FactoryMap::iterator f = factories.find(config_id);
resetConfiguration();
};
+ // Checks if config_result (result of DHCP server configuration) has
+ // expected code (0 for success, other for failures).
+ // Also stores result in rcode_ and comment_.
+ void checkResult(ConstElementPtr status, int expected_code) {
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_EQ(expected_code, rcode_);
+ }
+
/// @brief Create the simple configuration with single option.
///
/// This function allows to set one of the parameters that configure
"\"renew-timer\": 1000, "
"\"valid-lifetime\": 4000, "
"\"subnet6\": [ ], "
+ "\"option-def\": [ ], "
"\"option-data\": [ ] }";
try {
EXPECT_EQ(4000, subnet->getValid());
}
+// The goal of this test is to check whether an option definition
+// that defines an option carrying an IPv6 address can be created.
+TEST_F(Dhcp6ParserTest, optionDefIpv6Address) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"ipv6-address\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ElementPtr json = Element::fromJSON(config);
+
+ // Make sure that the particular option definition does not exist.
+ OptionDefinitionPtr def = CfgMgr::instance().getOptionDef("isc", 100);
+ ASSERT_FALSE(def);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+
+ // The option definition should now be available in the CfgMgr.
+ def = CfgMgr::instance().getOptionDef("isc", 100);
+ ASSERT_TRUE(def);
+
+ // Verify that the option definition data is valid.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(100, def->getCode());
+ EXPECT_FALSE(def->getArrayType());
+ EXPECT_EQ(OPT_IPV6_ADDRESS_TYPE, def->getType());
+}
+
+// The goal of this test is to check whether an option definiiton
+// that defines an option carrying a record of data fields can
+// be created.
+TEST_F(Dhcp6ParserTest, optionDefRecord) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"record\","
+ " \"array\": False,"
+ " \"record-types\": \"uint16, ipv4-address, ipv6-address, string\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ElementPtr json = Element::fromJSON(config);
+
+ // Make sure that the particular option definition does not exist.
+ OptionDefinitionPtr def = CfgMgr::instance().getOptionDef("isc", 100);
+ ASSERT_FALSE(def);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // The option definition should now be available in the CfgMgr.
+ def = CfgMgr::instance().getOptionDef("isc", 100);
+ ASSERT_TRUE(def);
+
+ // Check the option data.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(100, def->getCode());
+ EXPECT_EQ(OPT_RECORD_TYPE, def->getType());
+ EXPECT_FALSE(def->getArrayType());
+
+ // The option comprises the record of data fields. Verify that all
+ // fields are present and they are of the expected types.
+ const OptionDefinition::RecordFieldsCollection& record_fields =
+ def->getRecordFields();
+ ASSERT_EQ(4, record_fields.size());
+ EXPECT_EQ(OPT_UINT16_TYPE, record_fields[0]);
+ EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, record_fields[1]);
+ EXPECT_EQ(OPT_IPV6_ADDRESS_TYPE, record_fields[2]);
+ EXPECT_EQ(OPT_STRING_TYPE, record_fields[3]);
+}
+
+// The goal of this test is to verify that multiple option definitions
+// can be created.
+TEST_F(Dhcp6ParserTest, optionDefMultiple) {
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"uint32\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\""
+ " },"
+ " {"
+ " \"name\": \"foo-2\","
+ " \"code\": 101,"
+ " \"type\": \"ipv4-address\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ElementPtr json = Element::fromJSON(config);
+
+ // Make sure that the option definitions do not exist yet.
+ ASSERT_FALSE(CfgMgr::instance().getOptionDef("isc", 100));
+ ASSERT_FALSE(CfgMgr::instance().getOptionDef("isc", 101));
+
+ // Use the configuration string to create new option definitions.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // Check the first definition we have created.
+ OptionDefinitionPtr def1 = CfgMgr::instance().getOptionDef("isc", 100);
+ ASSERT_TRUE(def1);
+
+ // Check the option data.
+ EXPECT_EQ("foo", def1->getName());
+ EXPECT_EQ(100, def1->getCode());
+ EXPECT_EQ(OPT_UINT32_TYPE, def1->getType());
+ EXPECT_FALSE(def1->getArrayType());
+
+ // Check the second option definition we have created.
+ OptionDefinitionPtr def2 = CfgMgr::instance().getOptionDef("isc", 101);
+ ASSERT_TRUE(def2);
+
+ // Check the option data.
+ EXPECT_EQ("foo-2", def2->getName());
+ EXPECT_EQ(101, def2->getCode());
+ EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, def2->getType());
+ EXPECT_FALSE(def2->getArrayType());
+}
+
+// The goal of this test is to verify that the duplicated option
+// definition is not accepted.
+TEST_F(Dhcp6ParserTest, optionDefDuplicate) {
+
+ // Configuration string. Both option definitions have
+ // the same code and belong to the same option space.
+ // This configuration should not be accepted.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"uint32\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\""
+ " },"
+ " {"
+ " \"name\": \"foo-2\","
+ " \"code\": 100,"
+ " \"type\": \"ipv4-address\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ElementPtr json = Element::fromJSON(config);
+
+ // Make sure that the option definition does not exist yet.
+ ASSERT_FALSE(CfgMgr::instance().getOptionDef("isc", 100));
+
+ // Use the configuration string to create new option definitions.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 1);
+}
+
+// The goal of this test is to verify that the option definition
+// comprising an array of uint32 values can be created.
+TEST_F(Dhcp6ParserTest, optionDefArray) {
+
+ // Configuration string. Created option definition should
+ // comprise an array of uint32 values.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"uint32\","
+ " \"array\": True,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ElementPtr json = Element::fromJSON(config);
+
+ // Make sure that the particular option definition does not exist.
+ OptionDefinitionPtr def = CfgMgr::instance().getOptionDef("isc", 100);
+ ASSERT_FALSE(def);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // The option definition should now be available in the CfgMgr.
+ def = CfgMgr::instance().getOptionDef("isc", 100);
+ ASSERT_TRUE(def);
+
+ // Check the option data.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(100, def->getCode());
+ EXPECT_EQ(OPT_UINT32_TYPE, def->getType());
+ EXPECT_TRUE(def->getArrayType());
+}
+
+/// The purpose of this test is to verify that the option definition
+/// with invalid name is not accepted.
+TEST_F(Dhcp6ParserTest, optionDefInvalidName) {
+ // Configuration string. The option name is invalid as it
+ // contains the % character.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"invalid%name\","
+ " \"code\": 100,"
+ " \"type\": \"string\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ElementPtr json = Element::fromJSON(config);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 1);
+}
+
+/// The purpose of this test is to verify that the option definition
+/// with invalid type is not accepted.
+TEST_F(Dhcp6ParserTest, optionDefInvalidType) {
+ // Configuration string. The option type is invalid. It is
+ // "sting" instead of "string".
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"sting\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ElementPtr json = Element::fromJSON(config);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 1);
+}
+
+/// The purpose of this test is to verify that the option definition
+/// with invalid type is not accepted.
+TEST_F(Dhcp6ParserTest, optionDefInvalidRecordType) {
+ // Configuration string. The third of the record fields
+ // is invalid. It is "sting" instead of "string".
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"record\","
+ " \"array\": False,"
+ " \"record-types\": \"uint32,uint8,sting\","
+ " \"space\": \"isc\""
+ " } ]"
+ "}";
+ ElementPtr json = Element::fromJSON(config);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 1);
+}
+
+
+/// The purpose of this test is to verify that it is not allowed
+/// to override the standard option (that belongs to dhcp4 option
+/// space) and that it is allowed to define option in the dhcp4
+/// option space that has a code which is not used by any of the
+/// standard options.
+TEST_F(Dhcp6ParserTest, optionStandardDefOverride) {
+
+ // Configuration string. The option code 100 is unassigned
+ // so it can be used for a custom option definition in
+ // dhcp6 option space.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"string\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"dhcp6\""
+ " } ]"
+ "}";
+ ElementPtr json = Element::fromJSON(config);
+
+ OptionDefinitionPtr def = CfgMgr::instance().getOptionDef("dhcp6", 100);
+ ASSERT_FALSE(def);
+
+ // Use the configuration string to create new option definition.
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // The option definition should now be available in the CfgMgr.
+ def = CfgMgr::instance().getOptionDef("dhcp6", 100);
+ ASSERT_TRUE(def);
+
+ // Check the option data.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(100, def->getCode());
+ EXPECT_EQ(OPT_STRING_TYPE, def->getType());
+ EXPECT_FALSE(def->getArrayType());
+
+ // The combination of option space and code is
+ // invalid. The 'dhcp6' option space groups
+ // standard options and the code 3 is reserved
+ // for one of them.
+ config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 3,"
+ " \"type\": \"string\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"dhcp6\""
+ " } ]"
+ "}";
+ json = Element::fromJSON(config);
+
+ // Use the configuration string to create new option definition.
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+ // Expecting parsing error (error code 1).
+ checkResult(status, 1);
+}
+
// Goal of this test is to verify that global option
// data is configured for the subnet if the subnet
// configuration does not include options configuration.