diff options
Diffstat (limited to 'src/lib/dhcpsrv/tests/alloc_engine_expiration_unittest.cc')
-rw-r--r-- | src/lib/dhcpsrv/tests/alloc_engine_expiration_unittest.cc | 2323 |
1 files changed, 2323 insertions, 0 deletions
diff --git a/src/lib/dhcpsrv/tests/alloc_engine_expiration_unittest.cc b/src/lib/dhcpsrv/tests/alloc_engine_expiration_unittest.cc new file mode 100644 index 0000000..c758e47 --- /dev/null +++ b/src/lib/dhcpsrv/tests/alloc_engine_expiration_unittest.cc @@ -0,0 +1,2323 @@ +// Copyright (C) 2015-2020 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <dhcp/duid.h> +#include <dhcp/option_data_types.h> +#include <dhcp_ddns/ncr_msg.h> +#include <dhcpsrv/tests/alloc_engine_utils.h> +#include <dhcpsrv/testutils/test_utils.h> +#include <hooks/hooks_manager.h> +#include <stats/stats_mgr.h> +#include <gtest/gtest.h> +#include <boost/static_assert.hpp> +#include <functional> +#include <iomanip> +#include <sstream> +#include <time.h> +#include <unistd.h> +#include <vector> + +using namespace std; +using namespace isc; +using namespace isc::asiolink; +using namespace isc::dhcp; +using namespace isc::dhcp::test; +using namespace isc::dhcp_ddns; +using namespace isc::hooks; +using namespace isc::stats; +namespace ph = std::placeholders; + +namespace { + +/// @brief Number of leases to be initialized for each test. +/// +/// This value is expected by some of the tests to be multiple +/// of 10. +const unsigned int TEST_LEASES_NUM = 100; + +/// @brief Structure wrapping a lower limit within the collection +/// of leases. +/// +/// We're using this structure rather than a size_t value directly +/// to make API of the test fixture class more readable, i.e. the +/// struct name indicates the purpose of the value being held. +struct LowerBound { + /// @brief Constructor. + /// + /// @param lower_bound Integer value wrapped by the structure. + explicit LowerBound(const size_t lower_bound) + : lower_bound_(lower_bound) { }; + + /// @brief Operator returning the size_t value wrapped. + operator size_t() const { + return (lower_bound_); + } + + /// @brief Value wrapped in the structure. + size_t lower_bound_; +}; + +/// @brief Structure wrapping an upper limit within the collection +/// of leases. +/// +/// We're using this structure rather than a size_t value directly +/// to make API of the test fixture class more readable, i.e. the +/// struct name indicates the purpose of the value being held. +struct UpperBound { + /// @brief Constructor. + /// + /// @param lower_bound Integer value wrapped by the structure. + explicit UpperBound(const size_t upper_bound) + : upper_bound_(upper_bound) { }; + + /// @brief Operator returning the size_t value wrapped. + operator size_t() const { + return (upper_bound_); + } + + /// @brief Value wrapped in the structure. + size_t upper_bound_; +}; + +/// @brief List holding addresses for executed callouts. +std::list<IOAddress> callouts_; + +/// @brief Callout argument name for expired lease. +std::string callout_argument_name("lease4"); + +/// @brief Base test fixture class for the lease reclamation routines in the +/// @c AllocEngine. +/// +/// This class implements infrastructure for testing leases reclamation +/// routines. The lease reclamation routine has the following +/// characteristic: +/// - it processes multiple leases, +/// - leases are processed in certain order, +/// - number of processed leases may be limited by the parameters, +/// - maximum duration of the lease reclamation routine may be limited, +/// - reclaimed leases may be marked as reclaimed or deleted, +/// - DNS records for some of the leases must be removed when the lease +/// is reclaimed and DNS updates are enabled, +/// - hooks must be invoked (if installed) for each reclaimed lease +/// - statistics must be updated to increase the number of reclaimed +/// leases and decrease the number of allocated leases +/// +/// The typical test requires many leases to be initialized and stored +/// in the lease database for the test. The test fixture class creates +/// these leases upon construction. It is possible to modify these +/// leases to test various properties of the lease reclamation routine +/// as listed above. For example: some of the leases may be marked +/// as expired or hostname may be cleared for some of the leases to +/// check that DNS updates are not generated for them. +/// +/// The tests are built around the +/// @c ExpirationAllocEngineTest::testLeases methods. These methods +/// verify that the certain operations have been performed by the +/// lease reclamation routine on selected leases. The leases for which +/// certain conditions should be met are selected using the "index +/// algorithms". Various index algorithms are implemented in the +/// test fixture class as static functions and the algorithm is +/// selected by passing function pointer to the @c testLeases method. +/// +/// Examples of index algorithms are: +/// - evenLeaseIndex(index) - picks even index numbers, +/// - oddLeaseIndex(index) - picks odd index numbers, +/// - allLeasesIndexes(index) - picks all index number. +/// +/// For example, the test may use the @c evenLeaseIndex algorithm +/// to mark leases with even indexes as expired and then test whether +/// leases with even indexes have been successfully reclaimed. +/// +/// The "lease algorithm" verifies if the given lease fulfills the +/// specific conditions after reclamation. These are the examples of +/// the lease algorithms: +/// - leaseExists - lease still exists in the database, +/// - leaseDoesntExist - lease removed from the database, +/// - leaseReclaimed - lease exists but has reclaimed status, +/// - leaseNotReclaimed - lease exists and is not in the reclaimed status, +/// - leaseDeclined - lease exists and is in declined state, +/// - dnsUpdateGeneratedForLease - DNS updates generated for lease, +/// - dnsUpdateNotGeneratedForLease - DNS updates not generated for lease +/// +/// The combination of index algorithm and lease algorithm allows for +/// verifying that the whole sets of leases in the lease database fulfill +/// certain conditions. For example, it is possible to verify that +/// after lease reclamation leases with even indexes have state set to +/// "expired-reclaimed". +/// +/// See @c ExpirationAllocEngineTest::testLeases for further details. +/// +/// @todo These tests should be extended to cover the following cases: +/// - declined leases - declined leases expire and should be removed +/// from the lease database by the lease reclamation routine. See +/// ticket #3976. +template<typename LeasePtrType> +class ExpirationAllocEngineTest : public ::testing::Test { +public: + + /// @brief Type definition for the lease algorithm. + typedef std::function<bool (const LeasePtrType)> LeaseAlgorithmFun; + /// @brief type definition for the lease index algorithm. + typedef std::function<bool (const size_t)> IndexAlgorithmFun; + + /// @brief Constructor. + /// + /// Clears configuration, creates new lease manager and allocation engine + /// instances. + ExpirationAllocEngineTest(const std::string& lease_mgr_params) { + // Clear configuration. + CfgMgr::instance().clear(); + D2ClientConfigPtr cfg(new D2ClientConfig()); + CfgMgr::instance().setD2ClientConfig(cfg); + + // Remove all statistics. + StatsMgr::instance().resetAll(); + + // Set the 'reclaimed-leases' statistics to '0'. This statistics + // is used by some tests to verify that the leases reclamation + // routine has been called. + StatsMgr::instance().setValue("reclaimed-leases", + static_cast<int64_t>(0)); + + // Create lease manager. + LeaseMgrFactory::create(lease_mgr_params); + + // Create allocation engine instance. + engine_.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, + 100, true)); + } + + /// @brief Destructor + /// + /// Stops D2 client (if running), clears configuration and removes + /// an instance of the lease manager. + virtual ~ExpirationAllocEngineTest() { + // Stop D2 client if running and remove all queued name change + // requests. + D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr(); + if (mgr.amSending()) { + mgr.stopSender(); + mgr.clearQueue(); + } + + // Clear configuration. + CfgMgr::instance().clear(); + D2ClientConfigPtr cfg(new D2ClientConfig()); + CfgMgr::instance().setD2ClientConfig(cfg); + + // Remove all statistics. + StatsMgr::instance().resetAll(); + + // Kill lease manager. + LeaseMgrFactory::destroy(); + + // Remove callouts executed. + callouts_.clear(); + + // Unload libraries. + bool status = HooksManager::unloadLibraries(); + if (!status) { + cerr << "(fixture dtor) unloadLibraries failed" << endl; + } + } + + /// @brief Starts D2 client. + void enableDDNS() const { + // Start DDNS and assign no-op error handler. + D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr(); + D2ClientConfigPtr cfg(new D2ClientConfig()); + cfg->enableUpdates(true); + mgr.setD2ClientConfig(cfg); + mgr.startSender(std::bind(&ExpirationAllocEngineTest::d2ErrorHandler, ph::_1, ph::_2)); + } + + /// @brief No-op error handler for the D2 client. + static void d2ErrorHandler(const dhcp_ddns::NameChangeSender::Result, + dhcp_ddns::NameChangeRequestPtr&) { + } + + /// @brief Marks a lease as expired. + /// + /// @param lease_index Lease index. Must be between 0 and + /// @c TEST_LEASES_NUM. + /// @param secs Offset of the expiration time since now. For example + /// a value of 2 would set the lease expiration time to 2 seconds ago. + void expire(const uint16_t lease_index, const time_t secs) { + ASSERT_GT(leases_.size(), lease_index); + // We set the expiration time indirectly by modifying the cltt value. + leases_[lease_index]->cltt_ = time(NULL) - secs - + leases_[lease_index]->valid_lft_; + ASSERT_NO_THROW(updateLease(lease_index)); + } + + /// @brief Changes the owner of a lease. + /// + /// This method changes the owner of the lease. It must be implemented in + /// the derived classes to update the unique identifier(s) in the lease to + /// point to a different client. + /// + /// @param lease_index Lease index. Must be between 0 and + /// @c TEST_LEASES_NUM. + virtual void transferOwnership(const uint16_t lease_index) = 0; + + /// @brief Marks lease as expired-reclaimed. + /// + /// @param lease_index Lease index. Must be between 0 and + /// @c TEST_LEASES_NUM. + /// @param secs Offset of the expiration time since now. For example + /// a value of 2 would set the lease expiration time to 2 seconds ago. + void reclaim(const uint16_t lease_index, const time_t secs) { + ASSERT_GT(leases_.size(), lease_index); + leases_[lease_index]->cltt_ = time(NULL) - secs - + leases_[lease_index]->valid_lft_; + leases_[lease_index]->state_ = Lease::STATE_EXPIRED_RECLAIMED; + ASSERT_NO_THROW(updateLease(lease_index)); + } + + /// @brief Declines specified lease + /// + /// Sets specified lease to declined state and sets its probation-period. + /// @param lease_index Index of the lease. + /// @param probation_time value of probation period to be set (in seconds) + void decline(const uint16_t lease_index, const time_t probation_time) { + ASSERT_GT(leases_.size(), lease_index); + leases_[lease_index]->decline(probation_time); + ASSERT_NO_THROW(updateLease(lease_index)); + } + + /// @brief Updates lease in the lease database. + /// + /// @param lease_index Index of the lease. + virtual void updateLease(const unsigned int lease_index) = 0; + + /// @brief Retrieves lease from the database. + /// + /// @param lease_index Index of the lease. + virtual LeasePtrType getLease(const unsigned int lease_index) const = 0; + + /// @brief Sets subnet id for a lease. + /// + /// It also updates statistics of assigned leases in the stats manager. + /// + /// @param lease_index Lease index. + /// @param id New subnet id. + virtual void setSubnetId(const uint16_t lease_index, const SubnetID& id) = 0; + + /// @brief Wrapper method running lease reclamation routine. + /// + /// @param max_leases Maximum number of leases to be reclaimed. + /// @param timeout Maximum amount of time that the reclamation routine + /// may be processing expired leases, expressed in seconds. + /// @param remove_lease A boolean value indicating if the lease should + /// be removed when it is reclaimed (if true) or it should be left in the + /// database in the "expired-reclaimed" state (if false). + virtual void reclaimExpiredLeases(const size_t max_leases, + const uint16_t timeout, + const bool remove_lease) = 0; + + /// @brief Wrapper method for removing expired-reclaimed leases. + /// + /// @param secs The minimum amount of time, expressed in seconds, + /// for the lease to be left in the "expired-reclaimed" state + /// before it can be removed. + virtual void deleteExpiredReclaimedLeases(const uint32_t secs) = 0; + + /// @brief Test selected leases using the specified algorithms. + /// + /// This function picks leases from the range of 0 thru + /// @c TEST_LEASES_NUM and selects the ones to be verified using the + /// specified index algorithm. Selected leases are tested using + /// the specified lease algorithm. + /// + /// @param lease_algorithm Pointer to the lease algorithm function. + /// @param index_algorithm Pointer to the index algorithm function. + bool testLeases(const LeaseAlgorithmFun& lease_algorithm, + const IndexAlgorithmFun& index_algorithm) const { + // No limits are specified, so test all leases in the range of + // 0 .. TEST_LEASES_NUM. + return (testLeases(lease_algorithm, index_algorithm, LowerBound(0), + UpperBound(TEST_LEASES_NUM))); + } + + + /// @brief Test selected leases using the specified algorithms. + /// + /// This function picks leases from the range of @c lower_bound + /// thru @c upper_bound and selects the ones to be verified using the + /// specified index algorithm. Selected leases are tested using the + /// specified lease algorithm. + /// + /// @param lease_algorithm Pointer to the lease algorithm function. + /// @param index_algorithm Pointer to the index algorithm function. + /// @param lower_bound First index in the range. + /// @param upper_bound Last + 1 index in the range. + bool testLeases(const LeaseAlgorithmFun& lease_algorithm, + const IndexAlgorithmFun& index_algorithm, + const LowerBound& lower_bound, + const UpperBound& upper_bound) const { + // Select leases between the lower_bound and upper_bound. + for (size_t i = lower_bound; i < upper_bound; ++i) { + // Get the lease from the lease database. + LeasePtrType lease = getLease(i); + // index_algorithm(i) checks if the lease should be checked. + // If so, check if the lease_algorithm indicates that the + // lease fulfills a given condition, e.g. is present in the + // database. If not, return false. + if (index_algorithm(i) && !lease_algorithm(lease)) { + return (false); + } + } + // All leases checked, so return true. + return (true); + } + + /// @brief Index algorithm selecting even indexes. + /// + /// @param index Lease index. + /// @return true if index is an even number. + static bool evenLeaseIndex(const size_t index) { + return (index % 2 == 0); + } + + /// @brief Index algorithm selecting odd indexes. + /// + /// @param index Lease index. + /// @return true if index is an odd number. + static bool oddLeaseIndex(const size_t index) { + return (!evenLeaseIndex(index)); + } + + /// @brief Index algorithm selecting all indexes. + /// + /// @param index Lease index. + /// @return true if the index is in the range of [0 .. TEST_LEASES_NUM). + static bool allLeaseIndexes(const size_t index) { + return (index < TEST_LEASES_NUM); + } + + /// @brief Lease algorithm checking if lease exists. + /// + /// @param lease Pointer to lease. + /// @return true if lease pointer is non-null. + static bool leaseExists(const LeasePtrType& lease) { + return (static_cast<bool>(lease)); + } + + /// @brief Lease algorithm checking if lease doesn't exist. + /// + /// @param lease Pointer to lease. + /// @return true if lease pointer is null. + static bool leaseDoesntExist(const LeasePtrType& lease) { + return (static_cast<bool>(!lease)); + } + + /// @brief Lease algorithm checking if lease state is expired-reclaimed. + /// + /// This algorithm also checks that the FQDN information has been removed + /// from the lease. + /// + /// @param lease Pointer to lease. + /// @return true if lease state is "expired-reclaimed" and the FQDN + /// information has been removed from the lease. + static bool leaseReclaimed(const LeasePtrType& lease) { + return (lease && lease->stateExpiredReclaimed() && + lease->hostname_.empty() && !lease->fqdn_fwd_ && + !lease->fqdn_rev_); + } + + /// @brief Lease algorithm checking if lease state is Declined. + /// + /// @param lease Pointer to lease. + /// @return true if lease state is "declined". + static bool leaseDeclined(const LeasePtrType& lease) { + return (lease && lease->stateDeclined()); + } + + /// @brief Lease algorithm checking if lease state is not + /// expired-reclaimed. + /// + /// @param lease Pointer to lease. + /// @return true if lease state is not "expired-reclaimed". + static bool leaseNotReclaimed(const LeasePtrType& lease) { + return (lease && !lease->stateExpiredReclaimed()); + } + + /// @brief Lease algorithm checking if removal name change request + /// has been generated for lease. + /// + /// @param lease Pointer to lease. + /// @return true if NCR has been generated for the lease. + static bool dnsUpdateGeneratedForLease(const LeasePtrType& lease) { + try { + return (static_cast<bool>(getNCRForLease(lease))); + + } catch (...) { + // If error occurred, treat it as no match. + return (false); + } + } + + /// @brief Lease algorithm checking if removal name change request + /// hasn't been generated for lease. + /// + /// @param lease Pointer to lease. + /// @return true if NCR has not been generated for the lease. + static bool dnsUpdateNotGeneratedForLease(const LeasePtrType& lease) { + try { + // Iterate over the generated name change requests and try + // to find the match with our lease (using IP address). If + D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr(); + for (size_t i = 0; i < mgr.getQueueSize(); ++i) { + const NameChangeRequestPtr& ncr = mgr.peekAt(i); + // If match found, we treat it as if the test fails + // because we expected no NCR. + if (ncr->getIpAddress() == lease->addr_.toText()) { + return (false); + } + } + } catch (...) { + return (false); + } + + // No match found, so we're good. + return (true); + } + + /// @brief Lease algorithm checking if callout has been executed for + /// the expired lease. + /// + /// @param lease Pointer to lease. + /// @return true if callout has been executed for the lease. + static bool leaseCalloutExecuted(const LeasePtrType& lease) { + return (std::find(callouts_.begin(), callouts_.end(), lease->addr_) != + callouts_.end()); + } + + /// @brief Lease algorithm checking if callout hasn't been executed for + /// the expired lease. + /// + /// @param lease Pointer to lease. + /// @return true if callout hasn't been executed for the lease. + static bool leaseCalloutNotExecuted(const LeasePtrType& lease) { + return (!leaseCalloutExecuted(lease)); + } + + /// @brief Implements "lease{4,6}_expire" callout. + /// + /// @param callout_handle Callout handle. + /// @return Zero. + static int leaseExpireCallout(CalloutHandle& callout_handle) { + LeasePtrType lease; + callout_handle.getArgument(callout_argument_name, lease); + bool remove_lease = true; + callout_handle.getArgument("remove_lease", remove_lease); + + // Check if the remove_lease is set to false and assume that the callout + // has been successfully executed if it is. This is mainly to test + // that the lease reclamation routine sets this value at all. + if (!remove_lease) { + callouts_.push_back(lease->addr_); + } + + return (0); + } + + /// @brief Implements "lease{4,6}_expire callout returning skip flag. + /// + /// @param callout_handle Callout handle. + /// @return Zero. + static int leaseExpireWithSkipCallout(CalloutHandle& callout_handle) { + leaseExpireCallout(callout_handle); + callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP); + + return (0); + } + + /// @brief Implements "lease{4,6}_expire callout, which lasts at least + /// 40ms. + /// + /// This callout is useful to test scenarios where the reclamation of the + /// lease needs to take a known amount of time. If the callout is installed + /// it will take at least 40ms for each lease. It is then possible to calculate + /// the approximate time that the reclamation of all leases would take and + /// test that the timeouts for the leases' reclamation work as expected. + /// + /// The value of 40ms is relatively high, but it has been selected to + /// mitigate the problems with usleep on some virtual machines. On those + /// machines the wakeup from usleep may take significant amount of time, + /// i.e. usually around 10ms. Thus, the sleep time should be considerably + /// higher than this delay. + /// + /// @param callout_handle Callout handle. + /// @return Zero. + static int leaseExpireWithDelayCallout(CalloutHandle& callout_handle) { + leaseExpireCallout(callout_handle); + // Delay the return from the callout by 40ms. + usleep(40000); + + return (0); + } + + /// @brief Returns removal name change request from the D2 client queue. + /// + /// @param lease Pointer to the lease to be matched with NCR. + /// + /// @return null pointer if no match found. + static NameChangeRequestPtr getNCRForLease(const LeasePtrType& lease) { + // Iterate over the generated name change requests and try + // to find the match with our lease (using IP address). If + D2ClientMgr& mgr = CfgMgr::instance().getD2ClientMgr(); + for (size_t i = 0; i < mgr.getQueueSize(); ++i) { + const NameChangeRequestPtr& ncr = mgr.peekAt(i); + // If match found, return true. + if ((ncr->getIpAddress() == lease->addr_.toText()) && + (ncr->getChangeType() == CHG_REMOVE)) { + return (ncr); + } + } + return (NameChangeRequestPtr()); + } + + /// @brief Returns index of the lease from the address. + /// + /// This method assumes that leases are ordered from the smallest to + /// the highest address, e.g. 10.0.0.0, 10.0.0.1, 10.0.0.2 etc. The + /// last two bytes can be used to extract index. + /// + /// @param address Address. + /// + /// @return index + static uint16_t getLeaseIndexFromAddress(const IOAddress& address) { + std::vector<uint8_t> bytes = address.toBytes(); + std::vector<uint8_t>::reverse_iterator bytes_it = bytes.rbegin(); + uint16_t index = static_cast<uint16_t>(*bytes_it) | + (static_cast<uint16_t>(*(bytes_it + 1)) << 8); + return (index); + } + + /// @brief Generates hostname for lease index. + /// + /// Generates hostname in the form of "hostXXXX.example.org", where + /// XXXX is a lease index. + /// + /// @param index Lease index. + /// + /// @return Generated hostname. + static std::string generateHostnameForLeaseIndex(const uint16_t index) { + std::ostringstream hostname_s; + hostname_s << "host" << std::setw(4) << std::setfill('0') + << index << ".example.org"; + return (hostname_s.str()); + } + + /// @brief Test that leases can be reclaimed without being removed. + void testReclaimExpiredLeasesUpdateState() { + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Mark leases with even indexes as expired. + if (evenLeaseIndex(i)) { + // The higher the index, the more expired the lease. + expire(i, 10 + i); + } + } + + // Run leases reclamation routine on all leases. This should result + // in setting "expired-reclaimed" state for all leases with even + // indexes. + ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false)); + + // Leases with even indexes should be marked as reclaimed. + EXPECT_TRUE(testLeases(&leaseReclaimed, &evenLeaseIndex)); + // Leases with odd indexes shouldn't be marked as reclaimed. + EXPECT_TRUE(testLeases(&leaseNotReclaimed, &oddLeaseIndex)); + } + + /// @brief Test that the leases may be reclaimed by being deleted. + void testReclaimExpiredLeasesDelete() { + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Mark leases with even indexes as expired. + if (evenLeaseIndex(i)) { + // The higher the index, the more expired the lease. + expire(i, 10 + i); + } + } + + // Run leases reclamation routine on all leases. This should result + // in removal of all leases with even indexes. + ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, true)); + + // Leases with odd indexes should be retained and their state + // shouldn't be "expired-reclaimed". + EXPECT_TRUE(testLeases(&leaseNotReclaimed, &oddLeaseIndex)); + // Leases with even indexes should have been removed. + EXPECT_TRUE(testLeases(&leaseDoesntExist, &evenLeaseIndex)); + } + + /// @brief Test that it is possible to specify the limit for the number + /// of reclaimed leases. + void testReclaimExpiredLeasesLimit() { + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Mark all leases as expired. The higher the index the less + // expired the lease. + expire(i, 1000 - i); + } + + // We will be performing lease reclamation on lease groups of 10. + // Hence, it is convenient if the number of test leases is a + // multiple of 10. + const size_t reclamation_group_size = 10; + BOOST_STATIC_ASSERT(TEST_LEASES_NUM % reclamation_group_size == 0); + + // Leases will be reclaimed in groups of 10. + for (unsigned int i = reclamation_group_size; i < TEST_LEASES_NUM; + i += reclamation_group_size) { + + // Reclaim 10 most expired leases out of TEST_LEASES_NUM. Since + // leases are ordered from the most expired to the least expired + // this should reclaim leases between 0 and 9, then 10 and 19 etc. + ASSERT_NO_THROW(reclaimExpiredLeases(reclamation_group_size, + 0, false)); + + // Check that leases having all indexes between 0 and 9, 19, 29 etc. + // have been reclaimed. + EXPECT_TRUE(testLeases(&leaseReclaimed, &allLeaseIndexes, + LowerBound(0), UpperBound(i))) + << "check failed for i = " << i; + + // Check that all remaining leases haven't been reclaimed. + EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes, + LowerBound(i), UpperBound(TEST_LEASES_NUM))) + << "check failed for i = " << i; + } + } + + /// @brief Test that DNS updates are generated for the leases for which + /// the DNS records exist. + void testReclaimExpiredLeasesWithDDNS() { + // DNS must be started for the D2 client to accept NCRs. + ASSERT_NO_THROW(enableDDNS()); + + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Expire all leases with even indexes. + if (evenLeaseIndex(i)) { + // The higher the index, the more expired the lease. + expire(i, 10 + i); + } + } + + // Reclaim all expired leases. + ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false)); + + // Leases with odd indexes shouldn't be reclaimed. + EXPECT_TRUE(testLeases(&leaseNotReclaimed, &oddLeaseIndex)); + // Leases with even indexes should be reclaimed. + EXPECT_TRUE(testLeases(&leaseReclaimed, &evenLeaseIndex)); + // DNS updates (removal NCRs) should be generated for leases with even + // indexes. + EXPECT_TRUE(testLeases(&dnsUpdateGeneratedForLease, &evenLeaseIndex)); + // DNS updates (removal NCRs) shouldn't be generated for leases with + // odd indexes. + EXPECT_TRUE(testLeases(&dnsUpdateNotGeneratedForLease, &oddLeaseIndex)); + } + + /// @brief Test that DNS updates are only generated for the reclaimed + /// leases (not for all leases with hostname stored). + void testReclaimExpiredLeasesWithDDNSAndLimit() { + // DNS must be started for the D2 client to accept NCRs. + ASSERT_NO_THROW(enableDDNS()); + + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Expire only leases with even indexes. + if (evenLeaseIndex(i)) { + // The higher the index, the more expired the lease. + expire(i, 10 + i); + } + } + + const size_t reclamation_group_size = 10; + BOOST_STATIC_ASSERT(TEST_LEASES_NUM % reclamation_group_size == 0); + + // Leases will be reclaimed in groups of 10 + for (unsigned int i = 10; i < TEST_LEASES_NUM; i += reclamation_group_size) { + // Reclaim 10 most expired leases. Note that the leases with the + // higher index are more expired. For example, if the + // TEST_LEASES_NUM is equal to 100, the most expired lease will + // be 98, then 96, 94 etc. + ASSERT_NO_THROW(reclaimExpiredLeases(reclamation_group_size, 0, + false)); + + // After the first iteration the lower bound is 80, because there + // will be 10 the most expired leases in this group: 80, 82, 84, + // 86, 88, 90, 92, 94, 96, 98. For subsequent iterations + // accordingly. + int reclaimed_lower_bound = TEST_LEASES_NUM - 2 * i; + // At some point the lower bound will hit the negative value, which + // must be corrected to 0. + if (reclaimed_lower_bound < 0) { + reclaimed_lower_bound = 0; + } + + // Leases between the lower bound calculated above and the upper + // bound of all leases, and having even indexes should have been + // reclaimed. + EXPECT_TRUE(testLeases(&leaseReclaimed, &evenLeaseIndex, + LowerBound(reclaimed_lower_bound), + UpperBound(TEST_LEASES_NUM))) + << "check failed for i = " << i; + + // For the same leases we should have generated DNS updates + // (removal NCRs). + EXPECT_TRUE(testLeases(&dnsUpdateGeneratedForLease, &evenLeaseIndex, + LowerBound(reclaimed_lower_bound), + UpperBound(TEST_LEASES_NUM))) + << "check failed for i = " << i; + + // Leases with odd indexes (falling between the reclaimed ones) + // shouldn't have been reclaimed, because they are not expired. + EXPECT_TRUE(testLeases(&leaseNotReclaimed, &oddLeaseIndex, + LowerBound(reclaimed_lower_bound), + UpperBound(TEST_LEASES_NUM))) + << "check failed for i = " << i; + + EXPECT_TRUE(testLeases(&dnsUpdateNotGeneratedForLease, + &oddLeaseIndex, + LowerBound(reclaimed_lower_bound), + UpperBound(TEST_LEASES_NUM))) + << "check failed for i = " << i; + + + // At early stages of iterations, there should be continuous + // group of leases (expired and not expired) which haven't been + // reclaimed. + if (reclaimed_lower_bound > 0) { + EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes, + LowerBound(0), + UpperBound(reclaimed_lower_bound))) + << "check failed for i = " << i; + + EXPECT_TRUE(testLeases(&dnsUpdateNotGeneratedForLease, + &oddLeaseIndex, + LowerBound(0), + UpperBound(reclaimed_lower_bound))); + } + } + } + + /// @brief This test verifies that reclamation routine continues if the + /// DNS update has failed for some leases. + void testReclaimExpiredLeasesInvalidHostname() { + // DNS must be started for the D2 client to accept NCRs. + ASSERT_NO_THROW(enableDDNS()); + + for (size_t i = 0; i < TEST_LEASES_NUM; ++i) { + // Generate invalid hostname for every other lease. + if (evenLeaseIndex(i)) { + // Hostname with two consecutive dots is invalid and may result + // in exception if the reclamation routine doesn't protect + // against such exceptions. + std::ostringstream hostname_s; + hostname_s << "invalid-host" << i << "..example.com"; + leases_[i]->hostname_ = hostname_s.str(); + ASSERT_NO_THROW(updateLease(i)); + } + // Every lease is expired. + expire(i, 10 + i); + } + + // Although we know that some hostnames are broken we don't want the + // reclamation process to break when it finds a broken record. + // It should rather continue to process other leases. + ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false)); + + // All leases should have been reclaimed. Broken DNS entry doesn't + // warrant that we don't reclaim the lease. + EXPECT_TRUE(testLeases(&leaseReclaimed, &allLeaseIndexes)); + // The routine should not generate DNS updates for the leases with + // broken hostname. + EXPECT_TRUE(testLeases(&dnsUpdateNotGeneratedForLease, + &evenLeaseIndex)); + // But it should generate DNS updates for the leases with the correct + // hostname. + EXPECT_TRUE(testLeases(&dnsUpdateGeneratedForLease, &oddLeaseIndex)); + } + + /// @brief This test verifies that callouts are executed for each expired + /// lease when installed. + void testReclaimExpiredLeasesHooks() { + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + if (evenLeaseIndex(i)) { + expire(i, 1000 - i); + } + } + + HookLibsCollection libraries; // no libraries at this time + HooksManager::loadLibraries(libraries); + + // Install a callout: lease4_expire or lease6_expire. + std::ostringstream callout_name; + callout_name << callout_argument_name << "_expire"; + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + callout_name.str(), leaseExpireCallout)); + + ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false)); + + // Callouts should be executed for leases with even indexes and these + // leases should be reclaimed. + EXPECT_TRUE(testLeases(&leaseCalloutExecuted, &evenLeaseIndex)); + EXPECT_TRUE(testLeases(&leaseReclaimed, &evenLeaseIndex)); + // Callouts should not be executed for leases with odd indexes and these + // leases should not be reclaimed. + EXPECT_TRUE(testLeases(&leaseCalloutNotExecuted, &oddLeaseIndex)); + EXPECT_TRUE(testLeases(&leaseNotReclaimed, &oddLeaseIndex)); + } + + /// @brief This test verifies that callouts are executed for each expired + /// lease and that the lease is not reclaimed when skip flag is set. + void testReclaimExpiredLeasesHooksWithSkip() { + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + if (evenLeaseIndex(i)) { + expire(i, 1000 - i); + } + } + + HookLibsCollection libraries; // no libraries at this time + HooksManager::loadLibraries(libraries); + + // Install a callout: lease4_expire or lease6_expire. + std::ostringstream callout_name; + callout_name << callout_argument_name << "_expire"; + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + callout_name.str(), leaseExpireWithSkipCallout)); + + ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false)); + + // Callouts should have been executed for leases with even indexes. + EXPECT_TRUE(testLeases(&leaseCalloutExecuted, &evenLeaseIndex)); + // Callouts should not be executed for leases with odd indexes. + EXPECT_TRUE(testLeases(&leaseCalloutNotExecuted, &oddLeaseIndex)); + // Leases shouldn't be reclaimed because the callout sets the + // skip flag for each of them. + EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes)); + } + + /// @brief This test verifies that it is possible to set the timeout for + /// the execution of the lease reclamation routine. + void testReclaimExpiredLeasesTimeout(const uint16_t timeout) { + // Leases are segregated from the most expired to the least expired. + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + expire(i, 2000 - i); + } + + HookLibsCollection libraries; + HooksManager::loadLibraries(libraries); + + // Install a callout: lease4_expire or lease6_expire. Each callout + // takes at least 40ms to run (it uses usleep). + std::ostringstream callout_name; + callout_name << callout_argument_name << "_expire"; + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + callout_name.str(), leaseExpireWithDelayCallout)); + + // Reclaim leases with timeout. + ASSERT_NO_THROW(reclaimExpiredLeases(0, timeout, false)); + + // We reclaimed at most (timeout / 40ms) leases. + const uint16_t theoretical_reclaimed = static_cast<uint16_t>(timeout / 40); + + // The actual number of leases reclaimed is likely to be lower than + // the theoretical number. For low theoretical number the adjusted + // number is always 1. For higher number, it will be 10 less than the + // theoretical number. + const uint16_t adjusted_reclaimed = (theoretical_reclaimed > 10 ? + theoretical_reclaimed - 10 : 1); + + EXPECT_TRUE(testLeases(&leaseCalloutExecuted, &allLeaseIndexes, + LowerBound(0), UpperBound(adjusted_reclaimed))); + EXPECT_TRUE(testLeases(&leaseReclaimed, &allLeaseIndexes, + LowerBound(0), UpperBound(adjusted_reclaimed))); + EXPECT_TRUE(testLeases(&leaseCalloutNotExecuted, &allLeaseIndexes, + LowerBound(theoretical_reclaimed + 1), + UpperBound(TEST_LEASES_NUM))); + EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes, + LowerBound(theoretical_reclaimed + 1), + UpperBound(TEST_LEASES_NUM))); + } + + /// @brief This test verifies that expired-reclaimed leases are removed + /// from the lease database. + void testDeleteExpiredReclaimedLeases() { + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Mark leases with even indexes as expired. + if (evenLeaseIndex(i)) { + // The higher the index, the more expired the lease. + reclaim(i, 10 + i); + } + } + + // Run leases reclamation routine on all leases. This should result + // in removal of all leases with even indexes. + ASSERT_NO_THROW(deleteExpiredReclaimedLeases(10)); + + // Leases with odd indexes shouldn't be removed from the database. + EXPECT_TRUE(testLeases(&leaseExists, &oddLeaseIndex)); + // Leases with even indexes should have been removed. + EXPECT_TRUE(testLeases(&leaseDoesntExist, &evenLeaseIndex)); + } + + /// @brief Test that declined expired leases can be removed. + /// + /// This method allows controlling remove_leases parameter when calling + /// @ref AllocEngine::reclaimExpiredLeases4 or + /// @ref AllocEngine::reclaimExpiredLeases6. This should not matter, as + /// the address affinity doesn't make sense for declined leases (they don't + /// have any useful information in them anymore), so AllocEngine should + /// remove them all the time. + /// + /// @param remove see description above + void testReclaimDeclined(bool remove) { + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + + // Mark leases with even indexes as expired. + if (evenLeaseIndex(i)) { + + // Mark lease as declined with 100 seconds of probation-period + // (i.e. lease is supposed to be off limits for 100 seconds) + decline(i, 100); + + // The higher the index, the more expired the lease. + expire(i, 10 + i); + } + } + + // Run leases reclamation routine on all leases. This should result + // in removing all leases with status = declined, i.e. all + // even leases should be gone. + ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, remove)); + + // Leases with even indexes should not exist in the DB + EXPECT_TRUE(testLeases(&leaseDoesntExist, &evenLeaseIndex)); + } + + /// @brief Test that appropriate statistics are updated when + /// declined expired leases are processed by AllocEngine. + /// + /// This method works for both v4 and v6. Just make sure the correct + /// statistic name is passed. This is the name of the assigned addresses, + /// that is expected to be decreased once the reclamation procedure + /// is complete. + /// + /// @param stat_name name of the statistic for assigned addresses statistic + /// ("assigned-addresses" for both v4 and "assigned-nas" for v6) + void testReclaimDeclinedStats(const std::string& stat_name) { + + // Leases by default all belong to subnet_id_ = 1. Let's count the + // number of declined leases. + int subnet1_cnt = 0; + int subnet2_cnt = 0; + + // Let's move all leases to declined,expired state. + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + + // Move the lease to declined state + decline(i, 100); + + // And expire it, so it will be reclaimed + expire(i, 10 + 1); + + // Move every other lease to subnet-id = 2. + if (evenLeaseIndex(i)) { + subnet1_cnt++; + } else { + subnet2_cnt++; + setSubnetId(i, 2); + } + } + + StatsMgr& stats_mgr = StatsMgr::instance(); + + // Let's set the global statistic. Values are arbitrary and can + // be used to easily detect whether a given stat was decreased or + // increased. They are sufficiently high compared to number of leases + // to avoid any chances of going into negative. + stats_mgr.setValue("declined-addresses", static_cast<int64_t>(1000)); + + // Let's set global the counter for reclaimed declined addresses. + stats_mgr.setValue("reclaimed-declined-addresses", + static_cast<int64_t>(2000)); + + // And those subnet specific as well + stats_mgr.setValue(stats_mgr.generateName("subnet", 1, + stat_name), int64_t(1000)); + stats_mgr.setValue(stats_mgr.generateName("subnet", 2, + stat_name), int64_t(2000)); + + stats_mgr.setValue(stats_mgr.generateName("subnet", 1, + "reclaimed-declined-addresses"), int64_t(10000)); + stats_mgr.setValue(stats_mgr.generateName("subnet", 2, + "reclaimed-declined-addresses"), int64_t(20000)); + + stats_mgr.setValue(stats_mgr.generateName("subnet", 1, + "declined-addresses"), int64_t(100)); + stats_mgr.setValue(stats_mgr.generateName("subnet", 2, + "declined-addresses"), int64_t(200)); + + // Run leases reclamation routine on all leases. This should result + // in removal of all leases with even indexes. + ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, true)); + + // Declined-addresses should be decreased from its initial value (1000) + // for both recovered addresses from subnet1 and subnet2. + testStatistics("declined-addresses", 1000 - subnet1_cnt - subnet2_cnt); + + // The code should bump up global counter for reclaimed declined + // addresses. + testStatistics("reclaimed-declined-addresses", 2000 + subnet1_cnt + subnet2_cnt); + + // subnet[X].assigned-addresses should go down. Between the time + // of DHCPDECLINE(v4)/DECLINE(v6) reception and declined expired lease + // reclamation, we count this address as assigned-addresses. We decrease + // assigned-addresses(v4)/assigned-nas(v6) when we reclaim the lease, + // not when the packet is received. For explanation, see Duplicate + // Addresses (DHCPDECLINE support) (v4) or Duplicate Addresses (DECLINE + // support) sections in the User's Guide or a comment in + // Dhcpv4Srv::declineLease or Dhcpv6Srv::declineLease. + testStatistics("subnet[1]." + stat_name, 1000 - subnet1_cnt); + testStatistics("subnet[2]." + stat_name, 2000 - subnet2_cnt); + + testStatistics("subnet[1].declined-addresses", 100 - subnet1_cnt); + testStatistics("subnet[2].declined-addresses", 200 - subnet2_cnt); + + // subnet[X].reclaimed-declined-addresses should go up in each subnet + testStatistics("subnet[1].reclaimed-declined-addresses", 10000 + subnet1_cnt); + testStatistics("subnet[2].reclaimed-declined-addresses", 20000 + subnet1_cnt); + } + + /// @brief Collection of leases created at construction time. + std::vector<LeasePtrType> leases_; + + /// @brief Allocation engine instance used for tests. + AllocEnginePtr engine_; +}; + + + +/// @brief Specialization of the @c ExpirationAllocEngineTest class to test +/// reclamation of the IPv6 leases. +class ExpirationAllocEngine6Test : public ExpirationAllocEngineTest<Lease6Ptr> { +public: + + /// @brief Class constructor. + /// + /// This constructor initializes @c TEST_LEASES_NUM leases and + /// stores them in the lease manager. + ExpirationAllocEngine6Test(); + + /// @brief Virtual destructor. + /// + /// Clears up static fields that may be modified by hooks. + virtual ~ExpirationAllocEngine6Test() { + callout_lease_.reset(); + callout_name_ = string(""); + } + + /// @brief Creates collection of leases for a test. + /// + /// It is called internally at the construction time. + void createLeases(); + + /// @brief Updates lease in the lease database. + /// + /// @param lease_index Index of the lease. + virtual void updateLease(const unsigned int lease_index) { + LeaseMgrFactory::instance().updateLease6(leases_[lease_index]); + } + + /// @brief Changes the owner of a lease. + /// + /// This method changes the owner of the lease by modifying the DUID. + /// + /// @param lease_index Lease index. Must be between 0 and + /// @c TEST_LEASES_NUM. + virtual void transferOwnership(const uint16_t lease_index); + + /// @brief Sets subnet id for a lease. + /// + /// It also updates statistics of assigned leases in the stats manager. + /// + /// @param lease_index Lease index. + /// @param id New subnet id. + virtual void setSubnetId(const uint16_t lease_index, const SubnetID& id); + + /// @brief Sets type of a lease. + /// + /// It also updates statistics of assigned leases in the stats manager. + /// + /// @param lease_index Lease index. + /// @param lease_type Lease type. + void setLeaseType(const uint16_t lease_index, const Lease6::Type& lease_type); + + /// @brief Retrieves lease from the database. + /// + /// @param lease_index Index of the lease. + virtual Lease6Ptr getLease(const unsigned int lease_index) const { + return (LeaseMgrFactory::instance().getLease6(leases_[lease_index]->type_, + leases_[lease_index]->addr_)); + } + + /// @brief Wrapper method running lease reclamation routine. + /// + /// @param max_leases Maximum number of leases to be reclaimed. + /// @param timeout Maximum amount of time that the reclamation routine + /// may be processing expired leases, expressed in seconds. + /// @param remove_lease A boolean value indicating if the lease should + /// be removed when it is reclaimed (if true) or it should be left in the + /// database in the "expired-reclaimed" state (if false). + virtual void reclaimExpiredLeases(const size_t max_leases, + const uint16_t timeout, + const bool remove_lease) { + engine_->reclaimExpiredLeases6(max_leases, timeout, remove_lease); + } + + /// @brief Wrapper method for removing expired-reclaimed leases. + /// + /// @param secs The minimum amount of time, expressed in seconds, + /// for the lease to be left in the "expired-reclaimed" state + /// before it can be removed. + virtual void deleteExpiredReclaimedLeases(const uint32_t secs) { + engine_->deleteExpiredReclaimedLeases6(secs); + } + + /// @brief Test that statistics is updated when leases are reclaimed. + void testReclaimExpiredLeasesStats(); + + /// @brief Test that expired leases are reclaimed before they are allocated. + /// + /// @param msg_type DHCPv6 message type. + /// @param use_reclaimed Boolean parameter indicating if the leases + /// stored in the lease database should be marked as 'expired-reclaimed' + /// or 'expired'. This allows to test whether the allocation engine can + /// determine that the lease has been reclaimed already and not reclaim + /// it the second time. + void testReclaimReusedLeases(const uint16_t msg_type, const bool use_reclaimed); + + /// @brief Callout for lease6_recover + /// + /// This callout stores passed parameter into static fields. + /// + /// @param callout_handle will be provided by hooks framework + /// @return always 0 + static int lease6RecoverCallout(CalloutHandle& callout_handle) { + callout_name_ = "lease6_recover"; + + callout_handle.getArgument("lease6", callout_lease_); + + return (0); + } + + /// @brief Callout for lease6_recover that sets status to SKIP + /// + /// This callout stores passed parameter into static fields. + /// + /// @param callout_handle will be provided by hooks framework + /// @return always 0 + static int lease6RecoverSkipCallout(CalloutHandle& callout_handle) { + // Set the next step status to SKIP + callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP); + + return (lease6RecoverCallout(callout_handle)); + } + + /// @brief Test install a hook callout, recovers declined leases + /// + /// This test: declines, then expires half of the leases, then + /// installs a callout on lease6_recover hook, then reclaims + /// expired leases and checks that: + /// - the callout was indeed called + /// - the parameter (lease6) was indeed passed as expected + /// - checks that the leases are removed (skip=false) or + /// - checks that the leases are still there (skip=true) + /// @param skip should the callout set the next step status to skip? + void + testReclaimDeclinedHook(bool skip); + + /// The following parameters will be written by a callout + static std::string callout_name_; ///< Stores callout name + static Lease6Ptr callout_lease_; ///< Stores callout parameter +}; + +std::string ExpirationAllocEngine6Test::callout_name_; +Lease6Ptr ExpirationAllocEngine6Test::callout_lease_; + +ExpirationAllocEngine6Test::ExpirationAllocEngine6Test() + : ExpirationAllocEngineTest<Lease6Ptr>("type=memfile universe=6 persist=false") { + createLeases(); + callout_argument_name = "lease6"; + + // Let's clear any garbage previous test may have left in static fields. + callout_name_ = string(""); + callout_lease_.reset(); +} + +void +ExpirationAllocEngine6Test::createLeases() { + // Create TEST_LEASES_NUM leases. + for (uint16_t i = 0; i < TEST_LEASES_NUM; ++i) { + // DUID + std::ostringstream duid_s; + duid_s << "01020304050607" << std::setw(4) << std::setfill('0') << i; + DuidPtr duid(new DUID(DUID::fromText(duid_s.str()).getDuid())); + + // Address. + std::ostringstream address_s; + address_s << "2001:db8:1::" << std::setw(4) << std::setfill('0') << i; + IOAddress address(address_s.str()); + + // Create lease. + Lease6Ptr lease(new Lease6(Lease::TYPE_NA, address, duid, 1, 50, 60, + SubnetID(1), true, true, + generateHostnameForLeaseIndex(i))); + leases_.push_back(lease); + // Copy the lease before adding it to the lease manager. We want to + // make sure that modifications to the leases held in the leases_ + // container doesn't affect the leases in the lease manager. + Lease6Ptr tmp(new Lease6(*lease)); + LeaseMgrFactory::instance().addLease(tmp); + + // Note in the statistics that this lease has been added. + StatsMgr& stats_mgr = StatsMgr::instance(); + std::string stat_name = + lease->type_ == Lease::TYPE_NA ? "assigned-nas" : "assigned-pds"; + stats_mgr.addValue(stats_mgr.generateName("subnet", lease->subnet_id_, stat_name), + int64_t(1)); + } +} + +void +ExpirationAllocEngine6Test::transferOwnership(const uint16_t lease_index) { + ASSERT_GT(leases_.size(), lease_index); + std::vector<uint8_t> bytes = leases_[lease_index]->duid_->getDuid(); + if (bytes.size() > 1) { + if (++bytes[0] == 0) { + ++bytes[1]; + } + } + + leases_[lease_index]->duid_.reset(new DUID(bytes)); +} + +void +ExpirationAllocEngine6Test::setSubnetId(const uint16_t lease_index, const SubnetID& id) { + ASSERT_GT(leases_.size(), lease_index); + if (leases_[lease_index]->subnet_id_ != id) { + StatsMgr& stats_mgr = StatsMgr::instance(); + std::string stats_name = (leases_[lease_index]->type_ == Lease::TYPE_NA ? + "assigned-nas" : "assigned-pds"); + stats_mgr.addValue(stats_mgr.generateName("subnet", id, stats_name), + int64_t(1)); + stats_mgr.addValue(stats_mgr.generateName("subnet", + leases_[lease_index]->subnet_id_, + stats_name), + int64_t(-1)); + leases_[lease_index]->subnet_id_ = id; + ASSERT_NO_THROW(updateLease(lease_index)); + } +} + +void +ExpirationAllocEngine6Test::setLeaseType(const uint16_t lease_index, + const Lease6::Type& lease_type) { + ASSERT_GT(leases_.size(), lease_index); + if (leases_[lease_index]->type_ != lease_type) { + StatsMgr& stats_mgr = StatsMgr::instance(); + std::string stats_name = (lease_type == Lease::TYPE_NA ? + "assigned-nas" : "assigned-pds"); + stats_mgr.addValue(stats_mgr.generateName("subnet", + leases_[lease_index]->subnet_id_, + stats_name), + int64_t(1)); + stats_name = (leases_[lease_index]->type_ == Lease::TYPE_NA ? + "assigned-nas" : "assigned-pds"); + stats_mgr.addValue(stats_mgr.generateName("subnet", + leases_[lease_index]->subnet_id_, + stats_name), + int64_t(-1)); + leases_[lease_index]->type_ = lease_type; + ASSERT_NO_THROW(updateLease(lease_index)); + } +} + + +void +ExpirationAllocEngine6Test::testReclaimExpiredLeasesStats() { + // This test requires that the number of leases is an even number. + BOOST_STATIC_ASSERT(TEST_LEASES_NUM % 2 == 0); + + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Mark all leases as expired. The higher the index the less + // expired the lease. + expire(i, 1000 - i); + // Modify subnet ids and lease types for some leases. + if (evenLeaseIndex(i)) { + setSubnetId(i, SubnetID(2)); + setLeaseType(i, Lease::TYPE_PD); + } + } + + // Leases will be reclaimed in groups of 8. + const size_t reclamation_group_size = 8; + for (unsigned int i = reclamation_group_size; i < TEST_LEASES_NUM; + i += reclamation_group_size) { + + // Reclaim 8 most expired leases out of TEST_LEASES_NUM. + ASSERT_NO_THROW(reclaimExpiredLeases(reclamation_group_size, + 0, false)); + + // Number of reclaimed leases should increase as we loop. + EXPECT_TRUE(testStatistics("reclaimed-leases", i)); + // Make sure that the number of reclaimed leases is also distributed + // across two subnets. + EXPECT_TRUE(testStatistics("subnet[1].reclaimed-leases", i / 2)); + EXPECT_TRUE(testStatistics("subnet[2].reclaimed-leases", i / 2)); + // Number of assigned leases should decrease as we reclaim them. + EXPECT_TRUE(testStatistics("subnet[1].assigned-nas", + (TEST_LEASES_NUM - i) / 2)); + EXPECT_TRUE(testStatistics("subnet[2].assigned-pds", + (TEST_LEASES_NUM - i) / 2)); + } +} + +void +ExpirationAllocEngine6Test::testReclaimReusedLeases(const uint16_t msg_type, + const bool use_reclaimed) { + BOOST_STATIC_ASSERT(TEST_LEASES_NUM < 1000); + + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Depending on the parameter, mark leases 'expired-reclaimed' or + // simply 'expired'. + if (use_reclaimed) { + reclaim(i, 1000 - i); + + } else { + // Mark all leases as expired. + expire(i, 1000 - i); + } + + // For the Renew case, we don't change the ownership of leases. We + // will let the lease owners renew them. For other cases, we modify + // the DUIDs to simulate reuse of expired leases. + if (msg_type != DHCPV6_RENEW) { + transferOwnership(i); + } + } + + // Create subnet and the pool. This is required by the allocation process. + Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 64, 10, 20, 50, 60, + SubnetID(1))); + ASSERT_NO_THROW(subnet->addPool(Pool6Ptr(new Pool6(Lease::TYPE_NA, + IOAddress("2001:db8:1::"), + IOAddress("2001:db8:1::FFFF"))))); + + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Build the context. + AllocEngine::ClientContext6 ctx(subnet, leases_[i]->duid_, + false, false, + leases_[i]->hostname_, + msg_type == DHCPV6_SOLICIT, + Pkt6Ptr(new Pkt6(msg_type, 0x1234))); + ctx.currentIA().iaid_ = 1; + ctx.currentIA().addHint(leases_[i]->addr_); + + // Depending on the message type, we will call a different function. + if (msg_type == DHCPV6_RENEW) { + ASSERT_NO_THROW(engine_->renewLeases6(ctx)); + + } else { + ASSERT_NO_THROW(engine_->allocateLeases6(ctx)); + } + } + + // The Solicit should not trigger leases reclamation. The Renew and + // Request must trigger leases reclamation unless the lease is + // initially reclaimed. + if (use_reclaimed || (msg_type == DHCPV6_SOLICIT)) { + EXPECT_TRUE(testStatistics("reclaimed-leases", 0)); + } else { + EXPECT_TRUE(testStatistics("reclaimed-leases", TEST_LEASES_NUM)); + EXPECT_TRUE(testStatistics("assigned-nas", TEST_LEASES_NUM, subnet->getID())); + // Leases should have been updated in the lease database and their + // state should not be 'expired-reclaimed' anymore. + EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes)); + } + +} + +void +ExpirationAllocEngine6Test::testReclaimDeclinedHook(bool skip) { + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + + // Mark leases with even indexes as expired. + if (evenLeaseIndex(i)) { + + // Mark lease as declined with 100 seconds of probation-period + // (i.e. lease is supposed to be off limits for 100 seconds) + decline(i, 100); + + // The higher the index, the more expired the lease. + expire(i, 10 + i); + } + } + + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "lease6_recover", + skip ? lease6RecoverSkipCallout : lease6RecoverCallout)); + + // Run leases reclamation routine on all leases. + ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, true)); + + // Make sure that the callout really was called. It was supposed to modify + // the callout_name_ and store the lease in callout_lease_ + EXPECT_EQ("lease6_recover", callout_name_); + EXPECT_TRUE(callout_lease_); + + // Leases with even indexes should not exist in the DB + if (skip) { + // Skip status should have prevented removing the lease. + EXPECT_TRUE(testLeases(&leaseExists, &evenLeaseIndex)); + } else { + // The hook hasn't modified next step status. The lease should be gone. + EXPECT_TRUE(testLeases(&leaseDoesntExist, &evenLeaseIndex)); + } +}; + +// This test verifies that the leases can be reclaimed without being removed +// from the database. In such case, the leases' state is set to +// "expired-reclaimed". +TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeases6UpdateState) { + testReclaimExpiredLeasesUpdateState(); +} + +// This test verifies that the reclaimed leases are deleted when requested. +TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesDelete) { + testReclaimExpiredLeasesDelete(); +} + +// This test verifies that it is possible to specify the limit for the +// number of reclaimed leases. +TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesLimit) { + testReclaimExpiredLeasesLimit(); +} + +// This test verifies that DNS updates are generated for the leases +// for which the DNS records exist. +TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesWithDDNS) { + testReclaimExpiredLeasesWithDDNS(); +} + +// This test verifies that it is DNS updates are generated only for the +// reclaimed expired leases. In this case we limit the number of leases +// reclaimed during a single call to reclamation routine. +TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesWithDDNSAndLimit) { + testReclaimExpiredLeasesWithDDNSAndLimit(); +} + +// This test verifies that if some leases have invalid hostnames, the +// lease reclamation routine continues with reclamation of leases anyway. +TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesInvalidHostname) { + testReclaimExpiredLeasesInvalidHostname(); +} + +// This test verifies that statistics is correctly updated when the leases +// are reclaimed. +TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesStats) { + testReclaimExpiredLeasesStats(); +} + +// This test verifies that callouts are executed for each expired lease. +TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesHooks) { + testReclaimExpiredLeasesHooks(); +} + +// This test verifies that callouts are executed for each expired lease +// and that the lease is not reclaimed when the skip flag is set. +TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesHooksWithSkip) { + testReclaimExpiredLeasesHooksWithSkip(); +} + +// This test verifies that it is possible to set the timeout for the +// execution of the lease reclamation routine. +TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesTimeout) { + // This test needs at least 40 leases to make sense. + BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 40); + // Run with timeout of 1.2s. + testReclaimExpiredLeasesTimeout(1200); +} + +// This test verifies that at least one lease is reclaimed if the timeout +// for the lease reclamation routine is shorter than the time needed for +// the reclamation of a single lease. This prevents the situation when +// very short timeout (perhaps misconfigured) effectively precludes leases +// reclamation. +TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesShortTimeout) { + // We will most likely reclaim just one lease, so 5 is more than enough. + BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 5); + // Reclaim leases with the 1ms timeout. + testReclaimExpiredLeasesTimeout(1); +} + +// This test verifies that expired-reclaimed leases are removed from the +// lease database. +TEST_F(ExpirationAllocEngine6Test, deleteExpiredReclaimedLeases) { + BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 10); + testDeleteExpiredReclaimedLeases(); +} + +/// This test verifies that @ref AllocEngine::reclaimExpiredLeases6 properly +/// handles declined leases that have expired in case when it is told to +/// remove leases.} +TEST_F(ExpirationAllocEngine6Test, reclaimDeclined1) { + testReclaimDeclined(true); +} + +/// This test verifies that @ref AllocEngine::reclaimExpiredLeases6 properly +/// handles declined leases that have expired in case when it is told to +/// not remove leases. This flag should not matter and declined expired +/// leases should always be removed. +TEST_F(ExpirationAllocEngine6Test, reclaimDeclined2) { + testReclaimDeclined(false); +} + +/// This test verifies that statistics are modified correctly after +/// reclaim expired leases is called. +TEST_F(ExpirationAllocEngine6Test, reclaimDeclinedStats) { + testReclaimDeclinedStats("assigned-nas"); +} + +// This test verifies that expired leases are reclaimed before they are +// allocated to another client sending a Request message. +TEST_F(ExpirationAllocEngine6Test, reclaimReusedLeases) { + testReclaimReusedLeases(DHCPV6_REQUEST, false); +} + +// This test verifies that allocation engine detects that the expired +// lease has been reclaimed already when it reuses this lease. +TEST_F(ExpirationAllocEngine6Test, reclaimReusedLeasesAlreadyReclaimed) { + testReclaimReusedLeases(DHCPV6_REQUEST, true); +} + +// This test verifies that expired leases are reclaimed before they +// are renewed. +TEST_F(ExpirationAllocEngine6Test, reclaimRenewedLeases) { + testReclaimReusedLeases(DHCPV6_RENEW, false); +} + +// This test verifies that allocation engine detects that the expired +// lease has been reclaimed already when it renews the lease. +TEST_F(ExpirationAllocEngine6Test, reclaimRenewedLeasesAlreadyReclaimed) { + testReclaimReusedLeases(DHCPV6_RENEW, true); +} + +// This test verifies that the expired leases are not reclaimed when the +// Solicit message is being processed. +TEST_F(ExpirationAllocEngine6Test, reclaimReusedLeasesSolicit) { + testReclaimReusedLeases(DHCPV6_SOLICIT, false); +} + +// This test verifies that the 'expired-reclaimed' leases are not reclaimed +// again when the Solicit message is being processed. +TEST_F(ExpirationAllocEngine6Test, reclaimReusedLeasesSolicitAlreadyReclaimed) { + testReclaimReusedLeases(DHCPV6_SOLICIT, true); +} + +// This test verifies if the hooks installed on lease6_recover are called +// when the lease expires. +TEST_F(ExpirationAllocEngine6Test, reclaimDeclinedHook1) { + testReclaimDeclinedHook(false); // false = don't use skip callout +} + +// This test verifies if the hooks installed on lease6_recover are called +// when the lease expires and that the next step status set to SKIP +// causes the recovery to not be conducted. +TEST_F(ExpirationAllocEngine6Test, reclaimDeclinedHook2) { + testReclaimDeclinedHook(true); // true = use skip callout +} + +// ******************************************************* +// +// DHCPv4 lease reclamation routine tests start here! +// +// ******************************************************* + +/// @brief Specialization of the @c ExpirationAllocEngineTest class to test +/// reclamation of the IPv4 leases. +class ExpirationAllocEngine4Test : public ExpirationAllocEngineTest<Lease4Ptr> { +public: + + /// @brief Class constructor. + /// + /// This constructor initializes @c TEST_LEASES_NUM leases and + /// stores them in the lease manager. + ExpirationAllocEngine4Test(); + + /// @brief Virtual destructor. + /// + /// Clears up static fields that may be modified by hooks. + virtual ~ExpirationAllocEngine4Test() { + callout_lease_.reset(); + callout_name_ = string(""); + } + + /// @brief Creates collection of leases for a test. + /// + /// It is called internally at the construction time. + void createLeases(); + + /// @brief Generates unique client identifier from lease index. + /// + /// @param index lease index. + void setUniqueClientId(const uint16_t index); + + /// @brief Updates lease in the lease database. + /// + /// @param lease_index Index of the lease. + virtual void updateLease(const unsigned int lease_index) { + LeaseMgrFactory::instance().updateLease4(leases_[lease_index]); + } + + /// @brief Changes the owner of a lease. + /// + /// This method changes the owner of the lease by updating the client + /// identifier (if present) or HW address. + /// + /// @param lease_index Lease index. Must be between 0 and + /// @c TEST_LEASES_NUM. + virtual void transferOwnership(const uint16_t lease_index); + + /// @brief Retrieves lease from the database. + /// + /// @param lease_index Index of the lease. + virtual Lease4Ptr getLease(const unsigned int lease_index) const { + return (LeaseMgrFactory::instance().getLease4(leases_[lease_index]->addr_)); + } + + /// @brief Sets subnet id for a lease. + /// + /// It also updates statistics of assigned leases in the stats manager. + /// + /// @param lease_index Lease index. + /// @param id New subnet id. + virtual void setSubnetId(const uint16_t lease_index, const SubnetID& id); + + /// @brief Wrapper method running lease reclamation routine. + /// + /// @param max_leases Maximum number of leases to be reclaimed. + /// @param timeout Maximum amount of time that the reclamation routine + /// may be processing expired leases, expressed in seconds. + /// @param remove_lease A boolean value indicating if the lease should + /// be removed when it is reclaimed (if true) or it should be left in the + /// database in the "expired-reclaimed" state (if false). + virtual void reclaimExpiredLeases(const size_t max_leases, + const uint16_t timeout, + const bool remove_lease) { + engine_->reclaimExpiredLeases4(max_leases, timeout, remove_lease); + } + + /// @brief Wrapper method for removing expired-reclaimed leases. + /// + /// @param secs The minimum amount of time, expressed in seconds, + /// for the lease to be left in the "expired-reclaimed" state + /// before it can be removed. + virtual void deleteExpiredReclaimedLeases(const uint32_t secs) { + engine_->deleteExpiredReclaimedLeases4(secs); + } + + /// @brief Lease algorithm checking if NCR has been generated from client + /// identifier. + /// + /// @param lease Pointer to the lease for which the NCR needs to be checked. + static bool dnsUpdateGeneratedFromClientId(const Lease4Ptr& lease); + + /// @brief Lease algorithm checking if NCR has been generated from + /// HW address. + static bool dnsUpdateGeneratedFromHWAddress(const Lease4Ptr& lease); + + /// @brief Test that DNS updates are properly generated when the + /// reclaimed leases contain client identifier. + void testReclaimExpiredLeasesWithDDNSAndClientId(); + + /// @brief Test that statistics is updated when leases are reclaimed.. + void testReclaimExpiredLeasesStats(); + + /// @brief Test that the lease is reclaimed before it is renewed or + /// reused. + /// + /// @param msg_type DHCPv4 message type, i.e. DHCPDISCOVER or DHCPREQUEST. + /// @param client_renews A boolean value which indicates if the test should + /// simulate renewals of leases (if true) or reusing expired leases which + /// belong to different clients (if false). + /// @param use_reclaimed Boolean parameter indicating if the leases being + /// reused should initially be reclaimed. + void testReclaimReusedLeases(const uint8_t msg_type, const bool client_renews, + const bool use_reclaimed); + + /// @brief Callout for lease4_recover + /// + /// This callout stores passed parameter into static fields. + /// + /// @param callout_handle will be provided by hooks framework + /// @return always 0 + static int lease4RecoverCallout(CalloutHandle& callout_handle) { + callout_name_ = "lease4_recover"; + + callout_handle.getArgument("lease4", callout_lease_); + + return (0); + } + + /// @brief Callout for lease4_recover that sets status to SKIP + /// + /// This callout stores passed parameter into static fields. + /// + /// @param callout_handle will be provided by hooks framework + /// @return always 0 + static int lease4RecoverSkipCallout(CalloutHandle& callout_handle) { + // Set the next step status to SKIP + callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP); + + return (lease4RecoverCallout(callout_handle)); + } + + /// @brief Test install a hook callout, recovers declined leases + /// + /// This test: declines, then expires half of the leases, then + /// installs a callout on lease4_recover hook, then reclaims + /// expired leases and checks that: + /// - the callout was indeed called + /// - the parameter (lease4) was indeed passed as expected + /// - checks that the leases are removed (skip=false) or + /// - checks that the leases are still there (skip=true) + /// @param skip should the callout set the next step status to skip? + void + testReclaimDeclinedHook(bool skip); + + /// The following parameters will be written by a callout + static std::string callout_name_; ///< Stores callout name + static Lease4Ptr callout_lease_; ///< Stores callout parameter +}; + +std::string ExpirationAllocEngine4Test::callout_name_; +Lease4Ptr ExpirationAllocEngine4Test::callout_lease_; + +ExpirationAllocEngine4Test::ExpirationAllocEngine4Test() + : ExpirationAllocEngineTest<Lease4Ptr>("type=memfile universe=4 persist=false") { + createLeases(); + callout_argument_name = "lease4"; + + // Let's clear any garbage previous test may have left in static fields. + callout_name_ = string(""); + callout_lease_.reset(); +} + +void +ExpirationAllocEngine4Test::createLeases() { + // Create TEST_LEASES_NUM leases. + for (uint16_t i = 0; i < TEST_LEASES_NUM; ++i) { + // HW address + std::ostringstream hwaddr_s; + hwaddr_s << "01:02:03:04:" << std::setw(2) << std::setfill('0') + << (i >> 8) << ":" << std::setw(2) << std::setfill('0') + << (i & 0x00FF); + HWAddrPtr hwaddr(new HWAddr(HWAddr::fromText(hwaddr_s.str(), + HTYPE_ETHER))); + + // Address. + std::ostringstream address_s; + address_s << "10.0." << (i >> 8) << "." << (i & 0x00FF); + IOAddress address(address_s.str()); + + // Create lease. + Lease4Ptr lease(new Lease4(address, hwaddr, ClientIdPtr(), 60, + time(NULL), SubnetID(1), true, true, + generateHostnameForLeaseIndex(i))); + leases_.push_back(lease); + // Copy the lease before adding it to the lease manager. We want to + // make sure that modifications to the leases held in the leases_ + // container doesn't affect the leases in the lease manager. + Lease4Ptr tmp(new Lease4(*lease)); + LeaseMgrFactory::instance().addLease(tmp); + + // Note in the statistics that this lease has been added. + StatsMgr& stats_mgr = StatsMgr::instance(); + std::string stat_name = "assigned-addresses"; + stats_mgr.addValue(stats_mgr.generateName("subnet", lease->subnet_id_, stat_name), + int64_t(1)); + } +} + +void +ExpirationAllocEngine4Test::setUniqueClientId(const uint16_t index) { + std::ostringstream clientid_s; + clientid_s << "AA:BB:" << std::setw(2) << std::setfill('0') + << (index >> 8) << ":" << std::setw(2) << std::setfill('0') + << (index & 0x00FF); + ClientIdPtr client_id(ClientId::fromText(clientid_s.str())); + leases_[index]->client_id_ = client_id; + LeaseMgrFactory::instance().updateLease4(leases_[index]); +} + +void +ExpirationAllocEngine4Test::setSubnetId(const uint16_t lease_index, const SubnetID& id) { + ASSERT_GT(leases_.size(), lease_index); + if (leases_[lease_index]->subnet_id_ != id) { + StatsMgr& stats_mgr = StatsMgr::instance(); + stats_mgr.addValue(stats_mgr.generateName("subnet", id, "assigned-addresses"), + int64_t(1)); + stats_mgr.addValue(stats_mgr.generateName("subnet", + leases_[lease_index]->subnet_id_, + "assigned-addresses"), + int64_t(-1)); + leases_[lease_index]->subnet_id_ = id; + ASSERT_NO_THROW(updateLease(lease_index)); + } +} + +void +ExpirationAllocEngine4Test::transferOwnership(const uint16_t lease_index) { + ASSERT_GT(leases_.size(), lease_index); + std::vector<uint8_t> bytes; + if (leases_[lease_index]->client_id_) { + bytes = leases_[lease_index]->client_id_->getClientId(); + } else { + bytes = leases_[lease_index]->hwaddr_->hwaddr_; + } + + if (!bytes.empty()) { + if (++bytes[0] == 0) { + ++bytes[1]; + } + } + + if (leases_[lease_index]->client_id_) { + leases_[lease_index]->client_id_.reset(new ClientId(bytes)); + } else { + leases_[lease_index]->hwaddr_.reset(new HWAddr(bytes, HTYPE_ETHER)); + } +} + + +bool +ExpirationAllocEngine4Test::dnsUpdateGeneratedFromClientId(const Lease4Ptr& lease) { + try { + NameChangeRequestPtr ncr = getNCRForLease(lease); + if (ncr) { + if (lease->client_id_) { + // Generate hostname for this lease. Note that the lease + // in the database doesn't have the hostname because it + // has been removed by the lease reclamation routine. + std::string hostname = generateHostnameForLeaseIndex( + getLeaseIndexFromAddress(lease->addr_)); + + // Get DHCID from NCR. + const D2Dhcid& dhcid = ncr->getDhcid(); + // Generate reference DHCID to compare with the one from + // the NCR. + std::vector<uint8_t> fqdn_wire; + OptionDataTypeUtil::writeFqdn(hostname, fqdn_wire, true); + D2Dhcid clientid_dhcid(lease->client_id_->getClientId(), + fqdn_wire); + // Return true if they match. + return (dhcid == clientid_dhcid); + } + } + + } catch (...) { + // If error occurred, treat it as no match. + return (false); + } + + // All leases checked - no match. + return (false); +} + +bool +ExpirationAllocEngine4Test::dnsUpdateGeneratedFromHWAddress(const Lease4Ptr& lease) { + try { + NameChangeRequestPtr ncr = getNCRForLease(lease); + if (ncr) { + if (lease->hwaddr_) { + // Generate hostname for this lease. Note that the lease + // in the database doesn't have the hostname because it + // has been removed by the lease reclamation routine. + std::string hostname = generateHostnameForLeaseIndex( + getLeaseIndexFromAddress(lease->addr_)); + + // Get DHCID from NCR. + const D2Dhcid& dhcid = ncr->getDhcid(); + // Generate reference DHCID to compare with the one from + // the NCR. + std::vector<uint8_t> fqdn_wire; + OptionDataTypeUtil::writeFqdn(hostname, fqdn_wire, true); + D2Dhcid hwaddr_dhcid(lease->hwaddr_, fqdn_wire); + // Return true if they match. + return (dhcid == hwaddr_dhcid); + } + } + + } catch (...) { + // If error occurred, treat it as no match. + return (false); + } + + // All leases checked - no match. + return (false); +} + +void +ExpirationAllocEngine4Test::testReclaimExpiredLeasesWithDDNSAndClientId() { + // DNS must be started for the D2 client to accept NCRs. + ASSERT_NO_THROW(enableDDNS()); + + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Set client identifiers for leases with even indexes only. + if (evenLeaseIndex(i)) { + setUniqueClientId(i); + } + // Expire all leases. The higher the index, the more expired the lease. + expire(i, 10 + i); + } + + // Reclaim all expired leases. + ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false)); + + // Leases with even indexes should be reclaimed. + EXPECT_TRUE(testLeases(&leaseReclaimed, &evenLeaseIndex)); + // DNS updates (removal NCRs) should be generated for all leases. + EXPECT_TRUE(testLeases(&dnsUpdateGeneratedForLease, &allLeaseIndexes)); + // Leases with even indexes include client identifiers so the DHCID should + // be generated from the client identifiers. + EXPECT_TRUE(testLeases(&dnsUpdateGeneratedFromClientId, &evenLeaseIndex)); + // Leases with odd indexes do not include client identifiers so their + // DHCID should be generated from the HW address. + EXPECT_TRUE(testLeases(&dnsUpdateGeneratedFromHWAddress, &oddLeaseIndex)); +} + +void +ExpirationAllocEngine4Test::testReclaimExpiredLeasesStats() { + // This test requires that the number of leases is an even number. + BOOST_STATIC_ASSERT(TEST_LEASES_NUM % 2 == 0); + + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Mark all leases as expired. The higher the index the less + // expired the lease. + expire(i, 1000 - i); + // Modify subnet ids of some leases. + if (evenLeaseIndex(i)) { + setSubnetId(i, 2); + } + } + + // Leases will be reclaimed in groups of 8. + const size_t reclamation_group_size = 8; + for (unsigned int i = reclamation_group_size; i < TEST_LEASES_NUM; + i += reclamation_group_size) { + + // Reclaim 8 most expired leases out of TEST_LEASES_NUM. + ASSERT_NO_THROW(reclaimExpiredLeases(reclamation_group_size, + 0, false)); + + // Number of reclaimed leases should increase as we loop. + EXPECT_TRUE(testStatistics("reclaimed-leases", i)); + // Make sure that the number of reclaimed leases is also distributed + // across two subnets. + EXPECT_TRUE(testStatistics("subnet[1].reclaimed-leases", i / 2)); + EXPECT_TRUE(testStatistics("subnet[2].reclaimed-leases", i / 2)); + // Number of assigned leases should decrease as we reclaim them. + EXPECT_TRUE(testStatistics("subnet[1].assigned-addresses", + (TEST_LEASES_NUM - i) / 2)); + EXPECT_TRUE(testStatistics("subnet[2].assigned-addresses", + (TEST_LEASES_NUM - i) / 2)); + } +} + +void +ExpirationAllocEngine4Test::testReclaimReusedLeases(const uint8_t msg_type, + const bool client_renews, + const bool use_reclaimed) { + // Let's restrict the number of leases. + BOOST_STATIC_ASSERT(TEST_LEASES_NUM < 1000); + + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Depending on the parameter, mark leases 'expired-reclaimed' or + // simply 'expired'. + if (use_reclaimed) { + reclaim(i, 1000 - i); + + } else { + // Mark all leases as expired. + expire(i, 1000 - i); + } + + // Check if we're simulating renewals or reusing leases. If this is + // about reusing leases, we should be using different MAC addresses + // or client identifiers for the leases than those stored presently + // in the database. + if (!client_renews) { + // This function modifies the MAC address or the client identifier + // of the test lease to make sure it doesn't match the one we + // have in the database. + transferOwnership(i); + } + } + + // The call to AllocEngine::allocateLease4 requires the subnet selection. + // The pool must be present within a subnet for the allocation engine to + // hand out address from. + Subnet4Ptr subnet(new Subnet4(IOAddress("10.0.0.0"), 16, 10, 20, 60, SubnetID(1))); + ASSERT_NO_THROW(subnet->addPool(Pool4Ptr(new Pool4(IOAddress("10.0.0.0"), + IOAddress("10.0.255.255"))))); + + // Re-allocate leases (reuse or renew). + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + // Build the context. + AllocEngine::ClientContext4 ctx(subnet, leases_[i]->client_id_, + leases_[i]->hwaddr_, + leases_[i]->addr_, false, false, + leases_[i]->hostname_, + msg_type == DHCPDISCOVER); + // Query is needed for logging purposes. + ctx.query_.reset(new Pkt4(msg_type, 0x1234)); + + // Re-allocate a lease. Note that the iterative will pick addresses + // starting from the beginning of the pool. This matches exactly + // the set of addresses we have allocated and stored in the database. + // Since all leases are marked expired the allocation engine will + // reuse them or renew them as appropriate. + ASSERT_NO_THROW(engine_->allocateLease4(ctx)); + } + + // If DHCPDISCOVER is being processed, the leases should not be reclaimed. + // Also, the leases should not be reclaimed if they are already in the + // 'expired-reclaimed' state. + if (use_reclaimed || (msg_type == DHCPDISCOVER)) { + EXPECT_TRUE(testStatistics("reclaimed-leases", 0)); + + } else if (msg_type == DHCPREQUEST) { + // Re-allocation of expired leases should result in reclamations. + EXPECT_TRUE(testStatistics("reclaimed-leases", TEST_LEASES_NUM)); + EXPECT_TRUE(testStatistics("assigned-addresses", TEST_LEASES_NUM, subnet->getID())); + // Leases should have been updated in the lease database and their + // state should not be 'expired-reclaimed' anymore. + EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes)); + } +} + +void +ExpirationAllocEngine4Test::testReclaimDeclinedHook(bool skip) { + for (unsigned int i = 0; i < TEST_LEASES_NUM; ++i) { + + // Mark leases with even indexes as expired. + if (evenLeaseIndex(i)) { + + // Mark lease as declined with 100 seconds of probation-period + // (i.e. lease is supposed to be off limits for 100 seconds) + decline(i, 100); + + // The higher the index, the more expired the lease. + expire(i, 10 + i); + } + } + + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "lease4_recover", + skip ? lease4RecoverSkipCallout : lease4RecoverCallout)); + + // Run leases reclamation routine on all leases. + ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, true)); + + // Make sure that the callout really was called. It was supposed to modify + // the callout_name_ and store the lease in callout_lease_ + EXPECT_EQ("lease4_recover", callout_name_); + EXPECT_TRUE(callout_lease_); + + // Leases with even indexes should not exist in the DB + if (skip) { + // Skip status should have prevented removing the lease. + EXPECT_TRUE(testLeases(&leaseExists, &evenLeaseIndex)); + } else { + // The hook hasn't modified next step status. The lease should be gone. + EXPECT_TRUE(testLeases(&leaseDoesntExist, &evenLeaseIndex)); + } +}; + +// This test verifies that the leases can be reclaimed without being removed +// from the database. In such case, the leases' state is set to +// "expired-reclaimed". +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesUpdateState) { + testReclaimExpiredLeasesUpdateState(); +} + +// This test verifies that the reclaimed leases are deleted when requested. +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesDelete) { + testReclaimExpiredLeasesDelete(); +} + +// This test verifies that it is possible to specify the limit for the +// number of reclaimed leases. +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesLimit) { + testReclaimExpiredLeasesLimit(); +} + +// This test verifies that DNS updates are generated for the leases +// for which the DNS records exist. +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesWithDDNS) { + testReclaimExpiredLeasesWithDDNS(); +} + +// This test verifies that it is DNS updates are generated only for the +// reclaimed expired leases. In this case we limit the number of leases +// reclaimed during a single call to reclamation routine. +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesWithDDNSAndLimit) { + testReclaimExpiredLeasesWithDDNSAndLimit(); +} + +// This test verifies that if some leases have invalid hostnames, the +// lease reclamation routine continues with reclamation of leases anyway. +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesInvalidHostname) { + testReclaimExpiredLeasesInvalidHostname(); +} + +// This test verifies that DNS updates are properly generated when the +// client id is used as a primary identifier in the lease. +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesWithDDNSAndClientId) { + testReclaimExpiredLeasesWithDDNSAndClientId(); +} + +// This test verifies that statistics is correctly updated when the leases +// are reclaimed. +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesStats) { + testReclaimExpiredLeasesStats(); +} + +// This test verifies that callouts are executed for each expired lease. +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesHooks) { + testReclaimExpiredLeasesHooks(); +} + +// This test verifies that callouts are executed for each expired lease +// and that the lease is not reclaimed when the skip flag is set. +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesHooksWithSkip) { + testReclaimExpiredLeasesHooksWithSkip(); +} + +// This test verifies that it is possible to set the timeout for the +// execution of the lease reclamation routine. +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesTimeout) { + // This test needs at least 40 leases to make sense. + BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 40); + // Run with timeout of 1.2s. + testReclaimExpiredLeasesTimeout(1200); +} + +// This test verifies that at least one lease is reclaimed if the timeout +// for the lease reclamation routine is shorter than the time needed for +// the reclamation of a single lease. This prevents the situation when +// very short timeout (perhaps misconfigured) effectively precludes leases +// reclamation. +TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesShortTimeout) { + // We will most likely reclaim just one lease, so 5 is more than enough. + BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 5); + // Reclaim leases with the 1ms timeout. + testReclaimExpiredLeasesTimeout(1); +} + +// This test verifies that expired-reclaimed leases are removed from the +// lease database. +TEST_F(ExpirationAllocEngine4Test, deleteExpiredReclaimedLeases) { + BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 10); + testDeleteExpiredReclaimedLeases(); +} + +/// This test verifies that @ref AllocEngine::reclaimExpiredLeases4 properly +/// handles declined leases that have expired in case when it is told to +/// remove leases. +TEST_F(ExpirationAllocEngine4Test, reclaimDeclined1) { + testReclaimDeclined(true); +} + +/// This test verifies that @ref AllocEngine::reclaimExpiredLeases4 properly +/// handles declined leases that have expired in case when it is told to +/// not remove leases. This flag should not matter and declined expired +/// leases should always be removed. +TEST_F(ExpirationAllocEngine4Test, reclaimDeclined2) { + testReclaimDeclined(false); +} + +/// This test verifies that statistics are modified correctly after +/// reclaim expired leases is called. +TEST_F(ExpirationAllocEngine4Test, reclaimDeclinedStats) { + testReclaimDeclinedStats("assigned-addresses"); +} + +// This test verifies that the lease is reclaimed before it is reused. +TEST_F(ExpirationAllocEngine4Test, reclaimReusedLeases) { + // First false value indicates that the leases will be reused. + // Second false value indicates that the lease will not be + // initially reclaimed. + testReclaimReusedLeases(DHCPREQUEST, false, false); +} + +// This test verifies that the lease is not reclaimed when it is +// reused and if its state indicates that it has been already reclaimed. +TEST_F(ExpirationAllocEngine4Test, reclaimReusedLeasesAlreadyReclaimed) { + // false value indicates that the leases will be reused + // true value indicates that the lease will be initially reclaimed. + testReclaimReusedLeases(DHCPREQUEST, false, true); +} + +// This test verifies that the expired lease is reclaimed before it +// is renewed. +TEST_F(ExpirationAllocEngine4Test, reclaimRenewedLeases) { + // true value indicates that the leases will be renewed. + // false value indicates that the lease will not be initially + // reclaimed. + testReclaimReusedLeases(DHCPREQUEST, true, false); +} + +// This test verifies that the lease is not reclaimed upon renewal +// if its state indicates that it has been already reclaimed. +TEST_F(ExpirationAllocEngine4Test, reclaimRenewedLeasesAlreadyReclaimed) { + // First true value indicates that the leases will be renewed. + // Second true value indicates that the lease will be initially + // reclaimed. + testReclaimReusedLeases(DHCPREQUEST, true, true); +} + +// This test verifies that the reused lease is not reclaimed when the +// processed message is a DHCPDISCOVER. +TEST_F(ExpirationAllocEngine4Test, reclaimReusedLeasesDiscover) { + testReclaimReusedLeases(DHCPDISCOVER, false, false); +} + +// This test verifies that the lease being in the 'expired-reclaimed' +// state is not reclaimed again when processing the DHCPDISCOVER +// message. +TEST_F(ExpirationAllocEngine4Test, reclaimRenewedLeasesDiscoverAlreadyReclaimed) { + testReclaimReusedLeases(DHCPDISCOVER, false, true); +} + +// This test verifies if the hooks installed on lease4_recover are called +// when the lease expires. +TEST_F(ExpirationAllocEngine4Test, reclaimDeclinedHook1) { + testReclaimDeclinedHook(false); // false = don't use skip callout +} + +// This test verifies if the hooks installed on lease4_recover are called +// when the lease expires and that the next step status set to SKIP +// causes the recovery to not be conducted. +TEST_F(ExpirationAllocEngine4Test, reclaimDeclinedHook2) { + testReclaimDeclinedHook(true); // true = use skip callout +} + +}; // end of anonymous namespace |