diff options
author | Marcin Siodelski <marcin@isc.org> | 2016-08-25 18:18:07 +0200 |
---|---|---|
committer | Marcin Siodelski <marcin@isc.org> | 2016-08-25 18:18:07 +0200 |
commit | 9b79fe005dd77328ea7c596fc6886f8fb838d1cf (patch) | |
tree | 06262dd383223a4f00eb95722b2cb9cf01989dd6 | |
parent | [master] Added ChangeLog entry 1156 for #4294. (diff) | |
parent | [4552] Addressed review comments. (diff) | |
download | kea-9b79fe005dd77328ea7c596fc6886f8fb838d1cf.tar.xz kea-9b79fe005dd77328ea7c596fc6886f8fb838d1cf.zip |
[master] Merge branch 'trac4552'
23 files changed, 778 insertions, 47 deletions
diff --git a/doc/guide/dhcp4-srv.xml b/doc/guide/dhcp4-srv.xml index d995f53e90..23b67ec48b 100644 --- a/doc/guide/dhcp4-srv.xml +++ b/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> diff --git a/src/bin/dhcp4/dhcp4.spec b/src/bin/dhcp4/dhcp4.spec index 43940ef68b..dbec910df6 100644 --- a/src/bin/dhcp4/dhcp4.spec +++ b/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": "" } ] } }, diff --git a/src/bin/dhcp4/dhcp4_srv.cc b/src/bin/dhcp4/dhcp4_srv.cc index 1ef9d7f0f8..5d4b3d328d 100644 --- a/src/bin/dhcp4/dhcp4_srv.cc +++ b/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); } diff --git a/src/bin/dhcp4/dhcp4_srv.h b/src/bin/dhcp4/dhcp4_srv.h index 516ec9921f..2877220d34 100644 --- a/src/bin/dhcp4/dhcp4_srv.h +++ b/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. diff --git a/src/bin/dhcp4/tests/dhcp4_client.cc b/src/bin/dhcp4/tests/dhcp4_client.cc index 28b6cc08bf..6773db4da6 100644 --- a/src/bin/dhcp4/tests/dhcp4_client.cc +++ b/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)); diff --git a/src/bin/dhcp4/tests/dhcp4_client.h b/src/bin/dhcp4/tests/dhcp4_client.h index 480a8a426d..a30b7d821f 100644 --- a/src/bin/dhcp4/tests/dhcp4_client.h +++ b/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(); diff --git a/src/bin/dhcp4/tests/dora_unittest.cc b/src/bin/dhcp4/tests/dora_unittest.cc index edfdba556f..4c6f8f50a7 100644 --- a/src/bin/dhcp4/tests/dora_unittest.cc +++ b/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 diff --git a/src/bin/dhcp4/tests/inform_unittest.cc b/src/bin/dhcp4/tests/inform_unittest.cc index 5ed038b4dc..7a45b7e0a1 100644 --- a/src/bin/dhcp4/tests/inform_unittest.cc +++ b/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) { diff --git a/src/lib/dhcpsrv/cfg_hosts.cc b/src/lib/dhcpsrv/cfg_hosts.cc index 2f4a0d1785..748fd4d273 100644 --- a/src/lib/dhcpsrv/cfg_hosts.cc +++ b/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; diff --git a/src/lib/dhcpsrv/host.cc b/src/lib/dhcpsrv/host.cc index 3c54e188b2..8b3bbb3e1e 100644 --- a/src/lib/dhcpsrv/host.cc +++ b/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)"; diff --git a/src/lib/dhcpsrv/host.h b/src/lib/dhcpsrv/host.h index 8b38185a5e..fa080e67ce 100644 --- a/src/lib/dhcpsrv/host.h +++ b/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) diff --git a/src/lib/dhcpsrv/mysql_host_data_source.cc b/src/lib/dhcpsrv/mysql_host_data_source.cc index 622b0641bb..3dde4c4c04 100644 --- a/src/lib/dhcpsrv/mysql_host_data_source.cc +++ b/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, diff --git a/src/lib/dhcpsrv/parsers/host_reservation_parser.cc b/src/lib/dhcpsrv/parsers/host_reservation_parser.cc index 491abfa495..f7e7a24680 100644 --- a/src/lib/dhcpsrv/parsers/host_reservation_parser.cc +++ b/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() << " (" diff --git a/src/lib/dhcpsrv/pgsql_host_data_source.cc b/src/lib/dhcpsrv/pgsql_host_data_source.cc index 2ab5280923..732db7cf1d 100644 --- a/src/lib/dhcpsrv/pgsql_host_data_source.cc +++ b/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 diff --git a/src/lib/dhcpsrv/tests/generic_host_data_source_unittest.cc b/src/lib/dhcpsrv/tests/generic_host_data_source_unittest.cc index 42387388ad..515b6757b3 100644 --- a/src/lib/dhcpsrv/tests/generic_host_data_source_unittest.cc +++ b/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 diff --git a/src/lib/dhcpsrv/tests/generic_host_data_source_unittest.h b/src/lib/dhcpsrv/tests/generic_host_data_source_unittest.h index 30701e2621..807ca21490 100644 --- a/src/lib/dhcpsrv/tests/generic_host_data_source_unittest.h +++ b/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 diff --git a/src/lib/dhcpsrv/tests/host_reservation_parser_unittest.cc b/src/lib/dhcpsrv/tests/host_reservation_parser_unittest.cc index 11ed6afe23..f083299927 100644 --- a/src/lib/dhcpsrv/tests/host_reservation_parser_unittest.cc +++ b/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 diff --git a/src/lib/dhcpsrv/tests/host_unittest.cc b/src/lib/dhcpsrv/tests/host_unittest.cc index 535d861ef4..e51733bbb1 100644 --- a/src/lib/dhcpsrv/tests/host_unittest.cc +++ b/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", diff --git a/src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc b/src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc index f827f45e33..60359afd89 100644 --- a/src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc +++ b/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 diff --git a/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc b/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc index 29dcf29864..b5456bfb8e 100644 --- a/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc +++ b/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 diff --git a/src/share/database/scripts/mysql/dhcpdb_create.mysql b/src/share/database/scripts/mysql/dhcpdb_create.mysql index 80aaf082c2..fefebb53e5 100644 --- a/src/share/database/scripts/mysql/dhcpdb_create.mysql +++ b/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 diff --git a/src/share/database/scripts/pgsql/dhcpdb_create.pgsql b/src/share/database/scripts/pgsql/dhcpdb_create.pgsql index 2d5241a6c4..8052ca53ef 100644 --- a/src/share/database/scripts/pgsql/dhcpdb_create.pgsql +++ b/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'; diff --git a/src/share/database/scripts/pgsql/upgrade_2.0_to_3.0.sh.in b/src/share/database/scripts/pgsql/upgrade_2.0_to_3.0.sh.in index 660142cecc..7163d3eea6 100644 --- a/src/share/database/scripts/pgsql/upgrade_2.0_to_3.0.sh.in +++ b/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'; |