This commit is contained in:
Shawn Edwards 2026-01-22 09:17:16 +01:00 committed by GitHub
commit 47d46a640b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 294 additions and 17 deletions

View File

@ -36,6 +36,7 @@ public class DhcpEntryCommand extends NetworkElementCommand {
private boolean isDefault;
boolean executeInSequence = false;
boolean remove;
Long leaseTime;
public boolean isRemove() {
return remove;
@ -152,4 +153,12 @@ public class DhcpEntryCommand extends NetworkElementCommand {
public void setDefault(boolean isDefault) {
this.isDefault = isDefault;
}
public Long getLeaseTime() {
return leaseTime;
}
public void setLeaseTime(Long leaseTime) {
this.leaseTime = leaseTime;
}
}

View File

@ -35,7 +35,7 @@ public class DhcpEntryConfigItem extends AbstractConfigItemFacade {
final DhcpEntryCommand command = (DhcpEntryCommand) cmd;
final VmDhcpConfig vmDhcpConfig = new VmDhcpConfig(command.getVmName(), command.getVmMac(), command.getVmIpAddress(), command.getVmIp6Address(), command.getDuid(), command.getDefaultDns(),
command.getDefaultRouter(), command.getStaticRoutes(), command.isDefault(), command.isRemove());
command.getDefaultRouter(), command.getStaticRoutes(), command.isDefault(), command.isRemove(), command.getLeaseTime());
return generateConfigItems(vmDhcpConfig);
}

View File

@ -29,6 +29,7 @@ public class VmDhcpConfig extends ConfigBase {
private String defaultGateway;
private String staticRoutes;
private boolean defaultEntry;
private Long leaseTime;
// Indicate if the entry should be removed when set to true
private boolean remove;
@ -39,6 +40,11 @@ public class VmDhcpConfig extends ConfigBase {
public VmDhcpConfig(String hostName, String macAddress, String ipv4Address, String ipv6Address, String ipv6Duid, String dnsAddresses, String defaultGateway,
String staticRoutes, boolean defaultEntry, boolean remove) {
this(hostName, macAddress, ipv4Address, ipv6Address, ipv6Duid, dnsAddresses, defaultGateway, staticRoutes, defaultEntry, remove, null);
}
public VmDhcpConfig(String hostName, String macAddress, String ipv4Address, String ipv6Address, String ipv6Duid, String dnsAddresses, String defaultGateway,
String staticRoutes, boolean defaultEntry, boolean remove, Long leaseTime) {
super(VM_DHCP);
this.hostName = hostName;
this.macAddress = macAddress;
@ -50,6 +56,7 @@ public class VmDhcpConfig extends ConfigBase {
this.staticRoutes = staticRoutes;
this.defaultEntry = defaultEntry;
this.remove = remove;
this.leaseTime = leaseTime;
}
public String getHostName() {
@ -132,4 +139,12 @@ public class VmDhcpConfig extends ConfigBase {
this.defaultEntry = defaultEntry;
}
public Long getLeaseTime() {
return leaseTime;
}
public void setLeaseTime(Long leaseTime) {
this.leaseTime = leaseTime;
}
}

View File

@ -91,6 +91,10 @@ public interface NetworkOrchestrationService {
ConfigKey<Integer> NetworkThrottlingRate = new ConfigKey<>("Network", Integer.class, NetworkThrottlingRateCK, "200",
"Default data transfer rate in megabits per second allowed in network.", true, ConfigKey.Scope.Zone);
ConfigKey<Integer> DhcpLeaseTimeout = new ConfigKey<>("Network", Integer.class, "dhcp.lease.timeout", "0",
"DHCP lease time in seconds for VMs. Use 0 for infinite lease time (default). A non-zero value sets the lease duration in seconds.",
true, ConfigKey.Scope.Zone, "0-");
ConfigKey<Boolean> PromiscuousMode = new ConfigKey<>("Advanced", Boolean.class, "network.promiscuous.mode", "false",
"Whether to allow or deny promiscuous mode on NICs for applicable network elements such as for vswitch/dvswitch portgroups.", true);

View File

@ -17,18 +17,18 @@
# under the License.
# Usage: dhcpd_edithosts.py mac ip hostname dns gateway nextserver
# Usage: dhcpd_edithosts.py mac ip hostname dns gateway nextserver [leasetime]
import sys, os
from os.path import exists
from time import sleep
from os import remove
usage = '''dhcpd_edithosts.py mac ip hostname dns gateway nextserver'''
usage = '''dhcpd_edithosts.py mac ip hostname dns gateway nextserver [leasetime]'''
conf_path = "/etc/dhcpd.conf"
file_lock = "/etc/dhcpd.conf_locked"
sleep_max = 20
host_entry = 'host %s { hardware ethernet %s; fixed-address %s; option domain-name-servers %s; option domain-name "%s"; option routers %s; default-lease-time infinite; max-lease-time infinite; min-lease-time infinite; filename "pxelinux.0";}'
host_entry1 = 'host %s { hardware ethernet %s; fixed-address %s; option domain-name-servers %s; option domain-name "%s"; option routers %s; default-lease-time infinite; max-lease-time infinite; min-lease-time infinite; next-server %s; filename "pxelinux.0";}'
host_entry = 'host %s { hardware ethernet %s; fixed-address %s; option domain-name-servers %s; option domain-name "%s"; option routers %s; default-lease-time %s; max-lease-time %s; min-lease-time %s; filename "pxelinux.0";}'
host_entry1 = 'host %s { hardware ethernet %s; fixed-address %s; option domain-name-servers %s; option domain-name "%s"; option routers %s; default-lease-time %s; max-lease-time %s; min-lease-time %s; next-server %s; filename "pxelinux.0";}'
def lock():
if exists(file_lock):
count = 0
@ -59,10 +59,14 @@ def unlock():
print "Cannot remove file lock at %s" % file_lock
return False
def insert_host_entry(mac, ip, hostname, dns, gateway, next_server):
def insert_host_entry(mac, ip, hostname, dns, gateway, next_server, lease_time="infinite"):
if lock() == False:
return 1
# Convert 0 to 'infinite' for lease time
if lease_time == "0":
lease_time = "infinite"
cmd = 'sed -i /"fixed-address %s"/d %s' % (ip, conf_path)
ret = os.system(cmd)
if ret != 0:
@ -78,9 +82,9 @@ def insert_host_entry(mac, ip, hostname, dns, gateway, next_server):
return 1
if next_server != "null":
entry = host_entry1 % (hostname, mac, ip, dns, "cloudnine.internal", gateway, next_server)
entry = host_entry1 % (hostname, mac, ip, dns, "cloudnine.internal", gateway, lease_time, lease_time, lease_time, next_server)
else:
entry = host_entry % (hostname, mac, ip, dns, "cloudnine.internal", gateway)
entry = host_entry % (hostname, mac, ip, dns, "cloudnine.internal", gateway, lease_time, lease_time, lease_time)
cmd = '''echo '%s' >> %s''' % (entry, conf_path)
ret = os.system(cmd)
if ret != 0:
@ -111,6 +115,7 @@ if __name__ == "__main__":
dns = sys.argv[4]
gateway = sys.argv[5]
next_server = sys.argv[6]
lease_time = sys.argv[7] if len(sys.argv) > 7 else "infinite"
if exists(conf_path) == False:
conf_path = "/etc/dhcp/dhcpd.conf"
@ -118,5 +123,5 @@ if __name__ == "__main__":
print "Cannot find dhcpd.conf"
sys.exit(1)
ret = insert_host_entry(mac, ip, hostname, dns, gateway, next_server)
ret = insert_host_entry(mac, ip, hostname, dns, gateway, next_server, lease_time)
sys.exit(ret)

View File

@ -21,6 +21,7 @@
# $1 : the mac address
# $2 : the associated ip address
# $3 : the hostname
# $4 : the lease time (optional, defaults to 'infinite')
wait_for_dnsmasq () {
local _pid=$(pidof dnsmasq)
@ -41,11 +42,17 @@ no_dhcp_release=$?
[ ! -f /etc/dhcphosts.txt ] && touch /etc/dhcphosts.txt
[ ! -f /var/lib/misc/dnsmasq.leases ] && touch /var/lib/misc/dnsmasq.leases
# Set lease time, default to 'infinite', convert 0 to 'infinite'
lease_time=${4:-infinite}
if [ "$lease_time" = "0" ]; then
lease_time=infinite
fi
sed -i /$1/d /etc/dhcphosts.txt
sed -i /$2,/d /etc/dhcphosts.txt
sed -i /$3,/d /etc/dhcphosts.txt
echo "$1,$2,$3,infinite" >>/etc/dhcphosts.txt
echo "$1,$2,$3,$lease_time" >>/etc/dhcphosts.txt
#release previous dhcp lease if present
if [ $no_dhcp_release -eq 0 ]

View File

@ -296,6 +296,10 @@ public class CommandSetupHelper {
dhcpCommand.setDefault(nic.isDefaultNic());
dhcpCommand.setRemove(remove);
// Set DHCP lease timeout from zone-scoped config (0 = infinite)
Long leaseTime = (long) NetworkOrchestrationService.DhcpLeaseTimeout.valueIn(router.getDataCenterId());
dhcpCommand.setLeaseTime(leaseTime);
dhcpCommand.setAccessDetail(NetworkElementCommand.ROUTER_IP, _routerControlHelper.getRouterControlIp(router.getId()));
dhcpCommand.setAccessDetail(NetworkElementCommand.ROUTER_NAME, router.getInstanceName());
dhcpCommand.setAccessDetail(NetworkElementCommand.ROUTER_GUEST_IP, _routerControlHelper.getRouterIpInNetwork(nic.getNetworkId(), router.getId()));

View File

@ -46,6 +46,8 @@ import com.cloud.utils.net.Ip;
import com.cloud.vm.NicVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.NicDao;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.network.BgpPeerVO;
import org.apache.cloudstack.network.dao.BgpPeerDetailsDao;
import org.junit.Assert;
@ -271,4 +273,42 @@ public class CommandSetupHelperTest {
Assert.assertTrue(cmd instanceof SetBgpPeersCommand);
Assert.assertEquals(4, ((SetBgpPeersCommand) cmd).getBpgPeers().length);
}
@Test
public void testDhcpLeaseTimeoutDefaultValue() {
// Test that the default value is 0 (infinite)
Integer defaultValue = NetworkOrchestrationService.DhcpLeaseTimeout.value();
Assert.assertEquals("Default DHCP lease timeout should be 0 (infinite)", 0, defaultValue.intValue());
}
@Test
public void testDhcpLeaseTimeoutAcceptsZero() {
// Test that 0 value is accepted (infinite lease)
ConfigKey<Integer> configKey = NetworkOrchestrationService.DhcpLeaseTimeout;
Assert.assertNotNull("ConfigKey should not be null", configKey);
Assert.assertEquals("ConfigKey default should be 0", "0", configKey.defaultValue());
}
@Test
public void testDhcpLeaseTimeoutAcceptsPositiveValues() {
// Test that positive values are accepted
ConfigKey<Integer> configKey = NetworkOrchestrationService.DhcpLeaseTimeout;
Assert.assertNotNull("ConfigKey should not be null", configKey);
// Verify the config key exists and has expected default
Assert.assertEquals("ConfigKey default should be 0", "0", configKey.defaultValue());
}
@Test
public void testDhcpLeaseTimeoutHasZoneScope() {
// Test that the ConfigKey has Zone scope
ConfigKey<Integer> configKey = NetworkOrchestrationService.DhcpLeaseTimeout;
Assert.assertTrue("ConfigKey should have Zone scope", configKey.getScopes().contains(ConfigKey.Scope.Zone));
}
@Test
public void testDhcpLeaseTimeoutIsDynamic() {
// Test that the ConfigKey is dynamic (can be updated at runtime)
ConfigKey<Integer> configKey = NetworkOrchestrationService.DhcpLeaseTimeout;
Assert.assertTrue("ConfigKey should be dynamic", configKey.isDynamic());
}
}

View File

@ -0,0 +1,184 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.network.router;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.routing.DhcpEntryCommand;
import com.cloud.agent.manager.Commands;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.network.NetworkModel;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkDetailsDao;
import com.cloud.offerings.dao.NetworkOfferingDao;
import com.cloud.offerings.dao.NetworkOfferingDetailsDao;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.NicVO;
import com.cloud.vm.dao.NicDao;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;
/**
* Integration tests for DHCP lease timeout functionality.
* Tests the end-to-end flow from ConfigKey through DhcpEntryCommand creation.
*/
@RunWith(MockitoJUnitRunner.class)
public class DhcpLeaseTimeoutIntegrationTest {
@InjectMocks
protected CommandSetupHelper commandSetupHelper = new CommandSetupHelper();
@Mock
NicDao nicDao;
@Mock
NetworkDao networkDao;
@Mock
NetworkModel networkModel;
@Mock
NetworkOfferingDao networkOfferingDao;
@Mock
NetworkOfferingDetailsDao networkOfferingDetailsDao;
@Mock
NetworkDetailsDao networkDetailsDao;
@Mock
RouterControlHelper routerControlHelper;
@Mock
DataCenterDao dcDao;
private VirtualRouter mockRouter;
private UserVmVO mockVm;
private NicVO mockNic;
private DataCenterVO mockDc;
@Before
public void setUp() {
ReflectionTestUtils.setField(commandSetupHelper, "_nicDao", nicDao);
ReflectionTestUtils.setField(commandSetupHelper, "_networkDao", networkDao);
ReflectionTestUtils.setField(commandSetupHelper, "_networkModel", networkModel);
ReflectionTestUtils.setField(commandSetupHelper, "_networkOfferingDao", networkOfferingDao);
ReflectionTestUtils.setField(commandSetupHelper, "networkOfferingDetailsDao", networkOfferingDetailsDao);
ReflectionTestUtils.setField(commandSetupHelper, "networkDetailsDao", networkDetailsDao);
ReflectionTestUtils.setField(commandSetupHelper, "_routerControlHelper", routerControlHelper);
ReflectionTestUtils.setField(commandSetupHelper, "_dcDao", dcDao);
// Create common mocks
mockRouter = Mockito.mock(VirtualRouter.class);
mockVm = Mockito.mock(UserVmVO.class);
mockNic = Mockito.mock(NicVO.class);
mockDc = Mockito.mock(DataCenterVO.class);
// Setup default mock behaviors
when(mockRouter.getId()).thenReturn(100L);
when(mockRouter.getInstanceName()).thenReturn("r-100-VM");
when(mockRouter.getDataCenterId()).thenReturn(1L);
when(mockVm.getHostName()).thenReturn("test-vm");
when(mockNic.getMacAddress()).thenReturn("02:00:0a:0b:0c:0d");
when(mockNic.getIPv4Address()).thenReturn("10.1.1.10");
when(mockNic.getIPv6Address()).thenReturn(null);
when(mockNic.getNetworkId()).thenReturn(400L);
when(mockNic.isDefaultNic()).thenReturn(true);
when(dcDao.findById(anyLong())).thenReturn(mockDc);
when(mockDc.getNetworkType()).thenReturn(com.cloud.dc.DataCenter.NetworkType.Advanced);
when(routerControlHelper.getRouterControlIp(anyLong())).thenReturn("10.1.1.1");
when(routerControlHelper.getRouterIpInNetwork(anyLong(), anyLong())).thenReturn("10.1.1.1");
when(networkModel.getExecuteInSeqNtwkElmtCmd()).thenReturn(false);
}
@Test
public void testDhcpEntryCommandContainsLeaseTime() {
// Test that DhcpEntryCommand includes the lease time from ConfigKey
Commands cmds = new Commands(Command.OnError.Continue);
commandSetupHelper.createDhcpEntryCommand(mockRouter, mockVm, mockNic, false, cmds);
Assert.assertEquals("Should have one DHCP command", 1, cmds.size());
DhcpEntryCommand dhcpCmd = (DhcpEntryCommand) cmds.toCommands()[0];
Assert.assertNotNull("DHCP command should not be null", dhcpCmd);
Assert.assertNotNull("Lease time should not be null", dhcpCmd.getLeaseTime());
// Default value should be 0 (infinite)
Assert.assertEquals("Default lease time should be 0", Long.valueOf(0L), dhcpCmd.getLeaseTime());
}
@Test
public void testDhcpEntryCommandUsesZoneScopedValue() {
// Test that the command uses zone-scoped configuration
Long zoneId = mockRouter.getDataCenterId();
Integer expectedLeaseTime = NetworkOrchestrationService.DhcpLeaseTimeout.valueIn(zoneId);
Commands cmds = new Commands(Command.OnError.Continue);
commandSetupHelper.createDhcpEntryCommand(mockRouter, mockVm, mockNic, false, cmds);
DhcpEntryCommand dhcpCmd = (DhcpEntryCommand) cmds.toCommands()[0];
Assert.assertEquals("Lease time should match zone-scoped config",
expectedLeaseTime.longValue(), dhcpCmd.getLeaseTime().longValue());
}
@Test
public void testInfiniteLeaseWithZeroValue() {
// Test that 0 value represents infinite lease
ConfigKey<Integer> configKey = NetworkOrchestrationService.DhcpLeaseTimeout;
Assert.assertEquals("Default value should be 0 for infinite lease", "0", configKey.defaultValue());
Commands cmds = new Commands(Command.OnError.Continue);
commandSetupHelper.createDhcpEntryCommand(mockRouter, mockVm, mockNic, false, cmds);
DhcpEntryCommand dhcpCmd = (DhcpEntryCommand) cmds.toCommands()[0];
Assert.assertEquals("Lease time 0 represents infinite lease", Long.valueOf(0L), dhcpCmd.getLeaseTime());
}
@Test
public void testDhcpCommandForNonDefaultNic() {
// Test DHCP command creation for non-default NIC
when(mockNic.isDefaultNic()).thenReturn(false);
Commands cmds = new Commands(Command.OnError.Continue);
commandSetupHelper.createDhcpEntryCommand(mockRouter, mockVm, mockNic, false, cmds);
DhcpEntryCommand dhcpCmd = (DhcpEntryCommand) cmds.toCommands()[0];
Assert.assertNotNull("DHCP command should be created for non-default NIC", dhcpCmd);
Assert.assertNotNull("Lease time should be set even for non-default NIC", dhcpCmd.getLeaseTime());
Assert.assertFalse("Command should reflect non-default NIC", dhcpCmd.isDefault());
}
@Test
public void testDhcpCommandWithRemoveFlag() {
// Test DHCP command with remove flag set
Commands cmds = new Commands(Command.OnError.Continue);
commandSetupHelper.createDhcpEntryCommand(mockRouter, mockVm, mockNic, true, cmds);
DhcpEntryCommand dhcpCmd = (DhcpEntryCommand) cmds.toCommands()[0];
Assert.assertNotNull("DHCP command should be created even with remove flag", dhcpCmd);
Assert.assertTrue("Remove flag should be set", dhcpCmd.isRemove());
// Lease time should still be included even for removal
Assert.assertNotNull("Lease time should be present even for removal", dhcpCmd.getLeaseTime());
}
}

View File

@ -199,12 +199,14 @@ class CsDhcp(CsDataBag):
def add(self, entry):
self.add_host(entry['ipv4_address'], entry['host_name'])
# Lease time set to "infinite" since we properly control all DHCP/DNS config via CloudStack.
# Lease time is configurable via CloudStack global config dhcp.lease.timeout
# 0 = infinite (default), otherwise the value represents seconds
# Infinite time helps avoid some edge cases which could cause DHCPNAK being sent to VMs since
# (RHEL) system lose routes when they receive DHCPNAK.
# When VM is expunged, its active lease and DHCP/DNS config is properly removed from related files in VR,
# so the infinite duration of lease does not cause any issues or garbage.
lease = 'infinite'
lease_time = entry.get('lease_time', 0)
lease = 'infinite' if lease_time == 0 else str(lease_time)
if entry['default_entry']:
self.dhcp_hosts.add("%s,%s,%s,%s" % (entry['mac_address'],

View File

@ -21,7 +21,7 @@
# edithosts.sh -- edit the dhcphosts file on the routing domain
usage() {
printf "Usage: %s: -m <MAC address> -4 <IPv4 address> -6 <IPv6 address> -h <hostname> -d <default router> -n <name server address> -s <Routes> -u <DUID> [-N]\n" $(basename $0) >&2
printf "Usage: %s: -m <MAC address> -4 <IPv4 address> -6 <IPv6 address> -h <hostname> -d <default router> -n <name server address> -s <Routes> -u <DUID> -l <lease time> [-N]\n" $(basename $0) >&2
}
mac=
@ -33,8 +33,9 @@ dns=
routes=
duid=
nondefault=
lease_time=infinite
while getopts 'm:4:h:d:n:s:6:u:N' OPTION
while getopts 'm:4:h:d:n:s:6:u:l:N' OPTION
do
case $OPTION in
m) mac="$OPTARG"
@ -53,6 +54,8 @@ do
;;
s) routes="$OPTARG"
;;
l) lease_time="$OPTARG"
;;
N) nondefault=1
;;
?) usage
@ -124,17 +127,21 @@ fi
sed -i /$host,/d $DHCP_HOSTS
#put in the new entry
# If lease_time is 0, use 'infinite', otherwise use the value
if [ "$lease_time" = "0" ]; then
lease_time=infinite
fi
if [ $ipv4 ]
then
echo "$mac,$ipv4,$host,infinite" >>$DHCP_HOSTS
echo "$mac,$ipv4,$host,$lease_time" >>$DHCP_HOSTS
fi
if [ $ipv6 ]
then
if [ $nondefault ]
then
echo "id:$duid,set:nondefault6,[$ipv6],$host,infinite" >>$DHCP_HOSTS
echo "id:$duid,set:nondefault6,[$ipv6],$host,$lease_time" >>$DHCP_HOSTS
else
echo "id:$duid,[$ipv6],$host,infinite" >>$DHCP_HOSTS
echo "id:$duid,[$ipv6],$host,$lease_time" >>$DHCP_HOSTS
fi
fi