Browse Source

[master] Merge branch 'trac4552'

Marcin Siodelski 8 years ago
parent
commit
9b79fe005d

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

@@ -2854,6 +2854,35 @@ It is merely echoed by the server
 
     </section>
 
+    <section id="reservation4-message-fields">
+      <title>Reserving Next Server, Server Hostname and Boot File Name</title>
+      <para>BOOTP/DHCPv4 messages include "siaddr", "sname" and "file" fields.
+      Even though, DHCPv4 includes corresponding options, such as option 66 and
+      option 67, some clients may not support these options. Thus, server
+      administrators often use "siaddr", "sname" and "file" fields instead.</para>
+
+      <para>With Kea, it is possible to make static reservations for these DHCPv4
+      message fields:</para>
+
+    <screen>
+{
+    "subnet4": [ {
+        "reservations": [
+        {
+            "hw-address": "aa:bb:cc:dd:ee:ff",
+            <userinput>"next-server": "10.1.1.2",
+            "server-hostname": "server-hostname.example.org",
+            "boot-file-name": "/tmp/bootfile.efi"</userinput>
+        } ]
+    } ]
+}</screen>
+
+    <para>Note that those parameters can be specified in combination with
+    other parameters for a reservation, e.g. reserved IPv4 address. These
+    parameters are optional, i.e. a subset of them can specified, or all of
+    them can be omitted.</para>
+    </section>
+
     <section id="reservations4-mysql-pgsql">
       <title>Storing host reservations in MySQL or PostgreSQL</title>
 

+ 18 - 0
src/bin/dhcp4/dhcp4.spec

@@ -513,6 +513,24 @@
                         "item_type": "string",
                         "item_optional": false,
                         "item_default": "0.0.0.0"
+                      },
+                      {
+                        "item_name": "next-server",
+                        "item_type": "string",
+                        "item_optional": true,
+                        "item_default": "0.0.0.0"
+                      },
+                      {
+                        "item_name": "server-hostname",
+                        "item_type": "string",
+                        "item_optional": true,
+                        "item_default": ""
+                      },
+                      {
+                        "item_name": "boot-file-name",
+                        "item_type": "string",
+                        "item_optional": true,
+                        "item_default": ""
                       } ]
                   }
                 },

+ 36 - 9
src/bin/dhcp4/dhcp4_srv.cc

@@ -153,6 +153,9 @@ Dhcpv4Exchange::Dhcpv4Exchange(const AllocEnginePtr& alloc_engine,
 
             // Check for static reservations.
             alloc_engine->findReservation(*context_);
+
+            // Set siaddr, sname and file.
+            setReservedMessageFields();
         }
     }
 };
@@ -333,6 +336,27 @@ Dhcpv4Exchange::setHostIdentifiers() {
     }
 }
 
+void
+Dhcpv4Exchange::setReservedMessageFields() {
+    ConstHostPtr host = context_->host_;
+    // Nothing to do if host reservations not specified for this client.
+    if (host) {
+        if (!host->getNextServer().isV4Zero()) {
+            resp_->setSiaddr(host->getNextServer());
+        }
+
+        if (!host->getServerHostname().empty()) {
+            resp_->setSname(reinterpret_cast<
+                            const uint8_t*>(host->getServerHostname().c_str()));
+        }
+
+        if (!host->getBootFileName().empty()) {
+            resp_->setFile(reinterpret_cast<
+                           const uint8_t*>(host->getBootFileName().c_str()));
+        }
+    }
+}
+
 const std::string Dhcpv4Srv::VENDOR_CLASS_PREFIX("VENDOR_CLASS_");
 
 Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const bool use_bcast,
@@ -1515,12 +1539,6 @@ Dhcpv4Srv::assignLease(Dhcpv4Exchange& ex) {
         return;
     }
 
-    // Set up siaddr. Perhaps assignLease is not the best place to call this
-    // as siaddr has nothing to do with a lease, but otherwise we would have
-    // to select subnet twice (performance hit) or update too many functions
-    // at once.
-    /// @todo: move subnet selection to a common code
-    resp->setSiaddr(subnet->getSiaddr());
 
     // Get the server identifier. It will be used to determine the state
     // of the client.
@@ -2635,9 +2653,12 @@ Dhcpv4Srv::vendorClassSpecificProcessing(const Dhcpv4Exchange& ex) {
 
     if (query->inClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_MODEM)) {
 
-        // Set next-server. This is TFTP server address. Cable modems will
-        // download their configuration from that server.
-        rsp->setSiaddr(subnet->getSiaddr());
+        // Do not override
+        if (rsp->getSiaddr().isV4Zero()) {
+            // Set next-server. This is TFTP server address. Cable modems will
+            // download their configuration from that server.
+            rsp->setSiaddr(subnet->getSiaddr());
+        }
 
         // Now try to set up file field in DHCPv4 packet. We will just copy
         // content of the boot-file option, which contains the same information.
@@ -2664,6 +2685,12 @@ Dhcpv4Srv::vendorClassSpecificProcessing(const Dhcpv4Exchange& ex) {
         rsp->setSiaddr(IOAddress::IPV4_ZERO_ADDRESS());
     }
 
+    // Set up siaddr. Do not override siaddr if host specific value or
+    // vendor class specific value present.
+    if (rsp->getSiaddr().isV4Zero()) {
+        rsp->setSiaddr(subnet->getSiaddr());
+    }
+
     return (true);
 }
 

+ 4 - 0
src/bin/dhcp4/dhcp4_srv.h

@@ -150,6 +150,10 @@ private:
     /// host-reservation-identifiers
     void setHostIdentifiers();
 
+    /// @brief Sets reserved values of siaddr, sname and file in the
+    /// server's response.
+    void setReservedMessageFields();
+
     /// @brief Pointer to the allocation engine used by the server.
     AllocEnginePtr alloc_engine_;
     /// @brief Pointer to the DHCPv4 message sent by the client.

+ 15 - 1
src/bin/dhcp4/tests/dhcp4_client.cc

@@ -23,7 +23,7 @@ namespace test {
 
 Dhcp4Client::Configuration::Configuration()
     : routers_(), dns_servers_(), log_servers_(), quotes_servers_(),
-      serverid_("0.0.0.0") {
+      serverid_("0.0.0.0"), siaddr_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()) {
     reset();
 }
 
@@ -34,6 +34,9 @@ Dhcp4Client::Configuration::reset() {
     log_servers_.clear();
     quotes_servers_.clear();
     serverid_ = asiolink::IOAddress("0.0.0.0");
+    siaddr_ = asiolink::IOAddress::IPV4_ZERO_ADDRESS();
+    sname_.clear();
+    boot_file_name_.clear();
     lease_ = Lease4();
 }
 
@@ -178,6 +181,17 @@ Dhcp4Client::applyConfiguration() {
     if (opt_vendor) {
         config_.vendor_suboptions_ = opt_vendor->getOptions();
     }
+    // siaddr
+    config_.siaddr_ = resp->getSiaddr();
+    // sname
+    OptionBuffer buf = resp->getSname();
+    // sname is a fixed length field holding null terminated string. Use
+    // of c_str() guarantess that only a useful portion (ending with null
+    // character) is assigned.
+    config_.sname_.assign(std::string(buf.begin(), buf.end()).c_str());
+    // (boot)file
+    buf = resp->getFile();
+    config_.boot_file_name_.assign(std::string(buf.begin(), buf.end()).c_str());
     // Server Identifier
     OptionCustomPtr opt_serverid = boost::dynamic_pointer_cast<
         OptionCustom>(resp->getOption(DHO_DHCP_SERVER_IDENTIFIER));

+ 6 - 0
src/bin/dhcp4/tests/dhcp4_client.h

@@ -81,6 +81,12 @@ public:
         /// @brief Holds server id of the server which responded to the client's
         /// request.
         asiolink::IOAddress serverid_;
+        /// @brief Holds returned siaddr.
+        asiolink::IOAddress siaddr_;
+        /// @brief Holds returned sname.
+        std::string sname_;
+        /// @brief Holds returned (boot)file.
+        std::string boot_file_name_;
 
         /// @brief Constructor.
         Configuration();

+ 55 - 0
src/bin/dhcp4/tests/dora_unittest.cc

@@ -71,6 +71,15 @@ namespace {
 ///   - The same as configuration 4, but using the following order of
 ///     host-reservation-identifiers: duid, circuit-id, hw-address.
 ///
+/// - Configuration 6:
+///   - This configuration provides reservations for next-server,
+///     server-hostname and boot-file-name value.
+///   - 1 subnet: 10.0.0.0/24
+///   - 1 reservation for this subnet:
+///     - Client's HW address: aa:bb:cc:dd:ee:ff
+///     - next-server = 10.0.0.7
+///     - server name = "some-name.example.org"
+///     - boot-file-name = "bootfile.efi"
 const char* DORA_CONFIGS[] = {
 // Configuration 0
     "{ \"interfaces-config\": {"
@@ -233,6 +242,26 @@ const char* DORA_CONFIGS[] = {
         "       }"
         "    ]"
         "} ]"
+    "}",
+
+// Configuration 6
+    "{ \"interfaces-config\": {"
+        "      \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"valid-lifetime\": 600,"
+        "\"next-server\": \"10.0.0.1\","
+        "\"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\","
+        "         \"next-server\": \"10.0.0.7\","
+        "         \"server-hostname\": \"some-name.example.org\","
+        "         \"boot-file-name\": \"bootfile.efi\""
+        "       }"
+        "    ]"
+        "} ]"
     "}"
 };
 
@@ -989,6 +1018,32 @@ TEST_F(DORATest, changingHWAddress) {
     EXPECT_FALSE(client.config_.lease_.client_id_);
 }
 
+// This test verifies that the server assigns reserved values for the
+// siaddr, sname and file fields carried within DHCPv4 message.
+TEST_F(DORATest, messageFieldsReservations) {
+    // Client has a reservation.
+    Dhcp4Client client(Dhcp4Client::SELECTING);
+    // Set explicit HW address so as it matches the reservation in the
+    // configuration used below.
+    client.setHWAddress("aa:bb:cc:dd:ee:ff");
+    // Configure DHCP server.
+    configure(DORA_CONFIGS[6], *client.getServer());
+    // Client performs 4-way exchange and should obtain a reserved
+    // address and fixed fields.
+    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()));
+
+    // Check that the reserved values have been assigned.
+    EXPECT_EQ("10.0.0.7", client.config_.siaddr_.toText());
+    EXPECT_EQ("some-name.example.org", client.config_.sname_);
+    EXPECT_EQ("bootfile.efi", client.config_.boot_file_name_);
+}
+
 // This test checks the following scenario:
 // 1. Client A performs 4-way exchange and obtains a lease from the dynamic pool.
 // 2. Reservation is created for the client A using an address out of the dynamic

+ 56 - 1
src/bin/dhcp4/tests/inform_unittest.cc

@@ -39,6 +39,16 @@ namespace {
 ///   - Domain Name Server option present: 192.0.2.202, 192.0.2.203.
 ///   - Log Servers option present: 192.0.2.200 and 192.0.2.201
 ///   - Quotes Servers option present: 192.0.2.202, 192.0.2.203.
+///
+/// - Configuration 2:
+///   - This configuration provides reservations for next-server,
+///     server-hostname and boot-file-name value.
+///   - 1 subnet: 192.0.2.0/24
+///   - 1 reservation for this subnet:
+///     - Client's HW address: aa:bb:cc:dd:ee:ff
+///     - next-server = 10.0.0.7
+///     - server name = "some-name.example.org"
+///     - boot-file-name = "bootfile.efi"
 const char* INFORM_CONFIGS[] = {
 // Configuration 0
     "{ \"interfaces-config\": {"
@@ -91,7 +101,26 @@ const char* INFORM_CONFIGS[] = {
         "        \"data\": \"10.0.0.202,10.0.0.203\""
         "    } ]"
         " } ]"
-    "}"
+    "}",
+
+// Configuration 2
+    "{ \"interfaces-config\": {"
+        "      \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"valid-lifetime\": 600,"
+        "\"next-server\": \"10.0.0.1\","
+        "\"subnet4\": [ { "
+        "    \"subnet\": \"192.0.2.0/24\", "
+        "    \"reservations\": [ "
+        "       {"
+        "         \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+        "         \"next-server\": \"10.0.0.7\","
+        "         \"server-hostname\": \"some-name.example.org\","
+        "         \"boot-file-name\": \"bootfile.efi\""
+        "       }"
+        "    ]"
+        "} ]"
+    "}",
 };
 
 /// @brief Test fixture class for testing DHCPINFORM.
@@ -364,6 +393,32 @@ TEST_F(InformTest, relayedClientNoCiaddr) {
     EXPECT_EQ("192.0.2.203", client.config_.dns_servers_[1].toText());
 }
 
+// This test verifies that the server assigns reserved values for the
+// siaddr, sname and file fields carried within DHCPv4 message.
+TEST_F(InformTest, messageFieldsReservations) {
+    // Client has a reservation.
+    Dhcp4Client client(Dhcp4Client::SELECTING);
+    // Message is relayed.
+    client.useRelay();
+    // Set explicit HW address so as it matches the reservation in the
+    // configuration used below.
+    client.setHWAddress("aa:bb:cc:dd:ee:ff");
+    // Configure DHCP server.
+    configure(INFORM_CONFIGS[2], *client.getServer());
+    // Client sends DHCPINFORM and should receive reserved fields.
+    ASSERT_NO_THROW(client.doInform());
+    // 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()));
+
+    // Check that the reserved values have been assigned.
+    EXPECT_EQ("10.0.0.7", client.config_.siaddr_.toText());
+    EXPECT_EQ("some-name.example.org", client.config_.sname_);
+    EXPECT_EQ("bootfile.efi", client.config_.boot_file_name_);
+}
+
 /// This test verifies that after a client completes its INFORM exchange,
 /// appropriate statistics are updated.
 TEST_F(InformTest, statisticsInform) {

+ 5 - 2
src/lib/dhcpsrv/cfg_hosts.cc

@@ -572,10 +572,13 @@ CfgHosts::add4(const HostPtr& host) {
     DuidPtr duid = host->getDuid();
 
     // There should be at least one resource reserved: hostname, IPv4
-    // address, IPv6 address or prefix.
+    // address, siaddr, sname, file or IPv6 address or prefix.
     if (host->getHostname().empty() &&
         (host->getIPv4Reservation().isV4Zero()) &&
-        (!host->hasIPv6Reservation()) &&
+        !host->hasIPv6Reservation() &&
+        host->getNextServer().isV4Zero() &&
+        host->getServerHostname().empty() &&
+        host->getBootFileName().empty() &&
         host->getCfgOption4()->empty() &&
         host->getCfgOption6()->empty()) {
         std::ostringstream s;

+ 69 - 6
src/lib/dhcpsrv/host.cc

@@ -5,6 +5,7 @@
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 #include <config.h>
+#include <dhcp/pkt4.h>
 #include <dhcpsrv/host.h>
 #include <util/encode/hex.h>
 #include <util/strutil.h>
@@ -74,14 +75,20 @@ Host::Host(const uint8_t* identifier, const size_t identifier_len,
            const asiolink::IOAddress& ipv4_reservation,
            const std::string& hostname,
            const std::string& dhcp4_client_classes,
-           const std::string& dhcp6_client_classes)
+           const std::string& dhcp6_client_classes,
+           const asiolink::IOAddress& next_server,
+           const std::string& server_host_name,
+           const std::string& boot_file_name)
+
     : identifier_type_(identifier_type),
       identifier_value_(), ipv4_subnet_id_(ipv4_subnet_id),
       ipv6_subnet_id_(ipv6_subnet_id),
       ipv4_reservation_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
       hostname_(hostname), dhcp4_client_classes_(dhcp4_client_classes),
-      dhcp6_client_classes_(dhcp6_client_classes), host_id_(0),
-      cfg_option4_(new CfgOption()), cfg_option6_(new CfgOption()) {
+      dhcp6_client_classes_(dhcp6_client_classes),
+      next_server_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
+      server_host_name_(server_host_name), boot_file_name_(boot_file_name),
+      host_id_(0), cfg_option4_(new CfgOption()), cfg_option6_(new CfgOption()) {
 
     // Initialize host identifier.
     setIdentifier(identifier, identifier_len, identifier_type);
@@ -90,6 +97,11 @@ Host::Host(const uint8_t* identifier, const size_t identifier_len,
         // Validate and set IPv4 address reservation.
         setIPv4Reservation(ipv4_reservation);
     }
+
+    if (!next_server.isV4Zero()) {
+        // Validate and set next server address.
+        setNextServer(next_server);
+    }
 }
 
 Host::Host(const std::string& identifier, const std::string& identifier_name,
@@ -97,14 +109,19 @@ Host::Host(const std::string& identifier, const std::string& identifier_name,
            const asiolink::IOAddress& ipv4_reservation,
            const std::string& hostname,
            const std::string& dhcp4_client_classes,
-           const std::string& dhcp6_client_classes)
+           const std::string& dhcp6_client_classes,
+           const asiolink::IOAddress& next_server,
+           const std::string& server_host_name,
+           const std::string& boot_file_name)
     : identifier_type_(IDENT_HWADDR),
       identifier_value_(), ipv4_subnet_id_(ipv4_subnet_id),
       ipv6_subnet_id_(ipv6_subnet_id),
       ipv4_reservation_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
       hostname_(hostname), dhcp4_client_classes_(dhcp4_client_classes),
-      dhcp6_client_classes_(dhcp6_client_classes), host_id_(0),
-      cfg_option4_(new CfgOption()), cfg_option6_(new CfgOption()) {
+      dhcp6_client_classes_(dhcp6_client_classes),
+      next_server_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
+      server_host_name_(server_host_name), boot_file_name_(boot_file_name),
+      host_id_(0), cfg_option4_(new CfgOption()), cfg_option6_(new CfgOption()) {
 
     // Initialize host identifier.
     setIdentifier(identifier, identifier_name);
@@ -113,6 +130,11 @@ Host::Host(const std::string& identifier, const std::string& identifier_name,
         // Validate and set IPv4 address reservation.
         setIPv4Reservation(ipv4_reservation);
     }
+
+    if (!next_server.isV4Zero()) {
+        // Validate and set next server address.
+        setNextServer(next_server);
+    }
 }
 
 const std::vector<uint8_t>&
@@ -339,6 +361,37 @@ Host::addClientClassInternal(ClientClasses& classes,
     }
 }
 
+void
+Host::setNextServer(const asiolink::IOAddress& next_server) {
+    if (!next_server.isV4()) {
+        isc_throw(isc::BadValue, "next server address '" << next_server
+                  << "' is not a valid IPv4 address");
+    } else if (next_server.isV4Bcast()) {
+        isc_throw(isc::BadValue, "invalid next server address '"
+                  << next_server << "'");
+    }
+
+    next_server_ = next_server;
+}
+
+void
+Host::setServerHostname(const std::string& server_host_name) {
+    if (server_host_name.size() > Pkt4::MAX_SNAME_LEN - 1) {
+        isc_throw(isc::BadValue, "server hostname length must not exceed "
+                  << (Pkt4::MAX_SNAME_LEN - 1));
+    }
+    server_host_name_ = server_host_name;
+}
+
+void
+Host::setBootFileName(const std::string& boot_file_name) {
+    if (boot_file_name.size() > Pkt4::MAX_FILE_LEN - 1) {
+        isc_throw(isc::BadValue, "boot file length must not exceed "
+                  << (Pkt4::MAX_FILE_LEN - 1));
+    }
+    boot_file_name_ = boot_file_name;
+}
+
 std::string
 Host::toText() const {
     std::ostringstream s;
@@ -363,6 +416,16 @@ Host::toText() const {
     s << " ipv4_reservation=" << (ipv4_reservation_.isV4Zero() ? "(no)" :
                                   ipv4_reservation_.toText());
 
+    // Add next server.
+    s << " siaddr=" << (next_server_.isV4Zero() ? "(no)" :
+                             next_server_.toText());
+
+    // Add server host name.
+    s << " sname=" << (server_host_name_.empty() ? "(empty)" : server_host_name_);
+
+    // Add boot file name.
+    s << " file=" << (boot_file_name_.empty() ? "(empty)" : boot_file_name_);
+
     if (ipv6_reservations_.empty()) {
         s << " ipv6_reservations=(none)";
 

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

@@ -214,6 +214,9 @@ public:
     /// separated by commas. The names get trimmed by this constructor.
     /// @param dhcp6_client_classes A string holding DHCPv6 client class names
     /// separated by commas. The names get trimmed by this constructor.
+    /// @param next_server IPv4 address of next server (siaddr).
+    /// @param server_host_name Server host name (a.k.a. sname).
+    /// @param boot_file_name Boot file name (a.k.a. file).
     ///
     /// @throw BadValue if the provided values are invalid. In particular,
     /// if the identifier is invalid.
@@ -223,7 +226,10 @@ public:
          const asiolink::IOAddress& ipv4_reservation,
          const std::string& hostname = "",
          const std::string& dhcp4_client_classes = "",
-         const std::string& dhcp6_client_classes = "");
+         const std::string& dhcp6_client_classes = "",
+         const asiolink::IOAddress& next_server = asiolink::IOAddress::IPV4_ZERO_ADDRESS(),
+         const std::string& server_host_name = "",
+         const std::string& boot_file_name = "");
 
     /// @brief Constructor.
     ///
@@ -258,6 +264,9 @@ public:
     /// separated by commas. The names get trimmed by this constructor.
     /// @param dhcp6_client_classes A string holding DHCPv6 client class names
     /// separated by commas. The names get trimmed by this constructor.
+    /// @param next_server IPv4 address of next server (siaddr).
+    /// @param server_host_name Server host name (a.k.a. sname).
+    /// @param boot_file_name Boot file name (a.k.a. file).
     ///
     /// @throw BadValue if the provided values are invalid. In particular,
     /// if the identifier is invalid.
@@ -266,7 +275,10 @@ public:
          const asiolink::IOAddress& ipv4_reservation,
          const std::string& hostname = "",
          const std::string& dhcp4_client_classes = "",
-         const std::string& dhcp6_client_classes = "");
+         const std::string& dhcp6_client_classes = "",
+         const asiolink::IOAddress& next_server = asiolink::IOAddress::IPV4_ZERO_ADDRESS(),
+         const std::string& server_host_name = "",
+         const std::string& boot_file_name = "");
 
     /// @brief Replaces currently used identifier with a new identifier.
     ///
@@ -449,6 +461,43 @@ public:
         return (dhcp6_client_classes_);
     }
 
+    /// @brief Sets new value for next server field (siaddr).
+    ///
+    /// @param next_server New address of a next server.
+    ///
+    /// @throw isc::BadValue if the provided address is not an IPv4 address,
+    /// is broadcast address.
+    void setNextServer(const asiolink::IOAddress& next_server);
+
+    /// @brief Returns value of next server field (siaddr).
+    const asiolink::IOAddress& getNextServer() const {
+        return (next_server_);
+    }
+
+    /// @brief Sets new value for server hostname (sname).
+    ///
+    /// @param server_host_name New value for server hostname.
+    ///
+    /// @throw BadValue if hostname is longer than 63 bytes.
+    void setServerHostname(const std::string& server_host_name);
+
+    /// @brief Returns value of server hostname (sname).
+    const std::string& getServerHostname() const {
+        return (server_host_name_);
+    }
+
+    /// @brief Sets new value for boot file name (file).
+    ///
+    /// @param boot_file_name New value of boot file name.
+    ///
+    /// @throw BadValue if boot file name is longer than 128 bytes.
+    void setBootFileName(const std::string& boot_file_name);
+
+    /// @brief Returns value of boot file name (file).
+    const std::string& getBootFileName() const {
+        return (boot_file_name_);
+    }
+
     /// @brief Returns pointer to the DHCPv4 option data configuration for
     /// this host.
     ///
@@ -527,6 +576,12 @@ private:
     ClientClasses dhcp4_client_classes_;
     /// @brief Collection of classes associated with a DHCPv6 client.
     ClientClasses dhcp6_client_classes_;
+    /// @brief Next server (a.k.a. siaddr, carried in DHCPv4 message).
+    asiolink::IOAddress next_server_;
+    /// @brief Server host name (a.k.a. sname, carried in DHCPv4 message).
+    std::string server_host_name_;
+    /// @brief Boot file name (a.k.a. file, carried in DHCPv4 message)
+    std::string boot_file_name_;
 
     /// @brief HostID (a unique identifier assigned when the host is stored in
     ///                MySQL or Pgsql)

+ 127 - 6
src/lib/dhcpsrv/mysql_host_data_source.cc

@@ -63,6 +63,12 @@ const size_t OPTION_FORMATTED_VALUE_MAX_LEN = 8192;
 /// @brief Maximum length of option space name.
 const size_t OPTION_SPACE_MAX_LEN = 128;
 
+/// @brief Maximum length of the server hostname.
+const size_t SERVER_HOSTNAME_MAX_LEN = 64;
+
+/// @brief Maximum length of the boot file name.
+const size_t BOOT_FILE_NAME_MAX_LEN = 128;
+
 /// @brief Numeric value representing last supported identifier.
 ///
 /// This value is used to validate whether the identifier type stored in
@@ -81,7 +87,7 @@ class MySqlHostExchange {
 private:
 
     /// @brief Number of columns returned for SELECT queries send by this class.
-    static const size_t HOST_COLUMNS = 9;
+    static const size_t HOST_COLUMNS = 12;
 
 public:
 
@@ -99,17 +105,25 @@ public:
           dhcp4_subnet_id_(0), dhcp6_subnet_id_(0), ipv4_address_(0),
           hostname_length_(0), dhcp4_client_classes_length_(0),
           dhcp6_client_classes_length_(0),
+          dhcp4_next_server_(0),
+          dhcp4_server_hostname_length_(0),
+          dhcp4_boot_file_name_length_(0),
           dhcp4_subnet_id_null_(MLM_FALSE),
           dhcp6_subnet_id_null_(MLM_FALSE),
           ipv4_address_null_(MLM_FALSE), hostname_null_(MLM_FALSE),
           dhcp4_client_classes_null_(MLM_FALSE),
-          dhcp6_client_classes_null_(MLM_FALSE) {
+          dhcp6_client_classes_null_(MLM_FALSE),
+          dhcp4_next_server_null_(MLM_FALSE),
+          dhcp4_server_hostname_null_(MLM_FALSE),
+          dhcp4_boot_file_name_null_(MLM_FALSE) {
 
         // Fill arrays with 0 so as they don't include any garbage.
         memset(dhcp_identifier_buffer_, 0, sizeof(dhcp_identifier_buffer_));
         memset(hostname_, 0, sizeof(hostname_));
         memset(dhcp4_client_classes_, 0, sizeof(dhcp4_client_classes_));
         memset(dhcp6_client_classes_, 0, sizeof(dhcp6_client_classes_));
+        memset(dhcp4_server_hostname_, 0, sizeof(dhcp4_server_hostname_));
+        memset(dhcp4_boot_file_name_, 0, sizeof(dhcp4_boot_file_name_));
 
         // Set the column names for use by this class. This only comprises
         // names used by the MySqlHostExchange class. Derived classes will
@@ -123,8 +137,11 @@ public:
         columns_[6] = "hostname";
         columns_[7] = "dhcp4_client_classes";
         columns_[8] = "dhcp6_client_classes";
+        columns_[9] = "dhcp4_next_server";
+        columns_[10] = "dhcp4_server_hostname";
+        columns_[11] = "dhcp4_boot_file_name";
 
-        BOOST_STATIC_ASSERT(8 < HOST_COLUMNS);
+        BOOST_STATIC_ASSERT(11 < HOST_COLUMNS);
     };
 
     /// @brief Virtual destructor.
@@ -307,6 +324,32 @@ public:
             bind_[8].buffer = dhcp6_client_classes_;
             bind_[8].buffer_length = classes6_txt.length();
 
+            // ipv4_address : INT UNSIGNED NULL
+            // The address in the Host structure is an IOAddress object.  Convert
+            // this to an integer for storage.
+            dhcp4_next_server_ = static_cast<uint32_t>(host->getNextServer());
+            bind_[9].buffer_type = MYSQL_TYPE_LONG;
+            bind_[9].buffer = reinterpret_cast<char*>(&dhcp4_next_server_);
+            bind_[9].is_unsigned = MLM_TRUE;
+            // bind_[9].is_null = &MLM_FALSE; // commented out for performance
+                                              // reasons, see memset() above
+
+            // dhcp4_server_hostname
+            bind_[10].buffer_type = MYSQL_TYPE_STRING;
+            std::string server_hostname = host->getServerHostname();
+            strncpy(dhcp4_server_hostname_, server_hostname.c_str(),
+                    SERVER_HOSTNAME_MAX_LEN - 1);
+            bind_[10].buffer = dhcp4_server_hostname_;
+            bind_[10].buffer_length = server_hostname.length();
+
+            // dhcp4_boot_file_name
+            bind_[11].buffer_type = MYSQL_TYPE_STRING;
+            std::string boot_file_name = host->getBootFileName();
+            strncpy(dhcp4_boot_file_name_, boot_file_name.c_str(),
+                    BOOT_FILE_NAME_MAX_LEN - 1);
+            bind_[11].buffer = dhcp4_boot_file_name_;
+            bind_[11].buffer_length = boot_file_name.length();
+
         } catch (const std::exception& ex) {
             isc_throw(DbOperationError,
                       "Could not create bind array from Host: "
@@ -399,6 +442,31 @@ public:
         bind_[8].length = &dhcp6_client_classes_length_;
         bind_[8].is_null = &dhcp6_client_classes_null_;
 
+        // dhcp4_next_server
+        dhcp4_next_server_null_ = MLM_FALSE;
+        bind_[9].buffer_type = MYSQL_TYPE_LONG;
+        bind_[9].buffer = reinterpret_cast<char*>(&dhcp4_next_server_);
+        bind_[9].is_unsigned = MLM_TRUE;
+        bind_[9].is_null = &dhcp4_next_server_null_;
+
+        // dhcp4_server_hostname
+        dhcp4_server_hostname_null_ = MLM_FALSE;
+        dhcp4_server_hostname_length_ = sizeof(dhcp4_server_hostname_);
+        bind_[10].buffer_type = MYSQL_TYPE_STRING;
+        bind_[10].buffer = reinterpret_cast<char*>(dhcp4_server_hostname_);
+        bind_[10].buffer_length = dhcp4_server_hostname_length_;
+        bind_[10].length = &dhcp4_server_hostname_length_;
+        bind_[10].is_null = &dhcp4_server_hostname_null_;
+
+        // dhcp4_boot_file_name
+        dhcp4_boot_file_name_null_ = MLM_FALSE;
+        dhcp4_boot_file_name_length_ = sizeof(dhcp4_boot_file_name_);
+        bind_[11].buffer_type = MYSQL_TYPE_STRING;
+        bind_[11].buffer = reinterpret_cast<char*>(dhcp4_boot_file_name_);
+        bind_[11].buffer_length = dhcp4_boot_file_name_length_;
+        bind_[11].length = &dhcp4_boot_file_name_length_;
+        bind_[11].is_null = &dhcp4_boot_file_name_null_;
+
         // Add the error flags
         setErrorIndicators(bind_, error_);
 
@@ -468,10 +536,32 @@ public:
                                                dhcp6_client_classes_length_);
         }
 
+        // Set next server value (siaddr) if non NULL value returned.
+        asiolink::IOAddress next_server = asiolink::IOAddress::IPV4_ZERO_ADDRESS();
+        if (dhcp4_next_server_null_ == MLM_FALSE) {
+            next_server = asiolink::IOAddress(dhcp4_next_server_);
+        }
+
+        // Set server hostname (sname) if non NULL value returned.
+        std::string dhcp4_server_hostname;
+        if (dhcp4_server_hostname_null_ == MLM_FALSE) {
+            dhcp4_server_hostname = std::string(dhcp4_server_hostname_,
+                                                dhcp4_server_hostname_length_);
+        }
+
+        // Set boot file name (file) if non NULL value returned.
+        std::string dhcp4_boot_file_name;
+        if (dhcp4_boot_file_name_null_ == MLM_FALSE) {
+            dhcp4_boot_file_name = std::string(dhcp4_boot_file_name_,
+                                               dhcp4_boot_file_name_length_);
+        }
+
         // Create and return Host object from the data gathered.
         HostPtr h(new Host(dhcp_identifier_buffer_, dhcp_identifier_length_,
                            type, ipv4_subnet_id, ipv6_subnet_id, ipv4_reservation,
-                           hostname, dhcp4_client_classes, dhcp6_client_classes));
+                           hostname, dhcp4_client_classes, dhcp6_client_classes,
+                           next_server, dhcp4_server_hostname,
+                           dhcp4_boot_file_name));
         h->setHostId(host_id_);
 
         return (h);
@@ -580,6 +670,21 @@ private:
     /// client classes.
     unsigned long dhcp6_client_classes_length_;
 
+    /// Next server address (siaddr).
+    uint32_t dhcp4_next_server_;
+
+    /// Server hostname (sname).
+    char dhcp4_server_hostname_[SERVER_HOSTNAME_MAX_LEN];
+
+    /// A length of the string holding server hostname.
+    unsigned long dhcp4_server_hostname_length_;
+
+    /// Boot file name (file).
+    char dhcp4_boot_file_name_[BOOT_FILE_NAME_MAX_LEN];
+
+    /// A length of the string holding boot file name.
+    unsigned long dhcp4_boot_file_name_length_;
+
     /// @name Boolean values indicating if values of specific columns in
     /// the database are NULL.
     //@{
@@ -603,6 +708,15 @@ private:
     /// NULL.
     my_bool dhcp6_client_classes_null_;
 
+    /// Boolean flag indicating if the value of next server is NULL.
+    my_bool dhcp4_next_server_null_;
+
+    /// Boolean flag indicating if the value of server hostname is NULL.
+    my_bool dhcp4_server_hostname_null_;
+
+    /// Boolean flag indicating if the value of boot file name is NULL.
+    my_bool dhcp4_boot_file_name_null_;
+
     //@}
 
 };
@@ -1817,6 +1931,7 @@ TaggedStatementArray tagged_statements = { {
             "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
                 "h.dhcp4_subnet_id, h.dhcp6_subnet_id, h.ipv4_address, "
                 "h.hostname, h.dhcp4_client_classes, h.dhcp6_client_classes, "
+                "h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
                 "o4.option_id, o4.code, o4.value, o4.formatted_value, o4.space, "
                 "o4.persistent, "
                 "o6.option_id, o6.code, o6.value, o6.formatted_value, o6.space, "
@@ -1840,6 +1955,7 @@ TaggedStatementArray tagged_statements = { {
             "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
                 "h.dhcp4_subnet_id, h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
                 "h.dhcp4_client_classes, h.dhcp6_client_classes, "
+                "h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
                 "o.option_id, o.code, o.value, o.formatted_value, o.space, "
                 "o.persistent "
             "FROM hosts AS h "
@@ -1855,6 +1971,7 @@ TaggedStatementArray tagged_statements = { {
             "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
                 "h.dhcp4_subnet_id, h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
                 "h.dhcp4_client_classes, h.dhcp6_client_classes, "
+                "h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
                 "o.option_id, o.code, o.value, o.formatted_value, o.space, "
                 "o.persistent "
             "FROM hosts AS h "
@@ -1872,6 +1989,7 @@ TaggedStatementArray tagged_statements = { {
                 "h.dhcp_identifier_type, h.dhcp4_subnet_id, "
                 "h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
                 "h.dhcp4_client_classes, h.dhcp6_client_classes, "
+                "h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
                 "o.option_id, o.code, o.value, o.formatted_value, o.space, "
                 "o.persistent, "
                 "r.reservation_id, r.address, r.prefix_len, r.type, "
@@ -1893,6 +2011,7 @@ TaggedStatementArray tagged_statements = { {
             "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
                 "h.dhcp4_subnet_id, h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
                 "h.dhcp4_client_classes, h.dhcp6_client_classes, "
+                "h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
                 "o.option_id, o.code, o.value, o.formatted_value, o.space, "
                 "o.persistent "
             "FROM hosts AS h "
@@ -1912,6 +2031,7 @@ TaggedStatementArray tagged_statements = { {
                 "h.dhcp_identifier_type, h.dhcp4_subnet_id, "
                 "h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
                 "h.dhcp4_client_classes, h.dhcp6_client_classes, "
+                "h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
                 "o.option_id, o.code, o.value, o.formatted_value, o.space, "
                 "o.persistent, "
                 "r.reservation_id, r.address, r.prefix_len, r.type, "
@@ -1934,8 +2054,9 @@ TaggedStatementArray tagged_statements = { {
     {MySqlHostDataSourceImpl::INSERT_HOST,
          "INSERT INTO hosts(host_id, dhcp_identifier, dhcp_identifier_type, "
             "dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, "
-            "dhcp4_client_classes, dhcp6_client_classes) "
-         "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"},
+            "dhcp4_client_classes, dhcp6_client_classes, dhcp4_next_server, "
+            "dhcp4_server_hostname, dhcp4_boot_file_name) "
+         "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"},
 
     // Inserts a single IPv6 reservation into 'reservations' table.
     {MySqlHostDataSourceImpl::INSERT_V6_RESRV,

+ 12 - 1
src/lib/dhcpsrv/parsers/host_reservation_parser.cc

@@ -49,6 +49,9 @@ getSupportedParams4(const bool identifiers_only = false) {
         params_set.insert("hostname");
         params_set.insert("ip-address");
         params_set.insert("option-data");
+        params_set.insert("next-server");
+        params_set.insert("server-hostname");
+        params_set.insert("boot-file-name");
     }
     return (identifiers_only ? identifiers_set : params_set);
 }
@@ -120,7 +123,6 @@ HostReservationParser::build(isc::data::ConstElementPtr reservation_data) {
 
             } else if (element.first == "hostname") {
                 hostname = element.second->stringValue();
-
             }
         } catch (const std::exception& ex) {
             // Append line number where the error occurred.
@@ -207,7 +209,16 @@ HostReservationParser4::build(isc::data::ConstElementPtr reservation_data) {
                 if (element.first == "ip-address") {
                     host_->setIPv4Reservation(IOAddress(element.second->
                                                         stringValue()));
+                } else if (element.first == "next-server") {
+                host_->setNextServer(IOAddress(element.second->stringValue()));
+
+                } else if (element.first == "server-hostname") {
+                    host_->setServerHostname(element.second->stringValue());
+
+                } else if (element.first == "boot-file-name") {
+                    host_->setBootFileName(element.second->stringValue());
                 }
+
             } catch (const std::exception& ex) {
                 // Append line number where the error occurred.
                 isc_throw(DhcpConfigError, ex.what() << " ("

+ 55 - 17
src/lib/dhcpsrv/pgsql_host_data_source.cc

@@ -73,8 +73,11 @@ private:
     static const int HOSTNAME_COL = 6;
     static const int DHCP4_CLIENT_CLASSES_COL = 7;
     static const int DHCP6_CLIENT_CLASSES_COL = 8;
+    static const int DHCP4_NEXT_SERVER_COL = 9;
+    static const int DHCP4_SERVER_HOSTNAME_COL = 10;
+    static const int DHCP4_BOOT_FILE_NAME_COL = 11;
     /// @brief Number of columns returned for SELECT queries send by this class.
-    static const size_t HOST_COLUMNS = 9;
+    static const size_t HOST_COLUMNS = 12;
 
 public:
 
@@ -99,8 +102,11 @@ public:
         columns_[HOSTNAME_COL] = "hostname";
         columns_[DHCP4_CLIENT_CLASSES_COL] = "dhcp4_client_classes";
         columns_[DHCP6_CLIENT_CLASSES_COL] = "dhcp6_client_classes";
+        columns_[DHCP4_NEXT_SERVER_COL] = "dhcp4_next_server";
+        columns_[DHCP4_SERVER_HOSTNAME_COL] = "dhcp4_server_hostname";
+        columns_[DHCP4_BOOT_FILE_NAME_COL] = "dhcp4_boot_file_name";
 
-        BOOST_STATIC_ASSERT(8 < HOST_COLUMNS);
+        BOOST_STATIC_ASSERT(11 < HOST_COLUMNS);
     };
 
     /// @brief Virtual destructor.
@@ -186,7 +192,7 @@ public:
             bind_array->add(host->getIPv6SubnetID());
 
             // ipv4_address : BIGINT NULL
-            bind_array->add(host->getIPv4Reservation());
+            bind_array->add((host->getIPv4Reservation()));
 
             // hostname : VARCHAR(255) NULL
             bind_array->add(host->getHostname());
@@ -197,6 +203,16 @@ public:
 
             // dhcp6_client_classes : VARCHAR(255) NULL
             bind_array->addTempString(host->getClientClasses6().toText(","));
+
+            // dhcp4_next_server : BIGINT NULL
+            bind_array->add((host->getNextServer()));
+
+            // dhcp4_server_hostname : VARCHAR(64)
+            bind_array->add(host->getServerHostname());
+
+            // dhcp4_boot_file_name : VARCHAR(128)
+            bind_array->add(host->getBootFileName());
+
         } catch (const std::exception& ex) {
             host_.reset();
             isc_throw(DbOperationError,
@@ -221,7 +237,7 @@ public:
     /// @param [out] hosts Collection of hosts to which a new host created
     ///        from the processed data should be inserted.
     virtual void processRowData(ConstHostCollection& hosts,
-                                    const PgSqlResult& r, int row) {
+                                const PgSqlResult& r, int row) {
         // Peek at the host id , so we can skip it if we already have it
         // This lets us avoid constructing a copy of host for each
         // of its sub-rows (options, etc...)
@@ -247,7 +263,7 @@ public:
     /// @return HostPtr to the newly created Host object
     /// @throw DbOperationError if the host cannot be created.
     HostPtr retrieveHost(const PgSqlResult& r, int row,
-        const HostID& peeked_host_id = 0) {
+                         const HostID& peeked_host_id = 0) {
 
         // If the caller peeked ahead at the host_id use that, otherwise
         // read it from the row.
@@ -296,13 +312,28 @@ public:
         std::string dhcp6_client_classes;
         getColumnValue(r, row, DHCP6_CLIENT_CLASSES_COL, dhcp6_client_classes);
 
+        // dhcp4_next_server : BIGINT NULL
+        uint32_t dhcp4_next_server_as_uint32;
+        getColumnValue(r, row, DHCP4_NEXT_SERVER_COL, dhcp4_next_server_as_uint32);
+        isc::asiolink::IOAddress dhcp4_next_server(dhcp4_next_server_as_uint32);
+
+        // dhcp4_server_hostname : VARCHAR(64)
+        std::string dhcp4_server_hostname;
+        getColumnValue(r, row, DHCP4_SERVER_HOSTNAME_COL, dhcp4_server_hostname);
+
+        // dhcp4_boot_file_name : VARCHAR(128)
+        std::string dhcp4_boot_file_name;
+        getColumnValue(r, row, DHCP4_BOOT_FILE_NAME_COL, dhcp4_boot_file_name);
+
         // Finally, attempt to create the new host.
         HostPtr host;
         try {
             host.reset(new Host(identifier_value, identifier_len,
                                 identifier_type, dhcp4_subnet_id,
                                 dhcp6_subnet_id, ipv4_reservation, hostname,
-                                dhcp4_client_classes, dhcp6_client_classes));
+                                dhcp4_client_classes, dhcp6_client_classes,
+                                dhcp4_next_server, dhcp4_server_hostname,
+                                dhcp4_boot_file_name));
 
             host->setHostId(host_id);
         } catch (const isc::Exception& ex) {
@@ -1302,6 +1333,7 @@ TaggedStatementArray tagged_statements = { {
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
      "  h.dhcp4_subnet_id, h.dhcp6_subnet_id, h.ipv4_address, "
      "  h.hostname, h.dhcp4_client_classes, h.dhcp6_client_classes, "
+     "  h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
      "  o4.option_id, o4.code, o4.value, o4.formatted_value, o4.space, "
      "  o4.persistent, "
      "  o6.option_id, o6.code, o6.value, o6.formatted_value, o6.space, "
@@ -1323,8 +1355,9 @@ TaggedStatementArray tagged_statements = { {
      { OID_INT8 }, "get_host_addr",
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
      "  h.dhcp4_subnet_id, h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
-     "  h.dhcp4_client_classes, h.dhcp6_client_classes, o.option_id, o.code, "
-     "  o.value, o.formatted_value, o.space, o.persistent "
+     "  h.dhcp4_client_classes, h.dhcp6_client_classes, "
+     "  h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
+     "  o.option_id, o.code, o.value, o.formatted_value, o.space, o.persistent "
      "FROM hosts AS h "
      "LEFT JOIN dhcp4_options AS o ON h.host_id = o.host_id "
      "WHERE ipv4_address = $1 "
@@ -1340,8 +1373,9 @@ TaggedStatementArray tagged_statements = { {
      "get_host_subid4_dhcpid",
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
      "  h.dhcp4_subnet_id, h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
-     "  h.dhcp4_client_classes, h.dhcp6_client_classes, o.option_id, o.code, "
-     "  o.value, o.formatted_value, o.space, o.persistent "
+     "  h.dhcp4_client_classes, h.dhcp6_client_classes, "
+     "  h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
+     "  o.option_id, o.code, o.value, o.formatted_value, o.space, o.persistent "
      "FROM hosts AS h "
      "LEFT JOIN dhcp4_options AS o ON h.host_id = o.host_id "
      "WHERE h.dhcp4_subnet_id = $1 AND h.dhcp_identifier_type = $2 "
@@ -1349,7 +1383,7 @@ TaggedStatementArray tagged_statements = { {
      "ORDER BY h.host_id, o.option_id"
     },
 
-    //PgSqlHostDataSourceImpl::GET_HOST_SUBID6_DHCPID
+    // PgSqlHostDataSourceImpl::GET_HOST_SUBID6_DHCPID
     // Retrieves host information, IPv6 reservations and DHCPv6 options
     // associated with a host. The number of rows returned is a multiplication
     // of number of IPv6 reservations and DHCPv6 options.
@@ -1360,6 +1394,7 @@ TaggedStatementArray tagged_statements = { {
      "  h.dhcp_identifier_type, h.dhcp4_subnet_id, "
      "  h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
      "  h.dhcp4_client_classes, h.dhcp6_client_classes, "
+     "  h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
      "  o.option_id, o.code, o.value, o.formatted_value, o.space, "
      "  o.persistent, "
      "  r.reservation_id, r.address, r.prefix_len, r.type, r.dhcp6_iaid "
@@ -1381,8 +1416,9 @@ TaggedStatementArray tagged_statements = { {
      "get_host_subid_addr",
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
      "  h.dhcp4_subnet_id, h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
-     "  h.dhcp4_client_classes, h.dhcp6_client_classes, o.option_id, o.code, "
-     "  o.value, o.formatted_value, o.space, o.persistent "
+     "  h.dhcp4_client_classes, h.dhcp6_client_classes, "
+     "  h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
+     "  o.option_id, o.code, o.value, o.formatted_value, o.space, o.persistent "
      "FROM hosts AS h "
      "LEFT JOIN dhcp4_options AS o ON h.host_id = o.host_id "
      "WHERE h.dhcp4_subnet_id = $1 AND h.ipv4_address = $2 "
@@ -1403,6 +1439,7 @@ TaggedStatementArray tagged_statements = { {
      "  h.dhcp_identifier_type, h.dhcp4_subnet_id, "
      "  h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
      "  h.dhcp4_client_classes, h.dhcp6_client_classes, "
+     "  h.dhcp4_next_server, h.dhcp4_server_hostname, h.dhcp4_boot_file_name, "
      "  o.option_id, o.code, o.value, o.formatted_value, o.space, "
      "  o.persistent, "
      "  r.reservation_id, r.address, r.prefix_len, r.type, "
@@ -1416,7 +1453,7 @@ TaggedStatementArray tagged_statements = { {
      "ORDER BY h.host_id, o.option_id, r.reservation_id"
     },
 
-    //PgSqlHostDataSourceImpl::GET_VERSION
+    // PgSqlHostDataSourceImpl::GET_VERSION
     // Retrieves MySQL schema version.
     {0,
      { OID_NONE },
@@ -1426,15 +1463,16 @@ TaggedStatementArray tagged_statements = { {
 
     // PgSqlHostDataSourceImpl::INSERT_HOST
     // Inserts a host into the 'hosts' table. Returns the inserted host id.
-    {8,
+    {11, 
      { OID_BYTEA, OID_INT2,
        OID_INT4, OID_INT4, OID_INT8, OID_VARCHAR,
        OID_VARCHAR, OID_VARCHAR },
      "insert_host",
      "INSERT INTO hosts(dhcp_identifier, dhcp_identifier_type, "
      "  dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, "
-     "  dhcp4_client_classes, dhcp6_client_classes) "
-     "VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING host_id"
+     "  dhcp4_client_classes, dhcp6_client_classes, "
+     "  dhcp4_next_server, dhcp4_server_hostname, dhcp4_boot_file_name) "
+     "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING host_id"
     },
 
     //PgSqlHostDataSourceImpl::INSERT_V6_RESRV

+ 64 - 0
src/lib/dhcpsrv/tests/generic_host_data_source_unittest.cc

@@ -242,6 +242,9 @@ void GenericHostDataSourceTest::compareHosts(const ConstHostPtr& host1,
     EXPECT_EQ(host1->getIPv6SubnetID(), host2->getIPv6SubnetID());
     EXPECT_EQ(host1->getIPv4Reservation(), host2->getIPv4Reservation());
     EXPECT_EQ(host1->getHostname(), host2->getHostname());
+    EXPECT_EQ(host1->getNextServer(), host2->getNextServer());
+    EXPECT_EQ(host1->getServerHostname(), host2->getServerHostname());
+    EXPECT_EQ(host1->getBootFileName(), host2->getBootFileName());
 
     // Compare IPv6 reservations
     compareReservations6(host1->getIPv6Reservations(),
@@ -1360,6 +1363,67 @@ GenericHostDataSourceTest::testMultipleClientClassesBoth() {
     ASSERT_NO_FATAL_FAILURE(compareHosts(host, from_hds));
 }
 
+void
+GenericHostDataSourceTest::testMessageFields4() {
+    ASSERT_TRUE(hdsptr_);
+
+    // Create the Host object.
+    HostPtr host = initializeHost4("192.0.2.5", Host::IDENT_HWADDR);
+    // And assign values for DHCPv4 message fields.
+    ASSERT_NO_THROW({
+        host->setNextServer(IOAddress("10.1.1.1"));
+        host->setServerHostname("server-name.example.org");
+        host->setBootFileName("bootfile.efi");
+    });
+
+    // Add the host.
+    ASSERT_NO_THROW(hdsptr_->add(host));
+
+    // Subnet id will be used in quries to the database.
+    SubnetID subnet_id = host->getIPv4SubnetID();
+
+    // Fetch the host via:
+    // getAll(const HWAddrPtr& hwaddr, const DuidPtr& duid = DuidPtr()) const;
+    ConstHostCollection hosts_by_id = hdsptr_->getAll(host->getHWAddress());
+    ASSERT_EQ(1, hosts_by_id.size());
+    ASSERT_NO_FATAL_FAILURE(compareHosts(host, *hosts_by_id.begin()));
+
+    // Fetch the host via:
+    // getAll(const Host::IdentifierType, const uint8_t* identifier_begin,
+    //       const size_t identifier_len) const;
+    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()));
+
+    // Fetch the host via
+    // getAll4(const asiolink::IOAddress& address) const;
+    hosts_by_id = hdsptr_->getAll4(IOAddress("192.0.2.5"));
+    ASSERT_EQ(1, hosts_by_id.size());
+    ASSERT_NO_FATAL_FAILURE(compareHosts(host, *hosts_by_id.begin()));
+
+    // Fetch the host via
+    // get4(const SubnetID& subnet_id, const HWAddrPtr& hwaddr,
+    //     const DuidPtr& duid = DuidPtr()) const;
+    ConstHostPtr from_hds = hdsptr_->get4(subnet_id, host->getHWAddress());
+    ASSERT_TRUE(from_hds);
+    ASSERT_NO_FATAL_FAILURE(compareHosts(host, from_hds));
+
+    // Fetch the host via
+    // get4(const SubnetID& subnet_id, const Host::IdentifierType& identifier_type,
+    //     const uint8_t* identifier_begin, const size_t identifier_len) const;
+    from_hds = hdsptr_->get4(subnet_id, host->getIdentifierType(), &host->getIdentifier()[0],
+                             host->getIdentifier().size());
+    ASSERT_TRUE(from_hds);
+    ASSERT_NO_FATAL_FAILURE(compareHosts(host, from_hds));
+
+    // Fetch the host via:
+    // get4(const SubnetID& subnet_id, const asiolink::IOAddress& address) const;
+    from_hds = hdsptr_->get4(subnet_id, IOAddress("192.0.2.5"));
+    ASSERT_TRUE(from_hds);
+    ASSERT_NO_FATAL_FAILURE(compareHosts(host, from_hds));
+}
+
 }; // namespace test
 }; // namespace dhcp
 }; // namespace isc

+ 7 - 0
src/lib/dhcpsrv/tests/generic_host_data_source_unittest.h

@@ -513,6 +513,13 @@ public:
     ///
     void testMultipleClientClassesBoth();
 
+    /// @brief Test that siaddr, sname, file fields can be retrieved
+    /// from a database for a host.
+    ///
+    /// Uses gtest macros to report failures.
+    ///
+    void testMessageFields4();
+
     /// @brief Returns DUID with identical content as specified HW address
     ///
     /// This method does not have any sense in real life and is only useful

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

@@ -20,6 +20,7 @@
 #include <boost/pointer_cast.hpp>
 #include <gtest/gtest.h>
 #include <iterator>
+#include <sstream>
 #include <string>
 #include <vector>
 
@@ -299,6 +300,71 @@ TEST_F(HostReservationParserTest, dhcp4NoHostname) {
     EXPECT_TRUE(hosts[0]->getHostname().empty());
 }
 
+// This test verifies that the parser can parse reservation entry
+// containing next-server, server-hostname and boot-file-name values for
+// DHCPv4 message fields.
+TEST_F(HostReservationParserTest, dhcp4MessageFields) {
+    std::string config = "{ \"hw-address\": \"1:2:3:4:5:6\","
+        "\"next-server\": \"192.0.2.11\","
+        "\"server-hostname\": \"some-name.example.org\","
+        "\"boot-file-name\": \"/tmp/some-file.efi\" }";
+
+    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(Host::IDENT_HWADDR,
+                                              &hwaddr_->hwaddr_[0],
+                                              hwaddr_->hwaddr_.size()));
+
+    ASSERT_EQ(1, hosts.size());
+
+    EXPECT_EQ(10, hosts[0]->getIPv4SubnetID());
+    EXPECT_EQ("192.0.2.11", hosts[0]->getNextServer().toText());
+    EXPECT_EQ("some-name.example.org", hosts[0]->getServerHostname());
+    EXPECT_EQ("/tmp/some-file.efi", hosts[0]->getBootFileName());
+}
+
+// This test verifies that the invalid value of the next server is rejected.
+TEST_F(HostReservationParserTest, invalidNextServer) {
+    // Invalid IPv4 address.
+    std::string config = "{ \"hw-address\": \"1:2:3:4:5:6\","
+        "\"next-server\": \"192.0.2.foo\" }";
+    testInvalidConfig<HostReservationParser4>(config);
+
+    // Broadcast address.
+    config = "{ \"hw-address\": \"1:2:3:4:5:6\","
+        "\"next-server\": \"255.255.255.255\" }";
+    testInvalidConfig<HostReservationParser4>(config);
+
+    // IPv6 address.
+    config = "{ \"hw-address\": \"1:2:3:4:5:6\","
+        "\"next-server\": \"2001:db8:1::1\" }";
+    testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the invalid server hostname is rejected.
+TEST_F(HostReservationParserTest, invalidServerHostname) {
+    std::ostringstream config;
+    config << "{ \"hw-address\": \"1:2:3:4:5:6\","
+        "\"server-hostname\": \"";
+    config << std::string(64, 'a');
+    config << "\" }";
+    testInvalidConfig<HostReservationParser4>(config.str());
+}
+
+// This test verifies that the invalid boot file name is rejected.
+TEST_F(HostReservationParserTest, invalidBootFileName) {
+    std::ostringstream config;
+    config << "{ \"hw-address\": \"1:2:3:4:5:6\","
+        "\"boot-file-name\": \"";
+    config << std::string(128, 'a');
+    config << "\" }";
+    testInvalidConfig<HostReservationParser4>(config.str());
+}
 
 // This test verifies that the configuration parser for host reservations
 // throws an exception when IPv6 address is specified for IPv4 address
@@ -381,6 +447,30 @@ TEST_F(HostReservationParserTest, bcastAddress) {
 }
 
 // This test verifies that the configuration parser for host reservations
+// throws an exception when invalid next server address is specified.
+TEST_F(HostReservationParserTest, malformedNextServer) {
+    std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+        "\"next-server\": \"192.0.2.bogus\" }";
+    testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when zero next server address is specified.
+TEST_F(HostReservationParserTest, zeroNextServer) {
+    std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+        "\"next-server\": \"0.0.0.0\" }";
+    testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
+// throws an exception when broadcast next server address is specified.
+TEST_F(HostReservationParserTest, bcastNextServer) {
+    std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+        "\"next-server\": \"255.255.255.255\" }";
+    testInvalidConfig<HostReservationParser4>(config);
+}
+
+// This test verifies that the configuration parser for host reservations
 // throws an exception when unsupported parameter is specified.
 TEST_F(HostReservationParserTest, invalidParameterName) {
     // The "ip-addresses" parameter name is incorrect for the DHCPv4

+ 44 - 2
src/lib/dhcpsrv/tests/host_unittest.cc

@@ -192,7 +192,11 @@ TEST_F(HostTest, createFromHWAddrString) {
     ASSERT_NO_THROW(host.reset(new Host("01:02:03:04:05:06", "hw-address",
                                         SubnetID(1), SubnetID(2),
                                         IOAddress("192.0.2.3"),
-                                        "somehost.example.org")));
+                                        "somehost.example.org",
+                                        std::string(), std::string(),
+                                        IOAddress("192.0.0.2"),
+                                        "server-hostname.example.org",
+                                        "bootfile.efi")));
     // The HW address should be set to non-null.
     HWAddrPtr hwaddr = host->getHWAddress();
     ASSERT_TRUE(hwaddr);
@@ -205,6 +209,9 @@ TEST_F(HostTest, createFromHWAddrString) {
     EXPECT_EQ(2, host->getIPv6SubnetID());
     EXPECT_EQ("192.0.2.3", host->getIPv4Reservation().toText());
     EXPECT_EQ("somehost.example.org", host->getHostname());
+    EXPECT_EQ("192.0.0.2", host->getNextServer().toText());
+    EXPECT_EQ("server-hostname.example.org", host->getServerHostname());
+    EXPECT_EQ("bootfile.efi", host->getBootFileName());
 
     // Use invalid identifier name
     EXPECT_THROW(Host("01:02:03:04:05:06", "bogus", SubnetID(1), SubnetID(2),
@@ -264,7 +271,12 @@ TEST_F(HostTest, createFromHWAddrBinary) {
                                         Host::IDENT_HWADDR,
                                         SubnetID(1), SubnetID(2),
                                         IOAddress("192.0.2.3"),
-                                        "somehost.example.org")));
+                                        "somehost.example.org",
+                                        std::string(), std::string(),
+                                        IOAddress("192.0.0.2"),
+                                        "server-hostname.example.org",
+                                        "bootfile.efi")));
+
     // Hardware address should be non-null.
     HWAddrPtr hwaddr = host->getHWAddress();
     ASSERT_TRUE(hwaddr);
@@ -277,6 +289,9 @@ TEST_F(HostTest, createFromHWAddrBinary) {
     EXPECT_EQ(2, host->getIPv6SubnetID());
     EXPECT_EQ("192.0.2.3", host->getIPv4Reservation().toText());
     EXPECT_EQ("somehost.example.org", host->getHostname());
+    EXPECT_EQ("192.0.0.2", host->getNextServer().toText());
+    EXPECT_EQ("server-hostname.example.org", host->getServerHostname());
+    EXPECT_EQ("bootfile.efi", host->getBootFileName());
 }
 
 // This test verifies that it is possible to create a Host object using
@@ -644,11 +659,17 @@ TEST_F(HostTest, setValues) {
     host->setIPv6SubnetID(SubnetID(234));
     host->setIPv4Reservation(IOAddress("10.0.0.1"));
     host->setHostname("other-host.example.org");
+    host->setNextServer(IOAddress("192.0.2.2"));
+    host->setServerHostname("server-hostname.example.org");
+    host->setBootFileName("bootfile.efi");
 
     EXPECT_EQ(123, host->getIPv4SubnetID());
     EXPECT_EQ(234, host->getIPv6SubnetID());
     EXPECT_EQ("10.0.0.1", host->getIPv4Reservation().toText());
     EXPECT_EQ("other-host.example.org", host->getHostname());
+    EXPECT_EQ("192.0.2.2", host->getNextServer().toText());
+    EXPECT_EQ("server-hostname.example.org", host->getServerHostname());
+    EXPECT_EQ("bootfile.efi", host->getBootFileName());
 
     // Remove IPv4 reservation.
     host->removeIPv4Reservation();
@@ -664,6 +685,12 @@ TEST_F(HostTest, setValues) {
     // Broadcast address can't be set.
     EXPECT_THROW(host->setIPv4Reservation(IOAddress::IPV4_BCAST_ADDRESS()),
                  isc::BadValue);
+
+    // Broadcast and IPv6 are invalid addresses for next server.
+    EXPECT_THROW(host->setNextServer(asiolink::IOAddress::IPV4_BCAST_ADDRESS()),
+                                     isc::BadValue);
+    EXPECT_THROW(host->setNextServer(IOAddress("2001:db8:1::1")),
+                                     isc::BadValue);
 }
 
 // Test that Host constructors initialize client classes from string.
@@ -918,6 +945,9 @@ TEST_F(HostTest, toText) {
     EXPECT_EQ("hwaddr=010203040506 ipv4_subnet_id=1 ipv6_subnet_id=2"
               " hostname=myhost.example.com"
               " ipv4_reservation=192.0.2.3"
+              " siaddr=(no)"
+              " sname=(empty)"
+              " file=(empty)"
               " ipv6_reservation0=2001:db8:1::cafe"
               " ipv6_reservation1=2001:db8:1::1"
               " ipv6_reservation2=2001:db8:1:1::/64"
@@ -931,6 +961,9 @@ TEST_F(HostTest, toText) {
 
     EXPECT_EQ("hwaddr=010203040506 ipv6_subnet_id=2"
               " hostname=(empty) ipv4_reservation=(no)"
+              " siaddr=(no)"
+              " sname=(empty)"
+              " file=(empty)"
               " ipv6_reservation0=2001:db8:1::cafe"
               " ipv6_reservation1=2001:db8:1::1"
               " ipv6_reservation2=2001:db8:1:1::/64"
@@ -945,6 +978,9 @@ TEST_F(HostTest, toText) {
                                         "myhost")));
 
     EXPECT_EQ("duid=1112131415 hostname=myhost ipv4_reservation=(no)"
+              " siaddr=(no)"
+              " sname=(empty)"
+              " file=(empty)"
               " ipv6_reservations=(none)", host->toText());
 
     // Add some classes.
@@ -952,6 +988,9 @@ TEST_F(HostTest, toText) {
     host->addClientClass4("router");
 
     EXPECT_EQ("duid=1112131415 hostname=myhost ipv4_reservation=(no)"
+              " siaddr=(no)"
+              " sname=(empty)"
+              " file=(empty)"
               " ipv6_reservations=(none)"
               " dhcp4_class0=modem dhcp4_class1=router",
               host->toText());
@@ -960,6 +999,9 @@ TEST_F(HostTest, toText) {
     host->addClientClass6("device");
 
     EXPECT_EQ("duid=1112131415 hostname=myhost ipv4_reservation=(no)"
+              " siaddr=(no)"
+              " sname=(empty)"
+              " file=(empty)"
               " ipv6_reservations=(none)"
               " dhcp4_class0=modem dhcp4_class1=router"
               " dhcp6_class0=device dhcp6_class1=hub",

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

@@ -517,4 +517,10 @@ TEST_F(MySqlHostDataSourceTest, testAddRollback) {
     EXPECT_FALSE(from_hds);
 }
 
+// This test checks that siaddr, sname, file fields can be retrieved
+/// from a database for a host.
+TEST_F(MySqlHostDataSourceTest, messageFields) {
+    testMessageFields4();
+}
+
 }; // Of anonymous namespace

+ 6 - 0
src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc

@@ -475,4 +475,10 @@ TEST_F(PgSqlHostDataSourceTest, testAddRollback) {
     EXPECT_FALSE(from_hds);
 }
 
+// This test checks that siaddr, sname, file fields can be retrieved
+/// from a database for a host.
+TEST_F(PgSqlHostDataSourceTest, messageFields) {
+    testMessageFields4();
+}
+
 }; // Of anonymous namespace

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

@@ -469,6 +469,11 @@ ALTER TABLE dhcp6_options
 ALTER TABLE ipv6_reservations
     MODIFY reservation_id INT UNSIGNED NOT NULL AUTO_INCREMENT;
 
+# Add columns holding reservations for siaddr, sname and file fields
+# carried within DHCPv4 message.
+ALTER TABLE hosts ADD COLUMN dhcp4_next_server INT UNSIGNED NULL;
+ALTER TABLE hosts ADD COLUMN dhcp4_server_hostname VARCHAR(64) NULL;
+ALTER TABLE hosts ADD COLUMN dhcp4_boot_file_name VARCHAR(128) NULL;
 
 # Update the schema version number
 UPDATE schema_version

+ 6 - 0
src/share/database/scripts/pgsql/dhcpdb_create.pgsql

@@ -471,6 +471,12 @@ CREATE FUNCTION lease6DumpData() RETURNS
      ORDER BY l.address;
 $$ LANGUAGE SQL;
 
+-- Add columns holding reservations for siaddr, sname and file fields
+-- carried within DHCPv4 message.
+ALTER TABLE hosts ADD COLUMN dhcp4_next_server BIGINT DEFAULT NULL;
+ALTER TABLE hosts ADD COLUMN dhcp4_server_hostname VARCHAR(64) DEFAULT NULL;
+ALTER TABLE hosts ADD COLUMN dhcp4_boot_file_name VARCHAR(128) DEFAULT NULL;
+
 -- Set 3.0 schema version.
 UPDATE schema_version
     SET version = '3', minor = '0';

+ 6 - 0
src/share/database/scripts/pgsql/upgrade_2.0_to_3.0.sh.in

@@ -257,6 +257,12 @@ CREATE FUNCTION lease6DumpData() RETURNS
      ORDER BY l.address;
 \$\$ LANGUAGE SQL;
 
+-- Add columns holding reservations for siaddr, sname and file fields
+-- carried within DHCPv4 message.
+ALTER TABLE hosts ADD COLUMN dhcp4_next_server BIGINT DEFAULT NULL;
+ALTER TABLE hosts ADD COLUMN dhcp4_server_hostname VARCHAR(64) DEFAULT NULL;
+ALTER TABLE hosts ADD COLUMN dhcp4_boot_file_name VARCHAR(128) DEFAULT NULL;
+
 -- Set 3.0 schema version.
 UPDATE schema_version
     SET version = '3', minor = '0';