Browse Source

[3628] Addressed review comments.

One of the major changes was to eliminate possibility of defining the dead
reservation: neither hostname nor IP address reserved and covering it with
suitable unit tests.
Marcin Siodelski 10 years ago
parent
commit
ad0f44ba43

+ 2 - 2
src/bin/dhcp4/dhcp4.spec

@@ -339,13 +339,13 @@
                       {
                         "item_name": "hw-address",
                         "item_type": "string",
-                        "item_optional": false,
+                        "item_optional": true,
                         "item_default": ""
                       },
                       {
                         "item_name": "duid",
                         "item_type": "string",
-                        "item_optional": false,
+                        "item_optional": true,
                         "item_default": ""
                       },
                       {

+ 63 - 4
src/bin/dhcp4/tests/config_parser_unittest.cc

@@ -3245,7 +3245,8 @@ TEST_F(Dhcp4ParserTest, reservations) {
     string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
-        "\"subnet4\": [ { "
+        "\"subnet4\": [ "
+        " { "
         "    \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
         "    \"subnet\": \"192.0.2.0/24\", "
         "    \"id\": 123,"
@@ -3357,14 +3358,15 @@ TEST_F(Dhcp4ParserTest, reservations) {
 }
 
 // This test verfies that the bogus host reservation would trigger a
-// server configuration error. In this case the "hw-address" parameter in the
-// reservation is misspelled.
+// server configuration error.
 TEST_F(Dhcp4ParserTest, reservationBogus) {
+    // Case 1: misspelled hw-address parameter.
     ConstElementPtr x;
     string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
-        "\"subnet4\": [ { "
+        "\"subnet4\": [ "
+        " { "
         "    \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
         "    \"subnet\": \"192.0.4.0/24\","
         "    \"id\": 542,"
@@ -3380,8 +3382,65 @@ TEST_F(Dhcp4ParserTest, reservationBogus) {
 
     ElementPtr json = Element::fromJSON(config);
 
+    CfgMgr::instance().clear();
+
     EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
     checkResult(x, 1);
+
+    // Case 2: DUID and HW Address both specified.
+    config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ "
+        " { "
+        "    \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+        "    \"subnet\": \"192.0.4.0/24\","
+        "    \"id\": 542,"
+        "    \"reservations\": ["
+        "      {"
+        "        \"duid\": \"01:02:03:04:05:06\","
+        "        \"hw-address\": \"06:05:04:03:02:01\","
+        "        \"ip-address\": \"192.0.4.102\","
+        "        \"hostname\": \"\""
+        "      }"
+        "    ]"
+        " } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    json = Element::fromJSON(config);
+
+    // Remove existing configuration, if any.
+    CfgMgr::instance().clear();
+
+    EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
+    checkResult(x, 1);
+
+    // Case 3: Neither ip address nor hostname specified.
+    config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet4\": [ "
+        " { "
+        "    \"pools\": [ { \"pool\": \"192.0.4.101 - 192.0.4.150\" } ],"
+        "    \"subnet\": \"192.0.4.0/24\","
+        "    \"id\": 542,"
+        "    \"reservations\": ["
+        "      {"
+        "        \"hw-address\": \"06:05:04:03:02:01\""
+        "      }"
+        "    ]"
+        " } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    json = Element::fromJSON(config);
+
+    // Remove existing configuration, if any.
+    CfgMgr::instance().clear();
+
+    EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
+    checkResult(x, 1);
+
+
 }
 
 }

+ 2 - 2
src/bin/dhcp6/dhcp6.spec

@@ -377,13 +377,13 @@
                       {
                         "item_name": "hw-address",
                         "item_type": "string",
-                        "item_optional": false,
+                        "item_optional": true,
                         "item_default": ""
                       },
                       {
                         "item_name": "duid",
                         "item_type": "string",
-                        "item_optional": false,
+                        "item_optional": true,
                         "item_default": ""
                       },
                       {

+ 68 - 6
src/bin/dhcp6/tests/config_parser_unittest.cc

@@ -3407,7 +3407,8 @@ TEST_F(Dhcp6ParserTest, reservations) {
     string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
-        "\"subnet6\": [ { "
+        "\"subnet6\": [ "
+        " { "
         "    \"pools\": [ { \"pool\": \"2001:db8:1::/80\" } ],"
         "    \"subnet\": \"2001:db8:1::/64\", "
         "    \"id\": 123,"
@@ -3447,7 +3448,8 @@ TEST_F(Dhcp6ParserTest, reservations) {
         "        \"hostname\": \"\""
         "      }"
         "    ]"
-        " } ],"
+        " } "
+        "], "
         "\"preferred-lifetime\": 3000,"
         "\"valid-lifetime\": 4000 }";
 
@@ -3537,14 +3539,15 @@ TEST_F(Dhcp6ParserTest, reservations) {
 }
 
 // This test verfies that the bogus host reservation would trigger a
-// server configuration error. In this case the "duid" parameter in the
-// reservation is misspelled.
+// server configuration error.
 TEST_F(Dhcp6ParserTest, reservationBogus) {
+    // Case 1: misspelled "duid" parameter.
     ConstElementPtr x;
     string config = "{ \"interfaces\": [ \"*\" ],"
         "\"rebind-timer\": 2000, "
         "\"renew-timer\": 1000, "
-        "\"subnet6\": [ { "
+        "\"subnet6\": [ "
+        " { "
         "    \"pools\": [ ],"
         "    \"subnet\": \"2001:db8:3::/64\", "
         "    \"id\": 542,"
@@ -3555,7 +3558,8 @@ TEST_F(Dhcp6ParserTest, reservationBogus) {
         "        \"hostname\": \"\""
         "      }"
         "    ]"
-        " } ],"
+        " } "
+        "], "
         "\"preferred-lifetime\": 3000,"
         "\"valid-lifetime\": 4000 }";
 
@@ -3563,6 +3567,64 @@ TEST_F(Dhcp6ParserTest, reservationBogus) {
 
     EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
     checkResult(x, 1);
+
+    // Case 2: DUID and HW Address both specified.
+    config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ "
+        " { "
+        "    \"pools\": [ ],"
+        "    \"subnet\": \"2001:db8:3::/64\", "
+        "    \"id\": 542,"
+        "    \"reservations\": ["
+        "      {"
+        "        \"hw-address\": \"01:02:03:04:05:06\","
+        "        \"duid\": \"0A:09:08:07:06:05:04:03:02:01\","
+        "        \"prefixes\": [ \"2001:db8:3:2::/96\" ],"
+        "        \"hostname\": \"\""
+        "      }"
+        "    ]"
+        " } "
+        "], "
+        "\"preferred-lifetime\": 3000,"
+        "\"valid-lifetime\": 4000 }";
+
+    json = Element::fromJSON(config);
+
+    // Remove existing configuration, if any.
+    CfgMgr::instance().clear();
+
+    EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
+    checkResult(x, 1);
+
+    // Case 3: Neither ip address nor hostname specified.
+    config = "{ \"interfaces\": [ \"*\" ],"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ "
+        " { "
+        "    \"pools\": [ ],"
+        "    \"subnet\": \"2001:db8:3::/64\", "
+        "    \"id\": 542,"
+        "    \"reservations\": ["
+        "      {"
+        "        \"duid\": \"0A:09:08:07:06:05:04:03:02:01\""
+        "      }"
+        "    ]"
+        " } "
+        "], "
+        "\"preferred-lifetime\": 3000,"
+        "\"valid-lifetime\": 4000 }";
+
+    json = Element::fromJSON(config);
+
+    // Remove existing configuration, if any.
+    CfgMgr::instance().clear();
+
+    EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
+    checkResult(x, 1);
+
 }
 
 };

+ 22 - 3
src/lib/dhcpsrv/cfg_hosts.cc

@@ -14,6 +14,7 @@
 
 #include <dhcpsrv/cfg_hosts.h>
 #include <exceptions/exceptions.h>
+#include <ostream>
 
 using namespace isc::asiolink;
 
@@ -165,10 +166,26 @@ CfgHosts::add(const HostPtr& host) {
         isc_throw(BadValue, "specified host object must not be NULL when it"
                   " is added to the configuration");
     }
-    /// @todo This may need further sanity checks. For example, a duplicate
-    /// should be rejected.
+
     HWAddrPtr hwaddr = host->getHWAddress();
     DuidPtr duid = host->getDuid();
+
+    // There should be at least one resource reserved: hostname, IPv4
+    // address, IPv6 address or prefix.
+    if (host->getHostname().empty() &&
+        (host->getIPv4Reservation() == IOAddress("0.0.0.0")) &&
+        (!host->hasIPv6Reservation())) {
+        std::ostringstream s;
+        if (hwaddr) {
+            s << "for DUID: " << hwaddr->toText();
+        } else if (duid) {
+            s << "for HW address: " << duid->toText();
+        }
+        isc_throw(BadValue, "specified reservation " << s
+                  << " must include at least one resource, i.e. "
+                  "hostname, IPv4 address or IPv6 address/prefix");
+    }
+
     // Check for duplicates for the specified IPv4 subnet.
     if (get4(host->getIPv4SubnetID(), hwaddr, duid)) {
         isc_throw(DuplicateHost, "failed to add new host using the HW"
@@ -177,7 +194,7 @@ CfgHosts::add(const HostPtr& host) {
                   << "' to the IPv4 subnet id '" << host->getIPv4SubnetID()
                   << "' as this host has already been added");
 
-    // Checek for duplicates for the specified IPv6 subnet.
+    // Check for duplicates for the specified IPv6 subnet.
     } else if (get6(host->getIPv6SubnetID(), duid, hwaddr)) {
         isc_throw(DuplicateHost, "failed to add new host using the HW"
                   " address '" << (hwaddr ? hwaddr->toText(false) : "(null)")
@@ -186,6 +203,8 @@ CfgHosts::add(const HostPtr& host) {
                   << "' as this host has already been added");
     }
 
+    /// @todo This may need further sanity checks.
+
     // This is a new instance - add it.
     hosts_.insert(host);
 }

+ 5 - 0
src/lib/dhcpsrv/host.cc

@@ -195,6 +195,11 @@ Host::getIPv6Reservations(const IPv6Resrv::Type& type) const {
 }
 
 bool
+Host::hasIPv6Reservation() const {
+    return (!ipv6_reservations_.empty());
+}
+
+bool
 Host::hasReservation(const IPv6Resrv& reservation) const {
     IPv6ResrvRange reservations = getIPv6Reservations(reservation.getType());
     if (std::distance(reservations.first, reservations.second) > 0) {

+ 5 - 0
src/lib/dhcpsrv/host.h

@@ -352,6 +352,11 @@ public:
     /// the specified type.
     IPv6ResrvRange getIPv6Reservations(const IPv6Resrv::Type& type) const;
 
+    /// @brief Checks if there is at least one IPv6 reservation for this host.
+    ///
+    /// @return true if there is a reservation for the host, false otherwise.
+    bool hasIPv6Reservation() const;
+
     /// @brief Checks if specified IPv6 reservation exists for the host.
     ///
     /// @param reservation A reservation to be checked for the host.

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

@@ -966,6 +966,9 @@ void
 SubnetConfigParser::build(ConstElementPtr subnet) {
     BOOST_FOREACH(ConfigPair param, subnet->mapValue()) {
         // Host reservations must be parsed after subnet specific parameters.
+        // Note that the reservation parsing will be invoked by the build()
+        // in the derived classes, i.e. Subnet4ConfigParser and
+        // Subnet6ConfigParser.
         if (param.first == "reservations") {
             continue;
         }

+ 1 - 0
src/lib/dhcpsrv/tests/Makefile.am

@@ -70,6 +70,7 @@ libdhcpsrv_unittests_SOURCES += daemon_unittest.cc
 libdhcpsrv_unittests_SOURCES += dbaccess_parser_unittest.cc
 libdhcpsrv_unittests_SOURCES += host_unittest.cc
 libdhcpsrv_unittests_SOURCES += host_reservation_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += host_reservations_list_parser_unittest.cc
 libdhcpsrv_unittests_SOURCES += lease_file_io.cc lease_file_io.h
 libdhcpsrv_unittests_SOURCES += lease_unittest.cc
 libdhcpsrv_unittests_SOURCES += lease_mgr_factory_unittest.cc

+ 14 - 8
src/lib/dhcpsrv/tests/cfg_hosts_unittest.cc

@@ -343,53 +343,59 @@ TEST_F(CfgHostsTest, duplicatesSubnet4DUID) {
 }
 
 // This test verifies that it is not possible to add the same Host to the
-// same IPv4 subnet twice.
+// same IPv6 subnet twice.
 TEST_F(CfgHostsTest, duplicatesSubnet6HWAddr) {
     CfgHosts cfg;
     // Add a host.
     ASSERT_NO_THROW(cfg.add(HostPtr(new Host(hwaddrs_[0]->toText(false),
                                              "hw-address",
                                              SubnetID(10), SubnetID(1),
-                                             IOAddress("0.0.0.0")))));
+                                             IOAddress("0.0.0.0"),
+                                             "foo.example.com"))));
 
     // Try to add the host with the same HW address to the same subnet. The fact
     // that the IP address is different here shouldn't really matter.
     EXPECT_THROW(cfg.add(HostPtr(new Host(hwaddrs_[0]->toText(false),
                                           "hw-address",
                                           SubnetID(11), SubnetID(1),
-                                          IOAddress("0.0.0.0")))),
+                                          IOAddress("0.0.0.0"),
+                                          "foo.example.com"))),
                  isc::dhcp::DuplicateHost);
 
     // Now try to add it to a different subnet. It should go through.
     EXPECT_NO_THROW(cfg.add(HostPtr(new Host(hwaddrs_[0]->toText(false),
                                              "hw-address",
                                              SubnetID(11), SubnetID(2),
-                                             IOAddress("0.0.0.0")))));
+                                             IOAddress("0.0.0.0"),
+                                             "foo.example.com"))));
 }
 
 // This test verifies that it is not possible to add the same Host to the
-// same IPv4 subnet twice.
+// same IPv6 subnet twice.
 TEST_F(CfgHostsTest, duplicatesSubnet6DUID) {
     CfgHosts cfg;
     // Add a host.
     ASSERT_NO_THROW(cfg.add(HostPtr(new Host(duids_[0]->toText(),
                                              "duid",
                                              SubnetID(10), SubnetID(1),
-                                             IOAddress("0.0.0.0")))));
+                                             IOAddress("0.0.0.0"),
+                                             "foo.example.com"))));
 
     // Try to add the host with the same DUID to the same subnet. The fact
     // that the IP address is different here shouldn't really matter.
     EXPECT_THROW(cfg.add(HostPtr(new Host(duids_[0]->toText(),
                                           "duid",
                                           SubnetID(11), SubnetID(1),
-                                          IOAddress("0.0.0.0")))),
+                                          IOAddress("0.0.0.0"),
+                                          "foo.example.com"))),
                  isc::dhcp::DuplicateHost);
 
     // Now try to add it to a different subnet. It should go through.
     EXPECT_NO_THROW(cfg.add(HostPtr(new Host(duids_[0]->toText(),
                                              "duid",
                                              SubnetID(11), SubnetID(2),
-                                             IOAddress("0.0.0.0")))));
+                                             IOAddress("0.0.0.0"),
+                                             "foo.example.com"))));
 }
 
 

+ 109 - 0
src/lib/dhcpsrv/tests/host_reservation_parser_unittest.cc

@@ -141,6 +141,30 @@ TEST_F(HostReservationParserTest, dhcp4DUID) {
     EXPECT_TRUE(hosts[0]->getHostname().empty());
 }
 
+// This test verifies that the parser can parse the reservation entry
+// when IPv4 address is specified, but hostname is not.
+TEST_F(HostReservationParserTest, dhcp4NoHostname) {
+    std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+        "\"ip-address\": \"192.0.2.10\" }";
+
+    ElementPtr config_element = Element::fromJSON(config);
+
+    HostReservationParser4 parser(SubnetID(10));
+    ASSERT_NO_THROW(parser.build(config_element));
+
+    CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+    HostCollection hosts;
+    ASSERT_NO_THROW(hosts = cfg_hosts->getAll(HWAddrPtr(), duid_));
+
+    ASSERT_EQ(1, hosts.size());
+
+    EXPECT_EQ(10, hosts[0]->getIPv4SubnetID());
+    EXPECT_EQ(0, hosts[0]->getIPv6SubnetID());
+    EXPECT_EQ("192.0.2.10", hosts[0]->getIPv4Reservation().toText());
+    EXPECT_TRUE(hosts[0]->getHostname().empty());
+}
+
+
 // This test verifies that the configuration parser for host reservations
 // throws an exception when IPv6 address is specified for IPv4 address
 // reservation.
@@ -166,6 +190,53 @@ TEST_F(HostReservationParserTest, noIdentifier) {
     EXPECT_THROW(parser.build(config_element), DhcpConfigError);
 }
 
+// This test verifies  that the configuration parser for host reservations
+// throws an exception when neither ip address nor hostname is specified.
+TEST_F(HostReservationParserTest, noResource) {
+    std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\" }";
+
+    ElementPtr config_element = Element::fromJSON(config);
+
+    HostReservationParser4 parser(SubnetID(10));
+    EXPECT_THROW(parser.build(config_element), DhcpConfigError);
+}
+
+// This test verifies that the parser can parse the reservation entry
+// when IP address is not specified, but hostname is specified.
+TEST_F(HostReservationParserTest, noIPAddress) {
+    std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+        "\"hostname\": \"foo.example.com\" }";
+
+    ElementPtr config_element = Element::fromJSON(config);
+
+    HostReservationParser4 parser(SubnetID(10));
+    ASSERT_NO_THROW(parser.build(config_element));
+
+    CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+    HostCollection hosts;
+    ASSERT_NO_THROW(hosts = cfg_hosts->getAll(HWAddrPtr(), duid_));
+
+    ASSERT_EQ(1, hosts.size());
+
+    EXPECT_EQ(10, hosts[0]->getIPv4SubnetID());
+    EXPECT_EQ(0, hosts[0]->getIPv6SubnetID());
+    EXPECT_EQ("0.0.0.0", hosts[0]->getIPv4Reservation().toText());
+    EXPECT_EQ("foo.example.com", hosts[0]->getHostname());
+}
+
+// This test verifies  that the configuration parser for host reservations
+// throws an exception when hostname is empty, and IP address is not
+// specified.
+TEST_F(HostReservationParserTest, emptyHostname) {
+    std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+        "\"hostname\": \"\" }";
+
+    ElementPtr config_element = Element::fromJSON(config);
+
+    HostReservationParser4 parser(SubnetID(10));
+    EXPECT_THROW(parser.build(config_element), DhcpConfigError);
+}
+
 // This test verifies that the configuration parser for host reservations
 // throws an exception when invalid IP address is specified.
 TEST_F(HostReservationParserTest, malformedAddress) {
@@ -264,6 +335,44 @@ TEST_F(HostReservationParserTest, dhcp6DUID) {
     ASSERT_EQ(0, std::distance(prefixes.first, prefixes.second));
 }
 
+// This test verfies that the parser can parse the IPv6 reservation entry
+// which lacks hostname parameter.
+TEST_F(HostReservationParserTest, dhcp6NoHostname) {
+    std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+        "\"ip-addresses\": [ \"2001:db8:1::100\", \"2001:db8:1::200\" ],"
+        "\"prefixes\": [ ] }";
+
+    ElementPtr config_element = Element::fromJSON(config);
+
+    HostReservationParser6 parser(SubnetID(12));
+    ASSERT_NO_THROW(parser.build(config_element));
+
+    CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+    HostCollection hosts;
+    ASSERT_NO_THROW(hosts = cfg_hosts->getAll(HWAddrPtr(), duid_));
+
+    ASSERT_EQ(1, hosts.size());
+
+    EXPECT_EQ(0, hosts[0]->getIPv4SubnetID());
+    EXPECT_EQ(12, hosts[0]->getIPv6SubnetID());
+    EXPECT_TRUE(hosts[0]->getHostname().empty());
+
+    IPv6ResrvRange addresses = hosts[0]->
+        getIPv6Reservations(IPv6Resrv::TYPE_NA);
+    ASSERT_EQ(2, std::distance(addresses.first, addresses.second));
+
+    EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_NA,
+                                            IOAddress("2001:db8:1::100")),
+                                  addresses));
+    EXPECT_TRUE(reservationExists(IPv6Resrv(IPv6Resrv::TYPE_NA,
+                                            IOAddress("2001:db8:1::200")),
+                                  addresses));
+
+    IPv6ResrvRange prefixes = hosts[0]->getIPv6Reservations(IPv6Resrv::TYPE_PD);
+    ASSERT_EQ(0, std::distance(prefixes.first, prefixes.second));
+}
+
+
 // This test verifies that the configuration parser throws an exception
 // when IPv4 address is specified for IPv6 reservation.
 TEST_F(HostReservationParserTest, dhcp6IPv4Address) {

+ 178 - 0
src/lib/dhcpsrv/tests/host_reservations_list_parser_unittest.cc

@@ -0,0 +1,178 @@
+// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+
+#include <cc/data.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/cfg_hosts.h>
+#include <dhcpsrv/host.h>
+#include <dhcpsrv/subnet_id.h>
+#include <dhcpsrv/parsers/host_reservation_parser.h>
+#include <dhcpsrv/parsers/host_reservations_list_parser.h>
+#include <gtest/gtest.h>
+
+using namespace isc::data;
+using namespace isc::dhcp;
+
+namespace {
+
+/// @brief Test fixture class for @c HostReservationsListParser.
+class HostReservationsListParserTest : public ::testing::Test {
+protected:
+
+    /// @brief Setup for each test.
+    ///
+    /// Clears the configuration in the @c CfgMgr. It alse initializes
+    /// hwaddr_ and duid_ class members.
+    virtual void SetUp();
+
+    /// @brief Clean up after each test.
+    ///
+    /// Clears the configuration in the @c CfgMgr.
+    virtual void TearDown();
+
+    /// @brief HW Address object used by tests.
+    HWAddrPtr hwaddr_;
+
+    /// @brief DUID object used by tests.
+    DuidPtr duid_;
+};
+
+void
+HostReservationsListParserTest::SetUp() {
+    CfgMgr::instance().clear();
+    // Initialize HW address used by tests.
+    const uint8_t hwaddr_data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 };
+    hwaddr_ = HWAddrPtr(new HWAddr(hwaddr_data, sizeof(hwaddr_data),
+                                   HTYPE_ETHER));
+
+    // Initialize DUID used by tests.
+    const uint8_t duid_data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+                                  0x08, 0x09, 0x0A };
+    duid_ = DuidPtr(new DUID(duid_data, sizeof(duid_data)));
+}
+
+void
+HostReservationsListParserTest::TearDown() {
+    CfgMgr::instance().clear();
+}
+
+// This test verifies that the parser for the list of the host reservations
+// parses IPv4 reservations correctly.
+TEST_F(HostReservationsListParserTest, ipv4Reservations) {
+    std::string config =
+        "[ "
+        "  { "
+        "   \"hw-address\": \"01:02:03:04:05:06\","
+        "   \"ip-address\": \"192.0.2.134\","
+        "   \"hostname\": \"foo.example.com\""
+        "  }, "
+        "  { "
+        "   \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+        "   \"ip-address\": \"192.0.2.110\","
+        "   \"hostname\": \"bar.example.com\""
+        "  } "
+        "]";
+
+    ElementPtr config_element = Element::fromJSON(config);
+
+    HostReservationsListParser<HostReservationParser4> parser(SubnetID(1));
+    ASSERT_NO_THROW(parser.build(config_element));
+
+    CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+    HostCollection hosts;
+
+    // Get the first reservation for the host identified by the HW address.
+    ASSERT_NO_THROW(hosts = cfg_hosts->getAll(hwaddr_));
+    ASSERT_EQ(1, hosts.size());
+
+    EXPECT_EQ(1, hosts[0]->getIPv4SubnetID());
+    EXPECT_EQ(0, hosts[0]->getIPv6SubnetID());
+    EXPECT_EQ("192.0.2.134", hosts[0]->getIPv4Reservation().toText());
+    EXPECT_EQ("foo.example.com", hosts[0]->getHostname());
+
+    // Get the second reservation for the host identified by the DUID.
+    ASSERT_NO_THROW(hosts = cfg_hosts->getAll(HWAddrPtr(), duid_));
+    ASSERT_EQ(1, hosts.size());
+
+    EXPECT_EQ(1, hosts[0]->getIPv4SubnetID());
+    EXPECT_EQ(0, hosts[0]->getIPv6SubnetID());
+    EXPECT_EQ("192.0.2.110", hosts[0]->getIPv4Reservation().toText());
+    EXPECT_EQ("bar.example.com", hosts[0]->getHostname());
+}
+
+// This test verifies that the parser for the list of the host reservations
+// parses IPv6 reservations correctly.
+TEST_F(HostReservationsListParserTest, ipv6Reservations) {
+    std::string config = 
+        "[ "
+        "  { \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+        "    \"ip-addresses\": [ ],"
+        "    \"prefixes\": [ \"2001:db8:1:2::/80\" ],"
+        "    \"hostname\": \"foo.example.com\" "
+        "  }, "
+        "  { \"hw-address\": \"01:02:03:04:05:06\","
+        "    \"ip-addresses\": [ \"2001:db8:1::123\" ],"
+        "    \"prefixes\": [ ],"
+        "    \"hostname\": \"bar.example.com\" "
+        "  } "
+        "]";
+
+    ElementPtr config_element = Element::fromJSON(config);
+
+    // Parse configuration.
+    HostReservationsListParser<HostReservationParser6> parser(SubnetID(2));
+    ASSERT_NO_THROW(parser.build(config_element));
+
+    CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+    HostCollection hosts;
+
+    // Get the reservation for the host identified by the HW address.
+    ASSERT_NO_THROW(hosts = cfg_hosts->getAll(hwaddr_));
+    ASSERT_EQ(1, hosts.size());
+
+    // Make sure it belongs to a valid subnet.
+    EXPECT_EQ(0, hosts[0]->getIPv4SubnetID());
+    EXPECT_EQ(2, hosts[0]->getIPv6SubnetID());
+
+    // Get the reserved addresses for the host. There should be exactly one
+    // address reserved for this host.
+    IPv6ResrvRange prefixes =
+        hosts[0]->getIPv6Reservations(IPv6Resrv::TYPE_NA);
+    ASSERT_EQ(1, std::distance(prefixes.first, prefixes.second));
+
+    EXPECT_EQ(IPv6Resrv::TYPE_NA, prefixes.first->second.getType());
+    EXPECT_EQ("2001:db8:1::123", prefixes.first->second.getPrefix().toText());
+    EXPECT_EQ(128, prefixes.first->second.getPrefixLen());
+
+    // Validate the second reservation.
+    ASSERT_NO_THROW(hosts = cfg_hosts->getAll(HWAddrPtr(), duid_));
+    ASSERT_EQ(1, hosts.size());
+
+    EXPECT_EQ(0, hosts[0]->getIPv4SubnetID());
+    EXPECT_EQ(2, hosts[0]->getIPv6SubnetID());
+
+    // This reservation was for a prefix, instead of an IPv6 address.
+    prefixes = hosts[0]->getIPv6Reservations(IPv6Resrv::TYPE_PD);
+    ASSERT_EQ(1, std::distance(prefixes.first, prefixes.second));
+
+    EXPECT_EQ(IPv6Resrv::TYPE_PD, prefixes.first->second.getType());
+    EXPECT_EQ("2001:db8:1:2::", prefixes.first->second.getPrefix().toText());
+    EXPECT_EQ(80, prefixes.first->second.getPrefixLen());
+}
+
+} // end of anonymous namespace

+ 4 - 0
src/lib/dhcpsrv/tests/host_unittest.cc

@@ -353,6 +353,8 @@ TEST(HostTest, addReservations) {
                                         SubnetID(1), SubnetID(2),
                                         IOAddress("192.0.2.3"))));
 
+    EXPECT_FALSE(host->hasIPv6Reservation());
+
     // Add 4 reservations: 2 for NAs, 2 for PDs.
     ASSERT_NO_THROW(
         host->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
@@ -365,6 +367,8 @@ TEST(HostTest, addReservations) {
                                        IOAddress("2001:db8:1::1")));
     );
 
+    EXPECT_TRUE(host->hasIPv6Reservation());
+
     // Check that reservations exist.
     EXPECT_TRUE(host->hasReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
                                                IOAddress("2001:db8:1::cafe"))));