Skip to content

Commit 6eee828

Browse files
committed
Fix native VPN lifecycle ownership and endpoint reservation
Keep gateway provider identity through partial cleanup and recheck the IP allocation before release, connection operations and polling. Preserve the prior lifecycle and reserved-endpoint fixes. Strengthen validation, provider-selection and route/NAT payload tests; exercise customer-gateway updates through the public API and control retry waits in tests.
1 parent 8f1028a commit 6eee828

30 files changed

Lines changed: 3621 additions & 436 deletions

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ public interface NetworkModel {
219219

220220
boolean checkIpForService(IpAddress ip, Service service, Long networkId);
221221

222+
boolean isIpAddressReserved(IpAddress ip);
223+
222224
boolean providerSupportsCapability(Set<Provider> providers, Service service, Capability cap);
223225

224226
void checkCapabilityForProvider(Set<Provider> providers, Service service, Capability cap, String capValue);

‎api/src/main/java/com/cloud/network/element/NetworkElement.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ default boolean releaseIp(IpAddress ipAddress) {
9696
return true;
9797
}
9898

99+
/**
100+
* Whether this provider reserves an address for its own endpoint. Such an address must not
101+
* be released or reassigned by ordinary public-IP operations, including while creation or
102+
* cleanup is being retried. The owning provider remains responsible for releasing it.
103+
*/
104+
default boolean isIpAddressReserved(IpAddress ipAddress) {
105+
return false;
106+
}
107+
99108
/**
100109
* The network is being shutdown.
101110
* @param network

‎api/src/main/java/com/cloud/network/element/Site2SiteVpnServiceProvider.java‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
import com.cloud.utils.component.Adapter;
2727

2828
public interface Site2SiteVpnServiceProvider extends Adapter {
29+
// Gateway-scoped identity survives release and reuse of its public IP allocation.
30+
String VPN_GATEWAY_PROVIDER_DETAIL = "vpnProvider";
31+
2932
default void validateSite2SiteVpnCustomerGateway(Site2SiteCustomerGateway customerGateway) {
3033
}
3134

@@ -64,9 +67,9 @@ default void releaseVpnGatewayIp(Site2SiteVpnGateway gateway) {
6467
}
6568

6669
/**
67-
* Identifies a gateway previously owned by this provider. This is used during teardown when
68-
* an offering has been edited since the gateway was created and the current service map no
69-
* longer advertises the provider.
70+
* Identifies the gateway resources currently owned by this provider, independent of the
71+
* current offering. A recorded gateway provider retains teardown ownership after its public
72+
* IP has been released; this live claim must not adopt a later allocation of the same IP.
7073
*/
7174
default boolean ownsVpnGateway(Site2SiteVpnGateway gateway) {
7275
return false;

‎plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/resource/NsxResource.java‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -274,12 +274,14 @@ private Answer executeRequest(CreateOrUpdateNsxTier1NatRuleCommand cmd) {
274274
String translatedIpAddress = cmd.getTranslatedIpAddress();
275275
String natRuleId = cmd.getNatRuleId();
276276
String natId = "USER";
277-
try {
278-
nsxApiClient.createTier1NatRule(tier1GatewayName, natId, natRuleId, action, translatedIpAddress);
279-
} catch (CloudRuntimeException e) {
280-
String msg = String.format("Error creating the NAT rule with ID %s on Tier1 Gateway %s: %s", natRuleId, tier1GatewayName, e.getMessage());
281-
logger.error(msg, e);
282-
return new NsxAnswer(cmd, e);
277+
synchronized (getVpnTier1Lock(tier1GatewayName)) {
278+
try {
279+
nsxApiClient.createTier1NatRule(tier1GatewayName, natId, natRuleId, action, translatedIpAddress);
280+
} catch (CloudRuntimeException e) {
281+
String msg = String.format("Error creating the NAT rule with ID %s on Tier1 Gateway %s: %s", natRuleId, tier1GatewayName, e.getMessage());
282+
logger.error(msg, e);
283+
return new NsxAnswer(cmd, e);
284+
}
283285
}
284286
return new NsxAnswer(cmd, true, "");
285287
}
@@ -532,7 +534,11 @@ private NsxAnswer executeRequest(CreateNsxVpnGatewayCommand cmd) {
532534
} catch (Exception e) {
533535
logger.error(String.format("Failed to check the existing NSX VPN service on tier-1 gateway %s before creation: %s",
534536
tier1GatewayName, e.getMessage()));
535-
return new NsxAnswer(cmd, new CloudRuntimeException(e.getMessage()));
537+
NsxAnswer answer = new NsxAnswer(cmd, new CloudRuntimeException(e.getMessage()));
538+
// A retry may own an endpoint left by an earlier ambiguous failure. A failed
539+
// read cannot prove that endpoint is gone. Fresh attempts have not mutated NSX.
540+
answer.setEndpointMayBeInUse(cmd.isReconcileExistingService());
541+
return answer;
536542
}
537543
if (vpnServiceExisted && !cmd.isReconcileExistingService()) {
538544
NsxAnswer answer = new NsxAnswer(cmd, new CloudRuntimeException(String.format(

‎plugins/network-elements/nsx/src/main/java/org/apache/cloudstack/service/NsxApiClient.java‎

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -300,10 +300,16 @@ public boolean isNsxControllerActive() {
300300
public void createTier1NatRule(String tier1GatewayName, String natId, String natRuleId,
301301
String action, String translatedIp) {
302302
NatRules natRulesService = (NatRules) nsxService.apply(NatRules.class);
303-
PolicyNatRule natPolicy = new PolicyNatRule.Builder()
304-
.setAction(action)
305-
.setTranslatedNetwork(translatedIp)
306-
.build();
303+
PolicyNatRule natPolicy;
304+
try {
305+
natPolicy = natRulesService.get(tier1GatewayName, natId, natRuleId);
306+
} catch (NotFound e) {
307+
natPolicy = new PolicyNatRule.Builder().build();
308+
}
309+
// Policy PATCH replaces omitted fields unless partial PATCH is explicitly enabled.
310+
// Preserve VPN ordering, ownership tags and the rest of an existing rule on IP updates.
311+
natPolicy.setAction(action);
312+
natPolicy.setTranslatedNetwork(translatedIp);
307313
natRulesService.patch(tier1GatewayName, natId, natRuleId, natPolicy);
308314
}
309315

@@ -353,8 +359,9 @@ private void demoteCatchAllSourceNatRule(String tier1GatewayName) {
353359
}
354360
logger.debug("Moving CloudStack source NAT rule {} on tier-1 gateway {} behind VPN no-SNAT rules",
355361
ruleId, tier1GatewayName);
356-
natRulesService.patch(tier1GatewayName, NatId.USER.name(), ruleId,
357-
copyNatRule(natRule, CATCH_ALL_NAT_SEQUENCE_NUMBER, tags));
362+
natRule.setSequenceNumber(CATCH_ALL_NAT_SEQUENCE_NUMBER);
363+
natRule.setTags(tags);
364+
natRulesService.patch(tier1GatewayName, NatId.USER.name(), ruleId, natRule);
358365
} catch (NotFound e) {
359366
logger.debug("CloudStack source NAT rule is absent on tier-1 gateway {}; no VPN NAT ordering change is required",
360367
tier1GatewayName);
@@ -385,8 +392,9 @@ void restoreSourceNatRuleSequence(String tier1GatewayName) {
385392
originalSequenceTag.getTag(), tier1GatewayName), e);
386393
}
387394
tags.remove(originalSequenceTag);
388-
natRulesService.patch(tier1GatewayName, NatId.USER.name(), ruleId,
389-
copyNatRule(natRule, originalSequence, tags));
395+
natRule.setSequenceNumber(originalSequence);
396+
natRule.setTags(tags);
397+
natRulesService.patch(tier1GatewayName, NatId.USER.name(), ruleId, natRule);
390398
} catch (NotFound e) {
391399
logger.debug("CloudStack source NAT rule is absent on tier-1 gateway {}; no sequence restoration is required",
392400
tier1GatewayName);
@@ -413,27 +421,6 @@ private Tag findOriginalSnatSequenceTag(List<Tag> tags) {
413421
.orElse(null);
414422
}
415423

416-
private PolicyNatRule copyNatRule(PolicyNatRule natRule, long sequenceNumber, List<Tag> tags) {
417-
return new PolicyNatRule.Builder()
418-
.setId(natRule.getId())
419-
.setDisplayName(natRule.getDisplayName())
420-
.setDescription(natRule.getDescription())
421-
.setAction(natRule.getAction())
422-
.setTranslatedNetwork(natRule.getTranslatedNetwork())
423-
.setTranslatedPorts(natRule.getTranslatedPorts())
424-
.setSourceNetwork(natRule.getSourceNetwork())
425-
.setDestinationNetwork(natRule.getDestinationNetwork())
426-
.setService(natRule.getService())
427-
.setScope(natRule.getScope())
428-
.setFirewallMatch(natRule.getFirewallMatch())
429-
.setPolicyBasedVpnMode(natRule.getPolicyBasedVpnMode())
430-
.setLogging(natRule.getLogging())
431-
.setEnabled(natRule.getEnabled())
432-
.setTags(tags)
433-
.setSequenceNumber(sequenceNumber)
434-
.build();
435-
}
436-
437424
private boolean isCatchAllSourceNatRule(PolicyNatRule natRule) {
438425
return NatAction.SNAT.name().equals(natRule.getAction())
439426
&& isAnyNatMatch(natRule.getSourceNetwork())
@@ -1515,10 +1502,9 @@ private void ensureTier1IpsecLocalEndpointAdvertisement(String tier1GatewayName)
15151502
}
15161503
updatedTypes.add(RouteAdvertisementType.TIER1_IPSEC_LOCAL_ENDPOINT.name());
15171504
Tier1s tier1service = (Tier1s) nsxService.apply(Tier1s.class);
1518-
Tier1 tier1Update = new Tier1.Builder()
1519-
.setRouteAdvertisementTypes(updatedTypes)
1520-
.build();
1521-
tier1service.patch(tier1GatewayName, tier1Update);
1505+
// Retain the Tier-0 attachment and all other router settings under full PATCH semantics.
1506+
tier1.setRouteAdvertisementTypes(updatedTypes);
1507+
tier1service.patch(tier1GatewayName, tier1);
15221508
}
15231509

15241510
public void deleteVpnService(String tier1GatewayName) {
@@ -1560,7 +1546,7 @@ private <T> T retryWhileMarkedForDeletion(long connectionId, Supplier<T> operati
15601546
logger.info("A VPN object for connection {} is still being purged by NSX, retrying in {}s (attempt {}/{})",
15611547
connectionId, VPN_MARKED_FOR_DELETION_RETRY_INTERVAL_SECS, attempt, VPN_MARKED_FOR_DELETION_RETRIES);
15621548
try {
1563-
Thread.sleep(VPN_MARKED_FOR_DELETION_RETRY_INTERVAL_SECS * 1000L);
1549+
waitBeforeVpnRetry(VPN_MARKED_FOR_DELETION_RETRY_INTERVAL_SECS * 1000L);
15641550
} catch (InterruptedException ie) {
15651551
Thread.currentThread().interrupt();
15661552
throw e;
@@ -1570,6 +1556,10 @@ private <T> T retryWhileMarkedForDeletion(long connectionId, Supplier<T> operati
15701556
throw lastFailure;
15711557
}
15721558

1559+
void waitBeforeVpnRetry(long milliseconds) throws InterruptedException {
1560+
Thread.sleep(milliseconds);
1561+
}
1562+
15731563
private boolean isMarkedForDeletionError(CloudRuntimeException e) {
15741564
return e.getMessage() != null && e.getMessage().contains("marked for deletion");
15751565
}

0 commit comments

Comments
 (0)