diff options
author | Marcin Siodelski <marcin@isc.org> | 2020-09-25 14:12:14 +0200 |
---|---|---|
committer | Marcin Siodelski <marcin@isc.org> | 2020-10-05 15:14:57 +0200 |
commit | 99c44ffd314947a20aca781e1a5cc7b58ade0ff4 (patch) | |
tree | d53aad88d4f284222a2ba6437da2552d8a1acd40 /src/lib/dhcpsrv | |
parent | [#1418] Fixed ChangeLog number (diff) | |
download | kea-99c44ffd314947a20aca781e1a5cc7b58ade0ff4.tar.xz kea-99c44ffd314947a20aca781e1a5cc7b58ade0ff4.zip |
[#1428] Allow non-unique IPs in MySQL and PgSQL
Introduced new host API function which allows for configuring selected
backends to accept non-unique IP reservations for multiple hosts. Support
for it was added in MySQL, Postgres and Kea config file. It is not
supported in Cassandra. New migrations for MySQL and Postgres have been
created.
Diffstat (limited to 'src/lib/dhcpsrv')
23 files changed, 720 insertions, 90 deletions
diff --git a/src/lib/dhcpsrv/base_host_data_source.h b/src/lib/dhcpsrv/base_host_data_source.h index 7eb2cf644a..576b89b02a 100644 --- a/src/lib/dhcpsrv/base_host_data_source.h +++ b/src/lib/dhcpsrv/base_host_data_source.h @@ -416,6 +416,27 @@ public: /// Rolls back all pending database operations. On databases that don't /// support transactions, this is a no-op. virtual void rollback() {}; + + /// @brief Controls whether IP reservations are unique or non-unique. + /// + + /// In a typical case, the IP reservations are unique and backends verify + /// prior to adding a host reservation to the database that the reservation + /// for a given IP address/subnet does not exist. In some cases it may be + /// required to allow non-unique IP reservations, e.g. in the case when a + /// host has several interfaces and independently of which interface is used + /// by this host to communicate with the DHCP server the same IP address + /// should be assigned. In this case the @c unique value should be set to + /// false to disable the checks for uniqueness on the backend side. + /// + /// All backends are required to support the case when unique setting is + /// @c true and they must use this setting by default. + /// + /// @param unique boolean flag indicating if the IP reservations must be + /// unique or can be non-unique. + /// @return true if the new setting was accepted by the backend or false + /// otherwise. + virtual bool setIPReservationUnique(const bool unique) = 0; }; /// @brief HostDataSource pointer diff --git a/src/lib/dhcpsrv/cfg_hosts.cc b/src/lib/dhcpsrv/cfg_hosts.cc index 544cdf74a4..fe25c071f8 100644 --- a/src/lib/dhcpsrv/cfg_hosts.cc +++ b/src/lib/dhcpsrv/cfg_hosts.cc @@ -1009,7 +1009,7 @@ CfgHosts::add4(const HostPtr& host) { } // Check if the address is already reserved for the specified IPv4 subnet. - if (!host->getIPv4Reservation().isV4Zero() && + if (ip_reservations_unique_ && !host->getIPv4Reservation().isV4Zero() && (host->getIPv4SubnetID() != SUBNET_ID_UNUSED) && get4(host->getIPv4SubnetID(), host->getIPv4Reservation())) { isc_throw(ReservedAddress, "failed to add new host using the HW" @@ -1061,15 +1061,17 @@ CfgHosts::add6(const HostPtr& host) { for (IPv6ResrvIterator it = reservations.first; it != reservations.second; ++it) { - // If there's an entry for this (subnet-id, address), reject it. - if (get6(host->getIPv6SubnetID(), it->second.getPrefix())) { - isc_throw(DuplicateHost, "failed to add address reservation for " - << "host using the HW address '" - << (hwaddr ? hwaddr->toText(false) : "(null)") - << " and DUID '" << (duid ? duid->toText() : "(null)") - << "' to the IPv6 subnet id '" << host->getIPv6SubnetID() - << "' for address/prefix " << it->second.getPrefix() - << ": There's already reservation for this address/prefix"); + if (ip_reservations_unique_) { + // If there's an entry for this (subnet-id, address), reject it. + if (get6(host->getIPv6SubnetID(), it->second.getPrefix())) { + isc_throw(DuplicateHost, "failed to add address reservation for " + << "host using the HW address '" + << (hwaddr ? hwaddr->toText(false) : "(null)") + << " and DUID '" << (duid ? duid->toText() : "(null)") + << "' to the IPv6 subnet id '" << host->getIPv6SubnetID() + << "' for address/prefix " << it->second.getPrefix() + << ": There's already reservation for this address/prefix"); + } } hosts6_.insert(HostResrv6Tuple(it->second, host)); } @@ -1132,6 +1134,13 @@ CfgHosts::del6(const SubnetID& /*subnet_id*/, return (false); } +bool +CfgHosts::setIPReservationUnique(const bool unique) { + ip_reservations_unique_ = unique; + return (true); +} + + ElementPtr CfgHosts::toElement() const { uint16_t family = CfgMgr::instance().getFamily(); diff --git a/src/lib/dhcpsrv/cfg_hosts.h b/src/lib/dhcpsrv/cfg_hosts.h index f5b37d900a..050df7c11f 100644 --- a/src/lib/dhcpsrv/cfg_hosts.h +++ b/src/lib/dhcpsrv/cfg_hosts.h @@ -544,6 +544,23 @@ public: return (std::string("configuration file")); } + /// @brief Controls whether IP reservations are unique or non-unique. + /// + /// In a typical case, the IP reservations are unique and backends verify + /// prior to adding a host reservation to the database that the reservation + /// for a given IP address/subnet does not exist. In some cases it may be + /// required to allow non-unique IP reservations, e.g. in the case when a + /// host has several interfaces and independently of which interface is used + /// by this host to communicate with the DHCP server the same IP address + /// should be assigned. In this case the @c unique value should be set to + /// false to disable the checks for uniqueness on the backend side. + /// + /// @param unique boolean flag indicating if the IP reservations must be + /// unique or can be non-unique. + /// @return always true because this data source supports both the case when + /// the addresses must be unique and when they may be non-unique. + virtual bool setIPReservationUnique(const bool unique); + /// @brief Unparse a configuration object /// /// host reservation lists are not autonomous so they are @@ -849,6 +866,10 @@ private: /// - IPv6 prefix HostContainer6 hosts6_; + /// @brief Holds the setting whether the IP reservations must be unique or + /// may be non-unique. + bool ip_reservations_unique_ = true; + /// @brief Unparse a configuration object (DHCPv4 reservations) /// /// @return a pointer to unparsed configuration diff --git a/src/lib/dhcpsrv/cql_host_data_source.cc b/src/lib/dhcpsrv/cql_host_data_source.cc index e3ef27b28a..dc83a23bcc 100644 --- a/src/lib/dhcpsrv/cql_host_data_source.cc +++ b/src/lib/dhcpsrv/cql_host_data_source.cc @@ -3660,5 +3660,14 @@ CqlHostDataSource::rollback() { LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_CQL_ROLLBACK); } +bool +CqlHostDataSource::setIPReservationUnique(const bool unique) { + // This backend does not support the mode in which multiple reservations + // for the same IP address are created. If selecting this mode is + // attempted this function returns false to indicate that this is + // not allowed. + return (unique ? true : false); +} + } // namespace dhcp } // namespace isc diff --git a/src/lib/dhcpsrv/cql_host_data_source.h b/src/lib/dhcpsrv/cql_host_data_source.h index f725312300..15fb02de3a 100644 --- a/src/lib/dhcpsrv/cql_host_data_source.h +++ b/src/lib/dhcpsrv/cql_host_data_source.h @@ -411,6 +411,25 @@ public: /// Rolls back all pending database operations (no-op for Cassandra) virtual void rollback() override; + /// @brief Controls whether IP reservations are unique or non-unique. + /// + + /// In a typical case, the IP reservations are unique and backends verify + /// prior to adding a host reservation to the database that the reservation + /// for a given IP address does not exist. In some cases it may be required + /// to allow non-unique IP reservations, e.g. in the case when a host has + /// several interfaces and independently of which interface is used by this + /// host to communicate with the DHCP server the same IP address should be + /// assigned. In this case the @c unique value should be set to false to + /// disable the checks for uniqueness on the backend side. + /// + /// @param unique boolean flag indicating if the IP reservations must be + /// unique within the subnet or can be non-unique. + /// @return true when addresses must be unique, false otherwise because + /// this backend does not support specifying the same IP address in multiple + /// host reservations. + virtual bool setIPReservationUnique(const bool unique) override; + private: /// @brief Pointer to the implementation of the @ref CqlHostDataSource. CqlHostDataSourceImpl* impl_; diff --git a/src/lib/dhcpsrv/host_mgr.cc b/src/lib/dhcpsrv/host_mgr.cc index cc70a5bacc..ba043d0316 100644 --- a/src/lib/dhcpsrv/host_mgr.cc +++ b/src/lib/dhcpsrv/host_mgr.cc @@ -604,5 +604,26 @@ HostMgr::cacheNegative(const SubnetID& ipv4_subnet_id, } } +bool +HostMgr::setIPReservationUnique(const bool unique) { + // Iterate over the alternate sources first, because they may include those + // for which the new setting is not supported. + for (auto source : alternate_sources_) { + if (!source->setIPReservationUnique(unique)) { + // One of the sources does not support this new mode of operation. + // Let's log a warning and back off the changes to the default + // setting which should always be supported. + LOG_WARN(hosts_logger, HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED) + .arg(source->getType()); + for (auto source : alternate_sources_) { + source->setIPReservationUnique(true); + } + return (false); + } + } + return (true); +} + + } // end of isc::dhcp namespace } // end of isc namespace diff --git a/src/lib/dhcpsrv/host_mgr.h b/src/lib/dhcpsrv/host_mgr.h index bfb977056f..6db3581759 100644 --- a/src/lib/dhcpsrv/host_mgr.h +++ b/src/lib/dhcpsrv/host_mgr.h @@ -529,6 +529,26 @@ public: disable_single_query_ = disable_single_query; } + /// @brief Controls whether IP reservations are unique or non-unique. + /// + /// In a typical case, the IP reservations are unique and backends verify + /// prior to adding a host reservation to the database that the reservation + /// for a given IP address/subnet does not exist. In some cases it may be + /// required to allow non-unique IP reservations, e.g. in the case when a + /// host has several interfaces and independently of which interface is used + /// by this host to communicate with the DHCP server the same IP address + /// should be assigned. In this case the @c unique value should be set to + /// false to disable the checks for uniqueness on the backend side. + /// + /// Calling this function on @c HostMgr causes the manager to attempt to + /// set this flag on all backends in use. + /// + /// @param unique boolean flag indicating if the IP reservations must be + /// unique or can be non-unique. + /// @return true if the new setting was accepted by the backend or false + /// otherwise. + virtual bool setIPReservationUnique(const bool unique); + protected: /// @brief The negative caching flag. /// diff --git a/src/lib/dhcpsrv/hosts_messages.cc b/src/lib/dhcpsrv/hosts_messages.cc index c5784a517c..9be0525239 100644 --- a/src/lib/dhcpsrv/hosts_messages.cc +++ b/src/lib/dhcpsrv/hosts_messages.cc @@ -66,6 +66,7 @@ extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_ADDRESS6 = " extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER = "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER"; extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_HOST = "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_HOST"; extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_NULL = "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_NULL"; +extern const isc::log::MessageID HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED = "HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED"; } // namespace dhcp } // namespace isc @@ -132,6 +133,7 @@ const char* values[] = { "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER", "get one host with IPv6 reservation for subnet id %1, identified by %2", "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_HOST", "using subnet id %1 and identifier %2, found in %3 host: %4", "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_NULL", "host not found using subnet id %1 and identifier %2", + "HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED", "host data source %1 does not support the mode in which IP reservations are non-unique", NULL }; diff --git a/src/lib/dhcpsrv/hosts_messages.h b/src/lib/dhcpsrv/hosts_messages.h index 7d40971d36..a1d2b0712d 100644 --- a/src/lib/dhcpsrv/hosts_messages.h +++ b/src/lib/dhcpsrv/hosts_messages.h @@ -67,6 +67,7 @@ extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_ADDRESS6; extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER; extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_HOST; extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_NULL; +extern const isc::log::MessageID HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED; } // namespace dhcp } // namespace isc diff --git a/src/lib/dhcpsrv/hosts_messages.mes b/src/lib/dhcpsrv/hosts_messages.mes index 280c759149..8c1e8c0f8e 100644 --- a/src/lib/dhcpsrv/hosts_messages.mes +++ b/src/lib/dhcpsrv/hosts_messages.mes @@ -283,3 +283,11 @@ identifier. % HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_NULL host not found using subnet id %1 and identifier %2 This debug message is issued when no host was found using the specified subnet id and host identifier. + +% HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED host data source %1 does not support the mode in which IP reservations are non-unique +This warning message is issued when an administrator attempted to configure the +server to allow multiple host reservations for the same IP address or prefix. +Some host database backends may not support this mode of operation. In this +case the administrator should stop using these backends or fall back to the +default setting which requires that IP addresses are unique within a subnet. +This setting is guaranteed to work for MySQL and Postgres host backends.
\ No newline at end of file diff --git a/src/lib/dhcpsrv/mysql_host_data_source.cc b/src/lib/dhcpsrv/mysql_host_data_source.cc index bcfcc59806..e4c97998d7 100644 --- a/src/lib/dhcpsrv/mysql_host_data_source.cc +++ b/src/lib/dhcpsrv/mysql_host_data_source.cc @@ -235,9 +235,11 @@ public: /// @param host Host object to be added to the database. /// None of the fields in the host reservation are modified - /// the host data is only read. + /// @param unique_ip boolean value indicating if multiple reservations for the + /// same IP address are allowed (false) or not (true). /// /// @return Vector of MySQL BIND objects representing the data to be added. - std::vector<MYSQL_BIND> createBindForSend(const HostPtr& host) { + std::vector<MYSQL_BIND> createBindForSend(const HostPtr& host, const bool unique_ip) { // Store host object to ensure it remains valid. host_ = host; @@ -381,7 +383,16 @@ public: } // Add the data to the vector. - return (std::vector<MYSQL_BIND>(bind_.begin(), bind_.begin() + columns_num_)); + std::vector<MYSQL_BIND> vec(bind_.begin(), bind_.begin() + HOST_COLUMNS); + + // When checking whether the IP is unique we need to bind the IPv4 address + // at the end of the query as it has additional binding for the IPv4 + // address. + if (unique_ip) { + vec.push_back(bind_[5]); + vec.push_back(bind_[3]); + } + return (vec); }; /// @brief Create BIND array to receive Host data. @@ -1610,10 +1621,13 @@ public: /// None of the fields in the reservation are modified - /// the reservation data is only read. /// @param id ID of a host owning this reservation + /// @param unique_ip boolean value indicating if multiple reservations for the + /// same IP address are allowed (false) or not (true). /// /// @return Vector of MySQL BIND objects representing the data to be added. std::vector<MYSQL_BIND> createBindForSend(const IPv6Resrv& resv, - const HostID& id) { + const HostID& id, + const bool unique_ip) { // Store the values to ensure they remain valid. resv_ = resv; @@ -1672,7 +1686,17 @@ public: // Add the data to the vector. Note the end element is one after the // end of the array. // RESRV_COLUMNS -1 as we do not set reservation_id. - return (std::vector<MYSQL_BIND>(&bind_[0], &bind_[RESRV_COLUMNS-1])); + std::vector<MYSQL_BIND> vec(&bind_[0], &bind_[RESRV_COLUMNS-1]); + + // When checking whether the IP is unique we need to bind the IPv6 address + // and prefix length at the end of the query as it has additional binding + // for the IPv6 address and prefix length. + if (unique_ip) { + vec.push_back(bind_[0]); + vec.push_back(bind_[1]); + } + + return (vec); } private: @@ -1990,30 +2014,32 @@ public: /// @note: please add new statements doing read only operations before /// the WRITE_STMTS_BEGIN position. enum StatementIndex { - GET_HOST_DHCPID, // Gets hosts by host identifier - GET_HOST_ADDR, // Gets hosts by IPv4 address - GET_HOST_SUBID4_DHCPID, // Gets host by IPv4 SubnetID, HW address/DUID - GET_HOST_SUBID6_DHCPID, // Gets host by IPv6 SubnetID, HW address/DUID - GET_HOST_SUBID_ADDR, // Gets host by IPv4 SubnetID and IPv4 address - GET_HOST_PREFIX, // Gets host by IPv6 prefix - GET_HOST_SUBID6_ADDR, // Gets host by IPv6 SubnetID and IPv6 prefix - GET_HOST_SUBID4, // Gets hosts by IPv4 SubnetID - GET_HOST_SUBID6, // Gets hosts by IPv6 SubnetID - GET_HOST_HOSTNAME, // Gets hosts by hostname - GET_HOST_HOSTNAME_SUBID4, // Gets hosts by hostname and IPv4 SubnetID - GET_HOST_HOSTNAME_SUBID6, // Gets hosts by hostname and IPv6 SubnetID - GET_HOST_SUBID4_PAGE, // Gets hosts by IPv4 SubnetID beginning by HID - GET_HOST_SUBID6_PAGE, // Gets hosts by IPv6 SubnetID beginning by HID - GET_HOST_PAGE4, // Gets v4 hosts beginning by HID - GET_HOST_PAGE6, // Gets v6 hosts beginning by HID - INSERT_HOST, // Insert new host to collection - INSERT_V6_RESRV, // Insert v6 reservation - INSERT_V4_HOST_OPTION, // Insert DHCPv4 option - INSERT_V6_HOST_OPTION, // Insert DHCPv6 option - DEL_HOST_ADDR4, // Delete v4 host (subnet-id, addr4) - DEL_HOST_SUBID4_ID, // Delete v4 host (subnet-id, ident.type, identifier) - DEL_HOST_SUBID6_ID, // Delete v6 host (subnet-id, ident.type, identifier) - NUM_STATEMENTS // Number of statements + GET_HOST_DHCPID, // Gets hosts by host identifier + GET_HOST_ADDR, // Gets hosts by IPv4 address + GET_HOST_SUBID4_DHCPID, // Gets host by IPv4 SubnetID, HW address/DUID + GET_HOST_SUBID6_DHCPID, // Gets host by IPv6 SubnetID, HW address/DUID + GET_HOST_SUBID_ADDR, // Gets host by IPv4 SubnetID and IPv4 address + GET_HOST_PREFIX, // Gets host by IPv6 prefix + GET_HOST_SUBID6_ADDR, // Gets host by IPv6 SubnetID and IPv6 prefix + GET_HOST_SUBID4, // Gets hosts by IPv4 SubnetID + GET_HOST_SUBID6, // Gets hosts by IPv6 SubnetID + GET_HOST_HOSTNAME, // Gets hosts by hostname + GET_HOST_HOSTNAME_SUBID4, // Gets hosts by hostname and IPv4 SubnetID + GET_HOST_HOSTNAME_SUBID6, // Gets hosts by hostname and IPv6 SubnetID + GET_HOST_SUBID4_PAGE, // Gets hosts by IPv4 SubnetID beginning by HID + GET_HOST_SUBID6_PAGE, // Gets hosts by IPv6 SubnetID beginning by HID + GET_HOST_PAGE4, // Gets v4 hosts beginning by HID + GET_HOST_PAGE6, // Gets v6 hosts beginning by HID + INSERT_HOST_NON_UNIQUE_IP, // Insert new host to collection with allowing IP duplicates + INSERT_HOST_UNIQUE_IP, // Insert new host to collection with checking for IP duplicates + INSERT_V6_RESRV_NON_UNIQUE,// Insert v6 reservation without checking that it is unique + INSERT_V6_RESRV_UNIQUE, // Insert v6 reservation with checking that it is unique + INSERT_V4_HOST_OPTION, // Insert DHCPv4 option + INSERT_V6_HOST_OPTION, // Insert DHCPv6 option + DEL_HOST_ADDR4, // Delete v4 host (subnet-id, addr4) + DEL_HOST_SUBID4_ID, // Delete v4 host (subnet-id, ident.type, identifier) + DEL_HOST_SUBID6_ID, // Delete v6 host (subnet-id, ident.type, identifier) + NUM_STATEMENTS // Number of statements }; /// @brief Index of first statement performing write to the database. @@ -2021,7 +2047,7 @@ public: /// This value is used to mark border line between queries and other /// statements and statements performing write operation on the database, /// such as INSERT, DELETE, UPDATE. - static const StatementIndex WRITE_STMTS_BEGIN = INSERT_HOST; + static const StatementIndex WRITE_STMTS_BEGIN = INSERT_HOST_UNIQUE_IP; /// @brief Constructor. /// @@ -2197,6 +2223,10 @@ public: /// @brief The parameters DatabaseConnection::ParameterMap parameters_; + /// @brief Holds the setting whether the IP reservations must be unique or + /// may be non-unique. + bool ip_reservations_unique_; + /// @brief The pool of contexts MySqlHostContextPoolPtr pool_; }; @@ -2565,8 +2595,9 @@ TaggedStatementArray tagged_statements = { { "ON h.host_id = r.host_id " "ORDER BY h.host_id, o.option_id, r.reservation_id"}, - // Inserts a host into the 'hosts' table. - {MySqlHostDataSourceImpl::INSERT_HOST, + // Inserts a host into the 'hosts' table without checking that there is + // a reservation for the IP address. + {MySqlHostDataSourceImpl::INSERT_HOST_NON_UNIQUE_IP, "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, " @@ -2574,12 +2605,46 @@ TaggedStatementArray tagged_statements = { { "dhcp4_server_hostname, dhcp4_boot_file_name, auth_key) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"}, - // Inserts a single IPv6 reservation into 'reservations' table. - {MySqlHostDataSourceImpl::INSERT_V6_RESRV, + // Inserts a host into the 'hosts' table with checking that reserved IP + // address is unique. The innermost query checks if there is at least + // one host for the given IP/subnet combination. If it not exists the + // new host is inserted. DUAL is a special MySQL table from which we + // can select the values to be inserted. If the host with the given + // IP address already exists the new host won't be inserted. The caller + // can check the number of affected rows to detect that there was + // a duplicate host in the database. + {MySqlHostDataSourceImpl::INSERT_HOST_UNIQUE_IP, + "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, " + "user_context, dhcp4_next_server, " + "dhcp4_server_hostname, dhcp4_boot_file_name, auth_key) " + "SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM DUAL " + "WHERE NOT EXISTS (" + "SELECT ipv4_address FROM hosts " + "WHERE ipv4_address = ? AND dhcp4_subnet_id = ? " + "LIMIT 1" + ")"}, + + // Inserts a single IPv6 reservation into 'reservations' table without + // checking that the inserted reservation is unique. + {MySqlHostDataSourceImpl::INSERT_V6_RESRV_NON_UNIQUE, "INSERT INTO ipv6_reservations(address, prefix_len, type, " "dhcp6_iaid, host_id) " "VALUES (?, ?, ?, ?, ?)"}, + // Inserts a single IPv6 reservation into 'reservations' table with + // checking that the inserted reservation is unique. + {MySqlHostDataSourceImpl::INSERT_V6_RESRV_UNIQUE, + "INSERT INTO ipv6_reservations(address, prefix_len, type, " + "dhcp6_iaid, host_id) " + "SELECT ?, ?, ?, ?, ? FROM DUAL " + "WHERE NOT EXISTS (" + "SELECT 1 FROM ipv6_reservations " + "WHERE address = ? AND prefix_len = ? " + "LIMIT 1" + ")"}, + // Inserts a single DHCPv4 option into 'dhcp4_options' table. // Using fixed scope_id = 3, which associates an option with host. {MySqlHostDataSourceImpl::INSERT_V4_HOST_OPTION, @@ -2655,7 +2720,7 @@ MySqlHostDataSource::MySqlHostContextAlloc::~MySqlHostContextAlloc() { } MySqlHostDataSourceImpl::MySqlHostDataSourceImpl(const MySqlConnection::ParameterMap& parameters) - : parameters_(parameters) { + : parameters_(parameters), ip_reservations_unique_(true) { // Validate the schema version first. std::pair<uint32_t, uint32_t> code_version(MYSQL_SCHEMA_VERSION_MAJOR, @@ -2743,6 +2808,16 @@ MySqlHostDataSourceImpl::addStatement(MySqlHostContextPtr& ctx, } checkError(ctx, status, stindex, "unable to execute"); } + + // If the number of rows inserted is 0 it means that the query detected + // an attempt to insert duplicated data for which there is no unique + // index in the database. Unique indexes are not created in the database + // when it may be sometimes allowed to insert duplicated records per + // server's configuration. + my_ulonglong numrows = mysql_stmt_affected_rows(ctx->conn_.statements_[stindex]); + if (numrows == 0) { + isc_throw(DuplicateEntry, "Database duplicate entry error"); + } } bool @@ -2770,9 +2845,10 @@ void MySqlHostDataSourceImpl::addResv(MySqlHostContextPtr& ctx, const IPv6Resrv& resv, const HostID& id) { - std::vector<MYSQL_BIND> bind = ctx->host_ipv6_reservation_exchange_->createBindForSend(resv, id); + std::vector<MYSQL_BIND> bind = ctx->host_ipv6_reservation_exchange_-> + createBindForSend(resv, id, ip_reservations_unique_); - addStatement(ctx, INSERT_V6_RESRV, bind); + addStatement(ctx, ip_reservations_unique_ ? INSERT_V6_RESRV_UNIQUE : INSERT_V6_RESRV_NON_UNIQUE, bind); } void @@ -2958,11 +3034,19 @@ MySqlHostDataSource::add(const HostPtr& host) { // the MySqlTransaction class. MySqlTransaction transaction(ctx->conn_); + // If we're configured to check that an IP reservation within a given subnet + // is unique, the IP reservation exists and the subnet is actually set + // we will be using a special query that checks for uniqueness. Otherwise, + // we will use a regular insert statement. + bool unique_ip = impl_->ip_reservations_unique_ && !host->getIPv4Reservation().isV4Zero() + && host->getIPv4SubnetID() != SUBNET_ID_UNUSED; + // Create the MYSQL_BIND array for the host - std::vector<MYSQL_BIND> bind = ctx->host_ipv4_exchange_->createBindForSend(host); + std::vector<MYSQL_BIND> bind = ctx->host_ipv4_exchange_->createBindForSend(host, unique_ip); // ... and insert the host. - impl_->addStatement(ctx, MySqlHostDataSourceImpl::INSERT_HOST, bind); + impl_->addStatement(ctx, unique_ip ? MySqlHostDataSourceImpl::INSERT_HOST_UNIQUE_IP : + MySqlHostDataSourceImpl::INSERT_HOST_NON_UNIQUE_IP, bind); // Gets the last inserted hosts id uint64_t host_id = mysql_insert_id(ctx->conn_.mysql_); @@ -3646,5 +3730,12 @@ MySqlHostDataSource::rollback() { ctx->conn_.rollback(); } +bool +MySqlHostDataSource::setIPReservationUnique(const bool unique) { + impl_->ip_reservations_unique_ = unique; + return (true); +} + + } // namespace dhcp } // namespace isc diff --git a/src/lib/dhcpsrv/mysql_host_data_source.h b/src/lib/dhcpsrv/mysql_host_data_source.h index 7790e6525c..f6266d4c67 100644 --- a/src/lib/dhcpsrv/mysql_host_data_source.h +++ b/src/lib/dhcpsrv/mysql_host_data_source.h @@ -384,6 +384,23 @@ public: /// Rolls back all pending database operations. virtual void rollback(); + /// @brief Controls whether IP reservations are unique or non-unique. + /// + /// In a typical case, the IP reservations are unique and backends verify + /// prior to adding a host reservation to the database that the reservation + /// for a given IP address/subnet does not exist. In some cases it may be + /// required to allow non-unique IP reservations, e.g. in the case when a + /// host has several interfaces and independently of which interface is used + /// by this host to communicate with the DHCP server the same IP address + /// should be assigned. In this case the @c unique value should be set to + /// false to disable the checks for uniqueness on the backend side. + /// + /// @param unique boolean flag indicating if the IP reservations must be + /// unique within the subnet or can be non-unique. + /// @return always true because this backend supports both the case when + /// the addresses must be unique and when they may be non-unique. + virtual bool setIPReservationUnique(const bool unique); + /// @brief Context RAII Allocator. class MySqlHostContextAlloc { public: diff --git a/src/lib/dhcpsrv/pgsql_host_data_source.cc b/src/lib/dhcpsrv/pgsql_host_data_source.cc index 0eeb52a33e..84a859b169 100644 --- a/src/lib/dhcpsrv/pgsql_host_data_source.cc +++ b/src/lib/dhcpsrv/pgsql_host_data_source.cc @@ -186,12 +186,14 @@ public: /// @param host Host object to be added to the database. /// None of the fields in the host reservation are modified - /// the host data is only read. + /// @param unique_ip boolean value indicating if multiple reservations for the + /// same IP address are allowed (false) or not (true). /// /// @return pointer to newly constructed bind_array containing the /// bound values extracted from host /// /// @throw DbOperationError if bind_array cannot be populated. - PsqlBindArrayPtr createBindForSend(const HostPtr& host) { + PsqlBindArrayPtr createBindForSend(const HostPtr& host, const bool unique_ip) { if (!host) { isc_throw(BadValue, "createBindForSend:: host object is NULL"); } @@ -265,6 +267,15 @@ public: bind_array->add(key); } + // When checking whether the IP is unique we need to bind the IPv4 address + // at the end of the query as it has additional binding for the IPv4 + // address. + if (unique_ip) { + bind_array->add(host->getIPv4Reservation()); + bind_array->add(host->getIPv4SubnetID()); + } + + } catch (const std::exception& ex) { host_.reset(); isc_throw(DbOperationError, @@ -1081,13 +1092,16 @@ public: /// @param resv The IPv6 reservation to be added to the database. /// None of the fields in the reservation are modified - /// @param host_id ID of the host to which this reservation belongs. + /// @param unique_ip boolean value indicating if multiple reservations for the + /// same IP address are allowed (false) or not (true). /// /// @return pointer to newly constructed bind_array containing the /// bound values extracted the IPv6 reservation /// /// @throw DbOperationError if bind_array cannot be populated. PsqlBindArrayPtr createBindForSend(const IPv6Resrv& resv, - const HostID& host_id) { + const HostID& host_id, + const bool unique_ip) { // Store the values to ensure they remain valid. // Technically we don't need this, as currently all the values // are converted to strings and stored by the bind array. @@ -1113,6 +1127,14 @@ public: // host_id: BIGINT NOT NULL bind_array->add(host_id); + + // When checking whether the IP is unique we need to bind the IPv6 address + // and prefix length at the end of the query as it has additional binding + // for the IPv6 address and prefix length. + if (unique_ip) { + bind_array->add(resv.getPrefix()); + bind_array->add(resv.getPrefixLen()); + } } catch (const std::exception& ex) { isc_throw(DbOperationError, "Could not create bind array from IPv6 Reservation: " @@ -1354,30 +1376,32 @@ public: /// @note: please add new statements doing read only operations before /// the WRITE_STMTS_BEGIN position. enum StatementIndex { - GET_HOST_DHCPID, // Gets hosts by host identifier - GET_HOST_ADDR, // Gets hosts by IPv4 address - GET_HOST_SUBID4_DHCPID, // Gets host by IPv4 SubnetID, HW address/DUID - GET_HOST_SUBID6_DHCPID, // Gets host by IPv6 SubnetID, HW address/DUID - GET_HOST_SUBID_ADDR, // Gets host by IPv4 SubnetID and IPv4 address - GET_HOST_PREFIX, // Gets host by IPv6 prefix - GET_HOST_SUBID6_ADDR, // Gets host by IPv6 SubnetID and IPv6 prefix - GET_HOST_SUBID4, // Gets hosts by IPv4 SubnetID - GET_HOST_SUBID6, // Gets hosts by IPv6 SubnetID - GET_HOST_HOSTNAME, // Gets hosts by hostname - GET_HOST_HOSTNAME_SUBID4, // Gets hosts by hostname and IPv4 SubnetID - GET_HOST_HOSTNAME_SUBID6, // Gets hosts by hostname and IPv6 SubnetID - GET_HOST_SUBID4_PAGE, // Gets hosts by IPv4 SubnetID beginning by HID - GET_HOST_SUBID6_PAGE, // Gets hosts by IPv6 SubnetID beginning by HID - GET_HOST_PAGE4, // Gets v4 hosts beginning by HID - GET_HOST_PAGE6, // Gets v6 hosts beginning by HID - INSERT_HOST, // Insert new host to collection - INSERT_V6_RESRV, // Insert v6 reservation - INSERT_V4_HOST_OPTION, // Insert DHCPv4 option - INSERT_V6_HOST_OPTION, // Insert DHCPv6 option - DEL_HOST_ADDR4, // Delete v4 host (subnet-id, addr4) - DEL_HOST_SUBID4_ID, // Delete v4 host (subnet-id, ident.type, identifier) - DEL_HOST_SUBID6_ID, // Delete v6 host (subnet-id, ident.type, identifier) - NUM_STATEMENTS // Number of statements + GET_HOST_DHCPID, // Gets hosts by host identifier + GET_HOST_ADDR, // Gets hosts by IPv4 address + GET_HOST_SUBID4_DHCPID, // Gets host by IPv4 SubnetID, HW address/DUID + GET_HOST_SUBID6_DHCPID, // Gets host by IPv6 SubnetID, HW address/DUID + GET_HOST_SUBID_ADDR, // Gets host by IPv4 SubnetID and IPv4 address + GET_HOST_PREFIX, // Gets host by IPv6 prefix + GET_HOST_SUBID6_ADDR, // Gets host by IPv6 SubnetID and IPv6 prefix + GET_HOST_SUBID4, // Gets hosts by IPv4 SubnetID + GET_HOST_SUBID6, // Gets hosts by IPv6 SubnetID + GET_HOST_HOSTNAME, // Gets hosts by hostname + GET_HOST_HOSTNAME_SUBID4, // Gets hosts by hostname and IPv4 SubnetID + GET_HOST_HOSTNAME_SUBID6, // Gets hosts by hostname and IPv6 SubnetID + GET_HOST_SUBID4_PAGE, // Gets hosts by IPv4 SubnetID beginning by HID + GET_HOST_SUBID6_PAGE, // Gets hosts by IPv6 SubnetID beginning by HID + GET_HOST_PAGE4, // Gets v4 hosts beginning by HID + GET_HOST_PAGE6, // Gets v6 hosts beginning by HID + INSERT_HOST_NON_UNIQUE_IP, // Insert new host to collection with allowing IP duplicates + INSERT_HOST_UNIQUE_IP, // Insert new host to collection with checking for IP duplicates + INSERT_V6_RESRV_NON_UNIQUE,// Insert v6 reservation without checking that it is unique + INSERT_V6_RESRV_UNIQUE, // Insert v6 reservation with checking that it is unique + INSERT_V4_HOST_OPTION, // Insert DHCPv4 option + INSERT_V6_HOST_OPTION, // Insert DHCPv6 option + DEL_HOST_ADDR4, // Delete v4 host (subnet-id, addr4) + DEL_HOST_SUBID4_ID, // Delete v4 host (subnet-id, ident.type, identifier) + DEL_HOST_SUBID6_ID, // Delete v6 host (subnet-id, ident.type, identifier) + NUM_STATEMENTS // Number of statements }; /// @brief Index of first statement performing write to the database. @@ -1385,7 +1409,7 @@ public: /// This value is used to mark border line between queries and other /// statements and statements performing write operation on the database, /// such as INSERT, DELETE, UPDATE. - static const StatementIndex WRITE_STMTS_BEGIN = INSERT_HOST; + static const StatementIndex WRITE_STMTS_BEGIN = INSERT_HOST_UNIQUE_IP; /// @brief Constructor. /// @@ -1551,6 +1575,10 @@ public: /// @brief The parameters PgSqlConnection::ParameterMap parameters_; + /// @brief Holds the setting whether the IP reservations must be unique or + /// may be non-unique. + bool ip_reservations_unique_; + /// @brief The pool of contexts PgSqlHostContextPoolPtr pool_; }; @@ -1961,32 +1989,75 @@ TaggedStatementArray tagged_statements = { { "ORDER BY h.host_id, o.option_id, r.reservation_id" }, - // PgSqlHostDataSourceImpl::INSERT_HOST - // Inserts a host into the 'hosts' table. Returns the inserted host id. + // Inserts a host into the 'hosts' table without checking that there is + // a reservation for the IP address. {13, { OID_BYTEA, OID_INT2, OID_INT8, OID_INT8, OID_INT8, OID_VARCHAR, OID_VARCHAR, OID_VARCHAR, OID_TEXT, - OID_INT8, OID_VARCHAR, OID_VARCHAR, OID_VARCHAR }, - "insert_host", + OID_INT8, OID_VARCHAR, OID_VARCHAR, OID_VARCHAR}, + "insert_host_non_unique_ip", "INSERT INTO hosts(dhcp_identifier, dhcp_identifier_type, " " dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, " " dhcp4_client_classes, dhcp6_client_classes, user_context, " - " dhcp4_next_server, dhcp4_server_hostname, dhcp4_boot_file_name, auth_key) " - "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) " + " dhcp4_next_server, dhcp4_server_hostname, dhcp4_boot_file_name, auth_key)" + "VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 ) " "RETURNING host_id" }, - // PgSqlHostDataSourceImpl::INSERT_V6_RESRV - // Inserts a single IPv6 reservation into 'reservations' table. + // PgSqlHostDataSourceImpl::INSERT_HOST_UNIQUE_IP + // Inserts a host into the 'hosts' table with checking that reserved IP + // address is unique. The innermost query checks if there is at least + // one host for the given IP/subnet combination. If it not exists the + // new host is inserted. If the host with the given IP address already + // exists the new host won't be inserted. The caller can check the + // number of affected rows to detect that there was a duplicate host + // in the database. Returns the inserted host id. + {15, + { OID_BYTEA, OID_INT2, + OID_INT8, OID_INT8, OID_INT8, OID_VARCHAR, + OID_VARCHAR, OID_VARCHAR, OID_TEXT, + OID_INT8, OID_VARCHAR, OID_VARCHAR, OID_VARCHAR, OID_INT8, + OID_INT8}, + "insert_host_unique_ip", + "INSERT INTO hosts(dhcp_identifier, dhcp_identifier_type, " + " dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, " + " dhcp4_client_classes, dhcp6_client_classes, user_context, " + " dhcp4_next_server, dhcp4_server_hostname, dhcp4_boot_file_name, auth_key)" + " SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13" + " WHERE NOT EXISTS (" + " SELECT 1 FROM HOSTS WHERE ipv4_address = $14 AND dhcp4_subnet_id = $15" + " ) " + "RETURNING host_id" + }, + + // PgSqlHostDataSourceImpl::INSERT_V6_RESRV_NON_UNIQUE + // Inserts a single IPv6 reservation into 'reservations' table without + // checking that the inserted reservation is unique. {5, { OID_VARCHAR, OID_INT2, OID_INT4, OID_INT4, OID_INT4 }, - "insert_v6_resrv", + "insert_v6_resrv_non_unique", "INSERT INTO ipv6_reservations(address, prefix_len, type, " " dhcp6_iaid, host_id) " "VALUES ($1, $2, $3, $4, $5)" }, + // PgSqlHostDataSourceImpl::INSERT_V6_RESRV_UNIQUE + // Inserts a single IPv6 reservation into 'reservations' table with + // checking that the inserted reservation is unique. + {7, + { OID_VARCHAR, OID_INT2, OID_INT4, OID_INT4, OID_INT4, OID_VARCHAR, OID_INT2 }, + "insert_v6_resrv_unique", + "INSERT INTO ipv6_reservations(address, prefix_len, type, " + " dhcp6_iaid, host_id) " + "SELECT $1, $2, $3, $4, $5 " + " WHERE NOT EXISTS (" + " SELECT 1 FROM ipv6_reservations" + " WHERE address = $6 AND prefix_len = $7" + " LIMIT 1" + " )" + }, + // PgSqlHostDataSourceImpl::INSERT_V4_HOST_OPTION // Inserts a single DHCPv4 option into 'dhcp4_options' table. // Using fixed scope_id = 3, which associates an option with host. @@ -2086,7 +2157,7 @@ PgSqlHostDataSource::PgSqlHostContextAlloc::~PgSqlHostContextAlloc() { } PgSqlHostDataSourceImpl::PgSqlHostDataSourceImpl(const PgSqlConnection::ParameterMap& parameters) - : parameters_(parameters) { + : parameters_(parameters), ip_reservations_unique_(true) { // Validate the schema version first. std::pair<uint32_t, uint32_t> code_version(PG_SCHEMA_VERSION_MAJOR, @@ -2168,6 +2239,22 @@ PgSqlHostDataSourceImpl::addStatement(PgSqlHostContextPtr& ctx, ctx->conn_.checkStatementError(r, tagged_statements[stindex]); } + // Get the number of affected rows. + char* rows_affected = PQcmdTuples(r); + if (!rows_affected) { + isc_throw(DbOperationError, + "Could not retrieve the number of affected rows."); + } + + // If the number of rows inserted is 0 it means that the query detected + // an attempt to insert duplicated data for which there is no unique + // index in the database. Unique indexes are not created in the database + // when it may be sometimes allowed to insert duplicated records per + // server's configuration. + if (rows_affected[0] == '0') { + isc_throw(DuplicateEntry, "Database duplicate entry error"); + } + if (return_last_id) { PgSqlExchange::getColumnValue(r, 0, 0, last_id); } @@ -2207,9 +2294,11 @@ void PgSqlHostDataSourceImpl::addResv(PgSqlHostContextPtr& ctx, const IPv6Resrv& resv, const HostID& id) { - PsqlBindArrayPtr bind_array = ctx->host_ipv6_reservation_exchange_->createBindForSend(resv, id); + PsqlBindArrayPtr bind_array = ctx->host_ipv6_reservation_exchange_-> + createBindForSend(resv, id, ip_reservations_unique_); - addStatement(ctx, INSERT_V6_RESRV, bind_array); + addStatement(ctx, ip_reservations_unique_ ? INSERT_V6_RESRV_UNIQUE : INSERT_V6_RESRV_NON_UNIQUE, + bind_array); } void @@ -2349,11 +2438,19 @@ PgSqlHostDataSource::add(const HostPtr& host) { // the PgSqlTransaction class. PgSqlTransaction transaction(ctx->conn_); + // If we're configured to check that an IP reservation within a given subnet + // is unique, the IP reservation exists and the subnet is actually set + // we will be using a special query that checks for uniqueness. Otherwise, + // we will use a regular insert statement. + bool unique_ip = impl_->ip_reservations_unique_ && !host->getIPv4Reservation().isV4Zero() + && host->getIPv4SubnetID() != SUBNET_ID_UNUSED; + // Create the PgSQL Bind array for the host - PsqlBindArrayPtr bind_array = ctx->host_ipv4_exchange_->createBindForSend(host); + PsqlBindArrayPtr bind_array = ctx->host_ipv4_exchange_->createBindForSend(host, unique_ip); // ... and insert the host. - uint32_t host_id = impl_->addStatement(ctx, PgSqlHostDataSourceImpl::INSERT_HOST, + uint32_t host_id = impl_->addStatement(ctx, unique_ip ? PgSqlHostDataSourceImpl::INSERT_HOST_UNIQUE_IP : + PgSqlHostDataSourceImpl::INSERT_HOST_NON_UNIQUE_IP, bind_array, true); // Insert DHCPv4 options. @@ -2902,5 +2999,11 @@ PgSqlHostDataSource::rollback() { ctx->conn_.rollback(); } +bool +PgSqlHostDataSource::setIPReservationUnique(const bool unique) { + impl_->ip_reservations_unique_ = unique; + return (true); +} + } // namespace dhcp } // namespace isc diff --git a/src/lib/dhcpsrv/pgsql_host_data_source.h b/src/lib/dhcpsrv/pgsql_host_data_source.h index 5f7cb2b6aa..152d8b0f72 100644 --- a/src/lib/dhcpsrv/pgsql_host_data_source.h +++ b/src/lib/dhcpsrv/pgsql_host_data_source.h @@ -435,6 +435,23 @@ public: /// Rolls back all pending database operations. virtual void rollback(); + /// @brief Controls whether IP reservations are unique or non-unique. + /// + /// In a typical case, the IP reservations are unique and backends verify + /// prior to adding a host reservation to the database that the reservation + /// for a given IP address/subnet does not exist. In some cases it may be + /// required to allow non-unique IP reservations, e.g. in the case when a + /// host has several interfaces and independently of which interface is used + /// by this host to communicate with the DHCP server the same IP address + /// should be assigned. In this case the @c unique value should be set to + /// false to disable the checks for uniqueness on the backend side. + /// + /// @param unique boolean flag indicating if the IP reservations must be + /// unique within the subnet or can be non-unique. + /// @return always true because this backend supports both the case when + /// the addresses must be unique and when they may be non-unique. + virtual bool setIPReservationUnique(const bool unique); + /// @brief Context RAII Allocator. class PgSqlHostContextAlloc { public: diff --git a/src/lib/dhcpsrv/tests/cfg_hosts_unittest.cc b/src/lib/dhcpsrv/tests/cfg_hosts_unittest.cc index 1df25a79cb..0432587c21 100644 --- a/src/lib/dhcpsrv/tests/cfg_hosts_unittest.cc +++ b/src/lib/dhcpsrv/tests/cfg_hosts_unittest.cc @@ -931,6 +931,29 @@ TEST_F(CfgHostsTest, add4AlreadyReserved) { EXPECT_THROW(cfg.add(host2), isc::dhcp::ReservedAddress); } +// Test that it is possible to allow inserting multiple reservations for +// the same IP address. +TEST_F(CfgHostsTest, allow4AlreadyReserved) { + CfgHosts cfg; + // Allow creating multiple reservations for the same IP address. + ASSERT_TRUE(cfg.setIPReservationUnique(false)); + + // First host has a reservation for address 192.0.2.1 + HostPtr host1 = HostPtr(new Host(hwaddrs_[0]->toText(false), + "hw-address", + SubnetID(1), SubnetID(SUBNET_ID_UNUSED), + IOAddress("192.0.2.1"))); + ASSERT_NO_THROW(cfg.add(host1)); + + // The second host has a reservation for the same address. + HostPtr host2 = HostPtr(new Host(hwaddrs_[1]->toText(false), + "hw-address", + SubnetID(1), SUBNET_ID_UNUSED, + IOAddress("192.0.2.1"))); + // Adding this should work because the HW address is different. + EXPECT_NO_THROW(cfg.add(host2)); +} + // Checks that it's not possible for two hosts to have the same address // reserved at the same time. TEST_F(CfgHostsTest, add6Invalid2Hosts) { @@ -957,6 +980,33 @@ TEST_F(CfgHostsTest, add6Invalid2Hosts) { EXPECT_THROW(cfg.add(host2), isc::dhcp::DuplicateHost); } +// Test that it is possible to allow inserting multiple reservations for +// the same IPv6 address. +TEST_F(CfgHostsTest, allow6AlreadyReserved) { + CfgHosts cfg; + // Allow creating multiple reservations for the same IP address. + ASSERT_TRUE(cfg.setIPReservationUnique(false)); + + // First host has a reservation for address 2001:db8::1 + HostPtr host1 = HostPtr(new Host(duids_[0]->toText(), "duid", + SUBNET_ID_UNUSED, SubnetID(1), + IOAddress("0.0.0.0"))); + host1->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA, + IOAddress("2001:db8::1"))); + // Adding this should work. + EXPECT_NO_THROW(cfg.add(host1)); + + // The second host has a reservation for the same address. + HostPtr host2 = HostPtr(new Host(duids_[1]->toText(), "duid", + SUBNET_ID_UNUSED, SubnetID(1), + IOAddress("0.0.0.0"))); + host2->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA, + IOAddress("2001:db8::1"))); + + // Adding this should work because the DUID is different. + EXPECT_NO_THROW(cfg.add(host2)); +} + // Check that no error is reported when adding a host with subnet // ids equal to global. TEST_F(CfgHostsTest, globalSubnetIDs) { diff --git a/src/lib/dhcpsrv/tests/cql_host_data_source_unittest.cc b/src/lib/dhcpsrv/tests/cql_host_data_source_unittest.cc index 1466f6f05d..d7d372fbfb 100644 --- a/src/lib/dhcpsrv/tests/cql_host_data_source_unittest.cc +++ b/src/lib/dhcpsrv/tests/cql_host_data_source_unittest.cc @@ -619,6 +619,12 @@ TEST_F(CqlHostDataSourceTest, addDuplicate4) { testAddDuplicate4(); } +/// @brief Test that the CQL backend does not support using non-unique +/// IP addresses between multiple reservations. +TEST_F(CqlHostDataSourceTest, disallowDuplicateIP) { + testDisallowDuplicateIP(); +} + // This test verifies that DHCPv4 options can be inserted in a binary format /// and retrieved from the CQL host database. TEST_F(CqlHostDataSourceTest, optionsReservations4) { diff --git a/src/lib/dhcpsrv/tests/host_cache_unittest.cc b/src/lib/dhcpsrv/tests/host_cache_unittest.cc index e277b7f41d..1a9e1e6cdf 100644 --- a/src/lib/dhcpsrv/tests/host_cache_unittest.cc +++ b/src/lib/dhcpsrv/tests/host_cache_unittest.cc @@ -692,6 +692,10 @@ public: return ("one"); } + bool setIPReservationUnique(const bool) { + return (true); + } + /// Specific methods /// @brief Set the entry diff --git a/src/lib/dhcpsrv/tests/host_mgr_unittest.cc b/src/lib/dhcpsrv/tests/host_mgr_unittest.cc index 174c429e6b..adbbd07ec1 100644 --- a/src/lib/dhcpsrv/tests/host_mgr_unittest.cc +++ b/src/lib/dhcpsrv/tests/host_mgr_unittest.cc @@ -1505,10 +1505,18 @@ TEST_F(MySQLHostMgrTest, get6ByPrefix) { testGet6ByPrefix(*getCfgHosts(), HostMgr::instance()); } +// This test verifies that it is possible to control whether the reserved +// IP addresses are unique or non unique via the HostMgr. +TEST_F(MySQLHostMgrTest, setIPReservationUnique) { + EXPECT_TRUE(HostMgr::instance().setIPReservationUnique(true)); + EXPECT_TRUE(HostMgr::instance().setIPReservationUnique(false)); +} + // Verifies that loss of connectivity to MySQL is handled correctly. TEST_F(MySQLHostMgrDbLostCallbackTest, testDbLostCallback) { testDbLostCallback(); } + #endif @@ -1662,6 +1670,13 @@ TEST_F(PostgreSQLHostMgrTest, get6ByPrefix) { testGet6ByPrefix(*getCfgHosts(), HostMgr::instance()); } +// This test verifies that it is possible to control whether the reserved +// IP addresses are unique or non unique via the HostMgr. +TEST_F(PostgreSQLHostMgrTest, setIPReservationUnique) { + EXPECT_TRUE(HostMgr::instance().setIPReservationUnique(true)); + EXPECT_TRUE(HostMgr::instance().setIPReservationUnique(false)); +} + // Verifies that loss of connectivity to PostgreSQL is handled correctly. TEST_F(PostgreSQLHostMgrDbLostCallbackTest, testDbLostCallback) { testDbLostCallback(); @@ -1800,6 +1815,14 @@ TEST_F(CQLHostMgrTest, get6ByPrefix) { testGet6ByPrefix(*getCfgHosts(), HostMgr::instance()); } +// This test verifies that it is possible to control whether the reserved +// IP addresses are unique or non unique via the HostMgr. +TEST_F(CQLHostMgrTest, setIPReservationUnique) { + EXPECT_TRUE(HostMgr::instance().setIPReservationUnique(true)); + // This is currently not supported for Cassandra. + EXPECT_FALSE(HostMgr::instance().setIPReservationUnique(false)); +} + #endif } // namespace 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 40d728bc63..d4f990cca6 100644 --- a/src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc +++ b/src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc @@ -1071,6 +1071,32 @@ TEST_F(MySqlHostDataSourceTest, addDuplicate6WithHWAddrMultiThreading) { testAddDuplicate6WithSameHWAddr(); } +/// @brief Test if the same IPv6 reservation can't be inserted multiple times. +TEST_F(MySqlHostDataSourceTest, addDuplicateIPv6) { + testAddDuplicateIPv6(); +} + +/// @brief Test if the same IPv6 reservation can't be inserted multiple times. +TEST_F(MySqlHostDataSourceTest, addDuplicateIPv6MultiThreading) { + MultiThreadingTest mt(true); + testAddDuplicateIPv6(); +} + +/// @brief Test if the host reservation for the same IPv6 address can be inserted +/// multiple times when allowed by the configuration and when the host identifier +/// is different. +TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv6) { + testAllowDuplicateIPv6(); +} + +/// @brief Test if the host reservation for the same IPv6 address can be inserted +/// multiple times when allowed by the configuration and when the host identifier +/// is different. +TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv6MultiThreading) { + MultiThreadingTest mt(true); + testAllowDuplicateIPv6(); +} + /// @brief Test if the duplicate IPv4 host instances can't be inserted. The test logic is as /// follows: try to add multiple instances of the same host reservation and /// verify that the second and following attempts will throw exceptions. @@ -1086,6 +1112,21 @@ TEST_F(MySqlHostDataSourceTest, addDuplicate4MultiThreading) { testAddDuplicate4(); } +/// @brief Test if the host reservation for the same IPv4 address can be inserted +/// multiple times when allowed by the configuration and when the host identifier +/// is different. +TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv4) { + testAllowDuplicateIPv4(); +} + +/// @brief Test if the host reservation for the same IPv4 address can be inserted +/// multiple times when allowed by the configuration and when the host identifier +/// is different. +TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv4MultiThreading) { + MultiThreadingTest mt(true); + testAllowDuplicateIPv4(); +} + /// @brief This test verifies that DHCPv4 options can be inserted in a binary format /// and retrieved from the MySQL host database. TEST_F(MySqlHostDataSourceTest, optionsReservations4) { 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 6d1e0a4e1d..07551a0cb3 100644 --- a/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc +++ b/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc @@ -1080,6 +1080,17 @@ TEST_F(PgSqlHostDataSourceTest, addDuplicate6WithHWAddrMultiThreading) { testAddDuplicate6WithSameHWAddr(); } +/// @brief Test if the same IPv6 reservation can't be inserted multiple times. +TEST_F(PgSqlHostDataSourceTest, addDuplicateIPv6) { + testAddDuplicateIPv6(); +} + +/// @brief Test if the same IPv6 reservation can't be inserted multiple times. +TEST_F(PgSqlHostDataSourceTest, addDuplicateIPv6MultiThreading) { + MultiThreadingTest mt(true); + testAddDuplicateIPv6(); +} + /// @brief Test if the duplicate IPv4 host instances can't be inserted. The test logic is as /// follows: try to add multiple instances of the same host reservation and /// verify that the second and following attempts will throw exceptions. @@ -1095,6 +1106,21 @@ TEST_F(PgSqlHostDataSourceTest, addDuplicate4MultiThreading) { testAddDuplicate4(); } +/// @brief Test if the host reservation for the same IPv4 address can be inserted +/// multiple times when allowed by the configuration and when the host identifier +/// is different. +TEST_F(PgSqlHostDataSourceTest, allowDuplicateIPv4) { + testAllowDuplicateIPv4(); +} + +/// @brief Test if the host reservation for the same IPv4 address can be inserted +/// multiple times when allowed by the configuration and when the host identifier +/// is different. +TEST_F(PgSqlHostDataSourceTest, allowDuplicateIPv4MultiThreading) { + MultiThreadingTest mt(true); + testAllowDuplicateIPv4(); +} + /// @brief This test verifies that DHCPv4 options can be inserted in a binary format /// and retrieved from the PostgreSQL host database. TEST_F(PgSqlHostDataSourceTest, optionsReservations4) { diff --git a/src/lib/dhcpsrv/testutils/generic_host_data_source_unittest.cc b/src/lib/dhcpsrv/testutils/generic_host_data_source_unittest.cc index 9cd30e8fa3..8e34bc1506 100644 --- a/src/lib/dhcpsrv/testutils/generic_host_data_source_unittest.cc +++ b/src/lib/dhcpsrv/testutils/generic_host_data_source_unittest.cc @@ -1809,6 +1809,47 @@ GenericHostDataSourceTest::testAddDuplicate6WithSameHWAddr() { } void +GenericHostDataSourceTest::testAddDuplicateIPv6() { + // Make sure we have the pointer to the host data source. + ASSERT_TRUE(hdsptr_); + + // Create a host reservation. + HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8::1", Host::IDENT_HWADDR, true); + + // Add this reservation once. + ASSERT_NO_THROW(hdsptr_->add(host)); + + // Create a host with a different identifier but the same IPv6 address. An attempt + // to create the reservation for the same IPv6 address should fail. + host = HostDataSourceUtils::initializeHost6("2001:db8::1", Host::IDENT_HWADDR, true); + EXPECT_THROW(hdsptr_->add(host), DuplicateEntry); +} + +void +GenericHostDataSourceTest::testAllowDuplicateIPv6() { + // Make sure we have the pointer to the host data source. + ASSERT_TRUE(hdsptr_); + ASSERT_TRUE(hdsptr_->setIPReservationUnique(false)); + + // Create a host reservations. + HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8::1", Host::IDENT_HWADDR, true); + + // Add this reservation once. + ASSERT_NO_THROW(hdsptr_->add(host)); + + // Then try to add it again, it should throw an exception because the + // HWADDR is the same. + ASSERT_THROW(hdsptr_->add(host), DuplicateEntry); + + // This time use a different host identifier and try again. + // This update should succeed because we permitted to create + // multiple IP reservations for the same IP address but different + // identifier. + ASSERT_NO_THROW(host->setIdentifier("01:02:03:04:05:06", "hw-address")); + ASSERT_NO_THROW(hdsptr_->add(host)); +} + +void GenericHostDataSourceTest::testAddDuplicate4() { // Make sure we have the pointer to the host data source. ASSERT_TRUE(hdsptr_); @@ -1835,6 +1876,42 @@ GenericHostDataSourceTest::testAddDuplicate4() { } void +GenericHostDataSourceTest::testDisallowDuplicateIP() { + // Make sure we have the pointer to the host data source. + ASSERT_TRUE(hdsptr_); + // The backend does not support switching to the mode in which multiple + // reservations for the same address can be created. + EXPECT_FALSE(hdsptr_->setIPReservationUnique(false)); + + // The default mode still can be used. + EXPECT_TRUE(hdsptr_->setIPReservationUnique(true)); +} + +void +GenericHostDataSourceTest::testAllowDuplicateIPv4() { + // Make sure we have the pointer to the host data source. + ASSERT_TRUE(hdsptr_); + ASSERT_TRUE(hdsptr_->setIPReservationUnique(false)); + + // Create a host reservations. + HostPtr host = HostDataSourceUtils::initializeHost4("192.0.2.1", Host::IDENT_DUID); + + // Add this reservation once. + ASSERT_NO_THROW(hdsptr_->add(host)); + + // Then try to add it again, it should throw an exception because the + // DUID is the same. + ASSERT_THROW(hdsptr_->add(host), DuplicateEntry); + + // This time use a different host identifier and try again. + // This update should succeed because we permitted to create + // multiple IP reservations for the same IP address but different + // identifier. + ASSERT_NO_THROW(host->setIdentifier("01:02:03:04:05:06", "hw-address")); + ASSERT_NO_THROW(hdsptr_->add(host)); +} + +void GenericHostDataSourceTest::testAddr6AndPrefix() { // Make sure we have the pointer to the host data source. ASSERT_TRUE(hdsptr_); diff --git a/src/lib/dhcpsrv/testutils/generic_host_data_source_unittest.h b/src/lib/dhcpsrv/testutils/generic_host_data_source_unittest.h index 57cb9f6cff..1420f306d2 100644 --- a/src/lib/dhcpsrv/testutils/generic_host_data_source_unittest.h +++ b/src/lib/dhcpsrv/testutils/generic_host_data_source_unittest.h @@ -382,11 +382,32 @@ public: /// Uses gtest macros to report failures. void testAddDuplicate6WithSameHWAddr(); - /// @brief Test if the duplicate IPv4 host with can't be inserted. + /// @brief Test that duplicate IPv6 reservation can't be inserted. + /// + /// Uses gtest macros to report failures. + void testAddDuplicateIPv6(); + + /// @brief Test if the reservation for the same IPv6 address can be + /// inserted when allowed by the configuration. + /// + /// Uses gtest macros to report failures. + void testAllowDuplicateIPv6(); + + /// @brief Test that duplicate IPv4 reservation can't be inserted. /// /// Uses gtest macros to report failures. void testAddDuplicate4(); + /// @brief Test that the backend does not support a mode in which multiple + /// host reservations for the same IP address can be created. + void testDisallowDuplicateIP(); + + /// @brief Test if the reservation for the same IPv4 address can be + /// inserted when allowed by the configuration. + /// + /// Uses gtest macros to report failures. + void testAllowDuplicateIPv4(); + /// @brief Test that DHCPv4 options can be inserted and retrieved from /// the database. /// diff --git a/src/lib/dhcpsrv/testutils/memory_host_data_source.h b/src/lib/dhcpsrv/testutils/memory_host_data_source.h index f3f4ae6d8a..7ea08e0b08 100644 --- a/src/lib/dhcpsrv/testutils/memory_host_data_source.h +++ b/src/lib/dhcpsrv/testutils/memory_host_data_source.h @@ -267,6 +267,29 @@ public: /// @return number of hosts in the store. virtual size_t size() const; + /// @brief Controls whether IP reservations are unique or non-unique. + /// + + /// In a typical case, the IP reservations are unique and backends verify + /// prior to adding a host reservation to the database that the reservation + /// for a given IP address does not exist. In some cases it may be required + /// to allow non-unique IP reservations, e.g. in the case when a host has + /// several interfaces and independently of which interface is used by this + /// host to communicate with the DHCP server the same IP address should be + /// assigned. In this case the @c unique value should be set to false to + /// disable the checks for uniqueness on the backend side. + /// + /// All backends are required to support the case when unique setting is + /// @c true and they must use this setting by default. + /// + /// @param unique boolean flag indicating if the IP reservations must be + /// unique or can be non-unique. + /// @return true if the new setting was accepted by the backend or false + /// otherwise. + virtual bool setIPReservationUnique(const bool) { + return (true); + } + protected: // This is very simple storage. |