@@ -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