Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions api/src/main/java/com/cloud/network/Network.java
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,16 @@ public void setIp6Address(String ip6Address) {

void setCidr(String cidr);

// For an isolated guest network, an optional custom DHCP range within the CIDR that guest IPs
// are allocated from; null means the whole CIDR is used, which is the default.
String getDhcpStartIp();

void setDhcpStartIp(String dhcpStartIp);

String getDhcpEndIp();

void setDhcpEndIp(String dhcpEndIp);

// "networkcidr" is the network CIDR of the guest network which uses IP reservation.
// It is the summation of "cidr" and the reservedIPrange(the address space used for non CloudStack purposes).
// For networks not configured with IP reservation, "networkcidr" is always null
Expand Down
24 changes: 24 additions & 0 deletions api/src/main/java/com/cloud/network/NetworkProfile.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public class NetworkProfile implements Network {
private TrafficType trafficType;
private String gateway;
private String cidr;
private String dhcpStartIp;
private String dhcpEndIp;
private final String networkCidr;
private final String ip6Gateway;
private final String ip6Cidr;
Expand Down Expand Up @@ -79,6 +81,8 @@ public NetworkProfile(Network network) {
gateway = network.getGateway();
cidr = network.getCidr();
networkCidr = network.getNetworkCidr();
dhcpStartIp = network.getDhcpStartIp();
dhcpEndIp = network.getDhcpEndIp();
ip6Gateway = network.getIp6Gateway();
ip6Cidr = network.getIp6Cidr();
networkOfferingId = network.getNetworkOfferingId();
Expand Down Expand Up @@ -228,6 +232,26 @@ public void setCidr(String cidr) {
this.cidr = cidr;
}

@Override
public String getDhcpStartIp() {
return dhcpStartIp;
}

@Override
public void setDhcpStartIp(String dhcpStartIp) {
this.dhcpStartIp = dhcpStartIp;
}

@Override
public String getDhcpEndIp() {
return dhcpEndIp;
}

@Override
public void setDhcpEndIp(String dhcpEndIp) {
this.dhcpEndIp = dhcpEndIp;
}

@Override
public String getNetworkCidr() {
return networkCidr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement
@Param(description = "CloudStack managed address space, all CloudStack managed Instances get IP address from CIDR")
private String cidr;

@SerializedName(ApiConstants.START_IP)
@Param(description = "the start of the custom DHCP range for an isolated network, within its CIDR", since = "4.24.0")
private String dhcpStartIp;

@SerializedName(ApiConstants.END_IP)
@Param(description = "the end of the custom DHCP range for an isolated network, within its CIDR", since = "4.24.0")
private String dhcpEndIp;

@SerializedName(ApiConstants.NETWORK_CIDR)
@Param(description = "The Network CIDR of the guest Network configured with IP reservation. It is the summation of CIDR and RESERVED_IP_RANGE")
private String networkCidr;
Expand Down Expand Up @@ -511,6 +519,14 @@ public void setCidr(String cidr) {
this.cidr = cidr;
}

public void setDhcpStartIp(String dhcpStartIp) {
this.dhcpStartIp = dhcpStartIp;
}

public void setDhcpEndIp(String dhcpEndIp) {
this.dhcpEndIp = dhcpEndIp;
}

public void setNetworkCidr(String networkCidr) {
this.networkCidr = networkCidr;
}
Expand Down
26 changes: 26 additions & 0 deletions engine/schema/src/main/java/com/cloud/network/dao/NetworkVO.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ public class NetworkVO implements Network {
@Column(name = "network_cidr")
String networkCidr;

@Column(name = "dhcp_start_ip")
String dhcpStartIp;

@Column(name = "dhcp_end_ip")
String dhcpEndIp;

@Column(name = "network_offering_id")
long networkOfferingId;

Expand Down Expand Up @@ -470,6 +476,26 @@ public void setCidr(String cidr) {
this.cidr = cidr;
}

@Override
public String getDhcpStartIp() {
return dhcpStartIp;
}

@Override
public void setDhcpStartIp(String dhcpStartIp) {
this.dhcpStartIp = dhcpStartIp;
}

@Override
public String getDhcpEndIp() {
return dhcpEndIp;
}

@Override
public void setDhcpEndIp(String dhcpEndIp) {
this.dhcpEndIp = dhcpEndIp;
}

// "networkcidr" is the network CIDR of the guest network which is configured with IP reservation feature
// It is the summation of "cidr" and the reservedIPrange(the address space used for non cloudstack purposes.)
// For networks not using IP reservation "networkcidr" is always null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,6 @@ CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_details` (
PRIMARY KEY (`id`),
CONSTRAINT `fk_instance_boot_group_details__group_id` FOREIGN KEY (`boot_group_id`) REFERENCES `instance_boot_group` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Custom DHCP (guest IP allocation) range for isolated guest networks
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'' ');
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'' ');
2 changes: 2 additions & 0 deletions server/src/main/java/com/cloud/api/ApiResponseHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -2603,6 +2603,8 @@ public NetworkResponse createNetworkResponse(ResponseView view, Network network)

// FIXME - either set netmask or cidr
response.setCidr(cidr);
response.setDhcpStartIp(network.getDhcpStartIp());
response.setDhcpEndIp(network.getDhcpEndIp());
if (network.getNetworkCidr() != null) {
response.setNetworkCidr((network.getNetworkCidr()));
}
Expand Down
6 changes: 6 additions & 0 deletions server/src/main/java/com/cloud/network/NetworkModelImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2333,6 +2333,12 @@ public Set<Long> getAvailableIps(Network network, String requestedIp) {
if ((gateway != null) && (allPossibleIps.contains(NetUtils.ip2Long(gateway))))
allPossibleIps.remove(NetUtils.ip2Long(gateway));

if (StringUtils.isNoneBlank(network.getDhcpStartIp(), network.getDhcpEndIp())) {
long start = NetUtils.ip2Long(network.getDhcpStartIp());
long end = NetUtils.ip2Long(network.getDhcpEndIp());
allPossibleIps.removeIf(ip -> ip < start || ip > end);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if the network is later shrunk with the guest vm cidr option? the saved range could end up partly outside the new cidr

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1bade4b. The updateNetwork guestVmCidr path now rejects a reservation that would leave the stored DHCP range outside the new Guest VM CIDR, with a message to remove or adjust the range first, so the saved range can no longer end up partly outside the CIDR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that covers it, shrinking the network is now refused when the saved range wont fit

}

return allPossibleIps;
}

Expand Down
50 changes: 50 additions & 0 deletions server/src/main/java/com/cloud/network/NetworkServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -1834,10 +1834,23 @@ public Network createGuestNetwork(CreateNetworkCmd cmd) throws InsufficientCapac
}
}

// For an isolated network a start/end IP defines a custom DHCP range within the CIDR that guest IPs are
// allocated from; without it the whole CIDR is used, which is the default. Validate it up front so a bad
// range is rejected before the network is persisted, rather than throwing after commit and leaving an
// orphaned network.
boolean hasIsolatedDhcpRange = ntwkOff.getGuestType() == GuestType.Isolated && StringUtils.isNotBlank(startIP);
if (hasIsolatedDhcpRange) {
validateIsolatedNetworkDhcpRange(startIP, endIP, cidr, gateway);
}

Network network = commitNetwork(networkOfferingId, gateway, startIP, endIP, netmask, networkDomain, vlanId, bypassVlanOverlapCheck, name, displayText, caller, physicalNetworkId, zone.getId(),
domainId, isDomainSpecific, subdomainAccess, vpcId, startIPv6, endIPv6, ip6Gateway, ip6Cidr, displayNetwork, aclId, secondaryVlanId, privateVlanType, ntwkOff, pNtwk, aclType, owner, cidr, createVlan,
externalId, routerIPv4, routerIPv6, associatedNetwork, ip4Dns1, ip4Dns2, ip6Dns1, ip6Dns2, interfaceMTUs, networkCidrSize, keepMacAddressOnPublicNic);

if (hasIsolatedDhcpRange) {
storeIsolatedNetworkDhcpRange(network.getId(), startIP, endIP);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the range is bad this throws after the network is already saved, so does the user end up with a leftover network? could we check the range before creating it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1bade4b. The range is now validated before commitNetwork via validateIsolatedNetworkDhcpRange, so a range outside the CIDR (or reversed, or over the gateway) is rejected up front and nothing is persisted, no orphaned network. Added a unit test for the validator.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that covers it, the range is checked now before the network is saved

}

// retrieve, acquire and associate the correct IP addresses
checkAndSetRouterSourceNatIp(owner, cmd, network);

Expand Down Expand Up @@ -1872,6 +1885,32 @@ private boolean isNonVpcNetworkSupportingDynamicRouting(NetworkOffering networkO
return !networkOffering.isForVpc() && NetworkOffering.RoutingMode.Dynamic == networkOffering.getRoutingMode();
}

protected void validateIsolatedNetworkDhcpRange(String startIP, String endIP, String cidr, String gateway) {
if (endIP == null) {
endIP = startIP;
}
if (!NetUtils.isIpWithInCidrRange(startIP, cidr) || !NetUtils.isIpWithInCidrRange(endIP, cidr)) {
throw new InvalidParameterValueException(String.format("The DHCP range %s-%s is not within the network CIDR %s", startIP, endIP, cidr));
}
if (NetUtils.ip2Long(startIP) > NetUtils.ip2Long(endIP)) {
throw new InvalidParameterValueException(String.format("The DHCP start IP %s is greater than the end IP %s", startIP, endIP));
}
if (gateway != null && NetUtils.ip2Long(gateway) >= NetUtils.ip2Long(startIP) && NetUtils.ip2Long(gateway) <= NetUtils.ip2Long(endIP)) {
throw new InvalidParameterValueException(String.format("The DHCP range %s-%s must not include the gateway %s", startIP, endIP, gateway));
}
}

protected void storeIsolatedNetworkDhcpRange(long networkId, String startIP, String endIP) {
NetworkVO network = _networksDao.findById(networkId);
if (endIP == null) {
endIP = startIP;
}
validateIsolatedNetworkDhcpRange(startIP, endIP, network.getCidr(), network.getGateway());
network.setDhcpStartIp(startIP);
network.setDhcpEndIp(endIP);
_networksDao.update(networkId, network);
}

private void validateNetworkCreationSupported(long zoneId, String zoneName, GuestType guestType) {
NsxProviderVO nsxProviderVO = nsxProviderDao.findByZoneId(zoneId);
if (Objects.nonNull(nsxProviderVO) && GuestType.L2.equals(guestType)) {
Expand Down Expand Up @@ -3409,6 +3448,17 @@ public Network updateGuestNetwork(final UpdateNetworkCmd cmd) {
}
}

// A custom DHCP range must stay within the reserved Guest VM CIDR; reject a reservation that would
// leave the stored range partly or wholly outside it, otherwise guest IP allocation could be starved.
if (StringUtils.isNoneBlank(network.getDhcpStartIp(), network.getDhcpEndIp())) {
long dhcpStart = NetUtils.ip2Long(network.getDhcpStartIp());
long dhcpEnd = NetUtils.ip2Long(network.getDhcpEndIp());
if (dhcpStart < startIp || dhcpEnd > endIp) {
throw new InvalidParameterValueException(String.format("The network has a custom DHCP range %s-%s that falls outside the requested Guest VM CIDR %s. Remove or adjust the DHCP range before applying the reservation.",
network.getDhcpStartIp(), network.getDhcpEndIp(), guestVmCidr));
}
}

// In some scenarios even though guesVmCidr and network CIDR do not appear similar but
// the IP ranges exactly matches, in these special cases make sure no Reservation gets applied
if (network.getNetworkCidr() == null) {
Expand Down
40 changes: 40 additions & 0 deletions server/src/test/java/com/cloud/network/NetworkModelImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import com.cloud.offerings.dao.NetworkOfferingServiceMapDao;
import com.cloud.utils.Pair;
import com.cloud.utils.net.Ip;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.Nic;
import com.cloud.vm.NicProfile;
import com.cloud.vm.VirtualMachine;
Expand Down Expand Up @@ -452,4 +453,43 @@ public void listSupportedNetworkServiceProvidersExcludesExtensionBackedProviders
Mockito.verify(physicalNetworkServiceProviderDao, Mockito.times(1)).listAll();
Mockito.verify(physicalNetworkServiceProviderDao, Mockito.never()).listBy(Mockito.anyLong());
}

private NetworkVO networkWithDhcpRange(String cidr, String gateway, String dhcpStart, String dhcpEnd) {
NetworkVO network = mock(NetworkVO.class);
when(network.getCidr()).thenReturn(cidr);
when(network.getGateway()).thenReturn(gateway);
when(network.getDhcpStartIp()).thenReturn(dhcpStart);
when(network.getDhcpEndIp()).thenReturn(dhcpEnd);
Mockito.doReturn(new ArrayList<String>()).when(networkModel).getUsedIpsInNetwork(network);
return network;
}

@Test
public void getAvailableIpsRestrictsToTheDhcpRangeWhenSet() {
NetworkVO network = networkWithDhcpRange("10.1.1.0/24", "10.1.1.1", "10.1.1.10", "10.1.1.20");

Set<Long> ips = networkModel.getAvailableIps(network, null);

long start = NetUtils.ip2Long("10.1.1.10");
long end = NetUtils.ip2Long("10.1.1.20");
for (Long ip : ips) {
assertTrue(ip >= start && ip <= end);
}
assertEquals(11, ips.size());
assertTrue(ips.contains(NetUtils.ip2Long("10.1.1.10")));
assertTrue(ips.contains(NetUtils.ip2Long("10.1.1.20")));
assertFalse(ips.contains(NetUtils.ip2Long("10.1.1.9")));
assertFalse(ips.contains(NetUtils.ip2Long("10.1.1.21")));
}

@Test
public void getAvailableIpsUsesTheWholeCidrWhenNoDhcpRangeIsSet() {
NetworkVO network = networkWithDhcpRange("10.1.1.0/24", "10.1.1.1", null, null);

Set<Long> ips = networkModel.getAvailableIps(network, null);

assertTrue(ips.contains(NetUtils.ip2Long("10.1.1.10")));
assertTrue(ips.contains(NetUtils.ip2Long("10.1.1.250")));
assertFalse(ips.contains(NetUtils.ip2Long("10.1.1.1")));
}
}
58 changes: 58 additions & 0 deletions server/src/test/java/com/cloud/network/NetworkServiceImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1378,4 +1378,62 @@ public void getAndValidateSupportForKeepMacAddressOnPublicNicParameterTestReturn

Assert.assertFalse(service.getAndValidateSupportForKeepMacAddressOnPublicNicParameter(false, networkOfferingVO));
}

private NetworkVO isolatedNetworkForDhcpRange() {
NetworkVO network = Mockito.mock(NetworkVO.class);
Mockito.when(network.getCidr()).thenReturn("10.1.1.0/24");
Mockito.when(network.getGateway()).thenReturn("10.1.1.1");
Mockito.when(networkDao.findById(5L)).thenReturn(network);
return network;
}

@Test
public void storeIsolatedNetworkDhcpRangeStoresAValidRange() {
NetworkVO network = isolatedNetworkForDhcpRange();

service.storeIsolatedNetworkDhcpRange(5L, "10.1.1.10", "10.1.1.20");

Mockito.verify(network).setDhcpStartIp("10.1.1.10");
Mockito.verify(network).setDhcpEndIp("10.1.1.20");
Mockito.verify(networkDao).update(5L, network);
}

@Test
public void storeIsolatedNetworkDhcpRangeDefaultsEndToStart() {
NetworkVO network = isolatedNetworkForDhcpRange();

service.storeIsolatedNetworkDhcpRange(5L, "10.1.1.10", null);

Mockito.verify(network).setDhcpStartIp("10.1.1.10");
Mockito.verify(network).setDhcpEndIp("10.1.1.10");
}

@Test(expected = InvalidParameterValueException.class)
public void storeIsolatedNetworkDhcpRangeRejectsRangeOutsideCidr() {
isolatedNetworkForDhcpRange();
service.storeIsolatedNetworkDhcpRange(5L, "10.2.2.10", "10.2.2.20");
}

@Test(expected = InvalidParameterValueException.class)
public void storeIsolatedNetworkDhcpRangeRejectsStartGreaterThanEnd() {
isolatedNetworkForDhcpRange();
service.storeIsolatedNetworkDhcpRange(5L, "10.1.1.20", "10.1.1.10");
}

@Test(expected = InvalidParameterValueException.class)
public void storeIsolatedNetworkDhcpRangeRejectsRangeIncludingGateway() {
isolatedNetworkForDhcpRange();
service.storeIsolatedNetworkDhcpRange(5L, "10.1.1.1", "10.1.1.20");
}

@Test
public void validateIsolatedNetworkDhcpRangeAcceptsRangeWithinCidr() {
// validated up front (before the network is persisted) so a bad range never leaves an orphaned network
service.validateIsolatedNetworkDhcpRange("10.1.1.10", "10.1.1.20", "10.1.1.0/24", "10.1.1.1");
}

@Test(expected = InvalidParameterValueException.class)
public void validateIsolatedNetworkDhcpRangeRejectsRangeOutsideCidr() {
service.validateIsolatedNetworkDhcpRange("10.9.9.10", "10.9.9.20", "10.1.1.0/24", "10.1.1.1");
}
}
Loading