Browse Source

[master] Merge branch 'trac5032' (mac-sources, control-socket, relay parsers)

# Conflicts:
#	src/lib/testutils/io_utils.cc
#	src/lib/testutils/io_utils.h
Tomek Mrugalski 8 years ago
parent
commit
f1c9dee093

+ 16 - 1
doc/examples/kea4/advanced.json

@@ -44,6 +44,14 @@
         "lfc-interval": 3600
         "lfc-interval": 3600
     },
     },
 
 
+    // This defines a control socket. If defined, Kea will open a UNIX socket
+    // and will listen for incoming commands. See section 15 of the Kea User's
+    // Guide for list of supported commands.
+    "control-socket": {
+        "socket-type": "unix",
+        "socket-name": "/tmp/kea4-ctrl-socket"
+    },
+
     // Addresses will be assigned with a lifetime of 4000 seconds.
     // Addresses will be assigned with a lifetime of 4000 seconds.
     // The client is told to start renewing after 1000 seconds. If the server
     // The client is told to start renewing after 1000 seconds. If the server
     // does not respond within 2000 seconds of the lease being granted, client
     // does not respond within 2000 seconds of the lease being granted, client
@@ -83,7 +91,14 @@
         },
         },
         {
         {
             "pools": [ { "pool": "192.0.4.1 - 192.0.4.254" } ],
             "pools": [ { "pool": "192.0.4.1 - 192.0.4.254" } ],
-            "subnet": "192.0.4.0/24"
+            "subnet": "192.0.4.0/24",
+
+            // Sometimes the relay may use an IPv4 address that does not match
+            // the subnet. This is discouraged, but there are valid cases when it
+            // makes sense. One case is when there is a shared subnet.
+            "relay": {
+                "ip-address": "192.168.1.1"
+            }
         }
         }
     ]
     ]
 },
 },

+ 72 - 51
doc/examples/kea6/advanced.json

@@ -1,77 +1,90 @@
-# This is an example configuration file for DHCPv6 server in Kea.
-# It attempts to showcase some of the more advanced features.
-# Topology wise, it's a basic scenario with one IPv6 subnet configured.
-# It is assumed that one subnet (2001:db8:1::/64) is available directly
-# over ethX interface.
-#
-# The following features are currently showcased here:
-# 1. Configuration of MAC/hardware address sources in DHCPv6
+// This is an example configuration file for DHCPv6 server in Kea.
+// It attempts to showcase some of the more advanced features.
+// Topology wise, it's a basic scenario with one IPv6 subnet configured.
+// It is assumed that one subnet (2001:db8:1::/64) is available directly
+// over ethX interface.
+//
+// The following features are currently showcased here:
+// 1. Configuration of MAC/hardware address sources in DHCPv6
+// 2. RSOO (Relay supplied options) - Some relays may insert options with the
+//    intention for the server to insert them into client directed messages.
+// 3. Control socket. Kea can open a socket and listen for incoming
+//    commands.
 
 
 { "Dhcp6":
 { "Dhcp6":
 
 
 {
 {
-# Kea is told to listen on ethX network interface only.
+  // Kea is told to listen on ethX network interface only.
   "interfaces-config": {
   "interfaces-config": {
     "interfaces": [ "ethX" ]
     "interfaces": [ "ethX" ]
   },
   },
 
 
-# We need to specify the the database used to store leases. As of
-# September 2016, four database backends are supported: MySQL,
-# PostgreSQL, Cassandra, and the in-memory database, Memfile.
-# We will use memfile  because it doesn't require any prior set up.
+  // We need to specify the the database used to store leases. As of
+  // September 2016, four database backends are supported: MySQL,
+  // PostgreSQL, Cassandra, and the in-memory database, Memfile.
+  // We will use memfile  because it doesn't require any prior set up.
   "lease-database": {
   "lease-database": {
       "type": "memfile",
       "type": "memfile",
       "lfc-interval": 3600
       "lfc-interval": 3600
   },
   },
 
 
-# Kea 0.9.1 introduced MAC/hardware addresses support in DHCPv6. There is
-# no single reliable method of getting MAC address information in DHCPv6.
-# Kea supports several methods. Depending on your network set up, some
-# methods may be more preferable than others, hence the configuration
-# parameter. 'mac-sources' is a list of methods. Allowed parameters are:
-# any, raw, duid, ipv6-link-local, client-link-addr-option, rfc6939 (which
-# is an alias for client-link-addr-option), remote-id, rfc4649 (which is an
-# alias for remote-id, subscriber-id, rfc4580 (which is an alias for
-# subscriber-id) and docsis.
-#
-# Note that the order matters. Methods are attempted one by one in the order
-# specified until hardware address is obtained. If you don't care which method
-# is used, using 'any' is marginally faster than enumerating them all.
-#
-# If mac-sources are not specified, a default value of 'any' is used.
+// Kea 0.9.1 introduced MAC/hardware addresses support in DHCPv6. There is
+// no single reliable method of getting MAC address information in DHCPv6.
+// Kea supports several methods. Depending on your network set up, some
+// methods may be more preferable than others, hence the configuration
+// parameter. 'mac-sources' is a list of methods. Allowed parameters are:
+// any, raw, duid, ipv6-link-local, client-link-addr-option, rfc6939 (which
+// is an alias for client-link-addr-option), remote-id, rfc4649 (which is an
+// alias for remote-id, subscriber-id, rfc4580 (which is an alias for
+// subscriber-id) and docsis.
+//
+// Note that the order matters. Methods are attempted one by one in the order
+// specified until hardware address is obtained. If you don't care which method
+// is used, using 'any' is marginally faster than enumerating them all.
+//
+// If mac-sources are not specified, a default value of 'any' is used.
   "mac-sources": [ "client-link-addr-option", "duid", "ipv6-link-local" ],
   "mac-sources": [ "client-link-addr-option", "duid", "ipv6-link-local" ],
 
 
-# RFC6422 defines a mechanism called relay-supplied options option. The relay
-# agent may insert certain options that the server will echo back to the
-# client, if certain criteria are met. One condition is that the option must
-# be RSOO-enabled (i.e. allowed to be echoed back). IANA maintains a list
-# of those options here:
-# http://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#options-relay-supplied
-# However, it is possible to allow the server to echo back additional options.
-# This entry marks options 110, 120 and 130 as RSOO-enabled.
+// RFC6422 defines a mechanism called relay-supplied options option. The relay
+// agent may insert certain options that the server will echo back to the
+// client, if certain criteria are met. One condition is that the option must
+// be RSOO-enabled (i.e. allowed to be echoed back). IANA maintains a list
+// of those options here:
+// http://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#options-relay-supplied
+// However, it is possible to allow the server to echo back additional options.
+// This entry marks options 110, 120 and 130 as RSOO-enabled.
    "relay-supplied-options": [ "110", "120", "130" ],
    "relay-supplied-options": [ "110", "120", "130" ],
 
 
-# Addresses will be assigned with preferred and valid lifetimes
-# being 3000 and 4000, respectively. Client is told to start
-# renewing after 1000 seconds. If the server does not respond
-# after 2000 seconds since the lease was granted, client is supposed
-# to start REBIND procedure (emergency renewal that allows switching
-# to a different server).
+
+    // This defines a control socket. If defined, Kea will open a UNIX socket
+    // and will listen for incoming commands. See section 15 of the Kea User's
+    // Guide for list of supported commands.
+    "control-socket": {
+        "socket-type": "unix",
+        "socket-name": "/tmp/kea6-ctrl-socket"
+    },
+
+// Addresses will be assigned with preferred and valid lifetimes
+// being 3000 and 4000, respectively. Client is told to start
+// renewing after 1000 seconds. If the server does not respond
+// after 2000 seconds since the lease was granted, client is supposed
+// to start REBIND procedure (emergency renewal that allows switching
+// to a different server).
   "preferred-lifetime": 3000,
   "preferred-lifetime": 3000,
   "valid-lifetime": 4000,
   "valid-lifetime": 4000,
   "renew-timer": 1000,
   "renew-timer": 1000,
   "rebind-timer": 2000,
   "rebind-timer": 2000,
 
 
-# The following list defines subnets. Each subnet consists of at
-# least subnet and pool entries.
+// The following list defines subnets. Each subnet consists of at
+// least subnet and pool entries.
   "subnet6": [
   "subnet6": [
     {
     {
       "pools": [ { "pool": "2001:db8:1::/80" } ],
       "pools": [ { "pool": "2001:db8:1::/80" } ],
 
 
-# This defines PD (prefix delegation) pools. In this case
-# we have only one pool. That consists of /64 prefixes
-# being delegated out of large /48 pool. Each delegated
-# prefix will contain an excluded-prefix option.
+// This defines PD (prefix delegation) pools. In this case
+// we have only one pool. That consists of /64 prefixes
+// being delegated out of large /48 pool. Each delegated
+// prefix will contain an excluded-prefix option.
       "pd-pools": [
       "pd-pools": [
       {
       {
           "prefix": "2001:db8:abcd::",
           "prefix": "2001:db8:abcd::",
@@ -82,13 +95,21 @@
       }
       }
       ],
       ],
       "subnet": "2001:db8:1::/64",
       "subnet": "2001:db8:1::/64",
-      "interface": "ethX"
+      "interface": "ethX",
+
+      // Sometimes the relay may use an odd IPv6 address that's not matching
+      // the subnet. This is discouraged, but there are valid cases when it
+      // makes sense. One case is when the relay has only link-local address
+      // and another is when there is a shared subnet scenario.
+      "relay": {
+          "ip-address": "3000::1"
+      }
     }
     }
   ]
   ]
 },
 },
 
 
-# The following configures logging. It assumes that messages with at least
-# informational level (info, warn, error and fatal) should be logged to stdout.
+// The following configures logging. It assumes that messages with at least
+// informational level (info, warn, error and fatal) should be logged to stdout.
 "Logging": {
 "Logging": {
     "loggers": [
     "loggers": [
         {
         {

+ 3 - 0
doc/guide/dhcp4-srv.xml

@@ -3203,6 +3203,9 @@ src/lib/dhcpsrv/cfg_host_operations.cc -->
 </screen>
 </screen>
       </para>
       </para>
 
 
+      <para>If "relay" is specified, the "ip-address" parameter within
+      it is mandatory.</para>
+
     </section>
     </section>
 
 
       <section id="dhcp4-srv-example-client-class-relay">
       <section id="dhcp4-srv-example-client-class-relay">

+ 5 - 1
doc/guide/dhcp6-srv.xml

@@ -3410,6 +3410,9 @@ If not specified, the default value is:
 </screen>
 </screen>
       </para>
       </para>
 
 
+      <para>If "relay" is specified, the "ip-address" parameter within
+      it is mandatory.</para>
+
     </section>
     </section>
 
 
       <section id="dhcp6-client-class-relay">
       <section id="dhcp6-client-class-relay">
@@ -3500,7 +3503,8 @@ If not specified, the default value is:
 
 
     When not specified, a special value of "any" is used, which
     When not specified, a special value of "any" is used, which
     instructs the server to attempt to use all the methods in sequence and use
     instructs the server to attempt to use all the methods in sequence and use
-    value returned by the first one that succeeds.</para>
+    value returned by the first one that succeeds. If specified, it
+    has to have at least one value.</para>
 
 
     <para>Supported methods are:
     <para>Supported methods are:
     <itemizedlist>
     <itemizedlist>

+ 9 - 4
src/bin/dhcp4/json_config_parser.cc

@@ -189,8 +189,7 @@ protected:
             parser = new StringParser(config_id, string_values_);
             parser = new StringParser(config_id, string_values_);
         } else if (config_id.compare("pools") == 0) {
         } else if (config_id.compare("pools") == 0) {
             parser = new Pools4ListParser(config_id, pools_);
             parser = new Pools4ListParser(config_id, pools_);
-        } else if (config_id.compare("relay") == 0) {
-            parser = new RelayInfoParser(config_id, relay_info_, Option::V4);
+            // relay has been converted to SimpleParser already.
         // option-data has been converted to SimpleParser already.
         // option-data has been converted to SimpleParser already.
         } else if (config_id.compare("match-client-id") == 0) {
         } else if (config_id.compare("match-client-id") == 0) {
             parser = new BooleanParser(config_id, boolean_values_);
             parser = new BooleanParser(config_id, boolean_values_);
@@ -440,8 +439,7 @@ DhcpConfigParser* createGlobalDhcp4ConfigParser(const std::string& config_id,
         parser = new D2ClientConfigParser(config_id);
         parser = new D2ClientConfigParser(config_id);
     } else if (config_id.compare("match-client-id") == 0) {
     } else if (config_id.compare("match-client-id") == 0) {
         parser = new BooleanParser(config_id, globalContext()->boolean_values_);
         parser = new BooleanParser(config_id, globalContext()->boolean_values_);
-    } else if (config_id.compare("control-socket") == 0) {
-        parser = new ControlSocketParser(config_id);
+    // control-socket has been converted to SimpleParser already.
     } else if (config_id.compare("expired-leases-processing") == 0) {
     } else if (config_id.compare("expired-leases-processing") == 0) {
         parser = new ExpirationConfigParser();
         parser = new ExpirationConfigParser();
     } else if (config_id.compare("client-classes") == 0) {
     } else if (config_id.compare("client-classes") == 0) {
@@ -637,6 +635,13 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
                 continue;
                 continue;
             }
             }
 
 
+            if (config_pair.first == "control-socket") {
+                ControlSocketParser parser;
+                SrvConfigPtr srv_cfg = CfgMgr::instance().getStagingCfg();
+                parser.parse(*srv_cfg, config_pair.second);
+                continue;
+            }
+
             ParserPtr parser(createGlobalDhcp4ConfigParser(config_pair.first,
             ParserPtr parser(createGlobalDhcp4ConfigParser(config_pair.first,
                                                            config_pair.second));
                                                            config_pair.second));
             LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_PARSER_CREATED)
             LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_PARSER_CREATED)

+ 22 - 87
src/bin/dhcp4/tests/parser_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -7,14 +7,14 @@
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
 #include <cc/data.h>
 #include <cc/data.h>
 #include <dhcp4/parser_context.h>
 #include <dhcp4/parser_context.h>
-#include <fstream>
-#include <cstdio>
-#include <exceptions/exceptions.h>
+#include <testutils/io_utils.h>
 
 
 using namespace isc::data;
 using namespace isc::data;
 using namespace std;
 using namespace std;
 
 
-namespace {
+namespace isc {
+namespace dhcp {
+namespace test {
 
 
 /// @brief compares two JSON trees
 /// @brief compares two JSON trees
 ///
 ///
@@ -128,6 +128,8 @@ TEST(ParserTest, keywordDhcp4) {
      testParser(txt, Parser4Context::PARSER_DHCP4);
      testParser(txt, Parser4Context::PARSER_DHCP4);
 }
 }
 
 
+// Tests if bash (#) comments are supported. That's the only comment type that
+// was supported by the old parser.
 TEST(ParserTest, bashComments) {
 TEST(ParserTest, bashComments) {
     string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
     string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "  \"interfaces\": [ \"*\" ]"
@@ -146,7 +148,8 @@ TEST(ParserTest, bashComments) {
     testParser(txt, Parser4Context::PARSER_DHCP4, false);
     testParser(txt, Parser4Context::PARSER_DHCP4, false);
 }
 }
 
 
-TEST(ParserTest, cComments) {
+// Tests if C++ (//) comments can start anywhere, not just in the first line.
+TEST(ParserTest, cppComments) {
     string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
     string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "  \"interfaces\": [ \"*\" ]"
                 "},\n"
                 "},\n"
@@ -161,6 +164,7 @@ TEST(ParserTest, cComments) {
     testParser(txt, Parser4Context::PARSER_DHCP4, false);
     testParser(txt, Parser4Context::PARSER_DHCP4, false);
 }
 }
 
 
+// Tests if bash (#) comments can start anywhere, not just in the first line.
 TEST(ParserTest, bashCommentsInline) {
 TEST(ParserTest, bashCommentsInline) {
     string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
     string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "  \"interfaces\": [ \"*\" ]"
@@ -176,6 +180,7 @@ TEST(ParserTest, bashCommentsInline) {
     testParser(txt, Parser4Context::PARSER_DHCP4, false);
     testParser(txt, Parser4Context::PARSER_DHCP4, false);
 }
 }
 
 
+// Tests if multi-line C style comments are handled correctly.
 TEST(ParserTest, multilineComments) {
 TEST(ParserTest, multilineComments) {
     string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
     string txt= "{ \"Dhcp4\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "  \"interfaces\": [ \"*\" ]"
@@ -193,89 +198,13 @@ TEST(ParserTest, multilineComments) {
     testParser(txt, Parser4Context::PARSER_DHCP4, false);
     testParser(txt, Parser4Context::PARSER_DHCP4, false);
 }
 }
 
 
-/// @brief removes comments from a JSON file
-///
-/// This is rather naive implementation, but it's probably sufficient for
-/// testing. It won't be able to pick any trickier cases, like # or //
-/// appearing in strings, nested C++ comments etc.
-///
-/// @param input_file file to be stripped of comments
-/// @return a new file that has comments stripped from it
-std::string decommentJSONfile(const std::string& input_file) {
-    ifstream f(input_file);
-    if (!f.is_open()) {
-        isc_throw(isc::BadValue, "can't open input file for reading: " + input_file);
-    }
-
-    string outfile;
-    size_t last_slash = input_file.find_last_of("/");
-    if (last_slash != string::npos) {
-        outfile = input_file.substr(last_slash + 1);
-    } else {
-        outfile = input_file;
-    }
-    outfile += "-decommented";
-
-    ofstream out(outfile);
-    if (!out.is_open()) {
-        isc_throw(isc::BadValue, "can't open output file for writing: " + input_file);
-    }
-
-    bool in_comment = false;
-    string line;
-    while (std::getline(f, line)) {
-        // First, let's get rid of the # comments
-        size_t hash_pos = line.find("#");
-        if (hash_pos != string::npos) {
-            line = line.substr(0, hash_pos);
-        }
-
-        // Second, let's get rid of the // comments
-        size_t dblslash_pos = line.find("//");
-        if (dblslash_pos != string::npos) {
-            line = line.substr(0, dblslash_pos);
-        }
-
-        // Now the tricky part: c comments.
-        size_t begin_pos = line.find("/*");
-        size_t end_pos = line.find("*/");
-        if (in_comment && end_pos == string::npos) {
-            // we continue through multiline comment
-            line = "";
-        } else {
-
-            if (begin_pos != string::npos) {
-                in_comment = true;
-                if (end_pos != string::npos) {
-                    // sigle line comment. Let's get rid of the content in between
-                    line = line.replace(begin_pos, end_pos + 2, end_pos + 2 - begin_pos, ' ');
-                    in_comment = false;
-                } else {
-                    line = line.substr(0, begin_pos);
-                }
-            } else {
-                if (in_comment && end_pos != string::npos) {
-                    line = line.replace(0, end_pos +2 , end_pos + 2, ' ');
-                    in_comment = false;
-                }
-            }
-        }
-
-        // Finally, write the line to the output file.
-        out << line << endl;
-    }
-    f.close();
-    out.close();
-
-    return (outfile);
-}
 
 
 /// @brief Loads specified example config file
 /// @brief Loads specified example config file
 ///
 ///
 /// This test loads specified example file twice: first, using the legacy
 /// This test loads specified example file twice: first, using the legacy
 /// JSON file and then second time using bison parser. Two created Element
 /// JSON file and then second time using bison parser. Two created Element
 /// trees are then compared. The input is decommented before it is passed
 /// trees are then compared. The input is decommented before it is passed
-/// to legacy parser (as its support for comments is very limited).
+/// to legacy parser (as legacy support for comments is very limited).
 ///
 ///
 /// @param fname name of the file to be loaded
 /// @param fname name of the file to be loaded
 void testFile(const std::string& fname) {
 void testFile(const std::string& fname) {
@@ -284,8 +213,7 @@ void testFile(const std::string& fname) {
 
 
     string decommented = decommentJSONfile(fname);
     string decommented = decommentJSONfile(fname);
 
 
-    cout << "Attempting to load file " << fname << " (" << decommented
-         << ")" << endl;
+    cout << "Parsing file " << fname << " (" << decommented << ")" << endl;
 
 
     EXPECT_NO_THROW(reference_json = Element::fromJSONFile(decommented, true));
     EXPECT_NO_THROW(reference_json = Element::fromJSONFile(decommented, true));
 
 
@@ -329,6 +257,11 @@ TEST(ParserTest, file) {
     }
     }
 }
 }
 
 
+/// @brief Tests error conditions in Dhcp4Parser
+///
+/// @param txt text to be parsed
+/// @param parser_type type of the parser to be used in the test
+/// @param msg expected content of the exception
 void testError(const std::string& txt,
 void testError(const std::string& txt,
                Parser4Context::ParserType parser_type,
                Parser4Context::ParserType parser_type,
                const std::string& msg)
                const std::string& msg)
@@ -347,7 +280,7 @@ void testError(const std::string& txt,
     }
     }
 }
 }
 
 
-// Check errors
+// Verify that error conditions are handled correctly.
 TEST(ParserTest, errors) {
 TEST(ParserTest, errors) {
     // no input
     // no input
     testError("", Parser4Context::PARSER_JSON,
     testError("", Parser4Context::PARSER_JSON,
@@ -591,7 +524,7 @@ TEST(ParserTest, unicodeEscapes) {
     }
     }
 }
 }
 
 
-// This test checks that all representations of a slash is recognized properly.
+// This test checks that all representations of a slash are recognized properly.
 TEST(ParserTest, unicodeSlash) {
 TEST(ParserTest, unicodeSlash) {
     // check the 4 possible encodings of solidus '/'
     // check the 4 possible encodings of solidus '/'
     ConstElementPtr result;
     ConstElementPtr result;
@@ -609,3 +542,5 @@ TEST(ParserTest, unicodeSlash) {
 }
 }
 
 
 };
 };
+};
+};

+ 17 - 7
src/bin/dhcp6/json_config_parser.cc

@@ -424,8 +424,7 @@ protected:
             parser = new StringParser(config_id, string_values_);
             parser = new StringParser(config_id, string_values_);
         } else if (config_id.compare("pools") == 0) {
         } else if (config_id.compare("pools") == 0) {
             parser = new Pools6ListParser(config_id, pools_);
             parser = new Pools6ListParser(config_id, pools_);
-        } else if (config_id.compare("relay") == 0) {
-            parser = new RelayInfoParser(config_id, relay_info_, Option::V6);
+        // relay has been converted to SimpleParser.
         } else if (config_id.compare("pd-pools") == 0) {
         } else if (config_id.compare("pd-pools") == 0) {
             parser = new PdPoolListParser(config_id, pools_);
             parser = new PdPoolListParser(config_id, pools_);
         // option-data was here, but it is now converted to SimpleParser
         // option-data was here, but it is now converted to SimpleParser
@@ -718,13 +717,10 @@ DhcpConfigParser* createGlobal6DhcpConfigParser(const std::string& config_id,
         parser = new HooksLibrariesParser(config_id);
         parser = new HooksLibrariesParser(config_id);
     } else if (config_id.compare("dhcp-ddns") == 0) {
     } else if (config_id.compare("dhcp-ddns") == 0) {
         parser = new D2ClientConfigParser(config_id);
         parser = new D2ClientConfigParser(config_id);
-    } else if (config_id.compare("mac-sources") == 0) {
-        parser = new MACSourcesListConfigParser(config_id,
-                                                globalContext());
+    // mac-source has been converted to SimpleParser.
     } else if (config_id.compare("relay-supplied-options") == 0) {
     } else if (config_id.compare("relay-supplied-options") == 0) {
         parser = new RSOOListConfigParser(config_id);
         parser = new RSOOListConfigParser(config_id);
-    } else if (config_id.compare("control-socket") == 0) {
-        parser = new ControlSocketParser(config_id);
+    // control-socket has been converted to SimpleParser.
     } else if (config_id.compare("expired-leases-processing") == 0) {
     } else if (config_id.compare("expired-leases-processing") == 0) {
         parser = new ExpirationConfigParser();
         parser = new ExpirationConfigParser();
     } else if (config_id.compare("client-classes") == 0) {
     } else if (config_id.compare("client-classes") == 0) {
@@ -911,6 +907,20 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
                 continue;
                 continue;
             }
             }
 
 
+            if (config_pair.first == "mac-sources") {
+                MACSourcesListConfigParser parser;
+                CfgMACSource& mac_source = CfgMgr::instance().getStagingCfg()->getMACSources();
+                parser.parse(mac_source, config_pair.second);
+                continue;
+            }
+
+            if (config_pair.first == "control-socket") {
+                ControlSocketParser parser;
+                SrvConfigPtr srv_config = CfgMgr::instance().getStagingCfg();
+                parser.parse(*srv_config, config_pair.second);
+                continue;
+            }
+
             ParserPtr parser(createGlobal6DhcpConfigParser(config_pair.first,
             ParserPtr parser(createGlobal6DhcpConfigParser(config_pair.first,
                                                            config_pair.second));
                                                            config_pair.second));
             LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_PARSER_CREATED)
             LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_PARSER_CREATED)

+ 49 - 28
src/bin/dhcp6/tests/config_parser_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -251,6 +251,9 @@ public:
         ASSERT_TRUE(status);
         ASSERT_TRUE(status);
         comment_ = parseAnswer(rcode_, status);
         comment_ = parseAnswer(rcode_, status);
         EXPECT_EQ(expected_code, rcode_);
         EXPECT_EQ(expected_code, rcode_);
+        if (expected_code != rcode_) {
+            cout << "The comment returned was: [" << comment_->stringValue() << "]" << endl;
+        }
     }
     }
 
 
     /// @brief Returns an interface configuration used by the most of the
     /// @brief Returns an interface configuration used by the most of the
@@ -762,7 +765,7 @@ TEST_F(Dhcp6ParserTest, emptySubnet) {
 
 
     ConstElementPtr status;
     ConstElementPtr status;
     EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
     EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
-                    
+
     // returned value should be 0 (success)
     // returned value should be 0 (success)
     checkResult(status, 0);
     checkResult(status, 0);
 }
 }
@@ -4292,16 +4295,15 @@ TEST_F(Dhcp6ParserTest, reservationBogus) {
 }
 }
 
 
 /// The goal of this test is to verify that configuration can include
 /// The goal of this test is to verify that configuration can include
-/// MAC/Hardware sources. This test also checks if the aliases are
-/// handled properly (rfc6939 = client-addr-relay, rfc4649 = remote-id,
-/// rfc4580 = subscriber-id).
-TEST_F(Dhcp6ParserTest, macSources) {
+/// MAC/Hardware sources. This case uses RFC numbers to reference methods.
+/// Also checks if the aliases are handled properly (rfc6939 = client-addr-relay,
+/// rfc4649 = remote-id, rfc4580 = subscriber-id).
+TEST_F(Dhcp6ParserTest, macSources1) {
 
 
     ConstElementPtr json;
     ConstElementPtr json;
     ASSERT_NO_THROW(json =
     ASSERT_NO_THROW(json =
         parseDHCP6("{ " + genIfaceConfig() + ","
         parseDHCP6("{ " + genIfaceConfig() + ","
-                   "\"mac-sources\": [ \"rfc6939\", \"rfc4649\", \"rfc4580\","
-                   "\"client-link-addr-option\", \"remote-id\", \"subscriber-id\"],"
+                   "\"mac-sources\": [ \"rfc6939\", \"rfc4649\", \"rfc4580\" ],"
                    "\"preferred-lifetime\": 3000,"
                    "\"preferred-lifetime\": 3000,"
                    "\"rebind-timer\": 2000, "
                    "\"rebind-timer\": 2000, "
                    "\"renew-timer\": 1000, "
                    "\"renew-timer\": 1000, "
@@ -4310,28 +4312,49 @@ TEST_F(Dhcp6ParserTest, macSources) {
 
 
     ConstElementPtr status;
     ConstElementPtr status;
     EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
     EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+    checkResult(status, 0);
 
 
-    // returned value should be 0 (success)
+    CfgMACSources sources = CfgMgr::instance().getStagingCfg()->getMACSources().get();
+
+    ASSERT_EQ(3, sources.size());
+    // Let's check the aliases. They should be recognized to their base methods.
+    EXPECT_EQ(HWAddr::HWADDR_SOURCE_CLIENT_ADDR_RELAY_OPTION, sources[0]);
+    EXPECT_EQ(HWAddr::HWADDR_SOURCE_REMOTE_ID, sources[1]);
+    EXPECT_EQ(HWAddr::HWADDR_SOURCE_SUBSCRIBER_ID, sources[2]);
+}
+
+/// The goal of this test is to verify that configuration can include
+/// MAC/Hardware sources. This uses specific method names.
+TEST_F(Dhcp6ParserTest, macSources2) {
+
+    ConstElementPtr json;
+    ASSERT_NO_THROW(json =
+        parseDHCP6("{ " + genIfaceConfig() + ","
+                   "\"mac-sources\": [ \"client-link-addr-option\", \"remote-id\", "
+                   "                   \"subscriber-id\"],"
+                   "\"preferred-lifetime\": 3000,"
+                   "\"rebind-timer\": 2000, "
+                   "\"renew-timer\": 1000, "
+                   "\"subnet6\": [  ], "
+                   "\"valid-lifetime\": 4000 }"));
+
+    ConstElementPtr status;
+    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
     checkResult(status, 0);
     checkResult(status, 0);
 
 
-    CfgMACSources mac_sources = CfgMgr::instance().getStagingCfg()->getMACSources().get();
-    ASSERT_EQ(6, mac_sources.size());
+    CfgMACSources sources = CfgMgr::instance().getStagingCfg()->getMACSources().get();
+
+    ASSERT_EQ(3, sources.size());
     // Let's check the aliases. They should be recognized to their base methods.
     // Let's check the aliases. They should be recognized to their base methods.
-    EXPECT_EQ(HWAddr::HWADDR_SOURCE_CLIENT_ADDR_RELAY_OPTION, mac_sources[0]);
-    EXPECT_EQ(HWAddr::HWADDR_SOURCE_REMOTE_ID, mac_sources[1]);
-    EXPECT_EQ(HWAddr::HWADDR_SOURCE_SUBSCRIBER_ID, mac_sources[2]);
-
-    // Let's check if the actual methods are recognized properly.
-    EXPECT_EQ(HWAddr::HWADDR_SOURCE_CLIENT_ADDR_RELAY_OPTION, mac_sources[3]);
-    EXPECT_EQ(HWAddr::HWADDR_SOURCE_REMOTE_ID, mac_sources[4]);
-    EXPECT_EQ(HWAddr::HWADDR_SOURCE_SUBSCRIBER_ID, mac_sources[5]);
+    EXPECT_EQ(HWAddr::HWADDR_SOURCE_CLIENT_ADDR_RELAY_OPTION, sources[0]);
+    EXPECT_EQ(HWAddr::HWADDR_SOURCE_REMOTE_ID, sources[1]);
+    EXPECT_EQ(HWAddr::HWADDR_SOURCE_SUBSCRIBER_ID, sources[2]);
 }
 }
 
 
-/// The goal of this test is to verify that MAC sources configuration can be
-/// empty.
-/// Note the Dhcp6 parser requires the list to NOT be empty?!
-TEST_F(Dhcp6ParserTest, macSourcesEmpty) {
 
 
+/// The goal of this test is to verify that empty MAC sources configuration
+/// is rejected. If specified, this has to have at least one value.
+TEST_F(Dhcp6ParserTest, macSourcesEmpty) {
     ConstElementPtr status;
     ConstElementPtr status;
 
 
     EXPECT_NO_THROW(status = configureDhcp6Server(srv_,
     EXPECT_NO_THROW(status = configureDhcp6Server(srv_,
@@ -4343,11 +4366,9 @@ TEST_F(Dhcp6ParserTest, macSourcesEmpty) {
                               "\"subnet6\": [  ], "
                               "\"subnet6\": [  ], "
                               "\"valid-lifetime\": 4000 }")));
                               "\"valid-lifetime\": 4000 }")));
 
 
-    // returned value should be 0 (success)
-    checkResult(status, 0);
-
-    CfgMACSources mac_sources = CfgMgr::instance().getStagingCfg()->getMACSources().get();
-    EXPECT_EQ(0, mac_sources.size());
+    // returned value should be 1 (failure), because the mac-sources must not
+    // be empty when specified.
+    checkResult(status, 1);
 }
 }
 
 
 /// The goal of this test is to verify that MAC sources configuration can
 /// The goal of this test is to verify that MAC sources configuration can

+ 75 - 48
src/bin/dhcp6/tests/parser_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -7,28 +7,42 @@
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
 #include <cc/data.h>
 #include <cc/data.h>
 #include <dhcp6/parser_context.h>
 #include <dhcp6/parser_context.h>
+#include <testutils/io_utils.h>
 
 
 using namespace isc::data;
 using namespace isc::data;
 using namespace std;
 using namespace std;
 
 
-namespace {
-
-void compareJSON(ConstElementPtr a, ConstElementPtr b, bool print = true) {
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief compares two JSON trees
+///
+/// If differences are discovered, gtest failure is reported (using EXPECT_EQ)
+///
+/// @param a first to be compared
+/// @param b second to be compared
+void compareJSON(ConstElementPtr a, ConstElementPtr b) {
     ASSERT_TRUE(a);
     ASSERT_TRUE(a);
     ASSERT_TRUE(b);
     ASSERT_TRUE(b);
-    if (print) {
-        // std::cout << "JSON A: -----" << endl << a->str() << std::endl;
-        // std::cout << "JSON B: -----" << endl << b->str() << std::endl;
-        // cout << "---------" << endl << endl;
-    }
     EXPECT_EQ(a->str(), b->str());
     EXPECT_EQ(a->str(), b->str());
 }
 }
 
 
-void testParser(const std::string& txt, Parser6Context::ParserType parser_type) {
-    ElementPtr reference_json;
+/// @brief Tests if the input string can be parsed with specific parser
+///
+/// The input text will be passed to bison parser of specified type.
+/// Then the same input text is passed to legacy JSON parser and outputs
+/// from both parsers are compared. The legacy comparison can be disabled,
+/// if the feature tested is not supported by the old parser (e.g.
+/// new comment styles)
+///
+/// @param txt text to be compared
+/// @param parser_type bison parser type to be instantiated
+/// @param compare whether to compare the output with legacy JSON parser
+void testParser(const std::string& txt, Parser6Context::ParserType parser_type,
+    bool compare = true) {
     ConstElementPtr test_json;
     ConstElementPtr test_json;
 
 
-    ASSERT_NO_THROW(reference_json = Element::fromJSON(txt, true));
     ASSERT_NO_THROW({
     ASSERT_NO_THROW({
             try {
             try {
                 Parser6Context ctx;
                 Parser6Context ctx;
@@ -40,29 +54,16 @@ void testParser(const std::string& txt, Parser6Context::ParserType parser_type)
 
 
     });
     });
 
 
+    if (!compare) {
+        return;
+    }
+
     // Now compare if both representations are the same.
     // Now compare if both representations are the same.
+    ElementPtr reference_json;
+    ASSERT_NO_THROW(reference_json = Element::fromJSON(txt, true));
     compareJSON(reference_json, test_json);
     compareJSON(reference_json, test_json);
 }
 }
 
 
-void testParser2(const std::string& txt, Parser6Context::ParserType parser_type) {
-    ConstElementPtr test_json;
-
-    ASSERT_NO_THROW({
-            try {
-                Parser6Context ctx;
-                test_json = ctx.parseString(txt, parser_type);
-            } catch (const std::exception &e) {
-                cout << "EXCEPTION: " << e.what() << endl;
-                throw;
-            }
-    });
-    /// @todo: Implement actual validation here. since the original
-    /// Element::fromJSON does not support several comment types, we don't
-    /// have anything to compare with.
-    /// std::cout << "Original text:" << txt << endl;
-    /// std::cout << "Parsed text  :" << test_json->str() << endl;
-}
-
 TEST(ParserTest, mapInMap) {
 TEST(ParserTest, mapInMap) {
     string txt = "{ \"xyzzy\": { \"foo\": 123, \"baz\": 456 } }";
     string txt = "{ \"xyzzy\": { \"foo\": 123, \"baz\": 456 } }";
     testParser(txt, Parser6Context::PARSER_JSON);
     testParser(txt, Parser6Context::PARSER_JSON);
@@ -125,9 +126,11 @@ TEST(ParserTest, keywordDhcp6) {
                   "    \"subnet\": \"2001:db8:1::/48\", "
                   "    \"subnet\": \"2001:db8:1::/48\", "
                   "    \"interface\": \"test\" } ],\n"
                   "    \"interface\": \"test\" } ],\n"
                    "\"valid-lifetime\": 4000 } }";
                    "\"valid-lifetime\": 4000 } }";
-     testParser2(txt, Parser6Context::PARSER_DHCP6);
+     testParser(txt, Parser6Context::PARSER_DHCP6);
 }
 }
 
 
+// Tests if bash (#) comments are supported. That's the only comment type that
+// was supported by the old parser.
 TEST(ParserTest, bashComments) {
 TEST(ParserTest, bashComments) {
     string txt= "{ \"Dhcp6\": { \"interfaces-config\": {"
     string txt= "{ \"Dhcp6\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "  \"interfaces\": [ \"*\" ]"
@@ -144,10 +147,11 @@ TEST(ParserTest, bashComments) {
                 "    \"interface\": \"eth0\""
                 "    \"interface\": \"eth0\""
                 " } ],"
                 " } ],"
                 "\"valid-lifetime\": 4000 } }";
                 "\"valid-lifetime\": 4000 } }";
-    testParser2(txt, Parser6Context::PARSER_DHCP6);
+    testParser(txt, Parser6Context::PARSER_DHCP6);
 }
 }
 
 
-TEST(ParserTest, cComments) {
+// Tests if C++ (//) comments can start anywhere, not just in the first line.
+TEST(ParserTest, cppComments) {
     string txt= "{ \"Dhcp6\": { \"interfaces-config\": {"
     string txt= "{ \"Dhcp6\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "  \"interfaces\": [ \"*\" ]"
                 "},\n"
                 "},\n"
@@ -160,9 +164,10 @@ TEST(ParserTest, cComments) {
                 "    \"interface\": \"eth0\""
                 "    \"interface\": \"eth0\""
                 " } ],"
                 " } ],"
                 "\"valid-lifetime\": 4000 } }";
                 "\"valid-lifetime\": 4000 } }";
-    testParser2(txt, Parser6Context::PARSER_DHCP6);
+    testParser(txt, Parser6Context::PARSER_DHCP6, false);
 }
 }
 
 
+// Tests if bash (#) comments can start anywhere, not just in the first line.
 TEST(ParserTest, bashCommentsInline) {
 TEST(ParserTest, bashCommentsInline) {
     string txt= "{ \"Dhcp6\": { \"interfaces-config\": {"
     string txt= "{ \"Dhcp6\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "  \"interfaces\": [ \"*\" ]"
@@ -176,9 +181,10 @@ TEST(ParserTest, bashCommentsInline) {
                 "    \"interface\": \"eth0\""
                 "    \"interface\": \"eth0\""
                 " } ],"
                 " } ],"
                 "\"valid-lifetime\": 4000 } }";
                 "\"valid-lifetime\": 4000 } }";
-    testParser2(txt, Parser6Context::PARSER_DHCP6);
+    testParser(txt, Parser6Context::PARSER_DHCP6, false);
 }
 }
 
 
+// Tests if multi-line C style comments are handled correctly.
 TEST(ParserTest, multilineComments) {
 TEST(ParserTest, multilineComments) {
     string txt= "{ \"Dhcp6\": { \"interfaces-config\": {"
     string txt= "{ \"Dhcp6\": { \"interfaces-config\": {"
                 "  \"interfaces\": [ \"*\" ]"
                 "  \"interfaces\": [ \"*\" ]"
@@ -193,17 +199,29 @@ TEST(ParserTest, multilineComments) {
                 "    \"interface\": \"eth0\""
                 "    \"interface\": \"eth0\""
                 " } ],"
                 " } ],"
                 "\"valid-lifetime\": 4000 } }";
                 "\"valid-lifetime\": 4000 } }";
-    testParser2(txt, Parser6Context::PARSER_DHCP6);
+    testParser(txt, Parser6Context::PARSER_DHCP6, false);
 }
 }
 
 
-
-void testFile(const std::string& fname, bool print) {
+/// @brief Loads specified example config file
+///
+/// This test loads specified example file twice: first, using the legacy
+/// JSON file and then second time using bison parser. Two created Element
+/// trees are then compared. The input is decommented before it is passed
+/// to legacy parser (as legacy support for comments is very limited).
+///
+/// @param fname name of the file to be loaded
+void testFile(const std::string& fname) {
     ElementPtr reference_json;
     ElementPtr reference_json;
     ConstElementPtr test_json;
     ConstElementPtr test_json;
 
 
-    cout << "Attempting to load file " << fname << endl;
+    string decommented = decommentJSONfile(fname);
+
+    cout << "Parsing file " << fname << "(" << decommented << ")" << endl;
+
+    EXPECT_NO_THROW(reference_json = Element::fromJSONFile(decommented, true));
 
 
-    EXPECT_NO_THROW(reference_json = Element::fromJSONFile(fname, true));
+    // remove the temporary file
+    EXPECT_NO_THROW(::remove(decommented.c_str()));
 
 
     EXPECT_NO_THROW(
     EXPECT_NO_THROW(
     try {
     try {
@@ -217,9 +235,7 @@ void testFile(const std::string& fname, bool print) {
     ASSERT_TRUE(reference_json);
     ASSERT_TRUE(reference_json);
     ASSERT_TRUE(test_json);
     ASSERT_TRUE(test_json);
 
 
-    compareJSON(reference_json, test_json, print);
-
-
+    compareJSON(reference_json, test_json);
 }
 }
 
 
 // This test loads all available existing files. Each config is loaded
 // This test loads all available existing files. Each config is loaded
@@ -243,10 +259,15 @@ TEST(ParserTest, file) {
     configs.push_back("stateless.json");
     configs.push_back("stateless.json");
 
 
     for (int i = 0; i<configs.size(); i++) {
     for (int i = 0; i<configs.size(); i++) {
-        testFile(string(CFG_EXAMPLES) + "/" + configs[i], false);
+        testFile(string(CFG_EXAMPLES) + "/" + configs[i]);
     }
     }
 }
 }
 
 
+/// @brief Tests error conditions in Dhcp6Parser
+///
+/// @param txt text to be parsed
+/// @param parser_type type of the parser to be used in the test
+/// @param msg expected content of the exception
 void testError(const std::string& txt,
 void testError(const std::string& txt,
                Parser6Context::ParserType parser_type,
                Parser6Context::ParserType parser_type,
                const std::string& msg)
                const std::string& msg)
@@ -265,7 +286,7 @@ void testError(const std::string& txt,
     }
     }
 }
 }
 
 
-// Check errors
+// Verify that error conditions are handled correctly.
 TEST(ParserTest, errors) {
 TEST(ParserTest, errors) {
     // no input
     // no input
     testError("", Parser6Context::PARSER_JSON,
     testError("", Parser6Context::PARSER_JSON,
@@ -507,9 +528,13 @@ TEST(ParserTest, unicodeEscapes) {
         ASSERT_EQ(Element::string, result->getType());
         ASSERT_EQ(Element::string, result->getType());
         EXPECT_EQ(ins, result->stringValue());
         EXPECT_EQ(ins, result->stringValue());
     }
     }
+}
 
 
+// This test checks that all representations of a slash is recognized properly.
+TEST(ParserTest, unicodeSlash) {
     // check the 4 possible encodings of solidus '/'
     // check the 4 possible encodings of solidus '/'
-    json = "\"/\\/\\u002f\\u002F\"";
+    ConstElementPtr result;
+    string json = "\"/\\/\\u002f\\u002F\"";
     ASSERT_NO_THROW(
     ASSERT_NO_THROW(
     try {
     try {
         Parser6Context ctx;
         Parser6Context ctx;
@@ -520,6 +545,8 @@ TEST(ParserTest, unicodeEscapes) {
     });
     });
     ASSERT_EQ(Element::string, result->getType());
     ASSERT_EQ(Element::string, result->getType());
     EXPECT_EQ("////", result->stringValue());
     EXPECT_EQ("////", result->stringValue());
-}       
+}
 
 
 };
 };
+};
+};

+ 11 - 1
src/lib/dhcpsrv/cfg_mac_source.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2015,2017 Internet Systems Consortium, Inc. ("ISC")
 //
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -47,6 +47,16 @@ uint32_t CfgMACSource::MACSourceFromText(const std::string& name) {
     isc_throw(BadValue, "Can't convert '" << name << "' to any known MAC source.");
     isc_throw(BadValue, "Can't convert '" << name << "' to any known MAC source.");
 }
 }
 
 
+void CfgMACSource::add(uint32_t source) {
+    for (CfgMACSources::const_iterator it = mac_sources_.begin();
+         it != mac_sources_.end(); ++it) {
+        if (*it == source) {
+            isc_throw(InvalidParameter, "mac-source paramter " << source
+                      << "' specified twice.");
+        }
+    }
+    mac_sources_.push_back(source);
+}
 
 
 };
 };
 };
 };

+ 3 - 5
src/lib/dhcpsrv/cfg_mac_source.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2015,2017 Internet Systems Consortium, Inc. ("ISC")
 //
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -52,10 +52,8 @@ class CfgMACSource {
     /// @param source MAC source (see constants in Pkt::HWADDR_SOURCE_*)
     /// @param source MAC source (see constants in Pkt::HWADDR_SOURCE_*)
     ///
     ///
     /// Specified source is being added to the mac_sources_ array.
     /// Specified source is being added to the mac_sources_ array.
-    /// @todo implement add(string) version of this method.
-    void add(uint32_t source) {
-        mac_sources_.push_back(source);
-    }
+    /// @throw InvalidParameter if such a source is already defined.
+    void add(uint32_t source);
 
 
     /// @brief Provides access to the configure MAC/Hardware address sources.
     /// @brief Provides access to the configure MAC/Hardware address sources.
     ///
     ///

+ 71 - 79
src/lib/dhcpsrv/parsers/dhcp_parsers.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -177,62 +177,48 @@ template <> void ValueParser<std::string>::build(ConstElementPtr value) {
 
 
 // ******************** MACSourcesListConfigParser *************************
 // ******************** MACSourcesListConfigParser *************************
 
 
-MACSourcesListConfigParser::
-MACSourcesListConfigParser(const std::string& param_name,
-                           ParserContextPtr global_context)
-    : param_name_(param_name), global_context_(global_context) {
-    if (param_name_ != "mac-sources") {
-        isc_throw(BadValue, "Internal error. MAC sources configuration "
-            "parser called for the wrong parameter: " << param_name);
-    }
-}
-
 void
 void
-MACSourcesListConfigParser::build(ConstElementPtr value) {
+MACSourcesListConfigParser::parse(CfgMACSource& mac_sources, ConstElementPtr value) {
     CfgIface cfg_iface;
     CfgIface cfg_iface;
     uint32_t source = 0;
     uint32_t source = 0;
+    size_t cnt = 0;
 
 
     // By default, there's only one source defined: ANY.
     // By default, there's only one source defined: ANY.
     // If user specified anything, we need to get rid of that default.
     // If user specified anything, we need to get rid of that default.
-    CfgMgr::instance().getStagingCfg()->getMACSources().clear();
+    mac_sources.clear();
 
 
     BOOST_FOREACH(ConstElementPtr source_elem, value->listValue()) {
     BOOST_FOREACH(ConstElementPtr source_elem, value->listValue()) {
         std::string source_str = source_elem->stringValue();
         std::string source_str = source_elem->stringValue();
         try {
         try {
             source = CfgMACSource::MACSourceFromText(source_str);
             source = CfgMACSource::MACSourceFromText(source_str);
-            CfgMgr::instance().getStagingCfg()->getMACSources().add(source);
+            mac_sources.add(source);
+            ++cnt;
+        } catch (const InvalidParameter& ex) {
+            isc_throw(DhcpConfigError, "The mac-sources value '" << source_str
+                      << "' was specified twice (" << value->getPosition() << ")");
         } catch (const std::exception& ex) {
         } catch (const std::exception& ex) {
             isc_throw(DhcpConfigError, "Failed to convert '"
             isc_throw(DhcpConfigError, "Failed to convert '"
                       << source_str << "' to any recognized MAC source:"
                       << source_str << "' to any recognized MAC source:"
                       << ex.what() << " (" << value->getPosition() << ")");
                       << ex.what() << " (" << value->getPosition() << ")");
         }
         }
     }
     }
-}
 
 
-void
-MACSourcesListConfigParser::commit() {
-    // Nothing to do.
+    if (!cnt) {
+        isc_throw(DhcpConfigError, "If specified, MAC Sources cannot be empty");
+    }
 }
 }
 
 
 // ******************** ControlSocketParser *************************
 // ******************** ControlSocketParser *************************
-ControlSocketParser::ControlSocketParser(const std::string& param_name) {
-    if (param_name != "control-socket") {
-        isc_throw(BadValue, "Internal error. Control socket parser called "
-                  " for wrong parameter:" << param_name);
+void ControlSocketParser::parse(SrvConfig& srv_cfg, isc::data::ConstElementPtr value) {
+    if (!value) {
+        isc_throw(DhcpConfigError, "Logic error: specified control-socket is null");
     }
     }
-}
 
 
-void ControlSocketParser::build(isc::data::ConstElementPtr value) {
     if (value->getType() != Element::map) {
     if (value->getType() != Element::map) {
-        isc_throw(BadValue, "Specified control-socket is expected to be a map"
+        isc_throw(DhcpConfigError, "Specified control-socket is expected to be a map"
                   ", i.e. a structure defined within { }");
                   ", i.e. a structure defined within { }");
     }
     }
-    CfgMgr::instance().getStagingCfg()->setControlSocketInfo(value);
-}
-
-/// @brief Does nothing.
-void ControlSocketParser::commit() {
-    // Nothing to do.
+    srv_cfg.setControlSocketInfo(value);
 }
 }
 
 
 // ******************** HooksLibrariesParser *************************
 // ******************** HooksLibrariesParser *************************
@@ -804,66 +790,66 @@ OptionDefListParser::parse(CfgOptionDefPtr storage, ConstElementPtr option_def_l
 }
 }
 
 
 //****************************** RelayInfoParser ********************************
 //****************************** RelayInfoParser ********************************
-RelayInfoParser::RelayInfoParser(const std::string&,
-                                 const isc::dhcp::Subnet::RelayInfoPtr& relay_info,
-                                 const Option::Universe& family)
-    :storage_(relay_info), local_(isc::asiolink::IOAddress(
-                                  family == Option::V4 ? "0.0.0.0" : "::")),
-     string_values_(new StringStorage()), family_(family) {
-    if (!relay_info) {
-        isc_throw(isc::dhcp::DhcpConfigError, "parser logic error: "
-                  << "relay-info storage may not be NULL");
-    }
-
+RelayInfoParser::RelayInfoParser(const Option::Universe& family)
+    : family_(family) {
 };
 };
 
 
 void
 void
-RelayInfoParser::build(ConstElementPtr relay_info) {
-
-    BOOST_FOREACH(ConfigPair param, relay_info->mapValue()) {
-        ParserPtr parser(createConfigParser(param.first));
-        parser->build(param.second);
-        parser->commit();
+RelayInfoParser::parse(const isc::dhcp::Subnet::RelayInfoPtr& cfg,
+                       ConstElementPtr relay_info) {
+    // Let's start with some sanity checks.
+    if (!relay_info || !cfg) {
+        isc_throw(DhcpConfigError, "Logic error: RelayInfoParser::parse() called "
+                  "with at least one NULL parameter.");
     }
     }
 
 
-    // Get the IP address
-    boost::scoped_ptr<asiolink::IOAddress> ip;
-    try {
-        ip.reset(new asiolink::IOAddress(string_values_->getParam("ip-address")));
-    } catch (...)  {
-        isc_throw(DhcpConfigError, "Failed to parse ip-address "
-                  "value: " << string_values_->getParam("ip-address")
-                  << " (" << string_values_->getPosition("ip-address") << ")");
+    if (relay_info->getType() != Element::map) {
+        isc_throw(DhcpConfigError, "Configuration error: RelayInfoParser::parse() "
+                  "called with non-map parameter");
     }
     }
 
 
-    if ( (ip->isV4() && family_ != Option::V4) ||
-         (ip->isV6() && family_ != Option::V6) ) {
-        isc_throw(DhcpConfigError, "ip-address field " << ip->toText()
-                  << " does not have IP address of expected family type: "
-                  << (family_ == Option::V4 ? "IPv4" : "IPv6")
-                  << " (" << string_values_->getPosition("ip-address") << ")");
-    }
+    // Now create the default value.
+    isc::asiolink::IOAddress ip(family_ == Option::V4 ? IOAddress::IPV4_ZERO_ADDRESS()
+                                : IOAddress::IPV6_ZERO_ADDRESS());
 
 
-    local_.addr_ = *ip;
-}
+    // Now iterate over all parameters. Currently there's only one supported
+    // parameter, so it should be an easy thing to check.
+    bool ip_address_specified = false;
+    BOOST_FOREACH(ConfigPair param, relay_info->mapValue()) {
+        if (param.first == "ip-address") {
+            ip_address_specified = true;
+
+            try {
+                ip = asiolink::IOAddress(param.second->stringValue());
+            } catch (...)  {
+                isc_throw(DhcpConfigError, "Failed to parse ip-address "
+                          "value: " << param.second
+                          << " (" << param.second->getPosition() << ")");
+            }
 
 
-isc::dhcp::ParserPtr
-RelayInfoParser::createConfigParser(const std::string& parameter) {
-    DhcpConfigParser* parser = NULL;
-    if (parameter.compare("ip-address") == 0) {
-        parser = new StringParser(parameter, string_values_);
-    } else {
-        isc_throw(NotImplemented,
-                  "parser error: RelayInfoParser parameter not supported: "
-                  << parameter);
+            // Check if the address family matches.
+            if ( (ip.isV4() && family_ != Option::V4) ||
+                 (ip.isV6() && family_ != Option::V6) ) {
+                isc_throw(DhcpConfigError, "ip-address field " << ip.toText()
+                          << " does not have IP address of expected family type: "
+                          << (family_ == Option::V4 ? "IPv4" : "IPv6")
+                          << " (" << param.second->getPosition() << ")");
+            }
+        } else {
+            isc_throw(NotImplemented,
+                      "parser error: RelayInfoParser parameter not supported: "
+                      << param.second);
+        }
     }
     }
 
 
-    return (isc::dhcp::ParserPtr(parser));
-}
+    if (!ip_address_specified) {
+        isc_throw(DhcpConfigError, "'relay' specified, but mandatory 'ip-address' "
+                  "paramter in it is missing");
+    }
 
 
-void
-RelayInfoParser::commit() {
-    *storage_ = local_;
+    // Ok, we're done with parsing. Let's store the result in the structure
+    // we were given as configuration storage.
+    *cfg = isc::dhcp::Subnet::RelayInfo(ip);
 }
 }
 
 
 //****************************** PoolsListParser ********************************
 //****************************** PoolsListParser ********************************
@@ -1069,6 +1055,12 @@ SubnetConfigParser::build(ConstElementPtr subnet) {
             continue;
             continue;
         }
         }
 
 
+        if (param.first == "relay") {
+            RelayInfoParser parser(global_context_->universe_);
+            parser.parse(relay_info_, param.second);
+            continue;
+        }
+
         ParserPtr parser;
         ParserPtr parser;
         // When unsupported parameter is specified, the function called
         // When unsupported parameter is specified, the function called
         // below will thrown an exception. We have to catch this exception
         // below will thrown an exception. We have to catch this exception

+ 21 - 66
src/lib/dhcpsrv/parsers/dhcp_parsers.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -16,6 +16,8 @@
 #include <dhcpsrv/cfg_option.h>
 #include <dhcpsrv/cfg_option.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/cfg_option_def.h>
 #include <dhcpsrv/cfg_option_def.h>
+#include <dhcpsrv/cfg_mac_source.h>
+#include <dhcpsrv/srv_config.h>
 #include <dhcpsrv/parsers/dhcp_config_parser.h>
 #include <dhcpsrv/parsers/dhcp_config_parser.h>
 #include <cc/simple_parser.h>
 #include <cc/simple_parser.h>
 #include <hooks/libinfo.h>
 #include <hooks/libinfo.h>
@@ -372,56 +374,33 @@ private:
 /// It contains a list of MAC/hardware acquisition source, i.e. methods how
 /// It contains a list of MAC/hardware acquisition source, i.e. methods how
 /// MAC address can possibly by obtained in DHCPv6. For a currently supported
 /// MAC address can possibly by obtained in DHCPv6. For a currently supported
 /// methods, see @ref isc::dhcp::Pkt::getMAC.
 /// methods, see @ref isc::dhcp::Pkt::getMAC.
-class MACSourcesListConfigParser : public DhcpConfigParser {
+class MACSourcesListConfigParser : public isc::data::SimpleParser {
 public:
 public:
-
-    /// @brief constructor
-    ///
-    /// As this is a dedicated parser, it must be used to parse
-    /// "mac-sources" parameter only. All other types will throw exception.
-    ///
-    /// @param param_name name of the configuration parameter being parsed
-    /// @param global_context Global parser context.
-    /// @throw BadValue if supplied parameter name is not "mac-sources"
-    MACSourcesListConfigParser(const std::string& param_name,
-                               ParserContextPtr global_context);
-
     /// @brief parses parameters value
     /// @brief parses parameters value
     ///
     ///
     /// Parses configuration entry (list of sources) and adds each element
     /// Parses configuration entry (list of sources) and adds each element
     /// to the sources list.
     /// to the sources list.
     ///
     ///
     /// @param value pointer to the content of parsed values
     /// @param value pointer to the content of parsed values
-    virtual void build(isc::data::ConstElementPtr value);
-
-    /// @brief Does nothing.
-    virtual void commit();
-
-private:
-
-    // Parsed parameter name
-    std::string param_name_;
-
-    /// Global parser context.
-    ParserContextPtr global_context_;
+    void parse(CfgMACSource& mac_sources, isc::data::ConstElementPtr value);
 };
 };
 
 
 /// @brief Parser for the control-socket structure
 /// @brief Parser for the control-socket structure
 ///
 ///
 /// It does not parse anything, simply stores the element in
 /// It does not parse anything, simply stores the element in
 /// the staging config.
 /// the staging config.
-class ControlSocketParser : public DhcpConfigParser {
+class ControlSocketParser : public isc::data::SimpleParser {
 public:
 public:
-
-    ControlSocketParser(const std::string& param_name);
-
-    /// @brief Stores contents of the control-socket map in the staging config.
+    /// @brief "Parses" control-socket structure
     ///
     ///
+    /// Since the SrvConfig structure takes the socket definition
+    /// as ConstElementPtr, there's really nothing to parse here.
+    /// It only does basic sanity checks and throws DhcpConfigError
+    /// if the value is null or is not a map.
+    ///
+    /// @param srv_cfg parsed values will be stored here
     /// @param value pointer to the content of parsed values
     /// @param value pointer to the content of parsed values
-    virtual void build(isc::data::ConstElementPtr value);
-
-    /// @brief Does nothing.
-    virtual void commit();
+    void parse(SrvConfig& srv_cfg, isc::data::ConstElementPtr value);
 };
 };
 
 
 /// @brief Parser for hooks library list
 /// @brief Parser for hooks library list
@@ -849,48 +828,24 @@ protected:
 /// is expected that the number of parameters will increase over time.
 /// is expected that the number of parameters will increase over time.
 ///
 ///
 /// It is useful for parsing Dhcp<4/6>/subnet<4/6>[x]/relay parameters.
 /// It is useful for parsing Dhcp<4/6>/subnet<4/6>[x]/relay parameters.
-class RelayInfoParser : public DhcpConfigParser {
+class RelayInfoParser : public isc::data::SimpleParser {
 public:
 public:
 
 
     /// @brief constructor
     /// @brief constructor
-    /// @param unused first argument is ignored, all Parser constructors
-    /// accept string as first argument.
-    /// @param relay_info is the storage in which to store the parsed
     /// @param family specifies protocol family (IPv4 or IPv6)
     /// @param family specifies protocol family (IPv4 or IPv6)
-    RelayInfoParser(const std::string& unused,
-                    const isc::dhcp::Subnet::RelayInfoPtr& relay_info,
-                    const isc::dhcp::Option::Universe& family);
+    RelayInfoParser(const isc::dhcp::Option::Universe& family);
 
 
     /// @brief parses the actual relay parameters
     /// @brief parses the actual relay parameters
-    /// @param relay_info JSON structure holding relay parameters to parse
-    virtual void build(isc::data::ConstElementPtr relay_info);
-
-    /// @brief stores parsed info in relay_info
-    virtual void commit();
-
-protected:
-
-    /// @brief Creates a parser for the given "relay" member element id.
     ///
     ///
     /// The elements currently supported are:
     /// The elements currently supported are:
     /// -# ip-address
     /// -# ip-address
     ///
     ///
-    /// @param parser is the "item_name" for a specific member element of
-    /// the "relay" specification.
-    ///
-    /// @return returns a pointer to newly created parser.
-    isc::dhcp::ParserPtr
-    createConfigParser(const std::string& parser);
-
-    /// Parsed data will be stored there on commit()
-    isc::dhcp::Subnet::RelayInfoPtr storage_;
-
-    /// Local storage information (for temporary values)
-    isc::dhcp::Subnet::RelayInfo local_;
-
-    /// Storage for subnet-specific string values.
-    StringStoragePtr string_values_;
+    /// @param cfg configuration will be stored here
+    /// @param relay_info JSON structure holding relay parameters to parse
+    void parse(const isc::dhcp::Subnet::RelayInfoPtr& cfg,
+               isc::data::ConstElementPtr relay_info);
 
 
+protected:
     /// Protocol family (IPv4 or IPv6)
     /// Protocol family (IPv4 or IPv6)
     Option::Universe family_;
     Option::Universe family_;
 };
 };

+ 127 - 52
src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -226,48 +226,95 @@ TEST_F(DhcpParserTest, uint32ParserTest) {
     EXPECT_EQ(test_value, actual_value);
     EXPECT_EQ(test_value, actual_value);
 }
 }
 
 
-/// @brief Check MACSourcesListConfigParser  basic functionality
+/// Verifies the code that parses mac sources and adds them to CfgMgr
+TEST_F(DhcpParserTest, MacSources) {
+
+    // That's an equivalent of the following snippet:
+    // "mac-sources: [ \"duid\", \"ipv6\" ]";
+    ElementPtr values = Element::createList();
+    values->add(Element::create("duid"));
+    values->add(Element::create("ipv6-link-local"));
+
+    // Let's grab server configuration from CfgMgr
+    SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
+    ASSERT_TRUE(cfg);
+    CfgMACSource& sources = cfg->getMACSources();
+
+    // This should parse the configuration and check that it doesn't throw.
+    MACSourcesListConfigParser parser;
+    EXPECT_NO_THROW(parser.parse(sources, values));
+
+    // Finally, check the sources that were configured
+    CfgMACSources configured_sources =  cfg->getMACSources().get();
+    ASSERT_EQ(2, configured_sources.size());
+    EXPECT_EQ(HWAddr::HWADDR_SOURCE_DUID, configured_sources[0]);
+    EXPECT_EQ(HWAddr::HWADDR_SOURCE_IPV6_LINK_LOCAL, configured_sources[1]);
+}
+
+/// @brief Check MACSourcesListConfigParser rejecting empty list
 ///
 ///
-/// Verifies that the parser:
-/// 1. Does not allow empty for storage.
-/// 2. Does not allow name other than "mac-sources"
-/// 3. Parses list of mac sources and adds them to CfgMgr
-TEST_F(DhcpParserTest, MacSourcesListConfigParserTest) {
+/// Verifies that the code rejects an empty mac-sources list.
+TEST_F(DhcpParserTest, MacSourcesEmpty) {
 
 
-    const std::string valid_name = "mac-sources";
-    const std::string bogus_name = "bogus-name";
+    // That's an equivalent of the following snippet:
+    // "mac-sources: [ \"duid\", \"ipv6\" ]";
+    ElementPtr values = Element::createList();
+
+    // Let's grab server configuration from CfgMgr
+    SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
+    ASSERT_TRUE(cfg);
+    CfgMACSource& sources = cfg->getMACSources();
 
 
-    ParserContextPtr parser_context(new ParserContext(Option::V6));
+    // This should throw, because if specified, at least one MAC source
+    // has to be specified.
+    MACSourcesListConfigParser parser;
+    EXPECT_THROW(parser.parse(sources, values), DhcpConfigError);
+}
 
 
-    // Verify that parser constructor fails if parameter name isn't "mac-sources"
-    EXPECT_THROW(MACSourcesListConfigParser(bogus_name, parser_context),
-                 isc::BadValue);
+/// @brief Check MACSourcesListConfigParser rejecting empty list
+///
+/// Verifies that the code rejects fake mac source.
+TEST_F(DhcpParserTest, MacSourcesBogus) {
 
 
     // That's an equivalent of the following snippet:
     // That's an equivalent of the following snippet:
     // "mac-sources: [ \"duid\", \"ipv6\" ]";
     // "mac-sources: [ \"duid\", \"ipv6\" ]";
-    ElementPtr config = Element::createList();
-    config->add(Element::create("duid"));
-    config->add(Element::create("ipv6-link-local"));
+    ElementPtr values = Element::createList();
+    values->add(Element::create("from-ebay"));
+    values->add(Element::create("just-guess-it"));
+
+    // Let's grab server configuration from CfgMgr
+    SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
+    ASSERT_TRUE(cfg);
+    CfgMACSource& sources = cfg->getMACSources();
+
+    // This should throw, because these are not valid sources.
+    MACSourcesListConfigParser parser;
+    EXPECT_THROW(parser.parse(sources, values), DhcpConfigError);
+}
 
 
-    boost::scoped_ptr<MACSourcesListConfigParser>
-        parser(new MACSourcesListConfigParser(valid_name, parser_context));
+/// Verifies the code that properly catches duplicate entries
+/// in mac-sources definition.
+TEST_F(DhcpParserTest, MacSourcesDuplicate) {
 
 
-    // This should parse the configuration and add eth0 and eth1 to the list
-    // of interfaces that server should listen on.
-    EXPECT_NO_THROW(parser->build(config));
-    EXPECT_NO_THROW(parser->commit());
+    // That's an equivalent of the following snippet:
+    // "mac-sources: [ \"duid\", \"ipv6\" ]";
+    ElementPtr values = Element::createList();
+    values->add(Element::create("ipv6-link-local"));
+    values->add(Element::create("duid"));
+    values->add(Element::create("duid"));
+    values->add(Element::create("duid"));
 
 
-    // Use CfgMgr instance to check if eth0 and eth1 was added, and that
-    // eth2 was not added.
+    // Let's grab server configuration from CfgMgr
     SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
     SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
     ASSERT_TRUE(cfg);
     ASSERT_TRUE(cfg);
-    CfgMACSources configured_sources =  cfg->getMACSources().get();
+    CfgMACSource& sources = cfg->getMACSources();
 
 
-    ASSERT_EQ(2, configured_sources.size());
-    EXPECT_EQ(HWAddr::HWADDR_SOURCE_DUID, configured_sources[0]);
-    EXPECT_EQ(HWAddr::HWADDR_SOURCE_IPV6_LINK_LOCAL, configured_sources[1]);
+    // This should parse the configuration and check that it throws.
+    MACSourcesListConfigParser parser;
+    EXPECT_THROW(parser.parse(sources, values), DhcpConfigError);
 }
 }
 
 
+
 /// @brief Test Fixture class which provides basic structure for testing
 /// @brief Test Fixture class which provides basic structure for testing
 /// configuration parsing.  This is essentially the same structure provided
 /// configuration parsing.  This is essentially the same structure provided
 /// by dhcp servers.
 /// by dhcp servers.
@@ -2416,6 +2463,19 @@ TEST_F(ParseConfigTest, validRelayInfo4) {
         "    }";
         "    }";
     ElementPtr json = Element::fromJSON(config_str);
     ElementPtr json = Element::fromJSON(config_str);
 
 
+    // We need to set the default ip-address to something.
+    Subnet::RelayInfoPtr result(new Subnet::RelayInfo(asiolink::IOAddress("0.0.0.0")));
+
+    RelayInfoParser parser(Option::V4);
+
+    // Subnet4 parser will pass 0.0.0.0 to the RelayInfoParser
+    EXPECT_NO_THROW(parser.parse(result, json));
+    EXPECT_EQ("192.0.2.1", result->addr_.toText());
+}
+
+/// @brief Checks that a bogus relay info structure for IPv4 is rejected.
+TEST_F(ParseConfigTest, bogusRelayInfo4) {
+
     // Invalid config (wrong family type of the ip-address field)
     // Invalid config (wrong family type of the ip-address field)
     std::string config_str_bogus1 =
     std::string config_str_bogus1 =
         "    {"
         "    {"
@@ -2430,24 +2490,25 @@ TEST_F(ParseConfigTest, validRelayInfo4) {
         "    }";
         "    }";
     ElementPtr json_bogus2 = Element::fromJSON(config_str_bogus2);
     ElementPtr json_bogus2 = Element::fromJSON(config_str_bogus2);
 
 
-    // We need to set the default ip-address to something.
-    Subnet::RelayInfoPtr result(new Subnet::RelayInfo(asiolink::IOAddress("0.0.0.0")));
+    // Invalid config (ip-address is mandatory)
+    std::string config_str_bogus3 =
+        "    {"
+        "    }";
+    ElementPtr json_bogus3 = Element::fromJSON(config_str_bogus3);
 
 
-    boost::shared_ptr<RelayInfoParser> parser;
+    // We need to set the default ip-address to something.
+    Subnet::RelayInfoPtr result(new Subnet::RelayInfo(IOAddress::IPV4_ZERO_ADDRESS()));
 
 
-    // Subnet4 parser will pass 0.0.0.0 to the RelayInfoParser
-    EXPECT_NO_THROW(parser.reset(new RelayInfoParser("ignored", result,
-                                                     Option::V4)));
-    EXPECT_NO_THROW(parser->build(json));
-    EXPECT_NO_THROW(parser->commit());
+    RelayInfoParser parser(Option::V4);
 
 
-    EXPECT_EQ("192.0.2.1", result->addr_.toText());
+    // wrong family type
+    EXPECT_THROW(parser.parse(result, json_bogus1), DhcpConfigError);
 
 
-    // Let's check negative scenario (wrong family type)
-    EXPECT_THROW(parser->build(json_bogus1), DhcpConfigError);
+    // Too large byte values in pseudo-IPv4 addr
+    EXPECT_THROW(parser.parse(result, json_bogus2), DhcpConfigError);
 
 
-    // Let's check negative scenario (too large byte values in pseudo-IPv4 addr)
-    EXPECT_THROW(parser->build(json_bogus2), DhcpConfigError);
+    // Mandatory ip-address is missing. What a pity.
+    EXPECT_THROW(parser.parse(result, json_bogus2), DhcpConfigError);
 }
 }
 
 
 /// @brief Checks that a valid relay info structure for IPv6 can be handled
 /// @brief Checks that a valid relay info structure for IPv6 can be handled
@@ -2460,6 +2521,18 @@ TEST_F(ParseConfigTest, validRelayInfo6) {
         "    }";
         "    }";
     ElementPtr json = Element::fromJSON(config_str);
     ElementPtr json = Element::fromJSON(config_str);
 
 
+    // We need to set the default ip-address to something.
+    Subnet::RelayInfoPtr result(new Subnet::RelayInfo(asiolink::IOAddress("::")));
+
+    RelayInfoParser parser(Option::V6);
+    // Subnet4 parser will pass :: to the RelayInfoParser
+    EXPECT_NO_THROW(parser.parse(result, json));
+    EXPECT_EQ("2001:db8::1", result->addr_.toText());
+}
+
+/// @brief Checks that a valid relay info structure for IPv6 can be handled
+TEST_F(ParseConfigTest, bogusRelayInfo6) {
+
     // Invalid config (wrong family type of the ip-address field
     // Invalid config (wrong family type of the ip-address field
     std::string config_str_bogus1 =
     std::string config_str_bogus1 =
         "    {"
         "    {"
@@ -2474,23 +2547,25 @@ TEST_F(ParseConfigTest, validRelayInfo6) {
         "    }";
         "    }";
     ElementPtr json_bogus2 = Element::fromJSON(config_str_bogus2);
     ElementPtr json_bogus2 = Element::fromJSON(config_str_bogus2);
 
 
+    // Missing mandatory ip-address field.
+    std::string config_str_bogus3 =
+        "    {"
+        "    }";
+    ElementPtr json_bogus3 = Element::fromJSON(config_str_bogus3);
+
     // We need to set the default ip-address to something.
     // We need to set the default ip-address to something.
     Subnet::RelayInfoPtr result(new Subnet::RelayInfo(asiolink::IOAddress("::")));
     Subnet::RelayInfoPtr result(new Subnet::RelayInfo(asiolink::IOAddress("::")));
 
 
-    boost::shared_ptr<RelayInfoParser> parser;
-    // Subnet4 parser will pass :: to the RelayInfoParser
-    EXPECT_NO_THROW(parser.reset(new RelayInfoParser("ignored", result,
-                                                     Option::V6)));
-    EXPECT_NO_THROW(parser->build(json));
-    EXPECT_NO_THROW(parser->commit());
+    RelayInfoParser parser(Option::V6);
 
 
-    EXPECT_EQ("2001:db8::1", result->addr_.toText());
+    // Negative scenario (wrong family type)
+    EXPECT_THROW(parser.parse(result, json_bogus1), DhcpConfigError);
 
 
-    // Let's check negative scenario (wrong family type)
-    EXPECT_THROW(parser->build(json_bogus1), DhcpConfigError);
+    // Looks like IPv6 address, but has too many colons
+    EXPECT_THROW(parser.parse(result, json_bogus2), DhcpConfigError);
 
 
-    // Unparseable text that looks like IPv6 address, but has too many colons
-    EXPECT_THROW(parser->build(json_bogus2), DhcpConfigError);
+    // Mandatory ip-address is missing. What a pity.
+    EXPECT_THROW(parser.parse(result, json_bogus3), DhcpConfigError);
 }
 }
 
 
 // There's no test for ControlSocketParser, as it is tested in the DHCPv4 code
 // There's no test for ControlSocketParser, as it is tested in the DHCPv4 code

+ 73 - 1
src/lib/testutils/io_utils.cc

@@ -4,6 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
+#include <exceptions/exceptions.h>
 #include <testutils/io_utils.h>
 #include <testutils/io_utils.h>
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
 #include <fstream>
 #include <fstream>
@@ -36,7 +37,78 @@ std::string readFile(const std::string& file_path) {
     return (output.str());
     return (output.str());
 }
 }
 
 
+std::string decommentJSONfile(const std::string& input_file) {
+
+    using namespace std;
+
+    ifstream f(input_file);
+    if (!f.is_open()) {
+        isc_throw(isc::BadValue, "can't open input file for reading: " + input_file);
+    }
+
+    string outfile;
+    size_t last_slash = input_file.find_last_of("/");
+    if (last_slash != string::npos) {
+        outfile = input_file.substr(last_slash + 1);
+    } else {
+        outfile = input_file;
+    }
+    outfile += "-decommented";
+
+    ofstream out(outfile);
+    if (!out.is_open()) {
+        isc_throw(isc::BadValue, "can't open output file for writing: " + input_file);
+    }
+
+    bool in_comment = false;
+    string line;
+    while (std::getline(f, line)) {
+        // First, let's get rid of the # comments
+        size_t hash_pos = line.find("#");
+        if (hash_pos != string::npos) {
+            line = line.substr(0, hash_pos);
+        }
+
+        // Second, let's get rid of the // comments
+        size_t dblslash_pos = line.find("//");
+        if (dblslash_pos != string::npos) {
+            line = line.substr(0, dblslash_pos);
+        }
+
+        // Now the tricky part: c comments.
+        size_t begin_pos = line.find("/*");
+        size_t end_pos = line.find("*/");
+        if (in_comment && end_pos == string::npos) {
+            // we continue through multiline comment
+            line = "";
+        } else {
+
+            if (begin_pos != string::npos) {
+                in_comment = true;
+                if (end_pos != string::npos) {
+                    // sigle line comment. Let's get rid of the content in between
+                    line = line.replace(begin_pos, end_pos + 2, end_pos + 2 - begin_pos, ' ');
+                    in_comment = false;
+                } else {
+                    line = line.substr(0, begin_pos);
+                }
+            } else {
+                if (in_comment && end_pos != string::npos) {
+                    line = line.replace(0, end_pos +2 , end_pos + 2, ' ');
+                    in_comment = false;
+                }
+            }
+        }
+
+        // Finally, write the line to the output file.
+        out << line << endl;
+    }
+    f.close();
+    out.close();
+
+    return (outfile);
+}
+
 }; // end of isc::dhcp::test namespace
 }; // end of isc::dhcp::test namespace
 }; // end of isc::dhcp namespace
 }; // end of isc::dhcp namespace
 }; // end of isc namespace
 }; // end of isc namespace
-

+ 14 - 0
src/lib/testutils/io_utils.h

@@ -26,6 +26,20 @@ bool fileExists(const std::string& file_path);
 /// @return File contents.
 /// @return File contents.
 std::string readFile(const std::string& file_path);
 std::string readFile(const std::string& file_path);
 
 
+/// @brief Removes comments from a JSON file
+///
+/// Removes //, # and /* */ comments from the input file and writes its content
+/// to another file. The comments are replaced with spaces, so the original
+/// token locations should remain unaffected. This is rather naive
+/// implementation, but it's probably sufficient for testing. It won't be able
+/// to pick any trickier cases, like # or // appearing in strings, nested C++
+/// comments etc.
+///
+/// @param input_file file to be stripped of comments
+/// @return filename of a new file that has comments stripped from it
+/// @throw BadValue if the input file cannot be opened
+std::string decommentJSONfile(const std::string& input_file);
+
 }; // end of isc::dhcp::test namespace
 }; // end of isc::dhcp::test namespace
 }; // end of isc::dhcp namespace
 }; // end of isc::dhcp namespace
 }; // end of isc namespace
 }; // end of isc namespace