Copilot commented on code in PR #13766:
URL: https://github.com/apache/cloudstack/pull/13766#discussion_r4515782566
##########
plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxElement.java:
##########
@@ -941,4 +988,341 @@ public List<Class<?>> getCommands() {
public boolean updateVpcSourceNatIp(Vpc vpc, IpAddress address) {
return nsxService.updateVpcSourceNatIp(vpc, address);
}
+
+ protected boolean isVpnProvidedByNsx(Vpc vpc) {
+ if (Objects.isNull(vpc)) {
+ return false;
+ }
+ return vpcManager.isProviderSupportServiceInVpc(vpc.getId(),
Network.Service.Vpn, Network.Provider.Nsx);
+ }
+
+ protected boolean isVpnProvidedByNsx(Vpc vpc, Site2SiteVpnGateway gateway)
{
+ return vpc != null && ownsVpnGateway(gateway);
+ }
+
+ private void validateVpnGatewayOwnedByNsx(Vpc vpc, Site2SiteVpnGateway
gateway) {
+ if (!isVpnProvidedByNsx(vpc, gateway)) {
+ throw new CloudRuntimeException(String.format(
+ "NSX does not own Site-to-Site VPN gateway %s of VPC %s",
gateway.getId(), vpc.getName()));
+ }
+ }
+
+ @Override
+ public IpAddress acquireVpnGatewayIp(Vpc vpc, IpAddress requestedIp) {
+ if (!isVpnProvidedByNsx(vpc)) {
+ return null;
+ }
+ IPAddressVO ip;
+ boolean autoAcquired = false;
+ boolean reconcileExistingService = false;
+ UserIpAddressDetailVO ownershipMarker = null;
+ if (Objects.nonNull(requestedIp)) {
+ ip = validateRequestedVpnGatewayIp(vpc, requestedIp);
+ ownershipMarker = userIpAddressDetailsDao.findDetail(ip.getId(),
NSX_VPN_GATEWAY_IP_DETAIL);
+ if (ownershipMarker != null) {
+ autoAcquired = isAutoAcquiredVpnGatewayIp(ownershipMarker, ip,
vpc);
+ reconcileExistingService = true;
+ }
+ } else {
+ Pair<IPAddressVO, UserIpAddressDetailVO> retainedIp =
findRetainedVpnGatewayIp(vpc);
+ if (retainedIp != null) {
+ ip = retainedIp.first();
+ ownershipMarker = retainedIp.second();
+ autoAcquired = isAutoAcquiredVpnGatewayIp(ownershipMarker, ip,
vpc);
+ reconcileExistingService = true;
+ } else {
+ ip = allocateVpnGatewayIp(vpc);
+ autoAcquired = true;
+ }
+ }
+ boolean requestedIpOwnershipMarkerAdded = false;
+ if (!autoAcquired && ownershipMarker == null) {
+ try {
+ userIpAddressDetailsDao.addDetail(ip.getId(),
NSX_VPN_GATEWAY_IP_DETAIL, "false", false);
+ requestedIpOwnershipMarkerAdded = true;
+ } catch (Exception e) {
+ throw new CloudRuntimeException(String.format(
+ "Failed to record NSX VPN ownership for requested IP
%s of VPC %s",
+ ip.getAddress(), vpc.getName()), e);
+ }
+ }
+ boolean endpointMayBeInUse = true;
+ try {
+ NsxVpnGatewayResult result = nsxService.createVpnGateway(vpc,
ip.getAddress().addr(), reconcileExistingService);
+ endpointMayBeInUse = result.isEndpointMayBeInUse();
+ if (!result.isSuccessful()) {
+ throw new CloudRuntimeException(String.format("The NSX VPN
gateway service for VPC %s was not created: the provider returned an
unsuccessful answer",
+ vpc.getName()));
+ }
+ } catch (Exception e) {
+ if (autoAcquired && !endpointMayBeInUse) {
+ try {
+ releaseAutoAcquiredVpnGatewayIp(ip);
+ } catch (Exception cleanupException) {
+ logger.warn("Failed to release the auto-acquired VPN
gateway IP {} of VPC {} after creation failed: {}",
+ ip.getAddress(), vpc.getName(),
cleanupException.getMessage());
+ }
+ } else if (autoAcquired) {
+ logger.warn("Retaining auto-acquired VPN gateway IP {} for VPC
{} because the NSX endpoint may still be using it",
+ ip.getAddress(), vpc.getName());
+ } else if (!endpointMayBeInUse && requestedIpOwnershipMarkerAdded)
{
+ try {
+ userIpAddressDetailsDao.removeDetail(ip.getId(),
NSX_VPN_GATEWAY_IP_DETAIL);
+ } catch (Exception cleanupException) {
+ logger.warn("Failed to remove the NSX VPN ownership marker
from requested IP {} of VPC {} after gateway creation failed: {}",
+ ip.getAddress(), vpc.getName(),
cleanupException.getMessage());
+ }
+ }
+ throw new CloudRuntimeException(String.format("Failed to create
the NSX VPN gateway for VPC %s: %s",
+ vpc.getName(), e.getMessage()), e);
+ }
+ return ip;
+ }
+
+ private Pair<IPAddressVO, UserIpAddressDetailVO>
findRetainedVpnGatewayIp(Vpc vpc) {
+ Pair<IPAddressVO, UserIpAddressDetailVO> retainedIp = null;
+ List<IPAddressVO> nonSourceNatIps =
ipAddressDao.listByAssociatedVpc(vpc.getId(), false);
+ if (CollectionUtils.isNullOrEmpty(nonSourceNatIps)) {
+ return null;
+ }
+ for (IPAddressVO candidate : nonSourceNatIps) {
+ UserIpAddressDetailVO ownershipMarker =
userIpAddressDetailsDao.findDetail(
+ candidate.getId(), NSX_VPN_GATEWAY_IP_DETAIL);
+ if (ownershipMarker == null) {
+ continue;
+ }
+ if (retainedIp != null) {
+ throw new CloudRuntimeException(String.format(
+ "VPC %s has more than one IP retained for an NSX VPN
gateway; refusing ambiguous recovery",
+ vpc.getName()));
+ }
+ retainedIp = new Pair<>(validateRequestedVpnGatewayIp(vpc,
candidate), ownershipMarker);
+ }
+ return retainedIp;
+ }
+
+ private boolean isAutoAcquiredVpnGatewayIp(UserIpAddressDetailVO
ownershipMarker, IPAddressVO ip, Vpc vpc) {
+ if ("true".equalsIgnoreCase(ownershipMarker.getValue())) {
+ return true;
+ }
+ if ("false".equalsIgnoreCase(ownershipMarker.getValue())) {
+ return false;
+ }
+ throw new CloudRuntimeException(String.format(
+ "IP %s retained for the NSX VPN gateway of VPC %s has invalid
ownership state '%s'",
+ ip.getAddress(), vpc == null ? ip.getVpcId() : vpc.getName(),
ownershipMarker.getValue()));
+ }
+
+ private IPAddressVO validateRequestedVpnGatewayIp(Vpc vpc, IpAddress
requestedIp) {
+ IPAddressVO ip = ipAddressDao.findById(requestedIp.getId());
+ if (Objects.isNull(ip) || !Objects.equals(ip.getVpcId(), vpc.getId())
+ || !ip.readyToUse() || ip.getRemoved() != null ||
ip.getAddress() == null) {
+ throw new InvalidParameterValueException(String.format(
+ "The requested IP id %s is not an allocated, active IP
associated to the VPC %s",
+ requestedIp.getId(), vpc.getName()));
+ }
+ if (ip.isSourceNat() || ip.isForSystemVms()) {
+ throw new InvalidParameterValueException(String.format(
+ "The requested IP %s cannot be used as the VPN gateway IP
as it is a source NAT or system IP", ip.getAddress().addr()));
+ }
+ if (ip.isOneToOneNat() ||
!firewallRulesDao.listByIpAndNotRevoked(ip.getId()).isEmpty()
+ ||
!portForwardingRulesDao.listByIpAndNotRevoked(ip.getId()).isEmpty()
+ || !loadBalancerDao.listByIpAddress(ip.getId()).isEmpty()) {
+ throw new InvalidParameterValueException(String.format(
+ "The requested IP %s cannot be used as the VPN gateway IP
as it is already in use by static NAT or network rules",
ip.getAddress().addr()));
+ }
+ return ip;
+ }
+
+ private IPAddressVO allocateVpnGatewayIp(Vpc vpc) {
+ Account owner = accountMgr.getAccount(vpc.getAccountId());
+ DataCenterVO zone = dataCenterDao.findById(vpc.getZoneId());
+ IpAddress allocatedIp = null;
+ try {
+ allocatedIp = ipAddressManager.allocateIp(owner, false,
CallContext.current().getCallingAccount(),
+ CallContext.current().getCallingUser(), zone, null, null);
+ vpcService.associateIPToVpc(allocatedIp.getId(), vpc.getId());
+ userIpAddressDetailsDao.addDetail(allocatedIp.getId(),
NSX_VPN_GATEWAY_IP_DETAIL, "true", false);
+ IPAddressVO ip = ipAddressDao.findById(allocatedIp.getId());
+ if (ip == null) {
+ throw new CloudRuntimeException(String.format("The allocated
VPN gateway IP %s could not be loaded after association",
+ allocatedIp.getId()));
+ }
+ if (ip.isSourceNat()) {
+ throw new CloudRuntimeException(String.format(
+ "The allocated IP %s became a source NAT IP when it
was associated to VPC %s; it cannot be used as a dedicated VPN endpoint",
+ ip.getAddress(), vpc.getName()));
+ }
+ return ip;
+ } catch (Exception e) {
+ // do not leak the IP when associating or tagging it fails after
allocation succeeded
+ if (Objects.nonNull(allocatedIp)) {
+ IPAddressVO ipToRelease =
ipAddressDao.findById(allocatedIp.getId());
+ if (Objects.nonNull(ipToRelease)) {
+ try {
+ releaseAutoAcquiredVpnGatewayIp(ipToRelease);
+ } catch (Exception releaseException) {
+ logger.warn("Failed to release the IP {} allocated for
the VPN gateway of VPC {}: {}",
+ ipToRelease.getAddress().addr(),
vpc.getName(), releaseException.getMessage());
+ }
+ } else {
+ try {
+
ipAddressManager.disassociatePublicIpAddress(allocatedIp,
CallContext.current().getCallingUserId(),
+ CallContext.current().getCallingAccount());
+ } catch (Exception releaseException) {
+ logger.warn("Failed to release allocated VPN gateway
IP {} of VPC {} after its database row disappeared: {}",
+ allocatedIp.getId(), vpc.getName(),
releaseException.getMessage());
+ }
+ }
+ }
+ throw new CloudRuntimeException(String.format("Failed to acquire
an IP for the VPN gateway of VPC %s: %s",
+ vpc.getName(), e.getMessage()), e);
+ }
+ }
+
+ private void releaseAutoAcquiredVpnGatewayIp(IPAddressVO ip) {
+ boolean disassociated =
ipAddressManager.disassociatePublicIpAddress(ip,
CallContext.current().getCallingUserId(),
+ CallContext.current().getCallingAccount());
+ if (!disassociated) {
+ throw new CloudRuntimeException(String.format("Failed to
disassociate auto-acquired VPN gateway IP %s", ip.getAddress()));
+ }
+ userIpAddressDetailsDao.removeDetail(ip.getId(),
NSX_VPN_GATEWAY_IP_DETAIL);
Review Comment:
`disassociatePublicIpAddress` and `removeDetail` are executed sequentially
without handling failures of `removeDetail`. If `removeDetail` throws after a
successful disassociation, the ownership marker can remain stale and may cause
future ownership checks/poller gating to incorrectly treat the IP as still
NSX-owned. Consider wrapping marker cleanup in a try/catch (logging +
compensating action if appropriate) or otherwise ensuring the marker state
cannot remain inconsistent with the IP’s actual association state.
##########
plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxServiceImpl.java:
##########
@@ -196,6 +298,192 @@ public boolean deleteFirewallRules(Network network,
List<NsxNetworkRule> netRule
return result.getResult();
}
+ @Override
+ public NsxVpnGatewayResult createVpnGateway(Vpc vpc, String
localEndpointIp) {
+ return createVpnGateway(vpc, localEndpointIp, false);
+ }
+
+ @Override
+ public NsxVpnGatewayResult createVpnGateway(Vpc vpc, String
localEndpointIp, boolean reconcileExistingService) {
+ CreateNsxVpnGatewayCommand createNsxVpnGatewayCommand = new
CreateNsxVpnGatewayCommand(vpc.getDomainId(),
+ vpc.getAccountId(), vpc.getZoneId(), vpc.getId(),
vpc.getName(), localEndpointIp,
+ reconcileExistingService);
+ NsxAnswer result =
nsxControllerUtils.sendNsxCommandForResult(createNsxVpnGatewayCommand,
vpc.getZoneId());
+ return new NsxVpnGatewayResult(result.getResult(),
result.isEndpointMayBeInUse());
+ }
+
+ public boolean deleteVpnGateway(Vpc vpc) {
+ DeleteNsxVpnGatewayCommand deleteNsxVpnGatewayCommand = new
DeleteNsxVpnGatewayCommand(vpc.getDomainId(),
+ vpc.getAccountId(), vpc.getZoneId(), vpc.getId(),
vpc.getName());
+ NsxAnswer result =
nsxControllerUtils.sendNsxCommand(deleteNsxVpnGatewayCommand, vpc.getZoneId());
+ return result.getResult();
+ }
+
+ public boolean createVpnConnection(Vpc vpc, long connectionId, String
peerAddress, String psk,
+ String ikePolicy, String espPolicy,
Long ikeLifetime, Long espLifetime,
+ boolean dpdEnabled, String ikeVersion,
boolean passive, List<String> peerCidrs,
+ String vtiLocalIp, String vtiPeerIp,
int vtiPrefixLength, String localEndpointIp) {
+ CreateNsxVpnConnectionCommand createNsxVpnConnectionCommand = new
CreateNsxVpnConnectionCommand(vpc.getDomainId(),
+ vpc.getAccountId(), vpc.getZoneId(), vpc.getId(),
vpc.getName(), connectionId, peerAddress, psk,
+ ikePolicy, espPolicy, ikeLifetime, espLifetime, dpdEnabled,
ikeVersion, passive, peerCidrs,
+ vtiLocalIp, vtiPeerIp, vtiPrefixLength, vpc.getCidr(),
localEndpointIp);
+ NsxAnswer result =
nsxControllerUtils.sendNsxCommand(createNsxVpnConnectionCommand,
vpc.getZoneId());
+ return result.getResult();
+ }
+
+ public boolean deleteVpnConnection(Vpc vpc, long connectionId) {
+ DeleteNsxVpnConnectionCommand deleteNsxVpnConnectionCommand = new
DeleteNsxVpnConnectionCommand(vpc.getDomainId(),
+ vpc.getAccountId(), vpc.getZoneId(), vpc.getId(),
vpc.getName(), connectionId);
+ NsxAnswer result =
nsxControllerUtils.sendNsxCommand(deleteNsxVpnConnectionCommand,
vpc.getZoneId());
+ return result.getResult();
+ }
+
+ public boolean updateVpnConnectionState(Vpc vpc, long connectionId,
boolean enabled) {
+ UpdateNsxVpnConnectionStateCommand command = new
UpdateNsxVpnConnectionStateCommand(vpc.getDomainId(),
+ vpc.getAccountId(), vpc.getZoneId(), vpc.getId(),
vpc.getName(), connectionId, enabled);
+ NsxAnswer result = nsxControllerUtils.sendNsxCommand(command,
vpc.getZoneId());
+ return result.getResult();
+ }
+
+ public String getVpnConnectionStatus(Vpc vpc, long connectionId) {
+ GetNsxVpnSessionStatusCommand getNsxVpnSessionStatusCommand = new
GetNsxVpnSessionStatusCommand(vpc.getDomainId(),
+ vpc.getAccountId(), vpc.getZoneId(), vpc.getId(),
vpc.getName(), connectionId);
+ NsxAnswer result =
nsxControllerUtils.sendNsxCommand(getNsxVpnSessionStatusCommand,
vpc.getZoneId());
+ return result.getDetails();
+ }
+
+ /**
+ * The global lock makes the scan a cluster-wide singleton. The connection
row lock in
+ * transitionVpnConnectionState separately protects each state transition
from a concurrent
+ * user operation that starts, stops, resets, or deletes the connection.
+ */
+ protected class VpnStatusPollTask extends ManagedContextRunnable {
+ @Override
+ protected void runInContext() {
+ GlobalLock scanLock =
GlobalLock.getInternLock(VPN_STATUS_POLL_LOCK_NAME);
+ try {
+ if (scanLock.lock(VPN_STATUS_POLL_LOCK_TIMEOUT)) {
+ try {
+ pollVpnConnectionStatuses();
+ } finally {
+ scanLock.unlock();
+ }
+ }
+ } catch (Exception e) {
+ logger.warn("Failed to poll the status of the NSX Site-to-Site
VPN connections: {}", e.getMessage(), e);
+ } finally {
+ scanLock.releaseRef();
+ }
+ }
+ }
+
+ private void pollVpnConnectionStatuses() {
+ Set<Long> polledConnectionIds = new HashSet<>();
+ List<Site2SiteVpnConnectionVO> connections =
site2SiteVpnConnectionDao.listByStates(
+ VPN_POLLED_STATES.toArray(new
Site2SiteVpnConnection.State[0]));
+ for (Site2SiteVpnConnectionVO connection : connections) {
+ Site2SiteVpnGatewayVO vpnGateway =
site2SiteVpnGatewayDao.findById(connection.getVpnGatewayId());
+ if (vpnGateway == null) {
+ continue;
+ }
+ VpcVO vpc = vpcDao.findById(vpnGateway.getVpcId());
+ if (vpc == null || !isVpnProvidedByNsx(vpc, vpnGateway)) {
+ continue;
+ }
+ polledConnectionIds.add(connection.getId());
+ pollVpnConnectionStatus(connection, vpc);
+ }
+ // Drop the failure counters of connections that were deleted or left
the polled states.
+ vpnStatusPollFailures.keySet().retainAll(polledConnectionIds);
+ }
+
+ private boolean isVpnProvidedByNsx(Vpc vpc, Site2SiteVpnGatewayVO
vpnGateway) {
+ return vpc != null
+ && userIpAddressDetailsDao != null
+ && vpnGateway != null
+ && userIpAddressDetailsDao.findDetail(vpnGateway.getAddrId(),
NsxElement.NSX_VPN_GATEWAY_IP_DETAIL) != null;
Review Comment:
`NsxServiceImpl` depends on `NsxElement.NSX_VPN_GATEWAY_IP_DETAIL`, which is
currently a `protected static final` implementation detail of another class.
This creates brittle coupling across components in the same package; consider
moving this key to a dedicated shared constants holder (or making it a clearly
documented public constant in an appropriate shared module) to prevent
accidental breakage during refactors.
##########
plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/utils/NsxControllerUtils.java:
##########
@@ -51,15 +51,30 @@ public static String
getNsxDistributedFirewallPolicyRuleId(String segmentName, l
}
public NsxAnswer sendNsxCommand(NsxCommand cmd, long zoneId) throws
IllegalArgumentException {
+ NsxAnswer answer = sendNsxCommandForResult(cmd, zoneId);
+ if (!answer.getResult()) {
+ logger.error("NSX command {} failed in zone {}",
cmd.getClass().getSimpleName(), zoneId);
+ throw new InvalidParameterValueException("Failed API call to NSX
controller");
+ }
+ return answer;
+ }
Review Comment:
The thrown error message is generic and discards potentially actionable
context already available (e.g., `answer.getDetails()`, command name). Consider
including sanitized details (and/or the command name/zone) in the exception
message so operators can troubleshoot without needing to reproduce with debug
logs.
##########
plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxServiceImpl.java:
##########
@@ -196,6 +298,192 @@ public boolean deleteFirewallRules(Network network,
List<NsxNetworkRule> netRule
return result.getResult();
}
+ @Override
+ public NsxVpnGatewayResult createVpnGateway(Vpc vpc, String
localEndpointIp) {
+ return createVpnGateway(vpc, localEndpointIp, false);
+ }
+
+ @Override
+ public NsxVpnGatewayResult createVpnGateway(Vpc vpc, String
localEndpointIp, boolean reconcileExistingService) {
+ CreateNsxVpnGatewayCommand createNsxVpnGatewayCommand = new
CreateNsxVpnGatewayCommand(vpc.getDomainId(),
+ vpc.getAccountId(), vpc.getZoneId(), vpc.getId(),
vpc.getName(), localEndpointIp,
+ reconcileExistingService);
+ NsxAnswer result =
nsxControllerUtils.sendNsxCommandForResult(createNsxVpnGatewayCommand,
vpc.getZoneId());
+ return new NsxVpnGatewayResult(result.getResult(),
result.isEndpointMayBeInUse());
+ }
+
+ public boolean deleteVpnGateway(Vpc vpc) {
+ DeleteNsxVpnGatewayCommand deleteNsxVpnGatewayCommand = new
DeleteNsxVpnGatewayCommand(vpc.getDomainId(),
+ vpc.getAccountId(), vpc.getZoneId(), vpc.getId(),
vpc.getName());
+ NsxAnswer result =
nsxControllerUtils.sendNsxCommand(deleteNsxVpnGatewayCommand, vpc.getZoneId());
+ return result.getResult();
+ }
+
+ public boolean createVpnConnection(Vpc vpc, long connectionId, String
peerAddress, String psk,
+ String ikePolicy, String espPolicy,
Long ikeLifetime, Long espLifetime,
+ boolean dpdEnabled, String ikeVersion,
boolean passive, List<String> peerCidrs,
+ String vtiLocalIp, String vtiPeerIp,
int vtiPrefixLength, String localEndpointIp) {
+ CreateNsxVpnConnectionCommand createNsxVpnConnectionCommand = new
CreateNsxVpnConnectionCommand(vpc.getDomainId(),
+ vpc.getAccountId(), vpc.getZoneId(), vpc.getId(),
vpc.getName(), connectionId, peerAddress, psk,
+ ikePolicy, espPolicy, ikeLifetime, espLifetime, dpdEnabled,
ikeVersion, passive, peerCidrs,
+ vtiLocalIp, vtiPeerIp, vtiPrefixLength, vpc.getCidr(),
localEndpointIp);
+ NsxAnswer result =
nsxControllerUtils.sendNsxCommand(createNsxVpnConnectionCommand,
vpc.getZoneId());
+ return result.getResult();
+ }
Review Comment:
These methods implement the `NsxService` interface but are missing
`@Override` annotations (same applies to the other newly added VPN methods in
this class). Adding `@Override` helps the compiler catch signature drift during
future refactors and improves readability.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]