Browse Source

[5022] DHCPv6 server now supports specifying options on pool level.

Marcin Siodelski 8 years ago
parent
commit
0a6024687c

+ 30 - 9
src/bin/dhcp6/dhcp6_srv.cc

@@ -820,6 +820,24 @@ Dhcpv6Srv::buildCfgOptionList(const Pkt6Ptr& question,
         co_list.push_back(ctx.host_->getCfgOption6());
     }
 
+    // Secondly, pool specific options. Pools are defined within a subnet, so
+    // if there is no subnet, there is nothing to do.
+    if (ctx.subnet_) {
+        BOOST_FOREACH(const AllocEngine::ResourceType& resource,
+                      ctx.allocated_resources_) {
+            /// @todo This is has significant performance implications. We
+            /// are performing full scan of pools within this subnet to
+            /// find the one we're interested in. We need to implement the
+            /// Patricia trie based storage for pools.
+            PoolPtr pool = ctx.subnet_->getPool(resource.second == 128 ?
+                                                Lease::TYPE_NA : Lease::TYPE_PD,
+                                                resource.first, false);
+            if (pool && !pool->getCfgOption()->empty()) {
+                co_list.push_back(pool->getCfgOption());
+            }
+        }
+    };
+
     // Next, subnet configured options.
     if (ctx.subnet_ && !ctx.subnet_->getCfgOption()->empty()) {
         co_list.push_back(ctx.subnet_->getCfgOption());
@@ -2316,6 +2334,9 @@ Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
         }
     }
 
+    processClientFqdn(solicit, response, ctx);
+    assignLeases(solicit, response, ctx);
+
     copyClientOptions(solicit, response);
     CfgOptionList co_list;
     buildCfgOptionList(solicit, ctx, co_list);
@@ -2323,9 +2344,6 @@ Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
     appendRequestedOptions(solicit, response, co_list);
     appendRequestedVendorOptions(solicit, response, ctx, co_list);
 
-    processClientFqdn(solicit, response, ctx);
-    assignLeases(solicit, response, ctx);
-
     // Only generate name change requests if sending a Reply as a result
     // of receiving Rapid Commit option.
     if (response->getType() == DHCPV6_REPLY) {
@@ -2347,6 +2365,9 @@ Dhcpv6Srv::processRequest(const Pkt6Ptr& request) {
 
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, request->getTransid()));
 
+    processClientFqdn(request, reply, ctx);
+    assignLeases(request, reply, ctx);
+
     copyClientOptions(request, reply);
     CfgOptionList co_list;
     buildCfgOptionList(request, ctx, co_list);
@@ -2354,8 +2375,6 @@ Dhcpv6Srv::processRequest(const Pkt6Ptr& request) {
     appendRequestedOptions(request, reply, co_list);
     appendRequestedVendorOptions(request, reply, ctx, co_list);
 
-    processClientFqdn(request, reply, ctx);
-    assignLeases(request, reply, ctx);
     generateFqdn(reply);
     createNameChangeRequests(reply, ctx);
 
@@ -2374,6 +2393,9 @@ Dhcpv6Srv::processRenew(const Pkt6Ptr& renew) {
 
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, renew->getTransid()));
 
+    processClientFqdn(renew, reply, ctx);
+    extendLeases(renew, reply, ctx);
+
     copyClientOptions(renew, reply);
     CfgOptionList co_list;
     buildCfgOptionList(renew, ctx, co_list);
@@ -2381,8 +2403,6 @@ Dhcpv6Srv::processRenew(const Pkt6Ptr& renew) {
     appendRequestedOptions(renew, reply, co_list);
     appendRequestedVendorOptions(renew, reply, ctx, co_list);
 
-    processClientFqdn(renew, reply, ctx);
-    extendLeases(renew, reply, ctx);
     generateFqdn(reply);
     createNameChangeRequests(reply, ctx);
 
@@ -2401,6 +2421,9 @@ Dhcpv6Srv::processRebind(const Pkt6Ptr& rebind) {
 
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, rebind->getTransid()));
 
+    processClientFqdn(rebind, reply, ctx);
+    extendLeases(rebind, reply, ctx);
+
     copyClientOptions(rebind, reply);
     CfgOptionList co_list;
     buildCfgOptionList(rebind, ctx, co_list);
@@ -2408,8 +2431,6 @@ Dhcpv6Srv::processRebind(const Pkt6Ptr& rebind) {
     appendRequestedOptions(rebind, reply, co_list);
     appendRequestedVendorOptions(rebind, reply, ctx, co_list);
 
-    processClientFqdn(rebind, reply, ctx);
-    extendLeases(rebind, reply, ctx);
     generateFqdn(reply);
     createNameChangeRequests(reply, ctx);
 

+ 2 - 0
src/bin/dhcp6/json_config_parser.cc

@@ -206,6 +206,8 @@ public:
             // Attempt to construct the local pool.
             pool_.reset(new Pool6(Lease::TYPE_PD, IOAddress(addr_str),
                                   prefix_len, delegated_len));
+            // Merge options specified for a pool into pool configuration.
+            options_->copyTo(*pool_->getCfgOption());
         } catch (const std::exception& ex) {
             // Some parameters don't exist or are invalid. Since we are not
             // aware whether they don't exist or are invalid, let's append

+ 65 - 21
src/bin/dhcp6/tests/config_parser_unittest.cc

@@ -2556,7 +2556,9 @@ TEST_F(Dhcp6ParserTest, optionDataInMultipleSubnets) {
                sizeof(user_class_expected));
 }
 
-TEST_F(Dhcp6ParserTest, optionDataInMultiplePools) {
+// This test verifies that it is possible to specify options on
+// pool levels.
+TEST_F(Dhcp6ParserTest, optionDataMultiplePools) {
     ConstElementPtr x;
     string config = "{ " + genIfaceConfig() + ","
         "\"preferred-lifetime\": 3000,"
@@ -2564,20 +2566,20 @@ TEST_F(Dhcp6ParserTest, optionDataInMultiplePools) {
         "\"renew-timer\": 1000, "
         "\"subnet6\": [ { "
         "    \"pools\": [ { "
-        "        \"pool\": \"2001:db8:1::10 - 2001:db8:1::100\""
-/*        "        \"option-data\": [ {"
+        "        \"pool\": \"2001:db8:1::10 - 2001:db8:1::100\","
+        "        \"option-data\": [ {"
         "            \"name\": \"subscriber-id\","
         "            \"data\": \"0102030405060708090A\","
         "            \"csv-format\": False"
-        "        } ]" */
+        "        } ]"
         "    },"
         "    {"
-        "        \"pool\": \"2001:db8:1::300 - 2001:db8:1::400\""
-/*        "        \"option-data\": [ {"
+        "        \"pool\": \"2001:db8:1::300 - 2001:db8:1::400\","
+        "        \"option-data\": [ {"
         "            \"name\": \"user-class\","
         "            \"data\": \"FFFEFDFCFB\","
         "            \"csv-format\": False"
-        "        } ]" */
+        "        } ]"
         "    } ],"
         "    \"pd-pools\": [ { "
         "        \"prefix\": \"3000::\","
@@ -2615,7 +2617,7 @@ TEST_F(Dhcp6ParserTest, optionDataInMultiplePools) {
     PoolPtr pool = subnet->getPool(Lease::TYPE_PD, IOAddress("3000::"), false);
     ASSERT_TRUE(pool);
     Pool6Ptr pool6 = boost::dynamic_pointer_cast<Pool6>(pool);
-
+    ASSERT_TRUE(pool6);
 
     OptionContainerPtr options1 = pool6->getCfgOption()->getAll("dhcp6");
     ASSERT_EQ(1, options1->size());
@@ -2629,21 +2631,22 @@ TEST_F(Dhcp6ParserTest, optionDataInMultiplePools) {
     std::pair<OptionContainerTypeIndex::const_iterator,
               OptionContainerTypeIndex::const_iterator> range1 =
         idx1.equal_range(D6O_SUBSCRIBER_ID);
-    // Expect single option with the code equal to 38.
+    // Expect a single Subscriber ID option instance.
     ASSERT_EQ(1, std::distance(range1.first, range1.second));
-    const uint8_t subid_expected[] = {
-        0x01, 0x02, 0x03, 0x04, 0x05,
-        0x06, 0x07, 0x08, 0x09, 0x0A
+    const uint8_t subscriber_id_expected[] = {
+        0x11, 0x22, 0x33, 0x44, 0x55, 0x66
     };
     // Check if option is valid in terms of code and carried data.
-    testOption(*range1.first, D6O_SUBSCRIBER_ID, subid_expected,
-               sizeof(subid_expected));
+    testOption(*range1.first, D6O_SUBSCRIBER_ID, subscriber_id_expected,
+               sizeof(subscriber_id_expected));
 
-/*    // Test another subnet in the same way.
-    Subnet6Ptr subnet2 = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->
-        selectSubnet(IOAddress("2001:db8:2::4"), classify_);
-    ASSERT_TRUE(subnet2);
-    OptionContainerPtr options2 = subnet2->getCfgOption()->getAll("dhcp6");
+    // Test another pool in the same way.
+    pool = subnet->getPool(Lease::TYPE_PD, IOAddress("3001::"), false);
+    ASSERT_TRUE(pool);
+    pool6 = boost::dynamic_pointer_cast<Pool6>(pool);
+    ASSERT_TRUE(pool6);
+
+    OptionContainerPtr options2 = pool6->getCfgOption()->getAll("dhcp6");
     ASSERT_EQ(1, options2->size());
 
     const OptionContainerTypeIndex& idx2 = options2->get<1>();
@@ -2653,11 +2656,52 @@ TEST_F(Dhcp6ParserTest, optionDataInMultiplePools) {
     ASSERT_EQ(1, std::distance(range2.first, range2.second));
 
     const uint8_t user_class_expected[] = {
-        0xFF, 0xFE, 0xFD, 0xFC, 0xFB
+        0xAA, 0xBB, 0xCC, 0xDD, 0xEE
     };
     testOption(*range2.first, D6O_USER_CLASS, user_class_expected,
-               sizeof(user_class_expected)); */
+               sizeof(user_class_expected));
+
+    // Test options in NA pools.
+    pool = subnet->getPool(Lease::TYPE_NA, IOAddress("2001:db8:1::10"));
+    ASSERT_TRUE(pool);
+    pool6 = boost::dynamic_pointer_cast<Pool6>(pool);
+    ASSERT_TRUE(pool6);
+
+    OptionContainerPtr options3 = pool6->getCfgOption()->getAll("dhcp6");
+    ASSERT_EQ(1, options3->size());
+
+    const OptionContainerTypeIndex& idx3 = options3->get<1>();
+    std::pair<OptionContainerTypeIndex::const_iterator,
+              OptionContainerTypeIndex::const_iterator> range3 =
+        idx3.equal_range(D6O_SUBSCRIBER_ID);
+    ASSERT_EQ(1, std::distance(range3.first, range3.second));
+
+    const uint8_t subscriber_id_expected2[] = {
+        0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A
+    };
+    testOption(*range3.first, D6O_SUBSCRIBER_ID, subscriber_id_expected2,
+               sizeof(subscriber_id_expected2));
 
+
+    pool = subnet->getPool(Lease::TYPE_NA, IOAddress("2001:db8:1::300"));
+    ASSERT_TRUE(pool);
+    pool6 = boost::dynamic_pointer_cast<Pool6>(pool);
+    ASSERT_TRUE(pool6);
+
+    OptionContainerPtr options4 = pool6->getCfgOption()->getAll("dhcp6");
+    ASSERT_EQ(1, options4->size());
+
+    const OptionContainerTypeIndex& idx4 = options4->get<1>();
+    std::pair<OptionContainerTypeIndex::const_iterator,
+              OptionContainerTypeIndex::const_iterator> range4 =
+        idx4.equal_range(D6O_USER_CLASS);
+    ASSERT_EQ(1, std::distance(range4.first, range4.second));
+
+    const uint8_t user_class_expected2[] = {
+        0xFF, 0xFE, 0xFD, 0xFC, 0xFB
+    };
+    testOption(*range4.first, D6O_USER_CLASS, user_class_expected2,
+               sizeof(user_class_expected2));
 }
 
 // The goal of this test is to check that the option carrying a boolean

+ 18 - 2
src/bin/dhcp6/tests/dhcp6_client.cc

@@ -8,11 +8,12 @@
 #include <dhcp/dhcp6.h>
 #include <dhcp/docsis3_option_defs.h>
 #include <dhcp/option_custom.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp/option_vendor.h>
+#include <dhcp/option6_addrlst.h>
 #include <dhcp/option6_ia.h>
 #include <dhcp/option6_iaaddr.h>
 #include <dhcp/option6_status_code.h>
-#include <dhcp/option_int_array.h>
-#include <dhcp/option_vendor.h>
 #include <dhcp/pkt6.h>
 #include <dhcpsrv/lease.h>
 #include <dhcpsrv/pool.h>
@@ -20,6 +21,7 @@
 #include <util/buffer.h>
 #include <boost/foreach.hpp>
 #include <boost/pointer_cast.hpp>
+#include <algorithm>
 #include <cstdlib>
 #include <time.h>
 
@@ -803,6 +805,20 @@ Dhcp6Client::hasLeaseWithZeroLifetimeForPrefix(const asiolink::IOAddress& prefix
     return (false);
 }
 
+bool
+Dhcp6Client::hasOptionWithAddress(const uint16_t option_type,
+                                  const std::string& expected_address) const {
+    Option6AddrLstPtr opt = boost::dynamic_pointer_cast<
+        Option6AddrLst>(config_.findOption(option_type));
+    if (opt) {
+        Option6AddrLst::AddressContainer addrs = opt->getAddresses();
+        if (!addrs.empty()) {
+            return (std::find(addrs.begin(), addrs.end(),
+                              IOAddress(expected_address)) != addrs.end());
+        }
+    }
+    return (false);
+}
 
 uint16_t
 Dhcp6Client::getStatusCode(const uint32_t iaid) const {

+ 13 - 0
src/bin/dhcp6/tests/dhcp6_client.h

@@ -468,6 +468,19 @@ public:
     bool hasLeaseWithZeroLifetimeForPrefix(const asiolink::IOAddress& prefix,
                                            const uint8_t prefix_len) const;
 
+    /// @brief Checks that specified option exists and contains a desired
+    /// address.
+    ///
+    /// The option must cast to the @ref Option6AddrLst type. The function
+    /// expects that this option contains at least one address and checks
+    /// first address for equality with @ref expected_address.
+    ///
+    /// @param option_type Option type.
+    /// @param expected_address Desired address.
+    /// @param config Configuration obtained from the server.
+    bool hasOptionWithAddress(const uint16_t option_type,
+                              const std::string& expected_address) const;
+
     /// @brief Returns the value of the global status code for the last
     /// transaction.
     uint16_t getStatusCode() const {

+ 1 - 0
src/bin/dhcp6/tests/dhcp6_test_utils.h

@@ -624,6 +624,7 @@ public:
     /// @param stat_name this statistic is expected to be set to 1
     void testReceiveStats(uint8_t pkt_type, const std::string& stat_name);
 
+
     /// A subnet used in most tests
     isc::dhcp::Subnet6Ptr subnet_;
 

+ 142 - 1
src/bin/dhcp6/tests/rebind_unittest.cc

@@ -69,6 +69,15 @@ namespace {
 ///   - 1 subnet for eth0 and 1 subnet for eth1
 ///   - DOCSIS vendor config file sub-option
 ///
+/// - Configuration 8:
+///   - single subnet 3000::/32,
+///   - two options specified in the subnet scope,
+///   - one option specified at the global scope,
+///   - two address pools: 3000::10-3000::20, 3000::40-3000::50,
+///   - two prefix pools: 2001:db8:3::/64 and 2001:db8:4::/64,
+///   - an option with unique value specified for each pool, so as it is
+///     possible to test that pool specific options can be assigned.
+///
 const char* REBIND_CONFIGS[] = {
 // Configuration 0
     "{ \"interfaces-config\": {"
@@ -256,7 +265,69 @@ const char* REBIND_CONFIGS[] = {
         "    \"interface-id\": \"\","
         "    \"interface\": \"eth1\""
         " } ],"
-        "\"valid-lifetime\": 4000 }"
+        "\"valid-lifetime\": 4000 }",
+
+// Configuration 8
+    "{ \"interfaces-config\": {"
+        "  \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"option-data\": [ {"
+        "    \"name\": \"dns-servers\","
+        "    \"data\": \"3000:1::234\""
+        "},"
+        "{"
+        "    \"name\": \"sntp-servers\","
+        "    \"data\": \"3000:2::1\""
+        "} ],"
+        "\"subnet6\": [ { "
+        "    \"option-data\": [ {"
+        "        \"name\": \"dns-servers\","
+        "        \"data\": \"3000:1::567\""
+        "    },"
+        "    {"
+        "        \"name\": \"sntp-servers\","
+        "        \"data\": \"3000:2::1\""
+        "    } ],"
+        "    \"pools\": [ { "
+        "        \"pool\": \"3000::10 - 3000::20\","
+        "        \"option-data\": [ {"
+        "            \"name\": \"sntp-servers\","
+        "            \"data\": \"3000:2::2\""
+        "        } ]"
+        "    },"
+        "    {"
+        "        \"pool\": \"3000::40 - 3000::50\","
+        "        \"option-data\": [ {"
+        "            \"name\": \"nisp-servers\","
+        "            \"data\": \"3000:2::3\""
+        "        } ]"
+        "    } ],"
+        "    \"pd-pools\": [ { "
+        "        \"prefix\": \"2001:db8:3::\","
+        "        \"prefix-len\": 64,"
+        "        \"delegated-len\": 64,"
+        "        \"option-data\": [ {"
+        "            \"name\": \"dns-servers\","
+        "            \"data\": \"3000:1::678\""
+        "        } ]"
+        "    },"
+        "    {"
+        "        \"prefix\": \"2001:db8:4::\","
+        "        \"prefix-len\": 64,"
+        "        \"delegated-len\": 64,"
+        "        \"option-data\": [ {"
+        "            \"name\": \"nis-servers\","
+        "            \"data\": \"3000:1::789\""
+        "        } ]"
+        "    } ],"
+        "    \"subnet\": \"3000::/32\", "
+        "    \"interface\": \"eth0\""
+        " } ],"
+        "\"valid-lifetime\": 4000"
+    "}"
 };
 
 /// @brief Test fixture class for testing Rebind.
@@ -1004,4 +1075,74 @@ TEST_F(RebindTest, docsisORO) {
     EXPECT_EQ("normal_erouter_v6.cm", config_file->getValue());
 }
 
+// This test verifies that the same options can be specified on the global
+// level, subnet level and pool level. The options associated with pools
+// are used when the lease is handed out from these pools.
+TEST_F(RebindTest, optionsInheritance) {
+    Dhcp6Client client;
+    // Request a single address and single prefix.
+    ASSERT_NO_THROW(client.requestPrefix(0xabac, 64, IOAddress("2001:db8:4::")));
+    ASSERT_NO_THROW(client.requestAddress(0xabca, IOAddress("3000::45")));
+    // Request two options configured for the pools from which the client may get
+    // a lease.
+    client.requestOption(D6O_NAME_SERVERS);
+    client.requestOption(D6O_NIS_SERVERS);
+    client.requestOption(D6O_NISP_SERVERS);
+    client.requestOption(D6O_SNTP_SERVERS);
+    ASSERT_NO_FATAL_FAILURE(configure(REBIND_CONFIGS[8], *client.getServer()));
+    // Make sure we ended-up having expected number of subnets configured.
+    const Subnet6Collection* subnets = CfgMgr::instance().getCurrentCfg()->
+        getCfgSubnets6()->getAll();
+    ASSERT_EQ(1, subnets->size());
+    // Perform 4-way exchange.
+    ASSERT_NO_THROW(client.doSARR());
+
+    // Simulate aging of leases.
+    client.fastFwdTime(1000);
+
+    // Send Rebind message to the server.
+    ASSERT_NO_THROW(client.doRebind());
+
+    // We have provided hints so we should get leases appropriate
+    // for the hints we provided.
+    ASSERT_TRUE(client.hasLeaseForPrefix(IOAddress("2001:db8:4::"), 64));
+    ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("3000::45")));
+    // We shouldn't have leases for the prefix and address which we didn't
+    // request.
+    ASSERT_FALSE(client.hasLeaseForPrefix(IOAddress("2001:db8:3::"), 64));
+    ASSERT_FALSE(client.hasLeaseForAddress(IOAddress("3000::11")));
+
+    // We should have received options associated with a prefix pool and
+    // address pool from which we have requested the leases. We should not
+    // have received options associated with the remaining pools. Instead,
+    // we should have received options associated with a subnet.
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "3000:1::567"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NIS_SERVERS, "3000:1::789"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NISP_SERVERS, "3000:2::3"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_SNTP_SERVERS, "3000:2::1"));
+
+    // Let's now also request a prefix and an address from the remaining pools.
+    ASSERT_NO_THROW(client.requestPrefix(0x6806, 64, IOAddress("2001:db8:3::")));
+    ASSERT_NO_THROW(client.requestAddress(0x6860, IOAddress("3000::11")));
+
+    client.fastFwdTime(1000);
+
+    // Send another Rebind.
+    ASSERT_NO_THROW(client.doRebind());
+
+    // We should now have two prefixes from two distinct pools.
+    ASSERT_TRUE(client.hasLeaseForPrefix(IOAddress("2001:db8:3::"), 64));
+    ASSERT_TRUE(client.hasLeaseForPrefix(IOAddress("2001:db8:4::"), 64));
+    //  We should also have two addresses from two distinct pools.
+    ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("3000::45")));
+    ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("3000::11")));
+
+    // This time, options from all pools should have been assigned.
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "3000:1::678"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NIS_SERVERS, "3000:1::789"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NISP_SERVERS, "3000:2::3"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_SNTP_SERVERS, "3000:2::2"));
+}
+
+
 } // end of anonymous namespace

+ 140 - 1
src/bin/dhcp6/tests/renew_unittest.cc

@@ -44,6 +44,15 @@ namespace {
 ///   - 1 subnet with 2001:db8:1::/64 pool
 ///   - DOCSIS vendor config file sub-option
 ///
+/// - Configuration 4:
+///   - single subnet 3000::/32,
+///   - two options specified in the subnet scope,
+///   - one option specified at the global scope,
+///   - two address pools: 3000::10-3000::20, 3000::40-3000::50,
+///   - two prefix pools: 2001:db8:3::/64 and 2001:db8:4::/64,
+///   - an option with unique value specified for each pool, so as it is
+///     possible to test that pool specific options can be assigned.
+///
 const char* RENEW_CONFIGS[] = {
 // Configuration 0
     "{ \"interfaces-config\": {"
@@ -117,8 +126,69 @@ const char* RENEW_CONFIGS[] = {
         "    \"interface-id\": \"\","
         "    \"interface\": \"eth0\""
         " } ],"
-        "\"valid-lifetime\": 4000 }"
+        "\"valid-lifetime\": 4000 }",
 
+// Configuration 4
+    "{ \"interfaces-config\": {"
+        "  \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"option-data\": [ {"
+        "    \"name\": \"dns-servers\","
+        "    \"data\": \"3000:1::234\""
+        "},"
+        "{"
+        "    \"name\": \"sntp-servers\","
+        "    \"data\": \"3000:2::1\""
+        "} ],"
+        "\"subnet6\": [ { "
+        "    \"option-data\": [ {"
+        "        \"name\": \"dns-servers\","
+        "        \"data\": \"3000:1::567\""
+        "    },"
+        "    {"
+        "        \"name\": \"sntp-servers\","
+        "        \"data\": \"3000:2::1\""
+        "    } ],"
+        "    \"pools\": [ { "
+        "        \"pool\": \"3000::10 - 3000::20\","
+        "        \"option-data\": [ {"
+        "            \"name\": \"sntp-servers\","
+        "            \"data\": \"3000:2::2\""
+        "        } ]"
+        "    },"
+        "    {"
+        "        \"pool\": \"3000::40 - 3000::50\","
+        "        \"option-data\": [ {"
+        "            \"name\": \"nisp-servers\","
+        "            \"data\": \"3000:2::3\""
+        "        } ]"
+        "    } ],"
+        "    \"pd-pools\": [ { "
+        "        \"prefix\": \"2001:db8:3::\","
+        "        \"prefix-len\": 64,"
+        "        \"delegated-len\": 64,"
+        "        \"option-data\": [ {"
+        "            \"name\": \"dns-servers\","
+        "            \"data\": \"3000:1::678\""
+        "        } ]"
+        "    },"
+        "    {"
+        "        \"prefix\": \"2001:db8:4::\","
+        "        \"prefix-len\": 64,"
+        "        \"delegated-len\": 64,"
+        "        \"option-data\": [ {"
+        "            \"name\": \"nis-servers\","
+        "            \"data\": \"3000:1::789\""
+        "        } ]"
+        "    } ],"
+        "    \"subnet\": \"3000::/32\", "
+        "    \"interface\": \"eth0\""
+        " } ],"
+        "\"valid-lifetime\": 4000"
+    "}"
 };
 
 /// @brief Test fixture class for testing Renew.
@@ -472,4 +542,73 @@ TEST_F(RenewTest, docsisORO) {
     EXPECT_EQ("normal_erouter_v6.cm", config_file->getValue());
 }
 
+// This test verifies that the same options can be specified on the global
+// level, subnet level and pool level. The options associated with pools
+// are used when the lease is handed out from these pools.
+TEST_F(RenewTest, optionsInheritance) {
+    Dhcp6Client client;
+    // Request a single address and single prefix.
+    ASSERT_NO_THROW(client.requestPrefix(0xabac, 64, IOAddress("2001:db8:4::")));
+    ASSERT_NO_THROW(client.requestAddress(0xabca, IOAddress("3000::45")));
+    // Request two options configured for the pools from which the client may get
+    // a lease.
+    client.requestOption(D6O_NAME_SERVERS);
+    client.requestOption(D6O_NIS_SERVERS);
+    client.requestOption(D6O_NISP_SERVERS);
+    client.requestOption(D6O_SNTP_SERVERS);
+    ASSERT_NO_FATAL_FAILURE(configure(RENEW_CONFIGS[4], *client.getServer()));
+    // Make sure we ended-up having expected number of subnets configured.
+    const Subnet6Collection* subnets = CfgMgr::instance().getCurrentCfg()->
+        getCfgSubnets6()->getAll();
+    ASSERT_EQ(1, subnets->size());
+    // Perform 4-way exchange.
+    ASSERT_NO_THROW(client.doSARR());
+
+    // Simulate aging of leases.
+    client.fastFwdTime(1000);
+
+    // Send Renew message to the server.
+    ASSERT_NO_THROW(client.doRenew());
+
+    // We have provided hints so we should get leases appropriate
+    // for the hints we provided.
+    ASSERT_TRUE(client.hasLeaseForPrefix(IOAddress("2001:db8:4::"), 64));
+    ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("3000::45")));
+    // We shouldn't have leases for the prefix and address which we didn't
+    // request.
+    ASSERT_FALSE(client.hasLeaseForPrefix(IOAddress("2001:db8:3::"), 64));
+    ASSERT_FALSE(client.hasLeaseForAddress(IOAddress("3000::11")));
+
+    // We should have received options associated with a prefix pool and
+    // address pool from which we have requested the leases. We should not
+    // have received options associated with the remaining pools. Instead,
+    // we should have received options associated with a subnet.
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "3000:1::567"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NIS_SERVERS, "3000:1::789"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NISP_SERVERS, "3000:2::3"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_SNTP_SERVERS, "3000:2::1"));
+
+    // Let's now also request a prefix and an address from the remaining pools.
+    ASSERT_NO_THROW(client.requestPrefix(0x6806, 64, IOAddress("2001:db8:3::")));
+    ASSERT_NO_THROW(client.requestAddress(0x6860, IOAddress("3000::11")));
+
+    client.fastFwdTime(1000);
+
+    // Send another Renew.
+    ASSERT_NO_THROW(client.doRenew());
+
+    // We should now have two prefixes from two distinct pools.
+    ASSERT_TRUE(client.hasLeaseForPrefix(IOAddress("2001:db8:3::"), 64));
+    ASSERT_TRUE(client.hasLeaseForPrefix(IOAddress("2001:db8:4::"), 64));
+    //  We should also have two addresses from two distinct pools.
+    ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("3000::45")));
+    ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("3000::11")));
+
+    // This time, options from all pools should have been assigned.
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "3000:1::678"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NIS_SERVERS, "3000:1::789"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NISP_SERVERS, "3000:2::3"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_SNTP_SERVERS, "3000:2::2"));
+}
+
 } // end of anonymous namespace

+ 132 - 0
src/bin/dhcp6/tests/sarr_unittest.cc

@@ -39,6 +39,15 @@ namespace {
 ///     one
 ///   - DNS updates enabled
 ///
+/// - Configuration 2:
+///   - single subnet 3000::/32,
+///   - two options specified in the subnet scope,
+///   - one option specified at the global scope,
+///   - two address pools: 3000::10-3000::20, 3000::40-3000::50,
+///   - two prefix pools: 2001:db8:3::/64 and 2001:db8:4::/64,
+///   - an option with unique value specified for each pool, so as it is
+///     possible to test that pool specific options can be assigned.
+///
 const char* CONFIGS[] = {
     // Configuration 0
     "{ \"interfaces-config\": {"
@@ -82,6 +91,68 @@ const char* CONFIGS[] = {
         " \"dhcp-ddns\" : {"
         "     \"enable-updates\" : True, "
         "     \"qualifying-suffix\" : \"example.com\" }"
+    "}",
+
+// Configuration 2
+    "{ \"interfaces-config\": {"
+        "  \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"option-data\": [ {"
+        "    \"name\": \"dns-servers\","
+        "    \"data\": \"3000:1::234\""
+        "},"
+        "{"
+        "    \"name\": \"sntp-servers\","
+        "    \"data\": \"3000:2::1\""
+        "} ],"
+        "\"subnet6\": [ { "
+        "    \"option-data\": [ {"
+        "        \"name\": \"dns-servers\","
+        "        \"data\": \"3000:1::567\""
+        "    },"
+        "    {"
+        "        \"name\": \"sntp-servers\","
+        "        \"data\": \"3000:2::1\""
+        "    } ],"
+        "    \"pools\": [ { "
+        "        \"pool\": \"3000::10 - 3000::20\","
+        "        \"option-data\": [ {"
+        "            \"name\": \"sntp-servers\","
+        "            \"data\": \"3000:2::2\""
+        "        } ]"
+        "    },"
+        "    {"
+        "        \"pool\": \"3000::40 - 3000::50\","
+        "        \"option-data\": [ {"
+        "            \"name\": \"nisp-servers\","
+        "            \"data\": \"3000:2::3\""
+        "        } ]"
+        "    } ],"
+        "    \"pd-pools\": [ { "
+        "        \"prefix\": \"2001:db8:3::\","
+        "        \"prefix-len\": 64,"
+        "        \"delegated-len\": 64,"
+        "        \"option-data\": [ {"
+        "            \"name\": \"dns-servers\","
+        "            \"data\": \"3000:1::678\""
+        "        } ]"
+        "    },"
+        "    {"
+        "        \"prefix\": \"2001:db8:4::\","
+        "        \"prefix-len\": 64,"
+        "        \"delegated-len\": 64,"
+        "        \"option-data\": [ {"
+        "            \"name\": \"nis-servers\","
+        "            \"data\": \"3000:1::789\""
+        "        } ]"
+        "    } ],"
+        "    \"subnet\": \"3000::/32\", "
+        "    \"interface\": \"eth0\""
+        " } ],"
+        "\"valid-lifetime\": 4000"
     "}"
 };
 
@@ -173,6 +244,67 @@ TEST_F(SARRTest, directClientPrefixHint) {
     ASSERT_TRUE(lease_server);
 }
 
+// This test verifies that the same options can be specified on the global
+// level, subnet level and pool level. The options associated with pools
+// are used when the lease is handed out from these pools.
+TEST_F(SARRTest, optionsInheritance) {
+    Dhcp6Client client;
+    // Request a single address and single prefix.
+    ASSERT_NO_THROW(client.requestPrefix(0xabac, 64, IOAddress("2001:db8:4::")));
+    ASSERT_NO_THROW(client.requestAddress(0xabca, IOAddress("3000::45")));
+    // Request two options configured for the pools from which the client may get
+    // a lease.
+    client.requestOption(D6O_NAME_SERVERS);
+    client.requestOption(D6O_NIS_SERVERS);
+    client.requestOption(D6O_NISP_SERVERS);
+    client.requestOption(D6O_SNTP_SERVERS);
+    ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[2], *client.getServer()));
+    // Make sure we ended-up having expected number of subnets configured.
+    const Subnet6Collection* subnets = CfgMgr::instance().getCurrentCfg()->
+        getCfgSubnets6()->getAll();
+    ASSERT_EQ(1, subnets->size());
+    // Perform 4-way exchange.
+    ASSERT_NO_THROW(client.doSARR());
+
+    // We have provided hints so we should get leases appropriate
+    // for the hints we provided.
+    ASSERT_TRUE(client.hasLeaseForPrefix(IOAddress("2001:db8:4::"), 64));
+    ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("3000::45")));
+    // We shouldn't have leases for the prefix and address which we didn't
+    // request.
+    ASSERT_FALSE(client.hasLeaseForPrefix(IOAddress("2001:db8:3::"), 64));
+    ASSERT_FALSE(client.hasLeaseForAddress(IOAddress("3000::11")));
+
+    // We should have received options associated with a prefix pool and
+    // address pool from which we have requested the leases. We should not
+    // have received options associated with the remaining pools. Instead,
+    // we should have received options associated with a subnet.
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "3000:1::567"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NIS_SERVERS, "3000:1::789"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NISP_SERVERS, "3000:2::3"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_SNTP_SERVERS, "3000:2::1"));
+
+    // Let's now also request a prefix and an address from the remaining pools.
+    ASSERT_NO_THROW(client.requestPrefix(0x6806, 64, IOAddress("2001:db8:3::")));
+    ASSERT_NO_THROW(client.requestAddress(0x6860, IOAddress("3000::11")));
+
+    // Perform 4-way exchange again.
+    ASSERT_NO_THROW(client.doSARR());
+
+    // We should now have two prefixes from two distinct pools.
+    ASSERT_TRUE(client.hasLeaseForPrefix(IOAddress("2001:db8:3::"), 64));
+    ASSERT_TRUE(client.hasLeaseForPrefix(IOAddress("2001:db8:4::"), 64));
+    //  We should also have two addresses from two distinct pools.
+    ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("3000::45")));
+    ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("3000::11")));
+
+    // This time, options from all pools should have been assigned.
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "3000:1::678"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NIS_SERVERS, "3000:1::789"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_NISP_SERVERS, "3000:2::3"));
+    ASSERT_TRUE(client.hasOptionWithAddress(D6O_SNTP_SERVERS, "3000:2::2"));
+}
+
 // Check that when the client includes the Rapid Commit option in its
 // Solicit, the server responds with Reply and commits the lease.
 TEST_F(SARRTest, rapidCommitEnable) {

+ 40 - 19
src/lib/dhcpsrv/parsers/dhcp_parsers.cc

@@ -1039,8 +1039,8 @@ void PoolsListParser::commit() {
 }
 
 //****************************** PoolParser ********************************
-PoolParser::PoolParser(const std::string&,  PoolStoragePtr pools)
-        :pools_(pools) {
+PoolParser::PoolParser(const std::string&, PoolStoragePtr pools)
+        :pools_(pools), options_(new CfgOption()) {
 
     if (!pools_) {
         isc_throw(isc::dhcp::DhcpConfigError, "parser logic error: "
@@ -1067,6 +1067,8 @@ PoolParser::build(ConstElementPtr pool_structure) {
     boost::erase_all(txt, " "); // space
     boost::erase_all(txt, "\t"); // tabulation
 
+    PoolPtr pool;
+
     // Is this prefix/len notation?
     size_t pos = txt.find("/");
     if (pos != string::npos) {
@@ -1093,28 +1095,47 @@ PoolParser::build(ConstElementPtr pool_structure) {
                       << " (" << text_pool->getPosition() << ")");
         }
 
-        PoolPtr pool(poolMaker(addr, len));
+        pool = poolMaker(addr, len);
         local_pools_.push_back(pool);
-        return;
-    }
 
-    // Is this min-max notation?
-    pos = txt.find("-");
-    if (pos != string::npos) {
-        // using min-max notation
-        isc::asiolink::IOAddress min(txt.substr(0,pos));
-        isc::asiolink::IOAddress max(txt.substr(pos + 1));
+    } else {
 
-        PoolPtr pool(poolMaker(min, max));
-        local_pools_.push_back(pool);
-        return;
+        // Is this min-max notation?
+        pos = txt.find("-");
+        if (pos != string::npos) {
+            // using min-max notation
+            isc::asiolink::IOAddress min(txt.substr(0,pos));
+            isc::asiolink::IOAddress max(txt.substr(pos + 1));
+
+            pool = poolMaker(min, max);
+            local_pools_.push_back(pool);
+        }
+    }
+
+    if (!pool) {
+        isc_throw(DhcpConfigError, "invalid pool definition: "
+                  << text_pool->stringValue() <<
+                  ". There are two acceptable formats <min address-max address>"
+                  " or <prefix/len> ("
+                  << text_pool->getPosition() << ")");
     }
 
-    isc_throw(DhcpConfigError, "invalid pool definition: "
-              << text_pool->stringValue() <<
-              ". There are two acceptable formats <min address-max address>"
-              " or <prefix/len> ("
-              << text_pool->getPosition() << ")");
+    // Parser pool specific options.
+    ConstElementPtr option_data = pool_structure->get("option-data");
+    if (option_data) {
+        try {
+            OptionDataListParserPtr option_parser(new OptionDataListParser("option-data",
+                                                                           options_,
+                                                                           AF_INET6));
+            option_parser->build(option_data);
+            option_parser->commit();
+            options_->copyTo(*pool->getCfgOption());;
+
+        } catch (const std::exception& ex) {
+            isc_throw(isc::dhcp::DhcpConfigError, ex.what()
+                      << " (" << option_data->getPosition() << ")");
+        }
+    }
 }
 
 void

+ 3 - 0
src/lib/dhcpsrv/parsers/dhcp_parsers.h

@@ -879,6 +879,9 @@ protected:
     /// A temporary storage for pools configuration. It is a
     /// storage where pools are stored by build function.
     PoolStorage local_pools_;
+
+    /// A storage for pool specific option values.
+    CfgOptionPtr options_;
 };
 
 /// @brief Parser for a list of pools