Skip to content

Commit 9d527a1

Browse files
network: allow a custom DHCP range for isolated guest networks
An isolated guest network allocates guest IPs from the whole CIDR, so there is no way to reserve part of the address space or restrict the DHCP pool. createNetwork already takes startip and endip, but they were only used for shared networks. This stores the start and end IP on an isolated network as a DHCP range, in two new networks columns dhcp_start_ip and dhcp_end_ip, validated to be within the network CIDR, in order, and not to include the gateway. NetworkModelImpl.getAvailableIps then restricts the guest IP pool to that range when it is set. The dnsmasq config on the VR is unchanged, since it serves the host entry CloudStack writes for the IP it allocated, so no system VM template change is needed. Without a range the whole CIDR is used, so existing networks are unaffected. The range is returned by listNetworks.
1 parent 510d0ec commit 9d527a1

11 files changed

Lines changed: 305 additions & 0 deletions

File tree

‎api/src/main/java/com/cloud/network/Network.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,16 @@ public void setIp6Address(String ip6Address) {
461461

462462
void setCidr(String cidr);
463463

464+
// For an isolated guest network, an optional custom DHCP range within the CIDR that guest IPs
465+
// are allocated from; null means the whole CIDR is used, which is the default.
466+
String getDhcpStartIp();
467+
468+
void setDhcpStartIp(String dhcpStartIp);
469+
470+
String getDhcpEndIp();
471+
472+
void setDhcpEndIp(String dhcpEndIp);
473+
464474
// "networkcidr" is the network CIDR of the guest network which uses IP reservation.
465475
// It is the summation of "cidr" and the reservedIPrange(the address space used for non CloudStack purposes).
466476
// For networks not configured with IP reservation, "networkcidr" is always null

‎api/src/main/java/com/cloud/network/NetworkProfile.java‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public class NetworkProfile implements Network {
4444
private TrafficType trafficType;
4545
private String gateway;
4646
private String cidr;
47+
private String dhcpStartIp;
48+
private String dhcpEndIp;
4749
private final String networkCidr;
4850
private final String ip6Gateway;
4951
private final String ip6Cidr;
@@ -79,6 +81,8 @@ public NetworkProfile(Network network) {
7981
gateway = network.getGateway();
8082
cidr = network.getCidr();
8183
networkCidr = network.getNetworkCidr();
84+
dhcpStartIp = network.getDhcpStartIp();
85+
dhcpEndIp = network.getDhcpEndIp();
8286
ip6Gateway = network.getIp6Gateway();
8387
ip6Cidr = network.getIp6Cidr();
8488
networkOfferingId = network.getNetworkOfferingId();
@@ -228,6 +232,26 @@ public void setCidr(String cidr) {
228232
this.cidr = cidr;
229233
}
230234

235+
@Override
236+
public String getDhcpStartIp() {
237+
return dhcpStartIp;
238+
}
239+
240+
@Override
241+
public void setDhcpStartIp(String dhcpStartIp) {
242+
this.dhcpStartIp = dhcpStartIp;
243+
}
244+
245+
@Override
246+
public String getDhcpEndIp() {
247+
return dhcpEndIp;
248+
}
249+
250+
@Override
251+
public void setDhcpEndIp(String dhcpEndIp) {
252+
this.dhcpEndIp = dhcpEndIp;
253+
}
254+
231255
@Override
232256
public String getNetworkCidr() {
233257
return networkCidr;

‎api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement
6767
@Param(description = "CloudStack managed address space, all CloudStack managed Instances get IP address from CIDR")
6868
private String cidr;
6969

70+
@SerializedName(ApiConstants.START_IP)
71+
@Param(description = "the start of the custom DHCP range for an isolated network, within its CIDR", since = "4.24.0")
72+
private String dhcpStartIp;
73+
74+
@SerializedName(ApiConstants.END_IP)
75+
@Param(description = "the end of the custom DHCP range for an isolated network, within its CIDR", since = "4.24.0")
76+
private String dhcpEndIp;
77+
7078
@SerializedName(ApiConstants.NETWORK_CIDR)
7179
@Param(description = "The Network CIDR of the guest Network configured with IP reservation. It is the summation of CIDR and RESERVED_IP_RANGE")
7280
private String networkCidr;
@@ -511,6 +519,14 @@ public void setCidr(String cidr) {
511519
this.cidr = cidr;
512520
}
513521

522+
public void setDhcpStartIp(String dhcpStartIp) {
523+
this.dhcpStartIp = dhcpStartIp;
524+
}
525+
526+
public void setDhcpEndIp(String dhcpEndIp) {
527+
this.dhcpEndIp = dhcpEndIp;
528+
}
529+
514530
public void setNetworkCidr(String networkCidr) {
515531
this.networkCidr = networkCidr;
516532
}

‎engine/schema/src/main/java/com/cloud/network/dao/NetworkVO.java‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ public class NetworkVO implements Network {
8282
@Column(name = "network_cidr")
8383
String networkCidr;
8484

85+
@Column(name = "dhcp_start_ip")
86+
String dhcpStartIp;
87+
88+
@Column(name = "dhcp_end_ip")
89+
String dhcpEndIp;
90+
8591
@Column(name = "network_offering_id")
8692
long networkOfferingId;
8793

@@ -470,6 +476,26 @@ public void setCidr(String cidr) {
470476
this.cidr = cidr;
471477
}
472478

479+
@Override
480+
public String getDhcpStartIp() {
481+
return dhcpStartIp;
482+
}
483+
484+
@Override
485+
public void setDhcpStartIp(String dhcpStartIp) {
486+
this.dhcpStartIp = dhcpStartIp;
487+
}
488+
489+
@Override
490+
public String getDhcpEndIp() {
491+
return dhcpEndIp;
492+
}
493+
494+
@Override
495+
public void setDhcpEndIp(String dhcpEndIp) {
496+
this.dhcpEndIp = dhcpEndIp;
497+
}
498+
473499
// "networkcidr" is the network CIDR of the guest network which is configured with IP reservation feature
474500
// It is the summation of "cidr" and the reservedIPrange(the address space used for non cloudstack purposes.)
475501
// For networks not using IP reservation "networkcidr" is always null

‎engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@
1818
--;
1919
-- Schema upgrade from 4.23.0.0 to 24.0.0
2020
--;
21+
22+
-- Custom DHCP (guest IP allocation) range for isolated guest networks
23+
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.networks', 'dhcp_start_ip', 'VARCHAR(15) DEFAULT NULL COMMENT ''start of the custom DHCP range for an isolated network, within its CIDR'' ');
24+
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.networks', 'dhcp_end_ip', 'VARCHAR(15) DEFAULT NULL COMMENT ''end of the custom DHCP range for an isolated network, within its CIDR'' ');

‎server/src/main/java/com/cloud/api/ApiResponseHelper.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,6 +2592,8 @@ public NetworkResponse createNetworkResponse(ResponseView view, Network network)
25922592

25932593
// FIXME - either set netmask or cidr
25942594
response.setCidr(cidr);
2595+
response.setDhcpStartIp(network.getDhcpStartIp());
2596+
response.setDhcpEndIp(network.getDhcpEndIp());
25952597
if (network.getNetworkCidr() != null) {
25962598
response.setNetworkCidr((network.getNetworkCidr()));
25972599
}

‎server/src/main/java/com/cloud/network/NetworkModelImpl.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2333,6 +2333,12 @@ public Set<Long> getAvailableIps(Network network, String requestedIp) {
23332333
if ((gateway != null) && (allPossibleIps.contains(NetUtils.ip2Long(gateway))))
23342334
allPossibleIps.remove(NetUtils.ip2Long(gateway));
23352335

2336+
if (StringUtils.isNoneBlank(network.getDhcpStartIp(), network.getDhcpEndIp())) {
2337+
long start = NetUtils.ip2Long(network.getDhcpStartIp());
2338+
long end = NetUtils.ip2Long(network.getDhcpEndIp());
2339+
allPossibleIps.removeIf(ip -> ip < start || ip > end);
2340+
}
2341+
23362342
return allPossibleIps;
23372343
}
23382344

‎server/src/main/java/com/cloud/network/NetworkServiceImpl.java‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,6 +1838,12 @@ public Network createGuestNetwork(CreateNetworkCmd cmd) throws InsufficientCapac
18381838
domainId, isDomainSpecific, subdomainAccess, vpcId, startIPv6, endIPv6, ip6Gateway, ip6Cidr, displayNetwork, aclId, secondaryVlanId, privateVlanType, ntwkOff, pNtwk, aclType, owner, cidr, createVlan,
18391839
externalId, routerIPv4, routerIPv6, associatedNetwork, ip4Dns1, ip4Dns2, ip6Dns1, ip6Dns2, interfaceMTUs, networkCidrSize, keepMacAddressOnPublicNic);
18401840

1841+
// For an isolated network a start/end IP defines a custom DHCP range within the CIDR that
1842+
// guest IPs are allocated from; without it the whole CIDR is used, which is the default.
1843+
if (ntwkOff.getGuestType() == GuestType.Isolated && StringUtils.isNotBlank(startIP)) {
1844+
storeIsolatedNetworkDhcpRange(network.getId(), startIP, endIP);
1845+
}
1846+
18411847
// retrieve, acquire and associate the correct IP addresses
18421848
checkAndSetRouterSourceNatIp(owner, cmd, network);
18431849

@@ -1872,6 +1878,27 @@ private boolean isNonVpcNetworkSupportingDynamicRouting(NetworkOffering networkO
18721878
return !networkOffering.isForVpc() && NetworkOffering.RoutingMode.Dynamic == networkOffering.getRoutingMode();
18731879
}
18741880

1881+
protected void storeIsolatedNetworkDhcpRange(long networkId, String startIP, String endIP) {
1882+
NetworkVO network = _networksDao.findById(networkId);
1883+
String cidr = network.getCidr();
1884+
if (endIP == null) {
1885+
endIP = startIP;
1886+
}
1887+
if (!NetUtils.isIpWithInCidrRange(startIP, cidr) || !NetUtils.isIpWithInCidrRange(endIP, cidr)) {
1888+
throw new InvalidParameterValueException(String.format("The DHCP range %s-%s is not within the network CIDR %s", startIP, endIP, cidr));
1889+
}
1890+
if (NetUtils.ip2Long(startIP) > NetUtils.ip2Long(endIP)) {
1891+
throw new InvalidParameterValueException(String.format("The DHCP start IP %s is greater than the end IP %s", startIP, endIP));
1892+
}
1893+
String gateway = network.getGateway();
1894+
if (gateway != null && NetUtils.ip2Long(gateway) >= NetUtils.ip2Long(startIP) && NetUtils.ip2Long(gateway) <= NetUtils.ip2Long(endIP)) {
1895+
throw new InvalidParameterValueException(String.format("The DHCP range %s-%s must not include the gateway %s", startIP, endIP, gateway));
1896+
}
1897+
network.setDhcpStartIp(startIP);
1898+
network.setDhcpEndIp(endIP);
1899+
_networksDao.update(networkId, network);
1900+
}
1901+
18751902
private void validateNetworkCreationSupported(long zoneId, String zoneName, GuestType guestType) {
18761903
NsxProviderVO nsxProviderVO = nsxProviderDao.findByZoneId(zoneId);
18771904
if (Objects.nonNull(nsxProviderVO) && GuestType.L2.equals(guestType)) {

‎server/src/test/java/com/cloud/network/NetworkModelImplTest.java‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
import com.cloud.offerings.dao.NetworkOfferingServiceMapDao;
7070
import com.cloud.utils.Pair;
7171
import com.cloud.utils.net.Ip;
72+
import com.cloud.utils.net.NetUtils;
7273
import com.cloud.vm.Nic;
7374
import com.cloud.vm.NicProfile;
7475
import com.cloud.vm.VirtualMachine;
@@ -452,4 +453,43 @@ public void listSupportedNetworkServiceProvidersExcludesExtensionBackedProviders
452453
Mockito.verify(physicalNetworkServiceProviderDao, Mockito.times(1)).listAll();
453454
Mockito.verify(physicalNetworkServiceProviderDao, Mockito.never()).listBy(Mockito.anyLong());
454455
}
456+
457+
private NetworkVO networkWithDhcpRange(String cidr, String gateway, String dhcpStart, String dhcpEnd) {
458+
NetworkVO network = mock(NetworkVO.class);
459+
when(network.getCidr()).thenReturn(cidr);
460+
when(network.getGateway()).thenReturn(gateway);
461+
when(network.getDhcpStartIp()).thenReturn(dhcpStart);
462+
when(network.getDhcpEndIp()).thenReturn(dhcpEnd);
463+
Mockito.doReturn(new ArrayList<String>()).when(networkModel).getUsedIpsInNetwork(network);
464+
return network;
465+
}
466+
467+
@Test
468+
public void getAvailableIpsRestrictsToTheDhcpRangeWhenSet() {
469+
NetworkVO network = networkWithDhcpRange("10.1.1.0/24", "10.1.1.1", "10.1.1.10", "10.1.1.20");
470+
471+
Set<Long> ips = networkModel.getAvailableIps(network, null);
472+
473+
long start = NetUtils.ip2Long("10.1.1.10");
474+
long end = NetUtils.ip2Long("10.1.1.20");
475+
for (Long ip : ips) {
476+
assertTrue(ip >= start && ip <= end);
477+
}
478+
assertEquals(11, ips.size());
479+
assertTrue(ips.contains(NetUtils.ip2Long("10.1.1.10")));
480+
assertTrue(ips.contains(NetUtils.ip2Long("10.1.1.20")));
481+
assertFalse(ips.contains(NetUtils.ip2Long("10.1.1.9")));
482+
assertFalse(ips.contains(NetUtils.ip2Long("10.1.1.21")));
483+
}
484+
485+
@Test
486+
public void getAvailableIpsUsesTheWholeCidrWhenNoDhcpRangeIsSet() {
487+
NetworkVO network = networkWithDhcpRange("10.1.1.0/24", "10.1.1.1", null, null);
488+
489+
Set<Long> ips = networkModel.getAvailableIps(network, null);
490+
491+
assertTrue(ips.contains(NetUtils.ip2Long("10.1.1.10")));
492+
assertTrue(ips.contains(NetUtils.ip2Long("10.1.1.250")));
493+
assertFalse(ips.contains(NetUtils.ip2Long("10.1.1.1")));
494+
}
455495
}

‎server/src/test/java/com/cloud/network/NetworkServiceImplTest.java‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,4 +1378,51 @@ public void getAndValidateSupportForKeepMacAddressOnPublicNicParameterTestReturn
13781378

13791379
Assert.assertFalse(service.getAndValidateSupportForKeepMacAddressOnPublicNicParameter(false, networkOfferingVO));
13801380
}
1381+
1382+
private NetworkVO isolatedNetworkForDhcpRange() {
1383+
NetworkVO network = Mockito.mock(NetworkVO.class);
1384+
Mockito.when(network.getCidr()).thenReturn("10.1.1.0/24");
1385+
Mockito.when(network.getGateway()).thenReturn("10.1.1.1");
1386+
Mockito.when(networkDao.findById(5L)).thenReturn(network);
1387+
return network;
1388+
}
1389+
1390+
@Test
1391+
public void storeIsolatedNetworkDhcpRangeStoresAValidRange() {
1392+
NetworkVO network = isolatedNetworkForDhcpRange();
1393+
1394+
service.storeIsolatedNetworkDhcpRange(5L, "10.1.1.10", "10.1.1.20");
1395+
1396+
Mockito.verify(network).setDhcpStartIp("10.1.1.10");
1397+
Mockito.verify(network).setDhcpEndIp("10.1.1.20");
1398+
Mockito.verify(networkDao).update(5L, network);
1399+
}
1400+
1401+
@Test
1402+
public void storeIsolatedNetworkDhcpRangeDefaultsEndToStart() {
1403+
NetworkVO network = isolatedNetworkForDhcpRange();
1404+
1405+
service.storeIsolatedNetworkDhcpRange(5L, "10.1.1.10", null);
1406+
1407+
Mockito.verify(network).setDhcpStartIp("10.1.1.10");
1408+
Mockito.verify(network).setDhcpEndIp("10.1.1.10");
1409+
}
1410+
1411+
@Test(expected = InvalidParameterValueException.class)
1412+
public void storeIsolatedNetworkDhcpRangeRejectsRangeOutsideCidr() {
1413+
isolatedNetworkForDhcpRange();
1414+
service.storeIsolatedNetworkDhcpRange(5L, "10.2.2.10", "10.2.2.20");
1415+
}
1416+
1417+
@Test(expected = InvalidParameterValueException.class)
1418+
public void storeIsolatedNetworkDhcpRangeRejectsStartGreaterThanEnd() {
1419+
isolatedNetworkForDhcpRange();
1420+
service.storeIsolatedNetworkDhcpRange(5L, "10.1.1.20", "10.1.1.10");
1421+
}
1422+
1423+
@Test(expected = InvalidParameterValueException.class)
1424+
public void storeIsolatedNetworkDhcpRangeRejectsRangeIncludingGateway() {
1425+
isolatedNetworkForDhcpRange();
1426+
service.storeIsolatedNetworkDhcpRange(5L, "10.1.1.1", "10.1.1.20");
1427+
}
13811428
}

0 commit comments

Comments
 (0)