Support to pass provider when creating public ip range and create IPAM on Netris (#28)

* UI: support to pass provider when creating public ip range

* prevent adding public ip range for a provider that isnt supported in zone

* Create public range on Netris when created on CloudStack

---------

Co-authored-by: nvazquez <nicovazquez90@gmail.com>
This commit is contained in:
Pearl Dsilva 2024-12-06 11:48:45 -05:00 committed by GitHub
parent aef61973f3
commit 4bf4dafcb3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 130 additions and 90 deletions

View File

@ -21,6 +21,7 @@ import com.cloud.network.SDNProviderNetworkRule;
import com.cloud.network.vpc.Vpc;
public interface NetrisService {
boolean createIPAMAllocationsForZoneLevelPublicRanges(long zoneId);
boolean createVpcResource(long zoneId, long accountId, long domainId, Long vpcId, String vpcName, boolean sourceNatEnabled, String cidr, boolean isVpcNetwork);
boolean deleteVpcResource(long zoneId, long accountId, long domainId, Vpc vpc);
boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr);

View File

@ -16,21 +16,14 @@
// under the License.
package org.apache.cloudstack.service;
import com.cloud.agent.api.Answer;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.VlanDetailsVO;
import com.cloud.dc.VlanVO;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.VlanDao;
import com.cloud.dc.dao.VlanDetailsDao;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.host.DetailVO;
import com.cloud.host.Host;
import com.cloud.host.dao.HostDetailsDao;
import com.cloud.network.Network;
import com.cloud.network.Networks;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.NetrisProviderDao;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
@ -38,16 +31,12 @@ import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.network.element.NetrisProviderVO;
import com.cloud.network.netris.NetrisProvider;
import com.cloud.network.netris.NetrisService;
import com.cloud.resource.ResourceManager;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import com.google.common.annotations.VisibleForTesting;
import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressString;
import org.apache.cloudstack.agent.api.SetupNetrisPublicRangeCommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.command.AddNetrisProviderCmd;
import org.apache.cloudstack.api.command.DeleteNetrisProviderCmd;
@ -56,7 +45,6 @@ import org.apache.cloudstack.api.response.NetrisProviderResponse;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.resource.NetrisResource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -68,7 +56,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
public class NetrisProviderServiceImpl implements NetrisProviderService {
@ -87,11 +74,7 @@ public class NetrisProviderServiceImpl implements NetrisProviderService {
@Inject
NetworkDao networkDao;
@Inject
private IPAddressDao ipAddressDao;
@Inject
private VlanDao vlanDao;
@Inject
private VlanDetailsDao vlanDetailsDao;
private NetrisService netrisService;
@Override
public NetrisProvider addProvider(AddNetrisProviderCmd cmd) {
@ -147,81 +130,13 @@ public class NetrisProviderServiceImpl implements NetrisProviderService {
} else {
throw new CloudRuntimeException("Failed to add Netris controller due to internal error.");
}
createNetrisPublicIpRangesOnNetrisProvider(zoneId, netrisResource);
netrisService.createIPAMAllocationsForZoneLevelPublicRanges(zoneId);
} catch (ConfigurationException e) {
throw new CloudRuntimeException(e.getMessage());
}
return netrisProvider;
}
/**
* Calculate the minimum CIDR subnet containing the IP range (using the library: <a href="https://github.com/seancfoley/IPAddress">IPAddress</a>)
* From: <a href="https://github.com/seancfoley/IPAddress/wiki/Code-Examples-3:-Subnetting-and-Other-Subnet-Operations#from-start-and-end-address-get-single-cidr-block-covering-both">Example</a>
* @param ipRange format: startIP-endIP
* @return the minimum CIDR containing the IP range
*/
protected String calculateSubnetCidrFromIpRange(String ipRange) {
if (StringUtils.isBlank(ipRange) || !ipRange.contains("-")) {
return null;
}
String[] rangeArray = ipRange.split("-");
String startIp = rangeArray[0];
String endIp = rangeArray[1];
IPAddress startIpAddress = new IPAddressString(startIp).getAddress();
IPAddress endIpAddress = new IPAddressString(endIp).getAddress();
return startIpAddress.coverWithPrefixBlock(endIpAddress).toPrefixLengthString();
}
/**
* Prepare the Netris Public Range to be used by CloudStack after the zone is created and the Netris provider is added
*/
public SetupNetrisPublicRangeCommand createSetupPublicRangeCommand(long zoneId, String gateway, String netmask, String ipRange) {
String superCidr = NetUtils.getCidrFromGatewayAndNetmask(gateway, netmask);
String subnetNatCidr = calculateSubnetCidrFromIpRange(ipRange);
return new SetupNetrisPublicRangeCommand(zoneId, superCidr, subnetNatCidr);
}
protected void createNetrisPublicIpRangesOnNetrisProvider(long zoneId, NetrisResource netrisResource) {
List<PhysicalNetworkVO> physicalNetworks = physicalNetworkDao.listByZoneAndTrafficType(zoneId, Networks.TrafficType.Public);
physicalNetworks = physicalNetworks.stream().filter(x -> x.getIsolationMethods().contains(Network.Provider.Netris.getName())).collect(Collectors.toList());
if (CollectionUtils.isEmpty(physicalNetworks)) {
return;
}
for (PhysicalNetworkVO physicalNetwork : physicalNetworks) {
List<IPAddressVO> publicIps = ipAddressDao.listByPhysicalNetworkId(physicalNetwork.getId());
List<Long> vlanDbIds = publicIps.stream()
.filter(x -> !x.isForSystemVms())
.map(IPAddressVO::getVlanId)
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(vlanDbIds)) {
String msg = "Cannot find a public IP range VLAN range for the Netris Public traffic";
logger.error(msg);
throw new CloudRuntimeException(msg);
}
for (Long vlanDbId : vlanDbIds) {
VlanVO vlanRecord = vlanDao.findById(vlanDbId);
if (vlanRecord == null) {
logger.error("Cannot set up the Netris Public IP range as it cannot find the public range on database");
return;
}
VlanDetailsVO vlanDetail = vlanDetailsDao.findDetail(vlanDbId, ApiConstants.NETRIS_DETAIL_KEY);
if (vlanDetail == null) {
logger.debug("Skipping the Public IP range {} creation on Netris as it does not belong to the Netris Public IP Pool", vlanRecord.getIpRange());
continue;
}
String gateway = vlanRecord.getVlanGateway();
String netmask = vlanRecord.getVlanNetmask();
String ipRange = vlanRecord.getIpRange();
SetupNetrisPublicRangeCommand cmd = createSetupPublicRangeCommand(zoneId, gateway, netmask, ipRange);
Answer answer = netrisResource.executeRequest(cmd);
boolean result = answer != null && answer.getResult();
if (!result) {
throw new CloudRuntimeException("Netris Public IP Range setup failed, please check the logs");
}
}
}
}
@Override
public List<BaseResponse> listNetrisProviders(Long zoneId) {
List<BaseResponse> netrisControllersResponseList = new ArrayList<>();

View File

@ -18,16 +18,29 @@ package org.apache.cloudstack.service;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.dc.VlanDetailsVO;
import com.cloud.dc.VlanVO;
import com.cloud.dc.dao.VlanDao;
import com.cloud.dc.dao.VlanDetailsDao;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.IpAddress;
import com.cloud.network.Network;
import com.cloud.network.Networks;
import com.cloud.network.SDNProviderNetworkRule;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.NetrisProviderDao;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.network.element.NetrisProviderVO;
import com.cloud.network.netris.NetrisService;
import com.cloud.network.vpc.Vpc;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressString;
import io.netris.model.NatPostBody;
import org.apache.cloudstack.agent.api.CreateNetrisVnetCommand;
import org.apache.cloudstack.agent.api.CreateNetrisVpcCommand;
@ -37,15 +50,21 @@ import org.apache.cloudstack.agent.api.DeleteNetrisVnetCommand;
import org.apache.cloudstack.agent.api.DeleteNetrisVpcCommand;
import org.apache.cloudstack.agent.api.NetrisAnswer;
import org.apache.cloudstack.agent.api.NetrisCommand;
import org.apache.cloudstack.agent.api.SetupNetrisPublicRangeCommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.resource.NetrisResourceObjectUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Collectors;
public class NetrisServiceImpl implements NetrisService, Configurable {
@ -57,6 +76,14 @@ public class NetrisServiceImpl implements NetrisService, Configurable {
private NetworkDao networkDao;
@Inject
private AgentManager agentMgr;
@Inject
private PhysicalNetworkDao physicalNetworkDao;
@Inject
private IPAddressDao ipAddressDao;
@Inject
private VlanDao vlanDao;
@Inject
private VlanDetailsDao vlanDetailsDao;
@Override
public String getConfigComponentName() {
@ -87,6 +114,75 @@ public class NetrisServiceImpl implements NetrisService, Configurable {
return (NetrisAnswer) answer;
}
/**
* Calculate the minimum CIDR subnet containing the IP range (using the library: <a href="https://github.com/seancfoley/IPAddress">IPAddress</a>)
* From: <a href="https://github.com/seancfoley/IPAddress/wiki/Code-Examples-3:-Subnetting-and-Other-Subnet-Operations#from-start-and-end-address-get-single-cidr-block-covering-both">Example</a>
* @param ipRange format: startIP-endIP
* @return the minimum CIDR containing the IP range
*/
protected String calculateSubnetCidrFromIpRange(String ipRange) {
if (StringUtils.isBlank(ipRange) || !ipRange.contains("-")) {
return null;
}
String[] rangeArray = ipRange.split("-");
String startIp = rangeArray[0];
String endIp = rangeArray[1];
IPAddress startIpAddress = new IPAddressString(startIp).getAddress();
IPAddress endIpAddress = new IPAddressString(endIp).getAddress();
return startIpAddress.coverWithPrefixBlock(endIpAddress).toPrefixLengthString();
}
/**
* Prepare the Netris Public Range to be used by CloudStack after the zone is created and the Netris provider is added
*/
public SetupNetrisPublicRangeCommand createSetupPublicRangeCommand(long zoneId, String gateway, String netmask, String ipRange) {
String superCidr = NetUtils.getCidrFromGatewayAndNetmask(gateway, netmask);
String subnetNatCidr = calculateSubnetCidrFromIpRange(ipRange);
return new SetupNetrisPublicRangeCommand(zoneId, superCidr, subnetNatCidr);
}
@Override
public boolean createIPAMAllocationsForZoneLevelPublicRanges(long zoneId) {
List<PhysicalNetworkVO> physicalNetworks = physicalNetworkDao.listByZoneAndTrafficType(zoneId, Networks.TrafficType.Public);
physicalNetworks = physicalNetworks.stream().filter(x -> x.getIsolationMethods().contains(Network.Provider.Netris.getName())).collect(Collectors.toList());
if (CollectionUtils.isEmpty(physicalNetworks)) {
return false;
}
for (PhysicalNetworkVO physicalNetwork : physicalNetworks) {
List<IPAddressVO> publicIps = ipAddressDao.listByPhysicalNetworkId(physicalNetwork.getId());
List<Long> vlanDbIds = publicIps.stream()
.filter(x -> !x.isForSystemVms())
.map(IPAddressVO::getVlanId)
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(vlanDbIds)) {
String msg = "Cannot find a public IP range VLAN range for the Netris Public traffic";
logger.error(msg);
throw new CloudRuntimeException(msg);
}
for (Long vlanDbId : vlanDbIds) {
VlanVO vlanRecord = vlanDao.findById(vlanDbId);
if (vlanRecord == null) {
logger.error("Cannot set up the Netris Public IP range as it cannot find the public range on database");
return false;
}
VlanDetailsVO vlanDetail = vlanDetailsDao.findDetail(vlanDbId, ApiConstants.NETRIS_DETAIL_KEY);
if (vlanDetail == null) {
logger.debug("Skipping the Public IP range {} creation on Netris as it does not belong to the Netris Public IP Pool", vlanRecord.getIpRange());
continue;
}
String gateway = vlanRecord.getVlanGateway();
String netmask = vlanRecord.getVlanNetmask();
String ipRange = vlanRecord.getIpRange();
SetupNetrisPublicRangeCommand cmd = createSetupPublicRangeCommand(zoneId, gateway, netmask, ipRange);
NetrisAnswer answer = sendNetrisCommand(cmd, zoneId);
if (!answer.getResult()) {
throw new CloudRuntimeException("Netris Public IP Range setup failed, please check the logs");
}
}
}
return true;
}
@Override
public boolean createVpcResource(long zoneId, long accountId, long domainId, Long vpcId, String vpcName, boolean sourceNatEnabled, String cidr, boolean isVpc) {
CreateNetrisVpcCommand cmd = new CreateNetrisVpcCommand(zoneId, accountId, domainId, vpcName, cidr, vpcId, isVpc);

View File

@ -19,9 +19,9 @@ package org.apache.cloudstack.service;
import org.junit.Assert;
import org.junit.Test;
public class NetrisProviderServiceImplTest {
public class NetrisServiceImplTest {
private NetrisProviderServiceImpl service = new NetrisProviderServiceImpl();
private NetrisServiceImpl service = new NetrisServiceImpl();
@Test
public void testCalculateSubnetCidrFromIpRange() {

View File

@ -47,6 +47,7 @@ import javax.naming.ConfigurationException;
import com.cloud.network.dao.NetrisProviderDao;
import com.cloud.network.element.NetrisProviderVO;
import com.cloud.network.netris.NetrisService;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.affinity.AffinityGroup;
import org.apache.cloudstack.affinity.AffinityGroupService;
@ -474,6 +475,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
NsxProviderDao nsxProviderDao;
@Inject
NetrisProviderDao netrisProviderDao;
@Inject
NetrisService netrisService;
// FIXME - why don't we have interface for DataCenterLinkLocalIpAddressDao?
@Inject
@ -4577,6 +4580,17 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
throw new InvalidParameterValueException("Unable to find zone by id " + zoneId);
}
// If external provider is provided, verify zone has that provider enabled
Provider provider = cmd.getProvider();
if (Objects.nonNull(provider)) {
boolean unsupported =
(Provider.Nsx == provider && nsxProviderDao.findByZoneId(zoneId) == null) ||
(Provider.Netris == provider && netrisProviderDao.findByZoneId(zoneId) == null);
if (unsupported) {
throw new InvalidParameterValueException(String.format("Cannot add public IP range as the zone does not support provider: %s", provider.getName()));
}
}
// verify that physical network exists
PhysicalNetworkVO pNtwk = null;
if (physicalNetworkId != null) {
@ -4748,6 +4762,9 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
}
});
if (provider == Provider.Netris) {
netrisService.createIPAMAllocationsForZoneLevelPublicRanges(zoneId);
}
messageBus.publish(_name, MESSAGE_CREATE_VLAN_IP_RANGE_EVENT, PublishScope.LOCAL, vlan);
return vlan;

View File

@ -244,6 +244,16 @@
<a-form-item name="endip" ref="endip" :label="$t('label.endip')" class="form__item">
<a-input v-model:value="form.endip" />
</a-form-item>
<a-form-item name="provider" ref="provider">
<template #label>
<tooltip-label :title="$t('label.provider')"/>
</template>
<a-select v-model:value="form.provider">
<a-select-option value=""></a-select-option>
<a-select-option value="NSX">{{ $t('label.nsx') }}</a-select-option>
<a-select-option value="Netris">{{ $t('label.netris') }}</a-select-option>
</a-select>
</a-form-item>
</div>
<div class="form__item" v-if="!basicGuestNetwork && form.iptype != 'ip6'">
<tooltip-label :title="$t('label.set.reservation')" :tooltip="$t('label.set.reservation.desc')" class="tooltip-label-wrapper"/>
@ -633,6 +643,7 @@ export default {
params.podid = values.podid
params.networkid = this.network.id
}
params.provider = values.provider
api('createVlanIpRange', params).then(() => {
this.$notification.success({
message: this.$t('message.success.add.iprange')