Browse Source

Merge branch 'master' of ssh://git.kea.isc.org/git/kea

Conflicts:
	ChangeLog
Shawn Routhier 9 years ago
parent
commit
28c4b2575b

+ 6 - 1
ChangeLog

@@ -1,8 +1,13 @@
-1120.	[bug]		sar
+1121.	[bug]		sar
 	Update the classification document to match the output from
 	Update the classification document to match the output from
 	the debug statements.
 	the debug statements.
 	(Trac NA, git TBD)
 	(Trac NA, git TBD)
 
 
+1120.	[func]		marcin
+	Extended MySQL host data source to retrieve DHCPv4 and DHCPv6
+	options associated with hosts from a MySQL database.
+	(Trac #4281, git b8a306a27d1cae03f6bc5223c30806f5cd1b64f4)
+
 1119.	[func]		sar
 1119.	[func]		sar
 	Add debug logging to the classification tokens.  This uses
 	Add debug logging to the classification tokens.  This uses
 	the loggers "kea-dhcp4.eval" and "kea-dhcp6.eval" to capture
 	the loggers "kea-dhcp4.eval" and "kea-dhcp6.eval" to capture

+ 6 - 0
src/bin/admin/tests/mysql_tests.sh.in

@@ -165,6 +165,12 @@ EOF
 EOF
 EOF
     ERRCODE=$?
     ERRCODE=$?
     assert_eq 0 $ERRCODE "host_identifier_type table is missing or broken. (returned status code %d, expected %d)"
     assert_eq 0 $ERRCODE "host_identifier_type table is missing or broken. (returned status code %d, expected %d)"
+    # Seventh table: dhcp_option_scope
+    mysql -u$db_user -p$db_password $db_name >/dev/null 2>&1 <<EOF
+    SELECT scope_id, scope_name FROM dhcp_option_scope;
+EOF
+    ERRCODE=$?
+    assert_eq 0 $ERRCODE "dhcp_option_scope table is missing or broken. (returned status code %d, expected %d)"
 
 
     # Let's wipe the whole database
     # Let's wipe the whole database
     mysql_wipe
     mysql_wipe

+ 91 - 4
src/bin/dhcp4/tests/dora_unittest.cc

@@ -59,8 +59,17 @@ namespace {
 ///     uses HW address for lease lookup, rather than client id
 ///     uses HW address for lease lookup, rather than client id
 ///
 ///
 /// - Configuration 4:
 /// - Configuration 4:
-///   - The same as configuration 2, but using different values in
-///     'host-reservation-identifiers'
+///   - Used for testing host reservations where circuit-id takes precedence
+///     over hw-address, and the hw-address takes precedence over duid.
+///   - 1 subnet: 10.0.0.0/24
+///   - 3 reservations for this subnet:
+///     - IP address 10.0.0.7 for HW address aa:bb:cc:dd:ee:ff
+///     - IP address 10.0.0.8 for DUID 01:02:03:04:05
+///     - IP address 10.0.0.9 for circuit-id 'charter950'
+///
+/// - Configuration 5:
+///   - The same as configuration 4, but using the following order of
+///     host-reservation-identifiers: duid, circuit-id, hw-address.
 ///
 ///
 const char* DORA_CONFIGS[] = {
 const char* DORA_CONFIGS[] = {
 // Configuration 0
 // Configuration 0
@@ -164,7 +173,33 @@ const char* DORA_CONFIGS[] = {
     "{ \"interfaces-config\": {"
     "{ \"interfaces-config\": {"
         "      \"interfaces\": [ \"*\" ]"
         "      \"interfaces\": [ \"*\" ]"
         "},"
         "},"
-        "\"host-reservation-identifiers\": [ \"circuit-id\", \"hw-address\" ],"
+        "\"host-reservation-identifiers\": [ \"circuit-id\", \"hw-address\", \"duid\" ],"
+        "\"valid-lifetime\": 600,"
+        "\"subnet4\": [ { "
+        "    \"subnet\": \"10.0.0.0/24\", "
+        "    \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+        "    \"reservations\": [ "
+        "       {"
+        "         \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+        "         \"ip-address\": \"10.0.0.7\""
+        "       },"
+        "       {"
+        "         \"duid\": \"01:02:03:04:05\","
+        "         \"ip-address\": \"10.0.0.8\""
+        "       },"
+        "       {"
+        "         \"circuit-id\": \"'charter950'\","
+        "         \"ip-address\": \"10.0.0.9\""
+        "       }"
+        "    ]"
+        "} ]"
+    "}",
+
+    // Configuration 5
+    "{ \"interfaces-config\": {"
+        "      \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"host-reservation-identifiers\": [ \"duid\", \"circuit-id\", \"hw-address\" ],"
         "\"valid-lifetime\": 600,"
         "\"valid-lifetime\": 600,"
         "\"subnet4\": [ { "
         "\"subnet4\": [ { "
         "    \"subnet\": \"10.0.0.0/24\", "
         "    \"subnet\": \"10.0.0.0/24\", "
@@ -728,6 +763,38 @@ TEST_F(DORATest, reservation) {
 }
 }
 
 
 // This test checks that it is possible to make a reservation by
 // This test checks that it is possible to make a reservation by
+// DUID carried in the Client Identifier option.
+TEST_F(DORATest, reservationByDUID) {
+    Dhcp4Client client(Dhcp4Client::SELECTING);
+    // Use relay agent so as the circuit-id can be inserted.
+    client.useRelay(true, IOAddress("10.0.0.1"), IOAddress("10.0.0.2"));
+    // Modify HW address so as the server doesn't assign reserved
+    // address by HW address.
+    client.modifyHWAddr();
+    // Specify DUID for which address 10.0.0.8 is reserved.
+    // The value specified as client id includes:
+    // - FF is a client identifier type for DUID,
+    // - 45454545 - represents 4 bytes for IAID
+    // - 01:02:03:04:05 - is an actual DUID for which there is a
+    //   reservation.
+    client.includeClientId("FF:45:45:45:45:01:02:03:04:05");
+
+    // Configure DHCP server.
+    configure(DORA_CONFIGS[2], *client.getServer());
+    // Client A performs 4-way exchange and should obtain a reserved
+    // address.
+    ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+                                  IOAddress>(new IOAddress("0.0.0.0"))));
+    // Make sure that the server responded.
+    ASSERT_TRUE(client.getContext().response_);
+    Pkt4Ptr resp = client.getContext().response_;
+    // Make sure that the server has responded with DHCPACK.
+    ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+    // Make sure that the client has got the lease for the reserved address.
+    ASSERT_EQ("10.0.0.8", client.config_.lease_.addr_.toText());
+}
+
+// This test checks that it is possible to make a reservation by
 // circuit-id inserted by the relay agent.
 // circuit-id inserted by the relay agent.
 TEST_F(DORATest, reservationByCircuitId) {
 TEST_F(DORATest, reservationByCircuitId) {
     Dhcp4Client client(Dhcp4Client::SELECTING);
     Dhcp4Client client(Dhcp4Client::SELECTING);
@@ -758,6 +825,13 @@ TEST_F(DORATest, hostIdentifiersOrder) {
     client.setHWAddress("aa:bb:cc:dd:ee:ff");
     client.setHWAddress("aa:bb:cc:dd:ee:ff");
     // Use relay agent so as the circuit-id can be inserted.
     // Use relay agent so as the circuit-id can be inserted.
     client.useRelay(true, IOAddress("10.0.0.1"), IOAddress("10.0.0.2"));
     client.useRelay(true, IOAddress("10.0.0.1"), IOAddress("10.0.0.2"));
+    // Specify DUID for which address 10.0.0.8 is reserved.
+    // The value specified as client id includes:
+    // - FF is a client identifier type for DUID,
+    // - 45454545 - represents 4 bytes for IAID
+    // - 01:02:03:04:05 - is an actual DUID for which there is a
+    //   reservation.
+    client.includeClientId("FF:45:45:45:45:01:02:03:04:05");
     // Specify circuit-id.
     // Specify circuit-id.
     client.setCircuitId("charter950");
     client.setCircuitId("charter950");
 
 
@@ -778,7 +852,7 @@ TEST_F(DORATest, hostIdentifiersOrder) {
 
 
     // Reconfigure the server to change the preference order of the
     // Reconfigure the server to change the preference order of the
     // host identifiers. The 'circuit-id' should now take precedence over
     // host identifiers. The 'circuit-id' should now take precedence over
-    // the hw-address.
+    // the hw-address and duid.
     configure(DORA_CONFIGS[4], *client.getServer());
     configure(DORA_CONFIGS[4], *client.getServer());
     ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
     ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
                                   IOAddress>(new IOAddress("0.0.0.0"))));
                                   IOAddress>(new IOAddress("0.0.0.0"))));
@@ -790,6 +864,19 @@ TEST_F(DORATest, hostIdentifiersOrder) {
     // Make sure that the client has got the lease for the reserved address.
     // Make sure that the client has got the lease for the reserved address.
     ASSERT_EQ("10.0.0.9", client.config_.lease_.addr_.toText());
     ASSERT_EQ("10.0.0.9", client.config_.lease_.addr_.toText());
 
 
+    // Reconfigure the server to change the preference order of the
+    // host identifiers. The 'duid' should now take precedence over
+    // the hw-address and circuit-id
+    configure(DORA_CONFIGS[5], *client.getServer());
+    ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
+                                  IOAddress>(new IOAddress("0.0.0.0"))));
+    // Make sure that the server responded.
+    ASSERT_TRUE(client.getContext().response_);
+    resp = client.getContext().response_;
+    // Make sure that the server has responded with DHCPACK.
+    ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+    // Make sure that the client has got the lease for the reserved address.
+    ASSERT_EQ("10.0.0.8", client.config_.lease_.addr_.toText());
 }
 }
 
 
 // This test checks that setting the match-client-id value to false causes
 // This test checks that setting the match-client-id value to false causes

+ 20 - 0
src/lib/cc/tests/data_unittests.cc

@@ -561,6 +561,26 @@ TEST(Element, escape) {
     EXPECT_NO_THROW(Element::fromJSON("\"  \n  \r \t \f  \n \n    \t\""));
     EXPECT_NO_THROW(Element::fromJSON("\"  \n  \r \t \f  \n \n    \t\""));
 }
 }
 
 
+// This test verifies that a backslash can be used in element content
+// when the element is created using constructor.
+TEST(Element, backslash1) {
+    string input = "SMSBoot\\x64";// One slash passed to elem constructor...
+    string exp = "SMSBoot\\x64";  // ... should result in one slash in the actual option.
+
+    StringElement elem(input);
+    EXPECT_EQ(exp, elem.stringValue());
+}
+
+// This test verifies that a backslash can be used in element content
+// when the element is created using fromJSON.
+TEST(Element, backslash2) {
+    string input = "\"SMSBoot\\\\x64\""; // Two slashes put in the config file...
+    string exp = "SMSBoot\\x64"; // ... should result in one slash in the actual option.
+
+    ElementPtr elem = Element::fromJSON(input);
+    EXPECT_EQ(exp, elem->stringValue());
+}
+
 TEST(Element, ListElement) {
 TEST(Element, ListElement) {
     // this function checks the specific functions for ListElements
     // this function checks the specific functions for ListElements
     ElementPtr el = Element::fromJSON("[ 1, \"bar\", 3 ]");
     ElementPtr el = Element::fromJSON("[ 1, \"bar\", 3 ]");

+ 29 - 1
src/lib/dhcp/libdhcp++.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2016 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
@@ -21,9 +21,11 @@
 #include <util/buffer.h>
 #include <util/buffer.h>
 #include <dhcp/option_definition.h>
 #include <dhcp/option_definition.h>
 
 
+#include <boost/lexical_cast.hpp>
 #include <boost/shared_array.hpp>
 #include <boost/shared_array.hpp>
 #include <boost/shared_ptr.hpp>
 #include <boost/shared_ptr.hpp>
 
 
+#include <limits>
 #include <list>
 #include <list>
 
 
 using namespace std;
 using namespace std;
@@ -844,6 +846,32 @@ LibDHCP::initVendorOptsIsc6() {
     initOptionSpace(vendor6_defs_[ENTERPRISE_ID_ISC], ISC_V6_DEFS, ISC_V6_DEFS_SIZE);
     initOptionSpace(vendor6_defs_[ENTERPRISE_ID_ISC], ISC_V6_DEFS, ISC_V6_DEFS_SIZE);
 }
 }
 
 
+uint32_t
+LibDHCP::optionSpaceToVendorId(const std::string& option_space) {
+    // 8 is a minimal length of "vendor-X" format
+    if ((option_space.size() < 8) || (option_space.substr(0,7) != "vendor-")) {
+        return (0);
+    }
+
+    int64_t check;
+    try {
+        // text after "vendor-", supposedly numbers only
+        std::string x = option_space.substr(7);
+
+        check = boost::lexical_cast<int64_t>(x);
+
+    } catch (const boost::bad_lexical_cast &) {
+        return (0);
+    }
+
+    if ((check < 0) || (check > std::numeric_limits<uint32_t>::max())) {
+        return (0);
+    }
+
+    // value is small enough to fit
+    return (static_cast<uint32_t>(check));
+}
+
 void initOptionSpace(OptionDefContainer& defs,
 void initOptionSpace(OptionDefContainer& defs,
                      const OptionDefParams* params,
                      const OptionDefParams* params,
                      size_t params_size) {
                      size_t params_size) {

+ 17 - 1
src/lib/dhcp/libdhcp++.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2016 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
@@ -14,6 +14,7 @@
 #include <util/staged_value.h>
 #include <util/staged_value.h>
 
 
 #include <iostream>
 #include <iostream>
+#include <stdint.h>
 #include <string>
 #include <string>
 
 
 namespace isc {
 namespace isc {
@@ -321,6 +322,21 @@ public:
     /// @brief Commits runtime option definitions.
     /// @brief Commits runtime option definitions.
     static void commitRuntimeOptionDefs();
     static void commitRuntimeOptionDefs();
 
 
+    /// @brief Converts option space name to vendor id.
+    ///
+    /// If the option space name is specified in the following format:
+    /// "vendor-X" where X is an uint32_t number, it is assumed to be
+    /// a vendor space and the uint32_t number is returned by this function.
+    /// If the option space name is invalid this method will return 0, which
+    /// is not a valid vendor-id, to signal an error.
+    ///
+    /// @todo remove this function once when the conversion is dealt by the
+    /// appropriate functions returning options by option space names.
+    ///
+    /// @param option_space Option space name.
+    /// @return vendor id.
+    static uint32_t optionSpaceToVendorId(const std::string& option_space);
+
 private:
 private:
 
 
     /// Initialize standard DHCPv4 option definitions.
     /// Initialize standard DHCPv4 option definitions.

+ 1 - 0
src/lib/dhcpsrv/base_host_data_source.h

@@ -105,6 +105,7 @@ public:
     /// for a specified identifier. This method may return multiple hosts
     /// for a specified identifier. This method may return multiple hosts
     /// because a particular client may have reservations in multiple subnets.
     /// because a particular client may have reservations in multiple subnets.
     ///
     ///
+    /// @param identifier_type Identifier type.
     /// @param identifier_begin Pointer to a begining of a buffer containing
     /// @param identifier_begin Pointer to a begining of a buffer containing
     /// an identifier.
     /// an identifier.
     /// @param identifier_len Identifier length.
     /// @param identifier_len Identifier length.

+ 1 - 1
src/lib/dhcpsrv/cfg_host_operations.h

@@ -63,7 +63,7 @@ public:
     /// @brief Adds new identifier type to a collection of identifiers
     /// @brief Adds new identifier type to a collection of identifiers
     /// to be used by the server to search for host reservations.
     /// to be used by the server to search for host reservations.
     ///
     ///
-    /// @param identifier_type Name of the identifier to be added. It
+    /// @param identifier_name Name of the identifier to be added. It
     /// must be one of the names supported by the @ref Host::getIdentifierType
     /// must be one of the names supported by the @ref Host::getIdentifierType
     /// function.
     /// function.
     void addIdentifierType(const std::string& identifier_name);
     void addIdentifierType(const std::string& identifier_name);

+ 27 - 47
src/lib/dhcpsrv/cfg_option.cc

@@ -1,14 +1,13 @@
-// Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2016 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
 // 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 <dhcp/libdhcp++.h>
 #include <dhcp/option_space.h>
 #include <dhcp/option_space.h>
 #include <dhcpsrv/cfg_option.h>
 #include <dhcpsrv/cfg_option.h>
-#include <boost/lexical_cast.hpp>
 #include <dhcp/dhcp6.h>
 #include <dhcp/dhcp6.h>
-#include <limits>
 #include <string>
 #include <string>
 
 
 namespace isc {
 namespace isc {
@@ -17,6 +16,7 @@ namespace dhcp {
 bool
 bool
 OptionDescriptor::equals(const OptionDescriptor& other) const {
 OptionDescriptor::equals(const OptionDescriptor& other) const {
     return (persistent_ == other.persistent_ &&
     return (persistent_ == other.persistent_ &&
+            formatted_value_ == other.formatted_value_ &&
             option_->equals(other.option_));
             option_->equals(other.option_));
 }
 }
 
 
@@ -37,7 +37,12 @@ CfgOption::equals(const CfgOption& other) const {
 void
 void
 CfgOption::add(const OptionPtr& option, const bool persistent,
 CfgOption::add(const OptionPtr& option, const bool persistent,
                const std::string& option_space) {
                const std::string& option_space) {
-    if (!option) {
+    add(OptionDescriptor(option, persistent), option_space);
+}
+
+void
+CfgOption::add(const OptionDescriptor& desc, const std::string& option_space) {
+    if (!desc.option_) {
         isc_throw(isc::BadValue, "option being configured must not be NULL");
         isc_throw(isc::BadValue, "option being configured must not be NULL");
 
 
     } else  if (!OptionSpace::validateName(option_space)) {
     } else  if (!OptionSpace::validateName(option_space)) {
@@ -45,13 +50,27 @@ CfgOption::add(const OptionPtr& option, const bool persistent,
                   << option_space << "'");
                   << option_space << "'");
     }
     }
 
 
-    const uint32_t vendor_id = optionSpaceToVendorId(option_space);
+    const uint32_t vendor_id = LibDHCP::optionSpaceToVendorId(option_space);
     if (vendor_id) {
     if (vendor_id) {
-        vendor_options_.addItem(OptionDescriptor(option, persistent),
-                                vendor_id);
+        vendor_options_.addItem(desc, vendor_id);
     } else {
     } else {
-        options_.addItem(OptionDescriptor(option, persistent), option_space);
+        options_.addItem(desc, option_space);
+    }
+}
+
+std::list<std::string>
+CfgOption::getVendorIdsSpaceNames() const {
+    std::list<uint32_t> ids = getVendorIds();
+    std::list<std::string> names;
+    for (std::list<uint32_t>::const_iterator id = ids.begin();
+         id != ids.end(); ++id) {
+        std::ostringstream s;
+        // Vendor space name is constructed as "vendor-XYZ" where XYZ is an
+        // uint32_t value, without leading zeros.
+        s << "vendor-" << *id;
+        names.push_back(s.str());
     }
     }
+    return (names);
 }
 }
 
 
 void
 void
@@ -152,44 +171,5 @@ CfgOption::getAll(const uint32_t vendor_id) const {
     return (vendor_options_.getItems(vendor_id));
     return (vendor_options_.getItems(vendor_id));
 }
 }
 
 
-uint32_t
-CfgOption::optionSpaceToVendorId(const std::string& option_space) {
-    if (option_space.size() < 8) {
-        // 8 is a minimal length of "vendor-X" format
-        return (0);
-    }
-    if (option_space.substr(0,7) != "vendor-") {
-        return (0);
-    }
-
-    // text after "vendor-", supposedly numbers only
-    std::string x = option_space.substr(7);
-
-    int64_t check;
-    try {
-        check = boost::lexical_cast<int64_t>(x);
-    } catch (const boost::bad_lexical_cast &) {
-        /// @todo: Should we throw here?
-        // isc_throw(BadValue, "Failed to parse vendor-X value (" << x
-        //           << ") as unsigned 32-bit integer.");
-        return (0);
-    }
-    if (check > std::numeric_limits<uint32_t>::max()) {
-        /// @todo: Should we throw here?
-        //isc_throw(BadValue, "Value " << x << "is too large"
-        //          << " for unsigned 32-bit integer.");
-        return (0);
-    }
-    if (check < 0) {
-        /// @todo: Should we throw here?
-        // isc_throw(BadValue, "Value " << x << "is negative."
-        //       << " Only 0 or larger are allowed for unsigned 32-bit integer.");
-        return (0);
-    }
-
-    // value is small enough to fit
-    return (static_cast<uint32_t>(check));
-}
-
 } // end of namespace isc::dhcp
 } // end of namespace isc::dhcp
 } // end of namespace isc
 } // end of namespace isc

+ 61 - 19
src/lib/dhcpsrv/cfg_option.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2016 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
@@ -18,7 +18,6 @@
 #include <boost/shared_ptr.hpp>
 #include <boost/shared_ptr.hpp>
 #include <stdint.h>
 #include <stdint.h>
 #include <string>
 #include <string>
-#include <set>
 #include <list>
 #include <list>
 
 
 namespace isc {
 namespace isc {
@@ -31,24 +30,47 @@ namespace dhcp {
 /// to DHCP client only on request (persistent = false) or always
 /// to DHCP client only on request (persistent = false) or always
 /// (persistent = true).
 /// (persistent = true).
 struct OptionDescriptor {
 struct OptionDescriptor {
-    /// Option instance.
+    /// @brief Option instance.
     OptionPtr option_;
     OptionPtr option_;
-    /// Persistent flag, if true option is always sent to the client,
-    /// if false option is sent to the client on request.
+
+    /// @brief Persistence flag.
+    ///
+    /// If true, option is always sent to the client. If false, option is
+    /// sent to the client when requested using ORO or PRL option.
     bool persistent_;
     bool persistent_;
 
 
+    /// @brief Option value in textual (CSV) format.
+    ///
+    /// This field is used to convey option value in human readable format,
+    /// the same as used to specify option value in the server configuration.
+    /// This value is optional and can be held in the host reservations
+    /// database instead of the binary format.
+    ///
+    /// Note that this value is carried in the option descriptor, rather than
+    /// @c Option instance because it is a server specific value (same as
+    /// persistence flag).
+    ///
+    /// An example of the formatted value is: "2001:db8:1::1, 23, some text"
+    /// for the option which carries IPv6 address, a number and a text.
+    std::string formatted_value_;
+
     /// @brief Constructor.
     /// @brief Constructor.
     ///
     ///
     /// @param opt option
     /// @param opt option
     /// @param persist if true option is always sent.
     /// @param persist if true option is always sent.
-    OptionDescriptor(const OptionPtr& opt, bool persist)
-        : option_(opt), persistent_(persist) {};
+    /// @param formatted_value option value in the textual format. Default
+    /// value is empty indicating that the value is not set.
+    OptionDescriptor(const OptionPtr& opt, bool persist,
+                     const std::string& formatted_value = "")
+        : option_(opt), persistent_(persist),
+          formatted_value_(formatted_value) {};
 
 
     /// @brief Constructor
     /// @brief Constructor
     ///
     ///
     /// @param persist if true option is always sent.
     /// @param persist if true option is always sent.
     OptionDescriptor(bool persist)
     OptionDescriptor(bool persist)
-        : option_(OptionPtr()), persistent_(persist) {};
+        : option_(OptionPtr()), persistent_(persist),
+          formatted_value_() {};
 
 
     /// @brief Checks if the one descriptor is equal to another.
     /// @brief Checks if the one descriptor is equal to another.
     ///
     ///
@@ -254,6 +276,16 @@ public:
     void add(const OptionPtr& option, const bool persistent,
     void add(const OptionPtr& option, const bool persistent,
              const std::string& option_space);
              const std::string& option_space);
 
 
+    /// @brief A variant of the @ref CfgOption::add method which takes option
+    /// descriptor as an argument.
+    ///
+    /// @param desc Option descriptor holding option instance and other
+    /// parameters pertaining to the option.
+    /// @param option_space Option space name.
+    ///
+    /// @throw isc::BadValue if the option space is invalid.
+    void add(const OptionDescriptor& desc, const std::string& option_space);
+
     /// @brief Merges this configuration to another configuration.
     /// @brief Merges this configuration to another configuration.
     ///
     ///
     /// This method iterates over the configuration items held in this
     /// This method iterates over the configuration items held in this
@@ -337,20 +369,30 @@ public:
         return (*od_itr);
         return (*od_itr);
     }
     }
 
 
-    /// @brief Converts option space name to vendor id.
+    /// @brief Returns a list of configured option space names.
     ///
     ///
-    /// If the option space name is specified in the following format:
-    /// "vendor-X" where X is an uint32_t number, it is assumed to be
-    /// a vendor space and the uint32_t number is returned by this function.
-    /// If the option space name is invalid this method will return 0, which
-    /// is not a valid vendor-id, to signal an error.
+    /// The returned option space names exclude vendor option spaces,
+    /// such as "vendor-1234". These are returned by the
+    /// @ref getVendorIdsSpaceNames.
+    ///
+    /// @return List comprising option space names.
+    std::list<std::string> getOptionSpaceNames() const {
+        return (options_.getOptionSpaceNames());
+    }
+
+    /// @brief Returns a list of all configured  vendor identifiers.
+    std::list<uint32_t> getVendorIds() const {
+        return (vendor_options_.getOptionSpaceNames());
+    }
+
+    /// @brief Returns a list of option space names for configured vendor ids.
     ///
     ///
-    /// @todo remove this function once when the conversion is dealt by the
-    /// appropriate functions returning options by option space names.
+    /// For each vendor-id the option space name returned is constructed
+    /// as "vendor-XYZ" where XYZ is a @c uint32_t value without leading
+    /// zeros.
     ///
     ///
-    /// @param option_space Option space name.
-    /// @return vendor id.
-    static uint32_t optionSpaceToVendorId(const std::string& option_space);
+    /// @return List comprising option space names for vendor options.
+    std::list<std::string> getVendorIdsSpaceNames() const;
 
 
 private:
 private:
 
 

+ 9 - 0
src/lib/dhcpsrv/dhcpsrv_messages.mes

@@ -539,6 +539,15 @@ information from the MySQL hosts database.
 The code has issued a rollback call.  All outstanding transaction will
 The code has issued a rollback call.  All outstanding transaction will
 be rolled back and not committed to the database.
 be rolled back and not committed to the database.
 
 
+% DHCPSRV_MYSQL_START_TRANSACTION starting new MySQL transaction
+A debug message issued whena new MySQL transaction is being started.
+This message is typically not issued when inserting data into a
+single table because the server doesn't explicitly start
+transactions in this case. This message is issued when data is
+inserted into multiple tables with multiple INSERT statements
+and there may be a need to rollback the whole transaction if
+any of these INSERT statements fail.
+
 % DHCPSRV_MYSQL_UPDATE_ADDR4 updating IPv4 lease for address %1
 % DHCPSRV_MYSQL_UPDATE_ADDR4 updating IPv4 lease for address %1
 A debug message issued when the server is attempting to update IPv4
 A debug message issued when the server is attempting to update IPv4
 lease from the MySQL database for the specified address.
 lease from the MySQL database for the specified address.

+ 1 - 1
src/lib/dhcpsrv/host.cc

@@ -81,7 +81,7 @@ Host::Host(const uint8_t* identifier, const size_t identifier_len,
       ipv4_reservation_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
       ipv4_reservation_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
       hostname_(hostname), dhcp4_client_classes_(dhcp4_client_classes),
       hostname_(hostname), dhcp4_client_classes_(dhcp4_client_classes),
       dhcp6_client_classes_(dhcp6_client_classes), host_id_(0),
       dhcp6_client_classes_(dhcp6_client_classes), host_id_(0),
-      cfg_option4_(), cfg_option6_() {
+      cfg_option4_(new CfgOption()), cfg_option6_(new CfgOption()) {
 
 
     // Initialize host identifier.
     // Initialize host identifier.
     setIdentifier(identifier, identifier_len, identifier_type);
     setIdentifier(identifier, identifier_len, identifier_type);

+ 2 - 2
src/lib/dhcpsrv/host.h

@@ -317,7 +317,7 @@ public:
 
 
     /// @brief Returns host identifier in a textual form.
     /// @brief Returns host identifier in a textual form.
     ///
     ///
-    /// @return Identifier in the form of <type>=<value>.
+    /// @return Identifier in the form of type=value.
     std::string getIdentifierAsText() const;
     std::string getIdentifierAsText() const;
 
 
     /// @brief Returns name of the identifier of a specified type.
     /// @brief Returns name of the identifier of a specified type.
@@ -328,7 +328,7 @@ public:
     /// @param type Identifier type.
     /// @param type Identifier type.
     /// @param value Pointer to a buffer holding identifier.
     /// @param value Pointer to a buffer holding identifier.
     /// @param length Length of the identifier.
     /// @param length Length of the identifier.
-    /// @return Identifier in the form of <type>=<value>.
+    /// @return Identifier in the form of type=value.
     static std::string getIdentifierAsText(const IdentifierType& type,
     static std::string getIdentifierAsText(const IdentifierType& type,
                                            const uint8_t* value,
                                            const uint8_t* value,
                                            const size_t length);
                                            const size_t length);

+ 1 - 0
src/lib/dhcpsrv/host_mgr.h

@@ -116,6 +116,7 @@ public:
     /// reservations from the primary data source are placed before the
     /// reservations from the primary data source are placed before the
     /// reservations from the alternate source.
     /// reservations from the alternate source.
     ///
     ///
+    /// @param identifier_type Identifier type.
     /// @param identifier_begin Pointer to a begining of a buffer containing
     /// @param identifier_begin Pointer to a begining of a buffer containing
     /// an identifier.
     /// an identifier.
     /// @param identifier_len Identifier length.
     /// @param identifier_len Identifier length.

+ 47 - 12
src/lib/dhcpsrv/mysql_connection.cc

@@ -31,6 +31,26 @@ const int MLM_MYSQL_FETCH_FAILURE = 1;
 
 
 const int MYSQL_DEFAULT_CONNECTION_TIMEOUT = 5;	// seconds
 const int MYSQL_DEFAULT_CONNECTION_TIMEOUT = 5;	// seconds
 
 
+MySqlTransaction::MySqlTransaction(MySqlConnection& conn)
+    : conn_(conn), committed_(false) {
+    conn_.startTransaction();
+}
+
+MySqlTransaction::~MySqlTransaction() {
+    // Rollback if the MySqlTransaction::commit wasn't explicitly
+    // called.
+    if (!committed_) {
+        conn_.rollback();
+    }
+}
+
+void
+MySqlTransaction::commit() {
+    conn_.commit();
+    committed_ = true;
+}
+
+
 // Open the database using the parameters passed to the constructor.
 // Open the database using the parameters passed to the constructor.
 
 
 void
 void
@@ -306,20 +326,35 @@ MySqlConnection::convertFromDatabaseTime(const MYSQL_TIME& expire,
     cltt = mktime(&expire_tm) - valid_lifetime;
     cltt = mktime(&expire_tm) - valid_lifetime;
 }
 }
 
 
-void MySqlConnection::commit() {
-        LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_MYSQL_COMMIT);
-        if (mysql_commit(mysql_) != 0) {
-                isc_throw(DbOperationError, "commit failed: "
-                        << mysql_error(mysql_));
-        }
+void
+MySqlConnection::startTransaction() {
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
+              DHCPSRV_MYSQL_START_TRANSACTION);
+    // We create prepared statements for all other queries, but MySQL
+    // don't support prepared statements for START TRANSACTION.
+    int status = mysql_query(mysql_, "START TRANSACTION");
+    if (status != 0) {
+        isc_throw(DbOperationError, "unable to start transaction, "
+                  "reason: " << mysql_error(mysql_));
+    }
 }
 }
 
 
-void MySqlConnection::rollback() {
-        LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_MYSQL_ROLLBACK);
-        if (mysql_rollback(mysql_) != 0) {
-                isc_throw(DbOperationError, "rollback failed: "
-                        << mysql_error(mysql_));
-        }
+void
+MySqlConnection::commit() {
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_MYSQL_COMMIT);
+    if (mysql_commit(mysql_) != 0) {
+        isc_throw(DbOperationError, "commit failed: "
+                  << mysql_error(mysql_));
+    }
+}
+
+void
+MySqlConnection::rollback() {
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_MYSQL_ROLLBACK);
+    if (mysql_rollback(mysql_) != 0) {
+        isc_throw(DbOperationError, "rollback failed: "
+                  << mysql_error(mysql_));
+    }
 }
 }
 
 
 
 

+ 127 - 2
src/lib/dhcpsrv/mysql_connection.h

@@ -8,8 +8,12 @@
 #define MYSQL_CONNECTION_H
 #define MYSQL_CONNECTION_H
 
 
 #include <dhcpsrv/database_connection.h>
 #include <dhcpsrv/database_connection.h>
+#include <dhcpsrv/dhcpsrv_log.h>
+#include <exceptions/exceptions.h>
 #include <boost/scoped_ptr.hpp>
 #include <boost/scoped_ptr.hpp>
 #include <mysql.h>
 #include <mysql.h>
+#include <mysqld_error.h>
+#include <errmsg.h>
 #include <vector>
 #include <vector>
 #include <stdint.h>
 #include <stdint.h>
 
 
@@ -140,6 +144,63 @@ private:
     MYSQL* mysql_;      ///< Initialization context
     MYSQL* mysql_;      ///< Initialization context
 };
 };
 
 
+/// @brief Forward declaration to @ref MySqlConnection.
+class MySqlConnection;
+
+/// @brief RAII object representing MySQL transaction.
+///
+/// An instance of this class should be created in a scope where multiple
+/// INSERT statements should be executed within a single transaction. The
+/// transaction is started when the constructor of this class is invoked.
+/// The transaction is ended when the @ref MySqlTransaction::commit is
+/// explicitly called or when the instance of this class is destroyed.
+/// The @ref MySqlTransaction::commit commits changes to the database
+/// and the changes remain in the database when the instance of the
+/// class is destroyed. If the class instance is destroyed before the
+/// @ref MySqlTransaction::commit is called, the transaction is rolled
+/// back. The rollback on destruction guarantees that partial data is
+/// not stored in the database when there is an error during any
+/// of the operations belonging to a transaction.
+///
+/// The default MySQL backend configuration enables 'autocommit'.
+/// Starting a transaction overrides 'autocommit' setting for this
+/// particular transaction only. It does not affect the global 'autocommit'
+/// setting for the database connection, i.e. all modifications to the
+/// database which don't use transactions will still be auto committed.
+class MySqlTransaction : public boost::noncopyable {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Starts transaction by making a "START TRANSACTION" query.
+    ///
+    /// @param conn MySQL connection to use for the transaction. This
+    /// connection will be later used to commit or rollback changes.
+    ///
+    /// @throw DbOperationError if "START TRANSACTION" query fails.
+    MySqlTransaction(MySqlConnection& conn);
+
+    /// @brief Destructor.
+    ///
+    /// Rolls back the transaction if changes haven't been committed.
+    ~MySqlTransaction();
+
+    /// @brief Commits transaction.
+    void commit();
+
+private:
+
+    /// @brief Holds reference to the MySQL database connection.
+    MySqlConnection& conn_;
+
+    /// @brief Boolean flag indicating if the transaction has been committed.
+    ///
+    /// This flag is used in the class destructor to assess if the
+    /// transaction should be rolled back.
+    bool committed_;
+};
+
+
 /// @brief Common MySQL Connector Pool
 /// @brief Common MySQL Connector Pool
 ///
 ///
 /// This class provides common operations for MySQL database connection
 /// This class provides common operations for MySQL database connection
@@ -259,6 +320,9 @@ public:
             uint32_t valid_lifetime, time_t& cltt);
             uint32_t valid_lifetime, time_t& cltt);
     ///@}
     ///@}
 
 
+    /// @brief Starts Transaction
+    void startTransaction();
+
     /// @brief Commit Transactions
     /// @brief Commit Transactions
     ///
     ///
     /// Commits all pending database operations. On databases that don't
     /// Commits all pending database operations. On databases that don't
@@ -275,6 +339,69 @@ public:
     /// @throw DbOperationError If the rollback failed.
     /// @throw DbOperationError If the rollback failed.
     void rollback();
     void rollback();
 
 
+    /// @brief Check Error and Throw Exception
+    ///
+    /// Virtually all MySQL functions return a status which, if non-zero,
+    /// indicates an error.  This function centralizes the error checking
+    /// code.
+    ///
+    /// It is used to determine whether or not the function succeeded, and
+    /// in the event of failures, decide whether or not those failures are
+    /// recoverable.
+    ///
+    /// If the error is recoverable, the method will throw a DbOperationError.
+    /// In the error is deemed unrecoverable, such as a loss of connectivity
+    /// with the server, this method will log the error and call exit(-1);
+    ///
+    /// @todo Calling exit() is viewed as a short term solution for Kea 1.0.
+    /// Two tickets are likely to alter this behavior, first is #3639, which
+    /// calls for the ability to attempt to reconnect to the database. The
+    /// second ticket, #4087 which calls for the implementation of a generic,
+    /// FatalException class which will propagate outward.
+    ///
+    /// @param status Status code: non-zero implies an error
+    /// @param index Index of statement that caused the error
+    /// @param what High-level description of the error
+    ///
+    /// @tparam Enumeration representing index of a statement to which an
+    /// error pertains.
+    ///
+    /// @throw isc::dhcp::DbOperationError An operation on the open database has
+    ///        failed.
+    template<typename StatementIndex>
+    void checkError(const int status, const StatementIndex& index,
+                    const char* what) const {
+        if (status != 0) {
+            switch(mysql_errno(mysql_)) {
+                // These are the ones we consider fatal. Remember this method is
+                // used to check errors of API calls made subsequent to successfully
+                // connecting.  Errors occuring while attempting to connect are
+                // checked in the connection code. An alternative would be to call
+                // mysql_ping() - assuming autoreconnect is off. If that fails
+                // then we know connection is toast.
+            case CR_SERVER_GONE_ERROR:
+            case CR_SERVER_LOST:
+            case CR_OUT_OF_MEMORY:
+            case CR_CONNECTION_ERROR:
+                // We're exiting on fatal
+                LOG_ERROR(dhcpsrv_logger, DHCPSRV_MYSQL_FATAL_ERROR)
+                         .arg(what)
+                         .arg(text_statements_[static_cast<int>(index)])
+                         .arg(mysql_error(mysql_))
+                         .arg(mysql_errno(mysql_));
+                exit (-1);
+
+            default:
+                // Connection is ok, so it must be an SQL error
+                isc_throw(DbOperationError, what << " for <"
+                          << text_statements_[static_cast<int>(index)]
+                          << ">, reason: "
+                          << mysql_error(mysql_) << " (error code "
+                          << mysql_errno(mysql_) << ")");
+            }
+        }
+    }
+
     /// @brief Prepared statements
     /// @brief Prepared statements
     ///
     ///
     /// This field is public, because it is used heavily from MySqlConnection
     /// This field is public, because it is used heavily from MySqlConnection
@@ -294,8 +421,6 @@ public:
     MySqlHolder mysql_;
     MySqlHolder mysql_;
 };
 };
 
 
-
-
 }; // end of isc::dhcp namespace
 }; // end of isc::dhcp namespace
 }; // end of isc namespace
 }; // end of isc namespace
 
 

File diff suppressed because it is too large
+ 1148 - 286
src/lib/dhcpsrv/mysql_host_data_source.cc


+ 1 - 16
src/lib/dhcpsrv/mysql_host_data_source.h

@@ -84,6 +84,7 @@ public:
     /// for a specified identifier. This method may return multiple hosts
     /// for a specified identifier. This method may return multiple hosts
     /// because a particular client may have reservations in multiple subnets.
     /// because a particular client may have reservations in multiple subnets.
     ///
     ///
+    /// @param identifier_type Identifier type.
     /// @param identifier_begin Pointer to a begining of a buffer containing
     /// @param identifier_begin Pointer to a begining of a buffer containing
     /// an identifier.
     /// an identifier.
     /// @param identifier_len Identifier length.
     /// @param identifier_len Identifier length.
@@ -251,22 +252,6 @@ public:
     /// Rolls back all pending database operations.
     /// Rolls back all pending database operations.
     virtual void rollback();
     virtual void rollback();
 
 
-    /// @brief Statement Tags
-    ///
-    /// The contents of the enum are indexes into the list of SQL statements
-    enum StatementIndex {
-        INSERT_HOST,            // Insert new host to collection
-        INSERT_V6_RESRV,        // Insert v6 reservation
-        GET_HOST_DHCPID,        // Gets hosts by host identifier
-        GET_HOST_ADDR,          // Gets hosts by IPv4 address
-        GET_HOST_SUBID4_DHCPID, // Gets host by IPv4 SubnetID, HW address/DUID
-        GET_HOST_SUBID6_DHCPID, // Gets host by IPv6 SubnetID, HW address/DUID
-        GET_HOST_SUBID_ADDR,    // Gets host by IPv4 SubnetID and IPv4 address
-        GET_HOST_PREFIX,        // Gets host by IPv6 prefix
-        GET_VERSION,            // Obtain version number
-        NUM_STATEMENTS          // Number of statements
-    };
-
 private:
 private:
 
 
     /// @brief Pointer to the implementation of the @ref MySqlHostDataSource.
     /// @brief Pointer to the implementation of the @ref MySqlHostDataSource.

+ 1 - 29
src/lib/dhcpsrv/mysql_lease_mgr.cc

@@ -15,7 +15,6 @@
 
 
 #include <boost/static_assert.hpp>
 #include <boost/static_assert.hpp>
 #include <mysqld_error.h>
 #include <mysqld_error.h>
-#include <errmsg.h>
 
 
 #include <iostream>
 #include <iostream>
 #include <iomanip>
 #include <iomanip>
@@ -2047,34 +2046,7 @@ MySqlLeaseMgr::rollback() {
 void
 void
 MySqlLeaseMgr::checkError(int status, StatementIndex index,
 MySqlLeaseMgr::checkError(int status, StatementIndex index,
                            const char* what) const {
                            const char* what) const {
-    if (status != 0) {
-        switch(mysql_errno(conn_.mysql_)) {
-            // These are the ones we consider fatal. Remember this method is
-            // used to check errors of API calls made subsequent to successfully
-            // connecting.  Errors occuring while attempting to connect are
-            // checked in the connection code. An alternative would be to call
-            // mysql_ping() - assuming autoreconnect is off. If that fails
-            // then we know connection is toast.
-            case CR_SERVER_GONE_ERROR:
-            case CR_SERVER_LOST:
-            case CR_OUT_OF_MEMORY:
-            case CR_CONNECTION_ERROR:
-                // We're exiting on fatal
-                LOG_ERROR(dhcpsrv_logger, DHCPSRV_MYSQL_FATAL_ERROR)
-                         .arg(what)
-                         .arg(conn_.text_statements_[index])
-                         .arg(mysql_error(conn_.mysql_))
-                         .arg(mysql_errno(conn_.mysql_));
-                exit (-1);
-
-            default:
-                // Connection is ok, so it must be an SQL error
-                isc_throw(DbOperationError, what << " for <"
-                          << conn_.text_statements_[index] << ">, reason: "
-                          << mysql_error(conn_.mysql_) << " (error code "
-                          << mysql_errno(conn_.mysql_) << ")");
-        }
-    }
+    conn_.checkError(status, index, what);
 }
 }
 
 
 }; // end of isc::dhcp namespace
 }; // end of isc::dhcp namespace

+ 1 - 17
src/lib/dhcpsrv/mysql_lease_mgr.h

@@ -593,23 +593,7 @@ private:
 
 
     /// @brief Check Error and Throw Exception
     /// @brief Check Error and Throw Exception
     ///
     ///
-    /// Virtually all MySQL functions return a status which, if non-zero,
-    /// indicates an error.  This function centralizes the error checking
-    /// code.
-    ///
-    /// It is used to determine whether or not the function succeeded, and
-    /// in the event of failures, decide whether or not those failures are
-    /// recoverable.
-    ///
-    /// If the error is recoverable, the method will throw a DbOperationError.
-    /// In the error is deemed unrecoverable, such as a loss of connectivity
-    /// with the server, this method will log the error and call exit(-1);
-    ///
-    /// @todo Calling exit() is viewed as a short term solution for Kea 1.0.
-    /// Two tickets are likely to alter this behavior, first is #3639, which
-    /// calls for the ability to attempt to reconnect to the database. The
-    /// second ticket, #4087 which calls for the implementation of a generic,
-    /// FatalException class which will propagate outward.
+    /// This method invokes @ref MySqlConnection::checkError.
     ///
     ///
     /// @param status Status code: non-zero implies an error
     /// @param status Status code: non-zero implies an error
     /// @param index Index of statement that caused the error
     /// @param index Index of statement that caused the error

+ 9 - 2
src/lib/dhcpsrv/parsers/dhcp_parsers.cc

@@ -164,7 +164,14 @@ template <> void ValueParser<std::string>::build(ConstElementPtr value) {
     // Invoke common code for all specializations of build().
     // Invoke common code for all specializations of build().
     buildCommon(value);
     buildCommon(value);
 
 
-    value_ = value->str();
+    // For strings we need to use stringValue() rather than str().
+    // str() returns fully escaped special characters, so
+    // single backslash would be misrepresented as "\\".
+    if (value->getType() == Element::string) {
+        value_ = value->stringValue();
+    } else {
+        value_ = value->str();
+    }
     boost::erase_all(value_, "\"");
     boost::erase_all(value_, "\"");
 }
 }
 
 
@@ -571,7 +578,7 @@ OptionDataParser::findOptionDefinition(const std::string& option_space,
     if (!def) {
     if (!def) {
         // Check if this is a vendor-option. If it is, get vendor-specific
         // Check if this is a vendor-option. If it is, get vendor-specific
         // definition.
         // definition.
-        uint32_t vendor_id = CfgOption::optionSpaceToVendorId(option_space);
+        uint32_t vendor_id = LibDHCP::optionSpaceToVendorId(option_space);
         if (vendor_id) {
         if (vendor_id) {
             def = LibDHCP::getVendorOptionDef(u, vendor_id, search_key);
             def = LibDHCP::getVendorOptionDef(u, vendor_id, search_key);
         }
         }

+ 32 - 1
src/lib/dhcpsrv/tests/cfg_option_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2016 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
@@ -12,7 +12,9 @@
 #include <dhcpsrv/cfg_option.h>
 #include <dhcpsrv/cfg_option.h>
 #include <boost/pointer_cast.hpp>
 #include <boost/pointer_cast.hpp>
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
+#include <iterator>
 #include <limits>
 #include <limits>
+#include <list>
 #include <sstream>
 #include <sstream>
 
 
 using namespace isc;
 using namespace isc;
@@ -490,5 +492,34 @@ TEST(CfgOptionTest, addVendorOptions) {
     EXPECT_TRUE(options->empty());
     EXPECT_TRUE(options->empty());
 }
 }
 
 
+// This test verifies that option space names for the vendor options are
+// correct.
+TEST(CfgOptionTest, getVendorIdsSpaceNames) {
+    CfgOption cfg;
+
+    // Create 10 options, each goes under a different vendor id.
+    for (uint16_t code = 100; code < 110; ++code) {
+        OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+        // Generate space name for a unique vendor id.
+        std::ostringstream s;
+        s << "vendor-" << code;
+        ASSERT_NO_THROW(cfg.add(option, false, s.str()));
+    }
+
+    // We should now have 10 different vendor ids.
+    std::list<std::string> space_names = cfg.getVendorIdsSpaceNames();
+    ASSERT_EQ(10, space_names.size());
+
+    // Check that the option space names for those vendor ids are correct.
+    for (std::list<std::string>::iterator name = space_names.begin();
+         name != space_names.end(); ++name) {
+        uint16_t id = static_cast<uint16_t>(std::distance(space_names.begin(),
+                                                          name));
+        std::ostringstream s;
+        s << "vendor-" << (100 + id);
+        EXPECT_EQ(s.str(), *name);
+    }
+}
+
 
 
 } // end of anonymous namespace
 } // end of anonymous namespace

+ 44 - 0
src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc

@@ -655,6 +655,50 @@ TEST_F(ParseConfigTest, minimalOptionDataTest) {
     EXPECT_EQ(val, opt_ptr->toText());
     EXPECT_EQ(val, opt_ptr->toText());
 }
 }
 
 
+/// @brief Check parsing of options with escape characters.
+///
+/// Note that this tests basic operation of the OptionDataListParser and
+/// OptionDataParser.  It uses a simple configuration consisting of one
+/// one definition and matching option data.  It verifies that the option
+/// is parsed and committed to storage correctly and that its content
+/// has the actual character (e.g. an actual backslash, not double backslash).
+TEST_F(ParseConfigTest, escapedOptionDataTest) {
+
+    parser_context_->universe_ = Option::V4;
+
+    // We need to use double escapes here. The first backslash will
+    // be consumed by C++ preprocessor, so the actual string will
+    // have two backslash characters: \\SMSBoot\\x64\\wdsnbp.com.
+    //
+    std::string config =
+        "{\"option-data\": [ {"
+        "    \"name\": \"boot-file-name\","
+        "    \"data\": \"\\\\SMSBoot\\\\x64\\\\wdsnbp.com\""
+        " } ]"
+        "}";
+    std::cout << config << std::endl;
+
+    // Verify that the configuration string parses.
+    int rcode = parseConfiguration(config);
+    ASSERT_EQ(0, rcode);
+
+    // Verify that the option can be retrieved.
+    OptionPtr opt = getOptionPtr("dhcp4", DHO_BOOT_FILE_NAME);
+    ASSERT_TRUE(opt);
+
+    util::OutputBuffer buf(100);
+
+    uint8_t exp[] = { DHO_BOOT_FILE_NAME, 23, '\\', 'S', 'M', 'S', 'B', 'o', 'o',
+                      't', '\\', 'x', '6', '4', '\\', 'w', 'd', 's', 'n', 'b',
+                      'p', '.', 'c', 'o', 'm' };
+    ASSERT_EQ(25, sizeof(exp));
+
+    opt->pack(buf);
+    EXPECT_EQ(Option::OPTION4_HDR_LEN + 23, buf.getLength());
+
+    EXPECT_TRUE(0 == memcmp(buf.getData(), exp, 25));
+}
+
 // This test checks behavior of the configuration parser for option data
 // This test checks behavior of the configuration parser for option data
 // for different values of csv-format parameter and when there is an option
 // for different values of csv-format parameter and when there is an option
 // definition present.
 // definition present.

+ 259 - 4
src/lib/dhcpsrv/tests/generic_host_data_source_unittest.cc

@@ -4,15 +4,30 @@
 // 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 <dhcp/dhcp6.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/option4_addrlst.h>
+#include <dhcp/option6_addrlst.h>
+#include <dhcp/option_space.h>
+#include <dhcp/option_string.h>
+#include <dhcp/option_int.h>
+#include <dhcp/option_vendor.h>
 #include <dhcpsrv/tests/generic_host_data_source_unittest.h>
 #include <dhcpsrv/tests/generic_host_data_source_unittest.h>
 #include <dhcpsrv/tests/test_utils.h>
 #include <dhcpsrv/tests/test_utils.h>
 #include <dhcpsrv/database_connection.h>
 #include <dhcpsrv/database_connection.h>
 #include <asiolink/io_address.h>
 #include <asiolink/io_address.h>
+#include <util/buffer.h>
+#include <boost/foreach.hpp>
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
+#include <cstring>
+#include <list>
+#include <string>
 #include <sstream>
 #include <sstream>
+#include <typeinfo>
 
 
 using namespace std;
 using namespace std;
 using namespace isc::asiolink;
 using namespace isc::asiolink;
+using namespace isc::util;
 
 
 namespace isc {
 namespace isc {
 namespace dhcp {
 namespace dhcp {
@@ -20,10 +35,11 @@ namespace test {
 
 
 GenericHostDataSourceTest::GenericHostDataSourceTest()
 GenericHostDataSourceTest::GenericHostDataSourceTest()
     :hdsptr_() {
     :hdsptr_() {
-
+    LibDHCP::clearRuntimeOptionDefs();
 }
 }
 
 
 GenericHostDataSourceTest::~GenericHostDataSourceTest() {
 GenericHostDataSourceTest::~GenericHostDataSourceTest() {
+    LibDHCP::clearRuntimeOptionDefs();
 }
 }
 
 
 std::vector<uint8_t>
 std::vector<uint8_t>
@@ -82,8 +98,8 @@ GenericHostDataSourceTest::initializeHost4(const std::string& address,
     // subnet4 with subnet6.
     // subnet4 with subnet6.
     static SubnetID subnet4 = 0;
     static SubnetID subnet4 = 0;
     static SubnetID subnet6 = 100;
     static SubnetID subnet6 = 100;
-    subnet4++;
-    subnet6++;
+    ++subnet4;
+    ++subnet6;
 
 
     IOAddress addr(address);
     IOAddress addr(address);
     HostPtr host(new Host(&ident[0], ident.size(), id, subnet4, subnet6, addr));
     HostPtr host(new Host(&ident[0], ident.size(), id, subnet4, subnet6, addr));
@@ -229,12 +245,16 @@ void GenericHostDataSourceTest::compareHosts(const ConstHostPtr& host1,
     compareReservations6(host1->getIPv6Reservations(),
     compareReservations6(host1->getIPv6Reservations(),
                          host2->getIPv6Reservations());
                          host2->getIPv6Reservations());
 
 
-    // And compare client classification details
+    // Compare client classification details
     compareClientClasses(host1->getClientClasses4(),
     compareClientClasses(host1->getClientClasses4(),
                          host2->getClientClasses4());
                          host2->getClientClasses4());
 
 
     compareClientClasses(host1->getClientClasses6(),
     compareClientClasses(host1->getClientClasses6(),
                          host2->getClientClasses6());
                          host2->getClientClasses6());
+
+    // Compare DHCPv4 and DHCPv6 options.
+    compareOptions(host1->getCfgOption4(), host2->getCfgOption4());
+    compareOptions(host1->getCfgOption6(), host2->getCfgOption6());
 }
 }
 
 
 DuidPtr
 DuidPtr
@@ -313,6 +333,178 @@ GenericHostDataSourceTest::compareClientClasses(const ClientClasses& /*classes1*
     ///        This is part of the work for #4213.
     ///        This is part of the work for #4213.
 }
 }
 
 
+void
+GenericHostDataSourceTest::compareOptions(const ConstCfgOptionPtr& cfg1,
+                                          const ConstCfgOptionPtr& cfg2) const {
+    ASSERT_TRUE(cfg1);
+    ASSERT_TRUE(cfg2);
+
+    // Combine option space names with vendor space names in a single list.
+    std::list<std::string> option_spaces = cfg2->getOptionSpaceNames();
+    std::list<std::string> vendor_spaces = cfg2->getVendorIdsSpaceNames();
+    option_spaces.insert(option_spaces.end(), vendor_spaces.begin(),
+                         vendor_spaces.end());
+
+    // Make sure that the number of option spaces is equal in both
+    // configurations.
+    EXPECT_EQ(option_spaces.size(), cfg1->getOptionSpaceNames().size());
+    EXPECT_EQ(vendor_spaces.size(), cfg1->getVendorIdsSpaceNames().size());
+
+    // Iterate over all option spaces existing in cfg2.
+    BOOST_FOREACH(std::string space, option_spaces) {
+        // Retrieve options belonging to the current option space.
+        OptionContainerPtr options1 = cfg1->getAll(space);
+        OptionContainerPtr options2 = cfg2->getAll(space);
+        ASSERT_TRUE(options1) << "failed for option space " << space;
+        ASSERT_TRUE(options2) << "failed for option space " << space;
+
+        // If number of options doesn't match, the test fails.
+        ASSERT_EQ(options1->size(), options2->size())
+            << "failed for option space " << space;
+
+        // Iterate over all options within this option space.
+        BOOST_FOREACH(OptionDescriptor desc1, *options1) {
+            OptionDescriptor desc2 = cfg2->get(space, desc1.option_->getType());
+            // Compare persistent flag.
+            EXPECT_EQ(desc1.persistent_, desc2.persistent_)
+                << "failed for option " << space << "." << desc1.option_->getType();
+            // Compare formatted value.
+            EXPECT_EQ(desc1.formatted_value_, desc2.formatted_value_)
+                << "failed for option " << space << "." << desc1.option_->getType();
+
+            // Retrieve options.
+            Option* option1 = desc1.option_.get();
+            Option* option2 = desc2.option_.get();
+
+            // Options must be represented by the same C++ class derived from
+            // the Option class.
+            EXPECT_TRUE(typeid(*option1) == typeid(*option2))
+                << "Comapared DHCP options, having option code "
+                << desc1.option_->getType() << " and belonging to the "
+                << space << " option space, are represented "
+                "by different C++ classes: "
+                << typeid(*option1).name() << " vs "
+                << typeid(*option2).name();
+
+            // Because we use different C++ classes to represent different
+            // options, the simplest way to make sure that the options are
+            // equal is to simply compare them in wire format.
+            OutputBuffer buf1(option1->len());
+            ASSERT_NO_THROW(option1->pack(buf1));
+            OutputBuffer buf2(option2->len());
+            ASSERT_NO_THROW(option2->pack(buf2));
+
+            ASSERT_EQ(buf1.getLength(), buf2.getLength())
+                 << "failed for option " << space << "." << desc1.option_->getType();
+            EXPECT_EQ(0, memcmp(buf1.getData(), buf2.getData(), buf1.getLength()))
+                << "failed for option " << space << "." << desc1.option_->getType();
+        }
+    }
+}
+
+OptionDescriptor
+GenericHostDataSourceTest::createEmptyOption(const Option::Universe& universe,
+                                             const uint16_t option_type,
+                                             const bool persist) const {
+    OptionPtr option(new Option(universe, option_type));
+    OptionDescriptor desc(option, persist);
+    return (desc);
+}
+
+
+OptionDescriptor
+GenericHostDataSourceTest::createVendorOption(const Option::Universe& universe,
+                                              const bool persist,
+                                              const bool formatted,
+                                              const uint32_t vendor_id) const {
+    OptionVendorPtr option(new OptionVendor(universe, vendor_id));
+
+    std::ostringstream s;
+    if (formatted) {
+        // Vendor id comprises vendor-id field, for which we need to
+        // assign a value in the textual (formatted) format.
+        s << vendor_id;
+    }
+
+    OptionDescriptor desc(option, persist, s.str());
+    return (desc);
+}
+
+void
+GenericHostDataSourceTest::addTestOptions(const HostPtr& host,
+                                          const bool formatted,
+                                          const AddedOptions& added_options) const {
+
+    OptionDefSpaceContainer defs;
+
+    if ((added_options == DHCP4_ONLY) || (added_options == DHCP4_AND_DHCP6)) {
+        // Add DHCPv4 options.
+        CfgOptionPtr opts = host->getCfgOption4();
+        opts->add(createOption<OptionString>(Option::V4, DHO_BOOT_FILE_NAME,
+                                             true, formatted, "my-boot-file"),
+                  DHCP4_OPTION_SPACE);
+        opts->add(createOption<OptionUint8>(Option::V4, DHO_DEFAULT_IP_TTL,
+                                            false, formatted, 64),
+                  DHCP4_OPTION_SPACE);
+        opts->add(createOption<OptionUint32>(Option::V4, 1, false, formatted, 312131),
+                  "vendor-encapsulated-options");
+        opts->add(createAddressOption<Option4AddrLst>(254, false, formatted, "192.0.2.3"),
+                  DHCP4_OPTION_SPACE);
+        opts->add(createEmptyOption(Option::V4, 1, true), "isc");
+        opts->add(createAddressOption<Option4AddrLst>(2, false, formatted, "10.0.0.5",
+                                                      "10.0.0.3", "10.0.3.4"),
+                  "isc");
+
+        // Add definitions for DHCPv4 non-standard options.
+        defs.addItem(OptionDefinitionPtr(new OptionDefinition("vendor-encapsulated-1",
+                                                              1, "uint32")),
+                     "vendor-encapsulated-options");
+        defs.addItem(OptionDefinitionPtr(new OptionDefinition("option-254", 254,
+                                                              "ipv4-address", true)),
+                     DHCP4_OPTION_SPACE);
+        defs.addItem(OptionDefinitionPtr(new OptionDefinition("isc-1", 1, "empty")),
+                     "isc");
+        defs.addItem(OptionDefinitionPtr(new OptionDefinition("isc-2", 2,
+                                                              "ipv4-address", true)),
+                     "isc");
+    }
+
+    if ((added_options == DHCP6_ONLY) || (added_options == DHCP4_AND_DHCP6)) {
+        // Add DHCPv6 options.
+        CfgOptionPtr opts = host->getCfgOption6();
+        opts->add(createOption<OptionString>(Option::V6, D6O_BOOTFILE_URL,
+                                             true, formatted, "my-boot-file"),
+                  DHCP6_OPTION_SPACE);
+        opts->add(createOption<OptionUint32>(Option::V6, D6O_INFORMATION_REFRESH_TIME,
+                                             false, formatted, 3600),
+                  DHCP6_OPTION_SPACE);
+        opts->add(createVendorOption(Option::V6, false, formatted, 2495),
+                  DHCP6_OPTION_SPACE);
+        opts->add(createAddressOption<Option6AddrLst>(1024, false, formatted,
+                                                      "2001:db8:1::1"),
+                  DHCP6_OPTION_SPACE);
+        opts->add(createEmptyOption(Option::V6, 1, true), "isc2");
+        opts->add(createAddressOption<Option6AddrLst>(2, false, formatted, "3000::1",
+                                                      "3000::2", "3000::3"),
+                  "isc2");
+
+        // Add definitions for DHCPv6 non-standard options.
+        defs.addItem(OptionDefinitionPtr(new OptionDefinition("option-1024", 1024,
+                                                              "ipv6-address", true)),
+                     DHCP6_OPTION_SPACE);
+        defs.addItem(OptionDefinitionPtr(new OptionDefinition("option-1", 1, "empty")),
+                     "isc2");
+        defs.addItem(OptionDefinitionPtr(new OptionDefinition("option-2", 2,
+                                                              "ipv6-address", true)),
+                     "isc2");
+    }
+
+    // Register created "runtime" option definitions. They will be used by a
+    // host data source to convert option data into the appropriate option
+    // classes when the options are retrieved.
+    LibDHCP::setRuntimeOptionDefs(defs);
+}
+
 void GenericHostDataSourceTest::testBasic4(const Host::IdentifierType& id) {
 void GenericHostDataSourceTest::testBasic4(const Host::IdentifierType& id) {
     // Make sure we have the pointer to the host data source.
     // Make sure we have the pointer to the host data source.
     ASSERT_TRUE(hdsptr_);
     ASSERT_TRUE(hdsptr_);
@@ -898,6 +1090,69 @@ void GenericHostDataSourceTest::testMultipleReservationsDifferentOrder(){
 
 
 }
 }
 
 
+void GenericHostDataSourceTest::testOptionsReservations4(const bool formatted) {
+    HostPtr host = initializeHost4("192.0.2.5", Host::IDENT_HWADDR);
+    // Add a bunch of DHCPv4 and DHCPv6 options for the host.
+    ASSERT_NO_THROW(addTestOptions(host, formatted, DHCP4_ONLY));
+    // Insert host and the options into respective tables.
+    ASSERT_NO_THROW(hdsptr_->add(host));
+    // Subnet id will be used in quries to the database.
+    SubnetID subnet_id = host->getIPv4SubnetID();
+
+    // getAll4(address)
+    ConstHostCollection hosts_by_addr = hdsptr_->getAll4(host->getIPv4Reservation());
+    ASSERT_EQ(1, hosts_by_addr.size());
+    ASSERT_NO_FATAL_FAILURE(compareHosts(host, *hosts_by_addr.begin()));
+
+    // get4(subnet_id, identifier_type, identifier, identifier_size)
+    ConstHostPtr host_by_id = hdsptr_->get4(subnet_id,
+                                            host->getIdentifierType(),
+                                            &host->getIdentifier()[0],
+                                            host->getIdentifier().size());
+    ASSERT_NO_FATAL_FAILURE(compareHosts(host, host_by_id));
+
+    // get4(subnet_id, address)
+    ConstHostPtr host_by_addr = hdsptr_->get4(subnet_id, IOAddress("192.0.2.5"));
+    ASSERT_NO_FATAL_FAILURE(compareHosts(host, host_by_addr));
+}
+
+void GenericHostDataSourceTest::testOptionsReservations6(const bool formatted) {
+    HostPtr host = initializeHost6("2001:db8::1", Host::IDENT_DUID, false);
+    // Add a bunch of DHCPv4 and DHCPv6 options for the host.
+    ASSERT_NO_THROW(addTestOptions(host, formatted, DHCP6_ONLY));
+    // Insert host, options and IPv6 reservations into respective tables.
+    ASSERT_NO_THROW(hdsptr_->add(host));
+    // Subnet id will be used in queries to the database.
+    SubnetID subnet_id = host->getIPv6SubnetID();
+
+    // get6(subnet_id, identifier_type, identifier, identifier_size)
+    ConstHostPtr host_by_id = hdsptr_->get6(subnet_id, host->getIdentifierType(),
+                                            &host->getIdentifier()[0],
+                                            host->getIdentifier().size());
+    ASSERT_NO_FATAL_FAILURE(compareHosts(host, host_by_id));
+
+    // get6(address, prefix_len)
+    ConstHostPtr host_by_addr = hdsptr_->get6(IOAddress("2001:db8::1"), 128);
+    ASSERT_NO_FATAL_FAILURE(compareHosts(host, host_by_addr));
+}
+
+void GenericHostDataSourceTest::testOptionsReservations46(const bool formatted) {
+    HostPtr host = initializeHost6("2001:db8::1", Host::IDENT_HWADDR, false);
+
+    // Add a bunch of DHCPv4 and DHCPv6 options for the host.
+    ASSERT_NO_THROW(addTestOptions(host, formatted, DHCP4_AND_DHCP6));
+    // Insert host, options and IPv6 reservations into respective tables.
+    ASSERT_NO_THROW(hdsptr_->add(host));
+
+    // getAll(identifier_type, identifier, identifier_size)
+    ConstHostCollection hosts_by_id = hdsptr_->getAll(host->getIdentifierType(),
+                                                      &host->getIdentifier()[0],
+                                                      host->getIdentifier().size());
+    ASSERT_EQ(1, hosts_by_id.size());
+    ASSERT_NO_FATAL_FAILURE(compareHosts(host, *hosts_by_id.begin()));
+}
+
+
 }; // namespace test
 }; // namespace test
 }; // namespace dhcp
 }; // namespace dhcp
 }; // namespace isc
 }; // namespace isc

+ 239 - 1
src/lib/dhcpsrv/tests/generic_host_data_source_unittest.h

@@ -7,10 +7,15 @@
 #ifndef GENERIC_HOST_DATA_SOURCE_UNITTEST_H
 #ifndef GENERIC_HOST_DATA_SOURCE_UNITTEST_H
 #define GENERIC_HOST_DATA_SOURCE_UNITTEST_H
 #define GENERIC_HOST_DATA_SOURCE_UNITTEST_H
 
 
+#include <asiolink/io_address.h>
 #include <dhcpsrv/base_host_data_source.h>
 #include <dhcpsrv/base_host_data_source.h>
 #include <dhcpsrv/host.h>
 #include <dhcpsrv/host.h>
 #include <dhcp/classify.h>
 #include <dhcp/classify.h>
+#include <dhcp/option.h>
+#include <boost/algorithm/string/join.hpp>
+#include <boost/shared_ptr.hpp>
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
+#include <sstream>
 #include <vector>
 #include <vector>
 
 
 namespace isc {
 namespace isc {
@@ -30,6 +35,16 @@ public:
         V6
         V6
     };
     };
 
 
+    /// @brief Options to be inserted into a host.
+    ///
+    /// Parameter of this type is passed to the @ref addTestOptions to
+    /// control which option types should be inserted into a host.
+    enum AddedOptions {
+        DHCP4_ONLY,
+        DHCP6_ONLY,
+        DHCP4_AND_DHCP6
+    };
+
     /// @brief Default constructor.
     /// @brief Default constructor.
     GenericHostDataSourceTest();
     GenericHostDataSourceTest();
 
 
@@ -122,7 +137,202 @@ public:
     /// @param classes1 first list of client classes
     /// @param classes1 first list of client classes
     /// @param classes2 second list of client classes
     /// @param classes2 second list of client classes
     void compareClientClasses(const ClientClasses& classes1,
     void compareClientClasses(const ClientClasses& classes1,
-                              const ClientClasses& classes2);
+                              const ClientClasses& classes2);                           
+
+    /// @brief Compares options within two configurations.
+    ///
+    /// This method uses gtest macros to signal errors.
+    ///
+    /// @param cfg1 First configuration.
+    /// @param cfg2 Second configuration.
+    void compareOptions(const ConstCfgOptionPtr& cfg1,
+                        const ConstCfgOptionPtr& cfg2) const;
+
+    /// @brief Creates an opton descriptor holding an empty option.
+    ///
+    /// @param universe V4 or V6.
+    /// @param option_type Option type.
+    /// @param persist A boolean flag indicating if the option is always
+    /// returned to the client or only when requested.
+    ///
+    /// @return Descriptor holding an empty option.
+    OptionDescriptor createEmptyOption(const Option::Universe& universe,
+                                       const uint16_t option_type,
+                                       const bool persist) const;
+
+    /// @brief Creates an instance of the option for which it is possible to
+    /// specify universe, option type, persistence flag  and value in
+    /// the constructor.
+    ///
+    /// Examples of options that can be created using this function are:
+    /// - @ref OptionString
+    /// - different variants of @ref OptionInt.
+    ///
+    /// @param universe V4 or V6.
+    /// @param option_type Option type.
+    /// @param persist A boolean flag indicating if the option is always
+    /// returned to the client or only when requested.
+    /// @param formatted A boolean value selecting if the formatted option
+    /// value should be used (if true), or binary value (if false).
+    /// @param value Option value to be assigned to the option.
+    /// @tparam OptionType Class encapsulating the option.
+    /// @tparam DataType Option value data type.
+    ///
+    /// @return Descriptor holding an instance of the option created.
+    template<typename OptionType, typename DataType>
+    OptionDescriptor createOption(const Option::Universe& universe,
+                                  const uint16_t option_type,
+                                  const bool persist,
+                                  const bool formatted,
+                                  const DataType& value) const {
+        boost::shared_ptr<OptionType> option(new OptionType(universe, option_type,
+                                                            value));
+        std::ostringstream s;
+        if (formatted) {
+            // Using formatted option value. Convert option value to a
+            // textual format.
+            s << value;
+        }
+        OptionDescriptor desc(option, persist, s.str());
+        return (desc);
+    }
+
+    /// @brief Creates an instance of the option for which it is possible to
+    /// specify option type, persistence flag  and value in the constructor.
+    ///
+    /// Examples of options that can be created using this function are:
+    /// - @ref Option4AddrLst
+    /// - @ref Option6AddrLst
+    ///
+    /// @param option_type Option type.
+    /// @param persist A boolean flag indicating if the option is always
+    /// returned to the client or only when requested.
+    /// @param formatted A boolean value selecting if the formatted option
+    /// value should be used (if true), or binary value (if false).
+    /// @param value Option value to be assigned to the option.
+    /// @tparam OptionType Class encapsulating the option.
+    /// @tparam DataType Option value data type.
+    ///
+    /// @return Descriptor holding an instance of the option created.
+    template<typename OptionType, typename DataType>
+    OptionDescriptor createOption(const uint16_t option_type,
+                                  const bool persist,
+                                  const bool formatted,
+                                  const DataType& value) const {
+        boost::shared_ptr<OptionType> option(new OptionType(option_type, value));
+
+        std::ostringstream s;
+        if (formatted) {
+            // Using formatted option value. Convert option value to a
+            // textual format.
+            s << value;
+        }
+
+        OptionDescriptor desc(option, persist, s.str());
+        return (desc);
+    }
+
+    /// @brief Creates an instance of the option holding list of IP addresses.
+    ///
+    /// @param option_type Option type.
+    /// @param persist A boolean flag indicating if the option is always
+    /// returned to the client or only when requested.
+    /// @param formatted A boolean value selecting if the formatted option
+    /// value should be used (if true), or binary value (if false).
+    /// @param address1 First address to be included. If address is empty, it is
+    /// not included.
+    /// @param address2 Second address to be included. If address is empty, it
+    /// is not included.
+    /// @param address3 Third address to be included. If address is empty, it
+    /// is not included.
+    /// @tparam OptionType Class encapsulating the option.
+    ///
+    /// @return Descriptor holding an instance of the option created.
+    template<typename OptionType>
+    OptionDescriptor
+    createAddressOption(const uint16_t option_type,
+                        const bool persist,
+                        const bool formatted,
+                        const std::string& address1 = "",
+                        const std::string& address2 = "",
+                        const std::string& address3 = "") const {
+        std::ostringstream s;
+        // First address.
+        typename OptionType::AddressContainer addresses;
+        if (!address1.empty()) {
+            addresses.push_back(asiolink::IOAddress(address1));
+            if (formatted) {
+                s << address1;
+            }
+        }
+        // Second address.
+        if (!address2.empty()) {
+            addresses.push_back(asiolink::IOAddress(address2));
+            if (formatted) {
+                if (s.tellp() != std::streampos(0)) {
+                    s << ",";
+                }
+                s << address2;
+            }
+        }
+        // Third address.
+        if (!address3.empty()) {
+            addresses.push_back(asiolink::IOAddress(address3));
+            if (formatted) {
+                if (s.tellp() != std::streampos(0)) {
+                    s << ",";
+                }
+                s << address3;
+            }
+        }
+
+        boost::shared_ptr<OptionType> option(new OptionType(option_type,
+                                                            addresses));
+        OptionDescriptor desc(option, persist, s.str());
+        return (desc);
+    }
+
+    /// @brief Creates an instance of the vendor option.
+    ///
+    /// @param universe V4 or V6.
+    /// @param persist A boolean flag indicating if the option is always
+    /// returned to the client or only when requested.
+    /// @param formatted A boolean value selecting if the formatted option
+    /// value should be used (if true), or binary value (if false).
+    /// @param vendor_id Vendor identifier.
+    ///
+    /// @return Descriptor holding an instance of the option created.
+    OptionDescriptor createVendorOption(const Option::Universe& universe,
+                                        const bool persist,
+                                        const bool formatted,
+                                        const uint32_t vendor_id) const;
+
+    /// @brief Adds multiple options into the host.
+    ///
+    /// This method creates the following options into the host object:
+    /// - DHCPv4 boot file name option,
+    /// - DHCPv4 default ip ttl option,
+    /// - DHCPv4 option 1 within vendor-encapsulated-options space,
+    /// - DHCPv4 option 254 with a single IPv4 address,
+    /// - DHCPv4 option 1 within isc option space,
+    /// - DHCPv6 boot file url option,
+    /// - DHCPv6 information refresh time option,
+    /// - DHCPv6 vendor option with vendor id 2495,
+    /// - DHCPv6 option 1024, with a sigle IPv6 address,
+    /// - DHCPv6 empty option 1, within isc2 option space,
+    /// - DHCPv6 option 2, within isc2 option space with 3 IPv6 addresses,
+    ///
+    /// This method also creates option definitions for the non-standard
+    /// options and registers them in the LibDHCP as runtime option
+    /// definitions.
+    ///
+    /// @param host Host object into which options should be added.
+    /// @param formatted A boolean value selecting if the formatted option
+    /// value should be used (if true), or binary value (if false).
+    /// @param added_options Controls which options should be inserted into
+    /// a host: DHCPv4, DHCPv6 options or both.
+    void addTestOptions(const HostPtr& host, const bool formatted,
+                        const AddedOptions& added_options) const;
 
 
     /// @brief Pointer to the host data source
     /// @brief Pointer to the host data source
     HostDataSourcePtr hdsptr_;
     HostDataSourcePtr hdsptr_;
@@ -238,6 +448,33 @@ public:
     /// Uses gtest macros to report failures.
     /// Uses gtest macros to report failures.
     void testAddDuplicate4();
     void testAddDuplicate4();
 
 
+    /// @brief Test that DHCPv4 options can be inserted and retrieved from
+    /// the database.
+    ///
+    /// Uses gtest macros to report failures.
+    ///
+    /// @param formatted Boolean value indicating if the option values
+    /// should be stored in the textual format in the database.
+    void testOptionsReservations4(const bool formatted);
+
+    /// @brief Test that DHCPv6 options can be inserted and retrieved from
+    /// the database.
+    ///
+    /// Uses gtest macros to report failures.
+    ///
+    /// @param formatted Boolean value indicating if the option values
+    /// should be stored in the textual format in the database.
+    void testOptionsReservations6(const bool formatted);
+
+    /// @brief Test that DHCPv4 and DHCPv6 options can be inserted and retrieved
+    /// with a single query to the database.
+    ///
+    /// Uses gtest macros to report failures.
+    ///
+    /// @param formatted Boolean value indicating if the option values
+    /// should be stored in the textual format in the database.
+    void testOptionsReservations46(const bool formatted);
+
     /// @brief Returns DUID with identical content as specified HW address
     /// @brief Returns DUID with identical content as specified HW address
     ///
     ///
     /// This method does not have any sense in real life and is only useful
     /// This method does not have any sense in real life and is only useful
@@ -257,6 +494,7 @@ public:
     /// @param duid DUID to be copied
     /// @param duid DUID to be copied
     /// @return HW address with the same value as specified DUID
     /// @return HW address with the same value as specified DUID
     HWAddrPtr DuidToHWAddr(const DuidPtr& duid);
     HWAddrPtr DuidToHWAddr(const DuidPtr& duid);
+
 };
 };
 
 
 }; // namespace test
 }; // namespace test

+ 83 - 0
src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc

@@ -427,4 +427,87 @@ TEST_F(MySqlHostDataSourceTest, addDuplicate4) {
     testAddDuplicate4();
     testAddDuplicate4();
 }
 }
 
 
+// This test verifies that DHCPv4 options can be inserted in a binary format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, optionsReservations4) {
+    testOptionsReservations4(false);
+}
+
+// This test verifies that DHCPv6 options can be inserted in a binary format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, optionsReservations6) {
+    testOptionsReservations6(false);
+}
+
+// This test verifies that DHCPv4 and DHCPv6 options can be inserted in a
+/// binary format and retrieved with a single query to the database.
+TEST_F(MySqlHostDataSourceTest, optionsReservations46) {
+    testOptionsReservations46(false);
+}
+
+// This test verifies that DHCPv4 options can be inserted in a textual format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, formattedOptionsReservations4) {
+    testOptionsReservations4(true);
+}
+
+// This test verifies that DHCPv6 options can be inserted in a textual format
+/// and retrieved from the MySQL host database.
+TEST_F(MySqlHostDataSourceTest, formattedOptionsReservations6) {
+    testOptionsReservations6(true);
+}
+
+// This test verifies that DHCPv4 and DHCPv6 options can be inserted in a
+// textual format and retrieved with a single query to the database.
+TEST_F(MySqlHostDataSourceTest, formattedOptionsReservations46) {
+    testOptionsReservations46(true);
+}
+
+// This test checks transactional insertion of the host information
+// into the database. The failure to insert host information at
+// any stage should cause the whole transaction to be rolled back.
+TEST_F(MySqlHostDataSourceTest, testAddRollback) {
+    // Make sure we have the pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    // To test the transaction rollback mechanism we need to cause the
+    // insertion of host information to fail at some stage. The 'hosts'
+    // table should be updated correctly but the failure should occur
+    // when inserting reservations or options. The simplest way to
+    // achieve that is to simply drop one of the tables. To do so, we
+    // connect to the database and issue a DROP query.
+    MySqlConnection::ParameterMap params;
+    params["name"] = "keatest";
+    params["user"] = "keatest";
+    params["password"] = "keatest";
+    MySqlConnection conn(params);
+    ASSERT_NO_THROW(conn.openDatabase());
+    int status = mysql_query(conn.mysql_,
+                             "DROP TABLE IF EXISTS ipv6_reservations");
+    ASSERT_EQ(0, status) << mysql_error(conn.mysql_);
+
+    // Create a host with a reservation.
+    HostPtr host = initializeHost6("2001:db8:1::1", Host::IDENT_HWADDR, false);
+    // Let's assign some DHCPv4 subnet to the host, because we will use the
+    // DHCPv4 subnet to try to retrieve the host after failed insertion.
+    host->setIPv4SubnetID(SubnetID(4));
+
+    // There is no ipv6_reservations table, so the insertion should fail.
+    ASSERT_THROW(hdsptr_->add(host), DbOperationError);
+
+    // Even though we have created a DHCPv6 host, we can't use get6()
+    // method to retrieve the host from the database, because the
+    // query would expect that the ipv6_reservations table is present.
+    // Therefore, the query would fail. Instead, we use the get4 method
+    // which uses the same client identifier, but doesn't attempt to
+    // retrieve the data from ipv6_reservations table. The query should
+    // pass but return no host because the (insert) transaction is expected
+    // to be rolled back.
+    ConstHostPtr from_hds = hdsptr_->get4(host->getIPv4SubnetID(),
+                                          host->getIdentifierType(),
+                                          &host->getIdentifier()[0],
+                                          host->getIdentifier().size());
+    EXPECT_FALSE(from_hds);
+}
+
 }; // Of anonymous namespace
 }; // Of anonymous namespace

+ 1 - 0
src/lib/dhcpsrv/writable_host_data_source.h

@@ -46,6 +46,7 @@ public:
     /// for a specified identifier. This method may return multiple hosts
     /// for a specified identifier. This method may return multiple hosts
     /// because a particular client may have reservations in multiple subnets.
     /// because a particular client may have reservations in multiple subnets.
     ///
     ///
+    /// @param identifier_type Identifier type.
     /// @param identifier_begin Pointer to a begining of a buffer containing
     /// @param identifier_begin Pointer to a begining of a buffer containing
     /// an identifier.
     /// an identifier.
     /// @param identifier_len Identifier length.
     /// @param identifier_len Identifier length.

+ 34 - 0
src/share/database/scripts/mysql/dhcpdb_create.mysql

@@ -436,6 +436,40 @@ ALTER TABLE hosts
     ADD CONSTRAINT fk_host_identifier_type FOREIGN KEY (dhcp_identifier_type)
     ADD CONSTRAINT fk_host_identifier_type FOREIGN KEY (dhcp_identifier_type)
     REFERENCES host_identifier_type (type);
     REFERENCES host_identifier_type (type);
 
 
+# Store DHCPv6 option code as 16-bit unsigned integer.
+ALTER TABLE dhcp6_options MODIFY code SMALLINT UNSIGNED NOT NULL;
+
+# Subnet identifier is unsigned.
+ALTER TABLE dhcp4_options MODIFY dhcp4_subnet_id INT UNSIGNED NULL;
+ALTER TABLE dhcp6_options MODIFY dhcp6_subnet_id INT UNSIGNED NULL;
+
+# Scopes associate DHCP options stored in dhcp4_options and
+# dhcp6_options tables with hosts, subnets, classes or indicate
+# that they are global options.
+CREATE TABLE IF NOT EXISTS dhcp_option_scope (
+    scope_id TINYINT UNSIGNED PRIMARY KEY NOT NULL,
+    scope_name VARCHAR(32)
+) ENGINE = INNODB;
+
+START TRANSACTION;
+INSERT INTO dhcp_option_scope VALUES (0, "global");
+INSERT INTO dhcp_option_scope VALUES (1, "subnet");
+INSERT INTO dhcp_option_scope VALUES (2, "client-class");
+INSERT INTO dhcp_option_scope VALUES (3, "host");
+COMMIT;
+
+# Add scopes into table holding DHCPv4 options
+ALTER TABLE dhcp4_options ADD COLUMN scope_id TINYINT UNSIGNED NOT NULL;
+ALTER TABLE dhcp4_options
+    ADD CONSTRAINT fk_dhcp4_option_scope FOREIGN KEY (scope_id)
+    REFERENCES dhcp_option_scope (scope_id);
+
+# Add scopes into table holding DHCPv6 options
+ALTER TABLE dhcp6_options ADD COLUMN scope_id TINYINT UNSIGNED NOT NULL;
+ALTER TABLE dhcp6_options
+    ADD CONSTRAINT fk_dhcp6_option_scope FOREIGN KEY (scope_id)
+    REFERENCES dhcp_option_scope (scope_id);
+
 # Update the schema version number
 # Update the schema version number
 UPDATE schema_version
 UPDATE schema_version
 SET version = '4', minor = '2';
 SET version = '4', minor = '2';

+ 1 - 0
src/share/database/scripts/mysql/dhcpdb_drop.mysql

@@ -17,6 +17,7 @@ DROP TABLE IF EXISTS dhcp4_options;
 DROP TABLE IF EXISTS dhcp6_options;
 DROP TABLE IF EXISTS dhcp6_options;
 DROP TABLE IF EXISTS host_identifier_type;
 DROP TABLE IF EXISTS host_identifier_type;
 DROP TABLE IF EXISTS lease_state;
 DROP TABLE IF EXISTS lease_state;
+DROP TABLE IF EXISTS dhcp_option_scope;
 DROP TRIGGER IF EXISTS host_BDEL;
 DROP TRIGGER IF EXISTS host_BDEL;
 DROP PROCEDURE IF EXISTS lease4DumpHeader;
 DROP PROCEDURE IF EXISTS lease4DumpHeader;
 DROP PROCEDURE IF EXISTS lease4DumpData;
 DROP PROCEDURE IF EXISTS lease4DumpData;