From: Marcin Siodelski Date: Thu, 10 Jan 2013 09:21:59 +0000 (+0100) Subject: [2317] Added option definition config parser for the DHCPv6 server. X-Git-Tag: bind10-1.0.0-rc-release~100^2~2^2~10 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=adf283b9aa964639693d23a4e809fe67657583fb;p=thirdparty%2Fkea.git [2317] Added option definition config parser for the DHCPv6 server. --- diff --git a/src/bin/dhcp6/config_parser.cc b/src/bin/dhcp6/config_parser.cc index 40d9a4b036..f615355987 100644 --- a/src/bin/dhcp6/config_parser.cc +++ b/src/bin/dhcp6/config_parser.cc @@ -40,12 +40,24 @@ #include 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 BooleanParserPtr; +typedef boost::shared_ptr StringParserPtr; +typedef boost::shared_ptr Uint32ParserPtr; + /// @brief Auxiliary type used for storing an element name and its parser. typedef pair ConfigPair; @@ -64,6 +76,10 @@ typedef std::map Uint32Storage; /// @brief Collection of elements that store string values. typedef std::map StringStorage; +/// @brief Storage for option definitions. +typedef OptionSpaceContainer OptionDefStorage; + /// @brief Collection of address pools. /// /// This type is used as intermediate storage, when pools are parsed, but there is @@ -943,6 +959,251 @@ public: 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::factory(entry))); + if (str_parser) { + str_parser->setStorage(&string_values_); + parser = str_parser; + } + } else if (entry == "code") { + Uint32ParserPtr + code_parser(dynamic_cast(Uint32Parser::factory(entry))); + if (code_parser) { + code_parser->setStorage(&uint32_values_); + parser = code_parser; + } + } else if (entry == "array") { + BooleanParserPtr + array_parser(dynamic_cast(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("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("name", string_values_); + uint32_t code = getParam("code", uint32_values_); + std::string type = getParam("type", string_values_); + bool array_type = getParam("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("record-types", + string_values_); + // Split the list of record types into tokens. + std::vector 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 + 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 @@ -1310,6 +1571,7 @@ DhcpConfigParser* createGlobalDhcpConfigParser(const std::string& config_id) { 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); diff --git a/src/bin/dhcp6/dhcp6.spec b/src/bin/dhcp6/dhcp6.spec index 350b530d56..71ca8c5914 100644 --- a/src/bin/dhcp6/dhcp6.spec +++ b/src/bin/dhcp6/dhcp6.spec @@ -40,6 +40,56 @@ "item_default": 4000 }, + { "item_name": "option-def", + "item_type": "list", + "item_optional": false, + "item_default": [], + "list_item_spec": + { + "item_name": "single-option-def", + "item_type": "map", + "item_optional": false, + "item_default": {}, + "map_item_spec": [ + { + "item_name": "name", + "item_type": "string", + "item_optional": false, + "item_default": "" + }, + + { "item_name": "code", + "item_type": "integer", + "item_optional": false, + "item_default": 0, + }, + + { "item_name": "type", + "item_type": "string", + "item_optional": false, + "item_default": "", + }, + + { "item_name": "array", + "item_type": "boolean", + "item_optional": false, + "item_default": False + }, + + { "item_name": "record_types", + "item_type": "string", + "item_optional": false, + "item_default": "", + }, + + { "item_name": "space", + "item_type": "string", + "item_optional": false, + "item_default": "" + } ] + } + }, + { "item_name": "option-data", "item_type": "list", "item_optional": false, diff --git a/src/bin/dhcp6/tests/config_parser_unittest.cc b/src/bin/dhcp6/tests/config_parser_unittest.cc index be5bbf882e..948602ab2e 100644 --- a/src/bin/dhcp6/tests/config_parser_unittest.cc +++ b/src/bin/dhcp6/tests/config_parser_unittest.cc @@ -53,6 +53,15 @@ public: 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 @@ -144,6 +153,7 @@ public: "\"renew-timer\": 1000, " "\"valid-lifetime\": 4000, " "\"subnet6\": [ ], " + "\"option-def\": [ ], " "\"option-data\": [ ] }"; try { @@ -427,6 +437,363 @@ TEST_F(Dhcp6ParserTest, poolPrefixLen) { 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.