diff --git a/changelog_unreleased/3693-if-dhcp-is-disabled-then-do-not-do-lease-reclamation b/changelog_unreleased/3693-if-dhcp-is-disabled-then-do-not-do-lease-reclamation new file mode 100644 index 0000000000..fc622fb7e6 --- /dev/null +++ b/changelog_unreleased/3693-if-dhcp-is-disabled-then-do-not-do-lease-reclamation @@ -0,0 +1,5 @@ +[func] tmark + Lease reclamation is now skipped and rescheduled + while DHCP service is disabled. This applies to + both kea-dhcp4 and kea-dhcp6. + (Gitlab #3693) diff --git a/src/bin/dhcp4/ctrl_dhcp4_srv.cc b/src/bin/dhcp4/ctrl_dhcp4_srv.cc index 1d2707bcad..a5c9371fe8 100644 --- a/src/bin/dhcp4/ctrl_dhcp4_srv.cc +++ b/src/bin/dhcp4/ctrl_dhcp4_srv.cc @@ -1610,9 +1610,15 @@ ControlledDhcpv4Srv::reclaimExpiredLeases(const size_t max_leases, const bool remove_lease, const uint16_t max_unwarned_cycles) { try { - server_->alloc_engine_->reclaimExpiredLeases4(max_leases, timeout, - remove_lease, - max_unwarned_cycles); + if (network_state_->isServiceEnabled()) { + server_->alloc_engine_->reclaimExpiredLeases4(max_leases, timeout, + remove_lease, + max_unwarned_cycles); + } else { + LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC, DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED) + .arg(CfgMgr::instance().getCurrentCfg()-> + getCfgExpiration()->getReclaimTimerWaitTime()); + } } catch (const std::exception& ex) { LOG_ERROR(dhcp4_logger, DHCP4_RECLAIM_EXPIRED_LEASES_FAIL) .arg(ex.what()); @@ -1623,7 +1629,10 @@ ControlledDhcpv4Srv::reclaimExpiredLeases(const size_t max_leases, void ControlledDhcpv4Srv::deleteExpiredReclaimedLeases(const uint32_t secs) { - server_->alloc_engine_->deleteExpiredReclaimedLeases4(secs); + if (network_state_->isServiceEnabled()) { + server_->alloc_engine_->deleteExpiredReclaimedLeases4(secs); + } + // We're using the ONE_SHOT timer so there is a need to re-schedule it. TimerMgr::instance()->setup(CfgExpiration::FLUSH_RECLAIMED_TIMER_NAME); } diff --git a/src/bin/dhcp4/dhcp4_messages.cc b/src/bin/dhcp4/dhcp4_messages.cc index d097cd3980..578d058ee8 100644 --- a/src/bin/dhcp4/dhcp4_messages.cc +++ b/src/bin/dhcp4/dhcp4_messages.cc @@ -141,6 +141,7 @@ extern const isc::log::MessageID DHCP4_POST_ALLOCATION_NAME_UPDATE_FAIL = "DHCP4 extern const isc::log::MessageID DHCP4_QUERY_DATA = "DHCP4_QUERY_DATA"; extern const isc::log::MessageID DHCP4_QUERY_LABEL = "DHCP4_QUERY_LABEL"; extern const isc::log::MessageID DHCP4_RECLAIM_EXPIRED_LEASES_FAIL = "DHCP4_RECLAIM_EXPIRED_LEASES_FAIL"; +extern const isc::log::MessageID DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED = "DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED"; extern const isc::log::MessageID DHCP4_RECOVERED_STASHED_RELAY_AGENT_INFO = "DHCP4_RECOVERED_STASHED_RELAY_AGENT_INFO"; extern const isc::log::MessageID DHCP4_RELEASE = "DHCP4_RELEASE"; extern const isc::log::MessageID DHCP4_RELEASE_DELETED = "DHCP4_RELEASE_DELETED"; @@ -319,6 +320,7 @@ const char* values[] = { "DHCP4_QUERY_DATA", "%1, packet details: %2", "DHCP4_QUERY_LABEL", "received query: %1", "DHCP4_RECLAIM_EXPIRED_LEASES_FAIL", "failed to reclaim expired leases: %1", + "DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED", "dhcp6 service is currently disabled. Try again in %1 seconds.", "DHCP4_RECOVERED_STASHED_RELAY_AGENT_INFO", "recovered for query %1 relay agent option from lease %2: %3", "DHCP4_RELEASE", "%1: address %2 was released properly.", "DHCP4_RELEASE_DELETED", "%1: address %2 was deleted on release.", diff --git a/src/bin/dhcp4/dhcp4_messages.h b/src/bin/dhcp4/dhcp4_messages.h index 7c2104fe6a..112ee36779 100644 --- a/src/bin/dhcp4/dhcp4_messages.h +++ b/src/bin/dhcp4/dhcp4_messages.h @@ -142,6 +142,7 @@ extern const isc::log::MessageID DHCP4_POST_ALLOCATION_NAME_UPDATE_FAIL; extern const isc::log::MessageID DHCP4_QUERY_DATA; extern const isc::log::MessageID DHCP4_QUERY_LABEL; extern const isc::log::MessageID DHCP4_RECLAIM_EXPIRED_LEASES_FAIL; +extern const isc::log::MessageID DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED; extern const isc::log::MessageID DHCP4_RECOVERED_STASHED_RELAY_AGENT_INFO; extern const isc::log::MessageID DHCP4_RELEASE; extern const isc::log::MessageID DHCP4_RELEASE_DELETED; diff --git a/src/bin/dhcp4/dhcp4_messages.mes b/src/bin/dhcp4/dhcp4_messages.mes index 96d0ea8424..05100f7d20 100644 --- a/src/bin/dhcp4/dhcp4_messages.mes +++ b/src/bin/dhcp4/dhcp4_messages.mes @@ -916,6 +916,11 @@ the client and the transaction identification information. This error message indicates that the reclaim expired leases operation failed and provides the cause of failure. +% DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED dhcp6 service is currently disabled. Try again in %1 seconds. +This debug message is emitted when lease reclamation was scheduled to begin +but skipped because DHCPv6 service was disabled. Reclamation will continue +to be scheduled according to the configured value of reclaim-timer-wait-time. + % DHCP4_RECOVERED_STASHED_RELAY_AGENT_INFO recovered for query %1 relay agent option from lease %2: %3 Logged at debug log level 55. This debug message indicates that agent options were stashed in the lease for diff --git a/src/bin/dhcp4/tests/kea_controller_unittest.cc b/src/bin/dhcp4/tests/kea_controller_unittest.cc index 3e4bdc79d8..c800123a64 100644 --- a/src/bin/dhcp4/tests/kea_controller_unittest.cc +++ b/src/bin/dhcp4/tests/kea_controller_unittest.cc @@ -188,6 +188,18 @@ public: // stub implementation used in tests. cb_control_.reset(new TestCBControlDHCPv4()); } + + + /// @brief Convenience method that enables or disables DHCP service. + /// + /// @param enable true to enable service, false to disable it. + void enableService(bool enable) { + if (enable) { + network_state_->enableService(NetworkState::USER_COMMAND); + } else { + network_state_->disableService(NetworkState::USER_COMMAND); + } + } }; /// @brief test class for Kea configuration backend. @@ -1103,4 +1115,107 @@ TEST_F(JSONFileBackendMySQLTest, reconfigureBackendMemfileToMySQL) { #endif +// This test verifies that the DHCP server only reclaims or flushes leases +// when DHCP6 service is enabled. +TEST_F(JSONFileBackendTest, reclaimOnlyWhenServiceEnabled) { + // This is a basic configuration which enables timers for reclaiming + // expired leases and flushing them after 500 seconds since they expire. + // Both timers run at 1 second intervals. + string config = + "{ \"Dhcp4\": {" + "\"interfaces-config\": {" + " \"interfaces\": [ ]" + "}," + "\"lease-database\": {" + " \"type\": \"memfile\"," + " \"persist\": false" + "}," + "\"expired-leases-processing\": {" + " \"reclaim-timer-wait-time\": 1," + " \"hold-reclaimed-time\": 500," + " \"flush-reclaimed-timer-wait-time\": 1" + "}," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, \n" + "\"subnet4\": [ ]," + "\"valid-lifetime\": 4000 }" + "}"; + writeFile(TEST_FILE, config); + + // Create an instance of the server and initialize it. + boost::scoped_ptr srv; + ASSERT_NO_THROW(srv.reset(new NakedControlledDhcpv4Srv())); + ASSERT_NO_THROW(srv->init(TEST_FILE)); + + // Create an expired lease. The lease is expired by 40 seconds ago + // (valid lifetime = 60, cltt = now - 100). The lease will be reclaimed + // but shouldn't be flushed in the database because the reclaimed are + // held in the database 500 seconds after reclamation, according to the + // current configuration. + HWAddrPtr hwaddr_expired(new HWAddr(HWAddr::fromText("00:01:02:03:04:05"))); + Lease4Ptr lease_expired(new Lease4(IOAddress("10.0.0.1"), hwaddr_expired, + ClientIdPtr(), 60, + time(NULL) - 100, SubnetID(1))); + + // Create expired-reclaimed lease. The lease has expired 1000 - 60 seconds + // ago. It should be removed from the lease database when the "flush" timer + // goes off. + HWAddrPtr hwaddr_reclaimed(new HWAddr(HWAddr::fromText("01:02:03:04:05:06"))); + Lease4Ptr lease_reclaimed(new Lease4(IOAddress("10.0.0.2"), hwaddr_reclaimed, + ClientIdPtr(), 60, + time(NULL) - 1000, SubnetID(1))); + lease_reclaimed->state_ = Lease4::STATE_EXPIRED_RECLAIMED; + + // Add leases to the database. + LeaseMgr& lease_mgr = LeaseMgrFactory::instance(); + ASSERT_NO_THROW(lease_mgr.addLease(lease_expired)); + ASSERT_NO_THROW(lease_mgr.addLease(lease_reclaimed)); + + // Make sure they have been added. + ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.1"))); + ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.2"))); + + // Disable service. + srv->enableService(false); + + // Poll the timers for a while to make sure that each of them is executed + // at least once. + ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 5000)); + + // Verify that the leases in the database have not been processed. + ASSERT_NO_THROW( + lease_expired = lease_mgr.getLease4(IOAddress("10.0.0.1")) + ); + ASSERT_TRUE(lease_expired); + ASSERT_EQ(Lease::STATE_DEFAULT, lease_expired->state_); + + // Second lease should not have been removed. + ASSERT_NO_THROW( + lease_reclaimed = lease_mgr.getLease4(IOAddress("10.0.0.2")) + ); + ASSERT_TRUE(lease_reclaimed); + ASSERT_EQ(Lease::STATE_EXPIRED_RECLAIMED, lease_reclaimed->state_); + + // Enable service. + srv->enableService(true); + + // Poll the timers for a while to make sure that each of them is executed + // at least once. + ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 5000)); + + // Verify that the leases in the database have been processed as expected. + + // First lease should be reclaimed, but not removed. + ASSERT_NO_THROW(lease_expired = lease_mgr.getLease4(IOAddress("10.0.0.1"))); + ASSERT_TRUE(lease_expired); + EXPECT_TRUE(lease_expired->stateExpiredReclaimed()); + + // Second lease should have been removed. + ASSERT_NO_THROW( + lease_reclaimed = lease_mgr.getLease4(IOAddress("10.0.0.2")); + ); + EXPECT_FALSE(lease_reclaimed); +} + + } // End of anonymous namespace diff --git a/src/bin/dhcp6/ctrl_dhcp6_srv.cc b/src/bin/dhcp6/ctrl_dhcp6_srv.cc index a5106cd0b8..d9761793a5 100644 --- a/src/bin/dhcp6/ctrl_dhcp6_srv.cc +++ b/src/bin/dhcp6/ctrl_dhcp6_srv.cc @@ -1396,9 +1396,15 @@ ControlledDhcpv6Srv::reclaimExpiredLeases(const size_t max_leases, const bool remove_lease, const uint16_t max_unwarned_cycles) { try { + if (network_state_->isServiceEnabled()) { server_->alloc_engine_->reclaimExpiredLeases6(max_leases, timeout, remove_lease, max_unwarned_cycles); + } else { + LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED) + .arg(CfgMgr::instance().getCurrentCfg()-> + getCfgExpiration()->getReclaimTimerWaitTime()); + } } catch (const std::exception& ex) { LOG_ERROR(dhcp6_logger, DHCP6_RECLAIM_EXPIRED_LEASES_FAIL) .arg(ex.what()); @@ -1409,7 +1415,10 @@ ControlledDhcpv6Srv::reclaimExpiredLeases(const size_t max_leases, void ControlledDhcpv6Srv::deleteExpiredReclaimedLeases(const uint32_t secs) { - server_->alloc_engine_->deleteExpiredReclaimedLeases6(secs); + if (network_state_->isServiceEnabled()) { + server_->alloc_engine_->deleteExpiredReclaimedLeases6(secs); + } + // We're using the ONE_SHOT timer so there is a need to re-schedule it. TimerMgr::instance()->setup(CfgExpiration::FLUSH_RECLAIMED_TIMER_NAME); } diff --git a/src/bin/dhcp6/dhcp6_messages.cc b/src/bin/dhcp6/dhcp6_messages.cc index 57a50c5ecc..c5d57d7f35 100644 --- a/src/bin/dhcp6/dhcp6_messages.cc +++ b/src/bin/dhcp6/dhcp6_messages.cc @@ -141,6 +141,7 @@ extern const isc::log::MessageID DHCP6_QUERY_DATA = "DHCP6_QUERY_DATA"; extern const isc::log::MessageID DHCP6_QUERY_LABEL = "DHCP6_QUERY_LABEL"; extern const isc::log::MessageID DHCP6_RAPID_COMMIT = "DHCP6_RAPID_COMMIT"; extern const isc::log::MessageID DHCP6_RECLAIM_EXPIRED_LEASES_FAIL = "DHCP6_RECLAIM_EXPIRED_LEASES_FAIL"; +extern const isc::log::MessageID DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED = "DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED"; extern const isc::log::MessageID DHCP6_REGISTERED_LEASE_ADD_FAIL = "DHCP6_REGISTERED_LEASE_ADD_FAIL"; extern const isc::log::MessageID DHCP6_REGISTERED_LEASE_UPDATE_FAIL = "DHCP6_REGISTERED_LEASE_UPDATE_FAIL"; extern const isc::log::MessageID DHCP6_RELEASE_NA = "DHCP6_RELEASE_NA"; @@ -314,6 +315,7 @@ const char* values[] = { "DHCP6_QUERY_LABEL", "received query: %1", "DHCP6_RAPID_COMMIT", "%1: Rapid Commit option received, following 2-way exchange", "DHCP6_RECLAIM_EXPIRED_LEASES_FAIL", "failed to reclaim expired leases: %1", + "DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED", "dhcp6 service is currently disabled. Try again in %1 seconds.", "DHCP6_REGISTERED_LEASE_ADD_FAIL", "error in registered lease add for %1", "DHCP6_REGISTERED_LEASE_UPDATE_FAIL", "error in registered lease update for %1: %2", "DHCP6_RELEASE_NA", "%1: binding for address %2 and iaid=%3 was released properly", diff --git a/src/bin/dhcp6/dhcp6_messages.h b/src/bin/dhcp6/dhcp6_messages.h index 8820f389cb..7b5cf18c05 100644 --- a/src/bin/dhcp6/dhcp6_messages.h +++ b/src/bin/dhcp6/dhcp6_messages.h @@ -142,6 +142,7 @@ extern const isc::log::MessageID DHCP6_QUERY_DATA; extern const isc::log::MessageID DHCP6_QUERY_LABEL; extern const isc::log::MessageID DHCP6_RAPID_COMMIT; extern const isc::log::MessageID DHCP6_RECLAIM_EXPIRED_LEASES_FAIL; +extern const isc::log::MessageID DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED; extern const isc::log::MessageID DHCP6_REGISTERED_LEASE_ADD_FAIL; extern const isc::log::MessageID DHCP6_REGISTERED_LEASE_UPDATE_FAIL; extern const isc::log::MessageID DHCP6_RELEASE_NA; diff --git a/src/bin/dhcp6/dhcp6_messages.mes b/src/bin/dhcp6/dhcp6_messages.mes index 6935877a27..466502cd4c 100644 --- a/src/bin/dhcp6/dhcp6_messages.mes +++ b/src/bin/dhcp6/dhcp6_messages.mes @@ -928,6 +928,11 @@ specifies the client and transaction identification information. This error message indicates that the reclaim expired leases operation failed and provides the cause of failure. +% DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED dhcp6 service is currently disabled. Try again in %1 seconds. +This debug message is emitted when lease reclamation was scheduled to begin +but skipped because DHCPv6 service was disabled. Reclamation will continue +to be scheduled according to the configured value of reclaim-timer-wait-time. + % DHCP6_REGISTERED_LEASE_ADD_FAIL error in registered lease add for %1 This error message indicates that the registered lease add failed and provides the address being registered. diff --git a/src/bin/dhcp6/tests/kea_controller_unittest.cc b/src/bin/dhcp6/tests/kea_controller_unittest.cc index 13af8004bf..80fb89b37a 100644 --- a/src/bin/dhcp6/tests/kea_controller_unittest.cc +++ b/src/bin/dhcp6/tests/kea_controller_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2024 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2025 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 @@ -185,6 +185,17 @@ public: // stub implementation used in tests. cb_control_.reset(new TestCBControlDHCPv6()); } + + /// @brief Convenience method that enables or disables DHCP service. + /// + /// @param enable true to enable service, false to disable it. + void enableService(bool enable) { + if (enable) { + network_state_->enableService(NetworkState::USER_COMMAND); + } else { + network_state_->disableService(NetworkState::USER_COMMAND); + } + } }; /// @brief test class for Kea configuration backend. @@ -1090,4 +1101,109 @@ TEST_F(JSONFileBackendMySQLTest, reconfigureBackendMemfileToMySQL) { #endif +// This test verifies that the DHCP server only reclaims or flushes leases +// when DHCP6 service is enabled. +TEST_F(JSONFileBackendTest, reclaimOnlyWhenServiceEnabled) { + // This is a basic configuration which enables timers for reclaiming + // expired leases and flushing them after 500 seconds since they expire. + // Both timers run at 1 second intervals. + string config = + "{ \"Dhcp6\": {" + "\"interfaces-config\": {" + " \"interfaces\": [ ]" + "}," + "\"lease-database\": {" + " \"type\": \"memfile\"," + " \"persist\": false" + "}," + "\"expired-leases-processing\": {" + " \"reclaim-timer-wait-time\": 1," + " \"hold-reclaimed-time\": 500," + " \"flush-reclaimed-timer-wait-time\": 1" + "}," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"subnet6\": [ ]," + "\"preferred-lifetime\": 3000, " + "\"valid-lifetime\": 4000 }" + "}"; + writeFile(TEST_FILE, config); + + // Create an instance of the server and initialize it. + boost::scoped_ptr srv; + ASSERT_NO_THROW(srv.reset(new NakedControlledDhcpv6Srv())); + ASSERT_NO_THROW(srv->init(TEST_FILE)); + + // Create an expired lease. The lease is expired by 40 seconds ago + // (valid lifetime = 60, cltt = now - 100). The lease will be reclaimed + // but shouldn't be flushed in the database because the reclaimed are + // held in the database 500 seconds after reclamation, according to the + // current configuration. + DuidPtr duid_expired(new DUID(DUID::fromText("00:01:02:03:04:05:06").getDuid())); + Lease6Ptr lease_expired(new Lease6(Lease::TYPE_NA, IOAddress("3000::1"), + duid_expired, 1, 50, 60, SubnetID(1))); + lease_expired->cltt_ = time(NULL) - 100; + + // Create expired-reclaimed lease. The lease has expired 1000 - 60 seconds + // ago. It should be removed from the lease database when the "flush" timer + // goes off. + DuidPtr duid_reclaimed(new DUID(DUID::fromText("01:02:03:04:05:06:07").getDuid())); + Lease6Ptr lease_reclaimed(new Lease6(Lease::TYPE_NA, IOAddress("3000::2"), + duid_reclaimed, 1, 50, 60, SubnetID(1))); + lease_reclaimed->cltt_ = time(NULL) - 1000; + lease_reclaimed->state_ = Lease6::STATE_EXPIRED_RECLAIMED; + + // Add leases to the database. + LeaseMgr& lease_mgr = LeaseMgrFactory::instance(); + ASSERT_NO_THROW(lease_mgr.addLease(lease_expired)); + ASSERT_NO_THROW(lease_mgr.addLease(lease_reclaimed)); + + // Make sure they have been added. + ASSERT_TRUE(lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::1"))); + ASSERT_TRUE(lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::2"))); + + // Disable service. + srv->enableService(false); + + // Poll the timers for a while to make sure that each of them is executed + // at least once. + ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 5000)); + + // Verify that the leases in the database have not been processed. + ASSERT_NO_THROW( + lease_expired = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::1")) + ); + ASSERT_TRUE(lease_expired); + ASSERT_EQ(Lease::STATE_DEFAULT, lease_expired->state_); + + // Second lease should not have been removed. + ASSERT_NO_THROW( + lease_reclaimed = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::2")) + ); + ASSERT_TRUE(lease_reclaimed); + ASSERT_EQ(Lease::STATE_EXPIRED_RECLAIMED, lease_reclaimed->state_); + + // Enable service. + srv->enableService(true); + + // Poll the timers for a while to make sure that each of them is executed + // at least once. + ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 5000)); + + // Verify that the leases in the database have been processed as expected. + + // First lease should be reclaimed, but not removed. + ASSERT_NO_THROW( + lease_expired = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::1")) + ); + ASSERT_TRUE(lease_expired); + EXPECT_TRUE(lease_expired->stateExpiredReclaimed()); + + // Second lease should have been removed. + ASSERT_NO_THROW( + lease_reclaimed = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::2")) + ); + EXPECT_FALSE(lease_reclaimed); +} + } // End of anonymous namespace